@shiyuphan/follow-me 0.5.0 → 0.6.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.
Files changed (102) hide show
  1. package/README.md +6 -3
  2. package/dist/src/cli.js +35 -11
  3. package/dist/src/comments.js +62 -9
  4. package/dist/src/definition.js +226 -0
  5. package/dist/src/evolve.js +145 -29
  6. package/dist/src/git.js +69 -13
  7. package/dist/src/host.js +12 -3
  8. package/dist/src/plan.js +26 -1
  9. package/dist/src/prompt.js +122 -12
  10. package/dist/src/server.js +120 -28
  11. package/dist/src/session.js +23 -0
  12. package/dist/src/types.js +2 -0
  13. package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-BXkLs7mV.js → abnfDiagram-WEYOIA3C-fYSYLdMl.js} +1 -1
  14. package/dist/ui/dist/assets/{arc-DNzjw7Ie.js → arc-DGuGSnwa.js} +1 -1
  15. package/dist/ui/dist/assets/{architecture-WOLXFQ4H-D66kgJJt.js → architecture-WOLXFQ4H-VFox_hV1.js} +1 -1
  16. package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-D8UWuhQr.js → architectureDiagram-ZHVYKT53-CBVK3nHZ.js} +1 -1
  17. package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-BLkmr62i.js → blockDiagram-RJAE7QA3-BFdsIWEr.js} +1 -1
  18. package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-3rZPYwOc.js → c4Diagram-XDPPPE54-BGntcPId.js} +1 -1
  19. package/dist/ui/dist/assets/channel-gz6cS9Z1.js +1 -0
  20. package/dist/ui/dist/assets/{chunk-2E7WHVMD-MMnvAgpB.js → chunk-2E7WHVMD-CNMuK9zj.js} +1 -1
  21. package/dist/ui/dist/assets/{chunk-4EMNKR2T-D9FGWA3l.js → chunk-4EMNKR2T-ByeTTJWS.js} +1 -1
  22. package/dist/ui/dist/assets/{chunk-55XCU76K-DuOUaV8F.js → chunk-55XCU76K-99xE7cLH.js} +1 -1
  23. package/dist/ui/dist/assets/{chunk-7S6XSTFO-p3YWPv0m.js → chunk-7S6XSTFO-ZVmT7eMZ.js} +1 -1
  24. package/dist/ui/dist/assets/{chunk-AQDTSKZB-DWfKYjwk.js → chunk-AQDTSKZB-B981JORL.js} +1 -1
  25. package/dist/ui/dist/assets/{chunk-DSNLSHJZ-C3CIwpv0.js → chunk-DSNLSHJZ-B2z7FX4h.js} +1 -1
  26. package/dist/ui/dist/assets/{chunk-HAGFOTED-Bwfl9gme.js → chunk-HAGFOTED-CHFbBS-4.js} +1 -1
  27. package/dist/ui/dist/assets/{chunk-HNE4Q36P-0_oeU0le.js → chunk-HNE4Q36P-rq11xv9X.js} +1 -1
  28. package/dist/ui/dist/assets/{chunk-NQOKRVIF-z_LPPPx5.js → chunk-NQOKRVIF-BYmg9kSX.js} +1 -1
  29. package/dist/ui/dist/assets/{chunk-UHW7PFBP-BH28V2yu.js → chunk-UHW7PFBP-JvL9IXWg.js} +1 -1
  30. package/dist/ui/dist/assets/{chunk-VQ5W62QI-BXDajDf7.js → chunk-VQ5W62QI-CYMQaAKn.js} +1 -1
  31. package/dist/ui/dist/assets/{chunk-WQQL4CDK-BphBlfKj.js → chunk-WQQL4CDK-CXnR0PHT.js} +1 -1
  32. package/dist/ui/dist/assets/{chunk-XXDRQBXY-BiCT5NwV.js → chunk-XXDRQBXY-CDUWJCXi.js} +1 -1
  33. package/dist/ui/dist/assets/{chunk-YZO67HFG-Bg5Pu8BX.js → chunk-YZO67HFG-mZfXIx0h.js} +1 -1
  34. package/dist/ui/dist/assets/{chunk-ZAUX2LTX-C_Tgd8tB.js → chunk-ZAUX2LTX-DBxVTCHX.js} +1 -1
  35. package/dist/ui/dist/assets/{chunk-ZCHMQSOR-MBRcMi8-.js → chunk-ZCHMQSOR-DrWA4IBo.js} +1 -1
  36. package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-1wyeCe5y.js → classDiagram-v2-4MYWR3UA-Beah5uxA.js} +1 -1
  37. package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-BtOYclF6.js → cose-bilkent-JH36ORCC-UaE-jnHk.js} +1 -1
  38. package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-dJmoOG8g.js → cynefin-EF2NZ3EQ-DKfGaCR8.js} +1 -1
  39. package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-AloyqbEN.js → cynefinDiagram-WF24UDRP-tTUzTs3D.js} +1 -1
  40. package/dist/ui/dist/assets/{dagre-NK4GIZVX-NCZhdnfb.js → dagre-NK4GIZVX-jSn35_T7.js} +1 -1
  41. package/dist/ui/dist/assets/{diagram-CBK3MWTC-D8w7x5Gb.js → diagram-CBK3MWTC-7W2uw8Zc.js} +1 -1
  42. package/dist/ui/dist/assets/{diagram-I5XIIV2D-BUFnCGlp.js → diagram-I5XIIV2D-CcfyQVh9.js} +1 -1
  43. package/dist/ui/dist/assets/{diagram-ODIL2UCW-BULmHKaW.js → diagram-ODIL2UCW-BjGvaLis.js} +1 -1
  44. package/dist/ui/dist/assets/{diagram-PFBEJUXE-B5MqFo0r.js → diagram-PFBEJUXE-jvdCrt8B.js} +1 -1
  45. package/dist/ui/dist/assets/{diagram-RJA3I4VV-BIkDLL8E.js → diagram-RJA3I4VV-D65_Vpi_.js} +1 -1
  46. package/dist/ui/dist/assets/{diagram-TYUNFSNR-Ca4gsQV8.js → diagram-TYUNFSNR-C52roQBE.js} +1 -1
  47. package/dist/ui/dist/assets/{dist-G85JKY5k.js → dist-BJd4jp9Y.js} +1 -1
  48. package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-rVjcFFtb.js → ebnfDiagram-V4P3TTPT-DXysoIBq.js} +1 -1
  49. package/dist/ui/dist/assets/{elk-QONTBCTQ-BWPdvULh.js → elk-QONTBCTQ-BVZcsgV3.js} +1 -1
  50. package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-BPxJJkRl.js → erDiagram-KYQIGQAA-BJnT4fVU.js} +1 -1
  51. package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-BSB9WYV3.js → eventmodeling-K75KTNOO-C5MzxCMY.js} +1 -1
  52. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-BqUgZSCz.js +1 -0
  53. package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-DKps530K.js → ganttDiagram-R7GPNTJY-CZRrnuah.js} +1 -1
  54. package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-D1d4UAFV.js → gitGraph-VSP46ZUC-l58aGSkh.js} +1 -1
  55. package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-N55yl6hc.js → gitGraphDiagram-PDYL7XHY-D4ks1WO-.js} +1 -1
  56. package/dist/ui/dist/assets/index-BW95UA5q.css +1 -0
  57. package/dist/ui/dist/assets/index-DOGXgoHF.js +134 -0
  58. package/dist/ui/dist/assets/{info-OHQRW6UA-CgxE3gf9.js → info-OHQRW6UA-BSqfzkdk.js} +1 -1
  59. package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-DOnUrnMf.js → infoDiagram-MVDRKAX5-B6NgVF_N.js} +1 -1
  60. package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-BIY0fFHB.js → ishikawaDiagram-HPEHV2GE-DvtfssEZ.js} +1 -1
  61. package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-CqA2Ern0.js → journeyDiagram-BOSHW7O7-DM64gT5v.js} +1 -1
  62. package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-vGrpQKIF.js → kanban-definition-FNSA4HC5-AliyRJHy.js} +1 -1
  63. package/dist/ui/dist/assets/{line-Brzv3qA6.js → line-CHncXbKO.js} +1 -1
  64. package/dist/ui/dist/assets/{linear-D9uIlqyh.js → linear-BWEWydI0.js} +1 -1
  65. package/dist/ui/dist/assets/{mermaid-parser.core-Bz9b-Thu.js → mermaid-parser.core-DGpOP8AM.js} +3 -3
  66. package/dist/ui/dist/assets/{mermaid.core-BaZipu3b.js → mermaid.core-BJtBFbJx.js} +4 -4
  67. package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-D6aie7uH.js → mindmap-definition-6OY57UG3-NjavnWBi.js} +1 -1
  68. package/dist/ui/dist/assets/{packet-JDAUHWVQ-28r3jVFY.js → packet-JDAUHWVQ-zuhpVsME.js} +1 -1
  69. package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-CbGvD4qP.js → pegDiagram-ECIUJNBX-sL4grM97.js} +1 -1
  70. package/dist/ui/dist/assets/{pie-XZMESJXO-DHpcGnZd.js → pie-XZMESJXO-C9l1Mp09.js} +1 -1
  71. package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-DuhauwnY.js → pieDiagram-GNMPY74T-rlRBr2U4.js} +1 -1
  72. package/dist/ui/dist/assets/{prism-core-CbZMOvrb.js → prism-core-DmuST30i.js} +1 -1
  73. package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-BfyNmVca.js → quadrantDiagram-D57K4MC3-4oQMip2Q.js} +1 -1
  74. package/dist/ui/dist/assets/{radar-ABXABTNO-Xp1TYwaj.js → radar-ABXABTNO-DvGg96L7.js} +1 -1
  75. package/dist/ui/dist/assets/{railroad-I3PHUGI6-c5FY4wwa.js → railroad-I3PHUGI6-8wdFKMZp.js} +1 -1
  76. package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-RDyWXqcL.js → railroad-abnf-LNEFI6M7-DUtOO0gn.js} +1 -1
  77. package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-CLD4jVou.js → railroad-ebnf-DWYD2UWJ-5YCGY1_d.js} +1 -1
  78. package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-17EYTA-w.js → railroad-peg-JR7BVNR7-Bwywxjhs.js} +1 -1
  79. package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-C4p2wtKe.js → railroadDiagram-Y72NRBL7-CwO6HSNQ.js} +1 -1
  80. package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-DTYIFN8T.js → requirementDiagram-EQYSW5RA-Cp0IbfNr.js} +1 -1
  81. package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-YDYhRyJU.js → sankeyDiagram-SY24JHO5-DeQKsO68.js} +1 -1
  82. package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-Crvy6IlU.js → sequenceDiagram-BLEHNHQU-DTDalCxF.js} +1 -1
  83. package/dist/ui/dist/assets/{src-BPod-Ype.js → src-XnrqevnL.js} +1 -1
  84. package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-BWr70YOQ.js → stateDiagram-v2-A6IESKWZ-2DZ-tKo9.js} +1 -1
  85. package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-DvbG4I9a.js → swimlanes-B3ENRKFQ-66yZJeRU.js} +1 -1
  86. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-rbX6bYG4.js +8 -0
  87. package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-CR950Bzb.js → timeline-definition-JXYKGLGB-Dm1xGU-d.js} +1 -1
  88. package/dist/ui/dist/assets/{treeView-D4JQ5SDB-DSDDXGrk.js → treeView-D4JQ5SDB-BvgsbSot.js} +1 -1
  89. package/dist/ui/dist/assets/{treemap-SAJKECNS-Ct5w2_RR.js → treemap-SAJKECNS-C3lnbG1a.js} +1 -1
  90. package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-_eP0371y.js → usecaseDiagram-WPOLBNNX-B6iPNIhZ.js} +1 -1
  91. package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-Cu-IA57L.js → vennDiagram-Q3Y3IDCW-D5dgflg9.js} +1 -1
  92. package/dist/ui/dist/assets/{wardley-7MLQ67FV-r8yd6frN.js → wardley-7MLQ67FV-Dn9mLIB4.js} +1 -1
  93. package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-CBUUiRQm.js → wardleyDiagram-IVAIDKWA-W1EwzN_X.js} +1 -1
  94. package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-DEVQjvgG.js → xychartDiagram-4URNHDU4-CKRzMUvp.js} +1 -1
  95. package/dist/ui/dist/index.html +2 -2
  96. package/package.json +1 -1
  97. package/skills/follow-me/SKILL.md +3 -1
  98. package/dist/ui/dist/assets/channel-B9JVdOBZ.js +0 -1
  99. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-N-Spyk2o.js +0 -1
  100. package/dist/ui/dist/assets/index-B8TYcxF8.css +0 -1
  101. package/dist/ui/dist/assets/index-BKtWBS7Z.js +0 -134
  102. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-D2wU-jW6.js +0 -8
package/dist/src/git.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { link, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { isSha, neededBlobs, parseDiff } from "./diff.js";
4
+ import { isRepoPath, isSha, neededBlobs, parseDiff } from "./diff.js";
5
5
  export class GitError extends Error {
6
6
  name = "GitError";
7
7
  args;
8
- constructor(args, stderr) {
8
+ /** git's exit status, or null when it did not run or was killed. */
9
+ status;
10
+ constructor(args, stderr, status = null) {
9
11
  super(`git ${args.join(" ")}: ${stderr}`);
10
12
  this.args = args;
13
+ this.status = status;
11
14
  }
12
15
  }
13
16
  function git(repo, args, opts = {}) {
@@ -18,8 +21,10 @@ function git(repo, args, opts = {}) {
18
21
  maxBuffer: 1 << 30,
19
22
  env: { ...process.env, ...opts.env },
20
23
  }, (error, stdout, stderr) => {
21
- if (error)
22
- reject(new GitError(args, stderr.toString("utf8").trim() || error.message));
24
+ if (error) {
25
+ const status = typeof error.code === "number" ? error.code : null;
26
+ reject(new GitError(args, stderr.toString("utf8").trim() || error.message, status));
27
+ }
23
28
  else
24
29
  resolve(stdout);
25
30
  });
@@ -97,25 +102,76 @@ export async function defaultBaseRef(repo) {
97
102
  }
98
103
  throw new Error("neither main nor master exists; pass --base <ref>");
99
104
  }
100
- async function catBlobs(repo, shas) {
101
- const blobs = new Map();
102
- if (shas.length === 0)
103
- return blobs;
104
- const out = await git(repo, ["cat-file", "--batch"], { input: `${shas.join("\n")}\n` });
105
+ /** The blob named by each object spec, in order, as latin1; throws when one is not a blob. */
106
+ async function catBatch(repo, specs) {
107
+ if (specs.length === 0)
108
+ return [];
109
+ const input = Buffer.from(`${specs.join("\n")}\n`, "latin1");
110
+ const out = await git(repo, ["cat-file", "--batch"], { input });
111
+ const blobs = [];
105
112
  let at = 0;
106
- for (const sha of shas) {
113
+ for (const spec of specs) {
107
114
  const nl = out.indexOf(0x0a, at);
108
115
  const header = out.subarray(at, nl).toString("latin1");
109
116
  const m = /^([0-9a-f]+) blob (\d+)$/.exec(header);
110
- if (nl === -1 || m === null || m[1] !== sha) {
111
- throw new Error(`cat-file returned "${header}" for ${sha}`);
117
+ if (nl === -1 || m === null || (isSha(spec) && m[1] !== spec)) {
118
+ throw new Error(`cat-file returned "${header}" for ${spec}`);
112
119
  }
113
120
  const size = Number(m[2]);
114
- blobs.set(sha, out.subarray(nl + 1, nl + 1 + size).toString("latin1"));
121
+ blobs.push(out.subarray(nl + 1, nl + 1 + size).toString("latin1"));
115
122
  at = nl + 1 + size + 1;
116
123
  }
117
124
  return blobs;
118
125
  }
126
+ async function catBlobs(repo, shas) {
127
+ const blobs = await catBatch(repo, shas);
128
+ return new Map(shas.map((sha, i) => [sha, blobs[i] ?? ""]));
129
+ }
130
+ /** Each path's bytes at the commit, as latin1. */
131
+ export async function blobsAt(repo, commit, paths) {
132
+ const blobs = await catBatch(repo, paths.map((p) => `${commit}:${p}`));
133
+ return new Map(paths.map((p, i) => [p, blobs[i] ?? ""]));
134
+ }
135
+ /**
136
+ * Lines of the commit's tree, not the working tree, that match any of the POSIX extended
137
+ * patterns, in files matching the pathspecs. Binary files are skipped.
138
+ */
139
+ export async function grepTip(repo, commit, patterns, pathspecs) {
140
+ const args = [
141
+ "grep",
142
+ "-n",
143
+ "-E",
144
+ "-I",
145
+ "-z",
146
+ "--no-color",
147
+ "--no-column",
148
+ ...patterns.flatMap((p) => ["-e", p]),
149
+ commit,
150
+ "--",
151
+ ...pathspecs,
152
+ ];
153
+ let out;
154
+ try {
155
+ out = await git(repo, args);
156
+ }
157
+ catch (error) {
158
+ // grep exits 1 when nothing matched.
159
+ if (error instanceof GitError && error.status === 1)
160
+ return [];
161
+ throw error;
162
+ }
163
+ const prefix = `${commit}:`;
164
+ return out
165
+ .toString("latin1")
166
+ .split("\n")
167
+ .flatMap((record) => {
168
+ const [name = "", line = "", ...text] = record.split("\0");
169
+ const path = name.slice(prefix.length);
170
+ if (!name.startsWith(prefix) || !isRepoPath(path) || !/^\d+$/.test(line))
171
+ return [];
172
+ return [{ path, line: Number(line), text: text.join("\0") }];
173
+ });
174
+ }
119
175
  const DIFF_FLAGS = ["--no-renames", "--full-index", "--no-color", "--no-ext-diff", "--no-textconv"];
120
176
  export async function loadDiff(repo, baseRef, tipRef) {
121
177
  const [tip, baseCommit] = await Promise.all([
package/dist/src/host.js CHANGED
@@ -6,8 +6,8 @@ import { join } from "node:path";
6
6
  import { parseCarried, parseComment, parseEvent, parseEventBatch, } from "./comments.js";
7
7
  import { carryComments } from "./evolve.js";
8
8
  import { isPlanVersion, serializePlanFile, validatePlan } from "./plan.js";
9
- import { HEALTH_APP, isSessionId, loadDraftComments, PortInUseError, RegistrationError, startServer, } from "./server.js";
10
- import { BranchMovedError, diffOfPlan, isAlive, openSessionAt, SessionFileError, SessionLockedError, } from "./session.js";
9
+ import { HEALTH_APP, isSessionId, loadDraft, PortInUseError, RegistrationError, startServer, } from "./server.js";
10
+ import { BranchMovedError, diffOfPlan, isAlive, openSessionAt, SessionFileError, SessionLockedError, sessionComments, } from "./session.js";
11
11
  const RECORD = "server.json";
12
12
  const DEFAULT_IDLE_MS = 30_000;
13
13
  const START_TIMEOUT_MS = 10_000;
@@ -127,7 +127,15 @@ async function loadHosted(registration, held) {
127
127
  throw new RegistrationError(`plan v${version} no longer fits ${branch}; check it again`);
128
128
  }
129
129
  const reviewsDir = session.reviewsDir();
130
- const comments = await loadDraftComments(reviewsDir, version);
130
+ const draft = await loadDraft(reviewsDir, version);
131
+ const { comments } = draft;
132
+ // A restart in the middle of a review that swapped: the draft names what was said before.
133
+ const earlier = draft.carried === null
134
+ ? null
135
+ : {
136
+ comments: sessionComments(draft.carried, reviewed.filter((r) => r.version < version).map((r) => r.review)),
137
+ concerns: draft.carriedConcerns,
138
+ };
131
139
  const prior = session.priorReview(mode, version);
132
140
  const previous = prior === null || prior.comments.length === 0
133
141
  ? null
@@ -151,6 +159,7 @@ async function loadHosted(registration, held) {
151
159
  release,
152
160
  previous,
153
161
  carried,
162
+ earlier,
154
163
  state: {
155
164
  mode,
156
165
  anchor: session.anchor,
package/dist/src/plan.js CHANGED
@@ -17,6 +17,10 @@ const STATUSES = ["addressed", "declined", "deferred", "answered"];
17
17
  function isResponseStatus(s) {
18
18
  return STATUSES.some((x) => x === s);
19
19
  }
20
+ const SECTIONS = ["story", "steps", "details", "wiring"];
21
+ function isCommitSection(s) {
22
+ return SECTIONS.some((x) => x === s);
23
+ }
20
24
  const SEVERITIES = ["blocker", "major", "minor"];
21
25
  const CONCERN_KINDS = ["bug", "missing-test", "naming", "risk"];
22
26
  export function isSeverity(s) {
@@ -90,6 +94,20 @@ function parseConcern(v, onUnit) {
90
94
  return "lines: expected a list of change indices";
91
95
  return { severity, kind, text, lines: [...new Set(indices)].sort((a, b) => a - b) };
92
96
  }
97
+ /** A concern as validation placed it, stored beside a review: with its id and its unit. */
98
+ export function parsePlacedConcern(v) {
99
+ if (!isFields(v))
100
+ return "expected an object";
101
+ if (!isConcernId(v.id))
102
+ return "id: expected a concern id";
103
+ if (v.unit !== undefined && !(typeof v.unit === "string" && isUnitId(v.unit))) {
104
+ return "unit: expected a unit id";
105
+ }
106
+ const concern = parseConcern(v, v.unit !== undefined);
107
+ if (typeof concern === "string")
108
+ return concern;
109
+ return { ...concern, id: v.id, ...(v.unit === undefined ? {} : { unit: v.unit }) };
110
+ }
93
111
  /** Absent, null and [] all mean no concerns, so a planner may always write the field. */
94
112
  function parseConcerns(v, onUnit) {
95
113
  if (v === undefined || v === null)
@@ -209,6 +227,10 @@ export function parsePlanFile(draft) {
209
227
  }
210
228
  if (typeof c.message !== "string")
211
229
  bad(`${at}.message`, "expected a string");
230
+ const section = c.section ?? null;
231
+ if (section !== null && !isCommitSection(section)) {
232
+ bad(`${at}.section`, `expected one of ${SECTIONS.join(", ")}`);
233
+ }
212
234
  const extras = parseExtras(c, false);
213
235
  if ("error" in extras)
214
236
  bad(`${at}.${extras.field}`, extras.error);
@@ -225,7 +247,8 @@ export function parsePlanFile(draft) {
225
247
  }
226
248
  }
227
249
  if (typeof c.message === "string" && !("error" in extras)) {
228
- commits.push({ message: c.message, ...extras, units });
250
+ const placed = isCommitSection(section) ? { section } : {};
251
+ commits.push({ message: c.message, ...placed, ...extras, units });
229
252
  }
230
253
  }
231
254
  }
@@ -273,6 +296,7 @@ export function serializePlanFile(file) {
273
296
  tip: file.tip,
274
297
  commits: file.commits.map((c) => ({
275
298
  message: c.message,
299
+ ...(c.section === undefined ? {} : { section: c.section }),
276
300
  ...serializeExtras(c),
277
301
  units: c.units.map((ref) => ref.narration === undefined && ref.diagram === undefined && ref.concerns === undefined
278
302
  ? formatUnitRef(ref)
@@ -564,6 +588,7 @@ function project(diff, file, claims, issues) {
564
588
  return {
565
589
  index: k,
566
590
  message: commit.message,
591
+ ...(commit.section === undefined ? {} : { section: commit.section }),
567
592
  ...withText(commit.narration ?? null, commit.diagram ?? null),
568
593
  ...(mine.length === 0 ? {} : { concerns: mine }),
569
594
  refs,
@@ -1,4 +1,5 @@
1
1
  import { tipStartOf } from "./diff.js";
2
+ import { anchorOn, concernOn } from "./evolve.js";
2
3
  /** Consecutive runs collapse: [0,1,2,3,7] becomes "0-3,7". */
3
4
  function formatRanges(changes) {
4
5
  const sorted = [...new Set(changes)].sort((a, b) => a - b);
@@ -383,10 +384,23 @@ function quoteHunk(units, commits, unit, k, picked) {
383
384
  return { line, text: lines.map((l) => l.text) };
384
385
  }
385
386
  const SEVERITY_RANK = { blocker: 0, major: 1, minor: 2 };
387
+ /** Marks the label of what was said before its code changed, and places what lost its code. */
388
+ function marked(ctx, id, where, quote) {
389
+ const mark = ctx.marks?.get(id);
390
+ if (mark === undefined)
391
+ return where;
392
+ if (mark.gone) {
393
+ return { path: mark.path, line: null, label: `the code is gone after v${mark.since}`, quote };
394
+ }
395
+ return { ...where, label: `${where.label} · the code changed after v${mark.since}` };
396
+ }
386
397
  function lineLabel(line, fallback) {
387
398
  return line === null ? fallback : `line ${line}`;
388
399
  }
389
400
  function whereComment(ctx, c) {
401
+ return marked(ctx, c.id, whereOnTip(ctx, c), c.context.lines ?? []);
402
+ }
403
+ function whereOnTip(ctx, c) {
390
404
  const { anchor } = c;
391
405
  switch (anchor.kind) {
392
406
  case "plan":
@@ -429,6 +443,9 @@ function whereComment(ctx, c) {
429
443
  }
430
444
  }
431
445
  function whereConcern(ctx, kept) {
446
+ return marked(ctx, kept.concern.id, concernOnTip(ctx, kept), []);
447
+ }
448
+ function concernOnTip(ctx, kept) {
432
449
  const { concern, commit } = kept;
433
450
  if (concern.unit === undefined) {
434
451
  return { path: null, line: null, label: `commit ${commit + 1}`, quote: [] };
@@ -727,8 +744,12 @@ function threadOf(c) {
727
744
  function withSeverity(severity, text) {
728
745
  return severity === null ? text : `**${severity}**\n\n${text}`;
729
746
  }
730
- function collectItems(review, commits, units, responses) {
731
- const ctx = { commits, units };
747
+ function changedOf(ctx, id) {
748
+ const mark = ctx.marks?.get(id);
749
+ return mark === undefined ? {} : { changed: { since: mark.since, gone: mark.gone } };
750
+ }
751
+ function collectItems(review, ctx, responses) {
752
+ const { commits, units } = ctx;
732
753
  const fromReviewer = review.comments.filter((c) => c.author !== "agent");
733
754
  const walk = review.mode === "walk";
734
755
  const rest = fromReviewer.filter((c) => c.kind !== "question");
@@ -748,6 +769,7 @@ function collectItems(review, commits, units, responses) {
748
769
  return {
749
770
  where: it.where,
750
771
  item: {
772
+ id: kept.concern.id,
751
773
  ...concernSpan(ctx, kept.concern),
752
774
  path: it.where.path,
753
775
  oldPath: it.where.path,
@@ -757,6 +779,7 @@ function collectItems(review, commits, units, responses) {
757
779
  thread,
758
780
  resolved: false,
759
781
  excluded: noConcerns.has(kept.concern.id),
782
+ ...changedOf(ctx, kept.concern.id),
760
783
  },
761
784
  };
762
785
  }
@@ -765,6 +788,7 @@ function collectItems(review, commits, units, responses) {
765
788
  return {
766
789
  where: it.where,
767
790
  item: {
791
+ id: first.id,
768
792
  ...commentSpan(ctx, first),
769
793
  path: it.where.path,
770
794
  oldPath: it.where.path,
@@ -774,6 +798,7 @@ function collectItems(review, commits, units, responses) {
774
798
  thread: threadOf(first),
775
799
  resolved: resolved.has(first.id),
776
800
  excluded: noComments.has(first.id),
801
+ ...changedOf(ctx, first.id),
777
802
  },
778
803
  };
779
804
  });
@@ -786,6 +811,7 @@ function collectItems(review, commits, units, responses) {
786
811
  out.push({
787
812
  where,
788
813
  item: {
814
+ id: q.id,
789
815
  ...commentSpan(ctx, q),
790
816
  path: where.path,
791
817
  oldPath: where.path,
@@ -795,6 +821,7 @@ function collectItems(review, commits, units, responses) {
795
821
  thread: threadOf(q),
796
822
  resolved: resolved.has(q.id),
797
823
  excluded: noComments.has(q.id),
824
+ ...changedOf(ctx, q.id),
798
825
  },
799
826
  });
800
827
  }
@@ -805,7 +832,8 @@ function collectItems(review, commits, units, responses) {
805
832
  * Excluded ones are present and marked; whoever posts them skips those.
806
833
  */
807
834
  export function reportItems(review, commits, units, opts) {
808
- return collectItems(review, commits, units, opts.responses).map((c) => c.item);
835
+ const session = folded(review, commits, units, opts.earlier);
836
+ return collectItems(session.review, session.ctx, opts.responses).map((c) => c.item);
809
837
  }
810
838
  /** A short list of what the reviewer left out, so nothing vanishes without a trace. */
811
839
  function renderExcluded(collected) {
@@ -821,6 +849,64 @@ function renderExcluded(collected) {
821
849
  out.push("");
822
850
  return out;
823
851
  }
852
+ /**
853
+ * The review of the whole session on this version's diff: the earlier versions' comments on
854
+ * their carried anchors, the concerns kept there that this plan lost added to the commit now
855
+ * taking their unit, and a mark on each whose code changed. Each id appears once; this
856
+ * version's own copy wins.
857
+ */
858
+ function folded(review, commits, units, earlier) {
859
+ if (earlier === undefined)
860
+ return { review, ctx: { commits, units } };
861
+ const marks = new Map();
862
+ const mark = (id, at) => {
863
+ if (at.since !== null)
864
+ marks.set(id, { since: at.since, gone: at.unit === null, path: at.path });
865
+ };
866
+ const owner = (unit, fallback) => {
867
+ const k = commits.findIndex((c) => c.refs.some((r) => r.unit === unit));
868
+ return k === -1 ? Math.min(fallback, Math.max(commits.length - 1, 0)) : k;
869
+ };
870
+ const own = new Set(review.comments.map((c) => c.id));
871
+ const comments = earlier.comments
872
+ .filter((c) => !own.has(c.comment.id))
873
+ .map((c) => {
874
+ mark(c.comment.id, c);
875
+ const { anchor } = c.comment;
876
+ if (anchor.kind !== "hunk")
877
+ return c.comment;
878
+ const moved = anchorOn(c, units, owner(c.unit, anchor.commit));
879
+ return moved === null ? c.comment : { ...c.comment, anchor: moved };
880
+ });
881
+ const inPlan = new Set(commits.flatMap((c) => (c.concerns ?? []).map((k) => k.id)));
882
+ const lost = earlier.concerns.filter((c) => !inPlan.has(c.concern.id));
883
+ for (const c of lost)
884
+ mark(c.concern.id, c);
885
+ const decided = new Set((review.concerns ?? []).map((d) => d.concern));
886
+ return {
887
+ review: {
888
+ ...review,
889
+ comments: [...comments, ...review.comments],
890
+ concerns: [
891
+ ...lost.filter((c) => !decided.has(c.concern.id)).map((c) => c.decision),
892
+ ...(review.concerns ?? []),
893
+ ],
894
+ },
895
+ ctx: {
896
+ units,
897
+ marks,
898
+ commits: commits.map((commit, k) => {
899
+ const mine = lost.filter((c) => owner(c.unit, c.commit) === k);
900
+ if (mine.length === 0)
901
+ return commit;
902
+ return {
903
+ ...commit,
904
+ concerns: [...(commit.concerns ?? []), ...mine.map((c) => concernOn(c, units))],
905
+ };
906
+ }),
907
+ },
908
+ };
909
+ }
824
910
  /**
825
911
  * The consolidated report of a review, in the voices of both the reviewer and the agent: the
826
912
  * reviewer's comments and the agent's concerns the reviewer kept, grouped by file and ordered by
@@ -829,22 +915,24 @@ function renderExcluded(collected) {
829
915
  * Concerns the reviewer has not decided are left out, as are those dismissed.
830
916
  */
831
917
  export function renderReport(full, commits, units, opts) {
832
- const excluded = full.excluded;
918
+ const session = folded(full, commits, units, opts.earlier);
919
+ const { ctx } = session;
920
+ const excluded = session.review.excluded;
833
921
  if (excluded === undefined)
834
- return renderIncluded(full, commits, units, opts);
922
+ return renderIncluded(session.review, ctx, opts);
835
923
  const noComments = new Set(excluded.comments);
836
924
  const noConcerns = new Set(excluded.concerns);
837
925
  const review = {
838
- ...full,
839
- comments: full.comments.filter((c) => !noComments.has(c.id)),
840
- concerns: (full.concerns ?? []).filter((d) => !noConcerns.has(d.concern)),
926
+ ...session.review,
927
+ comments: session.review.comments.filter((c) => !noComments.has(c.id)),
928
+ concerns: (session.review.concerns ?? []).filter((d) => !noConcerns.has(d.concern)),
841
929
  };
842
- const tail = renderExcluded(collectItems(full, commits, units, opts.responses));
843
- const main = renderIncluded(review, commits, units, opts);
930
+ const tail = renderExcluded(collectItems(session.review, ctx, opts.responses));
931
+ const main = renderIncluded(review, ctx, opts);
844
932
  return tail.length === 0 ? main : `${main}${main.endsWith("\n") ? "" : "\n"}${tail.join("\n")}`;
845
933
  }
846
- function renderIncluded(review, commits, units, opts) {
847
- const ctx = { commits, units };
934
+ function renderIncluded(review, ctx, opts) {
935
+ const { commits, units } = ctx;
848
936
  const walk = review.mode === "walk";
849
937
  const fromReviewer = review.comments.filter((c) => c.author !== "agent");
850
938
  // A thread the agent started matters to it only once the reviewer has answered.
@@ -977,3 +1065,25 @@ export function renderShownConcern(concern, commit, ctx) {
977
1065
  "",
978
1066
  ].join("\n");
979
1067
  }
1068
+ function utf8(latin1) {
1069
+ return Buffer.from(latin1, "latin1").toString("utf8");
1070
+ }
1071
+ /** Where a name is defined, each with the commit it lands in and its first lines at the tip. */
1072
+ export function renderShownDefinitions(name, candidates) {
1073
+ if (candidates.length === 0)
1074
+ return `no definition of ${name} at the tip\n`;
1075
+ return `${candidates
1076
+ .flatMap((c) => {
1077
+ const where = c.commit !== null
1078
+ ? `in commit ${c.commit + 1}`
1079
+ : c.unit !== null
1080
+ ? `in ${c.unit}, which no commit of the plan takes`
1081
+ : "unchanged by the branch";
1082
+ const width = String(c.newLine + c.preview.length - 1).length;
1083
+ return [
1084
+ `${utf8(c.path)}:${c.newLine} ${c.kind}, ${where}`,
1085
+ ...c.preview.map((l, i) => ` ${String(c.newLine + i).padStart(width)} ${utf8(l)}`),
1086
+ ];
1087
+ })
1088
+ .join("\n")}\n`;
1089
+ }