@pmelab/gtd 56.0.0 → 57.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +20 -27
- package/dist/chunks/{reserved-B8EC-WE8.mjs → reserved-X2RkNHin.mjs} +34 -116
- package/dist/chunks/{review-HdrX0FJ7.mjs → review-v7sh5GF4.mjs} +2 -2
- package/dist/chunks/{tail-BSLah2mA.mjs → tail-S04KEewi.mjs} +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +52 -162
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +81 -274
- package/package.json +1 -1
- package/src/workflows/feature/criteria-coverage.feature +0 -1
- package/src/workflows/feature/criteria.test.ts +1 -41
- package/src/workflows/feature/criteria.ts +2 -14
- package/src/workflows/feature/fast-path.feature +0 -1
- package/src/workflows/feature/feature.ts +5 -6
- package/src/workflows/feature/flag-mode.feature +1 -1
- package/src/workflows/feature/lens-models.feature +0 -1
- package/src/workflows/feature/lens-review.feature +0 -1
- package/src/workflows/feature/naive-test.feature +258 -0
- package/src/workflows/feature/one-step-land.feature +0 -1
- package/src/workflows/feature/refactor-absorbs.feature +0 -1
- package/src/workflows/feature/refactor-first.feature +0 -1
- package/src/workflows/feature/refactor-rebuild.feature +0 -1
- package/src/workflows/feature/refactor-review.feature +0 -1
- package/src/workflows/feature/review-gate.feature +1 -2
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -1
- package/src/workflows/feature/seam-rejection.feature +0 -1
- package/src/workflows/feature/seam.feature +0 -1
- package/src/workflows/feature/seam.test.ts +1 -2
- package/src/workflows/feature/seam.ts +56 -10
- package/src/workflows/feature/standards.feature +0 -1
- package/src/workflows/shared/options.ts +9 -1
- package/src/workflows/shared/prose.ts +0 -6
- package/src/workflows/shared/skills.test.ts +0 -4
- package/src/workflows/shared/skills.ts +0 -5
- package/src/workflows/shared/steps.ts +1 -1
- package/src/workflows/shared/text.test.ts +22 -41
- package/src/workflows/shared/text.ts +42 -142
- package/src/workflows/shared/vars.ts +2 -3
- package/src/workflows/feature/hands-on-skip.feature +0 -316
- package/src/workflows/feature/prototype-rounds.feature +0 -156
- package/src/workflows/feature/prototype.feature +0 -428
- package/src/workflows/feature/prototype.test.ts +0 -43
- package/src/workflows/feature/prototype.ts +0 -198
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { answered, changes, human, judge, read, refuse, scope, vars } from "../../flows/index.js"
|
|
2
|
-
import { HEADING, markFenced } from "../shared/fences.js"
|
|
3
|
-
import { coder, PROTOTYPE, REQUIREMENTS } from "../shared/steps.js"
|
|
4
|
-
import * as t from "../shared/text.js"
|
|
5
|
-
import { defaults } from "../shared/vars.js"
|
|
6
|
-
import { green } from "./green.js"
|
|
7
|
-
import { criteriaOf, criterionIds, handsOnIds, requireCriteriaFrozen } from "./criteria.js"
|
|
8
|
-
|
|
9
|
-
/** Only a `no` at this or above skips a criterion: no verdict, or an unsure one, sends it to the hands-on test. */
|
|
10
|
-
const UNOBSERVABLE_MIN_P = 0.5
|
|
11
|
-
|
|
12
|
-
/** The `prototypeRounds` setting. Anything but a non-negative integer fails rather than guess a cap. */
|
|
13
|
-
export const prototypeRounds = (): number => {
|
|
14
|
-
const value = vars.prototypeRounds ?? defaults.prototypeRounds!
|
|
15
|
-
if (!/^(0|[1-9]\d*)$/.test(value))
|
|
16
|
-
refuse(`gtd: prototypeRounds must be a non-negative integer, got ${JSON.stringify(value)}`)
|
|
17
|
-
return Number(value)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type Verdict = "pass" | "fail" | "blocked"
|
|
21
|
-
|
|
22
|
-
export interface Item {
|
|
23
|
-
/** A criterion ID, or `Problem <title>`. */
|
|
24
|
-
readonly subject: string
|
|
25
|
-
readonly verdict: Verdict
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface Report {
|
|
29
|
-
readonly unguided: boolean
|
|
30
|
-
readonly items: readonly Item[]
|
|
31
|
-
/** Headings that read as an item but miss the shape: a failure there would go unseen. */
|
|
32
|
-
readonly malformed: readonly string[]
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const CHUNK = /^(AC-\d+|Problem\b.*?)\s*:\s*(pass|fail|blocked)$/i
|
|
36
|
-
const UNGUIDED = /^no testing instructions$/i
|
|
37
|
-
|
|
38
|
-
/** The `## ` chunks of a prototype report; a heading in neither shape is prose, not an item. */
|
|
39
|
-
export const reportOf = (text: string): Report => {
|
|
40
|
-
let unguided = false
|
|
41
|
-
const items: Item[] = []
|
|
42
|
-
const malformed: string[] = []
|
|
43
|
-
for (const { line, fenced } of markFenced(text)) {
|
|
44
|
-
const heading = fenced ? null : HEADING.exec(line)
|
|
45
|
-
if (heading?.[1] !== "##") continue
|
|
46
|
-
if (UNGUIDED.test(heading[2]!)) unguided = true
|
|
47
|
-
const chunk = CHUNK.exec(heading[2]!)
|
|
48
|
-
if (chunk === null && /^(ac-\d|problem)/i.test(heading[2]!)) malformed.push(heading[2]!)
|
|
49
|
-
if (chunk !== null)
|
|
50
|
-
items.push({
|
|
51
|
-
subject: /^ac-/i.test(chunk[1]!) ? chunk[1]!.toUpperCase() : chunk[1]!,
|
|
52
|
-
verdict: chunk[2]!.toLowerCase() as Verdict,
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
return { unguided, items, malformed }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The report the testing turn left, refused unless it is the only change and
|
|
60
|
-
* names each of `ids` once, with its open items: any other criterion is not
|
|
61
|
-
* this test's to fix.
|
|
62
|
-
*/
|
|
63
|
-
const requireReport = (
|
|
64
|
-
ids: readonly string[],
|
|
65
|
-
retest: boolean,
|
|
66
|
-
): { readonly unguided: boolean; readonly open: readonly Item[] } => {
|
|
67
|
-
const others = changes()
|
|
68
|
-
.map((c) => c.path)
|
|
69
|
-
.filter((path) => path !== PROTOTYPE)
|
|
70
|
-
if (others.length > 0)
|
|
71
|
-
refuse(
|
|
72
|
-
`gtd land: prototype-report: test in a disposable place and change nothing but ${PROTOTYPE} — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}`,
|
|
73
|
-
)
|
|
74
|
-
const text = read(PROTOTYPE)
|
|
75
|
-
if (text === undefined) return refuse(`gtd land: prototype-report: write ${PROTOTYPE}`)
|
|
76
|
-
const report = reportOf(text)
|
|
77
|
-
if (report.malformed.length > 0)
|
|
78
|
-
refuse(
|
|
79
|
-
`gtd land: prototype-report: head each chunk \`## <ID>: pass|fail|blocked\` or \`## Problem <title>: pass|fail|blocked\` — these miss it:\n${report.malformed.map((h) => ` - ${h}`).join("\n")}`,
|
|
80
|
-
)
|
|
81
|
-
// Only a first test with nothing else in it: a chunk beside it was tested after all, and a
|
|
82
|
-
// re-test's open items must reach a person, not slip past as untested.
|
|
83
|
-
if (report.unguided && report.items.length === 0 && !retest) return { unguided: true, open: [] }
|
|
84
|
-
const elsewhere = criterionIds(read(REQUIREMENTS) ?? "").filter((id) => !ids.includes(id))
|
|
85
|
-
const count = (id: string): number => report.items.filter((i) => i.subject === id).length
|
|
86
|
-
const wrong = ids.filter((id) => count(id) !== 1)
|
|
87
|
-
if (wrong.length > 0)
|
|
88
|
-
refuse(
|
|
89
|
-
`gtd land: prototype-report: ${PROTOTYPE} needs exactly one chunk headed \`## <ID>: pass|fail|blocked\` for ${wrong.join(", ")}`,
|
|
90
|
-
)
|
|
91
|
-
// A mistyped ID stays open: only a known criterion outside `ids` is not this test's to fix.
|
|
92
|
-
const open = report.items.filter((i) => i.verdict !== "pass" && !elsewhere.includes(i.subject))
|
|
93
|
-
return { unguided: false, open }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* The criteria a person can observe by using the change, by the judge: those
|
|
98
|
-
* tagged `(no hands-on)` are never asked about.
|
|
99
|
-
*/
|
|
100
|
-
const observable = async (): Promise<string[]> => {
|
|
101
|
-
const requirements = read(REQUIREMENTS) ?? ""
|
|
102
|
-
const own = handsOnIds(requirements)
|
|
103
|
-
const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id))
|
|
104
|
-
if (criteria.length === 0) return []
|
|
105
|
-
const { answers } = await judge("observable", {
|
|
106
|
-
questions: criteria.map(t.observableQuestion),
|
|
107
|
-
evidence: { requirements },
|
|
108
|
-
message: t.observableMessage(),
|
|
109
|
-
label: "Judging which criteria to test by hand",
|
|
110
|
-
})
|
|
111
|
-
return own.filter((id) => !answered(answers[id], "no", UNOBSERVABLE_MIN_P))
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const frozen = (): void => requireCriteriaFrozen(REQUIREMENTS)
|
|
115
|
-
|
|
116
|
-
const testing = async (
|
|
117
|
-
ids: readonly string[],
|
|
118
|
-
retest: boolean,
|
|
119
|
-
): Promise<ReturnType<typeof requireReport>> => {
|
|
120
|
-
const skipped = criterionIds(read(REQUIREMENTS) ?? "").filter((id) => !ids.includes(id))
|
|
121
|
-
await t.agentWithSkills("testing", t.prototypeTestingPrompt(skipped, retest), {
|
|
122
|
-
label: retest ? "Re-testing the change by hand" : "Testing the change by hand",
|
|
123
|
-
file: PROTOTYPE,
|
|
124
|
-
model: coder(),
|
|
125
|
-
system: t.testerSystem(),
|
|
126
|
-
// A test that saw exactly what the last round saw leaves the report as it was.
|
|
127
|
-
allowEmpty: true,
|
|
128
|
-
})
|
|
129
|
-
return requireReport(ids, retest)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const fixing = async (): Promise<void> => {
|
|
133
|
-
await scope("fix", () =>
|
|
134
|
-
t.agentWithSkills("fixing", t.prototypeFixingPrompt(), {
|
|
135
|
-
label: "Fixing what the hands-on test found",
|
|
136
|
-
file: PROTOTYPE,
|
|
137
|
-
model: coder(),
|
|
138
|
-
system: t.builderSystem(),
|
|
139
|
-
// A `fail` the tester got wrong has nothing to fix: the re-test says so, or the cap ends it.
|
|
140
|
-
allowEmpty: true,
|
|
141
|
-
}),
|
|
142
|
-
)
|
|
143
|
-
// The re-test may land empty: a report the fixer edited would pass untested.
|
|
144
|
-
if (changes().some((c) => c.path === PROTOTYPE))
|
|
145
|
-
refuse(`gtd land: prototype-fix: leave ${PROTOTYPE} as it is — the next round re-tests from it`)
|
|
146
|
-
frozen()
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
|
|
150
|
-
const askPerson = async (name: string, message: string, label: string): Promise<boolean> => {
|
|
151
|
-
await human(name, { message, label, file: PROTOTYPE, acceptClean: true })
|
|
152
|
-
// A note in the report alone tests nothing new.
|
|
153
|
-
if (!changes().some((c) => c.path !== PROTOTYPE)) return false
|
|
154
|
-
frozen()
|
|
155
|
-
return true
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Use the change by hand: a testing turn reports on every criterion a person
|
|
160
|
-
* can observe, a fix turn takes each failure, and the change is tested again —
|
|
161
|
-
* until the report is clean, or `prototypeRounds` fixes are spent and a person
|
|
162
|
-
* decides. A change with no such criterion is not tested by hand.
|
|
163
|
-
*/
|
|
164
|
-
export const prototypeTest = async (): Promise<void> => {
|
|
165
|
-
const cap = prototypeRounds()
|
|
166
|
-
if (cap === 0) return
|
|
167
|
-
await scope("prototype", async () => {
|
|
168
|
-
const ids = await observable()
|
|
169
|
-
if (ids.length === 0) return
|
|
170
|
-
let fixes = 0
|
|
171
|
-
let retest = false
|
|
172
|
-
for (;;) {
|
|
173
|
-
const { unguided, open } = await testing(ids, retest)
|
|
174
|
-
retest = !unguided
|
|
175
|
-
if (unguided) {
|
|
176
|
-
const instructed = await askPerson(
|
|
177
|
-
"unguided",
|
|
178
|
-
t.prototypeUnguidedMessage(),
|
|
179
|
-
"No way to test by hand",
|
|
180
|
-
)
|
|
181
|
-
if (!instructed) return
|
|
182
|
-
} else if (open.length === 0) return
|
|
183
|
-
else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
|
|
184
|
-
fixes++
|
|
185
|
-
await fixing()
|
|
186
|
-
} else {
|
|
187
|
-
const changed = await askPerson(
|
|
188
|
-
"open",
|
|
189
|
-
t.prototypeOpenMessage(open, fixes),
|
|
190
|
-
"Hands-on test still failing",
|
|
191
|
-
)
|
|
192
|
-
if (!changed) return
|
|
193
|
-
fixes = 0
|
|
194
|
-
}
|
|
195
|
-
await green()
|
|
196
|
-
}
|
|
197
|
-
})
|
|
198
|
-
}
|