@shiyuphan/follow-me 0.2.0 → 0.4.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 +44 -55
- package/dist/src/cli.js +740 -194
- package/dist/src/comments.js +109 -6
- package/dist/src/diff.js +11 -0
- package/dist/src/evolve.js +123 -0
- package/dist/src/git.js +12 -0
- package/dist/src/host.js +39 -8
- package/dist/src/install.js +21 -0
- package/dist/src/plan.js +1 -1
- package/dist/src/prompt.js +384 -51
- package/dist/src/server.js +195 -67
- package/dist/src/session.js +53 -12
- package/dist/ui/dist/assets/abnfDiagram-WEYOIA3C-byISxePP.js +1 -0
- package/dist/ui/dist/assets/arc-CBzsAsP2.js +1 -0
- package/dist/ui/dist/assets/architecture-WOLXFQ4H-0OSkIwdB.js +1 -0
- package/dist/ui/dist/assets/architectureDiagram-ZHVYKT53-ixHybNqO.js +36 -0
- package/dist/ui/dist/assets/array-BifhSqXX.js +1 -0
- package/dist/ui/dist/assets/blockDiagram-RJAE7QA3-1F8XL_Ou.js +140 -0
- package/dist/ui/dist/assets/c4Diagram-XDPPPE54-D9PFBXnc.js +38 -0
- package/dist/ui/dist/assets/channel-B-fwsHf1.js +1 -0
- package/dist/ui/dist/assets/chunk-2E7WHVMD-zZdNfMkj.js +88 -0
- package/dist/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/ui/dist/assets/chunk-4EMNKR2T-CTKoxSSj.js +1 -0
- package/dist/ui/dist/assets/chunk-55XCU76K-ifFnDlcS.js +1 -0
- package/dist/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/ui/dist/assets/chunk-7S6XSTFO-BCMVdi_1.js +2 -0
- package/dist/ui/dist/assets/chunk-AQDTSKZB-DC-5Whju.js +72 -0
- package/dist/ui/dist/assets/chunk-DSNLSHJZ-DASbe_gb.js +127 -0
- package/dist/ui/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/dist/ui/dist/assets/chunk-HAGFOTED-CArytDjj.js +62 -0
- package/dist/ui/dist/assets/chunk-HNE4Q36P-Dnl-Wtud.js +1 -0
- package/dist/ui/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/dist/ui/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/ui/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/dist/ui/dist/assets/chunk-NQOKRVIF-wvSLaeaK.js +213 -0
- package/dist/ui/dist/assets/chunk-UHW7PFBP-CckaOGiQ.js +1 -0
- package/dist/ui/dist/assets/chunk-VQ5W62QI-kQBMDJ7V.js +1 -0
- package/dist/ui/dist/assets/chunk-WQQL4CDK-CMg6bbLU.js +10 -0
- package/dist/ui/dist/assets/chunk-XXDRQBXY-CYlFr19K.js +1 -0
- package/dist/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/ui/dist/assets/chunk-YZO67HFG-CqkG1vVk.js +1 -0
- package/dist/ui/dist/assets/chunk-ZAUX2LTX-BUNEdx-e.js +1 -0
- package/dist/ui/dist/assets/chunk-ZCHMQSOR-CFTbvcrm.js +2 -0
- package/dist/ui/dist/assets/classDiagram-v2-4MYWR3UA-t2OdJ0LA.js +217 -0
- package/dist/ui/dist/assets/cose-bilkent-JH36ORCC-FGm030gc.js +1 -0
- package/dist/ui/dist/assets/cynefin-EF2NZ3EQ-BtU33Lsu.js +1 -0
- package/dist/ui/dist/assets/cynefinDiagram-WF24UDRP-DUr-I7hS.js +62 -0
- package/dist/ui/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/dist/ui/dist/assets/dagre-NK4GIZVX-BWTHwJKt.js +4 -0
- package/dist/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/ui/dist/assets/diagram-CBK3MWTC-PcHrsEBn.js +24 -0
- package/dist/ui/dist/assets/diagram-I5XIIV2D-CkGtKjYj.js +24 -0
- package/dist/ui/dist/assets/diagram-ODIL2UCW-B_HdBB1w.js +30 -0
- package/dist/ui/dist/assets/diagram-PFBEJUXE-DL9zayE6.js +3 -0
- package/dist/ui/dist/assets/diagram-RJA3I4VV-By5wl8nK.js +41 -0
- package/dist/ui/dist/assets/diagram-TYUNFSNR-ziWPVayL.js +200 -0
- package/dist/ui/dist/assets/dist-NDVqCM7k.js +1 -0
- package/dist/ui/dist/assets/ebnfDiagram-V4P3TTPT-DH4YGkPc.js +1 -0
- package/dist/ui/dist/assets/elk-QONTBCTQ-BL2RlCEv.js +27 -0
- package/dist/ui/dist/assets/erDiagram-KYQIGQAA-BKbTMaRp.js +99 -0
- package/dist/ui/dist/assets/eventmodeling-K75KTNOO-Dk5RCWc-.js +1 -0
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Dkjb3PCK.js +1 -0
- package/dist/ui/dist/assets/ganttDiagram-R7GPNTJY-3vM9gxeT.js +292 -0
- package/dist/ui/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/ui/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/ui/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/ui/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/ui/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/ui/dist/assets/gitGraph-VSP46ZUC-CXGFhz54.js +1 -0
- package/dist/ui/dist/assets/gitGraphDiagram-PDYL7XHY-Cl4fwUuA.js +106 -0
- package/dist/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/ui/dist/assets/index-B8TYcxF8.css +1 -0
- package/dist/ui/dist/assets/index-CBbx_pJk.js +134 -0
- package/dist/ui/dist/assets/info-OHQRW6UA-DfSsZ8ib.js +1 -0
- package/dist/ui/dist/assets/infoDiagram-MVDRKAX5-B_7tyrL5.js +2 -0
- package/dist/ui/dist/assets/init-C-OQMol4.js +1 -0
- package/dist/ui/dist/assets/ishikawaDiagram-HPEHV2GE-DUvHljpg.js +70 -0
- package/dist/ui/dist/assets/journeyDiagram-BOSHW7O7-BCgqzrb8.js +139 -0
- package/dist/ui/dist/assets/kanban-definition-FNSA4HC5-DOzZ3NmE.js +89 -0
- package/dist/ui/dist/assets/katex-B_JcIwqj.js +257 -0
- package/dist/ui/dist/assets/line-B-mPFVll.js +1 -0
- package/dist/ui/dist/assets/linear-k7o0jout.js +1 -0
- package/dist/ui/dist/assets/mermaid-parser.core-CAyLSQpH.js +7 -0
- package/dist/ui/dist/assets/mermaid.core-vfVMjgx2.js +44 -0
- package/dist/ui/dist/assets/mindmap-definition-6OY57UG3-B6y-B_r4.js +98 -0
- package/dist/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/ui/dist/assets/packet-JDAUHWVQ-C_r4ed30.js +1 -0
- package/dist/ui/dist/assets/path-fybaL0A-.js +1 -0
- package/dist/ui/dist/assets/pegDiagram-ECIUJNBX-B8VUm3Bh.js +1 -0
- package/dist/ui/dist/assets/pie-XZMESJXO-DcwtbH9y.js +1 -0
- package/dist/ui/dist/assets/pieDiagram-GNMPY74T-CvUNBi2F.js +39 -0
- package/dist/ui/dist/assets/prism-bash-D6zCJ74D.js +1 -0
- package/dist/ui/dist/assets/prism-c-04YixN25.js +1 -0
- package/dist/ui/dist/assets/prism-clike-DapgWyxx.js +1 -0
- package/dist/ui/dist/assets/prism-core-_2ju85Ya.js +1 -0
- package/dist/ui/dist/assets/prism-cpp-C-lJbC-6.js +1 -0
- package/dist/ui/dist/assets/prism-css-CPgOzxXp.js +1 -0
- package/dist/ui/dist/assets/prism-go-C_8qAH5n.js +1 -0
- package/dist/ui/dist/assets/prism-java-6wtusvIl.js +1 -0
- package/dist/ui/dist/assets/prism-javascript-BnO-swvr.js +1 -0
- package/dist/ui/dist/assets/prism-json-Dp_-W-Hv.js +1 -0
- package/dist/ui/dist/assets/prism-jsx-D-D0NWtC.js +1 -0
- package/dist/ui/dist/assets/prism-kotlin-BUlJgC-o.js +1 -0
- package/dist/ui/dist/assets/prism-markdown-Bc9-36OC.js +1 -0
- package/dist/ui/dist/assets/prism-markup-Cp04rIa1.js +1 -0
- package/dist/ui/dist/assets/prism-markup-templating-KkSMBj6Y.js +1 -0
- package/dist/ui/dist/assets/prism-php-Co4fM40K.js +1 -0
- package/dist/ui/dist/assets/prism-python-CfiSVtM6.js +1 -0
- package/dist/ui/dist/assets/prism-ruby-DFCASkuD.js +1 -0
- package/dist/ui/dist/assets/prism-rust-Bu9N8X3N.js +1 -0
- package/dist/ui/dist/assets/prism-sql-CYhVDMEE.js +1 -0
- package/dist/ui/dist/assets/prism-swift-B0kWc-ne.js +1 -0
- package/dist/ui/dist/assets/prism-toml-DKb54GAT.js +1 -0
- package/dist/ui/dist/assets/prism-tsx-p6J2Kc_4.js +1 -0
- package/dist/ui/dist/assets/prism-typescript-BMqONzxf.js +1 -0
- package/dist/ui/dist/assets/prism-yaml-C0JHu8gr.js +1 -0
- package/dist/ui/dist/assets/quadrantDiagram-D57K4MC3-Dg2eVS3j.js +7 -0
- package/dist/ui/dist/assets/radar-ABXABTNO-xCNTcZE9.js +1 -0
- package/dist/ui/dist/assets/railroad-I3PHUGI6-BI-sIL2Q.js +1 -0
- package/dist/ui/dist/assets/railroad-abnf-LNEFI6M7-Ddvk40Wx.js +1 -0
- package/dist/ui/dist/assets/railroad-ebnf-DWYD2UWJ-h35gE183.js +1 -0
- package/dist/ui/dist/assets/railroad-peg-JR7BVNR7-Zim9iPdp.js +1 -0
- package/dist/ui/dist/assets/railroadDiagram-Y72NRBL7-Cz6fdmOQ.js +1 -0
- package/dist/ui/dist/assets/requirementDiagram-EQYSW5RA-DTJkCrcD.js +84 -0
- package/dist/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/ui/dist/assets/sankeyDiagram-SY24JHO5-Bg1iiVla.js +40 -0
- package/dist/ui/dist/assets/sequenceDiagram-BLEHNHQU-9XfetsRA.js +169 -0
- package/dist/ui/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/ui/dist/assets/src-Dym5afQ5.js +1 -0
- package/dist/ui/dist/assets/stateDiagram-v2-A6IESKWZ-CMmcMV8z.js +291 -0
- package/dist/ui/dist/assets/swimlanes-B3ENRKFQ-B2PcshTY.js +1 -0
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DfB3XTTn.js +8 -0
- package/dist/ui/dist/assets/timeline-definition-JXYKGLGB-Cr1hCNtW.js +120 -0
- package/dist/ui/dist/assets/treeView-D4JQ5SDB-jH5ksKnh.js +1 -0
- package/dist/ui/dist/assets/treemap-SAJKECNS-BV90e8QL.js +1 -0
- package/dist/ui/dist/assets/usecaseDiagram-WPOLBNNX-BNwq5aJV.js +328 -0
- package/dist/ui/dist/assets/vennDiagram-Q3Y3IDCW-D-lLyZCL.js +34 -0
- package/dist/ui/dist/assets/wardley-7MLQ67FV-CWfHQUGt.js +1 -0
- package/dist/ui/dist/assets/wardleyDiagram-IVAIDKWA-BL_qNKVd.js +78 -0
- package/dist/ui/dist/assets/xychartDiagram-4URNHDU4-BHPK5Snp.js +7 -0
- package/dist/ui/dist/index.html +15 -0
- package/dist/ui/dist/style.css +121 -0
- package/package.json +31 -10
- package/skills/follow-me/SKILL.md +19 -89
- package/dist/src/anthropic.js +0 -133
- package/dist/ui/app.js +0 -2201
- package/dist/ui/concerns.js +0 -165
- package/dist/ui/highlight.js +0 -221
- package/dist/ui/index.html +0 -94
- package/dist/ui/live.js +0 -184
- package/dist/ui/playback.js +0 -579
- package/dist/ui/questions.js +0 -189
- package/dist/ui/style.css +0 -1331
- package/dist/ui/summary.js +0 -45
package/dist/src/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import { basename, isAbsolute, join } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { isReplyId, parseComment, parseDraft, parseNewDecision, parseNewReply, } from "./comments.js";
|
|
8
8
|
import { isConcernId, isPlanVersion } from "./plan.js";
|
|
9
|
-
import { renderReport } from "./prompt.js";
|
|
9
|
+
import { renderReport, reportItems } from "./prompt.js";
|
|
10
10
|
import { isLockToken } from "./session.js";
|
|
11
11
|
/** The requested port is taken; host.ts decides whether that is our server or a stranger. */
|
|
12
12
|
export class PortInUseError extends Error {
|
|
@@ -23,19 +23,16 @@ export class RegistrationError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
/** GET /api/health answers with this, so a CLI can tell our server from another program. */
|
|
25
25
|
export const HEALTH_APP = "follow-me";
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
/** The page Vite builds from ui/ into ui/dist; `npm run build` makes it. */
|
|
27
|
+
const UI_DIR = new URL("../ui/dist/", import.meta.url);
|
|
28
|
+
/** Hashed bundles under /assets/ and the few flat files of ui/public at the root. */
|
|
29
|
+
const STATIC_FILE = /^\/((?:assets\/)?[\w-]+(?:\.[\w-]+)*\.(js|css|woff2|svg))$/;
|
|
29
30
|
const STATIC_TYPES = new Map([
|
|
30
31
|
["js", "text/javascript; charset=utf-8"],
|
|
31
32
|
["css", "text/css; charset=utf-8"],
|
|
33
|
+
["woff2", "font/woff2"],
|
|
34
|
+
["svg", "image/svg+xml"],
|
|
32
35
|
]);
|
|
33
|
-
/** Mermaid's ESM entry imports its chunks by relative path, so they are served beside it. */
|
|
34
|
-
const MERMAID_ENTRY = new URL(import.meta.resolve("mermaid/dist/mermaid.esm.min.mjs"));
|
|
35
|
-
const MERMAID_CHUNK = /^\/vendor\/chunks\/mermaid\.esm\.min\/([\w-]+\.mjs)$/;
|
|
36
|
-
/** Prism is a plain script per component, loaded one at a time by the page as languages need. */
|
|
37
|
-
const PRISM_CORE = new URL(import.meta.resolve("prismjs/components/prism-core.min.js"));
|
|
38
|
-
const PRISM_PATH = /^\/vendor\/prism\/([a-z][a-z-]*)\.js$/;
|
|
39
36
|
const MAX_BODY_BYTES = 1_000_000;
|
|
40
37
|
const BASE32 = "abcdefghijklmnopqrstuvwxyz234567";
|
|
41
38
|
const COMMENT_ID = /^c_[a-z2-7]{6}$/;
|
|
@@ -85,7 +82,13 @@ export async function loadDraft(reviewsDir, version) {
|
|
|
85
82
|
}
|
|
86
83
|
catch (error) {
|
|
87
84
|
if (isRecord(error) && error["code"] === "ENOENT") {
|
|
88
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
comments: [],
|
|
87
|
+
concerns: [],
|
|
88
|
+
resolved: [],
|
|
89
|
+
excluded: { comments: [], concerns: [] },
|
|
90
|
+
earlierReplies: [],
|
|
91
|
+
};
|
|
89
92
|
}
|
|
90
93
|
throw error;
|
|
91
94
|
}
|
|
@@ -196,7 +199,7 @@ function indexPage(rows, now) {
|
|
|
196
199
|
const body = items.length === 0
|
|
197
200
|
? ` <section class="empty">
|
|
198
201
|
<h1>No sessions yet</h1>
|
|
199
|
-
<p>
|
|
202
|
+
<p>Ask your coding agent to review a branch with follow-me. Each review opens here.</p>
|
|
200
203
|
<p><a href="https://github.com/phanshiyu/follow-me#readme">How follow-me works</a></p>
|
|
201
204
|
</section>`
|
|
202
205
|
: ` <h1>Sessions</h1>
|
|
@@ -221,44 +224,18 @@ ${body}
|
|
|
221
224
|
</html>
|
|
222
225
|
`;
|
|
223
226
|
}
|
|
224
|
-
|
|
225
|
-
function vendorFile(path) {
|
|
226
|
-
const part = PRISM_PATH.exec(path)?.[1];
|
|
227
|
-
if (part !== undefined) {
|
|
228
|
-
return part === "core" ? PRISM_CORE : new URL(`prism-${part}.min.js`, PRISM_CORE);
|
|
229
|
-
}
|
|
230
|
-
if (path === "/vendor/mermaid.mjs")
|
|
231
|
-
return MERMAID_ENTRY;
|
|
232
|
-
const chunk = MERMAID_CHUNK.exec(path)?.[1];
|
|
233
|
-
return chunk === undefined ? null : new URL(`chunks/mermaid.esm.min/${chunk}`, MERMAID_ENTRY);
|
|
234
|
-
}
|
|
235
|
-
async function readUi(name) {
|
|
227
|
+
async function readUi(dir, name) {
|
|
236
228
|
try {
|
|
237
|
-
return await readFile(fileURLToPath(new URL(name,
|
|
229
|
+
return await readFile(fileURLToPath(new URL(name, dir)));
|
|
238
230
|
}
|
|
239
231
|
catch (error) {
|
|
240
232
|
if (isRecord(error) && error["code"] === "ENOENT")
|
|
241
|
-
|
|
233
|
+
return null;
|
|
242
234
|
throw error;
|
|
243
235
|
}
|
|
244
236
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
bytes = await readFile(fileURLToPath(file));
|
|
249
|
-
}
|
|
250
|
-
catch (error) {
|
|
251
|
-
if (isRecord(error) && error["code"] === "ENOENT")
|
|
252
|
-
throw new HttpError(404, "not found");
|
|
253
|
-
throw error;
|
|
254
|
-
}
|
|
255
|
-
// The bundle only changes with the installed package, and it is several megabytes.
|
|
256
|
-
res.writeHead(200, {
|
|
257
|
-
"content-type": "text/javascript; charset=utf-8",
|
|
258
|
-
"cache-control": "max-age=86400",
|
|
259
|
-
});
|
|
260
|
-
res.end(bytes);
|
|
261
|
-
}
|
|
237
|
+
const NOT_BUILT = `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="color-scheme" content="light dark" /><title>follow-me</title></head><body style="font-family: system-ui; max-width: 40rem; margin: 4rem auto; padding: 0 1rem"><h1>The review page is not built</h1><p>This follow-me runs from a checkout without <code>ui/dist</code>. Run <code>npm run build</code> (or <code>npm run ui:build</code>) in the checkout, then reload.</p></body></html>
|
|
238
|
+
`;
|
|
262
239
|
function viewOf(registration, loaded, draft) {
|
|
263
240
|
const { state, reviewsDir } = loaded;
|
|
264
241
|
return {
|
|
@@ -267,7 +244,11 @@ function viewOf(registration, loaded, draft) {
|
|
|
267
244
|
comments: new Map(state.comments.map((c) => [c.id, c])),
|
|
268
245
|
decisions: new Map(draft.concerns.map((d) => [d.concern, d])),
|
|
269
246
|
resolved: new Set(draft.resolved),
|
|
247
|
+
excludedComments: new Set(draft.excluded.comments),
|
|
248
|
+
excludedConcerns: new Set(draft.excluded.concerns),
|
|
249
|
+
earlierReplies: new Map(draft.earlierReplies.map((e) => [e.comment, e.replies])),
|
|
270
250
|
unitIds: new Set(state.units.map((u) => u.id)),
|
|
251
|
+
hunkRows: new Map(state.units.flatMap((u) => u.kind === "hunk" ? [[u.id, u.body.length]] : [])),
|
|
271
252
|
concernIds: new Set(state.commits.flatMap((c) => (c.concerns ?? []).map((k) => k.id))),
|
|
272
253
|
draftFile: draftPath(reviewsDir, state.version),
|
|
273
254
|
reviewFile: join(reviewsDir, `v${state.version}.json`),
|
|
@@ -277,6 +258,47 @@ function withReplies(comment, replies) {
|
|
|
277
258
|
const { replies: _dropped, ...rest } = comment;
|
|
278
259
|
return replies === undefined || replies.length === 0 ? rest : { ...rest, replies };
|
|
279
260
|
}
|
|
261
|
+
/** A comment of the previous version with the replies it got during this one. */
|
|
262
|
+
function withEarlier(view, comment) {
|
|
263
|
+
const later = view.earlierReplies.get(comment.id) ?? [];
|
|
264
|
+
return later.length === 0
|
|
265
|
+
? comment
|
|
266
|
+
: withReplies(comment, [...(comment.replies ?? []), ...later]);
|
|
267
|
+
}
|
|
268
|
+
function previousOf(view) {
|
|
269
|
+
const { previous } = view.loaded;
|
|
270
|
+
if (previous === null)
|
|
271
|
+
return null;
|
|
272
|
+
return { ...previous, comments: previous.comments.map((c) => withEarlier(view, c)) };
|
|
273
|
+
}
|
|
274
|
+
function carriedOf(view) {
|
|
275
|
+
return view.loaded.carried.map((c) => ({ ...c, comment: withEarlier(view, c.comment) }));
|
|
276
|
+
}
|
|
277
|
+
function earlierOf(view) {
|
|
278
|
+
return [...view.earlierReplies].map(([comment, replies]) => ({ comment, replies }));
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* What a restarted server's log begins with: each saved comment and each reply as the event that
|
|
282
|
+
* first carried it, oldest first, so a client that reads from 0 sees the whole conversation.
|
|
283
|
+
*/
|
|
284
|
+
function replayOf(view) {
|
|
285
|
+
const said = [];
|
|
286
|
+
for (const comment of view.comments.values()) {
|
|
287
|
+
said.push({
|
|
288
|
+
at: comment.createdAt,
|
|
289
|
+
event: { type: "comment", comment: withReplies(comment, undefined) },
|
|
290
|
+
});
|
|
291
|
+
for (const reply of comment.replies ?? []) {
|
|
292
|
+
said.push({ at: reply.createdAt, event: { type: "reply", commentId: comment.id, reply } });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const [commentId, replies] of view.earlierReplies) {
|
|
296
|
+
for (const reply of replies) {
|
|
297
|
+
said.push({ at: reply.createdAt, event: { type: "reply", commentId, reply } });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return said.sort((a, b) => a.at.localeCompare(b.at)).map((s) => s.event);
|
|
301
|
+
}
|
|
280
302
|
function mintReplyId(taken) {
|
|
281
303
|
for (;;) {
|
|
282
304
|
let id = "r_";
|
|
@@ -308,17 +330,26 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
308
330
|
for (const listener of [...listeners])
|
|
309
331
|
listener();
|
|
310
332
|
};
|
|
333
|
+
for (const event of replayOf(view))
|
|
334
|
+
emit(event);
|
|
311
335
|
const waitForEvents = (since, timeoutMs, signal) => new Promise((resolve) => {
|
|
312
|
-
const
|
|
313
|
-
|
|
336
|
+
const news = () => events.filter((e) => e.seq > since);
|
|
337
|
+
const answer = () => resolve({ seq: events.length, events: news() });
|
|
338
|
+
if (news().length > 0 || timeoutMs === 0 || signal.aborted)
|
|
314
339
|
return answer();
|
|
315
340
|
const wake = () => {
|
|
341
|
+
if (news().length === 0 && !signal.aborted)
|
|
342
|
+
return;
|
|
316
343
|
clearTimeout(timer);
|
|
317
344
|
waiters.delete(wake);
|
|
318
345
|
signal.removeEventListener("abort", wake);
|
|
319
346
|
answer();
|
|
320
347
|
};
|
|
321
|
-
const timer = setTimeout(
|
|
348
|
+
const timer = setTimeout(() => {
|
|
349
|
+
waiters.delete(wake);
|
|
350
|
+
signal.removeEventListener("abort", wake);
|
|
351
|
+
answer();
|
|
352
|
+
}, timeoutMs);
|
|
322
353
|
waiters.add(wake);
|
|
323
354
|
signal.addEventListener("abort", wake);
|
|
324
355
|
});
|
|
@@ -330,6 +361,10 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
330
361
|
if (swapping)
|
|
331
362
|
throw new HttpError(409, "the session is switching plan versions; try again");
|
|
332
363
|
};
|
|
364
|
+
/** The exclusions as stored, or null when nothing is excluded. */
|
|
365
|
+
const excludedOf = () => view.excludedComments.size + view.excludedConcerns.size === 0
|
|
366
|
+
? null
|
|
367
|
+
: { comments: [...view.excludedComments], concerns: [...view.excludedConcerns] };
|
|
333
368
|
// Writes are chained so two rapid edits never race on the same file.
|
|
334
369
|
let writes = Promise.resolve();
|
|
335
370
|
const saveDraft = () => {
|
|
@@ -337,12 +372,16 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
337
372
|
const file = view.draftFile;
|
|
338
373
|
const concerns = [...view.decisions.values()];
|
|
339
374
|
const resolved = [...view.resolved];
|
|
375
|
+
const excluded = excludedOf();
|
|
376
|
+
const earlierReplies = earlierOf(view);
|
|
340
377
|
const snapshot = JSON.stringify({
|
|
341
378
|
schema: "follow-me/review-draft@1",
|
|
342
379
|
planVersion: state.version,
|
|
343
380
|
comments: [...view.comments.values()],
|
|
344
381
|
...(concerns.length === 0 ? {} : { concerns }),
|
|
345
382
|
...(resolved.length === 0 ? {} : { resolved }),
|
|
383
|
+
...(excluded === null ? {} : { excluded }),
|
|
384
|
+
...(earlierReplies.length === 0 ? {} : { earlierReplies }),
|
|
346
385
|
}, null, 2);
|
|
347
386
|
const dir = view.loaded.reviewsDir;
|
|
348
387
|
writes = writes.then(async () => {
|
|
@@ -363,11 +402,21 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
363
402
|
if (anchor.kind === "hunk" && !view.unitIds.has(anchor.unit)) {
|
|
364
403
|
throw new HttpError(400, "anchor names an unknown unit");
|
|
365
404
|
}
|
|
405
|
+
if (anchor.kind === "hunk" && anchor.offsets !== undefined) {
|
|
406
|
+
const rows = view.hunkRows.get(anchor.unit);
|
|
407
|
+
if (rows === undefined)
|
|
408
|
+
throw new HttpError(400, "anchor.offsets names a unit without rows");
|
|
409
|
+
if (anchor.offsets.some((o) => o >= rows)) {
|
|
410
|
+
throw new HttpError(400, `anchor.offsets is out of range, the unit has ${rows} rows`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
366
413
|
return parsed;
|
|
367
414
|
};
|
|
368
415
|
const review = () => {
|
|
369
416
|
const concerns = [...view.decisions.values()];
|
|
370
417
|
const resolved = [...view.resolved];
|
|
418
|
+
const excluded = excludedOf();
|
|
419
|
+
const earlierReplies = earlierOf(view);
|
|
371
420
|
return {
|
|
372
421
|
schema: "follow-me/review@1",
|
|
373
422
|
mode: view.loaded.state.mode,
|
|
@@ -377,6 +426,8 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
377
426
|
comments: [...view.comments.values()],
|
|
378
427
|
...(concerns.length === 0 ? {} : { concerns }),
|
|
379
428
|
...(resolved.length === 0 ? {} : { resolved }),
|
|
429
|
+
...(excluded === null ? {} : { excluded }),
|
|
430
|
+
...(earlierReplies.length === 0 ? {} : { earlierReplies }),
|
|
380
431
|
};
|
|
381
432
|
};
|
|
382
433
|
const report = (finished) => {
|
|
@@ -396,7 +447,7 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
396
447
|
await rm(view.draftFile, { force: true });
|
|
397
448
|
return view.reviewFile;
|
|
398
449
|
};
|
|
399
|
-
const hasWork = () => view.comments.size > 0 || view.decisions.size > 0;
|
|
450
|
+
const hasWork = () => view.comments.size > 0 || view.decisions.size > 0 || view.earlierReplies.size > 0;
|
|
400
451
|
return {
|
|
401
452
|
id,
|
|
402
453
|
done,
|
|
@@ -412,14 +463,17 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
412
463
|
mode: view.loaded.state.mode,
|
|
413
464
|
version: view.loaded.state.version,
|
|
414
465
|
comments: view.comments.size,
|
|
466
|
+
openedAt: new Date(openedAt).toISOString(),
|
|
415
467
|
}),
|
|
416
468
|
snapshot: () => ({
|
|
417
469
|
...view.loaded.state,
|
|
418
470
|
comments: [...view.comments.values()],
|
|
419
471
|
concerns: [...view.decisions.values()],
|
|
420
472
|
resolved: [...view.resolved],
|
|
473
|
+
excluded: { comments: [...view.excludedComments], concerns: [...view.excludedConcerns] },
|
|
421
474
|
seq: events.length,
|
|
422
|
-
previous: view
|
|
475
|
+
previous: previousOf(view),
|
|
476
|
+
carried: carriedOf(view),
|
|
423
477
|
}),
|
|
424
478
|
/** Events after `since`, now and as they happen, until unfollowed or the review ends. */
|
|
425
479
|
latest: () => events.length,
|
|
@@ -429,7 +483,7 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
429
483
|
}
|
|
430
484
|
let last = since;
|
|
431
485
|
const flush = () => {
|
|
432
|
-
for (const event of events.
|
|
486
|
+
for (const event of events.filter((e) => e.seq > last)) {
|
|
433
487
|
send(event);
|
|
434
488
|
last = event.seq;
|
|
435
489
|
if (event.type === "ended") {
|
|
@@ -450,6 +504,14 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
450
504
|
return waitForEvents(since, timeoutMs, signal);
|
|
451
505
|
},
|
|
452
506
|
report: () => report(review()),
|
|
507
|
+
reportItems: () => {
|
|
508
|
+
const { state } = view.loaded;
|
|
509
|
+
const units = new Map(state.units.map((u) => [u.id, u]));
|
|
510
|
+
return reportItems(review(), state.commits, units, {
|
|
511
|
+
branch: state.anchor.branch,
|
|
512
|
+
responses: [],
|
|
513
|
+
});
|
|
514
|
+
},
|
|
453
515
|
add: async (raw) => {
|
|
454
516
|
open();
|
|
455
517
|
if (!isRecord(raw))
|
|
@@ -479,25 +541,37 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
479
541
|
if (gone === undefined)
|
|
480
542
|
throw new HttpError(404, "no such comment");
|
|
481
543
|
view.comments.delete(target);
|
|
544
|
+
view.excludedComments.delete(gone.id);
|
|
482
545
|
emit({ type: "comment-deleted", id: gone.id });
|
|
483
546
|
await saveDraft();
|
|
484
547
|
},
|
|
548
|
+
/** Replies under a comment of this version, or of the previous one, as after a swap. */
|
|
485
549
|
reply: async (target, raw) => {
|
|
486
550
|
open();
|
|
487
551
|
const comment = view.comments.get(target);
|
|
488
|
-
|
|
552
|
+
const earlier = view.loaded.previous?.comments.find((c) => c.id === target);
|
|
553
|
+
const owner = comment ?? earlier;
|
|
554
|
+
if (owner === undefined)
|
|
489
555
|
throw new HttpError(404, "no such comment");
|
|
490
556
|
const parsed = parseNewReply(raw);
|
|
491
557
|
if (typeof parsed === "string")
|
|
492
558
|
throw new HttpError(400, `malformed reply: ${parsed}`);
|
|
493
|
-
const taken = new Set([...view.comments.values()
|
|
559
|
+
const taken = new Set([...view.comments.values(), ...(previousOf(view)?.comments ?? [])]
|
|
560
|
+
.flatMap((c) => c.replies ?? [])
|
|
561
|
+
.map((r) => r.id));
|
|
494
562
|
const reply = {
|
|
495
563
|
id: mintReplyId(taken),
|
|
496
564
|
...parsed,
|
|
497
565
|
createdAt: new Date().toISOString(),
|
|
498
566
|
};
|
|
499
|
-
|
|
500
|
-
|
|
567
|
+
if (comment !== undefined) {
|
|
568
|
+
view.comments.set(comment.id, { ...comment, replies: [...(comment.replies ?? []), reply] });
|
|
569
|
+
}
|
|
570
|
+
else if (earlier !== undefined) {
|
|
571
|
+
const later = view.earlierReplies.get(earlier.id) ?? [];
|
|
572
|
+
view.earlierReplies.set(earlier.id, [...later, reply]);
|
|
573
|
+
}
|
|
574
|
+
emit({ type: "reply", commentId: owner.id, reply });
|
|
501
575
|
await saveDraft();
|
|
502
576
|
return reply.id;
|
|
503
577
|
},
|
|
@@ -506,15 +580,54 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
506
580
|
if (!isConcernId(target) || !view.concernIds.has(target)) {
|
|
507
581
|
throw new HttpError(404, "no such concern in this plan");
|
|
508
582
|
}
|
|
509
|
-
const
|
|
510
|
-
if (typeof
|
|
511
|
-
throw new HttpError(400, `malformed decision: ${
|
|
583
|
+
const parsed = parseNewDecision(target, raw);
|
|
584
|
+
if (typeof parsed === "string")
|
|
585
|
+
throw new HttpError(400, `malformed decision: ${parsed}`);
|
|
586
|
+
const decision = { ...parsed, decidedAt: new Date().toISOString() };
|
|
512
587
|
view.decisions.set(target, decision);
|
|
513
588
|
const { concern: concernId, ...rest } = decision;
|
|
514
589
|
emit({ type: "concern", concernId, ...rest });
|
|
515
590
|
await saveDraft();
|
|
516
591
|
return decision;
|
|
517
592
|
},
|
|
593
|
+
/** Leaves a comment or an agent concern out of the hand-off, or puts it back. */
|
|
594
|
+
exclude: async (raw) => {
|
|
595
|
+
open();
|
|
596
|
+
const body = isRecord(raw) ? raw : {};
|
|
597
|
+
const { comment, concern, excluded } = body;
|
|
598
|
+
if (typeof excluded !== "boolean" || (comment === undefined) === (concern === undefined)) {
|
|
599
|
+
throw new HttpError(400, "expected { comment } or { concern }, and { excluded: boolean }");
|
|
600
|
+
}
|
|
601
|
+
if (comment !== undefined) {
|
|
602
|
+
if (!isCommentId(comment) || !view.comments.has(comment)) {
|
|
603
|
+
throw new HttpError(404, "no such comment");
|
|
604
|
+
}
|
|
605
|
+
if (excluded)
|
|
606
|
+
view.excludedComments.add(comment);
|
|
607
|
+
else
|
|
608
|
+
view.excludedComments.delete(comment);
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
if (!isConcernId(concern) || !view.concernIds.has(concern)) {
|
|
612
|
+
throw new HttpError(404, "no such concern in this plan");
|
|
613
|
+
}
|
|
614
|
+
if (excluded)
|
|
615
|
+
view.excludedConcerns.add(concern);
|
|
616
|
+
else
|
|
617
|
+
view.excludedConcerns.delete(concern);
|
|
618
|
+
}
|
|
619
|
+
await saveDraft();
|
|
620
|
+
return { comments: [...view.excludedComments], concerns: [...view.excludedConcerns] };
|
|
621
|
+
},
|
|
622
|
+
/**
|
|
623
|
+
* The reviewer pressed Submit. Every comment already reached the agent, so this only tells it
|
|
624
|
+
* the reviewer is done; `released` stays for pages that still read it.
|
|
625
|
+
*/
|
|
626
|
+
submit: () => {
|
|
627
|
+
open();
|
|
628
|
+
emit({ type: "submitted" });
|
|
629
|
+
return { released: 0 };
|
|
630
|
+
},
|
|
518
631
|
resolve: async (raw) => {
|
|
519
632
|
open();
|
|
520
633
|
const body = isRecord(raw) ? raw : {};
|
|
@@ -556,7 +669,7 @@ function hostSession(id, registration, loaded, draft, load) {
|
|
|
556
669
|
else
|
|
557
670
|
await rm(view.draftFile, { force: true });
|
|
558
671
|
view = viewOf({ ...view.registration, version, hash }, next, await loadDraft(next.reviewsDir, version));
|
|
559
|
-
emit({ type: "version", version, hash });
|
|
672
|
+
emit({ type: "version", version, hash, carried: carriedOf(view) });
|
|
560
673
|
}
|
|
561
674
|
finally {
|
|
562
675
|
swapping = false;
|
|
@@ -588,6 +701,7 @@ function sameRegistration(a, b) {
|
|
|
588
701
|
* itself once it has had no session for idleMs.
|
|
589
702
|
*/
|
|
590
703
|
export function startServer(opts) {
|
|
704
|
+
const uiDir = opts.uiDir ?? UI_DIR;
|
|
591
705
|
const registry = new Map();
|
|
592
706
|
const pending = new Map();
|
|
593
707
|
let boundPort = 0;
|
|
@@ -671,17 +785,21 @@ export function startServer(opts) {
|
|
|
671
785
|
const method = req.method ?? "GET";
|
|
672
786
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
673
787
|
const path = url.pathname;
|
|
674
|
-
const asset = method === "GET" ? STATIC_FILE.exec(path)
|
|
675
|
-
if (asset !==
|
|
676
|
-
const
|
|
677
|
-
const
|
|
678
|
-
|
|
788
|
+
const asset = method === "GET" ? STATIC_FILE.exec(path) : null;
|
|
789
|
+
if (asset !== null) {
|
|
790
|
+
const [, name = "", ext = ""] = asset;
|
|
791
|
+
const bytes = await readUi(uiDir, name);
|
|
792
|
+
if (bytes === null)
|
|
793
|
+
throw new HttpError(404, "not found");
|
|
794
|
+
// Vite hashes every name under assets/, so those never change under the same URL.
|
|
795
|
+
const cache = name.startsWith("assets/") ? "max-age=31536000, immutable" : "no-store";
|
|
796
|
+
res.writeHead(200, {
|
|
797
|
+
"content-type": STATIC_TYPES.get(ext) ?? "text/plain",
|
|
798
|
+
"cache-control": cache,
|
|
799
|
+
});
|
|
679
800
|
res.end(bytes);
|
|
680
801
|
return;
|
|
681
802
|
}
|
|
682
|
-
const vendor = method === "GET" ? vendorFile(path) : null;
|
|
683
|
-
if (vendor)
|
|
684
|
-
return sendVendor(res, vendor);
|
|
685
803
|
if (method === "GET" && path === "/") {
|
|
686
804
|
const rows = [...registry.values()].map((h) => ({
|
|
687
805
|
summary: h.summary(),
|
|
@@ -711,11 +829,12 @@ export function startServer(opts) {
|
|
|
711
829
|
res.end();
|
|
712
830
|
return;
|
|
713
831
|
}
|
|
714
|
-
if (method === "GET" && rest === "/") {
|
|
832
|
+
if (method === "GET" && (rest === "/" || rest === "/report")) {
|
|
715
833
|
if (!isSessionId(rawId) || !registry.has(rawId)) {
|
|
716
834
|
return sendHtml(res, 404, '<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="color-scheme" content="light dark" /><title>follow-me</title><link rel="stylesheet" href="/style.css" /></head><body class="index"><header class="topbar"><strong>follow-me</strong></header><main class="index-main"><section class="empty"><h1>This session has ended</h1><p>It finished, or it never existed. <a href="/">All sessions</a></p></section></main></body></html>\n');
|
|
717
835
|
}
|
|
718
|
-
|
|
836
|
+
const page = await readUi(uiDir, "index.html");
|
|
837
|
+
return page === null ? sendHtml(res, 503, NOT_BUILT) : sendHtml(res, 200, page.toString());
|
|
719
838
|
}
|
|
720
839
|
const session = hosted(rawId);
|
|
721
840
|
if (method === "GET" && rest === "/api/state")
|
|
@@ -747,6 +866,9 @@ export function startServer(opts) {
|
|
|
747
866
|
const timeout = queryInt(url, "timeout", 0, MAX_POLL_MS, DEFAULT_POLL_MS);
|
|
748
867
|
return sendJson(res, 200, await session.poll(since, timeout, abort.signal));
|
|
749
868
|
}
|
|
869
|
+
if (method === "GET" && rest === "/api/report" && url.searchParams.get("format") === "json") {
|
|
870
|
+
return sendJson(res, 200, session.reportItems());
|
|
871
|
+
}
|
|
750
872
|
if (method === "GET" && rest === "/api/report") {
|
|
751
873
|
res.writeHead(200, { "content-type": "text/markdown; charset=utf-8" });
|
|
752
874
|
res.end(session.report());
|
|
@@ -762,6 +884,12 @@ export function startServer(opts) {
|
|
|
762
884
|
await session.swap(version, hash);
|
|
763
885
|
return sendJson(res, 200, session.summary());
|
|
764
886
|
}
|
|
887
|
+
if (method === "POST" && rest === "/api/submit") {
|
|
888
|
+
return sendJson(res, 200, session.submit());
|
|
889
|
+
}
|
|
890
|
+
if (method === "POST" && rest === "/api/excluded") {
|
|
891
|
+
return sendJson(res, 200, await session.exclude(await readJson(req)));
|
|
892
|
+
}
|
|
765
893
|
if (method === "POST" && rest === "/api/resolved") {
|
|
766
894
|
return sendJson(res, 200, { resolved: await session.resolve(await readJson(req)) });
|
|
767
895
|
}
|
package/dist/src/session.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, renameSync,
|
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { parseDraft, parseReview } from "./comments.js";
|
|
5
5
|
import { isSha } from "./diff.js";
|
|
6
|
+
import { evolvePlan } from "./evolve.js";
|
|
6
7
|
import { branchExists, currentBranch, defaultBaseRef, GitError, loadDiff, objectsDir, QUARANTINE_PREFIX, repoRoot, resolveCommit, } from "./git.js";
|
|
7
8
|
import { isFields, isPlanVersion, parsePlanFile, planHash, serializePlanFile, } from "./plan.js";
|
|
8
9
|
export class BranchMovedError extends Error {
|
|
@@ -202,8 +203,9 @@ function parseApplied(v) {
|
|
|
202
203
|
function makeSession(root, dir, anchor, diff) {
|
|
203
204
|
const plansDir = join(dir, "plans");
|
|
204
205
|
const splitReviews = join(dir, "reviews");
|
|
205
|
-
|
|
206
|
-
const
|
|
206
|
+
// Older builds wrote reviewer reviews to reviews/walk/; they stay readable, nothing new goes there.
|
|
207
|
+
const legacyWalkDir = join(splitReviews, "walk");
|
|
208
|
+
const reviewsDir = () => splitReviews;
|
|
207
209
|
const draftPath = join(dir, "draft.json");
|
|
208
210
|
const promptPath = join(dir, "prompt.md");
|
|
209
211
|
const appliedPath = join(dir, "applied.json");
|
|
@@ -216,16 +218,16 @@ function makeSession(root, dir, anchor, diff) {
|
|
|
216
218
|
}
|
|
217
219
|
return { version, file, hash: planHash(file) };
|
|
218
220
|
});
|
|
219
|
-
const reviewsIn = (
|
|
221
|
+
const reviewsIn = (dir) => versions(dir).map(({ version, path }) => {
|
|
220
222
|
const review = parseReview(readJson(path));
|
|
221
223
|
if (typeof review === "string")
|
|
222
224
|
throw new SessionFileError(`${path}: ${review}`);
|
|
223
225
|
return { version, review };
|
|
224
226
|
});
|
|
225
|
-
const reviews = () => reviewsIn(
|
|
226
|
-
const walkReviews = () => [...reviews().filter((r) => r.review.mode === "walk"), ...reviewsIn(
|
|
227
|
-
const draftVersions = (
|
|
228
|
-
const dir = reviewsDir(
|
|
227
|
+
const reviews = () => reviewsIn(splitReviews);
|
|
228
|
+
const walkReviews = () => [...reviews().filter((r) => r.review.mode === "walk"), ...reviewsIn(legacyWalkDir)].sort((a, b) => a.version - b.version);
|
|
229
|
+
const draftVersions = () => {
|
|
230
|
+
const dir = reviewsDir();
|
|
229
231
|
if (!existsSync(dir))
|
|
230
232
|
return [];
|
|
231
233
|
return readdirSync(dir).flatMap((name) => {
|
|
@@ -234,11 +236,11 @@ function makeSession(root, dir, anchor, diff) {
|
|
|
234
236
|
});
|
|
235
237
|
};
|
|
236
238
|
const draftReview = (mode, version) => {
|
|
237
|
-
const path = join(reviewsDir(
|
|
239
|
+
const path = join(reviewsDir(), `v${version}.draft.json`);
|
|
238
240
|
const draft = parseDraft(readJson(path));
|
|
239
241
|
if (typeof draft === "string")
|
|
240
242
|
throw new SessionFileError(`${path}: ${draft}`);
|
|
241
|
-
const { comments, concerns, resolved } = draft;
|
|
243
|
+
const { comments, concerns, resolved, excluded } = draft;
|
|
242
244
|
return {
|
|
243
245
|
schema: "follow-me/review@1",
|
|
244
246
|
mode,
|
|
@@ -248,6 +250,7 @@ function makeSession(root, dir, anchor, diff) {
|
|
|
248
250
|
comments,
|
|
249
251
|
...(concerns.length === 0 ? {} : { concerns }),
|
|
250
252
|
...(resolved.length === 0 ? {} : { resolved }),
|
|
253
|
+
...(excluded.comments.length + excluded.concerns.length === 0 ? {} : { excluded }),
|
|
251
254
|
};
|
|
252
255
|
};
|
|
253
256
|
const priorReview = (mode, before) => {
|
|
@@ -255,11 +258,20 @@ function makeSession(root, dir, anchor, diff) {
|
|
|
255
258
|
const done = (mode === "walk" ? walkReviews() : reviews().filter((r) => r.review.mode === mode))
|
|
256
259
|
.filter((r) => below(r.version))
|
|
257
260
|
.at(-1);
|
|
258
|
-
const unfinished = draftVersions(
|
|
261
|
+
const unfinished = draftVersions()
|
|
259
262
|
.filter((v) => below(v) && v > (done?.version ?? 0))
|
|
260
263
|
.sort((a, b) => a - b)
|
|
261
264
|
.at(-1);
|
|
262
|
-
|
|
265
|
+
if (unfinished === undefined)
|
|
266
|
+
return done?.review ?? null;
|
|
267
|
+
// A draft answers comments from every earlier version, so the finished review's
|
|
268
|
+
// comments stay visible beside the newer autosave's.
|
|
269
|
+
const current = draftReview(mode, unfinished);
|
|
270
|
+
if (done === undefined)
|
|
271
|
+
return current;
|
|
272
|
+
const seen = new Set(current.comments.map((c) => c.id));
|
|
273
|
+
const earlier = done.review.comments.filter((c) => !seen.has(c.id));
|
|
274
|
+
return { ...current, comments: [...earlier, ...current.comments] };
|
|
263
275
|
};
|
|
264
276
|
return {
|
|
265
277
|
root,
|
|
@@ -283,7 +295,7 @@ function makeSession(root, dir, anchor, diff) {
|
|
|
283
295
|
writeDraft: (draft) => writeJson(draftPath, draft),
|
|
284
296
|
writePrompt: (text) => writeText(promptPath, text),
|
|
285
297
|
writeReview: (review) => {
|
|
286
|
-
const path = join(reviewsDir(
|
|
298
|
+
const path = join(reviewsDir(), `v${review.planVersion}.json`);
|
|
287
299
|
if (existsSync(path))
|
|
288
300
|
throw new SessionFileError(`${path} is already written`);
|
|
289
301
|
writeJson(path, review);
|
|
@@ -408,6 +420,35 @@ export async function openSessionAt(dir) {
|
|
|
408
420
|
}
|
|
409
421
|
return openSession(dirname(top), { branch: anchor.branch });
|
|
410
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* The diff a stored plan was made on: the session's own when the plan is for this tip, else the
|
|
425
|
+
* plan's base and tip loaded again. Null when those commits are gone from the repo.
|
|
426
|
+
*/
|
|
427
|
+
export async function diffOfPlan(session, plan) {
|
|
428
|
+
const { base, tip } = plan.file;
|
|
429
|
+
if (base === session.anchor.base && tip === session.anchor.tip)
|
|
430
|
+
return session.diff;
|
|
431
|
+
try {
|
|
432
|
+
return await loadDiff(session.root, base, tip);
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
if (error instanceof GitError)
|
|
436
|
+
return null;
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* How the latest plan carries over to the branch as it is now, or null when that plan was made
|
|
442
|
+
* for this tip. The old diff is loaded from the plan's own base and tip, so this gives the same
|
|
443
|
+
* answer however many times it runs after the move.
|
|
444
|
+
*/
|
|
445
|
+
export async function sinceLastPlan(session) {
|
|
446
|
+
const latest = session.plans().at(-1);
|
|
447
|
+
if (latest === undefined || latest.file.tip === session.anchor.tip)
|
|
448
|
+
return null;
|
|
449
|
+
const oldDiff = await loadDiff(session.root, latest.file.base, latest.file.tip);
|
|
450
|
+
return { since: latest.version, ...evolvePlan(latest.file, oldDiff, session.diff) };
|
|
451
|
+
}
|
|
411
452
|
/** "<name>.<pid>.<random>.tmp" as written by writeJson, the lock and the review server. */
|
|
412
453
|
function abandonedTemps(dir) {
|
|
413
454
|
if (!existsSync(dir))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{m as e,t}from"./mermaid-parser.core-CAyLSQpH.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-Dym5afQ5.js";import"./chunk-DSNLSHJZ-DASbe_gb.js";import{n as i,r as a,t as o}from"./chunk-2E7WHVMD-zZdNfMkj.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-vfVMjgx2.js";var c=e().RailroadAbnf.parser.LangiumParser,l=n(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=n(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=n(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=n(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=n(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=n(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=n(e=>{s(e,o),e.title&&o.setTitle(e.title),e.rules.map(e=>o.addRule(m(e)))},`populateDb`),g={parser:{parse:n(e=>{o.clear(),r.debug(`[ABNF Parser] Starting Langium parse`);let n=c.parse(e);if(n.lexerErrors.length>0||n.parserErrors.length>0)throw new t(n);let i=n.value;r.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),r.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:o}},db:o,renderer:a,styles:i};export{g as diagram};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{n as e,t}from"./path-fybaL0A-.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-NDVqCM7k.js";function m(e){return e.innerRadius}function h(e){return e.outerRadius}function g(e){return e.startAngle}function _(e){return e.endAngle}function v(e){return e&&e.padAngle}function y(e,t,n,r,i,a,o,s){var c=n-e,l=r-t,u=o-i,d=s-a,f=d*c-u*l;if(!(f*f<1e-12))return f=(u*(t-a)-d*(e-i))/f,[e+f*c,t+f*l]}function b(e,t,n,r,i,a,o){var c=e-n,l=t-r,u=(o?a:-a)/d(c*c+l*l),f=u*l,p=-u*c,m=e+f,h=t+p,g=n+f,_=r+p,v=(m+g)/2,y=(h+_)/2,b=g-m,x=_-h,S=b*b+x*x,C=i-a,w=m*_-g*h,T=(x<0?-1:1)*d(s(0,C*C*S-w*w)),E=(w*x-b*T)/S,D=(-w*b-x*T)/S,O=(w*x+b*T)/S,k=(-w*b+x*T)/S,A=E-v,j=D-y,M=O-v,N=k-y;return A*A+j*j>M*M+N*N&&(E=O,D=k),{cx:E,cy:D,x01:-f,y01:-p,x11:E*(i/C-1),y11:D*(i/C-1)}}function x(){var s=m,x=h,S=e(0),C=null,w=g,T=_,E=v,D=null,O=t(k);function k(){var e,t,m=+s.apply(this,arguments),h=+x.apply(this,arguments),g=w.apply(this,arguments)-r,_=T.apply(this,arguments)-r,v=l(_-g),k=_>g;if(D||=e=O(),h<m&&(t=h,h=m,m=t),!(h>1e-12))D.moveTo(0,0);else if(v>c-1e-12)D.moveTo(h*u(g),h*a(g)),D.arc(0,0,h,g,_,!k),m>1e-12&&(D.moveTo(m*u(_),m*a(_)),D.arc(0,0,m,_,g,k));else{var A=g,j=_,M=g,N=_,P=v,F=v,I=E.apply(this,arguments)/2,L=I>1e-12&&(C?+C.apply(this,arguments):d(m*m+h*h)),R=p(l(h-m)/2,+S.apply(this,arguments)),z=R,B=R,V,H;if(L>1e-12){var U=o(L/m*a(I)),W=o(L/h*a(I));(P-=U*2)>1e-12?(U*=k?1:-1,M+=U,N-=U):(P=0,M=N=(g+_)/2),(F-=W*2)>1e-12?(W*=k?1:-1,A+=W,j-=W):(F=0,A=j=(g+_)/2)}var G=h*u(A),K=h*a(A),q=m*u(N),J=m*a(N);if(R>1e-12){var Y=h*u(j),X=h*a(j),Z=m*u(M),Q=m*a(M),$;if(v<i){if($=y(G,K,Z,Q,Y,X,q,J)){var ee=G-$[0],te=K-$[1],ne=Y-$[0],re=X-$[1],ie=1/a(f((ee*ne+te*re)/(d(ee*ee+te*te)*d(ne*ne+re*re)))/2),ae=d($[0]*$[0]+$[1]*$[1]);z=p(R,(m-ae)/(ie-1)),B=p(R,(h-ae)/(ie+1))}else z=B=0}}F>1e-12?B>1e-12?(V=b(Z,Q,G,K,h,B,k),H=b(Y,X,q,J,h,B,k),D.moveTo(V.cx+V.x01,V.cy+V.y01),B<R?D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,h,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),!k),D.arc(H.cx,H.cy,B,n(H.y11,H.x11),n(H.y01,H.x01),!k))):(D.moveTo(G,K),D.arc(0,0,h,A,j,!k)):D.moveTo(G,K),!(m>1e-12)||!(P>1e-12)?D.lineTo(q,J):z>1e-12?(V=b(q,J,Y,X,m,-z,k),H=b(G,K,Z,Q,m,-z,k),D.lineTo(V.cx+V.x01,V.cy+V.y01),z<R?D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,m,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),k),D.arc(H.cx,H.cy,z,n(H.y11,H.x11),n(H.y01,H.x01),!k))):D.arc(0,0,m,N,M,k)}if(D.closePath(),e)return D=null,e+``||null}return k.centroid=function(){var e=(+s.apply(this,arguments)+ +x.apply(this,arguments))/2,t=(+w.apply(this,arguments)+ +T.apply(this,arguments))/2-i/2;return[u(t)*e,a(t)*e]},k.innerRadius=function(t){return arguments.length?(s=typeof t==`function`?t:e(+t),k):s},k.outerRadius=function(t){return arguments.length?(x=typeof t==`function`?t:e(+t),k):x},k.cornerRadius=function(t){return arguments.length?(S=typeof t==`function`?t:e(+t),k):S},k.padRadius=function(t){return arguments.length?(C=t==null?null:typeof t==`function`?t:e(+t),k):C},k.startAngle=function(t){return arguments.length?(w=typeof t==`function`?t:e(+t),k):w},k.endAngle=function(t){return arguments.length?(T=typeof t==`function`?t:e(+t),k):T},k.padAngle=function(t){return arguments.length?(E=typeof t==`function`?t:e(+t),k):E},k.context=function(e){return arguments.length?(D=e??null,k):D},k}export{x as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./chunk-NGNAAXSQ-C2Wy9Xmz.js";export{M as createArchitectureServices}from"./mermaid-parser.core-CAyLSQpH.js";
|