@pmelab/gtd 51.0.1 → 52.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +41 -38
- package/dist/chunks/{reserved-CUkk9XgL.mjs → reserved-D0MKVFFa.mjs} +28 -34
- package/dist/chunks/{review-BaCXlIT2.mjs → review-ZzUQH1KZ.mjs} +11 -9
- package/dist/chunks/{steering-DtUVUz9l.mjs → steering-CLhu8C7q.mjs} +262 -52
- package/dist/chunks/{tail-CX6Pdsql.mjs → tail-I1-vVq0P.mjs} +1 -1
- package/dist/gtd.bundle.mjs +198 -43
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +1 -1
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +510 -147
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +181 -222
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +478 -313
- package/package.json +1 -1
- package/src/steering/FORMATS.md +4 -1
- package/src/steering/SteeringFormat.ts +7 -1
- package/src/steering/contract.test.ts +2 -1
- package/src/steering/fixtures/review/sectioned.md +31 -0
- package/src/steering/fixtures/review/sectioned.read.json +119 -0
- package/src/steering/index.ts +4 -0
- package/src/steering/qa.ts +1 -1
- package/src/steering/review.test.ts +270 -4
- package/src/steering/review.ts +328 -69
- package/src/workflows/feature/criteria.test.ts +39 -1
- package/src/workflows/feature/criteria.ts +21 -7
- package/src/workflows/feature/fast-path.feature +1 -6
- package/src/workflows/feature/feature.ts +1 -2
- package/src/workflows/feature/flag-mode.feature +1 -6
- package/src/workflows/feature/green.ts +22 -0
- package/src/workflows/feature/hands-on-skip.feature +344 -0
- package/src/workflows/feature/lens-models.feature +0 -4
- package/src/workflows/feature/lens-review.feature +6 -8
- package/src/workflows/feature/one-step-land.feature +1 -6
- package/src/workflows/feature/prototype-rounds.feature +1 -1
- package/src/workflows/feature/prototype.feature +10 -9
- package/src/workflows/feature/prototype.ts +48 -17
- package/src/workflows/feature/refactor-absorbs.feature +3 -8
- package/src/workflows/feature/refactor-first.feature +3 -8
- package/src/workflows/feature/refactor-rebuild.feature +7 -12
- package/src/workflows/feature/refactor-review.feature +52 -72
- package/src/workflows/feature/review-answers.feature +90 -5
- package/src/workflows/feature/review-gate.feature +11 -53
- package/src/workflows/feature/reviewer-walkthrough.feature +29 -12
- package/src/workflows/feature/seam-rejection.feature +1 -6
- package/src/workflows/feature/seam.feature +1 -6
- package/src/workflows/feature/seam.test.ts +1 -1
- package/src/workflows/feature/seam.ts +2 -3
- package/src/workflows/feature/severity-budget.feature +7 -7
- package/src/workflows/feature/severity-sorted-review.feature +50 -54
- package/src/workflows/feature/slice-findings.feature +5 -15
- package/src/workflows/feature/slicing.feature +4 -24
- package/src/workflows/feature/standards.feature +0 -4
- package/src/workflows/shared/review.ts +26 -6
- package/src/workflows/shared/text.test.ts +45 -11
- package/src/workflows/shared/text.ts +45 -38
- package/src/workflows/feature/coverage.feature +0 -165
- package/src/workflows/feature/coverage.ts +0 -120
|
@@ -15396,8 +15396,8 @@ const splitSeverity = (content, run) => {
|
|
|
15396
15396
|
};
|
|
15397
15397
|
};
|
|
15398
15398
|
/** Splits one chunk's body nodes into its file pointers (hunk pointers are task items, collected recursively at ANY nesting depth via `taskItems` — a nested hunk is the same kind of hunk as a top-level one) and description prose. The description is the chunk's own leading run and never a node that contains a hunk pointer: it stops at the first node whose `taskItems` include a real pointer (at any depth — a blockquote wrapping a pointer list stops it just as a top-level list would). Every node kind in what's left before that point contributes — a `footnoteDefinition` is the one exception, excluded by `blockNodesOfRun` itself (it's a note, never document content). */
|
|
15399
|
-
const parseChunkBody = (content, lines, title, body) => {
|
|
15400
|
-
const answer = answerListOf(content, body);
|
|
15399
|
+
const parseChunkBody = (content, lines, title, body, answerable) => {
|
|
15400
|
+
const answer = answerable ? answerListOf(content, body) : void 0;
|
|
15401
15401
|
const isAnswer = (n) => answer !== void 0 && (n === answer.list || n === body[answer.lead]);
|
|
15402
15402
|
const firstPointerIndex = body.findIndex((n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)));
|
|
15403
15403
|
const run = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
|
|
@@ -15419,45 +15419,78 @@ const parseChunkBody = (content, lines, title, body) => {
|
|
|
15419
15419
|
descriptionInline,
|
|
15420
15420
|
files,
|
|
15421
15421
|
errors,
|
|
15422
|
-
...answer === void 0 ? {} : {
|
|
15422
|
+
...answer === void 0 ? {} : {
|
|
15423
|
+
answers: answer.options,
|
|
15424
|
+
answerLine: toLspPosition(body[answer.lead].position.start).line
|
|
15425
|
+
},
|
|
15423
15426
|
...severity === void 0 ? {} : { severity },
|
|
15424
15427
|
boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0)
|
|
15425
15428
|
};
|
|
15426
15429
|
};
|
|
15427
|
-
/**
|
|
15428
|
-
const
|
|
15429
|
-
|
|
15430
|
-
|
|
15431
|
-
|
|
15432
|
-
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
|
|
15436
|
-
|
|
15437
|
-
|
|
15438
|
-
|
|
15439
|
-
|
|
15440
|
-
|
|
15441
|
-
|
|
15442
|
-
|
|
15443
|
-
|
|
15444
|
-
|
|
15445
|
-
|
|
15430
|
+
/** The `## ` headings gtd groups the review's chunks under; any other `## ` is a chunk of its own. */
|
|
15431
|
+
const REVIEW_SECTIONS = {
|
|
15432
|
+
findings: "Findings",
|
|
15433
|
+
walkthrough: "Walkthrough"
|
|
15434
|
+
};
|
|
15435
|
+
const sectionOf = (content, node) => {
|
|
15436
|
+
if (node.type !== "heading" || node.depth !== 2) return void 0;
|
|
15437
|
+
const text = headingText$1(content, node);
|
|
15438
|
+
return Object.keys(REVIEW_SECTIONS).find((s) => REVIEW_SECTIONS[s] === text);
|
|
15439
|
+
};
|
|
15440
|
+
/** The nodes after `from` up to the next heading at `depth` or shallower. */
|
|
15441
|
+
const bodyFrom = (children, from, depth) => {
|
|
15442
|
+
const end = children.findIndex((n, i) => i >= from && n.type === "heading" && n.depth <= depth);
|
|
15443
|
+
return children.slice(from, end === -1 ? void 0 : end);
|
|
15444
|
+
};
|
|
15445
|
+
/**
|
|
15446
|
+
* Splits the tree's top-level nodes into chunk blocks — a `## ` line found only
|
|
15447
|
+
* inside a fence never becomes a heading node at all. A document with a
|
|
15448
|
+
* `## Findings` or `## Walkthrough` heading nests `### ` chunks under them;
|
|
15449
|
+
* one without is the flat layout, every `## ` a chunk.
|
|
15450
|
+
*/
|
|
15451
|
+
const splitChunks = (tree, content) => {
|
|
15452
|
+
const sectioned = tree.children.some((n) => sectionOf(content, n) !== void 0);
|
|
15453
|
+
const chunks = [];
|
|
15454
|
+
const sections = [];
|
|
15455
|
+
let section;
|
|
15456
|
+
tree.children.forEach((node, i) => {
|
|
15457
|
+
if (node.type !== "heading") return;
|
|
15458
|
+
const headingLine = toLspPosition(node.position.start).line;
|
|
15459
|
+
if (sectioned && node.depth <= 2) {
|
|
15460
|
+
const s = sectionOf(content, node);
|
|
15461
|
+
section = s === void 0 ? void 0 : {
|
|
15462
|
+
heading: node,
|
|
15463
|
+
headingLine,
|
|
15464
|
+
section: s,
|
|
15465
|
+
body: []
|
|
15466
|
+
};
|
|
15467
|
+
if (section !== void 0) {
|
|
15468
|
+
sections.push(section);
|
|
15469
|
+
return;
|
|
15470
|
+
}
|
|
15446
15471
|
}
|
|
15447
|
-
|
|
15448
|
-
|
|
15472
|
+
const depth = section === void 0 ? 2 : 3;
|
|
15473
|
+
if (node.depth !== depth) return;
|
|
15474
|
+
const body = bodyFrom(tree.children, i + 1, depth);
|
|
15475
|
+
chunks.push({
|
|
15476
|
+
heading: node,
|
|
15449
15477
|
headingLine,
|
|
15450
|
-
body
|
|
15478
|
+
body,
|
|
15479
|
+
...section && { section: section.section }
|
|
15451
15480
|
});
|
|
15452
|
-
|
|
15453
|
-
|
|
15481
|
+
section?.body.push(node, ...body);
|
|
15482
|
+
});
|
|
15483
|
+
return {
|
|
15484
|
+
chunks,
|
|
15485
|
+
sections
|
|
15486
|
+
};
|
|
15454
15487
|
};
|
|
15455
15488
|
const parseChangesets = (tree, content, lines) => {
|
|
15456
15489
|
const changesets = [];
|
|
15457
15490
|
const errors = [];
|
|
15458
|
-
for (const { heading, headingLine, body } of splitChunks(tree)) {
|
|
15491
|
+
for (const { heading, headingLine, body, section } of splitChunks(tree, content).chunks) {
|
|
15459
15492
|
const title = headingText$1(content, heading);
|
|
15460
|
-
const { description, descriptionNodes, descriptionInline, files, answers, severity, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
15493
|
+
const { description, descriptionNodes, descriptionInline, files, answers, answerLine, severity, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body, section !== "walkthrough");
|
|
15461
15494
|
errors.push(...bodyErrors);
|
|
15462
15495
|
if (files.length === 0 && boxed) errors.push({
|
|
15463
15496
|
message: `Chunk "${title}" has no file pointers`,
|
|
@@ -15471,8 +15504,12 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
15471
15504
|
descriptionInline,
|
|
15472
15505
|
files,
|
|
15473
15506
|
headingLine,
|
|
15474
|
-
...answers === void 0 ? {} : {
|
|
15475
|
-
|
|
15507
|
+
...answers === void 0 ? {} : {
|
|
15508
|
+
answers,
|
|
15509
|
+
answerLine
|
|
15510
|
+
},
|
|
15511
|
+
...severity === void 0 ? {} : { severity },
|
|
15512
|
+
...section === void 0 ? {} : { section }
|
|
15476
15513
|
});
|
|
15477
15514
|
}
|
|
15478
15515
|
if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" });
|
|
@@ -15554,7 +15591,7 @@ const isHunkPointerItem = (content, item) => {
|
|
|
15554
15591
|
*/
|
|
15555
15592
|
const reviewClearTicks = (content) => {
|
|
15556
15593
|
const tree = parseMarkdown(content);
|
|
15557
|
-
const answers = new Set(splitChunks(tree).flatMap(({ body }) => answerListOf(content, body)?.list.children ?? []));
|
|
15594
|
+
const answers = new Set(splitChunks(tree, content).chunks.flatMap(({ body }) => answerListOf(content, body)?.list.children ?? []));
|
|
15558
15595
|
const offsets = taskItems(tree).filter((item) => item.checked === true && !answers.has(item) && isHunkPointerItem(content, item)).map((item) => checkboxOffset(content, item)).filter((o) => o !== void 0).sort((a, b) => a - b);
|
|
15559
15596
|
if (offsets.length === 0) return content;
|
|
15560
15597
|
let result = "";
|
|
@@ -15591,42 +15628,80 @@ const toggleFilePointer = (content, line) => {
|
|
|
15591
15628
|
const chunkEndLines = (content) => {
|
|
15592
15629
|
const tree = parseMarkdown(content);
|
|
15593
15630
|
const map = /* @__PURE__ */ new Map();
|
|
15594
|
-
for (const block of splitChunks(tree)) map.set(block.headingLine, blockEndLine(block));
|
|
15631
|
+
for (const block of splitChunks(tree, content).chunks) map.set(block.headingLine, blockEndLine(block));
|
|
15595
15632
|
return map;
|
|
15596
15633
|
};
|
|
15634
|
+
const CHOICE_NAMES = {
|
|
15635
|
+
fix: ANSWER_FIX,
|
|
15636
|
+
wontfix: ANSWER_WONT_FIX,
|
|
15637
|
+
answer: ""
|
|
15638
|
+
};
|
|
15639
|
+
/** A finding's decision as an outline node of its own; none for a chunk with no answer boxes. */
|
|
15640
|
+
const decisionNodes = (lines, chunk, end) => {
|
|
15641
|
+
if (chunk.answerLine === void 0) return [];
|
|
15642
|
+
const d = decisionOf(chunk);
|
|
15643
|
+
return [{
|
|
15644
|
+
name: `Decision: ${d === void 0 ? "open" : CHOICE_NAMES[d.choice] || d.text}`,
|
|
15645
|
+
range: spanRange(lines, chunk.answerLine, Math.max(chunk.answerLine, end)),
|
|
15646
|
+
selectionRange: lineRange(lines, chunk.answerLine)
|
|
15647
|
+
}];
|
|
15648
|
+
};
|
|
15649
|
+
const isOpenChunk = (chunk, markers) => chunk.files.some((file) => !file.checked) || markers > 0 || chunk.answers !== void 0 && decisionOf(chunk) === void 0;
|
|
15597
15650
|
/**
|
|
15598
|
-
* Document outline for `.gtd/REVIEW.md`: the
|
|
15599
|
-
*
|
|
15600
|
-
*
|
|
15601
|
-
*
|
|
15602
|
-
*
|
|
15603
|
-
* human's own comment vanish the moment they tick the last box. A chunk's
|
|
15604
|
-
* footnotes are `leaf: true` children; a chunk with none has no children.
|
|
15651
|
+
* Document outline for `.gtd/REVIEW.md`: the chunks still left to review — an
|
|
15652
|
+
* unchecked hunk, a footnote (so a human's comment never vanishes with the
|
|
15653
|
+
* last tick) or an open decision. A finding's decision is a node of its own.
|
|
15654
|
+
* In the sectioned layout `Findings` and `Walkthrough` are containers, and
|
|
15655
|
+
* every other `## ` (`Fixed automatically`, …) stays listed.
|
|
15605
15656
|
*/
|
|
15606
15657
|
const reviewOutline = (content) => {
|
|
15607
15658
|
const { changesets } = parseReviewDoc(content);
|
|
15659
|
+
const { sections } = splitChunks(parseMarkdown(content), content);
|
|
15608
15660
|
const { markers, definitions } = parseFootnotes(content);
|
|
15609
15661
|
const definitionByName = new Map(definitions.map((d) => [d.name, d.body]));
|
|
15610
15662
|
const lines = content.split(/\r?\n/);
|
|
15611
15663
|
const endLines = chunkEndLines(content);
|
|
15612
15664
|
const threads = threadOutlineNodes(content);
|
|
15613
15665
|
const nested = /* @__PURE__ */ new Set();
|
|
15614
|
-
|
|
15666
|
+
const chunkNodes = changesets.flatMap((chunk) => {
|
|
15615
15667
|
const start = chunk.headingLine;
|
|
15616
15668
|
const end = Math.max(start, endLines.get(start) ?? start);
|
|
15617
15669
|
const checkedCount = chunk.files.filter((file) => file.checked).length;
|
|
15618
15670
|
const chunkMarkers = markers.filter((m) => m.line >= start && m.line <= end);
|
|
15619
15671
|
const chunkThreads = threads.filter((t) => t.markerLine !== void 0 && t.markerLine >= start && t.markerLine <= end).map((t) => t.node);
|
|
15620
15672
|
chunkThreads.forEach((n) => nested.add(n));
|
|
15621
|
-
const children = [
|
|
15622
|
-
|
|
15623
|
-
|
|
15673
|
+
const children = [
|
|
15674
|
+
...decisionNodes(lines, chunk, end),
|
|
15675
|
+
...chunkMarkers.map((m) => footnoteLeaf(lines, definitionByName, m)),
|
|
15676
|
+
...chunkThreads
|
|
15677
|
+
];
|
|
15678
|
+
if (!(sections.length > 0 && chunk.section === void 0) && !isOpenChunk(chunk, chunkMarkers.length)) return [];
|
|
15679
|
+
const node = {
|
|
15680
|
+
name: chunk.files.length === 0 ? chunk.title : `${chunk.title} (${checkedCount}/${chunk.files.length})`,
|
|
15624
15681
|
range: spanRange(lines, start, end),
|
|
15625
15682
|
selectionRange: lineRange(lines, start),
|
|
15626
|
-
...children.length > 0 ? { children } : {}
|
|
15627
|
-
unchecked: checkedCount < chunk.files.length || chunkMarkers.length > 0
|
|
15683
|
+
...children.length > 0 ? { children } : {}
|
|
15628
15684
|
};
|
|
15629
|
-
|
|
15685
|
+
return [{
|
|
15686
|
+
line: start,
|
|
15687
|
+
section: chunk.section,
|
|
15688
|
+
node
|
|
15689
|
+
}];
|
|
15690
|
+
});
|
|
15691
|
+
const containers = sections.map((section) => ({
|
|
15692
|
+
line: section.headingLine,
|
|
15693
|
+
section: void 0,
|
|
15694
|
+
node: {
|
|
15695
|
+
name: REVIEW_SECTIONS[section.section],
|
|
15696
|
+
range: spanRange(lines, section.headingLine, blockEndLine(section)),
|
|
15697
|
+
selectionRange: lineRange(lines, section.headingLine),
|
|
15698
|
+
children: chunkNodes.filter((c) => c.section === section.section && c.line > section.headingLine).filter((c) => {
|
|
15699
|
+
const next = sections.find((o) => o.headingLine > section.headingLine);
|
|
15700
|
+
return next === void 0 || c.line < next.headingLine;
|
|
15701
|
+
}).map((c) => c.node)
|
|
15702
|
+
}
|
|
15703
|
+
}));
|
|
15704
|
+
return [...[...chunkNodes.filter((c) => c.section === void 0), ...containers].sort((a, b) => a.line - b.line).map((c) => c.node), ...threads.map((t) => t.node).filter((n) => !nested.has(n))];
|
|
15630
15705
|
};
|
|
15631
15706
|
/**
|
|
15632
15707
|
* The target state a whole-chunk toggle drives every hunk to: `true` (check
|
|
@@ -15678,6 +15753,7 @@ const reviewActions = (content, range) => {
|
|
|
15678
15753
|
const tree = parseMarkdown(content);
|
|
15679
15754
|
const lines = content.split(/\r?\n/);
|
|
15680
15755
|
const cursorLine = range.start.line;
|
|
15756
|
+
const endLines = chunkEndLines(content);
|
|
15681
15757
|
const actions = [...threadReplyActions(content, range.start)];
|
|
15682
15758
|
changesets.forEach((chunk, i) => {
|
|
15683
15759
|
const hunk = chunk.files.find((file) => cursorLine >= file.sourceLine && cursorLine <= file.endLine);
|
|
@@ -15688,7 +15764,7 @@ const reviewActions = (content, range) => {
|
|
|
15688
15764
|
edits: [edit]
|
|
15689
15765
|
});
|
|
15690
15766
|
}
|
|
15691
|
-
const chunkEnd = Math.max(chunk.headingLine, (changesets[i + 1]?.headingLine ?? lines.length) - 1);
|
|
15767
|
+
const chunkEnd = chunk.section === void 0 ? Math.max(chunk.headingLine, (changesets[i + 1]?.headingLine ?? lines.length) - 1) : endLines.get(chunk.headingLine) ?? chunk.headingLine;
|
|
15692
15768
|
if (chunk.files.length > 0 && cursorLine >= chunk.headingLine && cursorLine <= chunkEnd) {
|
|
15693
15769
|
const edits = toggleChunkEdits(content, chunk.headingLine);
|
|
15694
15770
|
if (edits.length > 0) {
|
|
@@ -15756,7 +15832,7 @@ const hunkLinkFor = (content, item) => {
|
|
|
15756
15832
|
const reviewDocumentLinks = (content) => {
|
|
15757
15833
|
const tree = parseMarkdown(content);
|
|
15758
15834
|
const links = [];
|
|
15759
|
-
for (const block of splitChunks(tree)) for (const item of block.body.flatMap((n) => taskItems(n))) {
|
|
15835
|
+
for (const block of splitChunks(tree, content).chunks) for (const item of block.body.flatMap((n) => taskItems(n))) {
|
|
15760
15836
|
const link = hunkLinkFor(content, item);
|
|
15761
15837
|
if (link) links.push(link);
|
|
15762
15838
|
}
|
|
@@ -15778,6 +15854,19 @@ const reviewView = (content) => {
|
|
|
15778
15854
|
kind: "chunk",
|
|
15779
15855
|
index: chunkIndex
|
|
15780
15856
|
},
|
|
15857
|
+
...chunk.section === void 0 ? {} : { group: REVIEW_SECTIONS[chunk.section] },
|
|
15858
|
+
...chunk.answers === void 0 ? {} : {
|
|
15859
|
+
answered: decisionOf(chunk) !== void 0,
|
|
15860
|
+
decision: chunk.answers.map((option, index) => ({
|
|
15861
|
+
title: option.text,
|
|
15862
|
+
checked: option.checked,
|
|
15863
|
+
anchor: {
|
|
15864
|
+
kind: "option",
|
|
15865
|
+
questionIndex: chunkIndex,
|
|
15866
|
+
index
|
|
15867
|
+
}
|
|
15868
|
+
}))
|
|
15869
|
+
},
|
|
15781
15870
|
...noteAt(chunk.headingLine),
|
|
15782
15871
|
children: chunk.files.map((file, index) => ({
|
|
15783
15872
|
title: pointerDisplay(file),
|
|
@@ -15875,8 +15964,66 @@ const setFileTickEdits = (content, files, checked) => {
|
|
|
15875
15964
|
}
|
|
15876
15965
|
return edits;
|
|
15877
15966
|
};
|
|
15878
|
-
/**
|
|
15967
|
+
/**
|
|
15968
|
+
* A finding's answer boxes are radio: ticking one unticks the others, and
|
|
15969
|
+
* `text` replaces the ticked box's label in the same edit set — the phone's
|
|
15970
|
+
* free-text answer, normalized as `qa`'s is.
|
|
15971
|
+
*/
|
|
15972
|
+
const decisionEdits = (content, chunkIndex, index, opts) => {
|
|
15973
|
+
const items = answerItemsOf(content, chunkIndex);
|
|
15974
|
+
const target = items[index];
|
|
15975
|
+
if (target === void 0) return {
|
|
15976
|
+
ok: false,
|
|
15977
|
+
reason: "anchor-not-found"
|
|
15978
|
+
};
|
|
15979
|
+
const checked = opts.checked ?? true;
|
|
15980
|
+
const ticks = items.flatMap((item) => {
|
|
15981
|
+
const want = item === target ? checked : !checked && item.checked === true;
|
|
15982
|
+
return item.checked === true === want ? [] : tickEdit(content, item, want);
|
|
15983
|
+
});
|
|
15984
|
+
const text = opts.text === void 0 ? [] : labelEdit(content, target, opts.text);
|
|
15985
|
+
return {
|
|
15986
|
+
ok: true,
|
|
15987
|
+
edits: [...ticks, ...text]
|
|
15988
|
+
};
|
|
15989
|
+
};
|
|
15990
|
+
/** A finding's answer boxes; none for a walkthrough chunk or a stale index. */
|
|
15991
|
+
const answerItemsOf = (content, chunkIndex) => {
|
|
15992
|
+
const chunk = splitChunks(parseMarkdown(content), content).chunks[chunkIndex];
|
|
15993
|
+
if (chunk === void 0 || chunk.section === "walkthrough") return [];
|
|
15994
|
+
return answerListOf(content, chunk.body)?.list.children ?? [];
|
|
15995
|
+
};
|
|
15996
|
+
const tickEdit = (content, item, checked) => {
|
|
15997
|
+
const offset = checkboxOffset(content, item);
|
|
15998
|
+
if (offset === void 0) return [];
|
|
15999
|
+
const start = toLspPositionFromOffset(content, offset);
|
|
16000
|
+
return [{
|
|
16001
|
+
range: {
|
|
16002
|
+
start,
|
|
16003
|
+
end: {
|
|
16004
|
+
...start,
|
|
16005
|
+
character: start.character + 1
|
|
16006
|
+
}
|
|
16007
|
+
},
|
|
16008
|
+
newText: checked ? "x" : " "
|
|
16009
|
+
}];
|
|
16010
|
+
};
|
|
16011
|
+
/** Replaces a box's label, from after its marker to the end of its first paragraph. */
|
|
16012
|
+
const labelEdit = (content, item, text) => {
|
|
16013
|
+
const from = firstParagraphContentOffset(item);
|
|
16014
|
+
const to = item.children.find((c) => c.type === "paragraph")?.position?.end.offset;
|
|
16015
|
+
if (from === void 0 || to === void 0) return [];
|
|
16016
|
+
return [{
|
|
16017
|
+
range: {
|
|
16018
|
+
start: toLspPositionFromOffset(content, from),
|
|
16019
|
+
end: toLspPositionFromOffset(content, to)
|
|
16020
|
+
},
|
|
16021
|
+
newText: normalizeFreeTextAnswer(text)
|
|
16022
|
+
}];
|
|
16023
|
+
};
|
|
16024
|
+
/** A `hunk` anchor sets one tick; a `chunk` anchor sets every hunk beneath it to the SAME target state — never `toggleChunkEdits`'s majority-flip heuristic, since the caller already knows the state it wants. An `option` anchor is a finding's decision. */
|
|
15879
16025
|
const reviewApply = (content, anchor, opts) => {
|
|
16026
|
+
if (anchor.kind === "option") return decisionEdits(content, anchor.questionIndex, anchor.index, opts);
|
|
15880
16027
|
const { changesets } = parseReviewDoc(content);
|
|
15881
16028
|
const checked = opts.checked ?? true;
|
|
15882
16029
|
if (anchor.kind === "hunk") {
|
|
@@ -16043,11 +16190,12 @@ const reviewDecisions = (before, after) => {
|
|
|
16043
16190
|
return old?.choice === decision.choice && old.text === decision.text ? [] : [decision];
|
|
16044
16191
|
});
|
|
16045
16192
|
};
|
|
16046
|
-
/** Each
|
|
16193
|
+
/** Each chunk in document order: its title, 0-based heading line and how many hunk pointers it holds. */
|
|
16047
16194
|
const reviewChunks = (content) => parseReviewDoc(content).changesets.map((c) => ({
|
|
16048
16195
|
title: c.title,
|
|
16049
16196
|
line: c.headingLine,
|
|
16050
16197
|
pointers: c.files.length,
|
|
16198
|
+
...c.section === void 0 ? {} : { section: c.section },
|
|
16051
16199
|
...c.severity === void 0 ? {} : { severityLine: c.severity.line },
|
|
16052
16200
|
files: c.files.map((f) => ({
|
|
16053
16201
|
path: f.path,
|
|
@@ -16055,6 +16203,68 @@ const reviewChunks = (content) => parseReviewDoc(content).changesets.map((c) =>
|
|
|
16055
16203
|
...f.rangeEnd === void 0 ? {} : { rangeEnd: f.rangeEnd }
|
|
16056
16204
|
}))
|
|
16057
16205
|
}));
|
|
16206
|
+
/** The `## Findings` / `## Walkthrough` headings in `content`, with their 0-based lines. */
|
|
16207
|
+
const reviewSections = (content) => splitChunks(parseMarkdown(content), content).sections.map((s) => ({
|
|
16208
|
+
section: s.section,
|
|
16209
|
+
line: s.headingLine
|
|
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)];
|
|
16224
|
+
};
|
|
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")}`;
|
|
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) => {
|
|
16241
|
+
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));
|
|
16251
|
+
};
|
|
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];
|
|
16259
|
+
};
|
|
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`;
|
|
16267
|
+
};
|
|
16058
16268
|
//#endregion
|
|
16059
16269
|
//#region src/steering/Edits.ts
|
|
16060
16270
|
/** Splices `edits` into `content`, sorted last-to-first so an earlier range's offset is never invalidated by a later edit. Positions are 0-based line/character over `\n`-split lines. */
|
|
@@ -16423,4 +16633,4 @@ const viewOf = (format, content) => ({
|
|
|
16423
16633
|
/** Every OPEN thread — last entry is the agent's — as `{ name, line, firstMe }` (0-based line). `[]` for a format with no thread concept. */
|
|
16424
16634
|
const openThreads = (format, content) => format.openThreads?.(content) ?? [];
|
|
16425
16635
|
//#endregion
|
|
16426
|
-
export {
|
|
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 };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { answered, head, judge, mark, numeric, read, refuse, removeScript, run, scope, sectionBodies, showScript, vars, writeScript } from "../flows.mjs";
|
|
2
2
|
import { lensEntriesOf, lensOf, qualityLap, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
-
import { A as reviewMissing, D as fixQuality, M as reviewing,
|
|
3
|
+
import { A as reviewMissing, D as fixQuality, M as reviewing, Mt as withLapFindings, O as fixRisks, S as awaitReview, T as fixCheck, ct as qualityTriageMessage, d as healthy, h as QUALITY, j as reviewQuality, q as handOffAwaitReviewMessage, st as qualityFindingsFile, u as guarded, y as REVIEW } from "./reserved-D0MKVFFa.mjs";
|
|
4
4
|
//#region src/workflows/shared/lenses.ts
|
|
5
5
|
const NO_FINDINGS = {
|
|
6
6
|
notes: [],
|