@pmelab/gtd 55.0.0 → 57.0.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 (83) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +72 -80
  3. package/dist/chunks/{reserved-DHafcW7t.mjs → reserved-X2RkNHin.mjs} +110 -318
  4. package/dist/chunks/{review-BJGKwlYv.mjs → review-v7sh5GF4.mjs} +30 -125
  5. package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
  6. package/dist/chunks/{tail-B9Hw2AFB.mjs → tail-S04KEewi.mjs} +144 -56
  7. package/dist/gtd.bundle.mjs +259 -22
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +6 -6
  10. package/dist/web/main.css +1 -1
  11. package/dist/web/main.js +551 -66
  12. package/dist/workflow-kit.mjs +1 -1
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +63 -175
  15. package/dist/workflows/fix.mjs +3 -3
  16. package/dist/workflows/review.mjs +3 -3
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +603 -727
  19. package/package.json +1 -1
  20. package/skills/authoring/SKILL.md +3 -3
  21. package/src/cli/Cli.ts +5 -5
  22. package/src/edge/Edge.test.ts +1 -1
  23. package/src/steering/FORMATS.md +5 -1
  24. package/src/steering/SteeringFormats.test.ts +2 -2
  25. package/src/steering/contract.test.ts +1 -0
  26. package/src/steering/findings.test.ts +47 -0
  27. package/src/steering/findings.ts +350 -0
  28. package/src/steering/fixtures/findings/gate.md +34 -0
  29. package/src/steering/fixtures/findings/gate.read.json +87 -0
  30. package/src/steering/index.test.ts +2 -2
  31. package/src/steering/index.ts +4 -3
  32. package/src/steering/review.test.ts +1 -75
  33. package/src/steering/review.ts +1 -74
  34. package/src/workflow/compile.test.ts +1 -0
  35. package/src/workflows/feature/criteria-coverage.feature +1 -7
  36. package/src/workflows/feature/criteria.test.ts +1 -41
  37. package/src/workflows/feature/criteria.ts +2 -14
  38. package/src/workflows/feature/fast-path.feature +0 -1
  39. package/src/workflows/feature/feature.test.ts +1 -1
  40. package/src/workflows/feature/feature.ts +16 -25
  41. package/src/workflows/feature/flag-mode.feature +1 -1
  42. package/src/workflows/feature/lens-models.feature +0 -1
  43. package/src/workflows/feature/lens-review.feature +323 -172
  44. package/src/workflows/feature/naive-test.feature +258 -0
  45. package/src/workflows/feature/one-step-land.feature +0 -1
  46. package/src/workflows/feature/refactor-absorbs.feature +1 -8
  47. package/src/workflows/feature/refactor-first.feature +2 -11
  48. package/src/workflows/feature/refactor-rebuild.feature +4 -25
  49. package/src/workflows/feature/refactor-review.feature +33 -118
  50. package/src/workflows/feature/refactor.test.ts +0 -2
  51. package/src/workflows/feature/refactor.ts +0 -1
  52. package/src/workflows/feature/review-gate.feature +7 -24
  53. package/src/workflows/feature/reviewer-walkthrough.feature +16 -104
  54. package/src/workflows/feature/seam-rejection.feature +0 -1
  55. package/src/workflows/feature/seam.feature +0 -1
  56. package/src/workflows/feature/seam.test.ts +1 -2
  57. package/src/workflows/feature/seam.ts +56 -10
  58. package/src/workflows/feature/standards.feature +75 -35
  59. package/src/workflows/fix/fix-lens-origin.feature +20 -7
  60. package/src/workflows/fix/fix-quality.feature +102 -23
  61. package/src/workflows/review/review-door.feature +62 -59
  62. package/src/workflows/shared/learn.test.ts +0 -14
  63. package/src/workflows/shared/learn.ts +1 -6
  64. package/src/workflows/shared/lenses.ts +149 -43
  65. package/src/workflows/shared/options.ts +9 -1
  66. package/src/workflows/shared/prose.ts +0 -6
  67. package/src/workflows/shared/review.test.ts +27 -26
  68. package/src/workflows/shared/review.ts +29 -196
  69. package/src/workflows/shared/skills.test.ts +0 -4
  70. package/src/workflows/shared/skills.ts +0 -5
  71. package/src/workflows/shared/steps.ts +1 -1
  72. package/src/workflows/shared/tail.ts +18 -9
  73. package/src/workflows/shared/text.test.ts +125 -329
  74. package/src/workflows/shared/text.ts +173 -471
  75. package/src/workflows/shared/vars.ts +2 -3
  76. package/src/workflows/feature/hands-on-skip.feature +0 -316
  77. package/src/workflows/feature/prototype-rounds.feature +0 -156
  78. package/src/workflows/feature/prototype.feature +0 -428
  79. package/src/workflows/feature/prototype.test.ts +0 -43
  80. package/src/workflows/feature/prototype.ts +0 -198
  81. package/src/workflows/feature/review-answers.feature +0 -324
  82. package/src/workflows/feature/severity-budget.feature +0 -116
  83. package/src/workflows/feature/severity-sorted-review.feature +0 -198
@@ -1,13 +1,12 @@
1
- import { answered, changesSince, head, judge, numeric, read, refuse, requireRevert, restoreScript, run, showScript, vars, writeScript } from "../flows.mjs";
2
- import { b as sectionedReview, h as reviewChunks, m as REVIEW_SECTIONS, v as reviewRisks, y as reviewSections } from "./steering-CLhu8C7q.mjs";
3
- import { removedAssertions, review } from "../workflow-kit.mjs";
4
- import { A as reviewing, At as withWalkthrough, Bt as parseStandards, C as fixCheck, E as fixRisks, F as chunkTexts, Ft as conflictBody, Ht as standardKey, It as conflictFor, Lt as conflictTitle, N as buildReviewTriageMessage, O as reviewMissing, Ot as walkthroughBlock, Pt as STANDARDS, R as composedReview, Rt as isConflictTitle, S as collecting, Tt as unranked, Vt as renderStandards, _t as reviewNotesCapture, b as awaitReview, d as healthy, mt as reviewEditsCapture, pt as reviewEditNotesCapture, st as rankedReview, u as guarded, v as REVIEW, vt as reviewRankingMessage, w as fixNits, wt as titledNotes, y as answerReviewQuestions, zt as oneLine } from "./reserved-DHafcW7t.mjs";
5
- import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-B9Hw2AFB.mjs";
1
+ import { answered, changesSince, head, judge, numeric, read, refuse, requireRevert, restoreScript, run, vars, writeScript } from "../flows.mjs";
2
+ import { S as reviewSections, _ as REVIEW_SECTIONS, v as reviewChunks, x as reviewRisks } from "./steering-CCNvngSH.mjs";
3
+ import { review } from "../workflow-kit.mjs";
4
+ import { At as oneLine, C as collecting, D as fixRisks, Dt as conflictFor, Et as conflictBody, Mt as renderStandards, Nt as standardKey, Ot as conflictTitle, P as buildReviewTriageMessage, T as fixNits, Tt as STANDARDS, b as answerReviewQuestions, d as guarded, et as nitFixBlock, f as healthy, j as reviewing, jt as parseStandards, k as reviewMissing, kt as isConflictTitle, lt as reviewEditNotesCapture, pt as reviewNotesCapture, ut as reviewEditsCapture, w as fixCheck, x as awaitReview, xt as withSummary, y as REVIEW } from "./reserved-X2RkNHin.mjs";
5
+ import { i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "./tail-S04KEewi.mjs";
6
6
  //#region src/workflows/shared/learn.ts
7
7
  const SUMMARY_MAX = 200;
8
- const BANNER = /^_(?:The fixer left this unresolved\.|A fix turn removed this while renaming another finding — check whether it was fixed\.|No detail given\.)_\s*/;
9
8
  const summary = (body) => {
10
- const text = oneLine(body).replace(BANNER, "");
9
+ const text = oneLine(body);
11
10
  return text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX - 1)}…` : text;
12
11
  };
13
12
  /** The standard a won't-fix or an own answer would make; a fix states none. */
@@ -276,139 +275,41 @@ const triage = (severities) => async (notes) => {
276
275
  return [n.id, "edit"];
277
276
  }));
278
277
  };
279
- /** Review order: security, one-way doors, the judge unsure, then should, then could. */
280
- const chunkKey = (s) => s.tier === "could" ? 4 : s.tier === "should" ? 3 : s.security === true ? 0 : s.rank === 0 ? 1 : 2;
281
- /** A chunk's text, then the code each of its pointers covers: a bare path the whole file. */
282
- const chunkEvidence = (text, files) => [text, ...files.map(({ path, line, rangeEnd }) => {
283
- const where = `${path}${line === void 0 ? "" : `#${line}${rangeEnd === void 0 ? "" : `-${rangeEnd}`}`}`;
284
- const lines = read(path.replace(/^\.\//, ""))?.split("\n");
285
- if (lines === void 0) return `${where}: no longer exists`;
286
- return `${where}:\n${(line === void 0 ? lines : lines.slice(line - 1, rangeEnd ?? line)).join("\n")}`;
287
- })].join("\n\n");
288
- const bytes = (text) => new TextEncoder().encode(text).length;
289
- /** Fills each batch while the budget's even share still holds its widest evidence, so nothing is cut that fits alone. */
290
- const batched = (items, budget) => {
291
- const batches = [];
292
- let batch = [];
293
- let widest = 0;
294
- for (const item of items) {
295
- const size = bytes(item.evidence);
296
- if (batch.length > 0 && Math.floor(budget / (batch.length + 1)) < Math.max(widest, size)) {
297
- batches.push(batch);
298
- batch = [];
299
- widest = 0;
300
- }
301
- batch.push(item);
302
- widest = Math.max(widest, size);
303
- }
304
- return batch.length === 0 ? batches : [...batches, batch];
305
- };
306
- /**
307
- * Rates every chunk the reviewer wrote and sorts the \`findings\` among them
308
- * first in \`full\`, each led by its tier and reason; a walkthrough chunk
309
- * keeps its place and carries no tier. It is still rated, so a review with no
310
- * findings still reaches \`review.ranking\` and older histories replay.
311
- */
312
- const ranked = async (review, full, findings) => {
313
- const texts = chunkTexts(full);
314
- const chunks = reviewChunks(review).map((c, i) => ({
315
- id: `C${i + 1}`,
316
- title: c.title,
317
- evidence: chunkEvidence(texts.get(c.title) ?? "", c.files)
318
- }));
319
- if (chunks.length === 0) return full;
320
- const minP = numeric(vars.reviewSeverityMinP, Infinity);
321
- const batches = batched(chunks, numeric(vars.judgeBudgetBytes, 32768));
322
- const severities = /* @__PURE__ */ new Map();
323
- for (const [i, batch] of batches.entries()) {
324
- const judgment = await judge("review.ranking", {
325
- questions: batch.flatMap((c) => severityQuestions(c.id, `the change review chunk "${c.id}" ("${c.title}") walks through`, `the change in review chunk "${c.id}"`)),
326
- evidence: Object.fromEntries(batch.map((c) => [c.id, c.evidence])),
327
- message: reviewRankingMessage(),
328
- label: batches.length === 1 ? "Ranking the review" : `Ranking the review (${i + 1}/${batches.length})`
329
- });
330
- for (const c of batch) severities.set(c.id, severityOf(c, judgment, minP));
331
- }
332
- return rankedReview(full, chunks.map((c, i) => ({
333
- c,
334
- i,
335
- s: severities.get(c.id)
336
- })).filter(({ c }) => findings.has(c.title)).sort((x, y) => chunkKey(x.s) - chunkKey(y.s) || x.i - y.i).map(({ c, s }) => ({
337
- title: c.title,
338
- label: s.reason
339
- })));
340
- };
341
278
  const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
342
- const composedOf = (base, report, nitFix, answered) => ({
343
- ...report,
344
- nitFix,
345
- answered,
346
- standards: {
347
- added: standardsAddedSince(base),
348
- undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
349
- }
350
- });
351
- /**
352
- * gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
353
- * turn. A re-review
354
- * after fixed nits restores the doc as committed at `carry`, answers kept;
355
- * a fresh one leaves out the notes the human already answered.
356
- */
357
- const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new Set()) => run("review.composing", carry === void 0 ? writeScript(REVIEW, composedReview(base, {
358
- ...composedOf(base, report, nitFix, answered),
359
- weakened: removedAssertions(changesSince(base))
360
- })) : showScript(carry, REVIEW, REVIEW), {
361
- label: "Composing the review",
362
- file: REVIEW,
363
- base
364
- });
365
- /**
366
- * Refuses a review with a chunk pointing at no code, or a lap note without a
367
- * chunk of its own. A range with no code change has nothing to point at, so
368
- * there only a note's chunk needs a pointer.
369
- */
370
- const requirePointers = (review, notes, codeChanged) => {
279
+ /** Refuses a review with a chunk pointing at no code; a range with no code change has nothing to point at. */
280
+ const requirePointers = (review, codeChanged) => {
371
281
  const chunks = reviewChunks(review);
372
- const titles = new Set(chunks.map((c) => c.title));
373
- const noted = new Set(notes.map((n) => n.title));
374
282
  const problems = [
375
- ...chunks.filter((c) => c.pointers === 0 && (codeChanged || noted.has(c.title))).map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — points at no code`),
283
+ ...chunks.filter((c) => c.pointers === 0 && codeChanged).map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — points at no code`),
376
284
  ...chunks.filter((c) => isConflictTitle(c.title)).map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — gtd writes conflicts; leave it out`),
377
- ...notes.filter((n) => !titles.has(n.title)).map((n) => ` - ${n.title} — no chunk of its own`),
378
285
  ...chunks.filter((c, i) => chunks.findIndex((d) => d.title === c.title) !== i).map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — a second chunk with this title`),
379
- ...reviewSections(review).map((s) => ` - ${REVIEW}:${s.line + 1}: ${REVIEW_SECTIONS[s.section]} — gtd sorts the chunks into sections; write \`## \` chunks only`),
380
- ...reviewRisks(review).filter((r) => [...noted].some((title) => r.anchor.startsWith(`${title} ./`))).map((r) => ` - ${r.anchor} — no \`Risk:\` in a quality finding's chunk`)
286
+ ...reviewSections(review).map((s) => ` - ${REVIEW}:${s.line + 1}: ${REVIEW_SECTIONS[s.section]} — write \`## \` chunks only`)
381
287
  ];
382
288
  if (problems.length === 0) return;
383
- refuse(`gtd land: review-pointers: ${problems.length} problem(s) in ${REVIEW} — every chunk needs a pointer to its code, and every quality finding its own chunk, headed exactly as the prompt gave it:\n${problems.join("\n")}`);
289
+ refuse(`gtd land: review-pointers: ${problems.length} problem(s) in ${REVIEW} — every chunk needs a pointer to its code:\n${problems.join("\n")}`);
384
290
  };
385
291
  /**
386
292
  * A reviewer walks the human through everything since `base`, every chunk
387
- * pointing at its code and each of the lap's notes a chunk of its own; gtd
388
- * then adds each note's text and answer boxes, and the summary chunks.
293
+ * pointing at its code; gtd then appends the summary chunks.
389
294
  */
390
- const walkthrough = async (base, report, lap, carry, afterRiskFix) => {
391
- const notes = titledNotes(report);
392
- await reviewing(base, carry, lap, walkthroughBlock(notes, report.nitFix));
295
+ const walkthrough = async (base, summary, lap, carry, nitFix, afterRiskFix) => {
296
+ await reviewing(base, carry, lap, nitFixBlock(nitFix));
393
297
  const written = read(REVIEW);
394
298
  if (written === void 0) return;
395
- const review = unranked(written);
396
- requirePointers(written, notes, changesSince(base).some((c) => isCode(c.path)));
299
+ requirePointers(written, changesSince(base).some((c) => isCode(c.path)));
397
300
  if (!afterRiskFix && reviewRisks(written).length > 0) return;
398
- const full = sectionedReview(await ranked(review, withWalkthrough(review, report), new Set(notes.map((n) => n.title))), new Set(reviewChunks(review).map((c) => c.title)));
301
+ const full = withSummary(written, summary);
399
302
  if (full !== written) await run("review.carrying", writeScript(REVIEW, full), {
400
- label: "Ordering the review and adding the quality lap's findings",
303
+ label: "Adding the quality lap's summary",
401
304
  file: REVIEW,
402
305
  base
403
306
  });
404
307
  };
405
308
  /**
406
- * The quality lap's notes reach a human over everything since `base`, who
407
- * answers each one — fix as suggested, won't fix, or their own answer — and
408
- * may comment. A fix joins the nit batch, a won't-fix is dropped, and an own
409
- * answer or comment is judged: edits end the review as `feedback` for the
410
- * caller to build, questions are answered in place, nits fixed in one batch,
411
- * praise dropped.
309
+ * A reviewer's walkthrough of everything since `base` reaches a human, who
310
+ * may comment: each note is judged — edits end the review as `feedback` for
311
+ * the caller to build, questions are answered in place, nits fixed in one
312
+ * batch, praise dropped.
412
313
  */
413
314
  const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
414
315
  const { guard, guardRests, fixAccess, rules } = options;
@@ -419,7 +320,6 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
419
320
  };
420
321
  const severities = /* @__PURE__ */ new Map();
421
322
  let nitFix;
422
- const answered = /* @__PURE__ */ new Set();
423
323
  let riskFixed = false;
424
324
  return review(base, {
425
325
  file: REVIEW,
@@ -429,8 +329,14 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
429
329
  const fixedAt = nitFix;
430
330
  const afterRiskFix = riskFixed;
431
331
  riskFixed = false;
432
- const report = options.report ?? NO_FINDINGS;
433
- return options.walkthrough === true ? guarded(() => walkthrough(b, composedOf(b, report, fixedAt, answered), options.lap ?? "first", carry, afterRiskFix), guard)() : composing(b, report, carry, fixedAt, answered);
332
+ const summary = {
333
+ report: options.report ?? NO_FINDINGS,
334
+ standards: {
335
+ added: standardsAddedSince(b),
336
+ undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
337
+ }
338
+ };
339
+ return guarded(() => walkthrough(b, summary, options.lap ?? "first", carry, fixedAt, afterRiskFix), guard)();
434
340
  },
435
341
  fixRisks: guarded((notes) => {
436
342
  riskFixed = true;
@@ -449,7 +355,6 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
449
355
  reviewMissing,
450
356
  collecting: feedback.collecting,
451
357
  triage: triage(severities),
452
- decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
453
358
  settles: settlesStandard,
454
359
  learn: learnStandards,
455
360
  pending: standardsPending,
@@ -519,4 +424,4 @@ const reUnwind = async (feedback) => {
519
424
  requireRevert(edited, base);
520
425
  };
521
426
  //#endregion
522
- export { reUnwind as i, humanReview as n, isCode as r, buildTail as t };
427
+ export { isCode as n, reUnwind as r, buildTail as t };
@@ -9415,7 +9415,7 @@ function serializeChunks(chunks, expandTabs) {
9415
9415
  * @returns {ParseContext}
9416
9416
  * Parser.
9417
9417
  */
9418
- function parse(options) {
9418
+ function parse$1(options) {
9419
9419
  /** @type {ParseContext} */
9420
9420
  const parser = {
9421
9421
  constructs: combineExtensions([constructs_exports, ...(options || {}).extensions || []]),
@@ -9730,7 +9730,7 @@ function fromMarkdown(value, encoding, options) {
9730
9730
  options = encoding;
9731
9731
  encoding = void 0;
9732
9732
  }
9733
- return compiler(options)(postprocess(parse(options).document().write(preprocess()(value, encoding, true))));
9733
+ return compiler(options)(postprocess(parse$1(options).document().write(preprocess()(value, encoding, true))));
9734
9734
  }
9735
9735
  /**
9736
9736
  * Note this compiler only understand complete buffering, not streaming.
@@ -13347,7 +13347,7 @@ const eolOf = (content) => content.includes("\r\n") ? "\r\n" : "\n";
13347
13347
  //#endregion
13348
13348
  //#region src/steering/Threads.ts
13349
13349
  const PREFIX_RE$1 = /^(H|A):\s*/;
13350
- const lineOf = (node, which) => toLspPosition(node.position[which]).line;
13350
+ const lineOf$1 = (node, which) => toLspPosition(node.position[which]).line;
13351
13351
  /** The item's own blocks joined — never the item node's span, which can run past its last line into the next bullet's marker. */
13352
13352
  const itemText$1 = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
13353
13353
  const prefixOf = (content, item) => PREFIX_RE$1.exec(itemText$1(content, item));
@@ -13361,9 +13361,9 @@ const isThreadBody = (content, children) => {
13361
13361
  };
13362
13362
  const foldName$1 = (name) => name.toLowerCase();
13363
13363
  /** One list item's entry, reporting any syntax problem; `previous` is the author before it. */
13364
- const entryOf = (content, item, previous, report) => {
13365
- const start = lineOf(item, "start");
13366
- const end = lineOf(item, "end");
13364
+ const entryOf$1 = (content, item, previous, report) => {
13365
+ const start = lineOf$1(item, "start");
13366
+ const end = lineOf$1(item, "end");
13367
13367
  const match = prefixOf(content, item);
13368
13368
  if (!match) {
13369
13369
  report(`an entry must start with "H:" or "A:"`, start, end);
@@ -13379,7 +13379,7 @@ const entryOf = (content, item, previous, report) => {
13379
13379
  author,
13380
13380
  text,
13381
13381
  line: start,
13382
- endLine: last ? lineOf(last, "end") : end,
13382
+ endLine: last ? lineOf$1(last, "end") : end,
13383
13383
  column: toLspPosition(item.position.start).character
13384
13384
  };
13385
13385
  };
@@ -13388,16 +13388,16 @@ const threadOf = (content, def, list, markers, report) => {
13388
13388
  const entries = [];
13389
13389
  let previous;
13390
13390
  for (const item of list.children) {
13391
- const entry = entryOf(content, item, previous, report);
13391
+ const entry = entryOf$1(content, item, previous, report);
13392
13392
  if (!entry) continue;
13393
13393
  previous = entry.author;
13394
13394
  entries.push(entry);
13395
13395
  }
13396
- for (const extra of def.children.slice(1)) report(`only one list of "H:"/"A:" entries may make up the body`, lineOf(extra, "start"), lineOf(extra, "end"));
13396
+ for (const extra of def.children.slice(1)) report(`only one list of "H:"/"A:" entries may make up the body`, lineOf$1(extra, "start"), lineOf$1(extra, "end"));
13397
13397
  return {
13398
13398
  name,
13399
- line: lineOf(def, "start"),
13400
- endLine: lineOf(def, "end"),
13399
+ line: lineOf$1(def, "start"),
13400
+ endLine: lineOf$1(def, "end"),
13401
13401
  markers: markers.filter((m) => foldName$1(m.name) === foldName$1(name)),
13402
13402
  entries,
13403
13403
  waitingOn: entries[entries.length - 1]?.author === "agent" ? "human" : "agent"
@@ -16208,62 +16208,249 @@ const reviewSections = (content) => splitChunks(parseMarkdown(content), content)
16208
16208
  section: s.section,
16209
16209
  line: s.headingLine
16210
16210
  }));
16211
- const isBlankLine = (line) => line !== void 0 && line.trim() === "";
16212
- /** How many lines a chunk's heading takes: two for a setext heading and its underline. */
16213
- const headingSize = ({ heading, headingLine }) => toLspPosition(heading.position.end).line - headingLine + 1;
16214
- /** \`block\` with its heading an ATX \`### \` on one line, and every heading in its body one level deeper. */
16215
- const demoted = (chunk, block) => {
16216
- const out = [...block];
16217
- for (const node of chunk.body) {
16218
- if (node.type !== "heading" || node.depth < 3 || node.depth > 5) continue;
16219
- const at = toLspPosition(node.position.start).line - chunk.headingLine;
16220
- out[at] = out[at].replace(/^( {0,3})#/, "$1##");
16221
- }
16222
- const size = headingSize(chunk);
16223
- return [size > 1 ? `### ${out[0].trim()}` : out[0].replace(/^ {0,3}##/, "###"), ...out.slice(size)];
16211
+ //#endregion
16212
+ //#region src/steering/findings.ts
16213
+ /** The `# ` heading the findings a judge called noise sit under. */
16214
+ const NOISE_HEADING = "Dropped as noise";
16215
+ /** The label of the one box under each finding. */
16216
+ const FIX_BOX = "fix";
16217
+ const OPEN_GROUP = "Open";
16218
+ const MARKER = /\[\^[^\]\s]+\]/g;
16219
+ const isBox = (item, content) => {
16220
+ if (typeof item.checked !== "boolean") return false;
16221
+ const first = item.children[0];
16222
+ if (first?.type !== "paragraph" || first.position === void 0) return false;
16223
+ const text = content.slice(first.position.start.offset, first.position.end.offset);
16224
+ return new RegExp(`^fix(?![\\w-])`).test(text);
16224
16225
  };
16225
- /** A chunk's \`size\` heading lines, then its body with the blank lines around it trimmed. */
16226
- const chunkText = (block, size) => {
16227
- const rest = block.slice(size);
16228
- while (rest.length > 0 && isBlankLine(rest[0])) rest.shift();
16229
- while (rest.length > 0 && isBlankLine(rest.at(-1))) rest.pop();
16230
- const heading = block.slice(0, size).join("\n");
16231
- return rest.length === 0 ? heading : `${heading}\n\n${rest.join("\n")}`;
16226
+ const slice = (content, node) => content.slice(node.position.start.offset, node.position.end.offset);
16227
+ const lineOf = (node, end = false) => toLspPosition(end ? node.position.end : node.position.start).line;
16228
+ /** The box item's own text past its label: a formatter may fold a guidance line into it. */
16229
+ const inlineGuidance = (content, item) => {
16230
+ const first = item.children[0];
16231
+ return content.slice(first.position.start.offset, item.position.end.offset).slice(3).replace(/^[\s:—–-]+/, "").trim();
16232
16232
  };
16233
- /**
16234
- * A flat review regrouped: every chunk with answer boxes under `## Findings`
16235
- * and every chunk titled in `walkthrough` under `## Walkthrough`, each in its
16236
- * current order and one heading level deeper. Any other chunk stays a `## ` of
16237
- * its own, before the sections when it came before them, after otherwise. An
16238
- * already sectioned review comes back unchanged.
16239
- */
16240
- const sectionedReview = (content, walkthrough) => {
16233
+ const guidanceOf = (parts) => parts.map((p) => p.replace(MARKER, "").trim()).filter((p) => p !== "").join("\n\n");
16234
+ /** `nodes` split at `box`: the body before it, the guidance after it. */
16235
+ const splitAtBox = (content, nodes, box) => {
16236
+ const at = nodes.findIndex((n) => n.type === "list" && n.children.includes(box));
16237
+ const list = nodes[at];
16238
+ const boxAt = list.children.indexOf(box);
16239
+ const bodyNodes = nodes.slice(0, boxAt > 0 ? at + 1 : at);
16240
+ const trailing = list.children.slice(boxAt + 1);
16241
+ const after = nodes.slice(at + 1);
16242
+ return {
16243
+ bodyNodes,
16244
+ body: bodyNodes.map((n) => n === list ? content.slice(n.position.start.offset, box.position.start.offset).trimEnd() : slice(content, n)).join("\n\n"),
16245
+ notBody: list.children.slice(boxAt),
16246
+ guidance: guidanceOf([inlineGuidance(content, box), ...[...trailing, ...after].map((n) => slice(content, n))]),
16247
+ last: after.at(-1) ?? trailing.at(-1) ?? box
16248
+ };
16249
+ };
16250
+ const entryOf = (content, title, noise, headingLine, nodes, endLine) => {
16251
+ const notes = nodes.filter((n) => n.type !== "footnoteDefinition");
16252
+ const boxes = notes.flatMap((n) => n.type === "list" ? n.children.filter((i) => isBox(i, content)) : []);
16253
+ const box = boxes[0];
16254
+ const split = box === void 0 ? {
16255
+ bodyNodes: notes,
16256
+ body: notes.map((n) => slice(content, n)).join("\n\n"),
16257
+ notBody: [],
16258
+ guidance: ""
16259
+ } : splitAtBox(content, notes, box);
16260
+ return {
16261
+ title,
16262
+ body: split.body,
16263
+ noise,
16264
+ fix: box?.checked === true,
16265
+ guidance: split.guidance,
16266
+ boxes: boxes.length,
16267
+ headingLine,
16268
+ bodyNodes: split.bodyNodes,
16269
+ ...box === void 0 ? {} : { box },
16270
+ notBody: new Set(split.notBody),
16271
+ lastLine: split.last === void 0 ? headingLine : lineOf(split.last, true),
16272
+ endLine
16273
+ };
16274
+ };
16275
+ const parse = (content) => {
16241
16276
  const tree = parseMarkdown(content);
16242
- const { chunks, sections } = splitChunks(tree, content);
16243
- if (sections.length > 0 || chunks.length === 0) return content;
16244
- const { changesets } = parseReviewDoc(content);
16245
- const lines = content.split("\n");
16246
- const sectionOfChunk = (i) => changesets[i].answers !== void 0 ? "findings" : walkthrough.has(changesets[i].title) ? "walkthrough" : void 0;
16247
- const text = (i, demote) => {
16248
- const chunk = chunks[i];
16249
- const block = lines.slice(chunk.headingLine, chunks[i + 1]?.headingLine ?? lines.length);
16250
- return chunkText(demote ? demoted(chunk, block) : block, demote ? 1 : headingSize(chunk));
16277
+ const entries = [];
16278
+ let noise = false;
16279
+ let open;
16280
+ const close = (endLine) => {
16281
+ if (open !== void 0) entries.push(entryOf(content, open.title, noise, open.line, open.nodes, endLine));
16282
+ open = void 0;
16251
16283
  };
16252
- const grouped = chunks.map((_, i) => sectionOfChunk(i));
16253
- const first = grouped.findIndex((g) => g !== void 0);
16254
- if (first === -1) return content;
16255
- const others = (keep) => chunks.flatMap((_, i) => grouped[i] === void 0 && keep(i) ? [text(i, false)] : []);
16256
- const section = (s) => {
16257
- const members = chunks.flatMap((_, i) => grouped[i] === s ? [text(i, true)] : []);
16258
- return members.length === 0 ? [] : [`## ${REVIEW_SECTIONS[s]}`, ...members];
16284
+ for (const node of tree.children) {
16285
+ if (node.type === "heading" && node.depth <= 2) {
16286
+ close(lineOf(node) - 1);
16287
+ const text = headingText$1(content, node, (t) => t.replace(MARKER, ""));
16288
+ if (node.depth === 1) noise = text === NOISE_HEADING;
16289
+ else open = {
16290
+ title: text,
16291
+ line: lineOf(node),
16292
+ nodes: []
16293
+ };
16294
+ continue;
16295
+ }
16296
+ open?.nodes.push(node);
16297
+ }
16298
+ close(content.split(/\r?\n/).length - 1);
16299
+ return entries;
16300
+ };
16301
+ /** Every `## ` finding in a finding-gate file, in file order. */
16302
+ const findingEntries = (content) => parse(content).map(({ title, body, noise, fix, guidance, boxes, headingLine }) => ({
16303
+ title,
16304
+ body,
16305
+ noise,
16306
+ fix,
16307
+ guidance,
16308
+ boxes,
16309
+ headingLine
16310
+ }));
16311
+ const entryFindings = (content) => parse(content).flatMap((e) => {
16312
+ if (e.boxes === 0) return [{
16313
+ message: `"${e.title}" has no \`- [ ] fix\` box — restore it, ticked to fix the finding or unticked to drop it`,
16314
+ line: e.headingLine
16315
+ }];
16316
+ if (e.boxes > 1) return [{
16317
+ message: `"${e.title}" has ${e.boxes} \`fix\` boxes — keep exactly one`,
16318
+ line: e.headingLine
16319
+ }];
16320
+ if (!e.fix && e.guidance !== "") return [{
16321
+ message: `"${e.title}" has guidance under an unticked box — tick it to fix the finding, or delete the text to drop it`,
16322
+ line: lineOf(e.box)
16323
+ }];
16324
+ return [];
16325
+ });
16326
+ const findingsView = (content) => {
16327
+ const noteAt = noteLookup(content);
16328
+ return { nodes: parse(content).map((e, index) => {
16329
+ const note = noteAt(e.headingLine);
16330
+ return {
16331
+ title: e.title,
16332
+ anchor: {
16333
+ kind: "chunk",
16334
+ index
16335
+ },
16336
+ group: e.noise ? NOISE_HEADING : OPEN_GROUP,
16337
+ ...e.boxes === 1 ? { checked: e.fix } : {},
16338
+ ...e.guidance === "" ? {} : { note: e.guidance },
16339
+ ...note?.thread !== void 0 ? { thread: note.thread } : {},
16340
+ body: blockNodesOfRun(content, e.bodyNodes, { skipListItem: (i) => e.notBody.has(i) })
16341
+ };
16342
+ }) };
16343
+ };
16344
+ const findingsOutline = (content) => {
16345
+ const lines = content.split(/\r?\n/);
16346
+ return [...parse(content).map((e) => ({
16347
+ name: e.title,
16348
+ detail: e.noise ? "noise" : e.fix ? "fix" : "drop",
16349
+ range: spanRange(lines, e.headingLine, Math.max(e.headingLine, e.endLine)),
16350
+ selectionRange: lineRange(lines, e.headingLine)
16351
+ })), ...threadOutlineNodes(content).map((t) => t.node)];
16352
+ };
16353
+ const findingsAnnotate = (content, anchor, text) => {
16354
+ const entries = parse(content);
16355
+ const lines = content.split(/\r?\n/);
16356
+ const resolved = (() => {
16357
+ if (anchor.kind === "chunk") {
16358
+ const e = entries[anchor.index];
16359
+ return e && {
16360
+ line: e.headingLine,
16361
+ blockEndLine: e.headingLine,
16362
+ key: `chunk:${anchor.index}`
16363
+ };
16364
+ }
16365
+ if (anchor.kind !== "paragraph") return void 0;
16366
+ const block = entries.flatMap((e) => e.bodyNodes).find((n) => anchor.line >= lineOf(n) && anchor.line <= lineOf(n, true));
16367
+ return block && {
16368
+ line: anchor.line,
16369
+ blockEndLine: lineOf(block, true),
16370
+ key: `paragraph:${anchor.line}`
16371
+ };
16372
+ })();
16373
+ if (resolved === void 0) return {
16374
+ ok: false,
16375
+ reason: "anchor-not-found"
16376
+ };
16377
+ const result = footnoteAttachEdits(content, {
16378
+ ...resolved,
16379
+ endCharacter: (lines[resolved.line] ?? "").length
16380
+ }, text);
16381
+ return result.ok ? {
16382
+ ok: true,
16383
+ edits: result.edits
16384
+ } : result;
16385
+ };
16386
+ /** Ticks or unticks a finding's box; `text` replaces its guidance, and unticking drops it. */
16387
+ const findingsApply = (content, anchor, opts) => {
16388
+ if (anchor.kind !== "chunk") return {
16389
+ ok: false,
16390
+ reason: "anchor-not-found"
16391
+ };
16392
+ const e = parse(content)[anchor.index];
16393
+ if (e?.box === void 0 || e.boxes !== 1) return {
16394
+ ok: false,
16395
+ reason: "anchor-not-found"
16396
+ };
16397
+ const fix = opts.checked ?? e.fix;
16398
+ const guidance = fix ? (opts.text ?? e.guidance).trim() : "";
16399
+ if (fix === e.fix && guidance === e.guidance) return {
16400
+ ok: true,
16401
+ edits: []
16402
+ };
16403
+ const lines = content.split(/\r?\n/);
16404
+ const eol = eolOf(content);
16405
+ const start = lineOf(e.box);
16406
+ const box = `${lines[start].match(/^\s*[-*+]\s+/)[0]}[${fix ? "x" : " "}] fix`;
16407
+ const text = guidance === "" ? box : `${box}${eol}${eol}${guidance.split(/\r?\n/).join(eol)}`;
16408
+ return {
16409
+ ok: true,
16410
+ edits: [{
16411
+ range: {
16412
+ start: {
16413
+ line: start,
16414
+ character: 0
16415
+ },
16416
+ end: lineRange(lines, e.lastLine).end
16417
+ },
16418
+ newText: text
16419
+ }]
16259
16420
  };
16260
- return `${[
16261
- lines.slice(0, chunks[0].headingLine).join("\n").trim(),
16262
- ...others((i) => i < first),
16263
- ...section("findings"),
16264
- ...section("walkthrough"),
16265
- ...others((i) => i > first)
16266
- ].filter((p) => p !== "").join("\n\n")}\n`;
16421
+ };
16422
+ /** The finding gate's file: one `## ` finding each, with one `fix` box and optional guidance below it. */
16423
+ const findingsDescriptor = {
16424
+ sample: `# Quality
16425
+
16426
+ ## A finding a lens wrote[^nafind1]
16427
+
16428
+ [^nafind1]:
16429
+ Attached via the phone UI, this note shows a comment on a finding with a
16430
+ \`multi word code span\` that runs past eighty characters in total length.
16431
+
16432
+ What the lens found, and where.
16433
+
16434
+ - [x] fix
16435
+
16436
+ Fix it the simple way.
16437
+
16438
+ # Dropped as noise
16439
+
16440
+ ## A finding the judge called noise
16441
+
16442
+ Why the judge dropped it.
16443
+
16444
+ - [ ] fix
16445
+ `,
16446
+ validate: (content) => [...entryFindings(content), ...threadFindings(content)],
16447
+ outline: findingsOutline,
16448
+ actions: (content, range) => threadReplyActions(content, range.start),
16449
+ view: findingsView,
16450
+ annotate: findingsAnnotate,
16451
+ apply: findingsApply,
16452
+ clearTicks: (content) => content,
16453
+ openThreads: openThreadsOf
16267
16454
  };
16268
16455
  //#endregion
16269
16456
  //#region src/steering/Edits.ts
@@ -16595,8 +16782,12 @@ const stripCodeThreads = (path, text) => {
16595
16782
  };
16596
16783
  //#endregion
16597
16784
  //#region src/steering/index.ts
16598
- /** The built-in registry: mode name → its format, in registry order. The single place `qa` and `review` are wired into names — everything else in this module dispatches over a resolved `SteeringFormat`, never a mode-name string. */
16599
- const REGISTRY = /* @__PURE__ */ new Map([["qa", qaDescriptor], ["review", reviewDescriptor]]);
16785
+ /** The built-in registry: mode name → its format, in registry order. The single place the built-in formats are wired into names — everything else in this module dispatches over a resolved `SteeringFormat`, never a mode-name string. */
16786
+ const REGISTRY = /* @__PURE__ */ new Map([
16787
+ ["qa", qaDescriptor],
16788
+ ["review", reviewDescriptor],
16789
+ ["findings", findingsDescriptor]
16790
+ ]);
16600
16791
  /** Every built-in format name, in registry order. */
16601
16792
  const BUILT_IN_MODE_NAMES = [...REGISTRY.keys()];
16602
16793
  /** The built-in `SteeringFormat` registered under `mode`'s name, or `undefined` when it isn't a built-in mode at all. The one place a bare mode-name string is ever looked up — every consumer takes the RESOLVED value this returns, never a name, so "mode is required, never inferred from a file's basename" is a type, not a rule to remember. */
@@ -16633,4 +16824,4 @@ const viewOf = (format, content) => ({
16633
16824
  /** Every OPEN thread — last entry is the agent's — as `{ name, line, firstMe }` (0-based line). `[]` for a format with no thread concept. */
16634
16825
  const openThreads = (format, content) => format.openThreads?.(content) ?? [];
16635
16826
  //#endregion
16636
- export { parseThreadsWithFindings as A, FREE_TEXT_PLACEHOLDER as C, openThreadFindings as D, THREAD_REPLY_ACTION_TITLE as E, headingSectionBodies as M, headingSections as N, parseFootnotes as O, formatFinding as P, unansweredNotes as S, FOOTNOTE_ACTION_TITLE as T, reviewNotes as _, unansweredQuestions as a, sectionedReview as b, stripCodeThreads as c, ANSWER_FIX as d, ANSWER_LEAD as f, reviewDecisions as g, reviewChunks as h, steeringFormatOrFreeForm as i, getParseCount as j, parseThreads as k, freeFormFormat as l, REVIEW_SECTIONS as m, openThreads as n, viewOf as o, ANSWER_WONT_FIX as p, steeringFormatFor as r, parseCodeThreads as s, BUILT_IN_MODE_NAMES as t, applySteeringEdits as u, reviewRisks as v, isAnswered as w, severityLine as x, reviewSections as y };
16827
+ export { parseThreads as A, unansweredNotes as C, THREAD_REPLY_ACTION_TITLE as D, FOOTNOTE_ACTION_TITLE as E, formatFinding as F, getParseCount as M, headingSectionBodies as N, openThreadFindings as O, headingSections as P, reviewSections as S, isAnswered as T, REVIEW_SECTIONS as _, unansweredQuestions as a, reviewNotes as b, stripCodeThreads as c, FIX_BOX as d, NOISE_HEADING as f, ANSWER_WONT_FIX as g, ANSWER_LEAD as h, steeringFormatOrFreeForm as i, parseThreadsWithFindings as j, parseFootnotes as k, freeFormFormat as l, ANSWER_FIX as m, openThreads as n, viewOf as o, findingEntries as p, steeringFormatFor as r, parseCodeThreads as s, BUILT_IN_MODE_NAMES as t, applySteeringEdits as u, reviewChunks as v, FREE_TEXT_PLACEHOLDER as w, reviewRisks as x, reviewDecisions as y };