@shiyuphan/follow-me 0.1.1 → 0.3.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 +119 -69
- package/dist/src/cli.js +1069 -140
- package/dist/src/comments.js +361 -0
- package/dist/src/git.js +32 -3
- package/dist/src/host.js +106 -9
- package/dist/src/install.js +22 -1
- package/dist/src/naive.js +26 -0
- package/dist/src/plan.js +282 -26
- package/dist/src/prompt.js +711 -98
- package/dist/src/server.js +573 -140
- package/dist/src/session.js +106 -104
- package/dist/src/syntax.js +199 -0
- package/dist/src/types.js +3 -1
- package/dist/ui/dist/assets/abnfDiagram-WEYOIA3C-DcmzRkVZ.js +1 -0
- package/dist/ui/dist/assets/arc-T5QB0T1C.js +1 -0
- package/dist/ui/dist/assets/architecture-WOLXFQ4H-DUIkQTeE.js +1 -0
- package/dist/ui/dist/assets/architectureDiagram-ZHVYKT53-BUYi4-bA.js +36 -0
- package/dist/ui/dist/assets/array-BifhSqXX.js +1 -0
- package/dist/ui/dist/assets/blockDiagram-RJAE7QA3-dyJxtT9e.js +140 -0
- package/dist/ui/dist/assets/c4Diagram-XDPPPE54-CrAgiaZR.js +38 -0
- package/dist/ui/dist/assets/channel-wW7ebm0i.js +1 -0
- package/dist/ui/dist/assets/chunk-2E7WHVMD-C6fAf4Vj.js +88 -0
- package/dist/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/ui/dist/assets/chunk-4EMNKR2T-BLZjwZ4Q.js +1 -0
- package/dist/ui/dist/assets/chunk-55XCU76K-DGLK2Q-L.js +1 -0
- package/dist/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/ui/dist/assets/chunk-7S6XSTFO-BablszTX.js +2 -0
- package/dist/ui/dist/assets/chunk-AQDTSKZB-Cuk3B_vb.js +72 -0
- package/dist/ui/dist/assets/chunk-DSNLSHJZ-DiC78RqE.js +127 -0
- package/dist/ui/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/dist/ui/dist/assets/chunk-HAGFOTED-X_RbvixX.js +62 -0
- package/dist/ui/dist/assets/chunk-HNE4Q36P-C14_x5ck.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-Dxd0dtYR.js +213 -0
- package/dist/ui/dist/assets/chunk-UHW7PFBP-BIxePNUS.js +1 -0
- package/dist/ui/dist/assets/chunk-VQ5W62QI-CKSKLbj4.js +1 -0
- package/dist/ui/dist/assets/chunk-WQQL4CDK-CWNmZGlR.js +10 -0
- package/dist/ui/dist/assets/chunk-XXDRQBXY-D-I5QHgG.js +1 -0
- package/dist/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/ui/dist/assets/chunk-YZO67HFG-DFP68CFd.js +1 -0
- package/dist/ui/dist/assets/chunk-ZAUX2LTX-DY90d4QG.js +1 -0
- package/dist/ui/dist/assets/chunk-ZCHMQSOR-Dlpr-5UC.js +2 -0
- package/dist/ui/dist/assets/classDiagram-v2-4MYWR3UA-CH9uhoLJ.js +217 -0
- package/dist/ui/dist/assets/cose-bilkent-JH36ORCC-DtYHrp8D.js +1 -0
- package/dist/ui/dist/assets/cynefin-EF2NZ3EQ-BtslbtrY.js +1 -0
- package/dist/ui/dist/assets/cynefinDiagram-WF24UDRP-DUU1Jcc7.js +62 -0
- package/dist/ui/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/dist/ui/dist/assets/dagre-NK4GIZVX-CDlP-dRW.js +4 -0
- package/dist/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/ui/dist/assets/diagram-CBK3MWTC-DiV2fn-r.js +24 -0
- package/dist/ui/dist/assets/diagram-I5XIIV2D-Bg5RehGi.js +24 -0
- package/dist/ui/dist/assets/diagram-ODIL2UCW-CFmazz3f.js +30 -0
- package/dist/ui/dist/assets/diagram-PFBEJUXE-uw3ZBqDp.js +3 -0
- package/dist/ui/dist/assets/diagram-RJA3I4VV-uEOYYRYa.js +41 -0
- package/dist/ui/dist/assets/diagram-TYUNFSNR-BN58LF_3.js +200 -0
- package/dist/ui/dist/assets/dist-tWS_lXDS.js +1 -0
- package/dist/ui/dist/assets/ebnfDiagram-V4P3TTPT-B68Tz-w8.js +1 -0
- package/dist/ui/dist/assets/elk-QONTBCTQ-DZZohNea.js +27 -0
- package/dist/ui/dist/assets/erDiagram-KYQIGQAA-BH_m74pv.js +99 -0
- package/dist/ui/dist/assets/eventmodeling-K75KTNOO-CTp5FHos.js +1 -0
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-CsCvexry.js +1 -0
- package/dist/ui/dist/assets/ganttDiagram-R7GPNTJY-BDau7_0f.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-Bv84IoCm.js +1 -0
- package/dist/ui/dist/assets/gitGraphDiagram-PDYL7XHY-DzXcMlm8.js +106 -0
- package/dist/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/ui/dist/assets/index-BTJoqVVN.js +133 -0
- package/dist/ui/dist/assets/index-D60U3smc.css +1 -0
- package/dist/ui/dist/assets/info-OHQRW6UA-222J0O_j.js +1 -0
- package/dist/ui/dist/assets/infoDiagram-MVDRKAX5-DE2sa3Vz.js +2 -0
- package/dist/ui/dist/assets/init-C-OQMol4.js +1 -0
- package/dist/ui/dist/assets/ishikawaDiagram-HPEHV2GE-BJDk0Zcj.js +70 -0
- package/dist/ui/dist/assets/journeyDiagram-BOSHW7O7-JSUXX5Hb.js +139 -0
- package/dist/ui/dist/assets/kanban-definition-FNSA4HC5-Bg79b49M.js +89 -0
- package/dist/ui/dist/assets/katex-B_JcIwqj.js +257 -0
- package/dist/ui/dist/assets/line-Bk5qQSIa.js +1 -0
- package/dist/ui/dist/assets/linear-PFnVMru5.js +1 -0
- package/dist/ui/dist/assets/mermaid-parser.core-Dcxlzc-S.js +7 -0
- package/dist/ui/dist/assets/mermaid.core-TeOHQJ00.js +44 -0
- package/dist/ui/dist/assets/mindmap-definition-6OY57UG3-IaNk8NnW.js +98 -0
- package/dist/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/ui/dist/assets/packet-JDAUHWVQ-DMff5cVK.js +1 -0
- package/dist/ui/dist/assets/path-fybaL0A-.js +1 -0
- package/dist/ui/dist/assets/pegDiagram-ECIUJNBX-66Xu4YRc.js +1 -0
- package/dist/ui/dist/assets/pie-XZMESJXO-GEtzLCIM.js +1 -0
- package/dist/ui/dist/assets/pieDiagram-GNMPY74T-7jMBi-kU.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-FDHlWQ43.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-8hIWayC2.js +7 -0
- package/dist/ui/dist/assets/radar-ABXABTNO-BTdwUOOF.js +1 -0
- package/dist/ui/dist/assets/railroad-I3PHUGI6-DcMbDqBL.js +1 -0
- package/dist/ui/dist/assets/railroad-abnf-LNEFI6M7-gTzUTWeI.js +1 -0
- package/dist/ui/dist/assets/railroad-ebnf-DWYD2UWJ-CNtdUPtk.js +1 -0
- package/dist/ui/dist/assets/railroad-peg-JR7BVNR7-DOiExAtE.js +1 -0
- package/dist/ui/dist/assets/railroadDiagram-Y72NRBL7-2kE_ygvM.js +1 -0
- package/dist/ui/dist/assets/requirementDiagram-EQYSW5RA-CoIAHRq2.js +84 -0
- package/dist/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/ui/dist/assets/sankeyDiagram-SY24JHO5-7p8D3wim.js +40 -0
- package/dist/ui/dist/assets/sequenceDiagram-BLEHNHQU-C6QFnMvn.js +169 -0
- package/dist/ui/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/ui/dist/assets/src-FgijoBVs.js +1 -0
- package/dist/ui/dist/assets/stateDiagram-v2-A6IESKWZ-n9eAb_bT.js +291 -0
- package/dist/ui/dist/assets/swimlanes-B3ENRKFQ-U18mtUzX.js +1 -0
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DX07QVy9.js +8 -0
- package/dist/ui/dist/assets/timeline-definition-JXYKGLGB-CD2Z8QGP.js +120 -0
- package/dist/ui/dist/assets/treeView-D4JQ5SDB-sqUjH5gB.js +1 -0
- package/dist/ui/dist/assets/treemap-SAJKECNS-DvZ38FP7.js +1 -0
- package/dist/ui/dist/assets/usecaseDiagram-WPOLBNNX-CqOGidaZ.js +328 -0
- package/dist/ui/dist/assets/vennDiagram-Q3Y3IDCW-D1SXSBQ5.js +34 -0
- package/dist/ui/dist/assets/wardley-7MLQ67FV-Bpwio-Dx.js +1 -0
- package/dist/ui/dist/assets/wardleyDiagram-IVAIDKWA-JSUXHMnk.js +78 -0
- package/dist/ui/dist/assets/xychartDiagram-4URNHDU4-B4IFqQTN.js +7 -0
- package/dist/ui/dist/index.html +15 -0
- package/dist/ui/dist/style.css +121 -0
- package/package.json +31 -8
- package/skills/follow-me/SKILL.md +15 -61
- package/dist/src/anthropic.js +0 -101
- package/dist/ui/app.js +0 -797
- package/dist/ui/index.html +0 -40
- package/dist/ui/playback.js +0 -175
- package/dist/ui/style.css +0 -466
package/dist/src/plan.js
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { formatUnitRef, isSha, parseUnitRef, projectFile } from "./diff.js";
|
|
2
|
+
import { formatUnitRef, isSha, isUnitId, parseUnitRef, projectFile } from "./diff.js";
|
|
3
3
|
export const PLAN_SCHEMA = "follow-me/plan@1";
|
|
4
|
+
/** Titles past this many characters draw a warning; the prompt asks for 50 or fewer. */
|
|
5
|
+
export const LONG_TITLE = 72;
|
|
4
6
|
const validated = Symbol("follow-me/validated");
|
|
5
7
|
export function isCommentId(s) {
|
|
6
8
|
return s.length > 0;
|
|
7
9
|
}
|
|
10
|
+
export function isConcernId(s) {
|
|
11
|
+
return typeof s === "string" && /^k_[0-9a-f]{8}$/.test(s);
|
|
12
|
+
}
|
|
8
13
|
export function isPlanVersion(n) {
|
|
9
14
|
return typeof n === "number" && Number.isInteger(n) && n >= 1;
|
|
10
15
|
}
|
|
11
|
-
const STATUSES = ["addressed", "declined", "deferred"];
|
|
16
|
+
const STATUSES = ["addressed", "declined", "deferred", "answered"];
|
|
12
17
|
function isResponseStatus(s) {
|
|
13
18
|
return STATUSES.some((x) => x === s);
|
|
14
19
|
}
|
|
20
|
+
const SEVERITIES = ["blocker", "major", "minor"];
|
|
21
|
+
const CONCERN_KINDS = ["bug", "missing-test", "naming", "risk"];
|
|
22
|
+
export function isSeverity(s) {
|
|
23
|
+
return SEVERITIES.some((x) => x === s);
|
|
24
|
+
}
|
|
25
|
+
function isConcernKind(s) {
|
|
26
|
+
return CONCERN_KINDS.some((x) => x === s);
|
|
27
|
+
}
|
|
15
28
|
/** Narrows to an object whose named fields are still unknown, which holds for any object. */
|
|
16
29
|
export function isFields(v) {
|
|
17
30
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -19,32 +32,150 @@ export function isFields(v) {
|
|
|
19
32
|
export function isArray(v) {
|
|
20
33
|
return Array.isArray(v);
|
|
21
34
|
}
|
|
22
|
-
const INVALID = Symbol("invalid
|
|
23
|
-
/** Absent, null and blank all mean
|
|
24
|
-
function
|
|
35
|
+
const INVALID = Symbol("invalid text");
|
|
36
|
+
/** Absent, null and blank all mean none, so a planner may always write the field. */
|
|
37
|
+
function parseText(v) {
|
|
25
38
|
if (v === undefined || v === null)
|
|
26
39
|
return null;
|
|
27
40
|
if (typeof v !== "string")
|
|
28
41
|
return INVALID;
|
|
29
42
|
return v.trim() === "" ? null : v;
|
|
30
43
|
}
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
const MERMAID_KEYWORDS = [
|
|
45
|
+
"graph",
|
|
46
|
+
"flowchart",
|
|
47
|
+
"sequenceDiagram",
|
|
48
|
+
"classDiagram",
|
|
49
|
+
"stateDiagram",
|
|
50
|
+
"stateDiagram-v2",
|
|
51
|
+
"erDiagram",
|
|
52
|
+
"journey",
|
|
53
|
+
"gantt",
|
|
54
|
+
"pie",
|
|
55
|
+
"gitGraph",
|
|
56
|
+
"mindmap",
|
|
57
|
+
"timeline",
|
|
58
|
+
"quadrantChart",
|
|
59
|
+
"block-beta",
|
|
60
|
+
"%%{init",
|
|
61
|
+
];
|
|
62
|
+
/** A diagram is Mermaid source, so its first line must name a diagram type. Else the reason. */
|
|
63
|
+
export function diagramProblem(diagram) {
|
|
64
|
+
const first = (diagram.split("\n").find((line) => line.trim() !== "") ?? "").trim();
|
|
65
|
+
const named = MERMAID_KEYWORDS.some((k) => first.startsWith(k) && !/[\w-]/.test(first.charAt(k.length)));
|
|
66
|
+
if (named)
|
|
67
|
+
return null;
|
|
68
|
+
return `expected Mermaid source whose first line starts with a diagram type such as flowchart, sequenceDiagram or stateDiagram-v2, got "${first.slice(0, 40)}"`;
|
|
69
|
+
}
|
|
70
|
+
/** A concern is { severity, kind, text, lines? }; lines are change indices, on a unit ref only. */
|
|
71
|
+
function parseConcern(v, onUnit) {
|
|
72
|
+
if (!isFields(v))
|
|
73
|
+
return "expected an object";
|
|
74
|
+
if (!isSeverity(v.severity))
|
|
75
|
+
return `severity: expected one of ${SEVERITIES.join(", ")}`;
|
|
76
|
+
if (!isConcernKind(v.kind))
|
|
77
|
+
return `kind: expected one of ${CONCERN_KINDS.join(", ")}`;
|
|
78
|
+
if (typeof v.text !== "string" || v.text.trim() === "")
|
|
79
|
+
return "text: expected a sentence";
|
|
80
|
+
const { severity, kind, text } = v;
|
|
81
|
+
if (v.lines === undefined || v.lines === null)
|
|
82
|
+
return { severity, kind, text };
|
|
83
|
+
if (!onUnit)
|
|
84
|
+
return "lines: only a concern on a unit ref has lines";
|
|
85
|
+
const lines = v.lines;
|
|
86
|
+
if (!isArray(lines) || lines.length === 0)
|
|
87
|
+
return "lines: expected a list of change indices";
|
|
88
|
+
const indices = lines.flatMap((n) => (Number.isInteger(n) && Number(n) >= 0 ? [Number(n)] : []));
|
|
89
|
+
if (indices.length !== lines.length)
|
|
90
|
+
return "lines: expected a list of change indices";
|
|
91
|
+
return { severity, kind, text, lines: [...new Set(indices)].sort((a, b) => a - b) };
|
|
92
|
+
}
|
|
93
|
+
/** Absent, null and [] all mean no concerns, so a planner may always write the field. */
|
|
94
|
+
function parseConcerns(v, onUnit) {
|
|
95
|
+
if (v === undefined || v === null)
|
|
96
|
+
return [];
|
|
97
|
+
if (!isArray(v))
|
|
98
|
+
return "expected an array";
|
|
99
|
+
const out = [];
|
|
100
|
+
for (const [i, raw] of v.entries()) {
|
|
101
|
+
const concern = parseConcern(raw, onUnit);
|
|
102
|
+
if (typeof concern === "string")
|
|
103
|
+
return `[${i}]: ${concern}`;
|
|
104
|
+
out.push(concern);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/** The optional narration, diagram and concerns of a commit or unit entry, or what is wrong. */
|
|
109
|
+
function parseExtras(v, onUnit) {
|
|
110
|
+
const narration = parseText(v.narration);
|
|
111
|
+
if (narration === INVALID)
|
|
112
|
+
return { field: "narration", error: "expected a string" };
|
|
113
|
+
const diagram = parseText(v.diagram);
|
|
114
|
+
if (diagram === INVALID)
|
|
115
|
+
return { field: "diagram", error: "expected a string" };
|
|
116
|
+
const problem = diagram === null ? null : diagramProblem(diagram);
|
|
117
|
+
if (problem !== null)
|
|
118
|
+
return { field: "diagram", error: problem };
|
|
119
|
+
const concerns = parseConcerns(v.concerns, onUnit);
|
|
120
|
+
if (typeof concerns === "string")
|
|
121
|
+
return { field: "concerns", error: concerns };
|
|
122
|
+
return withExtras(narration, diagram, concerns);
|
|
123
|
+
}
|
|
124
|
+
function withText(narration, diagram) {
|
|
125
|
+
return {
|
|
126
|
+
...(narration === null ? {} : { narration }),
|
|
127
|
+
...(diagram === null ? {} : { diagram }),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function withExtras(narration, diagram, concerns) {
|
|
131
|
+
return { ...withText(narration, diagram), ...(concerns.length === 0 ? {} : { concerns }) };
|
|
132
|
+
}
|
|
133
|
+
function serializeConcern(c) {
|
|
134
|
+
return {
|
|
135
|
+
severity: c.severity,
|
|
136
|
+
kind: c.kind,
|
|
137
|
+
text: c.text,
|
|
138
|
+
...(c.lines === undefined ? {} : { lines: c.lines }),
|
|
139
|
+
};
|
|
33
140
|
}
|
|
34
|
-
|
|
141
|
+
function serializeExtras(x) {
|
|
142
|
+
return {
|
|
143
|
+
...withText(x.narration ?? null, x.diagram ?? null),
|
|
144
|
+
...(x.concerns === undefined ? {} : { concerns: x.concerns.map(serializeConcern) }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** A unit entry is "h_x:0-3" or { "unit": "h_x:0-3", "narration", "diagram", "concerns" }. */
|
|
35
148
|
function parseUnitEntry(u) {
|
|
36
149
|
if (typeof u === "string")
|
|
37
150
|
return parseUnitRef(u);
|
|
38
151
|
if (!isFields(u) || typeof u.unit !== "string") {
|
|
39
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
error: 'expected a unit ref string or { "unit", "narration", "diagram", "concerns" }',
|
|
154
|
+
};
|
|
40
155
|
}
|
|
41
156
|
const ref = parseUnitRef(u.unit);
|
|
42
157
|
if ("error" in ref)
|
|
43
158
|
return ref;
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
return { error:
|
|
47
|
-
return { ...ref, ...
|
|
159
|
+
const extras = parseExtras(u, true);
|
|
160
|
+
if ("error" in extras)
|
|
161
|
+
return { error: `${extras.field}: ${extras.error}` };
|
|
162
|
+
return { ...ref, ...extras };
|
|
163
|
+
}
|
|
164
|
+
/** { "commit": 3, "unit": "h_x" } with commit 1-based; absent and null mean not given. */
|
|
165
|
+
function parseAnswerAt(v) {
|
|
166
|
+
if (v === undefined || v === null)
|
|
167
|
+
return null;
|
|
168
|
+
if (!isFields(v))
|
|
169
|
+
return 'expected { "commit", "unit" }';
|
|
170
|
+
const { commit, unit } = v;
|
|
171
|
+
if (typeof commit !== "number" || !Number.isInteger(commit) || commit < 1) {
|
|
172
|
+
return "commit: expected a commit number counted from 1";
|
|
173
|
+
}
|
|
174
|
+
if (unit === undefined || unit === null)
|
|
175
|
+
return { commit };
|
|
176
|
+
if (typeof unit !== "string" || !isUnitId(unit))
|
|
177
|
+
return "unit: expected a unit id";
|
|
178
|
+
return { commit, unit };
|
|
48
179
|
}
|
|
49
180
|
/** Boundary parse of draft.json or plans/vN.json. Reports every malformed field. */
|
|
50
181
|
export function parsePlanFile(draft) {
|
|
@@ -78,9 +209,9 @@ export function parsePlanFile(draft) {
|
|
|
78
209
|
}
|
|
79
210
|
if (typeof c.message !== "string")
|
|
80
211
|
bad(`${at}.message`, "expected a string");
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
bad(`${at}.
|
|
212
|
+
const extras = parseExtras(c, false);
|
|
213
|
+
if ("error" in extras)
|
|
214
|
+
bad(`${at}.${extras.field}`, extras.error);
|
|
84
215
|
const units = [];
|
|
85
216
|
if (!isArray(c.units))
|
|
86
217
|
bad(`${at}.units`, "expected an array of unit refs");
|
|
@@ -93,8 +224,8 @@ export function parsePlanFile(draft) {
|
|
|
93
224
|
units.push(ref);
|
|
94
225
|
}
|
|
95
226
|
}
|
|
96
|
-
if (typeof c.message === "string" &&
|
|
97
|
-
commits.push({ message: c.message, ...
|
|
227
|
+
if (typeof c.message === "string" && !("error" in extras)) {
|
|
228
|
+
commits.push({ message: c.message, ...extras, units });
|
|
98
229
|
}
|
|
99
230
|
}
|
|
100
231
|
}
|
|
@@ -115,8 +246,19 @@ export function parsePlanFile(draft) {
|
|
|
115
246
|
}
|
|
116
247
|
else if (typeof r.note !== "string")
|
|
117
248
|
bad(`${at}.note`, "expected a string");
|
|
118
|
-
else
|
|
119
|
-
|
|
249
|
+
else {
|
|
250
|
+
const where = parseAnswerAt(r.at);
|
|
251
|
+
if (typeof where === "string")
|
|
252
|
+
bad(`${at}.at`, where);
|
|
253
|
+
else {
|
|
254
|
+
responses.push({
|
|
255
|
+
comment: r.comment,
|
|
256
|
+
status: r.status,
|
|
257
|
+
note: r.note,
|
|
258
|
+
...(where === null ? {} : { at: where }),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
120
262
|
}
|
|
121
263
|
}
|
|
122
264
|
if (issues.length > 0 || base === null || tip === null)
|
|
@@ -131,12 +273,17 @@ export function serializePlanFile(file) {
|
|
|
131
273
|
tip: file.tip,
|
|
132
274
|
commits: file.commits.map((c) => ({
|
|
133
275
|
message: c.message,
|
|
134
|
-
...
|
|
135
|
-
units: c.units.map((ref) => ref.narration === undefined
|
|
276
|
+
...serializeExtras(c),
|
|
277
|
+
units: c.units.map((ref) => ref.narration === undefined && ref.diagram === undefined && ref.concerns === undefined
|
|
136
278
|
? formatUnitRef(ref)
|
|
137
|
-
: { unit: formatUnitRef(ref),
|
|
279
|
+
: { unit: formatUnitRef(ref), ...serializeExtras(ref) }),
|
|
280
|
+
})),
|
|
281
|
+
responses: file.responses.map((r) => ({
|
|
282
|
+
comment: r.comment,
|
|
283
|
+
status: r.status,
|
|
284
|
+
note: r.note,
|
|
285
|
+
...(r.at === undefined ? {} : { at: r.at }),
|
|
138
286
|
})),
|
|
139
|
-
responses: file.responses.map((r) => ({ comment: r.comment, status: r.status, note: r.note })),
|
|
140
287
|
};
|
|
141
288
|
}
|
|
142
289
|
export function planHash(file) {
|
|
@@ -307,7 +454,77 @@ function matchesTip(change, tip) {
|
|
|
307
454
|
}
|
|
308
455
|
return false;
|
|
309
456
|
}
|
|
457
|
+
/** "k_" + 8 hex over the unit, the lines and the text: stable while the concern is unchanged. */
|
|
458
|
+
function deriveConcernId(unit, c) {
|
|
459
|
+
const hex = createHash("sha256")
|
|
460
|
+
.update(`${unit ?? ""}\0${(c.lines ?? []).join(",")}\0${c.text}`)
|
|
461
|
+
.digest("hex")
|
|
462
|
+
.slice(0, 8);
|
|
463
|
+
const id = `k_${hex}`;
|
|
464
|
+
if (!isConcernId(id))
|
|
465
|
+
throw new Error(`sha256 produced ${id}`);
|
|
466
|
+
return id;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Gives every concern its id and checks where it sits: lines must be change lines the commit
|
|
470
|
+
* takes, and a unit carries at most one concern across the whole plan.
|
|
471
|
+
*/
|
|
472
|
+
function placeConcerns(diff, file, claims, issues) {
|
|
473
|
+
const ids = new Set();
|
|
474
|
+
const holders = new Map();
|
|
475
|
+
const placedPerCommit = file.commits.map((commit, k) => {
|
|
476
|
+
const placed = [];
|
|
477
|
+
const add = (c, unit) => {
|
|
478
|
+
const id = deriveConcernId(unit, c);
|
|
479
|
+
if (ids.has(id)) {
|
|
480
|
+
issues.push({ kind: "bad-concern", commit: k, reason: `${id} repeats another concern` });
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
ids.add(id);
|
|
484
|
+
placed.push({ ...c, id, ...(unit === null ? {} : { unit }) });
|
|
485
|
+
};
|
|
486
|
+
for (const c of commit.concerns ?? [])
|
|
487
|
+
add(c, null);
|
|
488
|
+
commit.units.forEach((ref, j) => {
|
|
489
|
+
const concerns = ref.concerns ?? [];
|
|
490
|
+
if (concerns.length === 0)
|
|
491
|
+
return;
|
|
492
|
+
holders.set(ref.unit, [...(holders.get(ref.unit) ?? []), ...concerns.map(() => k)]);
|
|
493
|
+
const taken = claims.resolved[k]?.[j]?.select;
|
|
494
|
+
const unit = diff.units.get(ref.unit);
|
|
495
|
+
for (const c of concerns) {
|
|
496
|
+
const lines = c.lines ?? [];
|
|
497
|
+
if (lines.length > 0 && unit?.kind !== "hunk") {
|
|
498
|
+
issues.push({
|
|
499
|
+
kind: "bad-concern",
|
|
500
|
+
commit: k,
|
|
501
|
+
reason: `${ref.unit}: only a text hunk has change lines`,
|
|
502
|
+
});
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const outside = taken?.kind === "lines" ? lines.filter((n) => !taken.changes.includes(n)) : [];
|
|
506
|
+
if (outside.length > 0) {
|
|
507
|
+
issues.push({
|
|
508
|
+
kind: "bad-concern",
|
|
509
|
+
commit: k,
|
|
510
|
+
reason: `${ref.unit}: concern lines ${outside.join(", ")} are not change lines this commit takes`,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
else
|
|
514
|
+
add(c, ref.unit);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
return placed;
|
|
518
|
+
});
|
|
519
|
+
for (const [unit, commits] of holders) {
|
|
520
|
+
if (commits.length > 1) {
|
|
521
|
+
issues.push({ kind: "too-many-concerns", unit, commits: [...new Set(commits)] });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return placedPerCommit;
|
|
525
|
+
}
|
|
310
526
|
function project(diff, file, claims, issues) {
|
|
527
|
+
const concerns = placeConcerns(diff, file, claims, issues);
|
|
311
528
|
const { stateAt, tipOf } = projector(diff, claims.owners);
|
|
312
529
|
const present = new Map();
|
|
313
530
|
for (const f of diff.textFiles.values())
|
|
@@ -318,6 +535,7 @@ function project(diff, file, claims, issues) {
|
|
|
318
535
|
}
|
|
319
536
|
const commits = file.commits.map((commit, k) => {
|
|
320
537
|
const refs = claims.resolved[k] ?? [];
|
|
538
|
+
const mine = concerns[k] ?? [];
|
|
321
539
|
const touched = [...new Set(refs.flatMap((r) => diff.units.get(r.unit)?.path ?? []))].sort();
|
|
322
540
|
const changes = touched.map((path) => {
|
|
323
541
|
const state = stateAt(path, k);
|
|
@@ -346,7 +564,8 @@ function project(diff, file, claims, issues) {
|
|
|
346
564
|
return {
|
|
347
565
|
index: k,
|
|
348
566
|
message: commit.message,
|
|
349
|
-
...
|
|
567
|
+
...withText(commit.narration ?? null, commit.diagram ?? null),
|
|
568
|
+
...(mine.length === 0 ? {} : { concerns: mine }),
|
|
350
569
|
refs,
|
|
351
570
|
changes,
|
|
352
571
|
};
|
|
@@ -366,6 +585,26 @@ function project(diff, file, claims, issues) {
|
|
|
366
585
|
}
|
|
367
586
|
return commits;
|
|
368
587
|
}
|
|
588
|
+
/**
|
|
589
|
+
* Why an "answered" response is not an answer, or null. It must answer a question and point at a
|
|
590
|
+
* commit, or a unit of it, that carries narration or a diagram to explain it.
|
|
591
|
+
*/
|
|
592
|
+
function answerProblem(file, r, isQuestion) {
|
|
593
|
+
if (!isQuestion)
|
|
594
|
+
return "answered is only for questions; use addressed, declined or deferred";
|
|
595
|
+
if (r.at === undefined)
|
|
596
|
+
return 'an answer needs "at" naming the commit that explains it';
|
|
597
|
+
const commit = file.commits[r.at.commit - 1];
|
|
598
|
+
if (commit === undefined)
|
|
599
|
+
return `at.commit ${r.at.commit} is not a commit of this plan`;
|
|
600
|
+
const target = r.at.unit;
|
|
601
|
+
const ref = target === undefined ? undefined : commit.units.find((u) => u.unit === target);
|
|
602
|
+
if (target !== undefined && ref === undefined) {
|
|
603
|
+
return `commit ${r.at.commit} does not take ${target}`;
|
|
604
|
+
}
|
|
605
|
+
const explains = [commit, ref].some((x) => x?.narration !== undefined || x?.diagram !== undefined);
|
|
606
|
+
return explains ? null : `commit ${r.at.commit} has no narration or diagram to point at`;
|
|
607
|
+
}
|
|
369
608
|
/**
|
|
370
609
|
* Parses and checks a draft against the diff: every change line used exactly once, every
|
|
371
610
|
* commit's tree valid, and the last commit equal to the tip. Collects all issues at once.
|
|
@@ -406,8 +645,25 @@ export function validatePlan(diff, draft, opts = {}) {
|
|
|
406
645
|
const warnings = review === null
|
|
407
646
|
? []
|
|
408
647
|
: review.comments
|
|
409
|
-
.filter((c) => !answered.has(c.id))
|
|
648
|
+
.filter((c) => c.author !== "agent" && !answered.has(c.id))
|
|
410
649
|
.map((c) => ({ kind: "unanswered", comment: c.id, review: review.planVersion }));
|
|
650
|
+
file.commits.forEach((c, commit) => {
|
|
651
|
+
const length = (c.message.split("\n")[0] ?? "").length;
|
|
652
|
+
if (length > LONG_TITLE)
|
|
653
|
+
warnings.push({ kind: "long-title", commit, length });
|
|
654
|
+
});
|
|
655
|
+
const asked = new Map(review?.comments.map((c) => [c.id, c.kind === "question"]));
|
|
656
|
+
for (const r of file.responses) {
|
|
657
|
+
const isQuestion = asked.get(r.comment);
|
|
658
|
+
if (isQuestion === undefined)
|
|
659
|
+
continue;
|
|
660
|
+
if (r.status === "addressed" && isQuestion) {
|
|
661
|
+
warnings.push({ kind: "question-addressed", comment: r.comment });
|
|
662
|
+
}
|
|
663
|
+
const problem = r.status === "answered" ? answerProblem(file, r, isQuestion) : null;
|
|
664
|
+
if (problem !== null)
|
|
665
|
+
issues.push({ kind: "bad-answer", comment: r.comment, reason: problem });
|
|
666
|
+
}
|
|
411
667
|
if (issues.length > 0)
|
|
412
668
|
return { ok: false, issues };
|
|
413
669
|
return {
|