@pmelab/gtd 58.2.0 → 59.0.1

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 (150) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +120 -395
  3. package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-07Z_ENch.mjs} +13503 -3460
  4. package/dist/chunks/{wire-C12dahrH.mjs → wire-BTUhRGXj.mjs} +1 -1
  5. package/dist/flows.mjs +1 -1
  6. package/dist/gtd.bundle.mjs +484 -497
  7. package/dist/testkit/gherkin.mjs +145 -65
  8. package/dist/testkit.mjs +1 -1
  9. package/dist/wire.mjs +1 -1
  10. package/package.json +3 -42
  11. package/skills/authoring/SKILL.md +34 -27
  12. package/src/cli/Cli.test.ts +148 -71
  13. package/src/cli/Cli.ts +384 -463
  14. package/src/cli/Help.test.ts +47 -0
  15. package/src/cli/Help.ts +70 -0
  16. package/src/cli/Program.test.ts +127 -33
  17. package/src/cli/Program.ts +70 -25
  18. package/src/edge/Edge.ts +4 -4
  19. package/src/emit/Emit.test.ts +1 -1
  20. package/src/emit/Emit.ts +1 -1
  21. package/src/flags/index.test.ts +0 -13
  22. package/src/flags/index.ts +1 -3
  23. package/src/flows/factory.ts +3 -1
  24. package/src/gherkin/index.ts +135 -71
  25. package/src/gherkin/messages.test.ts +90 -0
  26. package/src/gherkin/messages.ts +84 -0
  27. package/src/install/Install.test.ts +14 -11
  28. package/src/install/Install.ts +38 -15
  29. package/src/install/index.ts +1 -2
  30. package/src/replay/Replay.test.ts +1 -1
  31. package/src/replay/index.ts +1 -1
  32. package/src/step/planStart.ts +6 -1
  33. package/src/testkit/TestRepo.test.ts +47 -2
  34. package/src/testkit/TestRepo.ts +51 -6
  35. package/src/twins/EmittedScriptRecognizer.ts +5 -8
  36. package/src/twins/FlowScripts.ts +1 -1
  37. package/src/wire/Land.ts +1 -1
  38. package/src/workflow/index.ts +1 -0
  39. package/src/workflow/load.test.ts +138 -113
  40. package/src/workflow/load.ts +7 -7
  41. package/src/workflow/skills.test.ts +5 -5
  42. package/src/workflow/skills.ts +1 -1
  43. package/src/workflow/vars.test.ts +30 -3
  44. package/src/workflow/vars.ts +34 -2
  45. package/src/workflow-kit/index.ts +1 -1
  46. package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
  47. package/dist/chunks/review-Bfaror38.mjs +0 -427
  48. package/dist/chunks/tail-Bd7L940p.mjs +0 -355
  49. package/dist/chunks/testkit-JDtpNKyi.mjs +0 -10410
  50. package/dist/workflows/cleanup.mjs +0 -36
  51. package/dist/workflows/feature.mjs +0 -1018
  52. package/dist/workflows/fix.mjs +0 -41
  53. package/dist/workflows/review.mjs +0 -45
  54. package/dist/workflows/rollout.mjs +0 -48
  55. package/dist/workflows.mjs +0 -40651
  56. package/src/install/ConfigScaffold.ts +0 -22
  57. package/src/workflows/cleanup/cleanup-path.feature +0 -94
  58. package/src/workflows/cleanup/cleanup.feature +0 -315
  59. package/src/workflows/cleanup/cleanup.test.ts +0 -276
  60. package/src/workflows/cleanup/cleanup.ts +0 -52
  61. package/src/workflows/cleanup/index.ts +0 -6
  62. package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
  63. package/src/workflows/feature/acceptance-criteria.feature +0 -389
  64. package/src/workflows/feature/criteria-coverage.feature +0 -102
  65. package/src/workflows/feature/criteria.test.ts +0 -147
  66. package/src/workflows/feature/criteria.ts +0 -267
  67. package/src/workflows/feature/fast-path.feature +0 -93
  68. package/src/workflows/feature/feature.test.ts +0 -338
  69. package/src/workflows/feature/feature.ts +0 -156
  70. package/src/workflows/feature/green.ts +0 -19
  71. package/src/workflows/feature/index.ts +0 -18
  72. package/src/workflows/feature/lens-models.feature +0 -69
  73. package/src/workflows/feature/lens-review.feature +0 -566
  74. package/src/workflows/feature/naive-acceptance.feature +0 -147
  75. package/src/workflows/feature/naive-test.feature +0 -263
  76. package/src/workflows/feature/one-step-land.feature +0 -93
  77. package/src/workflows/feature/planning.ts +0 -41
  78. package/src/workflows/feature/refactor-absorbs.feature +0 -131
  79. package/src/workflows/feature/refactor-first.feature +0 -158
  80. package/src/workflows/feature/refactor-lens-models.feature +0 -79
  81. package/src/workflows/feature/refactor-rebuild.feature +0 -273
  82. package/src/workflows/feature/refactor-review.feature +0 -1528
  83. package/src/workflows/feature/refactor.test.ts +0 -1220
  84. package/src/workflows/feature/refactor.ts +0 -368
  85. package/src/workflows/feature/release-flag.feature +0 -167
  86. package/src/workflows/feature/release-flag.test.ts +0 -49
  87. package/src/workflows/feature/release-flag.ts +0 -46
  88. package/src/workflows/feature/review-gate.feature +0 -241
  89. package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
  90. package/src/workflows/feature/risk-tier.feature +0 -351
  91. package/src/workflows/feature/seam-rejection.feature +0 -146
  92. package/src/workflows/feature/seam.feature +0 -297
  93. package/src/workflows/feature/seam.test.ts +0 -527
  94. package/src/workflows/feature/seam.ts +0 -426
  95. package/src/workflows/feature/standards.feature +0 -354
  96. package/src/workflows/feature/steps.test.ts +0 -65
  97. package/src/workflows/feature/steps.ts +0 -45
  98. package/src/workflows/feature/tier-pin.feature +0 -66
  99. package/src/workflows/feature/tier.test.ts +0 -166
  100. package/src/workflows/feature/tier.ts +0 -56
  101. package/src/workflows/fix/fix-lens-origin.feature +0 -114
  102. package/src/workflows/fix/fix-quality.feature +0 -212
  103. package/src/workflows/fix/fix.feature +0 -79
  104. package/src/workflows/fix/fix.test.ts +0 -74
  105. package/src/workflows/fix/fix.ts +0 -52
  106. package/src/workflows/fix/index.ts +0 -6
  107. package/src/workflows/index.ts +0 -47
  108. package/src/workflows/review/index.ts +0 -6
  109. package/src/workflows/review/review-door.feature +0 -199
  110. package/src/workflows/review/review.test.ts +0 -27
  111. package/src/workflows/review/review.ts +0 -60
  112. package/src/workflows/rollout/index.ts +0 -6
  113. package/src/workflows/rollout/rollout.feature +0 -510
  114. package/src/workflows/rollout/rollout.test.ts +0 -263
  115. package/src/workflows/rollout/rollout.ts +0 -82
  116. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
  117. package/src/workflows/shared/access.test.ts +0 -62
  118. package/src/workflows/shared/access.ts +0 -24
  119. package/src/workflows/shared/config.fixture.ts +0 -12
  120. package/src/workflows/shared/fences.ts +0 -24
  121. package/src/workflows/shared/health.test.ts +0 -148
  122. package/src/workflows/shared/health.ts +0 -191
  123. package/src/workflows/shared/learn.test.ts +0 -140
  124. package/src/workflows/shared/learn.ts +0 -201
  125. package/src/workflows/shared/lenses.test.ts +0 -39
  126. package/src/workflows/shared/lenses.ts +0 -294
  127. package/src/workflows/shared/options.test.ts +0 -157
  128. package/src/workflows/shared/options.ts +0 -92
  129. package/src/workflows/shared/progress.feature +0 -124
  130. package/src/workflows/shared/prose.ts +0 -263
  131. package/src/workflows/shared/recheck.feature +0 -60
  132. package/src/workflows/shared/registry.test.ts +0 -345
  133. package/src/workflows/shared/registry.ts +0 -183
  134. package/src/workflows/shared/reserved.ts +0 -11
  135. package/src/workflows/shared/review.test.ts +0 -1062
  136. package/src/workflows/shared/review.ts +0 -446
  137. package/src/workflows/shared/skills.test.ts +0 -121
  138. package/src/workflows/shared/skills.ts +0 -49
  139. package/src/workflows/shared/standards.test.ts +0 -90
  140. package/src/workflows/shared/standards.ts +0 -156
  141. package/src/workflows/shared/steps.test.ts +0 -219
  142. package/src/workflows/shared/steps.ts +0 -201
  143. package/src/workflows/shared/tail.test.ts +0 -454
  144. package/src/workflows/shared/tail.ts +0 -264
  145. package/src/workflows/shared/text.fixture.ts +0 -128
  146. package/src/workflows/shared/text.test.ts +0 -1430
  147. package/src/workflows/shared/text.ts +0 -1700
  148. package/src/workflows/shared/threads.test.ts +0 -129
  149. package/src/workflows/shared/vars.ts +0 -47
  150. 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"