@pmelab/gtd 28.2.0 → 29.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 +13 -4
- package/dist/chunks/{reserved-DeBS4Ymh.mjs → reserved-DoHUII2D.mjs} +206 -121
- package/dist/chunks/{tail-B2SER2Ih.mjs → tail-C6YBG5zh.mjs} +19 -12
- package/dist/testkit.mjs +2 -1
- package/dist/workflow-kit.mjs +51 -21
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +247 -96
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +528 -262
- package/package.json +1 -1
- package/src/testkit/TestRepo.ts +3 -0
- package/src/testkit/index.ts +1 -1
- package/src/testkit/steps.ts +44 -0
- package/src/workflow-kit/review.test.ts +366 -8
- package/src/workflow-kit/review.ts +109 -26
- package/src/workflows/feature/feature.test.ts +313 -1
- package/src/workflows/feature/feature.ts +48 -43
- package/src/workflows/feature/one-step-land.feature +4 -0
- package/src/workflows/feature/refactor-first.feature +54 -24
- package/src/workflows/feature/refactor-review.feature +1706 -0
- package/src/workflows/feature/refactor.test.ts +592 -2
- package/src/workflows/feature/refactor.ts +160 -42
- package/src/workflows/feature/review.test.ts +322 -5
- package/src/workflows/feature/review.ts +77 -10
- package/src/workflows/feature/seam-rejection.feature +4 -0
- package/src/workflows/feature/seam.feature +3 -4
- package/src/workflows/feature/seam.test.ts +196 -1
- package/src/workflows/feature/seam.ts +23 -9
- package/src/workflows/feature/steps.test.ts +54 -2
- package/src/workflows/feature/steps.ts +45 -9
- package/src/workflows/feature/tier.test.ts +4 -0
- package/src/workflows/feature/tier.ts +2 -20
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +97 -0
- package/src/workflows/shared/access.test.ts +31 -1
- package/src/workflows/shared/access.ts +7 -1
- package/src/workflows/shared/fences.ts +19 -0
- package/src/workflows/shared/health.ts +32 -12
- package/src/workflows/shared/options.test.ts +18 -0
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/shared/prose.ts +4 -2
- package/src/workflows/shared/skills.test.ts +20 -1
- package/src/workflows/shared/skills.ts +43 -32
- package/src/workflows/shared/steps.ts +33 -19
- package/src/workflows/shared/tail.test.ts +84 -1
- package/src/workflows/shared/tail.ts +30 -12
- package/src/workflows/shared/text.test.ts +266 -3
- package/src/workflows/shared/text.ts +183 -69
- package/src/workflows/shared/vars.ts +12 -2
|
@@ -29,16 +29,29 @@ export const REVIEW_ORIG = ".gtd/REVIEW.orig.md"
|
|
|
29
29
|
* finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
|
|
30
30
|
* has any.
|
|
31
31
|
*/
|
|
32
|
-
export const qualityLap = (
|
|
33
|
-
|
|
32
|
+
export const qualityLap = (
|
|
33
|
+
guard?: Guard,
|
|
34
|
+
lenses?: readonly string[],
|
|
35
|
+
base?: string,
|
|
36
|
+
): Promise<"clean" | "findings"> =>
|
|
37
|
+
kit.qualityLap(
|
|
38
|
+
lenses ?? kit.lensesOf(vars.qualityReviews ?? ""),
|
|
39
|
+
guarded((lens: string) => reviewQuality(lens, base), guard),
|
|
40
|
+
QUALITY,
|
|
41
|
+
)
|
|
34
42
|
|
|
35
43
|
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
36
|
-
export const fixQualityFindings = (
|
|
44
|
+
export const fixQualityFindings = (
|
|
45
|
+
escalations: EscalationCount,
|
|
46
|
+
guard?: Guard,
|
|
47
|
+
guardRests = true,
|
|
48
|
+
rules?: string,
|
|
49
|
+
): Promise<boolean> =>
|
|
37
50
|
kit.fixFindings({
|
|
38
|
-
fix: guarded(fixQuality, guard),
|
|
51
|
+
fix: guarded(() => fixQuality(rules), guard),
|
|
39
52
|
findings: QUALITY,
|
|
40
53
|
cap: FIX_CAP,
|
|
41
|
-
escalate: () => escalation(escalations, guard),
|
|
54
|
+
escalate: () => escalation(escalations, guard, guardRests, rules),
|
|
42
55
|
})
|
|
43
56
|
|
|
44
57
|
/** `.gtd/REVIEW.md` removed at the end of a round at commit `round`. */
|
|
@@ -61,27 +74,32 @@ export const refusePendingHandOff = (): void => {
|
|
|
61
74
|
* The build tail: fix (when entered red), keep green, the quality lap, then
|
|
62
75
|
* `finish` — the human review, as the calling workflow runs it. `green`: the
|
|
63
76
|
* caller just left the full suite green (the package loop), so no run opens it.
|
|
77
|
+
* `guardRests: false` leaves the escalation's human rests unguarded.
|
|
64
78
|
*/
|
|
65
79
|
export const tail = <T>(
|
|
66
80
|
fixFirst: boolean,
|
|
67
81
|
finish: (escalations: EscalationCount) => Promise<T>,
|
|
68
82
|
green = false,
|
|
69
83
|
guard?: Guard,
|
|
84
|
+
lenses?: readonly string[],
|
|
85
|
+
guardRests = true,
|
|
86
|
+
lensBase?: string,
|
|
87
|
+
rules?: string,
|
|
70
88
|
): Promise<T> =>
|
|
71
89
|
scope("build", async () => {
|
|
72
90
|
const escalations: EscalationCount = { rounds: 0 }
|
|
73
|
-
const fix = fixCheck
|
|
91
|
+
const fix = () => fixCheck(rules)
|
|
74
92
|
const repair = async () => {
|
|
75
93
|
await guarded(fix, guard)()
|
|
76
|
-
await healthy(fix, { fixesSoFar: 1, escalations, guard })
|
|
94
|
+
await healthy(fix, { fixesSoFar: 1, escalations, guard, guardRests, rules })
|
|
77
95
|
}
|
|
78
96
|
// fixFirst's own fix + healthy below already is the full run.
|
|
79
97
|
if (fixFirst) await repair()
|
|
80
|
-
else if (!green) await healthy(fix, { escalations, guard })
|
|
98
|
+
else if (!green) await healthy(fix, { escalations, guard, guardRests, rules })
|
|
81
99
|
// The lap runs once a tail: after its findings are fixed, review follows.
|
|
82
|
-
if ((await qualityLap(guard)) === "findings") {
|
|
83
|
-
if (await fixQualityFindings(escalations, guard)) {
|
|
84
|
-
await healthy(fix, { escalations, guard })
|
|
100
|
+
if ((await qualityLap(guard, lenses, lensBase)) === "findings") {
|
|
101
|
+
if (await fixQualityFindings(escalations, guard, guardRests, rules)) {
|
|
102
|
+
await healthy(fix, { escalations, guard, guardRests, rules })
|
|
85
103
|
} else await repair()
|
|
86
104
|
}
|
|
87
105
|
return finish(escalations)
|
|
@@ -99,7 +117,7 @@ export const handOffReview = (
|
|
|
99
117
|
): Promise<void> =>
|
|
100
118
|
kit.reviewToHandOff(base, {
|
|
101
119
|
file: REVIEW,
|
|
102
|
-
reviewing: guarded(reviewing, guard),
|
|
120
|
+
reviewing: guarded((b, carry) => reviewing(b, carry), guard),
|
|
103
121
|
fixRisks: guarded(fixRisks, guard),
|
|
104
122
|
keepGreen: () => healthy(fixCheck, { escalations, guard }),
|
|
105
123
|
awaitReview: (b) => awaitReview(b, t.handOffAwaitReviewMessage(b)),
|
|
@@ -12,7 +12,12 @@ import {
|
|
|
12
12
|
architectureGateAnswerMessage,
|
|
13
13
|
buildFixPrompt,
|
|
14
14
|
buildFixQualityPrompt,
|
|
15
|
+
buildReviewFixNitsPrompt,
|
|
16
|
+
buildReviewFixRisksPrompt,
|
|
17
|
+
healthDescribePrompt,
|
|
18
|
+
refactorRules,
|
|
15
19
|
buildQualityReviewingPrompt,
|
|
20
|
+
specChallengeBrief,
|
|
16
21
|
buildReviewAwaitReviewMessage,
|
|
17
22
|
buildReviewCollectingPrompt,
|
|
18
23
|
designGateAnswerMessage,
|
|
@@ -20,6 +25,9 @@ import {
|
|
|
20
25
|
implementBuildingPrompt,
|
|
21
26
|
implementFixPrompt,
|
|
22
27
|
naivePassPrompt,
|
|
28
|
+
refactorBuildingPrompt,
|
|
29
|
+
refactorDroppedRefusal,
|
|
30
|
+
refactorFixPrompt,
|
|
23
31
|
seamByHandMessage,
|
|
24
32
|
seamSteerMessage,
|
|
25
33
|
summaryPrompt,
|
|
@@ -259,6 +267,29 @@ describe("buildQualityReviewingPrompt", () => {
|
|
|
259
267
|
"This lens's brief:\nBRIEF-TEXT",
|
|
260
268
|
)
|
|
261
269
|
})
|
|
270
|
+
it("reviews from the process start by default", () => {
|
|
271
|
+
const start = "START-SHA"
|
|
272
|
+
const text = renderText(() => buildQualityReviewingPrompt("x"), { start })
|
|
273
|
+
expect(text).toContain("from `START-SHA`")
|
|
274
|
+
})
|
|
275
|
+
it("reviews from a given base and still names the process start", () => {
|
|
276
|
+
const text = renderText(() => buildQualityReviewingPrompt("x", undefined, "BASE-SHA"), {
|
|
277
|
+
start: "START-SHA",
|
|
278
|
+
})
|
|
279
|
+
expect(text).toContain("from `BASE-SHA`")
|
|
280
|
+
expect(text).not.toContain("from `START-SHA`")
|
|
281
|
+
expect(text).toContain("started at `START-SHA`")
|
|
282
|
+
})
|
|
283
|
+
it("the spec-challenge brief looks up planning documents from the process start", () => {
|
|
284
|
+
const text = renderText(
|
|
285
|
+
() => buildQualityReviewingPrompt("x", specChallengeBrief, "BASE-SHA"),
|
|
286
|
+
{
|
|
287
|
+
start: "START-SHA",
|
|
288
|
+
},
|
|
289
|
+
)
|
|
290
|
+
expect(text).toContain("started at `START-SHA`")
|
|
291
|
+
expect(text).toContain("NOT the review base")
|
|
292
|
+
})
|
|
262
293
|
})
|
|
263
294
|
|
|
264
295
|
// These cases run real git and sh on purpose: they prove the command a person
|
|
@@ -746,6 +777,18 @@ Print the closing message and stop — this writes nothing itself.
|
|
|
746
777
|
})
|
|
747
778
|
})
|
|
748
779
|
|
|
780
|
+
describe("buildReviewCollectingPrompt without a refactor", () => {
|
|
781
|
+
it("is the feature prompt verbatim", () => {
|
|
782
|
+
const prompt = renderText(() => buildReviewCollectingPrompt("capture-text"))
|
|
783
|
+
expect(prompt).toBe(
|
|
784
|
+
readFileSync(
|
|
785
|
+
new URL("./__fixtures__/feature-collecting-prompt.txt", import.meta.url),
|
|
786
|
+
"utf8",
|
|
787
|
+
),
|
|
788
|
+
)
|
|
789
|
+
})
|
|
790
|
+
})
|
|
791
|
+
|
|
749
792
|
describe("the thread rules", () => {
|
|
750
793
|
it("are in all three answering prompts and no longer say a footnote is human input only", () => {
|
|
751
794
|
const prompts = renderText(() => [
|
|
@@ -812,7 +855,7 @@ describe("code threads in prompts", () => {
|
|
|
812
855
|
|
|
813
856
|
describe("buildReviewReviewingPrompt risk marking", () => {
|
|
814
857
|
it("states the Risk: marker rule and the re-review rule", () => {
|
|
815
|
-
const prompt = renderText(() => buildReviewReviewingPrompt("base"))
|
|
858
|
+
const prompt = renderText(() => buildReviewReviewingPrompt("base", "first"))
|
|
816
859
|
expect(prompt).toContain("Open a hunk's note with `Risk:` only for a concrete defect")
|
|
817
860
|
expect(prompt).toContain("never a style remark")
|
|
818
861
|
expect(prompt).toContain("describe what the fix changed under the")
|
|
@@ -922,7 +965,7 @@ describe("deferNotesScript — the in-memory recognizer agrees with the real sh
|
|
|
922
965
|
describe("buildReviewReviewingPrompt scenarios", () => {
|
|
923
966
|
it("names each removed assertion and asks for a Weakened: note", () => {
|
|
924
967
|
const prompt = renderText(() =>
|
|
925
|
-
buildReviewReviewingPrompt("base", undefined, [
|
|
968
|
+
buildReviewReviewingPrompt("base", "first", undefined, [
|
|
926
969
|
{
|
|
927
970
|
path: "e2e/a.feature",
|
|
928
971
|
lines: [{ scenario: "Scenario: shown", line: "Then it is shown" }],
|
|
@@ -934,7 +977,7 @@ describe("buildReviewReviewingPrompt scenarios", () => {
|
|
|
934
977
|
})
|
|
935
978
|
|
|
936
979
|
it("asks for no Weakened: note when nothing was removed", () => {
|
|
937
|
-
const prompt = renderText(() => buildReviewReviewingPrompt("base"))
|
|
980
|
+
const prompt = renderText(() => buildReviewReviewingPrompt("base", "first"))
|
|
938
981
|
expect(prompt).toContain("## Scenarios")
|
|
939
982
|
expect(prompt).not.toContain("Weakened:")
|
|
940
983
|
})
|
|
@@ -975,6 +1018,14 @@ describe("the seam phase's texts", () => {
|
|
|
975
1018
|
expect(low).toContain("git show abc123")
|
|
976
1019
|
})
|
|
977
1020
|
|
|
1021
|
+
it("drops the git show line when there is no reference", () => {
|
|
1022
|
+
const prompt = renderText(() => implementBuildingPrompt(undefined, "low"))
|
|
1023
|
+
expect(prompt).not.toContain("git show")
|
|
1024
|
+
expect(prompt).not.toContain("Reference only")
|
|
1025
|
+
expect(prompt).toContain("Add no release flag")
|
|
1026
|
+
expect(prompt).toContain("Implement them properly")
|
|
1027
|
+
})
|
|
1028
|
+
|
|
978
1029
|
it("keeps a fix turn to the feedback file's rules", () => {
|
|
979
1030
|
expect(renderText(() => implementFixPrompt())).toContain(".gtd/FEEDBACK.md")
|
|
980
1031
|
})
|
|
@@ -1003,3 +1054,215 @@ describe("the seam phase's texts", () => {
|
|
|
1003
1054
|
expect(message).not.toContain("a\nb.ts")
|
|
1004
1055
|
})
|
|
1005
1056
|
})
|
|
1057
|
+
|
|
1058
|
+
describe("the refactor prompts", () => {
|
|
1059
|
+
const strays = ["src/a.ts", "src/b.ts"]
|
|
1060
|
+
const pattern = "\\bGTD_FLAG_\\w+"
|
|
1061
|
+
|
|
1062
|
+
it("building and fix rules forbid every test file, with no stray exception", () => {
|
|
1063
|
+
for (const prompt of renderText(() => [
|
|
1064
|
+
refactorBuildingPrompt(".gtd/REFACTOR.md", strays, pattern),
|
|
1065
|
+
refactorFixPrompt(strays, pattern),
|
|
1066
|
+
])) {
|
|
1067
|
+
expect(prompt).toContain("never a test file")
|
|
1068
|
+
expect(prompt).not.toContain("only when it is")
|
|
1069
|
+
}
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
it("building adds the act-on-review-feedback line only when asked", () => {
|
|
1073
|
+
const [plain, feedback] = renderText(() => [
|
|
1074
|
+
refactorBuildingPrompt(".gtd/REFACTOR.md", strays, pattern),
|
|
1075
|
+
refactorBuildingPrompt(".gtd/REFACTOR.md", strays, pattern, undefined, true),
|
|
1076
|
+
])
|
|
1077
|
+
expect(plain).not.toContain("## Review feedback")
|
|
1078
|
+
expect(feedback).toContain("## Review feedback")
|
|
1079
|
+
})
|
|
1080
|
+
|
|
1081
|
+
describe("the review collecting prompt", () => {
|
|
1082
|
+
const prompt = renderText(() =>
|
|
1083
|
+
buildReviewCollectingPrompt("capture-text", { strays, round: "triaged" }),
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
it("folds into .gtd/REFACTOR.md and never names .gtd/REQUIREMENTS.md", () => {
|
|
1087
|
+
expect(prompt).toContain(".gtd/REFACTOR.md")
|
|
1088
|
+
expect(prompt).not.toContain(".gtd/REQUIREMENTS.md")
|
|
1089
|
+
expect(prompt).toContain("capture-text")
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
it("lists the strays and says a non-stray, test-file or flag concern cannot be built", () => {
|
|
1093
|
+
expect(prompt).toContain("`src/a.ts`") // gtd-path-exempt: fixture paths
|
|
1094
|
+
expect(prompt).toContain("`src/b.ts`") // gtd-path-exempt: fixture paths
|
|
1095
|
+
expect(prompt).toMatch(/cannot be built/)
|
|
1096
|
+
expect(prompt).toMatch(/test file/)
|
|
1097
|
+
expect(prompt).toMatch(/flag/)
|
|
1098
|
+
expect(prompt).toMatch(/behaviour/)
|
|
1099
|
+
expect(prompt).not.toMatch(/say in the reply/i)
|
|
1100
|
+
})
|
|
1101
|
+
|
|
1102
|
+
it("carries answered questions into the target's ## Answered Questions", () => {
|
|
1103
|
+
expect(prompt).toContain("`.gtd/REFACTOR.md`'s `## Answered Questions`")
|
|
1104
|
+
})
|
|
1105
|
+
|
|
1106
|
+
it("drops the reply rules and the closed review file's check", () => {
|
|
1107
|
+
expect(prompt).not.toContain("- A: <reply>")
|
|
1108
|
+
expect(prompt).not.toContain("gtd check review .gtd/REVIEW.md")
|
|
1109
|
+
expect(prompt).toContain("is already closed")
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
it("says an edit note or hand-edit must write the package", () => {
|
|
1113
|
+
expect(prompt).toContain("MUST write")
|
|
1114
|
+
expect(prompt).toContain("gtd abandon")
|
|
1115
|
+
})
|
|
1116
|
+
})
|
|
1117
|
+
})
|
|
1118
|
+
|
|
1119
|
+
describe("refactorDroppedRefusal", () => {
|
|
1120
|
+
const message = refactorDroppedRefusal({
|
|
1121
|
+
notes: [{ id: "note-2", anchor: "./src/a.ts#1-1", text: "rename it" }] as never,
|
|
1122
|
+
paths: ["src/b.ts"], // gtd-path-exempt: fixture paths
|
|
1123
|
+
})
|
|
1124
|
+
|
|
1125
|
+
it("starts with the refactor-feedback landing refusal", () => {
|
|
1126
|
+
expect(message.startsWith("gtd land: refactor-feedback:")).toBe(true)
|
|
1127
|
+
})
|
|
1128
|
+
|
|
1129
|
+
it("lists each note and each hand-edited path", () => {
|
|
1130
|
+
expect(message).toContain("note-2 (./src/a.ts#1-1): rename it")
|
|
1131
|
+
expect(message).toContain("src/b.ts") // gtd-path-exempt: fixture paths
|
|
1132
|
+
})
|
|
1133
|
+
|
|
1134
|
+
it("names gtd abandon and what it throws away", () => {
|
|
1135
|
+
expect(message).toContain("gtd abandon")
|
|
1136
|
+
expect(message).toContain("the naive pass, the refactor and its review")
|
|
1137
|
+
})
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
describe("the refactor collecting prompt", () => {
|
|
1141
|
+
const strays = ["src/a.ts"]
|
|
1142
|
+
|
|
1143
|
+
it("in-gate asks for one - A: per waiting thread, leaves one-shot footnotes and notes untouched, never says closed", () => {
|
|
1144
|
+
const prompt = renderText(() =>
|
|
1145
|
+
buildReviewCollectingPrompt("capture", { strays, round: "reply" }),
|
|
1146
|
+
)
|
|
1147
|
+
expect(prompt).toContain("exactly one `- A:` reply")
|
|
1148
|
+
expect(prompt).toContain("Fold each one-shot footnote and note")
|
|
1149
|
+
expect(prompt).toContain("gtd check review .gtd/REVIEW.md")
|
|
1150
|
+
expect(prompt).not.toContain("already closed")
|
|
1151
|
+
})
|
|
1152
|
+
|
|
1153
|
+
const closed = (round: "edits" | "notes" | "triaged", threads = false) =>
|
|
1154
|
+
renderText(() => buildReviewCollectingPrompt("capture", { strays, round }), {
|
|
1155
|
+
codeThreads: threads
|
|
1156
|
+
? [{ path: "src/a.ts", line: 4, waitingOn: "agent", first: "why?", faults: [] }]
|
|
1157
|
+
: [],
|
|
1158
|
+
})
|
|
1159
|
+
|
|
1160
|
+
it('closed "edits" folds every hand-edit and every note the human left that round', () => {
|
|
1161
|
+
expect(closed("edits")).toContain(
|
|
1162
|
+
"Fold every hand-edit and every note the human left that round",
|
|
1163
|
+
)
|
|
1164
|
+
})
|
|
1165
|
+
|
|
1166
|
+
it('closed "notes" folds each note that asks for a change', () => {
|
|
1167
|
+
expect(closed("notes")).toContain("Fold each note that asks for a change")
|
|
1168
|
+
})
|
|
1169
|
+
|
|
1170
|
+
it('closed "triaged" folds each edit note the capture lists, no other note', () => {
|
|
1171
|
+
expect(closed("triaged")).toContain("Fold each `edit` note the capture lists, no other note")
|
|
1172
|
+
})
|
|
1173
|
+
|
|
1174
|
+
it.each(["edits", "notes", "triaged"] as const)(
|
|
1175
|
+
"closed %s keeps the fence and must-write rule, drops both reply rules",
|
|
1176
|
+
(round) => {
|
|
1177
|
+
const prompt = closed(round, true)
|
|
1178
|
+
expect(prompt).toContain("`src/a.ts`") // gtd-path-exempt: fixture paths
|
|
1179
|
+
expect(prompt).toContain("MUST write")
|
|
1180
|
+
expect(prompt).not.toContain("A one-shot footnote needs no reply")
|
|
1181
|
+
expect(prompt).not.toContain("Code threads waiting on you")
|
|
1182
|
+
expect(prompt).not.toContain("src/a.ts:4: why?")
|
|
1183
|
+
},
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
it("the open variant and the feature prompt keep both reply rules", () => {
|
|
1187
|
+
const threads = [{ path: "src/a.ts", line: 4, waitingOn: "agent", first: "why?", faults: [] }]
|
|
1188
|
+
for (const prompt of renderText(
|
|
1189
|
+
() => [
|
|
1190
|
+
buildReviewCollectingPrompt("capture", { strays, round: "reply" }),
|
|
1191
|
+
buildReviewCollectingPrompt("capture"),
|
|
1192
|
+
],
|
|
1193
|
+
{ codeThreads: threads as never },
|
|
1194
|
+
)) {
|
|
1195
|
+
expect(prompt).toContain("A one-shot footnote needs no reply")
|
|
1196
|
+
expect(prompt).toContain("src/a.ts:4: why?")
|
|
1197
|
+
}
|
|
1198
|
+
})
|
|
1199
|
+
|
|
1200
|
+
it("end-of-round still says the file is closed and must not be recreated", () => {
|
|
1201
|
+
const prompt = renderText(() =>
|
|
1202
|
+
buildReviewCollectingPrompt("capture", { strays, round: "triaged" }),
|
|
1203
|
+
)
|
|
1204
|
+
expect(prompt).toContain("already closed")
|
|
1205
|
+
expect(prompt).toContain("do not\n recreate it")
|
|
1206
|
+
expect(prompt).not.toContain("exactly one `- A:` reply")
|
|
1207
|
+
})
|
|
1208
|
+
})
|
|
1209
|
+
|
|
1210
|
+
describe("the rules-taking fix prompts", () => {
|
|
1211
|
+
const strays = ["src/a.ts", "src/b.ts"]
|
|
1212
|
+
const pattern = "\\bGTD_FLAG_\\w+"
|
|
1213
|
+
const notes = [{ id: "n1", anchor: "src/a.ts#1-1", text: "Risk: x" }]
|
|
1214
|
+
const prompts: Record<string, (rules?: string) => string> = {
|
|
1215
|
+
fix: (rules) => buildFixPrompt(rules),
|
|
1216
|
+
quality: (rules) => buildFixQualityPrompt(rules),
|
|
1217
|
+
risks: (rules) => buildReviewFixRisksPrompt(notes, rules),
|
|
1218
|
+
nits: (rules) => buildReviewFixNitsPrompt(notes, rules),
|
|
1219
|
+
describe: (rules) => healthDescribePrompt(rules),
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
it.each(Object.keys(prompts))(
|
|
1223
|
+
"%s renders without the refactor block when no rules are given",
|
|
1224
|
+
(k) => {
|
|
1225
|
+
const [bare] = renderText(() => [prompts[k]!()])
|
|
1226
|
+
expect(bare).not.toContain("Change only these paths")
|
|
1227
|
+
expect(bare).not.toContain("never a test file")
|
|
1228
|
+
},
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
it.each(Object.keys(prompts))("%s lists the strays and both bans when rules are given", (k) => {
|
|
1232
|
+
const [rules, prompt] = renderText(() => {
|
|
1233
|
+
const r = refactorRules(strays, pattern)
|
|
1234
|
+
return [r, prompts[k]!(r)]
|
|
1235
|
+
})
|
|
1236
|
+
expect(prompt).toContain(rules)
|
|
1237
|
+
for (const stray of strays) expect(prompt).toContain(`\`${stray}\``)
|
|
1238
|
+
expect(prompt).toContain("never a test file")
|
|
1239
|
+
expect(prompt).toContain("no new flag read")
|
|
1240
|
+
})
|
|
1241
|
+
|
|
1242
|
+
it("the risks prompt drops its add-a-test line when rules are given", () => {
|
|
1243
|
+
const [plain, ruled] = renderText(() => [
|
|
1244
|
+
buildReviewFixRisksPrompt(notes),
|
|
1245
|
+
buildReviewFixRisksPrompt(notes, refactorRules(strays, pattern)),
|
|
1246
|
+
])
|
|
1247
|
+
expect(plain).toContain("add a test")
|
|
1248
|
+
expect(ruled).not.toContain("add a test")
|
|
1249
|
+
})
|
|
1250
|
+
})
|
|
1251
|
+
|
|
1252
|
+
describe("buildReviewReviewingPrompt lap wording", () => {
|
|
1253
|
+
const ranges = {
|
|
1254
|
+
first: "it covers everything since the base",
|
|
1255
|
+
feedback: "it covers only what is new since the previous review",
|
|
1256
|
+
refactor: "it covers the whole refactor since it started, on every round",
|
|
1257
|
+
} as const
|
|
1258
|
+
it.each(Object.keys(ranges) as (keyof typeof ranges)[])(
|
|
1259
|
+
"%s renders its own range wording and neither of the other two",
|
|
1260
|
+
(lap) => {
|
|
1261
|
+
const prompt = renderText(() => buildReviewReviewingPrompt("base", lap))
|
|
1262
|
+
for (const [other, wording] of Object.entries(ranges)) {
|
|
1263
|
+
if (other === lap) expect(prompt).toContain(wording)
|
|
1264
|
+
else expect(prompt).not.toContain(wording)
|
|
1265
|
+
}
|
|
1266
|
+
},
|
|
1267
|
+
)
|
|
1268
|
+
})
|