@pmelab/gtd 57.0.0 → 58.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 +12 -10
- package/dist/chunks/{Schema-C6fLlQzx.mjs → Schema-B3o6eRNU.mjs} +637 -3957
- package/dist/chunks/{reserved-X2RkNHin.mjs → reserved-lMYCaapY.mjs} +27 -20
- package/dist/chunks/{review-v7sh5GF4.mjs → review-Bfaror38.mjs} +2 -2
- package/dist/chunks/{tail-S04KEewi.mjs → tail-Bd7L940p.mjs} +1 -1
- package/dist/chunks/testkit-D1-auYau.mjs +10362 -0
- package/dist/gtd.bundle.mjs +8 -8
- package/dist/testkit/gherkin.mjs +255 -0
- package/dist/testkit.mjs +2 -15461
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +52 -13
- 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 +74 -28
- package/package.json +8 -1
- package/src/cli/Cli.test.ts +1 -1
- package/src/cli/History.test.ts +1 -1
- package/src/cli/Metrics.test.ts +1 -1
- package/src/cli/Program.test.ts +1 -1
- package/src/cli/Strays.test.ts +1 -1
- package/src/edge/Edge.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/emit/GitScript.ts +1 -1
- package/src/emit/OutcomeScript.ts +1 -1
- package/src/{testkit/steps.ts → gherkin/index.ts} +20 -3
- package/src/gherkin/module-config.feature +24 -0
- package/src/gherkin/tiny.fixture.ts +8 -0
- package/src/platform/Git.test.ts +2 -2
- package/src/platform/RetainedHistory.test.ts +2 -2
- package/src/platform/Workspace.test.ts +2 -2
- package/src/platform/WorktreeState.test.ts +1 -1
- package/src/step/planStart.test.ts +1 -1
- package/src/testkit/TestRepo.ts +7 -4
- package/src/testkit/index.ts +15 -9
- package/src/{testkit → twins}/Layers.ts +2 -2
- package/src/twins/index.ts +8 -0
- package/src/workflow/discovery.test.ts +1 -1
- package/src/workflow/discovery.ts +3 -3
- package/src/workflow/load.ts +3 -3
- package/src/workflows/feature/acceptance-criteria.feature +32 -0
- package/src/workflows/feature/criteria-coverage.feature +0 -1
- package/src/workflows/feature/criteria.ts +10 -4
- package/src/workflows/feature/fast-path.feature +4 -0
- package/src/workflows/feature/feature.test.ts +3 -39
- package/src/workflows/feature/feature.ts +3 -4
- package/src/workflows/feature/lens-models.feature +0 -1
- package/src/workflows/feature/lens-review.feature +0 -1
- package/src/workflows/feature/naive-acceptance.feature +4 -0
- package/src/workflows/feature/naive-test.feature +11 -6
- package/src/workflows/feature/one-step-land.feature +4 -0
- package/src/workflows/feature/planning.ts +2 -0
- package/src/workflows/feature/refactor-absorbs.feature +4 -0
- package/src/workflows/feature/refactor-first.feature +4 -0
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +4 -0
- package/src/workflows/feature/refactor-review.feature +20 -0
- package/src/workflows/feature/release-flag.feature +167 -0
- package/src/workflows/feature/release-flag.test.ts +49 -0
- package/src/workflows/feature/release-flag.ts +46 -0
- package/src/workflows/feature/review-gate.feature +0 -1
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -1
- package/src/workflows/feature/risk-tier.feature +32 -0
- package/src/workflows/feature/seam-rejection.feature +8 -0
- package/src/workflows/feature/seam.feature +8 -0
- package/src/workflows/feature/seam.test.ts +20 -7
- package/src/workflows/feature/seam.ts +7 -7
- package/src/workflows/feature/standards.feature +0 -1
- package/src/workflows/shared/options.test.ts +3 -10
- package/src/workflows/shared/options.ts +0 -9
- package/src/workflows/shared/text.test.ts +36 -28
- package/src/workflows/shared/text.ts +37 -20
- package/src/workflows/shared/vars.ts +0 -1
- package/src/workflows/feature/flag-mode.feature +0 -88
- /package/src/{testkit → gherkin}/testkit.feature +0 -0
- /package/src/{testkit → twins}/EmittedScriptRecognizer.test.ts +0 -0
- /package/src/{testkit → twins}/EmittedScriptRecognizer.ts +0 -0
- /package/src/{testkit → twins}/EmittedScripts.test.ts +0 -0
- /package/src/{testkit → twins}/FakeGitOperations.ts +0 -0
- /package/src/{testkit → twins}/FlowScripts.test.ts +0 -0
- /package/src/{testkit → twins}/FlowScripts.ts +0 -0
- /package/src/{testkit → twins}/GitTiers.test.ts +0 -0
- /package/src/{testkit → twins}/GitTiers.ts +0 -0
- /package/src/{testkit → twins}/InMemRepo.ts +0 -0
- /package/src/{testkit → twins}/Layers.test.ts +0 -0
- /package/src/{testkit → twins}/StrictGitOperations.ts +0 -0
- /package/src/{testkit → twins}/cliIo.ts +0 -0
|
@@ -11,7 +11,15 @@ import {
|
|
|
11
11
|
} from "quickpickle"
|
|
12
12
|
import assert from "node:assert"
|
|
13
13
|
import type { TestContext } from "vitest"
|
|
14
|
-
import {
|
|
14
|
+
import { resolve } from "node:path"
|
|
15
|
+
import { pathToFileURL } from "node:url"
|
|
16
|
+
import {
|
|
17
|
+
runsItsScript,
|
|
18
|
+
testRepo,
|
|
19
|
+
type Actor,
|
|
20
|
+
type TestRepo,
|
|
21
|
+
type WorkflowModule,
|
|
22
|
+
} from "../testkit/index.js"
|
|
15
23
|
|
|
16
24
|
export class TestkitWorld extends QuickPickleWorld {
|
|
17
25
|
// Re-declared: under `nodenext` a subclass loses the base's members from TS's view.
|
|
@@ -21,7 +29,7 @@ export class TestkitWorld extends QuickPickleWorld {
|
|
|
21
29
|
super(context, info)
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
private gtdConfig:
|
|
32
|
+
private gtdConfig: WorkflowModule | undefined = undefined
|
|
25
33
|
private env: Record<string, string> = {}
|
|
26
34
|
/** Per step, what its scripted actor does, in the order the scenario declared it. */
|
|
27
35
|
actions = new Map<string, Actor[]>()
|
|
@@ -33,7 +41,7 @@ export class TestkitWorld extends QuickPickleWorld {
|
|
|
33
41
|
// The repository is created on first use and keeps the config and
|
|
34
42
|
// environment it was created with, so a later change to either is refused
|
|
35
43
|
// rather than silently ignored.
|
|
36
|
-
configure(config:
|
|
44
|
+
configure(config: WorkflowModule): void {
|
|
37
45
|
this.assertNotCreated("a gtd config")
|
|
38
46
|
this.gtdConfig = config
|
|
39
47
|
}
|
|
@@ -41,6 +49,8 @@ export class TestkitWorld extends QuickPickleWorld {
|
|
|
41
49
|
extendConfig(tail: string): void {
|
|
42
50
|
this.assertNotCreated("a gtd config")
|
|
43
51
|
if (this.gtdConfig === undefined) throw new Error("no gtd config to extend")
|
|
52
|
+
if (typeof this.gtdConfig !== "string")
|
|
53
|
+
throw new Error("a gtd config from a module cannot be extended")
|
|
44
54
|
this.gtdConfig += `\n${tail}`
|
|
45
55
|
}
|
|
46
56
|
|
|
@@ -108,6 +118,13 @@ Given("a gtd config with:", (world: TestkitWorld, source: string) => {
|
|
|
108
118
|
world.configure(withNewline(source))
|
|
109
119
|
})
|
|
110
120
|
|
|
121
|
+
// Imported by the test runner itself, from the working directory: the module and
|
|
122
|
+
// the engine share one `@pmelab/gtd/flows`, and a config source string could
|
|
123
|
+
// import only files written into the in-memory repository.
|
|
124
|
+
Given("a gtd config from the module {string}", async (world: TestkitWorld, path: string) => {
|
|
125
|
+
world.configure({ ...(await import(pathToFileURL(resolve(path)).href)) })
|
|
126
|
+
})
|
|
127
|
+
|
|
111
128
|
Given("the gtd config ends with:", (world: TestkitWorld, source: string) => {
|
|
112
129
|
world.extendConfig(withNewline(source))
|
|
113
130
|
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: a scenario runs a workflow module from disk
|
|
3
|
+
|
|
4
|
+
The test runner imports the module, so it shares the engine's runtime and
|
|
5
|
+
may import anything on disk. A config written as source can import only
|
|
6
|
+
files inside the in-memory repository.
|
|
7
|
+
|
|
8
|
+
Scenario: the module's default export starts on a change in the tree
|
|
9
|
+
Given a gtd config from the module "src/gherkin/tiny.fixture.ts"
|
|
10
|
+
And a file "IDEA.md" with:
|
|
11
|
+
"""
|
|
12
|
+
a calculator
|
|
13
|
+
"""
|
|
14
|
+
And step "plan" writes "PLAN.md" with:
|
|
15
|
+
"""
|
|
16
|
+
add two numbers
|
|
17
|
+
"""
|
|
18
|
+
When the process is driven
|
|
19
|
+
Then the process rests at "sign-off"
|
|
20
|
+
And the commit subjects end with:
|
|
21
|
+
"""
|
|
22
|
+
gtd(agent): plan → sign-off
|
|
23
|
+
"""
|
|
24
|
+
|
package/src/platform/Git.test.ts
CHANGED
|
@@ -7,11 +7,11 @@ import { Cause, Effect, Exit, Option } from "effect"
|
|
|
7
7
|
import { describe, expect, it } from "vitest"
|
|
8
8
|
import { GitService, InvalidRefError, resolvedRefOrCorrupted } from "./Git.js"
|
|
9
9
|
import { Host } from "./index.js"
|
|
10
|
-
import { gitTiers, runGitServiceContract, runGitScriptContract } from "../
|
|
10
|
+
import { gitTiers, runGitServiceContract, runGitScriptContract } from "../twins/index.js"
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* The `GitOperations` contract, run identically against both the Live and
|
|
14
|
-
* InMemory tiers (`src/
|
|
14
|
+
* InMemory tiers (`src/twins/GitTiers.ts`) — 20 operations × 2 tiers.
|
|
15
15
|
*/
|
|
16
16
|
for (const makeTier of gitTiers) {
|
|
17
17
|
const name = makeTier().name
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Option } from "effect"
|
|
2
2
|
import { describe, expect, it, vi } from "vitest"
|
|
3
|
-
import { strictGitOperations as stubGit } from "../
|
|
3
|
+
import { strictGitOperations as stubGit } from "../twins/index.js"
|
|
4
4
|
import {
|
|
5
5
|
clearRetainedHistory,
|
|
6
6
|
HISTORY_REF,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Unit coverage for the retention edge module (`src/platform/RetainedHistory.ts`),
|
|
16
|
-
* using `strictGitOperations` (`src/
|
|
16
|
+
* using `strictGitOperations` (`src/twins/StrictGitOperations.ts`) as its
|
|
17
17
|
* `GitOperations` double — a Proxy failing on any method a test didn't
|
|
18
18
|
* override, so an unexpected call fails loudly instead of silently succeeding.
|
|
19
19
|
*/
|
|
@@ -15,7 +15,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"
|
|
|
15
15
|
import { Effect, Layer } from "effect"
|
|
16
16
|
import { NodeContext } from "@effect/platform-node"
|
|
17
17
|
import { GitService, Host, Workspace, type WorkspaceOps } from "./index.js"
|
|
18
|
-
import { InMemRepo, makeInMemoryWorkspaceOps } from "../
|
|
18
|
+
import { InMemRepo, makeInMemoryWorkspaceOps } from "../twins/index.js"
|
|
19
19
|
|
|
20
20
|
// `execFileSync` is a named export of the built-in `node:child_process` ESM
|
|
21
21
|
// module — not configurable, so `vi.spyOn` can't wrap it directly. `vi.mock`
|
|
@@ -31,7 +31,7 @@ vi.mock("node:child_process", async (importOriginal) => {
|
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* One `Workspace` tier, in the shape `src/
|
|
34
|
+
* One `Workspace` tier, in the shape `src/twins/GitTiers.ts` establishes
|
|
35
35
|
* for `GitOperations`: a `root`, a way to seed working-tree/committed
|
|
36
36
|
* content, and `provide` to run an Effect against this tier's `Workspace`.
|
|
37
37
|
*/
|
|
@@ -16,7 +16,7 @@ import { NodeContext } from "@effect/platform-node"
|
|
|
16
16
|
import { describe, expect, it } from "vitest"
|
|
17
17
|
import { loopLogPath, worktreeGitDir } from "./WorktreeState.js"
|
|
18
18
|
import { GitService, Host, Workspace } from "./index.js"
|
|
19
|
-
import { InMemRepo, testLayers } from "../
|
|
19
|
+
import { InMemRepo, testLayers } from "../twins/index.js"
|
|
20
20
|
|
|
21
21
|
const provide = <A>(
|
|
22
22
|
eff: Effect.Effect<A, never, Host | Workspace>,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import { describe, expect, it } from "vitest"
|
|
3
3
|
import { planStart, type StartOutcome } from "./planStart.js"
|
|
4
|
-
import { InMemRepo, testLayers } from "../
|
|
4
|
+
import { InMemRepo, testLayers } from "../twins/index.js"
|
|
5
5
|
import { ConfigService } from "../workflow/index.js"
|
|
6
6
|
|
|
7
7
|
const WORKFLOW = `import { agent, human } from "@pmelab/gtd/flows"
|
package/src/testkit/TestRepo.ts
CHANGED
|
@@ -2,10 +2,13 @@ import { Effect } from "effect"
|
|
|
2
2
|
import { PassThrough } from "node:stream"
|
|
3
3
|
import { runCli } from "../cli/index.js"
|
|
4
4
|
import { EXIT_OK, type BeatDocument } from "../wire/index.js"
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import {
|
|
6
|
+
applyEmittedScript,
|
|
7
|
+
applyFlowScript,
|
|
8
|
+
InMemRepo,
|
|
9
|
+
makeCapturingCliIo,
|
|
10
|
+
type CapturedCliResult,
|
|
11
|
+
} from "../twins/index.js"
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* The repository's `gtd.config.ts`: its source, or the module itself. A module
|
package/src/testkit/index.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
// The public testkit: runs any workflow against the real engine in an in-memory
|
|
2
|
+
// repository. It imports no test runner, so any runner can use it; the
|
|
3
|
+
// gherkin steps are the opt-in \`@pmelab/gtd/testkit/gherkin\`.
|
|
4
|
+
export {
|
|
5
|
+
runsItsScript,
|
|
6
|
+
testRepo,
|
|
7
|
+
type Actor,
|
|
8
|
+
type Actors,
|
|
9
|
+
type DriveOptions,
|
|
10
|
+
type TestRepo,
|
|
11
|
+
type TestRepoOptions,
|
|
12
|
+
type WorkflowModule,
|
|
13
|
+
} from "./TestRepo.js"
|
|
14
|
+
export { InMemRepo, type CapturedCliResult } from "../twins/index.js"
|
|
15
|
+
export type { TestkitWorld } from "../gherkin/index.js"
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// gtd itself spawns no subprocess at all any more (`gtd renders; the driver
|
|
5
5
|
// executes`) — a mode's `format:`/`validate:` command is emitted text, never
|
|
6
6
|
// run in-process. `ScriptedCommand` below still exists for the world's own
|
|
7
|
-
// fake-shell interpreter (`src/
|
|
7
|
+
// fake-shell interpreter (`src/twins/EmittedScriptRecognizer.ts`), which
|
|
8
8
|
// simulates a driver running gtd's emitted scripts against the in-memory
|
|
9
9
|
// worktree — a wholly separate mechanism from any gtd-internal layer.
|
|
10
10
|
|
|
@@ -193,7 +193,7 @@ export interface TestWorldOptions {
|
|
|
193
193
|
* Effect test has no `--verbose` flag of its own to gate on.
|
|
194
194
|
*/
|
|
195
195
|
readonly narrate?: (line: string) => void
|
|
196
|
-
/** Gates `narrate` above — defaults to `true` (fires whenever a sink is given) since these tests aren't exercising `Cli.ts`'s own `--verbose` gate. `src/
|
|
196
|
+
/** Gates `narrate` above — defaults to `true` (fires whenever a sink is given) since these tests aren't exercising `Cli.ts`'s own `--verbose` gate. `src/twins/cliIo.ts` overrides this with the real decoded flag. */
|
|
197
197
|
readonly verbose?: boolean
|
|
198
198
|
/** Real or fake driver and tool commands — each one left out fails when called. */
|
|
199
199
|
readonly tools?: Partial<ToolCommands["Type"]>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { InMemRepo } from "./InMemRepo.js"
|
|
2
|
+
export { makeInMemoryWorkspaceOps, testLayers, type ScriptedCommand } from "./Layers.js"
|
|
3
|
+
export { makeCapturingCliIo, type CapturedCliResult } from "./cliIo.js"
|
|
4
|
+
export { applyEmittedScript } from "./EmittedScriptRecognizer.js"
|
|
5
|
+
export { applyFlowScript } from "./FlowScripts.js"
|
|
6
|
+
export { gitTiers, runGitServiceContract, runGitScriptContract, type GitTier } from "./GitTiers.js"
|
|
7
|
+
export { strictGitOperations } from "./StrictGitOperations.js"
|
|
8
|
+
export { fakeGitOperations } from "./FakeGitOperations.js"
|
|
@@ -6,7 +6,7 @@ import { NodeContext } from "@effect/platform-node"
|
|
|
6
6
|
import { Effect, Layer } from "effect"
|
|
7
7
|
import { afterEach, describe, expect, it } from "vitest"
|
|
8
8
|
import { GitService, Host, Narrator, Workspace } from "../platform/index.js"
|
|
9
|
-
import { InMemRepo, testLayers } from "../
|
|
9
|
+
import { InMemRepo, testLayers } from "../twins/index.js"
|
|
10
10
|
import { ConfigDiscovery, ConfigService } from "./index.js"
|
|
11
11
|
|
|
12
12
|
// A config and the local modules it imports load the same from disk, where
|
|
@@ -38,7 +38,7 @@ export const WORKFLOW_MODULE = "gtd.config.ts"
|
|
|
38
38
|
* The file names gtd read repository settings from before `gtd.config.ts` took
|
|
39
39
|
* them over. Still looked for, only to refuse them: a leftover one would
|
|
40
40
|
* otherwise be silently ignored. Shared with the in-memory counterpart in
|
|
41
|
-
* `src/
|
|
41
|
+
* `src/twins/Layers.ts`, so both tiers refuse the same names.
|
|
42
42
|
*/
|
|
43
43
|
export const LEGACY_RC_FILES = [
|
|
44
44
|
".gtdrc",
|
|
@@ -71,7 +71,7 @@ const legacyRcFileAt = (path: string): LegacyRcFile | undefined => {
|
|
|
71
71
|
* the user's home dir (inclusive, when it is an ancestor) or after reaching the
|
|
72
72
|
* filesystem root — whichever comes first. Returned innermost→outermost.
|
|
73
73
|
* Exported (via the barrel) for the same reason `SEARCH_PLACES` is — one
|
|
74
|
-
* shared implementation, not a second copy in `src/
|
|
74
|
+
* shared implementation, not a second copy in `src/twins/Layers.ts` that
|
|
75
75
|
* could silently diverge.
|
|
76
76
|
*/
|
|
77
77
|
export const walkUp = (from: string, home: string): ReadonlyArray<string> => {
|
|
@@ -115,7 +115,7 @@ const workflowModule = (
|
|
|
115
115
|
* reads; discovery is effectful and environment-dependent: `.Live` reads
|
|
116
116
|
* REAL disk, so an `@inmem` scenario's fake
|
|
117
117
|
* `Workspace` — a pure in-memory `Map`, no real files anywhere on disk —
|
|
118
|
-
* cannot back it. `src/
|
|
118
|
+
* cannot back it. `src/twins/Layers.ts` provides the in-memory
|
|
119
119
|
* counterpart instead, mirroring how `Workspace`/`Host`/`GitService` already
|
|
120
120
|
* each have a production and a fake layer behind one Tag.
|
|
121
121
|
*/
|
package/src/workflow/load.ts
CHANGED
|
@@ -357,14 +357,14 @@ const shippedModules = (): Promise<ShippedModules> =>
|
|
|
357
357
|
return { modules, missing }
|
|
358
358
|
})())
|
|
359
359
|
|
|
360
|
-
/** The file each subpath a config may import serves: all but `flows`, the engine's own, and
|
|
360
|
+
/** The file each subpath a config may import serves: all but `flows`, the engine's own, and the testkit's. */
|
|
361
361
|
const subpathFiles = (condition: string): ReadonlyMap<string, string> => {
|
|
362
362
|
const { root, pkg } = ownPackage()
|
|
363
363
|
const files = new Map<string, string>()
|
|
364
364
|
for (const [subpath, target] of Object.entries(pkg.exports)) {
|
|
365
365
|
const file = typeof target === "string" ? target : target[condition]
|
|
366
|
-
//
|
|
367
|
-
if (subpath === "./flows" || subpath
|
|
366
|
+
// A config runs no tests, and the gherkin steps import the consumer's test runner.
|
|
367
|
+
if (subpath === "./flows" || subpath.startsWith("./testkit") || file === undefined) continue
|
|
368
368
|
files.set(`${PACKAGE}/${subpath.replace(/^\.\//, "")}`, join(root, file))
|
|
369
369
|
}
|
|
370
370
|
return files
|
|
@@ -39,6 +39,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
39
39
|
|
|
40
40
|
- the greeting module
|
|
41
41
|
|
|
42
|
+
## Release Flag
|
|
43
|
+
|
|
44
|
+
new
|
|
45
|
+
|
|
42
46
|
## Acceptance Criteria
|
|
43
47
|
|
|
44
48
|
- AC-1: `greeting` reads "Hello, world".
|
|
@@ -67,6 +71,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
67
71
|
|
|
68
72
|
- the greeting module
|
|
69
73
|
|
|
74
|
+
## Release Flag
|
|
75
|
+
|
|
76
|
+
new
|
|
77
|
+
|
|
70
78
|
## Acceptance Criteria
|
|
71
79
|
|
|
72
80
|
- `greeting` reads "Hello, world".
|
|
@@ -82,6 +90,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
82
90
|
|
|
83
91
|
- the greeting module
|
|
84
92
|
|
|
93
|
+
## Release Flag
|
|
94
|
+
|
|
95
|
+
new
|
|
96
|
+
|
|
85
97
|
## Acceptance Criteria
|
|
86
98
|
|
|
87
99
|
- AC-1: `greeting` reads "Hello, world".
|
|
@@ -98,6 +110,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
98
110
|
|
|
99
111
|
- the greeting module
|
|
100
112
|
|
|
113
|
+
## Release Flag
|
|
114
|
+
|
|
115
|
+
new
|
|
116
|
+
|
|
101
117
|
## Acceptance Criteria
|
|
102
118
|
|
|
103
119
|
- AC-1: `greeting` reads "Hello, world".
|
|
@@ -131,6 +147,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
131
147
|
|
|
132
148
|
- the greeting module
|
|
133
149
|
|
|
150
|
+
## Release Flag
|
|
151
|
+
|
|
152
|
+
new
|
|
153
|
+
|
|
134
154
|
## Acceptance Criteria
|
|
135
155
|
|
|
136
156
|
- AC-1: `greeting` reads "Hi".
|
|
@@ -150,6 +170,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
150
170
|
"""
|
|
151
171
|
# Requirements
|
|
152
172
|
|
|
173
|
+
## Release Flag
|
|
174
|
+
|
|
175
|
+
new
|
|
176
|
+
|
|
153
177
|
## Acceptance Criteria
|
|
154
178
|
|
|
155
179
|
- AC-1: `greeting` reads "Hello, world".
|
|
@@ -178,6 +202,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
178
202
|
|
|
179
203
|
- the greeting module
|
|
180
204
|
|
|
205
|
+
## Release Flag
|
|
206
|
+
|
|
207
|
+
new
|
|
208
|
+
|
|
181
209
|
## Acceptance Criteria
|
|
182
210
|
|
|
183
211
|
- AC-1: `greeting` reads "Hi".
|
|
@@ -226,6 +254,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
|
|
|
226
254
|
|
|
227
255
|
- the greeting module
|
|
228
256
|
|
|
257
|
+
## Release Flag
|
|
258
|
+
|
|
259
|
+
new
|
|
260
|
+
|
|
229
261
|
## Acceptance Criteria
|
|
230
262
|
|
|
231
263
|
- AC-1: `greeting` reads "Hello, world".
|
|
@@ -8,7 +8,6 @@ Feature: the criteria-coverage lens checks the change's tests against its criter
|
|
|
8
8
|
Background:
|
|
9
9
|
Given the shipped workflows, as gtd install scaffolds them
|
|
10
10
|
And an environment variable "GTD_QUALITYREVIEWS" set to "criteria-coverage"
|
|
11
|
-
And an environment variable "GTD_FLAGMODE" set to "false"
|
|
12
11
|
And a file "src/calc.ts" with:
|
|
13
12
|
"""
|
|
14
13
|
export const add = (a: number, b: number) => a + b
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { HEADING, ITEM, markFenced } from "../shared/fences.js"
|
|
14
14
|
import { planner, REQUIREMENTS } from "../shared/steps.js"
|
|
15
15
|
import * as t from "../shared/text.js"
|
|
16
|
+
import { requireReleaseFlag } from "./release-flag.js"
|
|
16
17
|
|
|
17
18
|
export interface Criterion {
|
|
18
19
|
readonly id: string
|
|
@@ -137,16 +138,20 @@ export const requireCriteria = (file: string): void => {
|
|
|
137
138
|
const isImpactHeading = (h: RegExpExecArray | null): boolean =>
|
|
138
139
|
h !== null && h[1]!.length === 2 && /^impact$/i.test(h[2]!.trim())
|
|
139
140
|
|
|
141
|
+
// The skip path's impact turn may write both: a sketch can ask for a flag.
|
|
142
|
+
const isImpactTurnHeading = (h: RegExpExecArray | null): boolean =>
|
|
143
|
+
h !== null && h[1]!.length === 2 && /^(impact|release flag)$/i.test(h[2]!.trim())
|
|
144
|
+
|
|
140
145
|
/** Whether `text` has an unfenced, level-2 `## Impact` heading. */
|
|
141
146
|
export const hasImpact = (text: string): boolean =>
|
|
142
147
|
markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)))
|
|
143
148
|
|
|
144
|
-
/** `text` without its `## Impact`
|
|
149
|
+
/** `text` without its `## Impact` and `## Release Flag` sections, whitespace collapsed. */
|
|
145
150
|
const withoutImpact = (text: string): string => {
|
|
146
151
|
let inImpact = false
|
|
147
152
|
const kept = markFenced(text).filter((l) => {
|
|
148
153
|
const h = l.fenced ? null : HEADING.exec(l.line)
|
|
149
|
-
if (h !== null && h[1]!.length <= 2) inImpact =
|
|
154
|
+
if (h !== null && h[1]!.length <= 2) inImpact = isImpactTurnHeading(h)
|
|
150
155
|
return !inImpact
|
|
151
156
|
})
|
|
152
157
|
return kept
|
|
@@ -172,13 +177,13 @@ export const requireImpact = (file: string): void => {
|
|
|
172
177
|
)
|
|
173
178
|
}
|
|
174
179
|
|
|
175
|
-
/** Refuse a turn that changed `file` beyond its `## Impact`
|
|
180
|
+
/** Refuse a turn that changed `file` beyond its `## Impact` and `## Release Flag` sections. */
|
|
176
181
|
export const requireImpactOnly = (file: string): void => {
|
|
177
182
|
const change = changes().get(file)
|
|
178
183
|
if (change === undefined) return
|
|
179
184
|
if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return
|
|
180
185
|
refuse(
|
|
181
|
-
`gtd land: impact-only: this turn may only add "## Impact" to ${file} — restore everything else`,
|
|
186
|
+
`gtd land: impact-only: this turn may only add "## Impact" (and "## Release Flag") to ${file} — restore everything else`,
|
|
182
187
|
)
|
|
183
188
|
}
|
|
184
189
|
|
|
@@ -240,6 +245,7 @@ const impact = async (): Promise<void> => {
|
|
|
240
245
|
requireCriteriaFrozen(REQUIREMENTS)
|
|
241
246
|
requireImpact(REQUIREMENTS)
|
|
242
247
|
requireImpactOnly(REQUIREMENTS)
|
|
248
|
+
requireReleaseFlag(REQUIREMENTS)
|
|
243
249
|
}
|
|
244
250
|
|
|
245
251
|
/**
|
|
@@ -302,38 +302,9 @@ describe("an in-flight process from before the architecture path was removed", (
|
|
|
302
302
|
})
|
|
303
303
|
})
|
|
304
304
|
|
|
305
|
-
describe("
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
'import { feature } from "@pmelab/gtd/workflows/feature"',
|
|
309
|
-
'export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"',
|
|
310
|
-
`export default ${workflow}`,
|
|
311
|
-
"",
|
|
312
|
-
].join("\n")
|
|
313
|
-
|
|
314
|
-
const designed = async (workflow: string, env: Record<string, string> = {}) => {
|
|
315
|
-
const repo = testRepo({ workflow, env: { GTD_QUALITYREVIEWS: "", ...env } })
|
|
316
|
-
repo.write("src/calc.ts", CALC)
|
|
317
|
-
repo.commit("feat: add calculator")
|
|
318
|
-
repo.write("TODO.md", "# Add subtract\n")
|
|
319
|
-
return repo.drive({
|
|
320
|
-
unwind: () => undefined,
|
|
321
|
-
"start-gate.check": () => undefined,
|
|
322
|
-
"design.triage": (_, r) =>
|
|
323
|
-
r.write(
|
|
324
|
-
".gtd/REQUIREMENTS.md",
|
|
325
|
-
"# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
326
|
-
),
|
|
327
|
-
"design.gate.answer": () => undefined,
|
|
328
|
-
})
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
it("set on explicitly, runs the naive pass like the default", async () => {
|
|
332
|
-
expect((await designed(config("feature({ flagMode: true })"))).state).toBe("seam.naive")
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
it("off through GTD_FLAGMODE=false, lands the naive pass with no seam judge", async () => {
|
|
336
|
-
const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_FLAGMODE: "false" } })
|
|
305
|
+
describe("release flag", () => {
|
|
306
|
+
it("with no Release Flag section, lands the naive pass with no seam judge", async () => {
|
|
307
|
+
const repo = testRepo({ workflow: CONFIG_SCAFFOLD })
|
|
337
308
|
repo.write("src/calc.ts", CALC)
|
|
338
309
|
repo.commit("feat: add calculator")
|
|
339
310
|
repo.write("TODO.md", "# Add subtract\n")
|
|
@@ -357,13 +328,6 @@ describe("flagMode", () => {
|
|
|
357
328
|
expect(repo.read("src/calc.ts")).toBe(`${CALC}// naive\n`)
|
|
358
329
|
expect(repo.subjects().some((subject) => subject.includes("seam.judge"))).toBe(false)
|
|
359
330
|
})
|
|
360
|
-
|
|
361
|
-
it("refuses a value other than true or false before the process starts", async () => {
|
|
362
|
-
const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_FLAGMODE: "off" } })
|
|
363
|
-
repo.write("TODO.md", "# Add subtract\n")
|
|
364
|
-
await expect(repo.drive({})).rejects.toThrow('flagMode must be "true" or "false", got "off"')
|
|
365
|
-
expect(repo.subjects()).toEqual(["chore: initial commit"])
|
|
366
|
-
})
|
|
367
331
|
})
|
|
368
332
|
|
|
369
333
|
// pins the public re-exports from the feature entry point
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
start,
|
|
10
10
|
} from "../../flows/index.js"
|
|
11
11
|
import { gate } from "../shared/health.js"
|
|
12
|
-
import {
|
|
12
|
+
import { naiveTestOn, shipped } from "../shared/options.js"
|
|
13
13
|
import { FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
|
|
14
14
|
import { refusePendingHandOff } from "../shared/tail.js"
|
|
15
15
|
import * as t from "../shared/text.js"
|
|
@@ -29,7 +29,7 @@ import { recordRisk, riskTierOn } from "./tier.js"
|
|
|
29
29
|
// The ordinary start. Any change to the tree starts a process: `idle` →
|
|
30
30
|
// `unwind` reverts the sketch (its intent survives in history) → a
|
|
31
31
|
// green-baseline gate → design → a naive pass, tried by a person with
|
|
32
|
-
// `naiveTest` on and judged
|
|
32
|
+
// `naiveTest` on and judged when the requirements ask for a release flag: it
|
|
33
33
|
// lands as the implementation, or (refactoring first when the judge asks) is
|
|
34
34
|
// built fresh from → the health loop → the triaged quality lap and its finding gate → a
|
|
35
35
|
// reviewer's walkthrough of the whole change for the human, which signs off
|
|
@@ -46,7 +46,7 @@ export const unwind = (): Promise<void> => {
|
|
|
46
46
|
export { reUnwind }
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* A naive pass (judged only
|
|
49
|
+
* A naive pass (judged only when a release flag is asked for), then implement and the
|
|
50
50
|
* quality lap, then the review; design must have run since `firstBase`.
|
|
51
51
|
*/
|
|
52
52
|
export const buildFeature = async (firstBase: string): Promise<void> => {
|
|
@@ -139,7 +139,6 @@ export const ordinaryStart = async (): Promise<void> => {
|
|
|
139
139
|
await human("idle", { message: t.idleMessage(), label: "Idle", file: ".gtd/TODO.md" })
|
|
140
140
|
refusePendingHandOff()
|
|
141
141
|
// Read right after idle, so a bad value refuses the landing that would pin it.
|
|
142
|
-
flagModeOn()
|
|
143
142
|
naiveTestOn()
|
|
144
143
|
const stated = statedCriteria(changes(), head())
|
|
145
144
|
await unwind()
|
|
@@ -11,7 +11,6 @@ Feature: each quality lens runs on its own model hint
|
|
|
11
11
|
import { feature } from "@pmelab/gtd/workflows/feature"
|
|
12
12
|
export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
|
|
13
13
|
export default feature({
|
|
14
|
-
flagMode: false,
|
|
15
14
|
qualityReviews: "correctness, conventions, test-audit=opus",
|
|
16
15
|
})
|
|
17
16
|
"""
|
|
@@ -12,7 +12,6 @@ Feature: the quality lap's findings are triaged, then decided at the finding gat
|
|
|
12
12
|
Background:
|
|
13
13
|
Given the shipped workflows, as gtd install scaffolds them
|
|
14
14
|
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
|
|
15
|
-
And an environment variable "GTD_FLAGMODE" set to "false"
|
|
16
15
|
And a file "src/calc.ts" with:
|
|
17
16
|
"""
|
|
18
17
|
export const add = (a: number, b: number) => a + b
|
|
@@ -4,19 +4,20 @@ Feature: a person tries the naive pass by hand before anything builds on it
|
|
|
4
4
|
`naiveTest` is a factory option of the shipped workflows, off by default. On,
|
|
5
5
|
every green naive pass is applied to the working tree and the process rests
|
|
6
6
|
at `seam.try`. A clean landing signs it off and the process goes on — to the
|
|
7
|
-
seam judge
|
|
7
|
+
seam judge when the requirements ask for a release flag, else straight to the
|
|
8
|
+
implementation. Notes in
|
|
8
9
|
`.gtd/NAIVE-TEST.md`, a code edit or a requirements edit send it back: the
|
|
9
10
|
pass and the person's code edits leave the tree again, and the next naive
|
|
10
11
|
pass gets the notes and the edits. Every lap ends at the gate again.
|
|
11
12
|
|
|
12
|
-
Rule: with
|
|
13
|
+
Rule: with no release flag asked for
|
|
13
14
|
|
|
14
15
|
Background:
|
|
15
16
|
Given a gtd config with:
|
|
16
17
|
"""
|
|
17
18
|
import { feature } from "@pmelab/gtd/workflows/feature"
|
|
18
19
|
export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
|
|
19
|
-
export default feature({
|
|
20
|
+
export default feature({ naiveTest: true })
|
|
20
21
|
"""
|
|
21
22
|
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
22
23
|
And a file "src/calc.ts" with:
|
|
@@ -177,7 +178,7 @@ Feature: a person tries the naive pass by hand before anything builds on it
|
|
|
177
178
|
gtd(check): seam.applying → seam.try
|
|
178
179
|
"""
|
|
179
180
|
|
|
180
|
-
Rule: with
|
|
181
|
+
Rule: with a release flag asked for
|
|
181
182
|
|
|
182
183
|
Background:
|
|
183
184
|
Given a gtd config with:
|
|
@@ -208,7 +209,11 @@ Feature: a person tries the naive pass by hand before anything builds on it
|
|
|
208
209
|
|
|
209
210
|
- the calculator module
|
|
210
211
|
|
|
211
|
-
##
|
|
212
|
+
## Release Flag
|
|
213
|
+
|
|
214
|
+
new
|
|
215
|
+
|
|
216
|
+
## Acceptance Criteria
|
|
212
217
|
|
|
213
218
|
- AC-1: subtract(a, b) returns a - b.
|
|
214
219
|
"""
|
|
@@ -226,7 +231,7 @@ Feature: a person tries the naive pass by hand before anything builds on it
|
|
|
226
231
|
And step "seam.applying" runs its script
|
|
227
232
|
And step "seam.restoring" runs its script
|
|
228
233
|
|
|
229
|
-
Scenario:
|
|
234
|
+
Scenario: the person tries the pass before the seam judge rules
|
|
230
235
|
When the process is driven
|
|
231
236
|
Then the process rests at "seam.try"
|
|
232
237
|
And the resting step's content contains "the seam judge rules on its shape"
|