@shiyuphan/follow-me 0.8.0 → 0.9.1
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 +8 -3
- package/dist/src/cli.js +204 -53
- package/dist/src/comments.js +95 -7
- package/dist/src/coverage.js +173 -38
- package/dist/src/host.js +1 -0
- package/dist/src/plan.js +44 -22
- package/dist/src/prompt.js +58 -17
- package/dist/src/refs.js +239 -0
- package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-Bp14ygmz.js → abnfDiagram-WEYOIA3C-BWgk01ve.js} +1 -1
- package/dist/ui/dist/assets/{arc-DUR6uzhI.js → arc-Cg_Qb5c4.js} +1 -1
- package/dist/ui/dist/assets/{architecture-WOLXFQ4H-CyfnP65O.js → architecture-WOLXFQ4H-EdfV1etv.js} +1 -1
- package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-dJTicIk2.js → architectureDiagram-ZHVYKT53-Dz-gytQU.js} +1 -1
- package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-BYiqh5Ib.js → blockDiagram-RJAE7QA3-B7Q5KhYC.js} +1 -1
- package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-DHHPQ7xE.js → c4Diagram-XDPPPE54-Cq9IvGR1.js} +1 -1
- package/dist/ui/dist/assets/channel-Da0YvuQF.js +1 -0
- package/dist/ui/dist/assets/{chunk-2E7WHVMD-BODm_awL.js → chunk-2E7WHVMD-BTJB-4C8.js} +1 -1
- package/dist/ui/dist/assets/{chunk-4EMNKR2T-CpZfjf2T.js → chunk-4EMNKR2T-BILthc2j.js} +1 -1
- package/dist/ui/dist/assets/{chunk-55XCU76K-CoP2pJTQ.js → chunk-55XCU76K-DH3fYrJH.js} +1 -1
- package/dist/ui/dist/assets/{chunk-7S6XSTFO-CaQcUvXY.js → chunk-7S6XSTFO-DDIiBvc2.js} +1 -1
- package/dist/ui/dist/assets/{chunk-AQDTSKZB-BZ9Czvbu.js → chunk-AQDTSKZB-b1EeJ3gb.js} +1 -1
- package/dist/ui/dist/assets/{chunk-DSNLSHJZ-CNjUo110.js → chunk-DSNLSHJZ-DZZ635s5.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HAGFOTED-BTmBE_D3.js → chunk-HAGFOTED-DhhrHTIh.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HNE4Q36P-D2cDuT0C.js → chunk-HNE4Q36P-B8wvhc2e.js} +1 -1
- package/dist/ui/dist/assets/{chunk-NQOKRVIF-AZcemFgG.js → chunk-NQOKRVIF-CLtGMwDx.js} +1 -1
- package/dist/ui/dist/assets/{chunk-UHW7PFBP-BszwjUqG.js → chunk-UHW7PFBP-B9fVLGl0.js} +1 -1
- package/dist/ui/dist/assets/{chunk-VQ5W62QI-vFSenYSe.js → chunk-VQ5W62QI-CgPvvMAv.js} +1 -1
- package/dist/ui/dist/assets/{chunk-WQQL4CDK-DZR6is1Q.js → chunk-WQQL4CDK-DdqKu6R9.js} +1 -1
- package/dist/ui/dist/assets/{chunk-XXDRQBXY-CACFh-si.js → chunk-XXDRQBXY-GlM07vbp.js} +1 -1
- package/dist/ui/dist/assets/{chunk-YZO67HFG-DRRTjGuW.js → chunk-YZO67HFG-YqT6M0W9.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZAUX2LTX-Bxuy-Roa.js → chunk-ZAUX2LTX-DqHK51Gc.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZCHMQSOR-rYXk__1o.js → chunk-ZCHMQSOR-DEeXkjWz.js} +1 -1
- package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-RiF44tTz.js → classDiagram-v2-4MYWR3UA-BcKsX4up.js} +1 -1
- package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-0oiQoThS.js → cose-bilkent-JH36ORCC-fu9djgYc.js} +1 -1
- package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-BAssFK5K.js → cynefin-EF2NZ3EQ-CFJASBsJ.js} +1 -1
- package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-Bmd05bak.js → cynefinDiagram-WF24UDRP-BVJD_n0g.js} +1 -1
- package/dist/ui/dist/assets/{dagre-NK4GIZVX-QJkLjPxj.js → dagre-NK4GIZVX-Cx8pjz9O.js} +1 -1
- package/dist/ui/dist/assets/{diagram-CBK3MWTC-PSaSiYXz.js → diagram-CBK3MWTC-CVIkEZ1X.js} +1 -1
- package/dist/ui/dist/assets/{diagram-I5XIIV2D-BmU5X_hr.js → diagram-I5XIIV2D-2rxpF8BZ.js} +1 -1
- package/dist/ui/dist/assets/{diagram-ODIL2UCW-AAV39kUu.js → diagram-ODIL2UCW-NJfGHnLk.js} +1 -1
- package/dist/ui/dist/assets/{diagram-PFBEJUXE-DWlV-oxd.js → diagram-PFBEJUXE-DZHVJSeM.js} +1 -1
- package/dist/ui/dist/assets/{diagram-RJA3I4VV-D8wHRCNX.js → diagram-RJA3I4VV-CQAfOObI.js} +1 -1
- package/dist/ui/dist/assets/{diagram-TYUNFSNR-B_JQEsNY.js → diagram-TYUNFSNR-DGGaMOl6.js} +1 -1
- package/dist/ui/dist/assets/{dist-DxQIK_Ay.js → dist-CW-4NrUq.js} +1 -1
- package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-D83qV8mv.js → ebnfDiagram-V4P3TTPT-92pb2Fcg.js} +1 -1
- package/dist/ui/dist/assets/{elk-QONTBCTQ-BKFAKVSD.js → elk-QONTBCTQ-vu0Z4rYG.js} +1 -1
- package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-CHYRd4aM.js → erDiagram-KYQIGQAA-BXnsRzCQ.js} +1 -1
- package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-BDJDv3Zf.js → eventmodeling-K75KTNOO-bSr88MT1.js} +1 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-CUH45H9n.js +1 -0
- package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-gTHjYkFj.js → ganttDiagram-R7GPNTJY-BQzVkGbj.js} +1 -1
- package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-D4cWkxKB.js → gitGraph-VSP46ZUC-BegblPQi.js} +1 -1
- package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-D3M9wYMr.js → gitGraphDiagram-PDYL7XHY-BaGlGpEX.js} +1 -1
- package/dist/ui/dist/assets/index-D74Skifa.css +1 -0
- package/dist/ui/dist/assets/index-D8ErIacA.js +136 -0
- package/dist/ui/dist/assets/{info-OHQRW6UA-B9y0ksVq.js → info-OHQRW6UA-D3HVGJDc.js} +1 -1
- package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-s4FMwb8r.js → infoDiagram-MVDRKAX5-Ckcp-D0C.js} +1 -1
- package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-CnQdxybf.js → ishikawaDiagram-HPEHV2GE-CWhd0CTS.js} +1 -1
- package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-I-9OpDWY.js → journeyDiagram-BOSHW7O7-CrH5cFXY.js} +1 -1
- package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-DqUbIdH4.js → kanban-definition-FNSA4HC5-CpORMTDZ.js} +1 -1
- package/dist/ui/dist/assets/{line-BELSui4d.js → line-B3El9R0Z.js} +1 -1
- package/dist/ui/dist/assets/{linear-77hmn2iQ.js → linear-DLfthkbB.js} +1 -1
- package/dist/ui/dist/assets/{mermaid-parser.core-Bxa71wW-.js → mermaid-parser.core-QtQjLaoK.js} +3 -3
- package/dist/ui/dist/assets/{mermaid.core-DBI9F4bh.js → mermaid.core-C2DGgMSc.js} +4 -4
- package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-BxXMOapP.js → mindmap-definition-6OY57UG3-CW1lSXnX.js} +1 -1
- package/dist/ui/dist/assets/{packet-JDAUHWVQ-DTT77cQh.js → packet-JDAUHWVQ-Dhu_m_17.js} +1 -1
- package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-HzrOQPT5.js → pegDiagram-ECIUJNBX-BGOhbzN4.js} +1 -1
- package/dist/ui/dist/assets/{pie-XZMESJXO-iHwL1Qw-.js → pie-XZMESJXO-Citt3cMX.js} +1 -1
- package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-DZ1tLVjw.js → pieDiagram-GNMPY74T-DPVZikvz.js} +1 -1
- package/dist/ui/dist/assets/{prism-core-NtO0YF9C.js → prism-core-YgwsYh9d.js} +1 -1
- package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-BjTi9H5L.js → quadrantDiagram-D57K4MC3-Bwye1B9e.js} +1 -1
- package/dist/ui/dist/assets/{radar-ABXABTNO-D2hYgXSB.js → radar-ABXABTNO-DRUb4G6V.js} +1 -1
- package/dist/ui/dist/assets/{railroad-I3PHUGI6-DpakXN74.js → railroad-I3PHUGI6-x3Ug2mxX.js} +1 -1
- package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-B8ZFR3iy.js → railroad-abnf-LNEFI6M7-Dw14XhkZ.js} +1 -1
- package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-S1XUyTsC.js → railroad-ebnf-DWYD2UWJ-AwoWTWgL.js} +1 -1
- package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-BbeHC_6K.js → railroad-peg-JR7BVNR7-U7MXfciE.js} +1 -1
- package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-fd-2iyBo.js → railroadDiagram-Y72NRBL7-BxWlCZYs.js} +1 -1
- package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-B9-HHKH-.js → requirementDiagram-EQYSW5RA-TPmnZguV.js} +1 -1
- package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-CeQN1ZEG.js → sankeyDiagram-SY24JHO5-nPGftpl5.js} +1 -1
- package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-Vff7tx4t.js → sequenceDiagram-BLEHNHQU-BdctgaWa.js} +1 -1
- package/dist/ui/dist/assets/{src-D5-ZzLwt.js → src-zq235FjH.js} +1 -1
- package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-Bzn_ma9c.js → stateDiagram-v2-A6IESKWZ-BK_E6kPg.js} +1 -1
- package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-CevqLchV.js → swimlanes-B3ENRKFQ-BY50Ne4l.js} +1 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-Ctw23I3i.js +8 -0
- package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-D9jSDGJI.js → timeline-definition-JXYKGLGB-Bg8AXJ2P.js} +1 -1
- package/dist/ui/dist/assets/{treeView-D4JQ5SDB-CoXKj11m.js → treeView-D4JQ5SDB-B4hz6M4l.js} +1 -1
- package/dist/ui/dist/assets/{treemap-SAJKECNS-cHsansGe.js → treemap-SAJKECNS-e_Vc97Ox.js} +1 -1
- package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-CW9i3bW6.js → usecaseDiagram-WPOLBNNX-Butu_LCX.js} +1 -1
- package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-DuU3qEKJ.js → vennDiagram-Q3Y3IDCW-BGIyxlbf.js} +1 -1
- package/dist/ui/dist/assets/{wardley-7MLQ67FV-dl9RgHUI.js → wardley-7MLQ67FV-Btv95mqq.js} +1 -1
- package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-BOXhrK-a.js → wardleyDiagram-IVAIDKWA-DwTKExJ4.js} +1 -1
- package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-CLQNGrh4.js → xychartDiagram-4URNHDU4-UmlA5uxc.js} +1 -1
- package/dist/ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/follow-me/SKILL.md +8 -8
- package/dist/ui/dist/assets/channel-CY2cQobm.js +0 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Bke3CfUH.js +0 -1
- package/dist/ui/dist/assets/index-BUOFihod.js +0 -134
- package/dist/ui/dist/assets/index-DLNJT0VS.css +0 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-Bkv8hAmN.js +0 -8
package/dist/src/comments.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseLoc } from "./coverage.js";
|
|
2
2
|
import { isRepoPath, isSha, isUnitId } from "./diff.js";
|
|
3
3
|
import { diagramProblem, isArray, isCommentId, isConcernId, isFields, isOfferId, isPlanVersion, isSeverity, parsePlacedConcern, } from "./plan.js";
|
|
4
|
+
import { parseRefText } from "./refs.js";
|
|
4
5
|
/**
|
|
5
6
|
* The wire formats of review state, parsed once at the boundary: comments, replies, concern
|
|
6
7
|
* decisions, the review file, its autosaved draft and the events a hosted session logs. Every
|
|
@@ -49,26 +50,42 @@ function parseAnchor(v) {
|
|
|
49
50
|
}
|
|
50
51
|
return { kind: "hunk", unit: v.unit, commit, changes, offsets };
|
|
51
52
|
}
|
|
53
|
+
/** A place written as a ref string (`path:a-b`, tip lines) or as { path, start, end }. */
|
|
54
|
+
function parsePlace(raw) {
|
|
55
|
+
if (typeof raw !== "string")
|
|
56
|
+
return parseLoc(raw);
|
|
57
|
+
const spec = parseRefText(raw);
|
|
58
|
+
if (spec === null)
|
|
59
|
+
return null;
|
|
60
|
+
if (spec.at !== null)
|
|
61
|
+
return `${JSON.stringify(raw)}: a map is in tip lines; drop the @`;
|
|
62
|
+
return parseLoc({ path: spec.path, start: spec.start, end: spec.end });
|
|
63
|
+
}
|
|
52
64
|
function parseLocs(v, at, needed) {
|
|
53
|
-
const what = `${at}: expected a list of { path, start, end }${needed ? " with at least one place" : ""}`;
|
|
65
|
+
const what = `${at}: expected a list of "path:a-b" or { path, start, end }${needed ? " with at least one place" : ""}`;
|
|
54
66
|
if (!isArray(v) || (needed && v.length === 0))
|
|
55
67
|
return what;
|
|
56
68
|
const out = [];
|
|
57
69
|
for (const [i, raw] of v.entries()) {
|
|
58
|
-
const loc =
|
|
70
|
+
const loc = parsePlace(raw);
|
|
59
71
|
if (loc === null)
|
|
60
|
-
return `${at}[${i}]: expected { path, start, end } with 1 <= start <= end`;
|
|
72
|
+
return `${at}[${i}]: expected "path:a-b" or { path, start, end } with 1 <= start <= end`;
|
|
73
|
+
if (typeof loc === "string")
|
|
74
|
+
return `${at}[${i}]: ${loc}`;
|
|
61
75
|
out.push(loc);
|
|
62
76
|
}
|
|
63
77
|
return out;
|
|
64
78
|
}
|
|
65
79
|
const TRANSITION_ENDS = ["from", "to"];
|
|
80
|
+
/** A stateDiagram's start and end marker. */
|
|
81
|
+
const START_END = "[*]";
|
|
66
82
|
function escapeRegExp(text) {
|
|
67
83
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
68
84
|
}
|
|
69
85
|
/**
|
|
70
86
|
* Boundary parse of a reply's state map. Every state has at least one place, every transition
|
|
71
|
-
* names states of the map, and with a stateDiagram every state name appears in it.
|
|
87
|
+
* names states of the map, and with a stateDiagram every state name appears in it. A transition
|
|
88
|
+
* to or from `[*]` is dropped: the diagram marks the start and end. The reason
|
|
72
89
|
* names the bad entry, as map.states.<name>[i] or map.transitions[i].<field>.
|
|
73
90
|
*/
|
|
74
91
|
export function parseStateMap(v, diagram) {
|
|
@@ -95,6 +112,10 @@ export function parseStateMap(v, diagram) {
|
|
|
95
112
|
const at = `map.transitions[${i}]`;
|
|
96
113
|
if (!isFields(raw))
|
|
97
114
|
return `${at}: expected { from, to, event, at }`;
|
|
115
|
+
// The diagram's [*] already marks where the machine starts and ends; a move to or from it
|
|
116
|
+
// has no code of its own, so it is dropped rather than refused.
|
|
117
|
+
if (raw.from === START_END || raw.to === START_END)
|
|
118
|
+
continue;
|
|
98
119
|
for (const end of TRANSITION_ENDS) {
|
|
99
120
|
const state = raw[end];
|
|
100
121
|
if (typeof state !== "string" || !named.has(state)) {
|
|
@@ -148,6 +169,55 @@ export function mapOffTip(map, lines, show = (path) => path) {
|
|
|
148
169
|
...(map.transitions ?? []).flatMap((t, i) => t.at.flatMap((loc, j) => check(loc, `map.transitions[${i}].at[${j}]`))),
|
|
149
170
|
];
|
|
150
171
|
}
|
|
172
|
+
function parseSpan(v) {
|
|
173
|
+
if (v === null)
|
|
174
|
+
return null;
|
|
175
|
+
if (!isFields(v) || !isLine(v.start) || !isLine(v.end) || v.end < v.start) {
|
|
176
|
+
return "expected null or { start, end } with 1 <= start <= end";
|
|
177
|
+
}
|
|
178
|
+
return { start: v.start, end: v.end };
|
|
179
|
+
}
|
|
180
|
+
function parseCodeRef(v) {
|
|
181
|
+
if (!isFields(v))
|
|
182
|
+
return "expected an object";
|
|
183
|
+
const { n, path, start, end, at, quote, tip } = v;
|
|
184
|
+
if (!isLine(n))
|
|
185
|
+
return "n is not a number from 1";
|
|
186
|
+
if (typeof path !== "string" || !isRepoPath(path))
|
|
187
|
+
return "path is not a repo path";
|
|
188
|
+
if (!isLine(start) || !isLine(end) || end < start)
|
|
189
|
+
return "expected 1 <= start <= end";
|
|
190
|
+
if (at !== null && at !== "base" && !isLine(at))
|
|
191
|
+
return 'at is not a commit, "base" or null';
|
|
192
|
+
if (!isArray(quote) || !quote.every(isString))
|
|
193
|
+
return "quote is not a list of strings";
|
|
194
|
+
if (typeof tip !== "string" || !isSha(tip))
|
|
195
|
+
return "tip is not a commit sha";
|
|
196
|
+
const lines = parseSpan(v.lines);
|
|
197
|
+
const old = parseSpan(v.old);
|
|
198
|
+
if (typeof lines === "string")
|
|
199
|
+
return `lines: ${lines}`;
|
|
200
|
+
if (typeof old === "string")
|
|
201
|
+
return `old: ${old}`;
|
|
202
|
+
return { n, path, start, end, at, quote: quote.filter(isString), tip, lines, old };
|
|
203
|
+
}
|
|
204
|
+
/** refs as stored beside an agent's words: absent, or a list numbered 1, 2, 3 in order. */
|
|
205
|
+
function parseCodeRefs(v) {
|
|
206
|
+
if (v === undefined)
|
|
207
|
+
return [];
|
|
208
|
+
if (!isArray(v))
|
|
209
|
+
return "refs is not a list";
|
|
210
|
+
const refs = [];
|
|
211
|
+
for (const [i, raw] of v.entries()) {
|
|
212
|
+
const ref = parseCodeRef(raw);
|
|
213
|
+
if (typeof ref === "string")
|
|
214
|
+
return `refs[${i}]: ${ref}`;
|
|
215
|
+
if (ref.n !== i + 1)
|
|
216
|
+
return `refs[${i}]: n is not ${i + 1}`;
|
|
217
|
+
refs.push(ref);
|
|
218
|
+
}
|
|
219
|
+
return refs;
|
|
220
|
+
}
|
|
151
221
|
/** A stored reply. Replies a client posts go through parseNewReply, which mints id and time. */
|
|
152
222
|
export function parseReply(v) {
|
|
153
223
|
if (!isFields(v)) {
|
|
@@ -165,32 +235,43 @@ export function parseReply(v) {
|
|
|
165
235
|
const map = v.map === undefined ? undefined : parseStateMap(v.map, v.diagram);
|
|
166
236
|
if (typeof map === "string")
|
|
167
237
|
return map;
|
|
238
|
+
const refs = parseCodeRefs(v.refs);
|
|
239
|
+
if (typeof refs === "string")
|
|
240
|
+
return refs;
|
|
168
241
|
return {
|
|
169
242
|
id: v.id,
|
|
170
243
|
author: v.author,
|
|
171
244
|
body: v.body,
|
|
172
245
|
...(v.diagram === undefined ? {} : { diagram: v.diagram }),
|
|
173
246
|
...(map === undefined ? {} : { map }),
|
|
247
|
+
...(refs.length === 0 ? {} : { refs }),
|
|
174
248
|
createdAt: v.createdAt,
|
|
175
249
|
};
|
|
176
250
|
}
|
|
177
251
|
/**
|
|
178
252
|
* The body of POST .../replies: who speaks, what, and an optional Mermaid diagram. The body may be
|
|
179
|
-
* empty only when the diagram is the answer.
|
|
253
|
+
* empty only when the diagram is the answer. Only the agent's words carry refs.
|
|
180
254
|
*/
|
|
181
255
|
export function parseNewReply(v) {
|
|
182
|
-
if (!isFields(v))
|
|
256
|
+
if (!isFields(v)) {
|
|
183
257
|
return "the reply is not an object";
|
|
258
|
+
}
|
|
184
259
|
if (v.author !== "agent" && v.author !== "reviewer")
|
|
185
260
|
return 'author is not "agent" or "reviewer"';
|
|
186
261
|
if (typeof v.body !== "string")
|
|
187
262
|
return "body is not a string";
|
|
263
|
+
const refs = parseCodeRefs(v.refs);
|
|
264
|
+
if (typeof refs === "string")
|
|
265
|
+
return refs;
|
|
266
|
+
if (refs.length > 0 && v.author !== "agent")
|
|
267
|
+
return "refs are only for the agent's words";
|
|
268
|
+
const withRefs = refs.length === 0 ? {} : { refs };
|
|
188
269
|
if (v.diagram === undefined) {
|
|
189
270
|
if (v.map !== undefined)
|
|
190
271
|
return "map needs the diagram it ties to code";
|
|
191
272
|
if (v.body.trim() === "")
|
|
192
273
|
return "body is empty; a reply needs words or a diagram";
|
|
193
|
-
return { author: v.author, body: v.body };
|
|
274
|
+
return { author: v.author, body: v.body, ...withRefs };
|
|
194
275
|
}
|
|
195
276
|
if (typeof v.diagram !== "string")
|
|
196
277
|
return "diagram is not a string";
|
|
@@ -205,6 +286,7 @@ export function parseNewReply(v) {
|
|
|
205
286
|
body: v.body,
|
|
206
287
|
diagram: v.diagram,
|
|
207
288
|
...(map === undefined ? {} : { map }),
|
|
289
|
+
...withRefs,
|
|
208
290
|
};
|
|
209
291
|
}
|
|
210
292
|
function isString(v) {
|
|
@@ -262,6 +344,11 @@ export function parseComment(v) {
|
|
|
262
344
|
return "offer is only for a reviewer's question";
|
|
263
345
|
}
|
|
264
346
|
}
|
|
347
|
+
const refs = parseCodeRefs(v.refs);
|
|
348
|
+
if (typeof refs === "string")
|
|
349
|
+
return refs;
|
|
350
|
+
if (refs.length > 0 && v.author !== "agent")
|
|
351
|
+
return "refs are only for an agent's comment";
|
|
265
352
|
const replies = parseList(v.replies, parseReply, "replies");
|
|
266
353
|
if (typeof replies === "string")
|
|
267
354
|
return replies;
|
|
@@ -281,6 +368,7 @@ export function parseComment(v) {
|
|
|
281
368
|
...(replies.length === 0 ? {} : { replies }),
|
|
282
369
|
...(v.author === "agent" ? { author: "agent" } : {}),
|
|
283
370
|
...(v.diagram === undefined ? {} : { diagram: v.diagram }),
|
|
371
|
+
...(refs.length === 0 ? {} : { refs }),
|
|
284
372
|
...(offer === undefined ? {} : { offer }),
|
|
285
373
|
};
|
|
286
374
|
}
|
package/dist/src/coverage.js
CHANGED
|
@@ -15,9 +15,12 @@ export class LcovError extends Error {
|
|
|
15
15
|
const RECORD = /^([A-Z][A-Z0-9_]*):(.*)$/;
|
|
16
16
|
const DA = /^([1-9][0-9]*),([0-9]+)(?:,.*)?$/;
|
|
17
17
|
const BRDA = /^([1-9][0-9]*),([^,]+),(.+),(-|[0-9]+)$/;
|
|
18
|
+
/** FN:<start>,<name>, or lcov 2's FN:<start>,<end>,<name>. */
|
|
19
|
+
const FN = /^([1-9][0-9]*),(?:([1-9][0-9]*),)?(.+)$/;
|
|
18
20
|
/**
|
|
19
21
|
* Reads lcov's tracefile format: TN names the test of the records after it, SF opens a file, DA
|
|
20
|
-
* and BRDA give line and branch hits, end_of_record closes it. Other
|
|
22
|
+
* and BRDA give line and branch hits, FN a function's place, end_of_record closes it. Other
|
|
23
|
+
* records are skipped.
|
|
21
24
|
*/
|
|
22
25
|
export function parseLcov(text) {
|
|
23
26
|
const records = [];
|
|
@@ -50,10 +53,10 @@ export function parseLcov(text) {
|
|
|
50
53
|
close();
|
|
51
54
|
if (value === "")
|
|
52
55
|
throw new LcovError(n, "SF needs a path");
|
|
53
|
-
open = { path: value, test, lines: new Map(), branches: new Map() };
|
|
56
|
+
open = { path: value, test, lines: new Map(), branches: new Map(), functions: [] };
|
|
54
57
|
continue;
|
|
55
58
|
}
|
|
56
|
-
if (key !== "DA" && key !== "BRDA")
|
|
59
|
+
if (key !== "DA" && key !== "BRDA" && key !== "FN")
|
|
57
60
|
continue;
|
|
58
61
|
if (open === null)
|
|
59
62
|
throw new LcovError(n, `${key} comes before any SF`);
|
|
@@ -65,15 +68,27 @@ export function parseLcov(text) {
|
|
|
65
68
|
open.lines.set(line, (open.lines.get(line) ?? 0) + Number(da[2]));
|
|
66
69
|
continue;
|
|
67
70
|
}
|
|
71
|
+
if (key === "FN") {
|
|
72
|
+
const fn = FN.exec(value);
|
|
73
|
+
if (fn === null)
|
|
74
|
+
throw new LcovError(n, `FN needs <line>,<name>, got "${value}"`);
|
|
75
|
+
const start = Number(fn[1]);
|
|
76
|
+
const end = fn[2] === undefined ? undefined : Number(fn[2]);
|
|
77
|
+
const name = fn[3] ?? "";
|
|
78
|
+
if (end !== undefined && end < start)
|
|
79
|
+
throw new LcovError(n, "FN ends before it starts");
|
|
80
|
+
open.functions.push(end === undefined ? { name, start } : { name, start, end });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
68
83
|
const br = BRDA.exec(value);
|
|
69
84
|
if (br === null) {
|
|
70
85
|
throw new LcovError(n, `BRDA needs <line>,<block>,<branch>,<taken>, got "${value}"`);
|
|
71
86
|
}
|
|
72
87
|
const line = Number(br[1]);
|
|
73
88
|
const id = `${br[2]},${br[3]}`;
|
|
74
|
-
const taken = br[4]
|
|
89
|
+
const taken = br[4] === "-" ? 0 : Number(br[4]);
|
|
75
90
|
const branches = open.branches.get(line) ?? new Map();
|
|
76
|
-
branches.set(id, (branches.get(id) ??
|
|
91
|
+
branches.set(id, (branches.get(id) ?? 0) + taken);
|
|
77
92
|
open.branches.set(line, branches);
|
|
78
93
|
}
|
|
79
94
|
close();
|
|
@@ -110,10 +125,42 @@ export function tipPlacer(root, cwd, atTip) {
|
|
|
110
125
|
return null;
|
|
111
126
|
};
|
|
112
127
|
}
|
|
128
|
+
/** Adds a record's branch counts into `into`, keeping lcov's branch order per line. */
|
|
129
|
+
function countInto(into, path, record) {
|
|
130
|
+
const joined = into.get(path) ?? new Map();
|
|
131
|
+
for (const [line, ids] of record.branches) {
|
|
132
|
+
const on = joined.get(line) ?? new Map();
|
|
133
|
+
for (const [id, n] of ids)
|
|
134
|
+
on.set(id, (on.get(id) ?? 0) + n);
|
|
135
|
+
joined.set(line, on);
|
|
136
|
+
}
|
|
137
|
+
into.set(path, joined);
|
|
138
|
+
}
|
|
139
|
+
function countsOf(counting) {
|
|
140
|
+
const out = new Map();
|
|
141
|
+
for (const [path, byLine] of counting) {
|
|
142
|
+
if (byLine.size === 0)
|
|
143
|
+
continue;
|
|
144
|
+
out.set(path, sorted(new Map([...byLine].map(([line, ids]) => [line, [...ids.values()]]))));
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
/** Functions of both lists, once each by start and name, in line order. */
|
|
149
|
+
function joinFunctions(a, b) {
|
|
150
|
+
const seen = new Map();
|
|
151
|
+
for (const f of [...a, ...b]) {
|
|
152
|
+
const key = `${f.start}|${f.name}`;
|
|
153
|
+
const had = seen.get(key);
|
|
154
|
+
if (had === undefined || (had.end === undefined && f.end !== undefined))
|
|
155
|
+
seen.set(key, f);
|
|
156
|
+
}
|
|
157
|
+
return [...seen.values()].sort((x, y) => x.start - y.start);
|
|
158
|
+
}
|
|
113
159
|
/** Sums hits and joins branches over records of the same file; drops what `place` rejects. */
|
|
114
160
|
export function runOf(records, place) {
|
|
115
161
|
const lines = new Map();
|
|
116
162
|
const branches = new Map();
|
|
163
|
+
const functions = new Map();
|
|
117
164
|
const tests = new Map();
|
|
118
165
|
for (const record of records) {
|
|
119
166
|
const path = place(record.path);
|
|
@@ -123,37 +170,42 @@ export function runOf(records, place) {
|
|
|
123
170
|
for (const [line, n] of record.lines)
|
|
124
171
|
hits.set(line, (hits.get(line) ?? 0) + n);
|
|
125
172
|
lines.set(path, hits);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const on = joined.get(line) ?? new Map();
|
|
129
|
-
for (const [id, taken] of ids)
|
|
130
|
-
on.set(id, (on.get(id) ?? false) || taken);
|
|
131
|
-
joined.set(line, on);
|
|
132
|
-
}
|
|
133
|
-
branches.set(path, joined);
|
|
173
|
+
countInto(branches, path, record);
|
|
174
|
+
functions.set(path, joinFunctions(functions.get(path) ?? [], record.functions));
|
|
134
175
|
if (record.test === null)
|
|
135
176
|
continue;
|
|
136
|
-
const ran = tests.get(record.test) ?? new Map();
|
|
137
|
-
const set = ran.get(path) ?? new Set();
|
|
177
|
+
const ran = tests.get(record.test) ?? { lines: new Map(), branches: new Map() };
|
|
178
|
+
const set = ran.lines.get(path) ?? new Set();
|
|
138
179
|
for (const [line, n] of record.lines)
|
|
139
180
|
if (n > 0)
|
|
140
181
|
set.add(line);
|
|
141
|
-
ran.set(path, set);
|
|
182
|
+
ran.lines.set(path, set);
|
|
183
|
+
countInto(ran.branches, path, record);
|
|
142
184
|
tests.set(record.test, ran);
|
|
143
185
|
}
|
|
144
186
|
const files = new Map();
|
|
145
187
|
for (const [path, hits] of lines) {
|
|
146
188
|
const counted = new Map();
|
|
147
189
|
for (const [line, ids] of branches.get(path) ?? []) {
|
|
148
|
-
|
|
190
|
+
const n = [...ids.values()];
|
|
191
|
+
counted.set(line, [n.filter((c) => c > 0).length, n.length]);
|
|
149
192
|
}
|
|
150
|
-
|
|
193
|
+
const fns = functions.get(path) ?? [];
|
|
194
|
+
files.set(path, {
|
|
195
|
+
lines: sorted(hits),
|
|
196
|
+
branches: sorted(counted),
|
|
197
|
+
...(fns.length === 0 ? {} : { functions: fns }),
|
|
198
|
+
});
|
|
151
199
|
}
|
|
152
200
|
return {
|
|
153
201
|
files,
|
|
202
|
+
counts: countsOf(branches),
|
|
154
203
|
tests: new Map([...tests].map(([id, ran]) => [
|
|
155
204
|
id,
|
|
156
|
-
|
|
205
|
+
{
|
|
206
|
+
lines: new Map([...ran.lines].map(([path, set]) => [path, [...set].sort((a, b) => a - b)])),
|
|
207
|
+
branches: countsOf(ran.branches),
|
|
208
|
+
},
|
|
157
209
|
])),
|
|
158
210
|
};
|
|
159
211
|
}
|
|
@@ -184,7 +236,12 @@ function mergeMax(a, b) {
|
|
|
184
236
|
const [t, n] = branches.get(l) ?? [0, 0];
|
|
185
237
|
branches.set(l, [Math.max(taken, t), Math.max(total, n)]);
|
|
186
238
|
}
|
|
187
|
-
|
|
239
|
+
const functions = joinFunctions(had.functions ?? [], f.functions ?? []);
|
|
240
|
+
out.set(path, {
|
|
241
|
+
lines: sorted(lines),
|
|
242
|
+
branches: sorted(branches),
|
|
243
|
+
...(functions.length === 0 ? {} : { functions }),
|
|
244
|
+
});
|
|
188
245
|
}
|
|
189
246
|
return out;
|
|
190
247
|
}
|
|
@@ -197,7 +254,7 @@ export const PREVIEW_LINES = 8;
|
|
|
197
254
|
export function recordRun(prior, run, at) {
|
|
198
255
|
const { tip, takenAt, test } = at;
|
|
199
256
|
if (test === undefined) {
|
|
200
|
-
const tests = new Map([...run.tests].map(([id,
|
|
257
|
+
const tests = new Map([...run.tests].map(([id, ran]) => [id, testEntry(ran)]));
|
|
201
258
|
return { tip, takenAt, files: run.files, tests };
|
|
202
259
|
}
|
|
203
260
|
const base = prior !== null && prior.tip === tip ? prior : null;
|
|
@@ -205,18 +262,18 @@ export function recordRun(prior, run, at) {
|
|
|
205
262
|
const entry = {
|
|
206
263
|
...(test.at === undefined ? {} : { at: test.at }),
|
|
207
264
|
...(test.preview === undefined ? {} : { preview: test.preview }),
|
|
208
|
-
lines: hitLines(run.files),
|
|
265
|
+
...testEntry({ lines: hitLines(run.files), branches: run.counts }),
|
|
209
266
|
};
|
|
210
267
|
tests.set(test.id, entry);
|
|
211
268
|
const files = base === null ? run.files : mergeMax(base.files, run.files);
|
|
212
269
|
return { tip, takenAt, files, tests };
|
|
213
270
|
}
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
271
|
+
/** A test's lines, and its branch counts when the run had any. */
|
|
272
|
+
function testEntry(ran) {
|
|
273
|
+
return { lines: ran.lines, ...(ran.branches.size === 0 ? {} : { branches: ran.branches }) };
|
|
274
|
+
}
|
|
275
|
+
/** Calls visit with each added row's tip line, per hunk, for files the run covers. */
|
|
276
|
+
function eachAddedLine(units, files, visit) {
|
|
220
277
|
for (const unit of units.values()) {
|
|
221
278
|
if (unit.kind !== "hunk")
|
|
222
279
|
continue;
|
|
@@ -224,22 +281,37 @@ export function changedLines(units, files) {
|
|
|
224
281
|
if (file === undefined)
|
|
225
282
|
continue;
|
|
226
283
|
let line = tipStartOf(unit, units);
|
|
227
|
-
let { covered, executable } = out.get(unit.path) ?? { covered: 0, executable: 0 };
|
|
228
284
|
for (const row of unit.body) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
executable += 1;
|
|
232
|
-
if (hits > 0)
|
|
233
|
-
covered += 1;
|
|
234
|
-
}
|
|
285
|
+
if (row.op === "+")
|
|
286
|
+
visit(unit.path, file, line);
|
|
235
287
|
if (row.op !== "-")
|
|
236
288
|
line += 1;
|
|
237
289
|
}
|
|
238
|
-
if (executable > 0)
|
|
239
|
-
out.set(unit.path, { covered, executable });
|
|
240
290
|
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Per file, the branch's added rows that lcov calls executable, and how many ran. Every added
|
|
294
|
+
* row is a tip line, which is where the run was taken. Files without such rows are left out.
|
|
295
|
+
*/
|
|
296
|
+
export function changedLines(units, files) {
|
|
297
|
+
const out = new Map();
|
|
298
|
+
eachAddedLine(units, files, (path, file, line) => {
|
|
299
|
+
const hits = file.lines.get(line);
|
|
300
|
+
if (hits === undefined)
|
|
301
|
+
return;
|
|
302
|
+
const { covered, executable } = out.get(path) ?? { covered: 0, executable: 0 };
|
|
303
|
+
out.set(path, { covered: covered + (hits > 0 ? 1 : 0), executable: executable + 1 });
|
|
304
|
+
});
|
|
241
305
|
return out;
|
|
242
306
|
}
|
|
307
|
+
/** How many branches the run took on the branch's added rows, summed over every decision. */
|
|
308
|
+
export function changedBranchesTaken(units, files) {
|
|
309
|
+
let taken = 0;
|
|
310
|
+
eachAddedLine(units, files, (_, file, line) => {
|
|
311
|
+
taken += file.branches.get(line)?.[0] ?? 0;
|
|
312
|
+
});
|
|
313
|
+
return taken;
|
|
314
|
+
}
|
|
243
315
|
export function totalOf(tallies) {
|
|
244
316
|
let covered = 0;
|
|
245
317
|
let executable = 0;
|
|
@@ -278,6 +350,7 @@ export function serializeCoverage(c) {
|
|
|
278
350
|
{
|
|
279
351
|
lines: Object.fromEntries(f.lines),
|
|
280
352
|
...(f.branches.size === 0 ? {} : { branches: Object.fromEntries(f.branches) }),
|
|
353
|
+
...(f.functions === undefined ? {} : { functions: f.functions }),
|
|
281
354
|
},
|
|
282
355
|
]));
|
|
283
356
|
const tests = Object.fromEntries([...c.tests].map(([id, t]) => [
|
|
@@ -286,6 +359,11 @@ export function serializeCoverage(c) {
|
|
|
286
359
|
...(t.at === undefined ? {} : { at: t.at }),
|
|
287
360
|
...(t.preview === undefined ? {} : { preview: [...t.preview] }),
|
|
288
361
|
lines: Object.fromEntries([...t.lines].map(([p, ls]) => [p, [...ls]])),
|
|
362
|
+
...(t.branches === undefined
|
|
363
|
+
? {}
|
|
364
|
+
: {
|
|
365
|
+
branches: Object.fromEntries([...t.branches].map(([p, byLine]) => [p, Object.fromEntries(byLine)])),
|
|
366
|
+
}),
|
|
289
367
|
},
|
|
290
368
|
]));
|
|
291
369
|
return { tip: c.tip, takenAt: c.takenAt, files, ...(c.tests.size === 0 ? {} : { tests }) };
|
|
@@ -321,6 +399,52 @@ export function parseLocArg(raw, place) {
|
|
|
321
399
|
const end = m[3] === undefined ? start : Number(m[3]);
|
|
322
400
|
return end < start ? `--at ${raw} runs backwards` : { path, start, end };
|
|
323
401
|
}
|
|
402
|
+
function parseFunction(v) {
|
|
403
|
+
if (!isFields(v))
|
|
404
|
+
return null;
|
|
405
|
+
const { name, start, end } = v;
|
|
406
|
+
if (typeof name !== "string" || !isLine(start))
|
|
407
|
+
return null;
|
|
408
|
+
if (end === undefined)
|
|
409
|
+
return { name, start };
|
|
410
|
+
return isLine(end) && end >= start ? { name, start, end } : null;
|
|
411
|
+
}
|
|
412
|
+
function parseFunctions(v) {
|
|
413
|
+
if (v === undefined)
|
|
414
|
+
return undefined;
|
|
415
|
+
if (!Array.isArray(v))
|
|
416
|
+
return "functions is not a list";
|
|
417
|
+
const out = [];
|
|
418
|
+
for (const raw of v) {
|
|
419
|
+
const f = parseFunction(raw);
|
|
420
|
+
if (f === null)
|
|
421
|
+
return "a function is not a { name, start, end? }";
|
|
422
|
+
out.push(f);
|
|
423
|
+
}
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
function parseCounts(v) {
|
|
427
|
+
if (v === undefined)
|
|
428
|
+
return undefined;
|
|
429
|
+
const files = entries(v);
|
|
430
|
+
if (files === null)
|
|
431
|
+
return "branches is not an object";
|
|
432
|
+
const out = new Map();
|
|
433
|
+
for (const [path, raw] of files) {
|
|
434
|
+
const lines = entries(raw);
|
|
435
|
+
if (!isRepoPath(path) || lines === null)
|
|
436
|
+
return `branches of ${path} are not by line`;
|
|
437
|
+
const byLine = new Map();
|
|
438
|
+
for (const [key, counts] of lines) {
|
|
439
|
+
if (!LINE_KEY.test(key) || !Array.isArray(counts) || !counts.every(isCount)) {
|
|
440
|
+
return `branches of ${path}:${key} are not counts`;
|
|
441
|
+
}
|
|
442
|
+
byLine.set(Number(key), counts.filter(isCount));
|
|
443
|
+
}
|
|
444
|
+
out.set(path, sorted(byLine));
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
324
448
|
function parseFile(v) {
|
|
325
449
|
if (!isFields(v))
|
|
326
450
|
return "a file is not an object";
|
|
@@ -347,7 +471,14 @@ function parseFile(v) {
|
|
|
347
471
|
}
|
|
348
472
|
branches.set(Number(key), [taken, total]);
|
|
349
473
|
}
|
|
350
|
-
|
|
474
|
+
const functions = parseFunctions(v.functions);
|
|
475
|
+
if (typeof functions === "string")
|
|
476
|
+
return functions;
|
|
477
|
+
return {
|
|
478
|
+
lines: sorted(hits),
|
|
479
|
+
branches: sorted(branches),
|
|
480
|
+
...(functions === undefined ? {} : { functions }),
|
|
481
|
+
};
|
|
351
482
|
}
|
|
352
483
|
function parseTest(v) {
|
|
353
484
|
if (!isFields(v))
|
|
@@ -370,10 +501,14 @@ function parseTest(v) {
|
|
|
370
501
|
}
|
|
371
502
|
lines.set(path, list);
|
|
372
503
|
}
|
|
504
|
+
const branches = parseCounts(v.branches);
|
|
505
|
+
if (typeof branches === "string")
|
|
506
|
+
return branches;
|
|
373
507
|
return {
|
|
374
508
|
...(at === undefined ? {} : { at }),
|
|
375
509
|
...(preview === undefined ? {} : { preview: preview.filter((l) => typeof l === "string") }),
|
|
376
510
|
lines,
|
|
511
|
+
...(branches === undefined ? {} : { branches }),
|
|
377
512
|
};
|
|
378
513
|
}
|
|
379
514
|
/** The one place coverage.json becomes a Coverage. */
|
package/dist/src/host.js
CHANGED
|
@@ -122,6 +122,7 @@ async function loadHosted(registration, held) {
|
|
|
122
122
|
}
|
|
123
123
|
const verdict = validatePlan(session.diff, serializePlanFile(stored.file), {
|
|
124
124
|
lastReview: session.lastReview(),
|
|
125
|
+
frozen: true,
|
|
125
126
|
});
|
|
126
127
|
if (!verdict.ok) {
|
|
127
128
|
throw new RegistrationError(`plan v${version} no longer fits ${branch}; check it again`);
|
package/dist/src/plan.js
CHANGED
|
@@ -556,12 +556,13 @@ function deriveConcernId(unit, c) {
|
|
|
556
556
|
}
|
|
557
557
|
/**
|
|
558
558
|
* Gives every concern its id and checks where it sits: lines must be change lines the commit
|
|
559
|
-
* takes, and a unit carries at most one concern
|
|
559
|
+
* takes, and a unit carries at most one concern in each commit, so each slice of a hunk split
|
|
560
|
+
* across commits may carry its own.
|
|
560
561
|
*/
|
|
561
562
|
function placeConcerns(diff, file, claims, issues) {
|
|
562
563
|
const ids = new Set();
|
|
563
|
-
const holders = new Map();
|
|
564
564
|
const placedPerCommit = file.commits.map((commit, k) => {
|
|
565
|
+
const held = new Map();
|
|
565
566
|
const placed = [];
|
|
566
567
|
const add = (c, unit) => {
|
|
567
568
|
const id = deriveConcernId(unit, c);
|
|
@@ -578,7 +579,7 @@ function placeConcerns(diff, file, claims, issues) {
|
|
|
578
579
|
const concerns = ref.concerns ?? [];
|
|
579
580
|
if (concerns.length === 0)
|
|
580
581
|
return;
|
|
581
|
-
|
|
582
|
+
held.set(ref.unit, (held.get(ref.unit) ?? 0) + concerns.length);
|
|
582
583
|
const taken = claims.resolved[k]?.[j]?.select;
|
|
583
584
|
const unit = diff.units.get(ref.unit);
|
|
584
585
|
for (const c of concerns) {
|
|
@@ -591,25 +592,25 @@ function placeConcerns(diff, file, claims, issues) {
|
|
|
591
592
|
});
|
|
592
593
|
continue;
|
|
593
594
|
}
|
|
594
|
-
const
|
|
595
|
-
|
|
595
|
+
const takes = taken?.kind === "lines" ? taken : null;
|
|
596
|
+
const outside = takes === null ? [] : lines.filter((n) => !takes.changes.includes(n));
|
|
597
|
+
if (takes !== null && outside.length > 0) {
|
|
596
598
|
issues.push({
|
|
597
599
|
kind: "bad-concern",
|
|
598
600
|
commit: k,
|
|
599
|
-
reason: `${ref.unit}: concern lines ${outside.join(", ")} are not change lines this commit takes`,
|
|
601
|
+
reason: `${ref.unit}: concern lines ${outside.join(", ")} are not change lines this commit takes; \`lines\` are the hunk's [n] as prompt.md shows them, on a slice too, and this commit takes ${formatUnitRef({ unit: ref.unit, select: takes })}`,
|
|
600
602
|
});
|
|
601
603
|
}
|
|
602
604
|
else
|
|
603
605
|
add(c, ref.unit);
|
|
604
606
|
}
|
|
605
607
|
});
|
|
608
|
+
for (const [unit, count] of held) {
|
|
609
|
+
if (count > 1)
|
|
610
|
+
issues.push({ kind: "too-many-concerns", unit, commit: k, count });
|
|
611
|
+
}
|
|
606
612
|
return placed;
|
|
607
613
|
});
|
|
608
|
-
for (const [unit, commits] of holders) {
|
|
609
|
-
if (commits.length > 1) {
|
|
610
|
-
issues.push({ kind: "too-many-concerns", unit, commits: [...new Set(commits)] });
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
614
|
return placedPerCommit;
|
|
614
615
|
}
|
|
615
616
|
/** "o_" + 8 hex over the unit and the label: stable while the offer stays at its place. */
|
|
@@ -744,6 +745,36 @@ function answerProblem(file, r, isQuestion) {
|
|
|
744
745
|
* Parses and checks a draft against the diff: every change line used exactly once, every
|
|
745
746
|
* commit's tree valid, and the last commit equal to the tip. Collects all issues at once.
|
|
746
747
|
*/
|
|
748
|
+
/**
|
|
749
|
+
* Sections read top-down: story, steps, details, wiring. A later section never comes back, so a
|
|
750
|
+
* commit whose section ranks below an earlier commit's must move ahead of the first such commit.
|
|
751
|
+
*/
|
|
752
|
+
function sectionOrder(file) {
|
|
753
|
+
const issues = [];
|
|
754
|
+
const placed = [];
|
|
755
|
+
file.commits.forEach((c, commit) => {
|
|
756
|
+
if (c.section === undefined)
|
|
757
|
+
return;
|
|
758
|
+
const rank = SECTIONS.indexOf(c.section);
|
|
759
|
+
const first = placed.find((p) => p.rank > rank);
|
|
760
|
+
if (first !== undefined) {
|
|
761
|
+
issues.push({
|
|
762
|
+
kind: "section-order",
|
|
763
|
+
commit,
|
|
764
|
+
section: c.section,
|
|
765
|
+
before: first.commit,
|
|
766
|
+
beforeSection: first.section,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
else
|
|
770
|
+
placed.push({ commit, rank, section: c.section });
|
|
771
|
+
});
|
|
772
|
+
return issues;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Judges a draft against the diff. `frozen` re-checks a plan that already passed when it was
|
|
776
|
+
* frozen: section order is a rule for drafts, so a plan frozen before it was one still fits.
|
|
777
|
+
*/
|
|
747
778
|
export function validatePlan(diff, draft, opts = {}) {
|
|
748
779
|
const parsed = parsePlanFile(draft);
|
|
749
780
|
if (Array.isArray(parsed))
|
|
@@ -766,6 +797,8 @@ export function validatePlan(diff, draft, opts = {}) {
|
|
|
766
797
|
issues.push({ kind: "empty-commit", commit: k });
|
|
767
798
|
});
|
|
768
799
|
issues.push(...offerLimits(file));
|
|
800
|
+
if (opts.frozen !== true)
|
|
801
|
+
issues.push(...sectionOrder(file));
|
|
769
802
|
const assignment = [];
|
|
770
803
|
const claims = claim(diff, file, assignment);
|
|
771
804
|
issues.push(...assignment);
|
|
@@ -788,17 +821,6 @@ export function validatePlan(diff, draft, opts = {}) {
|
|
|
788
821
|
if (length > LONG_TITLE)
|
|
789
822
|
warnings.push({ kind: "long-title", commit, length });
|
|
790
823
|
});
|
|
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
|
-
});
|
|
802
824
|
const asked = new Map(review?.comments.map((c) => [c.id, c.kind === "question"]));
|
|
803
825
|
for (const r of file.responses) {
|
|
804
826
|
const isQuestion = asked.get(r.comment);
|