@shiyuphan/follow-me 0.7.1 → 0.9.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 +9 -1
- package/dist/src/cli.js +181 -42
- package/dist/src/comments.js +215 -9
- package/dist/src/coverage.js +163 -25
- package/dist/src/host.js +23 -0
- package/dist/src/plan.js +128 -6
- package/dist/src/prompt.js +55 -2
- package/dist/src/refs.js +239 -0
- package/dist/src/server.js +65 -3
- package/dist/src/session.js +1 -0
- package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-cHEJvcBn.js → abnfDiagram-WEYOIA3C-DztIEwNE.js} +1 -1
- package/dist/ui/dist/assets/{arc-xolFyc0v.js → arc-DUYh-EqZ.js} +1 -1
- package/dist/ui/dist/assets/{architecture-WOLXFQ4H-B3hvvNV2.js → architecture-WOLXFQ4H-BQYTB92_.js} +1 -1
- package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-BNTMbPJW.js → architectureDiagram-ZHVYKT53-Dl7095A5.js} +1 -1
- package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-NYY6qeuY.js → blockDiagram-RJAE7QA3-QDZ5kE1J.js} +1 -1
- package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-B_bNpXHQ.js → c4Diagram-XDPPPE54-CKJZ1xOw.js} +1 -1
- package/dist/ui/dist/assets/channel-DF08Hd8f.js +1 -0
- package/dist/ui/dist/assets/{chunk-2E7WHVMD-BTet9STn.js → chunk-2E7WHVMD-BXBjaqd1.js} +1 -1
- package/dist/ui/dist/assets/{chunk-4EMNKR2T-qoQt4Fpx.js → chunk-4EMNKR2T-DPzfVuke.js} +1 -1
- package/dist/ui/dist/assets/{chunk-55XCU76K-C3I6EB-H.js → chunk-55XCU76K-B33Sj3w8.js} +1 -1
- package/dist/ui/dist/assets/{chunk-7S6XSTFO-CL-9s_8p.js → chunk-7S6XSTFO-B_u7dS03.js} +1 -1
- package/dist/ui/dist/assets/{chunk-AQDTSKZB-DHoGnGyP.js → chunk-AQDTSKZB-B4EzH5gx.js} +1 -1
- package/dist/ui/dist/assets/{chunk-DSNLSHJZ-Bk8WO5JF.js → chunk-DSNLSHJZ-CfWeHABr.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HAGFOTED-CwAC85lg.js → chunk-HAGFOTED-B8G8PjLZ.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HNE4Q36P-CIllywWr.js → chunk-HNE4Q36P-BA-PqoTF.js} +1 -1
- package/dist/ui/dist/assets/{chunk-NQOKRVIF-DDlpyYiH.js → chunk-NQOKRVIF-JvhuFcnR.js} +1 -1
- package/dist/ui/dist/assets/{chunk-UHW7PFBP-Cxdndcon.js → chunk-UHW7PFBP-BrfL5CGY.js} +1 -1
- package/dist/ui/dist/assets/{chunk-VQ5W62QI-CL9d9_iJ.js → chunk-VQ5W62QI-dBJqgdDy.js} +1 -1
- package/dist/ui/dist/assets/{chunk-WQQL4CDK-C4kzN8QU.js → chunk-WQQL4CDK-dfqd3k_P.js} +1 -1
- package/dist/ui/dist/assets/{chunk-XXDRQBXY-Cv_2FSiY.js → chunk-XXDRQBXY-kViWdB9M.js} +1 -1
- package/dist/ui/dist/assets/{chunk-YZO67HFG-oHiuYaCJ.js → chunk-YZO67HFG-ukfs4wT3.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZAUX2LTX-Cy_8d0XT.js → chunk-ZAUX2LTX-BxZHki81.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZCHMQSOR-piIpIcQi.js → chunk-ZCHMQSOR-PjluhR_E.js} +1 -1
- package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-DNKRIujO.js → classDiagram-v2-4MYWR3UA-Ct89LJec.js} +1 -1
- package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-BWKTGCHY.js → cose-bilkent-JH36ORCC-BnQceGmB.js} +1 -1
- package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-yx7KKkAk.js → cynefin-EF2NZ3EQ-D6WkCVE8.js} +1 -1
- package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-BTnZ7QVL.js → cynefinDiagram-WF24UDRP-dn6DUPAb.js} +1 -1
- package/dist/ui/dist/assets/{dagre-NK4GIZVX-CPh5Zn5I.js → dagre-NK4GIZVX-BtZSlLk7.js} +1 -1
- package/dist/ui/dist/assets/{diagram-CBK3MWTC-pI6NFdrD.js → diagram-CBK3MWTC-Ds4_tv6X.js} +1 -1
- package/dist/ui/dist/assets/{diagram-I5XIIV2D-DUIfARoS.js → diagram-I5XIIV2D-DcoVkg45.js} +1 -1
- package/dist/ui/dist/assets/{diagram-ODIL2UCW-lafSPvAw.js → diagram-ODIL2UCW-T47FAA3k.js} +1 -1
- package/dist/ui/dist/assets/{diagram-PFBEJUXE-u1lZas3a.js → diagram-PFBEJUXE-CRZcaVKP.js} +1 -1
- package/dist/ui/dist/assets/{diagram-RJA3I4VV-mc0GLMHo.js → diagram-RJA3I4VV-D6KXeUPg.js} +1 -1
- package/dist/ui/dist/assets/{diagram-TYUNFSNR-1NRbBrZW.js → diagram-TYUNFSNR-BHEIVyNN.js} +1 -1
- package/dist/ui/dist/assets/{dist-CHZ9mrl_.js → dist-DLlKIWBp.js} +1 -1
- package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-DdStjTDc.js → ebnfDiagram-V4P3TTPT-nUDvYRBV.js} +1 -1
- package/dist/ui/dist/assets/{elk-QONTBCTQ-BzRoFO45.js → elk-QONTBCTQ-D21BcRzc.js} +1 -1
- package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-DlMkgQxj.js → erDiagram-KYQIGQAA-CTpHuftU.js} +1 -1
- package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-BRYIOrod.js → eventmodeling-K75KTNOO-B6JaKuDY.js} +1 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-BTeOUR-p.js +1 -0
- package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-D8fQZ7Ah.js → ganttDiagram-R7GPNTJY-D2-pMcnC.js} +1 -1
- package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-C7ZydyWX.js → gitGraph-VSP46ZUC-B2yxwhEo.js} +1 -1
- package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-DCItam0v.js → gitGraphDiagram-PDYL7XHY-DnrhBMH1.js} +1 -1
- package/dist/ui/dist/assets/index-D74Skifa.css +1 -0
- package/dist/ui/dist/assets/index-Dnte9tPd.js +136 -0
- package/dist/ui/dist/assets/{info-OHQRW6UA-C3KneBKT.js → info-OHQRW6UA-D8oh6gvs.js} +1 -1
- package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-6wK09D32.js → infoDiagram-MVDRKAX5-BLSZjjgB.js} +1 -1
- package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-DTP5C8VX.js → ishikawaDiagram-HPEHV2GE-BXiznLSn.js} +1 -1
- package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-B--UIMtq.js → journeyDiagram-BOSHW7O7-BSYmVcO_.js} +1 -1
- package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-Ku4N9QGt.js → kanban-definition-FNSA4HC5-COthkkzK.js} +1 -1
- package/dist/ui/dist/assets/{line-C4-H5h1-.js → line-6ND8OaOL.js} +1 -1
- package/dist/ui/dist/assets/{linear-DhAID3hm.js → linear-DbWEPFJX.js} +1 -1
- package/dist/ui/dist/assets/{mermaid-parser.core-CMPNc-u6.js → mermaid-parser.core-Btsk294z.js} +3 -3
- package/dist/ui/dist/assets/{mermaid.core-Nd_xOg2y.js → mermaid.core-B4llDSFO.js} +4 -4
- package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-BlVsym5h.js → mindmap-definition-6OY57UG3-hv2PfX-x.js} +1 -1
- package/dist/ui/dist/assets/{packet-JDAUHWVQ-rsk7SM7S.js → packet-JDAUHWVQ-Dr4vBgjt.js} +1 -1
- package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-C0L20M8z.js → pegDiagram-ECIUJNBX-DvlFov3N.js} +1 -1
- package/dist/ui/dist/assets/{pie-XZMESJXO-B23-iZSN.js → pie-XZMESJXO-C9QsqC4n.js} +1 -1
- package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-BqFhe4Oh.js → pieDiagram-GNMPY74T-CrrHOj67.js} +1 -1
- package/dist/ui/dist/assets/{prism-core-DYsiqJeR.js → prism-core-Cc8H7LPG.js} +1 -1
- package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-BKP75B-m.js → quadrantDiagram-D57K4MC3-DQ7J6-ti.js} +1 -1
- package/dist/ui/dist/assets/{radar-ABXABTNO-BrEk2mQ7.js → radar-ABXABTNO-BBBPaujh.js} +1 -1
- package/dist/ui/dist/assets/{railroad-I3PHUGI6-wWO4Eapc.js → railroad-I3PHUGI6-C-P6Igaz.js} +1 -1
- package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-DfY-IPvE.js → railroad-abnf-LNEFI6M7-CAkorRLs.js} +1 -1
- package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-19wedWGk.js → railroad-ebnf-DWYD2UWJ-D8Hz6gCf.js} +1 -1
- package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-9vIslnoJ.js → railroad-peg-JR7BVNR7-D-XNMkOs.js} +1 -1
- package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-DVnmXs8w.js → railroadDiagram-Y72NRBL7-DE9LwxJQ.js} +1 -1
- package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-BEoMdlzR.js → requirementDiagram-EQYSW5RA-FrKFY6NJ.js} +1 -1
- package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-D7Tn2Gbs.js → sankeyDiagram-SY24JHO5-BDxI6RDX.js} +1 -1
- package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-CyjUDBaQ.js → sequenceDiagram-BLEHNHQU-pV1XcKX7.js} +1 -1
- package/dist/ui/dist/assets/{src-BiC9XNcI.js → src-snNJTXXq.js} +1 -1
- package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-CMp1VCO9.js → stateDiagram-v2-A6IESKWZ-BSc_5aAf.js} +1 -1
- package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-KEciLd6r.js → swimlanes-B3ENRKFQ-DC6qUXLh.js} +1 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-F1HykV3o.js +8 -0
- package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-C8YQtv7M.js → timeline-definition-JXYKGLGB-C8L0Dxtu.js} +1 -1
- package/dist/ui/dist/assets/{treeView-D4JQ5SDB-DgAzPaOO.js → treeView-D4JQ5SDB-DDyZIG5T.js} +1 -1
- package/dist/ui/dist/assets/{treemap-SAJKECNS-eCRqZMYc.js → treemap-SAJKECNS-CUaLgge0.js} +1 -1
- package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-Dirryw9G.js → usecaseDiagram-WPOLBNNX-BHRBd5pV.js} +1 -1
- package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-BjxQbAUx.js → vennDiagram-Q3Y3IDCW-Du4iwwkh.js} +1 -1
- package/dist/ui/dist/assets/{wardley-7MLQ67FV-CqHX5RUz.js → wardley-7MLQ67FV-DUgENI0s.js} +1 -1
- package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-Ds1HoEUY.js → wardleyDiagram-IVAIDKWA-B86zOdke.js} +1 -1
- package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-kodzDb12.js → xychartDiagram-4URNHDU4-twq9irqO.js} +1 -1
- package/dist/ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/follow-me/SKILL.md +5 -3
- package/dist/ui/dist/assets/channel-DPDY0XJP.js +0 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-C1IBh_GZ.js +0 -1
- package/dist/ui/dist/assets/index-8BehwQ7E.js +0 -134
- package/dist/ui/dist/assets/index-mC2OW2Av.css +0 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DzestpDE.js +0 -8
package/dist/src/plan.js
CHANGED
|
@@ -10,6 +10,12 @@ export function isCommentId(s) {
|
|
|
10
10
|
export function isConcernId(s) {
|
|
11
11
|
return typeof s === "string" && /^k_[0-9a-f]{8}$/.test(s);
|
|
12
12
|
}
|
|
13
|
+
export function isOfferId(s) {
|
|
14
|
+
return typeof s === "string" && /^o_[0-9a-f]{8}$/.test(s);
|
|
15
|
+
}
|
|
16
|
+
/** At most this many offers in one commit and in the whole plan; most plans need none. */
|
|
17
|
+
export const OFFERS_PER_COMMIT = 1;
|
|
18
|
+
export const OFFERS_PER_PLAN = 3;
|
|
13
19
|
export function isPlanVersion(n) {
|
|
14
20
|
return typeof n === "number" && Number.isInteger(n) && n >= 1;
|
|
15
21
|
}
|
|
@@ -29,6 +35,10 @@ export function isSeverity(s) {
|
|
|
29
35
|
function isConcernKind(s) {
|
|
30
36
|
return CONCERN_KINDS.some((x) => x === s);
|
|
31
37
|
}
|
|
38
|
+
const OFFER_KINDS = ["state-machine", "sequence", "diagram"];
|
|
39
|
+
function isOfferKind(s) {
|
|
40
|
+
return OFFER_KINDS.some((x) => x === s);
|
|
41
|
+
}
|
|
32
42
|
/** Narrows to an object whose named fields are still unknown, which holds for any object. */
|
|
33
43
|
export function isFields(v) {
|
|
34
44
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -123,7 +133,47 @@ function parseConcerns(v, onUnit) {
|
|
|
123
133
|
}
|
|
124
134
|
return out;
|
|
125
135
|
}
|
|
126
|
-
/**
|
|
136
|
+
/** An offer is { kind, label }; the label names the thing to draw. */
|
|
137
|
+
function parseOffer(v) {
|
|
138
|
+
if (!isFields(v))
|
|
139
|
+
return "expected an object";
|
|
140
|
+
if (!isOfferKind(v.kind))
|
|
141
|
+
return `kind: expected one of ${OFFER_KINDS.join(", ")}`;
|
|
142
|
+
if (typeof v.label !== "string" || v.label.trim() === "") {
|
|
143
|
+
return 'label: expected what to draw, like "Draw UploadButton as a state machine"';
|
|
144
|
+
}
|
|
145
|
+
return { kind: v.kind, label: v.label };
|
|
146
|
+
}
|
|
147
|
+
/** A stored offer as validation placed it: with its id and its unit. */
|
|
148
|
+
export function parsePlacedOffer(v) {
|
|
149
|
+
if (!isFields(v))
|
|
150
|
+
return "expected an object";
|
|
151
|
+
if (!isOfferId(v.id))
|
|
152
|
+
return "id: expected an offer id";
|
|
153
|
+
if (v.unit !== undefined && !(typeof v.unit === "string" && isUnitId(v.unit))) {
|
|
154
|
+
return "unit: expected a unit id";
|
|
155
|
+
}
|
|
156
|
+
const offer = parseOffer(v);
|
|
157
|
+
if (typeof offer === "string")
|
|
158
|
+
return offer;
|
|
159
|
+
return { ...offer, id: v.id, ...(v.unit === undefined ? {} : { unit: v.unit }) };
|
|
160
|
+
}
|
|
161
|
+
/** Absent, null and [] all mean no offers. */
|
|
162
|
+
function parseOffers(v) {
|
|
163
|
+
if (v === undefined || v === null)
|
|
164
|
+
return [];
|
|
165
|
+
if (!isArray(v))
|
|
166
|
+
return "expected an array";
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const [i, raw] of v.entries()) {
|
|
169
|
+
const offer = parseOffer(raw);
|
|
170
|
+
if (typeof offer === "string")
|
|
171
|
+
return `[${i}]: ${offer}`;
|
|
172
|
+
out.push(offer);
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
/** The optional narration, diagram, concerns and offers of a commit or unit entry, or what is wrong. */
|
|
127
177
|
function parseExtras(v, onUnit) {
|
|
128
178
|
const narration = parseText(v.narration);
|
|
129
179
|
if (narration === INVALID)
|
|
@@ -137,7 +187,13 @@ function parseExtras(v, onUnit) {
|
|
|
137
187
|
const concerns = parseConcerns(v.concerns, onUnit);
|
|
138
188
|
if (typeof concerns === "string")
|
|
139
189
|
return { field: "concerns", error: concerns };
|
|
140
|
-
|
|
190
|
+
const offers = parseOffers(v.offers);
|
|
191
|
+
if (typeof offers === "string")
|
|
192
|
+
return { field: "offers", error: offers };
|
|
193
|
+
return {
|
|
194
|
+
...withExtras(narration, diagram, concerns),
|
|
195
|
+
...(offers.length === 0 ? {} : { offers }),
|
|
196
|
+
};
|
|
141
197
|
}
|
|
142
198
|
function withText(narration, diagram) {
|
|
143
199
|
return {
|
|
@@ -160,15 +216,21 @@ function serializeExtras(x) {
|
|
|
160
216
|
return {
|
|
161
217
|
...withText(x.narration ?? null, x.diagram ?? null),
|
|
162
218
|
...(x.concerns === undefined ? {} : { concerns: x.concerns.map(serializeConcern) }),
|
|
219
|
+
...(x.offers === undefined
|
|
220
|
+
? {}
|
|
221
|
+
: { offers: x.offers.map((o) => ({ kind: o.kind, label: o.label })) }),
|
|
163
222
|
};
|
|
164
223
|
}
|
|
165
|
-
/**
|
|
224
|
+
/**
|
|
225
|
+
* A unit entry is "h_x:0-3" or { "unit": "h_x:0-3", "narration", "diagram", "concerns", "offers" }.
|
|
226
|
+
*/
|
|
166
227
|
function parseUnitEntry(u) {
|
|
167
228
|
if (typeof u === "string")
|
|
168
229
|
return parseUnitRef(u);
|
|
169
|
-
if (!isFields(u) ||
|
|
230
|
+
if (!isFields(u) ||
|
|
231
|
+
typeof u.unit !== "string") {
|
|
170
232
|
return {
|
|
171
|
-
error: 'expected a unit ref string or { "unit", "narration", "diagram", "concerns" }',
|
|
233
|
+
error: 'expected a unit ref string or { "unit", "narration", "diagram", "concerns", "offers" }',
|
|
172
234
|
};
|
|
173
235
|
}
|
|
174
236
|
const ref = parseUnitRef(u.unit);
|
|
@@ -298,7 +360,10 @@ export function serializePlanFile(file) {
|
|
|
298
360
|
message: c.message,
|
|
299
361
|
...(c.section === undefined ? {} : { section: c.section }),
|
|
300
362
|
...serializeExtras(c),
|
|
301
|
-
units: c.units.map((ref) => ref.narration === undefined &&
|
|
363
|
+
units: c.units.map((ref) => ref.narration === undefined &&
|
|
364
|
+
ref.diagram === undefined &&
|
|
365
|
+
ref.concerns === undefined &&
|
|
366
|
+
ref.offers === undefined
|
|
302
367
|
? formatUnitRef(ref)
|
|
303
368
|
: { unit: formatUnitRef(ref), ...serializeExtras(ref) }),
|
|
304
369
|
})),
|
|
@@ -547,8 +612,51 @@ function placeConcerns(diff, file, claims, issues) {
|
|
|
547
612
|
}
|
|
548
613
|
return placedPerCommit;
|
|
549
614
|
}
|
|
615
|
+
/** "o_" + 8 hex over the unit and the label: stable while the offer stays at its place. */
|
|
616
|
+
function deriveOfferId(unit, o) {
|
|
617
|
+
const hex = createHash("sha256")
|
|
618
|
+
.update(`offer\0${unit ?? ""}\0${o.label}`)
|
|
619
|
+
.digest("hex")
|
|
620
|
+
.slice(0, 8);
|
|
621
|
+
const id = `o_${hex}`;
|
|
622
|
+
if (!isOfferId(id))
|
|
623
|
+
throw new Error(`sha256 produced ${id}`);
|
|
624
|
+
return id;
|
|
625
|
+
}
|
|
626
|
+
/** Gives every offer its id: the commit's own first, then those of its unit refs in order. */
|
|
627
|
+
function placeOffers(file, issues) {
|
|
628
|
+
const ids = new Set();
|
|
629
|
+
return file.commits.map((commit, k) => {
|
|
630
|
+
const placed = [];
|
|
631
|
+
const add = (o, unit) => {
|
|
632
|
+
const id = deriveOfferId(unit, o);
|
|
633
|
+
if (ids.has(id)) {
|
|
634
|
+
issues.push({ kind: "bad-offer", commit: k, reason: `${id} repeats another offer` });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
ids.add(id);
|
|
638
|
+
placed.push({ ...o, id, ...(unit === null ? {} : { unit }) });
|
|
639
|
+
};
|
|
640
|
+
for (const o of commit.offers ?? [])
|
|
641
|
+
add(o, null);
|
|
642
|
+
for (const ref of commit.units)
|
|
643
|
+
for (const o of ref.offers ?? [])
|
|
644
|
+
add(o, ref.unit);
|
|
645
|
+
return placed;
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
/** At most OFFERS_PER_COMMIT in a commit, its units' included, and OFFERS_PER_PLAN in all. */
|
|
649
|
+
function offerLimits(file) {
|
|
650
|
+
const counts = file.commits.map((c) => (c.offers ?? []).length + c.units.reduce((n, r) => n + (r.offers ?? []).length, 0));
|
|
651
|
+
const issues = counts.flatMap((count, commit) => count > OFFERS_PER_COMMIT ? [{ kind: "too-many-offers", commit, count }] : []);
|
|
652
|
+
const total = counts.reduce((a, b) => a + b, 0);
|
|
653
|
+
if (total > OFFERS_PER_PLAN)
|
|
654
|
+
issues.push({ kind: "too-many-offers", commit: null, count: total });
|
|
655
|
+
return issues;
|
|
656
|
+
}
|
|
550
657
|
function project(diff, file, claims, issues) {
|
|
551
658
|
const concerns = placeConcerns(diff, file, claims, issues);
|
|
659
|
+
const offers = placeOffers(file, issues);
|
|
552
660
|
const { stateAt, tipOf } = projector(diff, claims.owners);
|
|
553
661
|
const present = new Map();
|
|
554
662
|
for (const f of diff.textFiles.values())
|
|
@@ -560,6 +668,7 @@ function project(diff, file, claims, issues) {
|
|
|
560
668
|
const commits = file.commits.map((commit, k) => {
|
|
561
669
|
const refs = claims.resolved[k] ?? [];
|
|
562
670
|
const mine = concerns[k] ?? [];
|
|
671
|
+
const offered = offers[k] ?? [];
|
|
563
672
|
const touched = [...new Set(refs.flatMap((r) => diff.units.get(r.unit)?.path ?? []))].sort();
|
|
564
673
|
const changes = touched.map((path) => {
|
|
565
674
|
const state = stateAt(path, k);
|
|
@@ -591,6 +700,7 @@ function project(diff, file, claims, issues) {
|
|
|
591
700
|
...(commit.section === undefined ? {} : { section: commit.section }),
|
|
592
701
|
...withText(commit.narration ?? null, commit.diagram ?? null),
|
|
593
702
|
...(mine.length === 0 ? {} : { concerns: mine }),
|
|
703
|
+
...(offered.length === 0 ? {} : { offers: offered }),
|
|
594
704
|
refs,
|
|
595
705
|
changes,
|
|
596
706
|
};
|
|
@@ -655,6 +765,7 @@ export function validatePlan(diff, draft, opts = {}) {
|
|
|
655
765
|
if (c.units.length === 0)
|
|
656
766
|
issues.push({ kind: "empty-commit", commit: k });
|
|
657
767
|
});
|
|
768
|
+
issues.push(...offerLimits(file));
|
|
658
769
|
const assignment = [];
|
|
659
770
|
const claims = claim(diff, file, assignment);
|
|
660
771
|
issues.push(...assignment);
|
|
@@ -677,6 +788,17 @@ export function validatePlan(diff, draft, opts = {}) {
|
|
|
677
788
|
if (length > LONG_TITLE)
|
|
678
789
|
warnings.push({ kind: "long-title", commit, length });
|
|
679
790
|
});
|
|
791
|
+
// Sections read top-down: story, steps, details, wiring. A later section never comes back.
|
|
792
|
+
let reached = null;
|
|
793
|
+
file.commits.forEach((c, commit) => {
|
|
794
|
+
if (c.section === undefined)
|
|
795
|
+
return;
|
|
796
|
+
if (reached !== null && SECTIONS.indexOf(c.section) < SECTIONS.indexOf(reached)) {
|
|
797
|
+
warnings.push({ kind: "section-order", commit, section: c.section, after: reached });
|
|
798
|
+
}
|
|
799
|
+
else
|
|
800
|
+
reached = c.section;
|
|
801
|
+
});
|
|
680
802
|
const asked = new Map(review?.comments.map((c) => [c.id, c.kind === "question"]));
|
|
681
803
|
for (const r of file.responses) {
|
|
682
804
|
const isQuestion = asked.get(r.comment);
|
package/dist/src/prompt.js
CHANGED
|
@@ -1018,6 +1018,28 @@ function indented(body) {
|
|
|
1018
1018
|
function said(head, body) {
|
|
1019
1019
|
return body.includes("\n") ? [`${head}:`, indented(body)] : [`${head}: ${body}`];
|
|
1020
1020
|
}
|
|
1021
|
+
/**
|
|
1022
|
+
* The one line that tells the agent a reviewer accepted an offer: where it sits, as the tip lines
|
|
1023
|
+
* of its hunk or its commit, its label, and the command that answers it.
|
|
1024
|
+
*/
|
|
1025
|
+
export function acceptedOfferLine(c, offer, ctx) {
|
|
1026
|
+
const found = (ctx?.commits ?? []).flatMap((commit, k) => (commit.offers ?? []).flatMap((o) => (o.id === offer ? [{ o, k }] : [])))[0];
|
|
1027
|
+
let place = describeAnchor(c.anchor, ctx);
|
|
1028
|
+
if (found !== undefined && found.o.unit === undefined)
|
|
1029
|
+
place = `commit ${found.k + 1}`;
|
|
1030
|
+
else if (found?.o.unit !== undefined) {
|
|
1031
|
+
const unit = ctx?.units.get(found.o.unit);
|
|
1032
|
+
let where = unit?.path ?? found.o.unit;
|
|
1033
|
+
if (unit?.kind === "hunk" && ctx !== null) {
|
|
1034
|
+
const from = tipStartOf(unit, ctx.units);
|
|
1035
|
+
const kept = unit.body.filter((row) => row.op !== "-").length;
|
|
1036
|
+
where = `${unit.path}:${from}-${from + Math.max(kept, 1) - 1}`;
|
|
1037
|
+
}
|
|
1038
|
+
place = `${where} (commit ${found.k + 1})`;
|
|
1039
|
+
}
|
|
1040
|
+
const label = (found?.o.label ?? c.body).trim().replace(/\.+$/, "");
|
|
1041
|
+
return `Reviewer accepted offer ${offer} on ${place}: ${label}. Reply with follow-me reply ${c.id} --diagram <file> [--map <file>].`;
|
|
1042
|
+
}
|
|
1021
1043
|
/**
|
|
1022
1044
|
* A new comment carries its short quote, since the agent has not seen it; everything else is one
|
|
1023
1045
|
* line naming its id, because the agent already has the comment it refers to.
|
|
@@ -1026,6 +1048,8 @@ function renderEvent(event, ctx) {
|
|
|
1026
1048
|
switch (event.type) {
|
|
1027
1049
|
case "comment": {
|
|
1028
1050
|
const { comment: c } = event;
|
|
1051
|
+
if (c.offer !== undefined)
|
|
1052
|
+
return [acceptedOfferLine(c, c.offer, ctx)];
|
|
1029
1053
|
const what = c.kind === "question" ? "question" : (c.severity ?? "comment");
|
|
1030
1054
|
const lines = [`- ${c.id} ${what} on ${describeAnchor(c.anchor, ctx)}`];
|
|
1031
1055
|
if (c.follows)
|
|
@@ -1047,6 +1071,8 @@ function renderEvent(event, ctx) {
|
|
|
1047
1071
|
}
|
|
1048
1072
|
case "resolved":
|
|
1049
1073
|
return [`- ${event.commentId} ${event.resolved ? "resolved" : "reopened"}`];
|
|
1074
|
+
case "offer-dismissed":
|
|
1075
|
+
return [`- the reviewer dismissed offer ${event.offer}`];
|
|
1050
1076
|
case "version":
|
|
1051
1077
|
return [`- plan v${event.version} is open`];
|
|
1052
1078
|
case "coverage":
|
|
@@ -1084,11 +1110,38 @@ export function renderShownComment(c, ctx, version) {
|
|
|
1084
1110
|
lines.push(...quoted.map((r) => ` ${r}`));
|
|
1085
1111
|
lines.push(...said(c.author === "agent" ? "agent" : "reviewer", c.body));
|
|
1086
1112
|
for (const reply of c.replies ?? []) {
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1113
|
+
const extras = [
|
|
1114
|
+
reply.diagram === undefined ? null : "a diagram",
|
|
1115
|
+
reply.map === undefined ? null : "a state map",
|
|
1116
|
+
];
|
|
1117
|
+
const parts = extras.filter((x) => x !== null);
|
|
1118
|
+
const head = `${reply.author} replied${parts.length === 0 ? "" : ` (with ${parts.join(" and ")})`}`;
|
|
1119
|
+
lines.push(...(reply.body.trim() === "" ? [head] : said(head, reply.body)));
|
|
1120
|
+
if (reply.map !== undefined)
|
|
1121
|
+
lines.push(...renderMap(reply.map).map((l) => ` ${l}`));
|
|
1089
1122
|
}
|
|
1090
1123
|
return `${lines.join("\n")}\n`;
|
|
1091
1124
|
}
|
|
1125
|
+
/** "src/a.ts:12-20" or "src/a.ts:12", a following place in the same file as ":30-41". */
|
|
1126
|
+
function formatLocs(locs) {
|
|
1127
|
+
return locs
|
|
1128
|
+
.map((loc, i) => {
|
|
1129
|
+
const lines = loc.start === loc.end ? `${loc.start}` : `${loc.start}-${loc.end}`;
|
|
1130
|
+
return i > 0 && locs[i - 1]?.path === loc.path ? `:${lines}` : `${utf8(loc.path)}:${lines}`;
|
|
1131
|
+
})
|
|
1132
|
+
.join(", ");
|
|
1133
|
+
}
|
|
1134
|
+
/** A state map, one line per state and per transition. */
|
|
1135
|
+
function renderMap(map) {
|
|
1136
|
+
return [
|
|
1137
|
+
...Object.entries(map.states).map(([name, locs]) => `state ${name}: ${formatLocs(locs)}`),
|
|
1138
|
+
...(map.transitions ?? []).map((t) => {
|
|
1139
|
+
const event = t.event === undefined ? "" : ` on ${t.event}`;
|
|
1140
|
+
const at = t.at.length === 0 ? "" : `: ${formatLocs(t.at)}`;
|
|
1141
|
+
return `${t.from} -> ${t.to}${event}${at}`;
|
|
1142
|
+
}),
|
|
1143
|
+
];
|
|
1144
|
+
}
|
|
1092
1145
|
/** A concern of the plan with the lines it is pinned to, as its commit writes them. */
|
|
1093
1146
|
export function renderShownConcern(concern, commit, ctx) {
|
|
1094
1147
|
const where = whereConcern(ctx, { concern, commit, severity: concern.severity, note: null });
|
package/dist/src/refs.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/** Lines quoted per ref, as comments quote their rows. */
|
|
2
|
+
export const QUOTE_LINES = 8;
|
|
3
|
+
const GRAMMAR = /^(.+):(\d+)(?:-(\d+))?(?:@(\d+|base))?$/;
|
|
4
|
+
/** `path:12`, `path:12-18`, `path:12@3`, `path:12-18@base`, or null when it is none of those. */
|
|
5
|
+
export function parseRefText(text) {
|
|
6
|
+
const m = GRAMMAR.exec(text);
|
|
7
|
+
const [, path, startRaw, endRaw, atRaw] = m ?? [];
|
|
8
|
+
if (path === undefined || startRaw === undefined)
|
|
9
|
+
return null;
|
|
10
|
+
const start = Number(startRaw);
|
|
11
|
+
const end = endRaw === undefined ? start : Number(endRaw);
|
|
12
|
+
const at = atRaw === undefined ? null : atRaw === "base" ? "base" : Number(atRaw);
|
|
13
|
+
return { text, path, start, end, at };
|
|
14
|
+
}
|
|
15
|
+
/** One key per place, so `a.ts:5` and `a.ts:5-5` share a number. */
|
|
16
|
+
export function refKey(spec) {
|
|
17
|
+
return `${spec.path}:${spec.start}-${spec.end}@${spec.at ?? "tip"}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A code span names a file when its path has a letter and a slash or a dot, so `10:30`,
|
|
21
|
+
* `localhost:3000` and `127.0.0.1:80` stay code.
|
|
22
|
+
*/
|
|
23
|
+
function looksLikeFile(path) {
|
|
24
|
+
return /[A-Za-z]/.test(path) && /[/.]/.test(path) && !/\s/.test(path);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The text of every inline code span outside fenced blocks, in order, read as CommonMark does: a
|
|
28
|
+
* run of backticks closes only on a run of the same length, and one space padding both ends is
|
|
29
|
+
* dropped.
|
|
30
|
+
*/
|
|
31
|
+
export function codeSpans(markdown) {
|
|
32
|
+
const spans = [];
|
|
33
|
+
let fence = null;
|
|
34
|
+
const prose = [];
|
|
35
|
+
for (const line of markdown.split("\n")) {
|
|
36
|
+
const opener = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
|
|
37
|
+
if (fence === null && opener !== undefined) {
|
|
38
|
+
fence = opener;
|
|
39
|
+
prose.push("");
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (fence !== null) {
|
|
43
|
+
const closer = /^ {0,3}(`{3,}|~{3,})\s*$/.exec(line)?.[1];
|
|
44
|
+
if (closer !== undefined && closer[0] === fence[0] && closer.length >= fence.length)
|
|
45
|
+
fence = null;
|
|
46
|
+
prose.push("");
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
prose.push(line);
|
|
50
|
+
}
|
|
51
|
+
// Spans never cross a blank line, which ends the paragraph.
|
|
52
|
+
for (const paragraph of prose.join("\n").split(/\n\s*\n/)) {
|
|
53
|
+
let i = 0;
|
|
54
|
+
while (i < paragraph.length) {
|
|
55
|
+
const c = paragraph[i];
|
|
56
|
+
if (c === "\\") {
|
|
57
|
+
i += 2;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (c !== "`") {
|
|
61
|
+
i += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
let run = 0;
|
|
65
|
+
while (paragraph[i + run] === "`")
|
|
66
|
+
run += 1;
|
|
67
|
+
const open = i + run;
|
|
68
|
+
let close = -1;
|
|
69
|
+
for (let j = open; j < paragraph.length;) {
|
|
70
|
+
if (paragraph[j] !== "`") {
|
|
71
|
+
j += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
let k = 0;
|
|
75
|
+
while (paragraph[j + k] === "`")
|
|
76
|
+
k += 1;
|
|
77
|
+
if (k === run) {
|
|
78
|
+
close = j;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
j += k;
|
|
82
|
+
}
|
|
83
|
+
if (close === -1) {
|
|
84
|
+
i = open;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
let text = paragraph.slice(open, close).replace(/\n/g, " ");
|
|
88
|
+
if (text.length > 2 && text.startsWith(" ") && text.endsWith(" ") && text.trim() !== "") {
|
|
89
|
+
text = text.slice(1, -1);
|
|
90
|
+
}
|
|
91
|
+
spans.push(text);
|
|
92
|
+
i = close + run;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return spans;
|
|
96
|
+
}
|
|
97
|
+
/** The refs a reply's markdown writes, each place once, in order of first appearance. */
|
|
98
|
+
export function refsIn(markdown) {
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
const specs = [];
|
|
101
|
+
for (const span of codeSpans(markdown)) {
|
|
102
|
+
const spec = parseRefText(span);
|
|
103
|
+
if (spec === null || !looksLikeFile(spec.path))
|
|
104
|
+
continue;
|
|
105
|
+
const key = refKey(spec);
|
|
106
|
+
if (seen.has(key))
|
|
107
|
+
continue;
|
|
108
|
+
seen.add(key);
|
|
109
|
+
specs.push(spec);
|
|
110
|
+
}
|
|
111
|
+
return specs;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Every line of a changed text file as it reads with the given change lines applied: none for
|
|
115
|
+
* the base, all for the tip, the plan's first n commits for commit n. Null when the file does
|
|
116
|
+
* not exist then.
|
|
117
|
+
*/
|
|
118
|
+
export function linesWith(file, applied) {
|
|
119
|
+
const changes = file.hunks.flatMap((h) => h.body.flatMap((l) => (l.op === " " ? [] : [applied(h, l)])));
|
|
120
|
+
const any = changes.some((c) => c);
|
|
121
|
+
const all = changes.every((c) => c);
|
|
122
|
+
if (file.base === null ? !any : file.tip === null && all)
|
|
123
|
+
return null;
|
|
124
|
+
const base = file.base?.lines ?? [];
|
|
125
|
+
const out = [];
|
|
126
|
+
let b = 0;
|
|
127
|
+
let t = 0;
|
|
128
|
+
const keep = (text) => {
|
|
129
|
+
b += 1;
|
|
130
|
+
t += 1;
|
|
131
|
+
out.push({ text, tip: t, base: b });
|
|
132
|
+
};
|
|
133
|
+
for (const hunk of file.hunks) {
|
|
134
|
+
while (b < hunk.oldStart)
|
|
135
|
+
keep(base[b] ?? "");
|
|
136
|
+
for (const line of hunk.body) {
|
|
137
|
+
if (line.op === " ")
|
|
138
|
+
keep(line.text);
|
|
139
|
+
else if (line.op === "-") {
|
|
140
|
+
b += 1;
|
|
141
|
+
if (!applied(hunk, line))
|
|
142
|
+
out.push({ text: line.text, tip: null, base: b });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
t += 1;
|
|
146
|
+
if (applied(hunk, line))
|
|
147
|
+
out.push({ text: line.text, tip: t, base: null });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
while (b < base.length)
|
|
152
|
+
keep(base[b] ?? "");
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/** Whether the plan's first n commits take this change line. */
|
|
156
|
+
export function takenBy(commits) {
|
|
157
|
+
return (hunk, line) => commits.some((c) => c.refs.some((r) => r.unit === hunk.id &&
|
|
158
|
+
(r.select.kind === "rest" || r.select.changes.includes(line.change))));
|
|
159
|
+
}
|
|
160
|
+
/** The repo path a typed path names: its UTF-8 as the latin1 bytes git prints. */
|
|
161
|
+
export function repoPathOfTyped(typed) {
|
|
162
|
+
return Buffer.from(typed, "utf8").toString("latin1");
|
|
163
|
+
}
|
|
164
|
+
function utf8(latin1) {
|
|
165
|
+
return Buffer.from(latin1, "latin1").toString("utf8");
|
|
166
|
+
}
|
|
167
|
+
function spanOf(ns) {
|
|
168
|
+
if (ns.length === 0)
|
|
169
|
+
return null;
|
|
170
|
+
return { start: Math.min(...ns), end: Math.max(...ns) };
|
|
171
|
+
}
|
|
172
|
+
function versionLabel(at) {
|
|
173
|
+
return at === null ? "at the tip" : at === "base" ? "in the base" : `in commit ${at}`;
|
|
174
|
+
}
|
|
175
|
+
/** A written ref resolved to its lines and quote, numbered n, or the reason it does not resolve. */
|
|
176
|
+
export function resolveRef(spec, n, source) {
|
|
177
|
+
const { text, start, end, at } = spec;
|
|
178
|
+
if (start < 1)
|
|
179
|
+
return `${text}: lines count from 1`;
|
|
180
|
+
if (end < start)
|
|
181
|
+
return `${text} ends before it starts`;
|
|
182
|
+
if (typeof at === "number" && (at < 1 || at > source.commits.length)) {
|
|
183
|
+
const count = source.commits.length;
|
|
184
|
+
return `${text}: plan v${source.version} has ${count} commit${count === 1 ? "" : "s"}`;
|
|
185
|
+
}
|
|
186
|
+
const path = repoPathOfTyped(spec.path);
|
|
187
|
+
const where = versionLabel(at);
|
|
188
|
+
let lines;
|
|
189
|
+
let repoPath;
|
|
190
|
+
const file = [...source.textFiles.values()].find((f) => f.path === path);
|
|
191
|
+
const plain = [...source.unchanged].find(([p]) => p === path);
|
|
192
|
+
if (file !== undefined) {
|
|
193
|
+
const applied = at === null ? () => true : at === "base" ? () => false : takenBy(source.commits.slice(0, at));
|
|
194
|
+
const found = linesWith(file, applied);
|
|
195
|
+
if (found === null)
|
|
196
|
+
return `${text}: the file does not exist ${where}`;
|
|
197
|
+
lines = found;
|
|
198
|
+
repoPath = file.path;
|
|
199
|
+
}
|
|
200
|
+
else if (source.otherChanged.has(path)) {
|
|
201
|
+
return `${text}: not a text file`;
|
|
202
|
+
}
|
|
203
|
+
else if (plain !== undefined) {
|
|
204
|
+
lines = plain[1].map((t, i) => ({ text: t, tip: i + 1, base: i + 1 }));
|
|
205
|
+
repoPath = plain[0];
|
|
206
|
+
}
|
|
207
|
+
else
|
|
208
|
+
return `${text}: no such file at the tip`;
|
|
209
|
+
if (end > lines.length) {
|
|
210
|
+
return `${text} is past the end of the file ${where} (${lines.length} lines)`;
|
|
211
|
+
}
|
|
212
|
+
const picked = lines.slice(start - 1, end);
|
|
213
|
+
const kept = picked.flatMap((l) => (l.tip === null ? [] : [l.tip]));
|
|
214
|
+
const removed = picked.flatMap((l) => (l.tip === null && l.base !== null ? [l.base] : []));
|
|
215
|
+
return {
|
|
216
|
+
n,
|
|
217
|
+
path: repoPath,
|
|
218
|
+
start,
|
|
219
|
+
end,
|
|
220
|
+
at,
|
|
221
|
+
quote: picked.slice(0, QUOTE_LINES).map((l) => utf8(l.text).replace(/\r?\n$/, "")),
|
|
222
|
+
tip: source.tip,
|
|
223
|
+
lines: spanOf(kept),
|
|
224
|
+
old: spanOf(removed),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/** Every ref of a body resolved, or every reason one does not, so one fix round is enough. */
|
|
228
|
+
export function resolveRefs(specs, source) {
|
|
229
|
+
const refs = [];
|
|
230
|
+
const problems = [];
|
|
231
|
+
specs.forEach((spec, i) => {
|
|
232
|
+
const ref = resolveRef(spec, i + 1, source);
|
|
233
|
+
if (typeof ref === "string")
|
|
234
|
+
problems.push(ref);
|
|
235
|
+
else
|
|
236
|
+
refs.push(ref);
|
|
237
|
+
});
|
|
238
|
+
return { refs, problems };
|
|
239
|
+
}
|