@pmelab/gtd 58.1.0 → 59.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.
Files changed (142) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +115 -390
  3. package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
  4. package/dist/gtd.bundle.mjs +76 -59
  5. package/dist/testkit/gherkin.mjs +30 -2
  6. package/dist/testkit.mjs +1 -1
  7. package/package.json +3 -42
  8. package/skills/authoring/SKILL.md +31 -25
  9. package/src/cli/Cli.test.ts +15 -19
  10. package/src/cli/Cli.ts +2 -4
  11. package/src/cli/Program.test.ts +60 -29
  12. package/src/cli/Program.ts +5 -7
  13. package/src/edge/Edge.ts +3 -3
  14. package/src/flags/index.test.ts +0 -13
  15. package/src/flags/index.ts +1 -3
  16. package/src/gherkin/git-control.feature +78 -0
  17. package/src/gherkin/index.ts +35 -1
  18. package/src/install/Install.test.ts +14 -11
  19. package/src/install/Install.ts +38 -15
  20. package/src/install/index.ts +1 -2
  21. package/src/replay/Replay.test.ts +1 -1
  22. package/src/replay/index.ts +1 -1
  23. package/src/step/planStart.ts +6 -1
  24. package/src/testkit/TestRepo.test.ts +118 -2
  25. package/src/testkit/TestRepo.ts +57 -6
  26. package/src/testkit/index.ts +1 -0
  27. package/src/twins/EmittedScriptRecognizer.ts +12 -14
  28. package/src/twins/FlowScripts.ts +10 -9
  29. package/src/twins/InMemRepo.ts +26 -1
  30. package/src/twins/Layers.ts +12 -1
  31. package/src/twins/index.ts +6 -1
  32. package/src/workflow/index.ts +1 -0
  33. package/src/workflow/load.test.ts +138 -113
  34. package/src/workflow/load.ts +1 -1
  35. package/src/workflow/vars.test.ts +30 -3
  36. package/src/workflow/vars.ts +34 -2
  37. package/src/workflow-kit/index.ts +1 -1
  38. package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
  39. package/dist/chunks/review-Bfaror38.mjs +0 -427
  40. package/dist/chunks/tail-Bd7L940p.mjs +0 -355
  41. package/dist/chunks/testkit-D1-auYau.mjs +0 -10362
  42. package/dist/workflows/cleanup.mjs +0 -36
  43. package/dist/workflows/feature.mjs +0 -1018
  44. package/dist/workflows/fix.mjs +0 -41
  45. package/dist/workflows/review.mjs +0 -45
  46. package/dist/workflows/rollout.mjs +0 -48
  47. package/dist/workflows.mjs +0 -40651
  48. package/src/install/ConfigScaffold.ts +0 -22
  49. package/src/workflows/cleanup/cleanup-path.feature +0 -94
  50. package/src/workflows/cleanup/cleanup.feature +0 -315
  51. package/src/workflows/cleanup/cleanup.test.ts +0 -276
  52. package/src/workflows/cleanup/cleanup.ts +0 -52
  53. package/src/workflows/cleanup/index.ts +0 -6
  54. package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
  55. package/src/workflows/feature/acceptance-criteria.feature +0 -389
  56. package/src/workflows/feature/criteria-coverage.feature +0 -102
  57. package/src/workflows/feature/criteria.test.ts +0 -147
  58. package/src/workflows/feature/criteria.ts +0 -267
  59. package/src/workflows/feature/fast-path.feature +0 -93
  60. package/src/workflows/feature/feature.test.ts +0 -338
  61. package/src/workflows/feature/feature.ts +0 -156
  62. package/src/workflows/feature/green.ts +0 -19
  63. package/src/workflows/feature/index.ts +0 -18
  64. package/src/workflows/feature/lens-models.feature +0 -69
  65. package/src/workflows/feature/lens-review.feature +0 -566
  66. package/src/workflows/feature/naive-acceptance.feature +0 -147
  67. package/src/workflows/feature/naive-test.feature +0 -263
  68. package/src/workflows/feature/one-step-land.feature +0 -93
  69. package/src/workflows/feature/planning.ts +0 -41
  70. package/src/workflows/feature/refactor-absorbs.feature +0 -131
  71. package/src/workflows/feature/refactor-first.feature +0 -158
  72. package/src/workflows/feature/refactor-lens-models.feature +0 -79
  73. package/src/workflows/feature/refactor-rebuild.feature +0 -273
  74. package/src/workflows/feature/refactor-review.feature +0 -1528
  75. package/src/workflows/feature/refactor.test.ts +0 -1220
  76. package/src/workflows/feature/refactor.ts +0 -368
  77. package/src/workflows/feature/release-flag.feature +0 -167
  78. package/src/workflows/feature/release-flag.test.ts +0 -49
  79. package/src/workflows/feature/release-flag.ts +0 -46
  80. package/src/workflows/feature/review-gate.feature +0 -241
  81. package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
  82. package/src/workflows/feature/risk-tier.feature +0 -351
  83. package/src/workflows/feature/seam-rejection.feature +0 -146
  84. package/src/workflows/feature/seam.feature +0 -297
  85. package/src/workflows/feature/seam.test.ts +0 -527
  86. package/src/workflows/feature/seam.ts +0 -426
  87. package/src/workflows/feature/standards.feature +0 -354
  88. package/src/workflows/feature/steps.test.ts +0 -65
  89. package/src/workflows/feature/steps.ts +0 -45
  90. package/src/workflows/feature/tier-pin.feature +0 -66
  91. package/src/workflows/feature/tier.test.ts +0 -166
  92. package/src/workflows/feature/tier.ts +0 -56
  93. package/src/workflows/fix/fix-lens-origin.feature +0 -114
  94. package/src/workflows/fix/fix-quality.feature +0 -212
  95. package/src/workflows/fix/fix.feature +0 -79
  96. package/src/workflows/fix/fix.test.ts +0 -74
  97. package/src/workflows/fix/fix.ts +0 -52
  98. package/src/workflows/fix/index.ts +0 -6
  99. package/src/workflows/index.ts +0 -47
  100. package/src/workflows/review/index.ts +0 -6
  101. package/src/workflows/review/review-door.feature +0 -199
  102. package/src/workflows/review/review.test.ts +0 -27
  103. package/src/workflows/review/review.ts +0 -60
  104. package/src/workflows/rollout/index.ts +0 -6
  105. package/src/workflows/rollout/rollout.feature +0 -510
  106. package/src/workflows/rollout/rollout.test.ts +0 -263
  107. package/src/workflows/rollout/rollout.ts +0 -82
  108. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
  109. package/src/workflows/shared/access.test.ts +0 -62
  110. package/src/workflows/shared/access.ts +0 -24
  111. package/src/workflows/shared/config.fixture.ts +0 -12
  112. package/src/workflows/shared/fences.ts +0 -24
  113. package/src/workflows/shared/health.test.ts +0 -148
  114. package/src/workflows/shared/health.ts +0 -191
  115. package/src/workflows/shared/learn.test.ts +0 -140
  116. package/src/workflows/shared/learn.ts +0 -201
  117. package/src/workflows/shared/lenses.test.ts +0 -39
  118. package/src/workflows/shared/lenses.ts +0 -294
  119. package/src/workflows/shared/options.test.ts +0 -157
  120. package/src/workflows/shared/options.ts +0 -92
  121. package/src/workflows/shared/progress.feature +0 -124
  122. package/src/workflows/shared/prose.ts +0 -263
  123. package/src/workflows/shared/recheck.feature +0 -60
  124. package/src/workflows/shared/registry.test.ts +0 -345
  125. package/src/workflows/shared/registry.ts +0 -183
  126. package/src/workflows/shared/reserved.ts +0 -11
  127. package/src/workflows/shared/review.test.ts +0 -1062
  128. package/src/workflows/shared/review.ts +0 -446
  129. package/src/workflows/shared/skills.test.ts +0 -121
  130. package/src/workflows/shared/skills.ts +0 -49
  131. package/src/workflows/shared/standards.test.ts +0 -90
  132. package/src/workflows/shared/standards.ts +0 -156
  133. package/src/workflows/shared/steps.test.ts +0 -219
  134. package/src/workflows/shared/steps.ts +0 -201
  135. package/src/workflows/shared/tail.test.ts +0 -454
  136. package/src/workflows/shared/tail.ts +0 -264
  137. package/src/workflows/shared/text.fixture.ts +0 -128
  138. package/src/workflows/shared/text.test.ts +0 -1430
  139. package/src/workflows/shared/text.ts +0 -1700
  140. package/src/workflows/shared/threads.test.ts +0 -129
  141. package/src/workflows/shared/vars.ts +0 -47
  142. package/src/workflows/shared/workflows.steps.ts +0 -54
@@ -1,368 +0,0 @@
1
- import {
2
- changes,
3
- changesSince,
4
- head,
5
- matches,
6
- quote,
7
- read,
8
- refuse,
9
- removeScript,
10
- run,
11
- scope,
12
- vars,
13
- type Change,
14
- type DiffShapeResult,
15
- } from "../../flows/index.js"
16
- import { lensEntriesOf } from "../../workflow-kit/index.js"
17
- import { markFenced } from "../shared/fences.js"
18
- import { healthy, type Guard } from "../shared/health.js"
19
- import { coder, FEEDBACK, REFACTOR } from "../shared/steps.js"
20
- import * as t from "../shared/text.js"
21
- import { defaults, testFileGlobs } from "../shared/vars.js"
22
- import { buildTail, reUnwind, type FeedbackTarget } from "../shared/review.js"
23
- import { refactorCollecting } from "./steps.js"
24
-
25
- export const REFACTOR_PACKAGE = REFACTOR
26
-
27
- export interface RefactorOptions {
28
- /** Blank counts as absent: `kept()` reports `""` while the keeping turn is pending. */
29
- readonly reference?: string | undefined
30
- /** The refactor note `seam.note` wrote; rides the package as `## Refactor note`. */
31
- readonly note?: string | undefined
32
- }
33
-
34
- const isCode = (path: string): boolean => path !== ".gtd" && !path.startsWith(".gtd/")
35
-
36
- export const requireWithin = (allowed: readonly string[]): void => {
37
- const outside = changes()
38
- .map((c) => c.path)
39
- .filter((path) => isCode(path) && !allowed.includes(path))
40
- if (outside.length === 0) return
41
- refuse(
42
- `gtd land: refactor-scope: the refactor may change only its stray paths — revert these:\n${outside.map((path) => ` - ${path}`).join("\n")}`,
43
- )
44
- }
45
-
46
- // A fixed list, not `seamAllow`: narrowing that setting must not let a test file through.
47
- export const isTestFile = (path: string): boolean =>
48
- testFileGlobs.some((glob) => matches(path, glob))
49
-
50
- // The bundled refactor uses `requireTestCallSitesOnly`; this stays a public export.
51
- export const requireNoTestFiles = (): void => {
52
- const tests = changes()
53
- .map((c) => c.path)
54
- .filter((path) => isCode(path) && isTestFile(path))
55
- if (tests.length === 0) return
56
- refuse(
57
- `gtd land: refactor-tests: the refactor may not change a test file — revert these:\n${tests.map((path) => ` - ${path}`).join("\n")}`,
58
- )
59
- }
60
-
61
- // Call sites live in code: a `.feature` file or a snapshot holds only expectations.
62
- export const holdsCallSites = (path: string): boolean =>
63
- !path.endsWith(".feature") && !path.endsWith(".snap")
64
-
65
- // `//` after a `:` (a URL) and `/*` after `*` or `/` (a glob) sit in strings, not comments.
66
- export const COMMENT = /(?<![*/])\/\*[\s\S]*?\*\/|(?<!:)\/\/.*$/gm
67
- const ASSERTION = /\b(?:expect|assert\w*)\s*[.(]/g
68
- const TEST_CASE = /(?<![.\w])x?(?:it|test|describe)\s*[.(]/g
69
- const MODIFIER =
70
- /(?<![.\w])(?:it|test|describe)\.(skip|only|todo|fails|skipIf|runIf)\b|(?<![.\w])(x)(?:it|test|describe)\b/g
71
-
72
- // Modifiers count per kind, so swapping a `.skip` for an `.only` shows.
73
- const counts = (text: string): string => {
74
- const live = text.replace(COMMENT, "")
75
- const modifiers = [...live.matchAll(MODIFIER)].map((m) => m[1] ?? m[2]).sort()
76
- return JSON.stringify([
77
- [...live.matchAll(ASSERTION)].length,
78
- [...live.matchAll(TEST_CASE)].length,
79
- modifiers,
80
- ])
81
- }
82
-
83
- const tested = (text: string): boolean => {
84
- const live = text.replace(COMMENT, "")
85
- return live.search(ASSERTION) >= 0 || live.search(TEST_CASE) >= 0
86
- }
87
-
88
- // A call-shape update keeps every live test and assertion, so the counts hold. A changed
89
- // expected value keeps every count — that is left to the refactor review.
90
- const callSitesOnly = ({ path, status, before, after }: Change): boolean =>
91
- status === "modified" &&
92
- holdsCallSites(path) &&
93
- before !== undefined &&
94
- after !== undefined &&
95
- // A file with no test or assertion is fixture or helper data, never a call site to follow.
96
- tested(before) &&
97
- counts(before) === counts(after)
98
-
99
- /**
100
- * The test-file half of the refactor guard: an existing test may follow a changed call shape,
101
- * nothing more. Returns the test files it admitted, which the scope check then allows.
102
- */
103
- export const requireTestCallSitesOnly = (): readonly string[] => {
104
- const tests = changes().filter((c) => isCode(c.path) && isTestFile(c.path))
105
- const refused = tests.filter((c) => !callSitesOnly(c)).map((c) => c.path)
106
- if (refused.length > 0)
107
- refuse(
108
- `gtd land: refactor-tests: the refactor may update an existing test file's call sites only — never add, delete, skip or comment out a test or an assertion, or touch a snapshot, a .feature file or test data; revert these:\n${refused.map((path) => ` - ${path}`).join("\n")}`,
109
- )
110
- return tests.map((c) => c.path)
111
- }
112
-
113
- const flagExpression = (pattern: string): RegExp => {
114
- if (pattern.trim() === "") throw new Error("gtd: refactorFlagPattern must not be blank")
115
- let expression: RegExp
116
- try {
117
- expression = new RegExp(pattern, "g")
118
- } catch (error) {
119
- throw new Error(`gtd: refactorFlagPattern is not a valid regular expression: ${String(error)}`)
120
- }
121
- // An empty match would count the name "" at every character.
122
- if (expression.test(""))
123
- throw new Error("gtd: refactorFlagPattern must not match the empty string")
124
- return expression
125
- }
126
-
127
- // A zero-length match is never counted: a lookahead-only pattern would count every read
128
- // under "" and let a flag swap pass. Checked on the strays up front and again at landing.
129
- const countFlags = (text: string, expression: RegExp, into: Map<string, number>): boolean => {
130
- let zeroLength = false
131
- for (const [name] of text.matchAll(expression)) {
132
- if (name === "") zeroLength = true
133
- else into.set(name, (into.get(name) ?? 0) + 1)
134
- }
135
- return zeroLength
136
- }
137
-
138
- const listPaths = (paths: readonly string[]): string =>
139
- paths.map((path) => ` - ${path}`).join("\n")
140
-
141
- // Counts per name, so a read moved between files passes.
142
- export const requireNoNewFlags = (pattern: string, since: string): void => {
143
- const expression = flagExpression(pattern)
144
- const before = new Map<string, number>()
145
- const after = new Map<string, number>()
146
- const unreadable: string[] = []
147
- const zeroLength: string[] = []
148
- for (const { path, status, before: was, after: now } of changesSince(since)) {
149
- if (!isCode(path)) continue
150
- // A side absent by status is empty; one absent otherwise could not be read.
151
- const oldSide = status === "added" ? "" : was
152
- const newSide = status === "deleted" ? "" : now
153
- if (oldSide === undefined || newSide === undefined) {
154
- unreadable.push(path)
155
- continue
156
- }
157
- countFlags(oldSide, expression, before)
158
- if (countFlags(newSide, expression, after)) zeroLength.push(path)
159
- }
160
- if (unreadable.length > 0)
161
- refuse(`gtd land: refactor-flags: cannot compare flag reads in:\n${listPaths(unreadable)}`)
162
- if (zeroLength.length > 0)
163
- refuse(
164
- `gtd land: refactor-flags: refactorFlagPattern (${pattern}) produced a zero-length match in:\n${listPaths(zeroLength)}`,
165
- )
166
- const raised = [...after].filter(([name, count]) => count > (before.get(name) ?? 0))
167
- if (raised.length === 0) return
168
- refuse(
169
- `gtd land: refactor-flags: the refactor may not introduce or add reads of a flag (${pattern}):\n${raised.map(([name, count]) => ` - ${name}: ${before.get(name) ?? 0} → ${count}`).join("\n")}`,
170
- )
171
- }
172
-
173
- /** A test-file stray is never refactored: the implement turn still reads it in the naive tip. */
174
- export const refactorStrays = (shape: DiffShapeResult): readonly string[] =>
175
- shape.strays.filter((path) => !isTestFile(path))
176
-
177
- const packageLines = (
178
- strays: readonly string[],
179
- pattern: string,
180
- reference: string | undefined,
181
- ): string[] => [
182
- "# Behaviour-neutral refactor",
183
- "",
184
- "## Scope (read-only)",
185
- "",
186
- ...strays.map((path) => `- \`${path}\``),
187
- "",
188
- "This list is fixed when the package is derived: editing it here widens or narrows nothing.",
189
- "",
190
- "## Rules",
191
- "",
192
- "- The suite's outcome must not change",
193
- "- Change only the paths above, plus an existing test file where a changed call shape reaches it: never add or delete a test file, and never add, remove or alter a test or an assertion",
194
- `- Introduce no flag read and add no second read of an existing one (\`${pattern}\`)`,
195
- "",
196
- "## Reference",
197
- "",
198
- ...(reference === undefined
199
- ? ["None."]
200
- : [
201
- `Reading material only, never applied: \`git show ${reference} -- ${strays.map(quote).join(" ")}\``,
202
- "",
203
- "Its changes to the existing code above are the seam this refactor exists to land, minus the feature's own behaviour.",
204
- ]),
205
- "",
206
- ]
207
-
208
- // Nested under a `##` section (review feedback, the refactor note), so every heading drops a level.
209
- const demoteHeadings = (text: string): string[] => {
210
- const marked = markFenced(text.trimEnd())
211
- const lines = marked.map((m) => m.line)
212
- const headings = marked.map((m) =>
213
- m.fenced ? undefined : /^(#{1,6}) /.exec(m.line)?.[1]?.length,
214
- )
215
- const shallowest = Math.min(...headings.filter((n): n is number => n !== undefined))
216
- // Collected headings nest under `## Review feedback`, so the shallowest lands at `###`.
217
- const shift = 3 - shallowest
218
- return lines.map((line, i) => {
219
- const depth = headings[i]
220
- if (depth === undefined) return line
221
- // Markdown has no heading past `######`.
222
- return "#".repeat(Math.min(6, depth + shift)) + line.slice(depth)
223
- })
224
- }
225
-
226
- const deriveScript = (lines: readonly string[]): string =>
227
- `mkdir -p .gtd
228
- printf '%s\\n' ${lines.map(quote).join(" ")} > ${quote(REFACTOR_PACKAGE)}
229
- `
230
-
231
- interface Plan {
232
- readonly pattern: string
233
- readonly strays: readonly string[]
234
- readonly reference: string | undefined
235
- readonly note: string | undefined
236
- }
237
-
238
- /** Undefined for every shape that is not a readable `refactor-first`; throws on a bad flag pattern. */
239
- const plan = (shape: DiffShapeResult, options: RefactorOptions): Plan | undefined => {
240
- if (shape.choice !== "refactor-first" || shape.unreadable.length > 0) return undefined
241
- const pattern = vars.refactorFlagPattern ?? defaults.refactorFlagPattern!
242
- const expression = flagExpression(pattern)
243
- const strays = refactorStrays(shape)
244
- if (strays.length === 0) return undefined
245
- const zeroLength = strays.filter((path) => countFlags(read(path) ?? "", expression, new Map()))
246
- if (zeroLength.length > 0)
247
- throw new Error(
248
- `gtd: refactorFlagPattern (${pattern}) produces a zero-length match in:\n${listPaths(zeroLength)}`,
249
- )
250
- const reference = options.reference?.trim() ? options.reference : undefined
251
- const note = options.note?.trim() ? options.note : undefined
252
- return { pattern, strays, reference, note }
253
- }
254
-
255
- // The in-memory `strays`, never the package file: a hand edit of its list changes nothing.
256
- // Scope and tests look at the turn's own changes: a person's edit at an escalation rest must
257
- // not wedge every later fix turn. Flags look at the whole range since `entry`.
258
- const guardOf =
259
- ({ pattern, strays }: Plan, entry: string): Guard =>
260
- () => {
261
- requireWithin([...strays, ...requireTestCallSitesOnly()])
262
- requireNoNewFlags(pattern, entry)
263
- }
264
-
265
- /** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
266
- const refactorLap = async (p: Plan, guard: Guard, feedback?: string): Promise<void> => {
267
- const { pattern, strays, reference, note } = p
268
- await run(
269
- "deriving",
270
- deriveScript([
271
- ...packageLines(strays, pattern, reference),
272
- ...(note === undefined ? [] : ["## Refactor note", "", ...demoteHeadings(note), ""]),
273
- ...(feedback === undefined
274
- ? []
275
- : ["## Review feedback", "", ...demoteHeadings(feedback), ""]),
276
- ]),
277
- { label: "Deriving the refactor package", file: REFACTOR_PACKAGE },
278
- )
279
- await t.agentWithSkills(
280
- "building",
281
- t.refactorBuildingPrompt({
282
- pkg: REFACTOR_PACKAGE,
283
- strays,
284
- pattern,
285
- reference,
286
- feedback: feedback !== undefined,
287
- note: note !== undefined,
288
- }),
289
- { label: "Refactoring", model: coder(), system: t.builderSystem() },
290
- )
291
- guard()
292
- await healthy(
293
- async () => {
294
- await t.agentWithSkills("fix.suite.fixing", t.refactorFixPrompt(strays, pattern), {
295
- label: "Fixing the check",
296
- file: FEEDBACK,
297
- model: coder(),
298
- system: t.builderSystem(),
299
- })
300
- guard()
301
- },
302
- { describeGuard: guard, rules: t.refactorRules(strays, pattern) },
303
- )
304
- await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" })
305
- }
306
-
307
- const strayAccess = (strays: readonly string[]) => ({
308
- write: [...strays, ...testFileGlobs, ".gtd/**"],
309
- })
310
-
311
- export const refactorFirst = async (
312
- shape: DiffShapeResult,
313
- options: RefactorOptions = {},
314
- ): Promise<boolean> => {
315
- const p = plan(shape, options)
316
- if (p === undefined) return false
317
- return scope({ name: "refactor", access: strayAccess(p.strays) }, async () => {
318
- await refactorLap(p, guardOf(p, head()))
319
- return true
320
- })
321
- }
322
-
323
- /**
324
- * `refactorFirst` plus its review, in the one `refactor` scope so the tail's
325
- * steps are `refactor.build.*`. A review edit re-runs the refactor, never a
326
- * re-plan: the collected notes ride the re-derived package. Every round
327
- * reviews the whole refactor since `entry`, since each lap rebuilds over it.
328
- */
329
- export const reviewedRefactor = async (
330
- shape: DiffShapeResult,
331
- options: RefactorOptions = {},
332
- ): Promise<boolean> => {
333
- const p = plan(shape, options)
334
- if (p === undefined) return false
335
- return scope({ name: "refactor", access: strayAccess(p.strays) }, async () => {
336
- const entry = head()
337
- const guard = guardOf(p, entry)
338
- const feedback: FeedbackTarget = {
339
- file: REFACTOR_PACKAGE,
340
- collecting: (capture, round) => refactorCollecting(capture, round, p.strays),
341
- unmatched:
342
- "gtd land: refactor-feedback: the review feedback changed files but wrote no .gtd/REFACTOR.md — a refactor cannot act on it; revert the change, or `gtd abandon` and restart",
343
- dropped: t.refactorDroppedRefusal,
344
- }
345
- let collected: string | undefined
346
- for (;;) {
347
- await refactorLap(p, guard, collected)
348
- // A person's edit at an escalation rest is never refused, as in the lap.
349
- const outcome = await buildTail(entry, {
350
- green: true,
351
- guard,
352
- guardRests: false,
353
- lenses: lensEntriesOf(vars.refactorQualityReviews ?? ""),
354
- feedback,
355
- fixAccess: strayAccess(p.strays),
356
- lensBase: entry,
357
- rules: t.refactorRules(p.strays, p.pattern),
358
- lap: "refactor",
359
- })
360
- if (outcome.verdict === "signoff") return true
361
- await reUnwind(outcome)
362
- // The kit returns `feedback` only once the collecting turn wrote the package.
363
- collected = read(REFACTOR_PACKAGE)
364
- if (collected === undefined)
365
- throw new Error(`gtd: review feedback left no ${REFACTOR_PACKAGE}`)
366
- }
367
- })
368
- }
@@ -1,167 +0,0 @@
1
- @inmem
2
- Feature: a release flag only when the requirements ask for one
3
-
4
- A change ships unflagged unless `.gtd/REQUIREMENTS.md` carries a
5
- `## Release Flag` section — written by triage, or by the skip path's impact
6
- turn, only when the sketch asks for a flag, and editable at the design gate.
7
- Its first line is `new` (a new release flag) or `existing: <NAME>` (behind a
8
- flag that already exists). Without it no seam judge or refactor runs and the
9
- naive pass lands as the implementation; with it the seam judge rules on the
10
- pass.
11
-
12
- Background:
13
- Given the shipped workflows, as gtd install scaffolds them
14
- And an environment variable "GTD_QUALITYREVIEWS" set to ""
15
- And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
16
- And a file "src/calc.ts" with:
17
- """
18
- export const add = (a: number, b: number) => a + b
19
- """
20
- And the working tree is committed as "feat: add calculator"
21
- And step "unwind" changes nothing
22
- And step "start-gate.check" changes nothing
23
- And step "seam.naive" writes "src/calc.test.ts" with:
24
- """
25
- it("[AC-1] subtracts", () => {})
26
- """
27
- And step "seam.naive" writes "src/calc.ts" with:
28
- """
29
- export const add = (a: number, b: number) => a + b
30
- export const subtract = (a: number, b: number) => a - b
31
- """
32
- And step "seam.check" changes nothing
33
-
34
- Rule: the design path
35
-
36
- Background:
37
- Given a file "TODO.md" with:
38
- """
39
- # Add subtract
40
-
41
- subtract(a, b) returns the difference.
42
- """
43
- And step "design.gate.answer" changes nothing
44
-
45
- Scenario: with no Release Flag section the naive pass lands unjudged, and no flag is asked for
46
- Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
47
- """
48
- # Requirements
49
-
50
- ## Impact
51
-
52
- - the calculator module
53
-
54
- ## Acceptance Criteria
55
-
56
- - AC-1: subtract(a, b) returns a - b.
57
- """
58
- And step "implement.applying" runs its script
59
- And step "implement.health.check" changes nothing
60
- And step "implement.closing" runs its script
61
- When the process is driven
62
- Then the process rests at "build.review.reviewing"
63
- And "src/calc.ts" contains "subtract"
64
- And no commit body contains "seam.judge"
65
- And the commit subjects end with:
66
- """
67
- gtd(human): design.gate.answer → seam.naive
68
- gtd(agent): seam.naive → seam.check
69
- gtd(check): seam.check → implement.applying
70
- gtd(check): implement.applying → implement.health.check
71
- gtd(check): implement.health.check → implement.closing
72
- gtd(check): implement.closing → build.review.reviewing
73
- """
74
-
75
- Scenario: a new release flag asked for sends the naive pass to the seam judge
76
- Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
77
- """
78
- # Requirements
79
-
80
- ## Impact
81
-
82
- - the calculator module
83
-
84
- ## Release Flag
85
-
86
- new
87
-
88
- ## Acceptance Criteria
89
-
90
- - AC-1: subtract(a, b) returns a - b.
91
- """
92
- When the process is driven
93
- Then the process rests at "seam.judge"
94
-
95
- Scenario: an existing flag asked for sends the naive pass to the seam judge too
96
- Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
97
- """
98
- # Requirements
99
-
100
- ## Impact
101
-
102
- - the calculator module
103
-
104
- ## Release Flag
105
-
106
- existing: GTD_FLAG_BETA
107
-
108
- ## Acceptance Criteria
109
-
110
- - AC-1: subtract(a, b) returns a - b.
111
- """
112
- When the process is driven
113
- Then the process rests at "seam.judge"
114
-
115
- Scenario: a Release Flag section that asks for neither is refused
116
- Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
117
- """
118
- # Requirements
119
-
120
- ## Impact
121
-
122
- - the calculator module
123
-
124
- ## Release Flag
125
-
126
- maybe later
127
-
128
- ## Acceptance Criteria
129
-
130
- - AC-1: subtract(a, b) returns a - b.
131
- """
132
- Then driving the process is refused with "release-flag"
133
-
134
- Rule: the skip path
135
-
136
- Background:
137
- Given a file "TODO.md" with:
138
- """
139
- # Add subtract
140
-
141
- Ship it behind the existing GTD_FLAG_BETA flag.
142
-
143
- ## Acceptance criteria
144
-
145
- - subtract(a, b) returns a - b.
146
- """
147
- And judge step "design.clear" answers:
148
- """
149
- [{"id":"clear","answer":"yes","p":0.9}]
150
- """
151
- And step "design.normalise" runs its script
152
-
153
- Scenario: the impact turn records the flag the sketch asks for, and the seam judge rules
154
- Given step "design.impact" appends to ".gtd/REQUIREMENTS.md":
155
- """
156
-
157
- ## Impact
158
-
159
- - the calculator module
160
-
161
- ## Release Flag
162
-
163
- existing: GTD_FLAG_BETA
164
- """
165
- When the process is driven
166
- Then the process rests at "seam.judge"
167
- And ".gtd/REQUIREMENTS.md" contains "existing: GTD_FLAG_BETA"
@@ -1,49 +0,0 @@
1
- import { describe, expect, it } from "vitest"
2
- import { releaseFlagOf } from "./release-flag.js"
3
-
4
- const requirements = (section: string): string =>
5
- `# Requirements\n\n## Impact\n\n- a module\n\n${section}## Acceptance Criteria\n\n- AC-1: a\n`
6
-
7
- describe("releaseFlagOf", () => {
8
- it("asks for no flag without the section, or with none", () => {
9
- expect(releaseFlagOf(requirements(""))).toEqual({ kind: "none" })
10
- expect(releaseFlagOf(requirements("## Release Flag\n\nnone\n\n"))).toEqual({ kind: "none" })
11
- })
12
-
13
- it("reads new and an existing flag's name, backticked or not", () => {
14
- expect(releaseFlagOf(requirements("## Release Flag\n\nnew\n\n"))).toEqual({ kind: "new" })
15
- for (const line of [
16
- "existing: GTD_FLAG_BETA",
17
- "existing: `GTD_FLAG_BETA`",
18
- "Existing:GTD_FLAG_BETA",
19
- ])
20
- expect(releaseFlagOf(requirements(`## Release Flag\n\n${line}\n\n`))).toEqual({
21
- kind: "existing",
22
- name: "GTD_FLAG_BETA",
23
- })
24
- })
25
-
26
- it("reads only the first line under the heading, and only a level-2 heading", () => {
27
- expect(
28
- releaseFlagOf(requirements("## Release Flag\n\nnew\n\nbecause the sketch says so\n\n")),
29
- ).toEqual({
30
- kind: "new",
31
- })
32
- expect(releaseFlagOf(requirements("### Release Flag\n\nnew\n\n"))).toEqual({ kind: "none" })
33
- })
34
-
35
- it("ignores a fenced heading", () => {
36
- expect(releaseFlagOf(requirements("```\n## Release Flag\n\nnew\n```\n\n"))).toEqual({
37
- kind: "none",
38
- })
39
- })
40
-
41
- it("names the line it cannot read", () => {
42
- expect(releaseFlagOf(requirements("## Release Flag\n\nmaybe later\n\n"))).toEqual({
43
- unreadable: "maybe later",
44
- })
45
- expect(releaseFlagOf(requirements("## Release Flag\n\nexisting:\n\n"))).toEqual({
46
- unreadable: "existing:",
47
- })
48
- })
49
- })
@@ -1,46 +0,0 @@
1
- import { read, refuse } from "../../flows/index.js"
2
- import { HEADING, markFenced } from "../shared/fences.js"
3
- import { REQUIREMENTS } from "../shared/steps.js"
4
- import type { ReleaseFlag } from "../shared/text.js"
5
-
6
- const NAME = /^existing:\s*`?([A-Za-z_][\w.-]*)`?$/i
7
-
8
- const isFlagHeading = (h: RegExpExecArray | null): boolean =>
9
- h !== null && h[1]!.length === 2 && /^release flag$/i.test(h[2]!.trim())
10
-
11
- /** The first line under `## Release Flag`, `undefined` without the section. */
12
- const requestLine = (text: string): string | undefined => {
13
- const lines = markFenced(text)
14
- const at = lines.findIndex((l) => !l.fenced && isFlagHeading(HEADING.exec(l.line)))
15
- if (at === -1) return undefined
16
- const body = lines.slice(at + 1)
17
- const end = body.findIndex((l) => !l.fenced && HEADING.test(l.line))
18
- return (end === -1 ? body : body.slice(0, end)).find((l) => l.line.trim() !== "")?.line.trim()
19
- }
20
-
21
- /** What `text` asks for, or why it cannot be read. No section asks for no flag. */
22
- export const releaseFlagOf = (text: string): ReleaseFlag | { readonly unreadable: string } => {
23
- const line = requestLine(text)
24
- if (line === undefined || /^none$/i.test(line)) return { kind: "none" }
25
- if (/^new$/i.test(line)) return { kind: "new" }
26
- const name = NAME.exec(line)?.[1]
27
- return name === undefined ? { unreadable: line } : { kind: "existing", name }
28
- }
29
-
30
- /** Refuse `file` when its `## Release Flag` section is neither `none`, `new` nor `existing: <NAME>`. */
31
- export const requireReleaseFlag = (file: string): void => {
32
- const request = releaseFlagOf(read(file) ?? "")
33
- if (!("unreadable" in request)) return
34
- refuse(
35
- `gtd land: release-flag: ${file}'s "## Release Flag" reads ${JSON.stringify(request.unreadable)} — its first line must be \`new\`, \`existing: <NAME>\` or \`none\`, or leave the section out for no flag`,
36
- )
37
- }
38
-
39
- /** What `.gtd/REQUIREMENTS.md` asks for now; a person's unreadable edit is refused. */
40
- export const requestedFlag = (): ReleaseFlag => {
41
- requireReleaseFlag(REQUIREMENTS)
42
- return releaseFlagOf(read(REQUIREMENTS) ?? "") as ReleaseFlag
43
- }
44
-
45
- /** Whether the change goes behind a release flag: only then does the seam judge rule. */
46
- export const flagged = (): boolean => requestedFlag().kind !== "none"