@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
package/package.json
CHANGED
package/src/steering/FORMATS.md
CHANGED
|
@@ -14,7 +14,10 @@ read them.
|
|
|
14
14
|
`<!-- base: <sha> -->` comment, then `##` chunks with prose and `- [ ]` hunk
|
|
15
15
|
pointers (`./path`, `./path#start-end`), each optionally followed by
|
|
16
16
|
`— <note>`. A pointer note starting `Risk:` is a reviewer's risk. A ticked
|
|
17
|
-
pointer is a hunk the human has read.
|
|
17
|
+
pointer is a hunk the human has read. A chunk's `Tick exactly one:` list is
|
|
18
|
+
its decision. A `## Findings` or `## Walkthrough` heading groups the `###`
|
|
19
|
+
chunks under it; a walkthrough chunk takes no decision. Any other `##` is a
|
|
20
|
+
chunk of its own.
|
|
18
21
|
- **Free-form**: any markdown, for a file with no mode or an unknown one. Only
|
|
19
22
|
its threads are validated.
|
|
20
23
|
- **Threads**, shared by all three: a footnote whose definition is a list of
|
|
@@ -64,6 +64,8 @@ export interface SteeringLink {
|
|
|
64
64
|
* format-agnostic: `review`'s chunk/hunk and `qa`'s question/option are the
|
|
65
65
|
* same two things here (a container and one of its numbered children), plus
|
|
66
66
|
* `paragraph`, which a client resolves from its cursor rather than the view.
|
|
67
|
+
* A `review` finding's answer boxes are `option`s too, `questionIndex` being
|
|
68
|
+
* the chunk's index.
|
|
67
69
|
*/
|
|
68
70
|
export type SteeringAnchor =
|
|
69
71
|
| { readonly kind: "chunk"; readonly index: number }
|
|
@@ -110,7 +112,7 @@ export interface SteeringViewNode {
|
|
|
110
112
|
readonly detail?: string
|
|
111
113
|
/** A free-form status label, when the format has one (`qa`'s `"open"`/`"answered"`). */
|
|
112
114
|
readonly status?: string
|
|
113
|
-
/** `true` when this node's own condition is fully satisfied (`qa`'s answered
|
|
115
|
+
/** `true` when this node's own condition is fully satisfied (`qa`'s answered question, `review`'s decided finding) — distinct from `checked`, which is a per-item tick. */
|
|
114
116
|
readonly answered?: boolean
|
|
115
117
|
/** This node's own checkbox state, when it has one (a hunk's tick, an option's tick). */
|
|
116
118
|
readonly checked?: boolean
|
|
@@ -138,6 +140,10 @@ export interface SteeringViewNode {
|
|
|
138
140
|
readonly endLine?: number
|
|
139
141
|
/** Where `annotate` attaches a NEW note to this node. */
|
|
140
142
|
readonly anchor: SteeringAnchor
|
|
143
|
+
/** The section a top-level node sits in, when the format groups them (`review`'s `Findings` / `Walkthrough`). */
|
|
144
|
+
readonly group?: string
|
|
145
|
+
/** A `review` finding's answer boxes, in order — fix as suggested, won't fix, and the free-text slot, whose `title` is the human's text (`""` while unfilled). Kept out of `children`, which are its hunks. */
|
|
146
|
+
readonly decision?: readonly SteeringViewNode[]
|
|
141
147
|
readonly children?: readonly SteeringViewNode[]
|
|
142
148
|
/**
|
|
143
149
|
* A node's body, a SIBLING of `children` and never a reuse of it: a `qa`
|
|
@@ -97,7 +97,8 @@ describe.each(FORMATS)("the %s format", (dir, format, ticks) => {
|
|
|
97
97
|
|
|
98
98
|
if (ticks)
|
|
99
99
|
it("round-trips every tick: writing back what was read leaves the bytes unchanged", () => {
|
|
100
|
-
|
|
100
|
+
const nodes = nodesOf(format.view(content).nodes)
|
|
101
|
+
for (const node of [...nodes, ...nodes.flatMap((n) => n.decision ?? [])]) {
|
|
101
102
|
if (node.checked === undefined || node.anchor.kind === "chunk") continue
|
|
102
103
|
const anchor: SteeringAnchor = node.anchor
|
|
103
104
|
const same = write(
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Review: 1a2b3c4
|
|
2
|
+
|
|
3
|
+
<!-- base: 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b -->
|
|
4
|
+
|
|
5
|
+
## Findings
|
|
6
|
+
|
|
7
|
+
### A lone CR is kept
|
|
8
|
+
|
|
9
|
+
_should: contract_
|
|
10
|
+
|
|
11
|
+
The parser keeps a lone `\r` as content.
|
|
12
|
+
|
|
13
|
+
In the code:
|
|
14
|
+
|
|
15
|
+
- [x] ./src/parse.ts#30-31 — where it is kept
|
|
16
|
+
|
|
17
|
+
Tick exactly one:
|
|
18
|
+
|
|
19
|
+
- [ ] fix as suggested
|
|
20
|
+
- [x] won't fix
|
|
21
|
+
- [ ] _your answer_
|
|
22
|
+
|
|
23
|
+
## Walkthrough
|
|
24
|
+
|
|
25
|
+
### Parser accepts CRLF
|
|
26
|
+
|
|
27
|
+
- [ ] ./src/parse.ts#10-14 — normalizes `\r\n` first
|
|
28
|
+
|
|
29
|
+
## Fixed automatically
|
|
30
|
+
|
|
31
|
+
- a test name typo (0123456)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"header": "1a2b3c4",
|
|
3
|
+
"nodes": [
|
|
4
|
+
{
|
|
5
|
+
"title": "A lone CR is kept",
|
|
6
|
+
"detail": "The parser keeps a lone `\\r` as content. In the code:",
|
|
7
|
+
"body": [
|
|
8
|
+
{
|
|
9
|
+
"title": "The parser keeps a lone `\\r` as content.",
|
|
10
|
+
"anchor": {
|
|
11
|
+
"kind": "paragraph",
|
|
12
|
+
"line": 10
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "In the code:",
|
|
17
|
+
"anchor": {
|
|
18
|
+
"kind": "paragraph",
|
|
19
|
+
"line": 12
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"status": "should: contract",
|
|
24
|
+
"anchor": {
|
|
25
|
+
"kind": "chunk",
|
|
26
|
+
"index": 0
|
|
27
|
+
},
|
|
28
|
+
"group": "Findings",
|
|
29
|
+
"answered": true,
|
|
30
|
+
"decision": [
|
|
31
|
+
{
|
|
32
|
+
"title": "fix as suggested",
|
|
33
|
+
"checked": false,
|
|
34
|
+
"anchor": {
|
|
35
|
+
"kind": "option",
|
|
36
|
+
"questionIndex": 0,
|
|
37
|
+
"index": 0
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"title": "won't fix",
|
|
42
|
+
"checked": true,
|
|
43
|
+
"anchor": {
|
|
44
|
+
"kind": "option",
|
|
45
|
+
"questionIndex": 0,
|
|
46
|
+
"index": 1
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"title": "",
|
|
51
|
+
"checked": false,
|
|
52
|
+
"anchor": {
|
|
53
|
+
"kind": "option",
|
|
54
|
+
"questionIndex": 0,
|
|
55
|
+
"index": 2
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"children": [
|
|
60
|
+
{
|
|
61
|
+
"title": "./src/parse.ts#30-31",
|
|
62
|
+
"path": "./src/parse.ts",
|
|
63
|
+
"line": 30,
|
|
64
|
+
"endLine": 31,
|
|
65
|
+
"checked": true,
|
|
66
|
+
"detail": "where it is kept",
|
|
67
|
+
"anchor": {
|
|
68
|
+
"kind": "hunk",
|
|
69
|
+
"chunkIndex": 0,
|
|
70
|
+
"index": 0
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"title": "Parser accepts CRLF",
|
|
77
|
+
"detail": "",
|
|
78
|
+
"body": [],
|
|
79
|
+
"anchor": {
|
|
80
|
+
"kind": "chunk",
|
|
81
|
+
"index": 1
|
|
82
|
+
},
|
|
83
|
+
"group": "Walkthrough",
|
|
84
|
+
"children": [
|
|
85
|
+
{
|
|
86
|
+
"title": "./src/parse.ts#10-14",
|
|
87
|
+
"path": "./src/parse.ts",
|
|
88
|
+
"line": 10,
|
|
89
|
+
"endLine": 14,
|
|
90
|
+
"checked": false,
|
|
91
|
+
"detail": "normalizes `\\r\\n` first",
|
|
92
|
+
"anchor": {
|
|
93
|
+
"kind": "hunk",
|
|
94
|
+
"chunkIndex": 1,
|
|
95
|
+
"index": 0
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"title": "Fixed automatically",
|
|
102
|
+
"detail": "- a test name typo (0123456)",
|
|
103
|
+
"body": [
|
|
104
|
+
{
|
|
105
|
+
"title": "a test name typo (0123456)",
|
|
106
|
+
"anchor": {
|
|
107
|
+
"kind": "paragraph",
|
|
108
|
+
"line": 30
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"anchor": {
|
|
113
|
+
"kind": "chunk",
|
|
114
|
+
"index": 2
|
|
115
|
+
},
|
|
116
|
+
"children": []
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
package/src/steering/index.ts
CHANGED
|
@@ -16,11 +16,15 @@ export {
|
|
|
16
16
|
reviewDecisions,
|
|
17
17
|
reviewNotes,
|
|
18
18
|
reviewRisks,
|
|
19
|
+
reviewSections,
|
|
20
|
+
REVIEW_SECTIONS,
|
|
21
|
+
sectionedReview,
|
|
19
22
|
severityLine,
|
|
20
23
|
unansweredNotes,
|
|
21
24
|
type ReviewChoice,
|
|
22
25
|
type ReviewDecision,
|
|
23
26
|
type ReviewNote,
|
|
27
|
+
type ReviewSection,
|
|
24
28
|
} from "./review.js"
|
|
25
29
|
export { applySteeringEdits } from "./Edits.js"
|
|
26
30
|
|
package/src/steering/qa.ts
CHANGED
|
@@ -830,7 +830,7 @@ const replaceOptionTextEdit = (
|
|
|
830
830
|
* offset for, breaking this anchor's `apply` forever. The placeholder reads
|
|
831
831
|
* back as `""`, so an erase still reads as unanswered.
|
|
832
832
|
*/
|
|
833
|
-
const normalizeFreeTextAnswer = (text: string): string => {
|
|
833
|
+
export const normalizeFreeTextAnswer = (text: string): string => {
|
|
834
834
|
const normalized = text.replace(/\s+/g, " ").trim()
|
|
835
835
|
return normalized.length === 0 ? FREE_TEXT_PLACEHOLDER : normalized
|
|
836
836
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import { FOOTNOTE_ACTION_TITLE, steeringFormatFor, viewOf } from "./index.js"
|
|
3
|
-
import { parseReviewDoc } from "./review.js"
|
|
3
|
+
import { parseReviewDoc, unansweredNotes } from "./review.js"
|
|
4
4
|
import { getParseCount } from "./index.js"
|
|
5
5
|
|
|
6
6
|
describe("parseReviewDoc", () => {
|
|
@@ -1532,13 +1532,90 @@ describe("review.apply", () => {
|
|
|
1532
1532
|
).toEqual({ ok: false, reason: "anchor-not-found" })
|
|
1533
1533
|
})
|
|
1534
1534
|
|
|
1535
|
-
it("a question
|
|
1535
|
+
it("a question anchor — not this format's own kind — refuses anchor-not-found", () => {
|
|
1536
1536
|
expect(review.apply(NESTED_CONTENT, { kind: "question", index: 0 }, { checked: true })).toEqual(
|
|
1537
1537
|
{ ok: false, reason: "anchor-not-found" },
|
|
1538
1538
|
)
|
|
1539
1539
|
})
|
|
1540
1540
|
})
|
|
1541
1541
|
|
|
1542
|
+
describe("review decisions — a finding's answer boxes", () => {
|
|
1543
|
+
const FINDINGS = [
|
|
1544
|
+
"# Review: abc1234",
|
|
1545
|
+
"",
|
|
1546
|
+
"<!-- base: abc1234def5678901234567890123456789abcd -->",
|
|
1547
|
+
"",
|
|
1548
|
+
"## Findings",
|
|
1549
|
+
"",
|
|
1550
|
+
"### Token check",
|
|
1551
|
+
"",
|
|
1552
|
+
"- [ ] ./src/auth.ts#1-2",
|
|
1553
|
+
"",
|
|
1554
|
+
"Tick exactly one:",
|
|
1555
|
+
"",
|
|
1556
|
+
"- [ ] fix as suggested",
|
|
1557
|
+
"- [x] won't fix",
|
|
1558
|
+
"- [ ] _your answer_",
|
|
1559
|
+
"",
|
|
1560
|
+
"## Walkthrough",
|
|
1561
|
+
"",
|
|
1562
|
+
"### Rename",
|
|
1563
|
+
"",
|
|
1564
|
+
"- [ ] ./src/calc.ts#1-1",
|
|
1565
|
+
"",
|
|
1566
|
+
].join("\n")
|
|
1567
|
+
const option = (index: number) => ({ kind: "option" as const, questionIndex: 0, index })
|
|
1568
|
+
const write = (content: string, index: number, opts: { checked?: boolean; text?: string }) => {
|
|
1569
|
+
const result = review.apply(content, option(index), opts)
|
|
1570
|
+
if (!result.ok) throw new Error(result.reason)
|
|
1571
|
+
return applyEdits(content, result.edits)
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
it("views a finding's boxes as its decision, apart from its hunks", () => {
|
|
1575
|
+
const [finding, walkthrough] = review.view(FINDINGS).nodes
|
|
1576
|
+
expect(finding!.answered).toBe(true)
|
|
1577
|
+
expect(finding!.decision!.map((o) => [o.title, o.checked])).toEqual([
|
|
1578
|
+
["fix as suggested", false],
|
|
1579
|
+
["won't fix", true],
|
|
1580
|
+
["", false],
|
|
1581
|
+
])
|
|
1582
|
+
expect(finding!.children!.map((c) => c.anchor.kind)).toEqual(["hunk"])
|
|
1583
|
+
expect(walkthrough!.decision).toBeUndefined()
|
|
1584
|
+
expect(walkthrough!.answered).toBeUndefined()
|
|
1585
|
+
})
|
|
1586
|
+
|
|
1587
|
+
it("ticking a box unticks the others", () => {
|
|
1588
|
+
const after = write(FINDINGS, 0, { checked: true })
|
|
1589
|
+
expect(after).toContain("- [x] fix as suggested\n- [ ] won't fix\n- [ ] _your answer_")
|
|
1590
|
+
expect(unansweredNotes(after)).toEqual([])
|
|
1591
|
+
})
|
|
1592
|
+
|
|
1593
|
+
it("an answer ticks the free-text slot and writes its text in one edit set", () => {
|
|
1594
|
+
const after = write(FINDINGS, 2, { checked: true, text: "Only for\n admin tokens " })
|
|
1595
|
+
expect(after).toContain("- [ ] won't fix\n- [x] Only for admin tokens\n")
|
|
1596
|
+
expect(review.view(after).nodes[0]!.decision![2]!.title).toBe("Only for admin tokens")
|
|
1597
|
+
expect(unansweredNotes(after)).toEqual([])
|
|
1598
|
+
expect(review.validate(after)).toEqual([])
|
|
1599
|
+
})
|
|
1600
|
+
|
|
1601
|
+
it("clearing the answer unticks the slot and restores its placeholder", () => {
|
|
1602
|
+
const answered = write(FINDINGS, 2, { checked: true, text: "Later" })
|
|
1603
|
+
const cleared = write(answered, 2, { checked: false, text: "" })
|
|
1604
|
+
expect(cleared).toContain("- [ ] won't fix\n- [ ] _your answer_\n")
|
|
1605
|
+
expect(review.view(cleared).nodes[0]!.answered).toBe(false)
|
|
1606
|
+
})
|
|
1607
|
+
|
|
1608
|
+
it("a walkthrough chunk or a missing box refuses anchor-not-found", () => {
|
|
1609
|
+
expect(
|
|
1610
|
+
review.apply(FINDINGS, { kind: "option", questionIndex: 1, index: 0 }, { checked: true }),
|
|
1611
|
+
).toEqual({ ok: false, reason: "anchor-not-found" })
|
|
1612
|
+
expect(review.apply(FINDINGS, option(3), { checked: true })).toEqual({
|
|
1613
|
+
ok: false,
|
|
1614
|
+
reason: "anchor-not-found",
|
|
1615
|
+
})
|
|
1616
|
+
})
|
|
1617
|
+
})
|
|
1618
|
+
|
|
1542
1619
|
describe("review.view — chunk-level footnote projection", () => {
|
|
1543
1620
|
it("projects a footnote marker on the chunk's own heading line as that chunk node's own `note`, distinct from a hunk's own note", () => {
|
|
1544
1621
|
// review.sample carries exactly this shape: `## Sample chunk[^naduiqc4]`
|
|
@@ -1899,7 +1976,7 @@ describe("reviewChunks", async () => {
|
|
|
1899
1976
|
"",
|
|
1900
1977
|
"<!-- base: abc1234def5678901234567890123456789abcd -->",
|
|
1901
1978
|
"",
|
|
1902
|
-
"##
|
|
1979
|
+
"## Changes",
|
|
1903
1980
|
"",
|
|
1904
1981
|
"- [ ] ./src/a.ts#1-3 — first",
|
|
1905
1982
|
"- [ ] ./src/b.ts#4-6 — second",
|
|
@@ -1914,7 +1991,7 @@ describe("reviewChunks", async () => {
|
|
|
1914
1991
|
].join("\n")
|
|
1915
1992
|
expect(reviewChunks(content)).toEqual([
|
|
1916
1993
|
{
|
|
1917
|
-
title: "
|
|
1994
|
+
title: "Changes",
|
|
1918
1995
|
line: 4,
|
|
1919
1996
|
pointers: 2,
|
|
1920
1997
|
files: [
|
|
@@ -2039,3 +2116,192 @@ describe("answer boxes", async () => {
|
|
|
2039
2116
|
])
|
|
2040
2117
|
})
|
|
2041
2118
|
})
|
|
2119
|
+
|
|
2120
|
+
describe("review — sectioned layout", async () => {
|
|
2121
|
+
const { reviewChunks, reviewDecisions, reviewSections, sectionedReview, unansweredNotes } =
|
|
2122
|
+
await import("./review.js")
|
|
2123
|
+
const boxes = (ticked?: 0 | 1 | 2): string[] => [
|
|
2124
|
+
"Tick exactly one:",
|
|
2125
|
+
"",
|
|
2126
|
+
`- [${ticked === 0 ? "x" : " "}] fix as suggested`,
|
|
2127
|
+
`- [${ticked === 1 ? "x" : " "}] won't fix`,
|
|
2128
|
+
`- [${ticked === 2 ? "x" : " "}] _your answer_`,
|
|
2129
|
+
]
|
|
2130
|
+
const head = [
|
|
2131
|
+
"# Review: abc1234",
|
|
2132
|
+
"",
|
|
2133
|
+
"<!-- base: abc1234def5678901234567890123456789abcd -->",
|
|
2134
|
+
"",
|
|
2135
|
+
]
|
|
2136
|
+
const sectioned = (ticked?: 0 | 1 | 2): string =>
|
|
2137
|
+
[
|
|
2138
|
+
...head,
|
|
2139
|
+
"## Findings",
|
|
2140
|
+
"",
|
|
2141
|
+
"### A finding",
|
|
2142
|
+
"",
|
|
2143
|
+
"_critical: security_",
|
|
2144
|
+
"",
|
|
2145
|
+
"Leaks a token.",
|
|
2146
|
+
"",
|
|
2147
|
+
"- [ ] ./src/a.ts#1-2 — here",
|
|
2148
|
+
"",
|
|
2149
|
+
...boxes(ticked),
|
|
2150
|
+
"",
|
|
2151
|
+
"## Walkthrough",
|
|
2152
|
+
"",
|
|
2153
|
+
"### Wiring",
|
|
2154
|
+
"",
|
|
2155
|
+
"- [x] ./src/b.ts#3-4 — wires it",
|
|
2156
|
+
"",
|
|
2157
|
+
"## Fixed automatically",
|
|
2158
|
+
"",
|
|
2159
|
+
"- f (fedcba9)",
|
|
2160
|
+
"",
|
|
2161
|
+
].join("\n")
|
|
2162
|
+
|
|
2163
|
+
it("reads `###` chunks under the sections and other `## ` chunks on their own", () => {
|
|
2164
|
+
expect(review.validate(sectioned())).toEqual([])
|
|
2165
|
+
expect(reviewChunks(sectioned())).toEqual([
|
|
2166
|
+
expect.objectContaining({ title: "A finding", line: 6, pointers: 1, section: "findings" }),
|
|
2167
|
+
expect.objectContaining({ title: "Wiring", line: 22, pointers: 1, section: "walkthrough" }),
|
|
2168
|
+
{ title: "Fixed automatically", line: 26, pointers: 0, files: [] },
|
|
2169
|
+
])
|
|
2170
|
+
expect(reviewSections(sectioned())).toEqual([
|
|
2171
|
+
{ section: "findings", line: 4 },
|
|
2172
|
+
{ section: "walkthrough", line: 20 },
|
|
2173
|
+
])
|
|
2174
|
+
})
|
|
2175
|
+
|
|
2176
|
+
it("reads a decision from a finding", () => {
|
|
2177
|
+
expect(unansweredNotes(sectioned())).toEqual([
|
|
2178
|
+
{ title: "A finding", line: 7, reason: "tick exactly one box" },
|
|
2179
|
+
])
|
|
2180
|
+
expect(reviewDecisions(sectioned(), sectioned(1))).toEqual([
|
|
2181
|
+
expect.objectContaining({ anchor: "A finding", choice: "wontfix" }),
|
|
2182
|
+
])
|
|
2183
|
+
})
|
|
2184
|
+
|
|
2185
|
+
it("reads no decision in the walkthrough: boxes there are a chunk gone wrong", () => {
|
|
2186
|
+
const content = sectioned(0).replace(
|
|
2187
|
+
"- [x] ./src/b.ts#3-4 — wires it\n",
|
|
2188
|
+
["- [x] ./src/b.ts#3-4 — wires it", "", ...boxes(1), ""].join("\n"),
|
|
2189
|
+
)
|
|
2190
|
+
expect(unansweredNotes(content)).toEqual([])
|
|
2191
|
+
expect(reviewDecisions(sectioned(), content)).toEqual([
|
|
2192
|
+
expect.objectContaining({ anchor: "A finding", choice: "fix" }),
|
|
2193
|
+
])
|
|
2194
|
+
})
|
|
2195
|
+
|
|
2196
|
+
it("still reads the flat layout, every `## ` a chunk and its boxes a decision", () => {
|
|
2197
|
+
const flat = [...head, "## A finding", "", "Leaks a token.", "", ...boxes(), ""].join("\n")
|
|
2198
|
+
expect(review.validate(flat)).toEqual([])
|
|
2199
|
+
expect(reviewChunks(flat)).toEqual([{ title: "A finding", line: 4, pointers: 0, files: [] }])
|
|
2200
|
+
expect(unansweredNotes(flat)).toHaveLength(1)
|
|
2201
|
+
})
|
|
2202
|
+
|
|
2203
|
+
it("outlines the sections as containers, each finding's decision a node of its own", () => {
|
|
2204
|
+
const outline = viewOf(review, sectioned()).outline
|
|
2205
|
+
expect(outline.map((n) => n.name)).toEqual(["Findings", "Walkthrough", "Fixed automatically"])
|
|
2206
|
+
expect(outline[0]!.children!.map((n) => n.name)).toEqual(["A finding (0/1)"])
|
|
2207
|
+
expect(outline[0]!.children![0]!.children!.map((n) => n.name)).toEqual(["Decision: open"])
|
|
2208
|
+
expect(outline[0]!.children![0]!.children![0]!.selectionRange.start.line).toBe(14)
|
|
2209
|
+
// A fully ticked walkthrough chunk is reviewed: its section is left empty.
|
|
2210
|
+
expect(outline[1]!.children).toEqual([])
|
|
2211
|
+
})
|
|
2212
|
+
|
|
2213
|
+
it("names the ticked decision, and keeps a decided finding while a hunk is unticked", () => {
|
|
2214
|
+
const outline = viewOf(review, sectioned(1)).outline
|
|
2215
|
+
expect(outline[0]!.children![0]!.children!.map((n) => n.name)).toEqual(["Decision: won't fix"])
|
|
2216
|
+
})
|
|
2217
|
+
|
|
2218
|
+
it("groups the view's chunks by section, anchors unchanged", () => {
|
|
2219
|
+
const { nodes } = review.view(sectioned())
|
|
2220
|
+
expect(nodes.map((n) => [n.title, n.group, n.anchor])).toEqual([
|
|
2221
|
+
["A finding", "Findings", { kind: "chunk", index: 0 }],
|
|
2222
|
+
["Wiring", "Walkthrough", { kind: "chunk", index: 1 }],
|
|
2223
|
+
["Fixed automatically", undefined, { kind: "chunk", index: 2 }],
|
|
2224
|
+
])
|
|
2225
|
+
})
|
|
2226
|
+
|
|
2227
|
+
it("offers a chunk's toggle only within its own span, never on the next section's heading", () => {
|
|
2228
|
+
const titles = viewOf(review, sectioned())
|
|
2229
|
+
.actionsAt({ start: { line: 20, character: 0 }, end: { line: 20, character: 0 } })
|
|
2230
|
+
.map((a) => a.title)
|
|
2231
|
+
expect(titles.some((t) => t.includes("A finding"))).toBe(false)
|
|
2232
|
+
})
|
|
2233
|
+
|
|
2234
|
+
describe("sectionedReview", () => {
|
|
2235
|
+
const flat = [
|
|
2236
|
+
...head,
|
|
2237
|
+
"## Weakened scenarios",
|
|
2238
|
+
"",
|
|
2239
|
+
"- a.feature — s — Then x",
|
|
2240
|
+
"",
|
|
2241
|
+
"## A finding",
|
|
2242
|
+
"",
|
|
2243
|
+
"_should: contract_",
|
|
2244
|
+
"",
|
|
2245
|
+
"Leaks a token.",
|
|
2246
|
+
"",
|
|
2247
|
+
...boxes(),
|
|
2248
|
+
"",
|
|
2249
|
+
"Wiring",
|
|
2250
|
+
"------",
|
|
2251
|
+
"",
|
|
2252
|
+
"### Detail",
|
|
2253
|
+
"",
|
|
2254
|
+
"- [ ] ./src/b.ts#3-4 — wires it",
|
|
2255
|
+
"",
|
|
2256
|
+
"## Fixed automatically",
|
|
2257
|
+
"",
|
|
2258
|
+
"- f (fedcba9)",
|
|
2259
|
+
"",
|
|
2260
|
+
].join("\n")
|
|
2261
|
+
|
|
2262
|
+
it("moves boxed chunks under Findings and the named ones under Walkthrough, a level deeper", () => {
|
|
2263
|
+
expect(sectionedReview(flat, new Set(["Wiring"]))).toBe(
|
|
2264
|
+
[
|
|
2265
|
+
...head,
|
|
2266
|
+
"## Weakened scenarios",
|
|
2267
|
+
"",
|
|
2268
|
+
"- a.feature — s — Then x",
|
|
2269
|
+
"",
|
|
2270
|
+
"## Findings",
|
|
2271
|
+
"",
|
|
2272
|
+
"### A finding",
|
|
2273
|
+
"",
|
|
2274
|
+
"_should: contract_",
|
|
2275
|
+
"",
|
|
2276
|
+
"Leaks a token.",
|
|
2277
|
+
"",
|
|
2278
|
+
...boxes(),
|
|
2279
|
+
"",
|
|
2280
|
+
"## Walkthrough",
|
|
2281
|
+
"",
|
|
2282
|
+
"### Wiring",
|
|
2283
|
+
"",
|
|
2284
|
+
"#### Detail",
|
|
2285
|
+
"",
|
|
2286
|
+
"- [ ] ./src/b.ts#3-4 — wires it",
|
|
2287
|
+
"",
|
|
2288
|
+
"## Fixed automatically",
|
|
2289
|
+
"",
|
|
2290
|
+
"- f (fedcba9)",
|
|
2291
|
+
"",
|
|
2292
|
+
].join("\n"),
|
|
2293
|
+
)
|
|
2294
|
+
})
|
|
2295
|
+
|
|
2296
|
+
it("is valid and stable once sectioned", () => {
|
|
2297
|
+
const once = sectionedReview(flat, new Set(["Wiring"]))
|
|
2298
|
+
expect(review.validate(once)).toEqual([])
|
|
2299
|
+
expect(sectionedReview(once, new Set(["Wiring"]))).toBe(once)
|
|
2300
|
+
})
|
|
2301
|
+
|
|
2302
|
+
it("leaves a review with nothing to group byte for byte", () => {
|
|
2303
|
+
const plain = [...head, "## Nothing to note", "", "Review the diff yourself.", ""].join("\n")
|
|
2304
|
+
expect(sectionedReview(plain, new Set())).toBe(plain)
|
|
2305
|
+
})
|
|
2306
|
+
})
|
|
2307
|
+
})
|