@pmelab/gtd 23.2.0 → 24.1.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 +44 -11
- package/claude/hooks/access.ts +1 -1
- package/dist/chunks/Schema-Dh-mkwkq.mjs +25353 -0
- package/dist/chunks/reserved-ejn-LVYO.mjs +1388 -0
- package/dist/chunks/rolldown-runtime-8H4AJuhK.mjs +14 -0
- package/dist/chunks/steering-D-PvRC5W.mjs +16296 -0
- package/dist/chunks/wire-DA7q2ikx.mjs +473 -0
- package/dist/flows.mjs +525 -0
- package/dist/gtd.bundle.mjs +43830 -56044
- package/dist/steering.mjs +2 -0
- package/dist/testkit.mjs +13006 -0
- package/dist/wire.mjs +2 -0
- package/dist/workflow-kit.mjs +352 -0
- package/dist/workflows/feature.mjs +315 -0
- package/dist/workflows/fix.mjs +40 -0
- package/dist/workflows/review.mjs +25 -0
- package/dist/workflows.mjs +1953 -1724
- package/package.json +70 -8
- package/skills/authoring/SKILL.md +5 -4
- package/src/cli/Cli.test.ts +1329 -0
- package/src/cli/Cli.ts +1348 -0
- package/src/cli/Program.test.ts +2262 -0
- package/src/cli/Program.ts +1331 -0
- package/src/cli/ToolCommands.ts +43 -0
- package/src/cli/index.ts +14 -0
- package/src/edge/Edge.test.ts +1016 -0
- package/src/edge/Edge.ts +1225 -0
- package/src/edge/index.ts +16 -0
- package/src/emit/Emit.test.ts +425 -0
- package/src/emit/Emit.ts +139 -0
- package/src/emit/GitScript.test.ts +260 -0
- package/src/emit/GitScript.ts +131 -0
- package/src/emit/ModeContradiction.test.ts +120 -0
- package/src/emit/ModeContradiction.ts +66 -0
- package/src/emit/OutcomeScript.test.ts +238 -0
- package/src/emit/OutcomeScript.ts +73 -0
- package/src/emit/SteeringMode.test.ts +274 -0
- package/src/emit/SteeringMode.ts +256 -0
- package/src/emit/index.ts +40 -0
- package/src/flows/factory.ts +5 -1
- package/src/flows/runtime.ts +3 -0
- package/src/install/ConfigScaffold.ts +19 -0
- package/src/install/Install.test.ts +426 -0
- package/src/install/Install.ts +634 -0
- package/src/install/Version.ts +27 -0
- package/src/install/index.ts +3 -0
- package/src/platform/CommandRunner.ts +74 -0
- package/src/platform/Commentary.test.ts +126 -0
- package/src/platform/Commentary.ts +92 -0
- package/src/platform/Git.test.ts +50 -0
- package/src/platform/Git.ts +552 -0
- package/src/platform/Host.ts +42 -0
- package/src/platform/RetainedHistory.test.ts +65 -0
- package/src/platform/RetainedHistory.ts +34 -0
- package/src/platform/Workspace.test.ts +669 -0
- package/src/platform/Workspace.ts +631 -0
- package/src/platform/WorktreeState.test.ts +162 -0
- package/src/platform/WorktreeState.ts +50 -0
- package/src/platform/index.ts +22 -0
- package/src/replay/Access.test.ts +53 -0
- package/src/replay/Access.ts +46 -0
- package/src/replay/Format.test.ts +20 -0
- package/src/replay/Format.ts +14 -0
- package/src/replay/Replay.test.ts +887 -0
- package/src/replay/Replay.ts +669 -0
- package/src/replay/Trailers.test.ts +117 -0
- package/src/replay/Trailers.ts +213 -0
- package/src/replay/Tree.test.ts +25 -0
- package/src/replay/Tree.ts +45 -0
- package/src/replay/index.ts +11 -0
- package/src/steering/Blocks.fixture.ts +6 -0
- package/src/steering/Blocks.test.ts +167 -0
- package/src/steering/Blocks.ts +321 -0
- package/src/steering/CodeThreads.test.ts +127 -0
- package/src/steering/CodeThreads.ts +197 -0
- package/src/steering/Edits.test.ts +19 -0
- package/src/steering/Edits.ts +20 -0
- package/src/steering/Eol.test.ts +16 -0
- package/src/steering/Eol.ts +2 -0
- package/src/steering/FORMATS.md +58 -0
- package/src/steering/Footnotes.test.ts +977 -0
- package/src/steering/Footnotes.ts +661 -0
- package/src/steering/Inline.fixture.ts +7 -0
- package/src/steering/Inline.test.ts +227 -0
- package/src/steering/Inline.ts +195 -0
- package/src/steering/MarkdownTree.test.ts +247 -0
- package/src/steering/MarkdownTree.ts +260 -0
- package/src/steering/Outline.ts +19 -0
- package/src/steering/SteeringFormat.test.ts +91 -0
- package/src/steering/SteeringFormat.ts +267 -0
- package/src/steering/SteeringFormats.test.ts +163 -0
- package/src/steering/Threads.ts +163 -0
- package/src/steering/contract.test.ts +264 -0
- package/src/steering/fixtures/code-threads/cache.stripped.ts.txt +6 -0
- package/src/steering/fixtures/code-threads/cache.ts.md +15 -0
- package/src/steering/fixtures/free-form/notes.md +12 -0
- package/src/steering/fixtures/free-form/notes.read.json +42 -0
- package/src/steering/fixtures/qa/answered.md +23 -0
- package/src/steering/fixtures/qa/answered.read.json +119 -0
- package/src/steering/fixtures/qa/requirements.md +160 -0
- package/src/steering/fixtures/qa/requirements.read.json +475 -0
- package/src/steering/fixtures/review/review.md +76 -0
- package/src/steering/fixtures/review/review.read.json +237 -0
- package/src/steering/fixtures/review/ticked.md +23 -0
- package/src/steering/fixtures/review/ticked.read.json +99 -0
- package/src/steering/fixtures/review-notes/baseline.md +14 -0
- package/src/steering/fixtures/review-notes/noted.md +19 -0
- package/src/steering/freeform.test.ts +311 -0
- package/src/steering/freeform.ts +148 -0
- package/src/steering/index.test.ts +62 -0
- package/src/steering/index.ts +140 -0
- package/src/steering/qa.test.ts +2160 -0
- package/src/steering/qa.ts +1104 -0
- package/src/steering/review.test.ts +1829 -0
- package/src/steering/review.ts +1180 -0
- package/src/step/LandStep.ts +31 -0
- package/src/step/RepoSnapshot.ts +29 -0
- package/src/step/accessRefusal.test.ts +58 -0
- package/src/step/accessRefusal.ts +26 -0
- package/src/step/index.ts +5 -0
- package/src/step/planStart.test.ts +202 -0
- package/src/step/planStart.ts +167 -0
- package/src/step/planStep.test.ts +104 -0
- package/src/step/planStep.ts +89 -0
- package/src/step/snapshot.fixture.ts +28 -0
- package/src/testkit/EmittedScriptRecognizer.test.ts +639 -0
- package/src/testkit/EmittedScriptRecognizer.ts +642 -0
- package/src/testkit/FakeGitOperations.ts +95 -0
- package/src/testkit/GitTiers.test.ts +11 -0
- package/src/testkit/GitTiers.ts +1052 -0
- package/src/testkit/InMemRepo.ts +467 -0
- package/src/testkit/Layers.test.ts +58 -0
- package/src/testkit/Layers.ts +241 -0
- package/src/testkit/StrictGitOperations.ts +31 -0
- package/src/testkit/TestRepo.test.ts +119 -0
- package/src/testkit/TestRepo.ts +274 -0
- package/src/testkit/cliIo.ts +36 -0
- package/src/testkit/index.ts +9 -0
- package/src/testkit/steps.ts +177 -0
- package/src/testkit/testkit.feature +65 -0
- package/src/wire/BeatDocument.test.ts +419 -0
- package/src/wire/BeatDocument.ts +114 -0
- package/src/wire/BeatPlain.test.ts +113 -0
- package/src/wire/BeatPlain.ts +127 -0
- package/src/wire/BeatStatus.test.ts +59 -0
- package/src/wire/BeatStatus.ts +81 -0
- package/src/wire/Demand.test.ts +142 -0
- package/src/wire/Demand.ts +108 -0
- package/src/wire/ExitCodes.test.ts +21 -0
- package/src/wire/ExitCodes.ts +16 -0
- package/src/wire/Glob.ts +29 -0
- package/src/wire/Judge.ts +22 -0
- package/src/wire/Land.test.ts +120 -0
- package/src/wire/Land.ts +75 -0
- package/src/wire/Select.test.ts +148 -0
- package/src/wire/Select.ts +78 -0
- package/src/wire/Sessions.test.ts +59 -0
- package/src/wire/Sessions.ts +39 -0
- package/src/wire/WireSchema.test.ts +162 -0
- package/src/wire/constants.ts +22 -0
- package/src/wire/golden/1/beat-capture.json +34 -0
- package/src/wire/golden/1/beat-message.json +34 -0
- package/src/wire/golden/1/beat-prompt.json +44 -0
- package/src/wire/golden/1/beat-script.json +34 -0
- package/src/wire/golden/1/beat-stalled.json +34 -0
- package/src/wire/golden/1/judge.json +14 -0
- package/src/wire/golden/1/land-noop.json +10 -0
- package/src/wire/golden/1/land.json +10 -0
- package/src/wire/index.ts +12 -0
- package/src/wire/types.ts +46 -0
- package/src/workflow/Diagnostic.test.ts +93 -0
- package/src/workflow/Diagnostic.ts +54 -0
- package/src/workflow/SteeringFormats.test.ts +27 -0
- package/src/workflow/SteeringFormats.ts +6 -0
- package/src/workflow/Workflow.ts +77 -0
- package/src/workflow/compile.test.ts +53 -0
- package/src/workflow/compile.ts +56 -0
- package/src/workflow/discovery.ts +122 -0
- package/src/workflow/fingerprint.test.ts +52 -0
- package/src/workflow/fingerprint.ts +53 -0
- package/src/workflow/index.ts +33 -0
- package/src/workflow/load.test.ts +1017 -0
- package/src/workflow/load.ts +836 -0
- package/src/workflow/skills.test.ts +109 -0
- package/src/workflow/skills.ts +90 -0
- package/src/workflow/vars.ts +33 -0
- package/src/workflow-kit/health.test.ts +30 -0
- package/src/workflow-kit/health.ts +86 -0
- package/src/workflow-kit/index.ts +7 -0
- package/src/workflow-kit/quality.test.ts +17 -0
- package/src/workflow-kit/quality.ts +45 -0
- package/src/workflow-kit/review.ts +324 -0
- package/src/{workflows → workflow-kit}/scenarios.ts +21 -28
- package/src/workflows/feature/feature.test.ts +32 -0
- package/src/workflows/{bundled.ts → feature/feature.ts} +11 -63
- package/src/workflows/feature/index.ts +10 -0
- package/src/workflows/{packages.test.ts → feature/packages.test.ts} +2 -2
- package/src/workflows/{packages.ts → feature/packages.ts} +3 -3
- package/src/workflows/{planning.ts → feature/planning.ts} +2 -3
- package/src/workflows/{review.test.ts → feature/review.test.ts} +9 -44
- package/src/workflows/feature/review.ts +106 -0
- package/src/workflows/feature/steps.test.ts +82 -0
- package/src/workflows/feature/steps.ts +92 -0
- package/src/workflows/fix/fix.feature +80 -0
- package/src/workflows/fix/fix.test.ts +74 -0
- package/src/workflows/fix/fix.ts +52 -0
- package/src/workflows/fix/index.ts +6 -0
- package/src/workflows/index.ts +36 -2
- package/src/workflows/review/index.ts +6 -0
- package/src/workflows/review/review.test.ts +99 -0
- package/src/workflows/review/review.ts +33 -0
- package/src/workflows/{access.test.ts → shared/access.test.ts} +1 -1
- package/src/workflows/{access.ts → shared/access.ts} +3 -5
- package/src/workflows/shared/config.fixture.ts +12 -0
- package/src/workflows/{health.test.ts → shared/health.test.ts} +1 -1
- package/src/workflows/{health.ts → shared/health.ts} +38 -55
- package/src/workflows/{options.test.ts → shared/options.test.ts} +2 -2
- package/src/workflows/{options.ts → shared/options.ts} +14 -2
- package/src/workflows/shared/reserved.ts +11 -0
- package/src/workflows/{scenarios.test.ts → shared/scenarios.test.ts} +1 -1
- package/src/workflows/shared/scenarios.ts +35 -0
- package/src/workflows/{skills.test.ts → shared/skills.test.ts} +3 -1
- package/src/workflows/{skills.ts → shared/skills.ts} +7 -6
- package/src/workflows/{steps.test.ts → shared/steps.test.ts} +1 -66
- package/src/workflows/{steps.ts → shared/steps.ts} +8 -98
- package/src/workflows/shared/tail.test.ts +205 -0
- package/src/workflows/shared/tail.ts +135 -0
- package/src/workflows/{text.fixture.ts → shared/text.fixture.ts} +2 -2
- package/src/workflows/{text.ts → shared/text.ts} +48 -7
- package/src/workflows/{threads.test.ts → shared/threads.test.ts} +1 -1
- package/src/workflows/shared/workflows.steps.ts +7 -0
- package/src/workflows/review.ts +0 -362
- /package/src/workflows/{prose.ts → shared/prose.ts} +0 -0
- /package/src/workflows/{text.test.ts → shared/text.test.ts} +0 -0
- /package/src/workflows/{vars.ts → shared/vars.ts} +0 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import {
|
|
2
|
+
changes,
|
|
3
|
+
changesSince,
|
|
4
|
+
hasThreadFor,
|
|
5
|
+
head,
|
|
6
|
+
read,
|
|
7
|
+
refuse,
|
|
8
|
+
requireReplies,
|
|
9
|
+
requireThreadsClosed,
|
|
10
|
+
type Change,
|
|
11
|
+
} from "../flows/index.js"
|
|
12
|
+
import { reviewNotes, reviewRisks, stripCodeThreads, type ReviewNote } from "../steering/index.js"
|
|
13
|
+
|
|
14
|
+
/** How a review round ended. `feedback` carries what the human edited, for the re-unwind to undo. */
|
|
15
|
+
export type ReviewOutcome =
|
|
16
|
+
| { readonly verdict: "signoff" }
|
|
17
|
+
| {
|
|
18
|
+
readonly verdict: "feedback"
|
|
19
|
+
/** The last round's commit, the re-plan base. */
|
|
20
|
+
readonly base: string
|
|
21
|
+
/** The commit the review file was written at; edits are restored from it. */
|
|
22
|
+
readonly restoreFrom: string
|
|
23
|
+
/** The code paths the rounds changed, with their content before. */
|
|
24
|
+
readonly edited: readonly Change[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type Verdict = "edit" | "question" | "nit" | "praise"
|
|
28
|
+
|
|
29
|
+
export type ReviewRisk = ReturnType<typeof reviewRisks>[number]
|
|
30
|
+
|
|
31
|
+
/** Everything a review round decides or says, passed in: its files, steps, judge and texts. */
|
|
32
|
+
export interface ReviewPolicy {
|
|
33
|
+
/** The review steering file the reviewer writes and the human annotates. */
|
|
34
|
+
readonly file: string
|
|
35
|
+
/** The file a collecting turn folds feedback into; its presence means feedback. */
|
|
36
|
+
readonly feedback: string
|
|
37
|
+
/** Whether a path is code, as opposed to steering state. */
|
|
38
|
+
readonly isCode: (path: string) => boolean
|
|
39
|
+
/** Write `file` over everything since `base`, carrying the answers of commit `carry` when given. */
|
|
40
|
+
readonly reviewing: (base: string, carry?: string) => Promise<void>
|
|
41
|
+
readonly fixRisks: (risks: readonly ReviewRisk[]) => Promise<void>
|
|
42
|
+
readonly keepGreen: () => Promise<void>
|
|
43
|
+
readonly awaitReview: (base: string) => Promise<void>
|
|
44
|
+
/** The round at commit `round` left no `file`. */
|
|
45
|
+
readonly reviewMissing: (round: string) => Promise<void>
|
|
46
|
+
/** Turn the round `capture` describes into `feedback`, or change nothing. */
|
|
47
|
+
readonly collecting: (capture: string) => Promise<void>
|
|
48
|
+
/** One verdict per note, keyed by note id; a note without one is an edit. */
|
|
49
|
+
readonly triage: (notes: readonly ReviewNote[]) => Promise<ReadonlyMap<string, Verdict>>
|
|
50
|
+
readonly answerQuestions: (notes: readonly ReviewNote[]) => Promise<void>
|
|
51
|
+
readonly fixNits: (notes: readonly ReviewNote[]) => Promise<void>
|
|
52
|
+
/** Remove `file` at the end of a round at commit `round`. */
|
|
53
|
+
readonly close: (round: string) => Promise<void>
|
|
54
|
+
/** What a collecting turn reads. */
|
|
55
|
+
readonly captures: {
|
|
56
|
+
readonly edits: (round: string) => string
|
|
57
|
+
readonly notes: (round: string) => string
|
|
58
|
+
readonly editNotes: (
|
|
59
|
+
round: string,
|
|
60
|
+
edits: readonly ReviewNote[],
|
|
61
|
+
answeredAt: string | undefined,
|
|
62
|
+
) => string
|
|
63
|
+
}
|
|
64
|
+
readonly refusals: {
|
|
65
|
+
/** `file` was deleted at the gate. */
|
|
66
|
+
readonly deleted: string
|
|
67
|
+
/** The collecting turn changed something without writing `feedback`. */
|
|
68
|
+
readonly unmatched: string
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const collect = async (p: ReviewPolicy, capture: string): Promise<"signoff" | "feedback"> => {
|
|
73
|
+
await p.collecting(capture)
|
|
74
|
+
if (read(p.feedback) !== undefined) return "feedback"
|
|
75
|
+
if (changes().length === 0) return "signoff"
|
|
76
|
+
return refuse(p.refusals.unmatched)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A review with ticks, footnote markers and thread definitions removed: what is left is line notes. */
|
|
80
|
+
const notesOf = (text: string): string =>
|
|
81
|
+
text
|
|
82
|
+
.replace(/^\[\^[^\]]+\]:[^\n]*(\n(?: {2,}|\t)[^\n]*|\n(?=\s*\n(?: {2,}|\t)))*/gm, "")
|
|
83
|
+
.replace(/\[\^[^\]]+\]/g, "")
|
|
84
|
+
.replace(/\[x\]/gi, "[ ]")
|
|
85
|
+
.replace(/\s+/g, " ")
|
|
86
|
+
.trim()
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The human gate and its reply rounds: a question gets an answer inside
|
|
90
|
+
* the review file and rests at the gate again, without a lap. Resolves the
|
|
91
|
+
* commit of the last collecting turn, `"missing"` when the round left no
|
|
92
|
+
* review, or `undefined` when no such turn ran.
|
|
93
|
+
*/
|
|
94
|
+
const converse = async (
|
|
95
|
+
p: ReviewPolicy,
|
|
96
|
+
base: string,
|
|
97
|
+
collectedBefore?: string,
|
|
98
|
+
): Promise<string | undefined> => {
|
|
99
|
+
let collectedAt = collectedBefore
|
|
100
|
+
for (;;) {
|
|
101
|
+
await p.awaitReview(base)
|
|
102
|
+
if (changes(p.file).some((c) => c.status === "deleted")) refuse(p.refusals.deleted)
|
|
103
|
+
if (read(p.file) === undefined) {
|
|
104
|
+
await p.reviewMissing(head())
|
|
105
|
+
return "missing"
|
|
106
|
+
}
|
|
107
|
+
requireThreadsClosed(p.file)
|
|
108
|
+
if (!hasThreadFor("agent", p.file)) return collectedAt
|
|
109
|
+
await p.collecting(p.captures.edits(head()))
|
|
110
|
+
collectedAt = head()
|
|
111
|
+
requireReplies(p.file)
|
|
112
|
+
if (!hasThreadFor("human", p.file)) return collectedAt
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** What `finish` hands back to `review`: an outcome, or another lap at the gate. */
|
|
117
|
+
type Finish =
|
|
118
|
+
| ReviewOutcome
|
|
119
|
+
| { readonly next: "await"; readonly reviewed: string }
|
|
120
|
+
| { readonly next: "rereview"; readonly carry: string | undefined }
|
|
121
|
+
|
|
122
|
+
/** What a round's routing needs from `finish`: its closing step and the ways it ends. */
|
|
123
|
+
interface Round {
|
|
124
|
+
readonly round: string
|
|
125
|
+
readonly close: () => Promise<void>
|
|
126
|
+
readonly outcome: (verdict: "signoff" | "feedback") => ReviewOutcome
|
|
127
|
+
readonly unfolded: () => Promise<Finish>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Judge every note, then run each verdict's route: answers, then nit fixes, then the edits' lap. */
|
|
131
|
+
const routeNotes = async (
|
|
132
|
+
p: ReviewPolicy,
|
|
133
|
+
notes: readonly ReviewNote[],
|
|
134
|
+
r: Round,
|
|
135
|
+
): Promise<Finish> => {
|
|
136
|
+
const verdicts = await p.triage(notes)
|
|
137
|
+
const of = (v: Verdict): ReviewNote[] => notes.filter((n) => (verdicts.get(n.id) ?? "edit") === v)
|
|
138
|
+
const [edits, questions, nits] = [of("edit"), of("question"), of("nit")]
|
|
139
|
+
if (edits.length + questions.length + nits.length === 0) return r.unfolded()
|
|
140
|
+
|
|
141
|
+
let answeredAt: string | undefined
|
|
142
|
+
if (questions.length > 0) {
|
|
143
|
+
await p.answerQuestions(questions)
|
|
144
|
+
answeredAt = head()
|
|
145
|
+
}
|
|
146
|
+
if (nits.length > 0) {
|
|
147
|
+
await p.fixNits(nits)
|
|
148
|
+
await p.keepGreen()
|
|
149
|
+
}
|
|
150
|
+
if (edits.length > 0) {
|
|
151
|
+
await r.close()
|
|
152
|
+
return r.outcome(await collect(p, p.captures.editNotes(r.round, edits, answeredAt)))
|
|
153
|
+
}
|
|
154
|
+
if (nits.length > 0) {
|
|
155
|
+
await r.close()
|
|
156
|
+
return { next: "rereview", carry: answeredAt }
|
|
157
|
+
}
|
|
158
|
+
return { next: "await", reviewed: answeredAt! }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A code edit, not a thread-only change: lines that only add, answer or remove code threads don't count. */
|
|
162
|
+
const isCodeEdit = (p: ReviewPolicy, c: Change): boolean =>
|
|
163
|
+
p.isCode(c.path) &&
|
|
164
|
+
stripCodeThreads(c.path, c.before ?? "") !== stripCodeThreads(c.path, c.after ?? "")
|
|
165
|
+
|
|
166
|
+
/** Whether anything but thread edits and ticks landed in the review file since `commit`. */
|
|
167
|
+
const notedSince = (p: ReviewPolicy, commit: string): boolean =>
|
|
168
|
+
changesSince(commit).some(
|
|
169
|
+
(c) => c.path === p.file && notesOf(c.before ?? "") !== notesOf(read(p.file) ?? ""),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* What the in-loop collecting turn left: `folded` when it wrote `feedback`
|
|
174
|
+
* (those folds need their lap whatever is judged later), `settled` when only
|
|
175
|
+
* thread edits and ticks landed after it.
|
|
176
|
+
*/
|
|
177
|
+
const afterCollect = (
|
|
178
|
+
p: ReviewPolicy,
|
|
179
|
+
collectedAt: string | undefined,
|
|
180
|
+
): { folded: boolean; settled: boolean } =>
|
|
181
|
+
// While the collecting turn is still pending its commit has no hash, so no
|
|
182
|
+
// range can start there; the flow rests at the close before it matters.
|
|
183
|
+
collectedAt === "" ? { folded: false, settled: false } : settledAfter(p, collectedAt)
|
|
184
|
+
|
|
185
|
+
const settledAfter = (
|
|
186
|
+
p: ReviewPolicy,
|
|
187
|
+
collectedAt: string | undefined,
|
|
188
|
+
): { folded: boolean; settled: boolean } => ({
|
|
189
|
+
folded: collectedAt !== undefined && read(p.feedback) !== undefined,
|
|
190
|
+
settled:
|
|
191
|
+
collectedAt !== undefined &&
|
|
192
|
+
changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) &&
|
|
193
|
+
!notedSince(p, collectedAt),
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const finish = async (
|
|
197
|
+
p: ReviewPolicy,
|
|
198
|
+
reviewed: string,
|
|
199
|
+
collectedAt: string | undefined,
|
|
200
|
+
): Promise<Finish> => {
|
|
201
|
+
// Everything the human did is read before any agent turn runs, so a nit fix
|
|
202
|
+
// is never counted as a hand-edit.
|
|
203
|
+
const round = head()
|
|
204
|
+
const since = changesSince(reviewed)
|
|
205
|
+
const edited = since.filter((c) => isCodeEdit(p, c))
|
|
206
|
+
const baselineText = since.get(p.file)?.before ?? ""
|
|
207
|
+
const record = read(p.file) ?? ""
|
|
208
|
+
const { folded, settled } = afterCollect(p, collectedAt)
|
|
209
|
+
// The review gate clears every tick before its commit, so a changed
|
|
210
|
+
// review file is a note, never a tick.
|
|
211
|
+
const noted = since.some((c) => c.path === p.file)
|
|
212
|
+
const close = (): Promise<void> => p.close(round)
|
|
213
|
+
const outcome = (verdict: "signoff" | "feedback"): ReviewOutcome =>
|
|
214
|
+
verdict === "signoff" ? { verdict } : { verdict, base: round, restoreFrom: reviewed, edited }
|
|
215
|
+
const unfolded = async (): Promise<Finish> => {
|
|
216
|
+
await close()
|
|
217
|
+
return outcome(folded ? "feedback" : "signoff")
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const collectWith = async (capture: string): Promise<Finish> => {
|
|
221
|
+
await close()
|
|
222
|
+
return outcome(await collect(p, capture))
|
|
223
|
+
}
|
|
224
|
+
if (settled || (edited.length === 0 && !noted)) return unfolded()
|
|
225
|
+
if (edited.length > 0) return collectWith(p.captures.edits(round))
|
|
226
|
+
const notes = reviewNotes(baselineText, record)
|
|
227
|
+
if (notes.length === 0) return collectWith(p.captures.notes(round))
|
|
228
|
+
return routeNotes(p, notes, { round, close, outcome, unfolded })
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Write the review; if it marks risks, fix them, keep green, and write it again — once, so the re-review's own risks reach the human unfixed. */
|
|
232
|
+
const reviewOnce = async (
|
|
233
|
+
p: Pick<ReviewPolicy, "file" | "reviewing" | "fixRisks" | "keepGreen">,
|
|
234
|
+
base: string,
|
|
235
|
+
carry: string | undefined,
|
|
236
|
+
) => {
|
|
237
|
+
await p.reviewing(base, carry)
|
|
238
|
+
const risks = reviewRisks(read(p.file) ?? "")
|
|
239
|
+
if (risks.length === 0) return
|
|
240
|
+
await p.fixRisks(risks)
|
|
241
|
+
await p.keepGreen()
|
|
242
|
+
await p.reviewing(base, carry)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* A reviewer writes the review over everything since `base`, a human
|
|
247
|
+
* reviews and signs off or comments, and each note is judged: edits go to
|
|
248
|
+
* another lap, questions are answered in place, nits fixed in one batch,
|
|
249
|
+
* praise dropped.
|
|
250
|
+
*/
|
|
251
|
+
export const review = async (base: string, p: ReviewPolicy): Promise<ReviewOutcome> => {
|
|
252
|
+
let carry: string | undefined
|
|
253
|
+
for (;;) {
|
|
254
|
+
await reviewOnce(p, base, carry)
|
|
255
|
+
carry = undefined
|
|
256
|
+
let reviewed = head()
|
|
257
|
+
let collectedAt: string | undefined
|
|
258
|
+
for (;;) {
|
|
259
|
+
collectedAt = await converse(p, base, collectedAt)
|
|
260
|
+
if (collectedAt === "missing") break
|
|
261
|
+
const result = await finish(p, reviewed, collectedAt)
|
|
262
|
+
if (!("next" in result)) return result
|
|
263
|
+
if (result.next === "rereview") {
|
|
264
|
+
carry = result.carry
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
reviewed = result.reviewed
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** What a review round that hands its notes on decides or says, passed in. */
|
|
273
|
+
export interface HandOffPolicy extends Pick<
|
|
274
|
+
ReviewPolicy,
|
|
275
|
+
"file" | "reviewing" | "fixRisks" | "keepGreen" | "awaitReview" | "reviewMissing" | "close"
|
|
276
|
+
> {
|
|
277
|
+
/** Keep `file` with the human's notes for another workflow; `reviewed` holds the reviewer's own version. */
|
|
278
|
+
readonly handOff: (reviewed: string) => Promise<void>
|
|
279
|
+
/** `file` was deleted at the gate. */
|
|
280
|
+
readonly deleted: string
|
|
281
|
+
/** A thread waits on the agent at the gate, and no turn here answers it. */
|
|
282
|
+
readonly threadsPending: string
|
|
283
|
+
/** The review was edited but the edit parses to no note, so `fix` would have nothing to act on. */
|
|
284
|
+
readonly unparsed: string
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* A reviewer writes the review over everything since `base` and a human
|
|
289
|
+
* reviews it. No note signs off and closes; any note ends the round with the
|
|
290
|
+
* review left in place, for another workflow to read its notes — nothing is
|
|
291
|
+
* judged or re-planned here.
|
|
292
|
+
*/
|
|
293
|
+
export const reviewToHandOff = async (base: string, p: HandOffPolicy): Promise<void> => {
|
|
294
|
+
for (;;) {
|
|
295
|
+
await reviewOnce(p, base, undefined)
|
|
296
|
+
const reviewed = head()
|
|
297
|
+
await p.awaitReview(base)
|
|
298
|
+
if (changes(p.file).some((c) => c.status === "deleted")) refuse(p.deleted)
|
|
299
|
+
const record = read(p.file)
|
|
300
|
+
if (record === undefined) {
|
|
301
|
+
await p.reviewMissing(head())
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
// `fix` runs in a new process, where a code thread is no longer "changed"
|
|
305
|
+
// and nothing answers it — so none may be left waiting on the agent.
|
|
306
|
+
requireThreadsClosed(p.file)
|
|
307
|
+
if (hasThreadFor("agent", p.file)) refuse(p.threadsPending)
|
|
308
|
+
const since = changesSince(reviewed)
|
|
309
|
+
const before = since.get(p.file)?.before ?? record
|
|
310
|
+
if (notesOf(before) === notesOf(record)) {
|
|
311
|
+
// A hand edit in a sign-off round was never checked or reviewed.
|
|
312
|
+
if (since.paths.some((f) => !f.startsWith(".gtd/"))) {
|
|
313
|
+
await p.keepGreen()
|
|
314
|
+
}
|
|
315
|
+
await p.close(head())
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
// `fix` acts only on parsed notes: an edit with none (a deleted chunk,
|
|
319
|
+
// stray prose) would be closed away unread, so it is refused here.
|
|
320
|
+
if (reviewNotes(before, record).length === 0) refuse(p.unparsed)
|
|
321
|
+
await p.handOff(reviewed)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { head,
|
|
2
|
-
import * as t from "./text.js"
|
|
1
|
+
import { head, read } from "../flows/index.js"
|
|
3
2
|
|
|
4
3
|
/** Mutable: an accepted change moves the snapshot forward. */
|
|
5
4
|
export interface FrozenScenarios {
|
|
@@ -93,26 +92,33 @@ const lineDiff = (a: readonly string[], b: readonly string[]): string[] => {
|
|
|
93
92
|
return [...out, ...a.slice(i).map((l) => `- ${l}`), ...b.slice(j).map((l) => `+ ${l}`)]
|
|
94
93
|
}
|
|
95
94
|
|
|
96
|
-
export
|
|
95
|
+
export interface WordingDrift {
|
|
96
|
+
readonly path: string
|
|
97
|
+
/** `- `/`+ `-prefixed, in file order. */
|
|
98
|
+
readonly lines: readonly string[]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const wordingDrift = (frozen: FrozenScenarios, path: string): WordingDrift => ({
|
|
97
102
|
path,
|
|
98
103
|
lines: lineDiff(lines(frozen.texts[path]), lines(textOf(path))),
|
|
99
104
|
})
|
|
100
105
|
|
|
101
|
-
/**
|
|
102
|
-
|
|
106
|
+
/**
|
|
107
|
+
* After a turn: rest at `stop` while wording drifted from `frozen`. Whatever
|
|
108
|
+
* still differs after it is accepted and moves the snapshot; what was
|
|
109
|
+
* restored stays frozen as it was.
|
|
110
|
+
*/
|
|
111
|
+
export const holdWording = async (
|
|
112
|
+
frozen: FrozenScenarios | undefined,
|
|
113
|
+
stop: (at: string, drifted: readonly WordingDrift[]) => Promise<void>,
|
|
114
|
+
): Promise<void> => {
|
|
103
115
|
if (frozen === undefined) return
|
|
104
116
|
const drifted = driftedScenarios(frozen)
|
|
105
117
|
if (drifted.length === 0) return
|
|
106
|
-
await
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
),
|
|
111
|
-
label: "Scenario wording changed",
|
|
112
|
-
acceptClean: true,
|
|
113
|
-
base: frozen.at,
|
|
114
|
-
})
|
|
115
|
-
// Still differing is accepted; restored is rejected and stays frozen as it was.
|
|
118
|
+
await stop(
|
|
119
|
+
frozen.at,
|
|
120
|
+
drifted.map((path) => wordingDrift(frozen, path)),
|
|
121
|
+
)
|
|
116
122
|
const accepted = driftedScenarios(frozen)
|
|
117
123
|
if (accepted.length === 0) return
|
|
118
124
|
for (const path of accepted) {
|
|
@@ -122,16 +128,3 @@ export const holdWording = async (frozen: FrozenScenarios | undefined): Promise<
|
|
|
122
128
|
}
|
|
123
129
|
frozen.at = head()
|
|
124
130
|
}
|
|
125
|
-
|
|
126
|
-
/** `turn`, then `holdWording`; `within` names the scope the gate rests in. */
|
|
127
|
-
export const guarded =
|
|
128
|
-
(
|
|
129
|
-
frozen: FrozenScenarios | undefined,
|
|
130
|
-
turn: () => Promise<void>,
|
|
131
|
-
within?: string,
|
|
132
|
-
): (() => Promise<void>) =>
|
|
133
|
-
async () => {
|
|
134
|
-
await turn()
|
|
135
|
-
if (within === undefined) await holdWording(frozen)
|
|
136
|
-
else await scope(within, () => holdWording(frozen))
|
|
137
|
-
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { testRepo } from "../../testkit/index.js"
|
|
3
|
+
import { CONFIG_SCAFFOLD } from "../../install/index.js"
|
|
4
|
+
import { reviewDoc } from "../shared/config.fixture.js"
|
|
5
|
+
|
|
6
|
+
describe("an ordinary feature start", () => {
|
|
7
|
+
it("unwinds a captured TODO, checks the baseline and starts design from it", async () => {
|
|
8
|
+
const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_QUALITYREVIEWS: "" } })
|
|
9
|
+
repo.write("TODO.md", "# Add numbers\n\nadd(a, b) returns the sum.\n")
|
|
10
|
+
const rest = await repo.drive({ unwind: () => undefined, "start-gate.check": () => undefined })
|
|
11
|
+
expect(rest).toMatchObject({
|
|
12
|
+
state: "design.triage",
|
|
13
|
+
actor: "agent",
|
|
14
|
+
file: ".gtd/REQUIREMENTS.md",
|
|
15
|
+
})
|
|
16
|
+
expect(repo.subjects().slice(1)).toEqual([
|
|
17
|
+
"gtd(human): idle → unwind",
|
|
18
|
+
"gtd(check): unwind → start-gate.check",
|
|
19
|
+
"gtd(check): start-gate.check → design.triage",
|
|
20
|
+
])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("refuses a pending hand-off right after idle, before unwinding", async () => {
|
|
24
|
+
const repo = testRepo({
|
|
25
|
+
workflow: CONFIG_SCAFFOLD,
|
|
26
|
+
files: { ".gtd/REVIEW.md": reviewDoc("note"), ".gtd/REVIEW.orig.md": reviewDoc("orig") },
|
|
27
|
+
})
|
|
28
|
+
repo.write("TODO.md", "# Add numbers\n")
|
|
29
|
+
await expect(repo.land()).rejects.toThrow(".gtd/REVIEW.orig.md is a pending hand-off")
|
|
30
|
+
expect(repo.subjects()).toEqual(["chore: initial commit"])
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -7,43 +7,22 @@ import {
|
|
|
7
7
|
revertScript,
|
|
8
8
|
run,
|
|
9
9
|
start,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "../
|
|
14
|
-
import {
|
|
10
|
+
} from "../../flows/index.js"
|
|
11
|
+
import { gate } from "../shared/health.js"
|
|
12
|
+
import { shipped } from "../shared/options.js"
|
|
13
|
+
import type { FrozenScenarios } from "../shared/scenarios.js"
|
|
14
|
+
import { FEEDBACK, REVIEW } from "../shared/steps.js"
|
|
15
|
+
import { refusePendingHandOff } from "../shared/tail.js"
|
|
16
|
+
import * as t from "../shared/text.js"
|
|
15
17
|
import { packages } from "./packages.js"
|
|
16
18
|
import { architecture, design } from "./planning.js"
|
|
17
|
-
import type { FrozenScenarios } from "./scenarios.js"
|
|
18
19
|
import { buildTail, type ReviewOutcome } from "./review.js"
|
|
19
|
-
import { ARCHITECTURE, FEEDBACK, REQUIREMENTS, REVIEW } from "./steps.js"
|
|
20
|
-
import { shipped } from "./options.js"
|
|
21
|
-
import * as t from "./text.js"
|
|
22
20
|
|
|
23
|
-
//
|
|
24
|
-
// `
|
|
21
|
+
// The ordinary start. Any change to the tree starts a process: `idle` →
|
|
22
|
+
// `unwind` reverts the sketch (its intent survives in history) → a
|
|
25
23
|
// green-baseline gate → design, architecture and one package per concern →
|
|
26
24
|
// the quality lap → human review, which signs off (the episode ends back at
|
|
27
|
-
// `idle`) or sends a full re-plan lap.
|
|
28
|
-
// `gtd --workflow fix` and `gtd --workflow review --var reviewBase=<commitish>`
|
|
29
|
-
// enter the same build tail further in. Each of the three is a factory over
|
|
30
|
-
// the settings in `./vars.ts`; its flow is exported too, for composing.
|
|
31
|
-
//
|
|
32
|
-
// Every part is exported for other workflows to compose; see the modules
|
|
33
|
-
// re-exported below.
|
|
34
|
-
|
|
35
|
-
export { threads, type ThreadInfo } from "../flows/index.js"
|
|
36
|
-
export { defaults, envDefaults } from "./vars.js"
|
|
37
|
-
export { ShippedOptions } from "./options.js"
|
|
38
|
-
export { skills } from "./skills.js"
|
|
39
|
-
export { access } from "./access.js"
|
|
40
|
-
export { agentWithSkills } from "./text.js"
|
|
41
|
-
export * from "./steps.js"
|
|
42
|
-
export * from "./health.js"
|
|
43
|
-
export * from "./planning.js"
|
|
44
|
-
export * from "./packages.js"
|
|
45
|
-
export * from "./scenarios.js"
|
|
46
|
-
export * from "./review.js"
|
|
25
|
+
// `idle`) or sends a full re-plan lap.
|
|
47
26
|
|
|
48
27
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
49
28
|
export const unwind = (): Promise<void> => {
|
|
@@ -97,6 +76,7 @@ export const afterTail = async (outcome: ReviewOutcome): Promise<void> => {
|
|
|
97
76
|
/** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
|
|
98
77
|
export const ordinaryStart = async (): Promise<void> => {
|
|
99
78
|
await human("idle", { message: t.idleMessage(), label: "Idle", file: ".gtd/TODO.md" })
|
|
79
|
+
refusePendingHandOff()
|
|
100
80
|
await unwind()
|
|
101
81
|
if (changes(FEEDBACK).some((c) => c.status !== "deleted")) {
|
|
102
82
|
await human("unwind-failed", {
|
|
@@ -109,36 +89,4 @@ export const ordinaryStart = async (): Promise<void> => {
|
|
|
109
89
|
await planAndBuild(start())
|
|
110
90
|
}
|
|
111
91
|
|
|
112
|
-
/** Repair a red baseline through the build tail, as its own reviewed commit. */
|
|
113
|
-
export const fixFlow = async (): Promise<void> => {
|
|
114
|
-
if (await baseline("fix-precheck")) return
|
|
115
|
-
return afterTail(await buildTail(true, start()))
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Pure review of everything since `reviewBase`. */
|
|
119
|
-
export const reviewFlow = async (): Promise<void> => {
|
|
120
|
-
await gate("review-gate", t.reviewGateBlockedMessage())
|
|
121
|
-
return afterTail(await buildTail(false, start()))
|
|
122
|
-
}
|
|
123
|
-
|
|
124
92
|
export const feature = shipped(ordinaryStart)
|
|
125
|
-
export const fix = shipped(fixFlow)
|
|
126
|
-
export const review = shipped(reviewFlow)
|
|
127
|
-
|
|
128
|
-
export default feature
|
|
129
|
-
|
|
130
|
-
export const summary: Summary = t.summaryPrompt
|
|
131
|
-
|
|
132
|
-
export const steering = { [REQUIREMENTS]: "qa", [ARCHITECTURE]: "qa", [REVIEW]: "review" }
|
|
133
|
-
|
|
134
|
-
export const base: WorkflowBase = (workflow, vars) =>
|
|
135
|
-
workflow === "review" ? (vars.reviewBase ?? "") : undefined
|
|
136
|
-
|
|
137
|
-
export const doors: Doors = {
|
|
138
|
-
fix: { workflow: "fix" },
|
|
139
|
-
review: {
|
|
140
|
-
workflow: "review",
|
|
141
|
-
args: [{ name: "base", optional: true }],
|
|
142
|
-
vars: ({ base }) => (base === undefined ? {} : { reviewBase: base }),
|
|
143
|
-
},
|
|
144
|
-
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { feature } from "./feature.js"
|
|
2
|
+
|
|
3
|
+
export * from "./steps.js"
|
|
4
|
+
export * from "./planning.js"
|
|
5
|
+
export * from "./packages.js"
|
|
6
|
+
export { buildTail, type ReviewOutcome } from "./review.js"
|
|
7
|
+
export * from "./feature.js"
|
|
8
|
+
export * from "../shared/reserved.js"
|
|
9
|
+
|
|
10
|
+
export default feature
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "vitest"
|
|
2
|
-
import { installContext, type Change, type StepRequest } from "
|
|
2
|
+
import { installContext, type Change, type StepRequest } from "../../flows/index.js"
|
|
3
3
|
import { declaredTests, packages, SCENARIO_PACKAGE } from "./packages.js"
|
|
4
|
-
import { fixtureContext } from "
|
|
4
|
+
import { fixtureContext } from "../shared/text.fixture.js"
|
|
5
5
|
|
|
6
6
|
afterEach(() => installContext(undefined))
|
|
7
7
|
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
removeScript,
|
|
9
9
|
run,
|
|
10
10
|
scope,
|
|
11
|
-
} from "
|
|
12
|
-
import { healthy } from "
|
|
13
|
-
import { freezeScenarios, guarded, type FrozenScenarios } from "
|
|
11
|
+
} from "../../flows/index.js"
|
|
12
|
+
import { healthy } from "../shared/health.js"
|
|
13
|
+
import { freezeScenarios, guarded, type FrozenScenarios } from "../shared/scenarios.js"
|
|
14
14
|
import { build, fixSuite } from "./steps.js"
|
|
15
15
|
|
|
16
16
|
/** Package 0: the e2e scenarios, written red. */
|
|
@@ -7,14 +7,13 @@ import {
|
|
|
7
7
|
requireThreadsClosed,
|
|
8
8
|
requireProgress,
|
|
9
9
|
scope,
|
|
10
|
-
} from "
|
|
10
|
+
} from "../../flows/index.js"
|
|
11
|
+
import { ARCHITECTURE, REQUIREMENTS } from "../shared/steps.js"
|
|
11
12
|
import {
|
|
12
13
|
answerProductQuestions,
|
|
13
14
|
answerTechnicalQuestions,
|
|
14
|
-
ARCHITECTURE,
|
|
15
15
|
author,
|
|
16
16
|
decompose,
|
|
17
|
-
REQUIREMENTS,
|
|
18
17
|
triage,
|
|
19
18
|
} from "./steps.js"
|
|
20
19
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "vitest"
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import {
|
|
3
|
+
installContext,
|
|
4
|
+
type Change,
|
|
5
|
+
type JudgeAnswer,
|
|
6
|
+
type StepRequest,
|
|
7
|
+
} from "../../flows/index.js"
|
|
8
|
+
import { review, type ReviewOutcome } from "./review.js"
|
|
9
|
+
import * as bundled from "../index.js"
|
|
10
|
+
import { fixtureContext } from "../shared/text.fixture.js"
|
|
6
11
|
|
|
7
12
|
afterEach(() => installContext(undefined))
|
|
8
13
|
|
|
@@ -378,43 +383,3 @@ describe("scenario wording in review's fix turns", () => {
|
|
|
378
383
|
expect(log.slice(-2)).toEqual(["fix", "scenario-wording"])
|
|
379
384
|
})
|
|
380
385
|
})
|
|
381
|
-
|
|
382
|
-
describe("the quality fix loop", () => {
|
|
383
|
-
it("a fix that resolves QUALITY.md and rewrites a scenario ends after the wording gate", async () => {
|
|
384
|
-
const QUALITY = ".gtd/QUALITY.md"
|
|
385
|
-
const log: string[] = []
|
|
386
|
-
const files = new Map([
|
|
387
|
-
[QUALITY, "- finding"],
|
|
388
|
-
["e2e/a.feature", "Given a"],
|
|
389
|
-
])
|
|
390
|
-
let last: readonly Change[] = []
|
|
391
|
-
installContext(
|
|
392
|
-
fixtureContext(
|
|
393
|
-
{},
|
|
394
|
-
{
|
|
395
|
-
step: (request: StepRequest) => {
|
|
396
|
-
if (request.kind === "restart") return Promise.resolve()
|
|
397
|
-
log.push(request.name)
|
|
398
|
-
last = []
|
|
399
|
-
if (request.name === "fix.quality.fixing") {
|
|
400
|
-
files.delete(QUALITY)
|
|
401
|
-
files.set("e2e/a.feature", "Given b")
|
|
402
|
-
last = [{ path: QUALITY, status: "deleted", before: "- finding", after: undefined }]
|
|
403
|
-
}
|
|
404
|
-
return Promise.resolve()
|
|
405
|
-
},
|
|
406
|
-
pushScope: () => undefined,
|
|
407
|
-
popScope: () => undefined,
|
|
408
|
-
read: (path) => files.get(path),
|
|
409
|
-
changes: () => last,
|
|
410
|
-
matches: (path, pattern) => path === pattern,
|
|
411
|
-
head: () => "h2",
|
|
412
|
-
start: () => "h0",
|
|
413
|
-
},
|
|
414
|
-
),
|
|
415
|
-
)
|
|
416
|
-
const frozen = { at: "h1", texts: { "e2e/a.feature": "Given a" } }
|
|
417
|
-
expect(await fixQualityFindings({ rounds: 0 }, frozen)).toBe(true)
|
|
418
|
-
expect(log).toEqual(["fix.quality.fixing", "scenario-wording"])
|
|
419
|
-
})
|
|
420
|
-
})
|