@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +115 -390
- package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
- package/dist/gtd.bundle.mjs +76 -59
- package/dist/testkit/gherkin.mjs +30 -2
- package/dist/testkit.mjs +1 -1
- package/package.json +3 -42
- package/skills/authoring/SKILL.md +31 -25
- package/src/cli/Cli.test.ts +15 -19
- package/src/cli/Cli.ts +2 -4
- package/src/cli/Program.test.ts +60 -29
- package/src/cli/Program.ts +5 -7
- package/src/edge/Edge.ts +3 -3
- package/src/flags/index.test.ts +0 -13
- package/src/flags/index.ts +1 -3
- package/src/gherkin/git-control.feature +78 -0
- package/src/gherkin/index.ts +35 -1
- package/src/install/Install.test.ts +14 -11
- package/src/install/Install.ts +38 -15
- package/src/install/index.ts +1 -2
- package/src/replay/Replay.test.ts +1 -1
- package/src/replay/index.ts +1 -1
- package/src/step/planStart.ts +6 -1
- package/src/testkit/TestRepo.test.ts +118 -2
- package/src/testkit/TestRepo.ts +57 -6
- package/src/testkit/index.ts +1 -0
- package/src/twins/EmittedScriptRecognizer.ts +12 -14
- package/src/twins/FlowScripts.ts +10 -9
- package/src/twins/InMemRepo.ts +26 -1
- package/src/twins/Layers.ts +12 -1
- package/src/twins/index.ts +6 -1
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +138 -113
- package/src/workflow/load.ts +1 -1
- package/src/workflow/vars.test.ts +30 -3
- package/src/workflow/vars.ts +34 -2
- package/src/workflow-kit/index.ts +1 -1
- package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
- package/dist/chunks/review-Bfaror38.mjs +0 -427
- package/dist/chunks/tail-Bd7L940p.mjs +0 -355
- package/dist/chunks/testkit-D1-auYau.mjs +0 -10362
- package/dist/workflows/cleanup.mjs +0 -36
- package/dist/workflows/feature.mjs +0 -1018
- package/dist/workflows/fix.mjs +0 -41
- package/dist/workflows/review.mjs +0 -45
- package/dist/workflows/rollout.mjs +0 -48
- package/dist/workflows.mjs +0 -40651
- package/src/install/ConfigScaffold.ts +0 -22
- package/src/workflows/cleanup/cleanup-path.feature +0 -94
- package/src/workflows/cleanup/cleanup.feature +0 -315
- package/src/workflows/cleanup/cleanup.test.ts +0 -276
- package/src/workflows/cleanup/cleanup.ts +0 -52
- package/src/workflows/cleanup/index.ts +0 -6
- package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
- package/src/workflows/feature/acceptance-criteria.feature +0 -389
- package/src/workflows/feature/criteria-coverage.feature +0 -102
- package/src/workflows/feature/criteria.test.ts +0 -147
- package/src/workflows/feature/criteria.ts +0 -267
- package/src/workflows/feature/fast-path.feature +0 -93
- package/src/workflows/feature/feature.test.ts +0 -338
- package/src/workflows/feature/feature.ts +0 -156
- package/src/workflows/feature/green.ts +0 -19
- package/src/workflows/feature/index.ts +0 -18
- package/src/workflows/feature/lens-models.feature +0 -69
- package/src/workflows/feature/lens-review.feature +0 -566
- package/src/workflows/feature/naive-acceptance.feature +0 -147
- package/src/workflows/feature/naive-test.feature +0 -263
- package/src/workflows/feature/one-step-land.feature +0 -93
- package/src/workflows/feature/planning.ts +0 -41
- package/src/workflows/feature/refactor-absorbs.feature +0 -131
- package/src/workflows/feature/refactor-first.feature +0 -158
- package/src/workflows/feature/refactor-lens-models.feature +0 -79
- package/src/workflows/feature/refactor-rebuild.feature +0 -273
- package/src/workflows/feature/refactor-review.feature +0 -1528
- package/src/workflows/feature/refactor.test.ts +0 -1220
- package/src/workflows/feature/refactor.ts +0 -368
- package/src/workflows/feature/release-flag.feature +0 -167
- package/src/workflows/feature/release-flag.test.ts +0 -49
- package/src/workflows/feature/release-flag.ts +0 -46
- package/src/workflows/feature/review-gate.feature +0 -241
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
- package/src/workflows/feature/risk-tier.feature +0 -351
- package/src/workflows/feature/seam-rejection.feature +0 -146
- package/src/workflows/feature/seam.feature +0 -297
- package/src/workflows/feature/seam.test.ts +0 -527
- package/src/workflows/feature/seam.ts +0 -426
- package/src/workflows/feature/standards.feature +0 -354
- package/src/workflows/feature/steps.test.ts +0 -65
- package/src/workflows/feature/steps.ts +0 -45
- package/src/workflows/feature/tier-pin.feature +0 -66
- package/src/workflows/feature/tier.test.ts +0 -166
- package/src/workflows/feature/tier.ts +0 -56
- package/src/workflows/fix/fix-lens-origin.feature +0 -114
- package/src/workflows/fix/fix-quality.feature +0 -212
- package/src/workflows/fix/fix.feature +0 -79
- package/src/workflows/fix/fix.test.ts +0 -74
- package/src/workflows/fix/fix.ts +0 -52
- package/src/workflows/fix/index.ts +0 -6
- package/src/workflows/index.ts +0 -47
- package/src/workflows/review/index.ts +0 -6
- package/src/workflows/review/review-door.feature +0 -199
- package/src/workflows/review/review.test.ts +0 -27
- package/src/workflows/review/review.ts +0 -60
- package/src/workflows/rollout/index.ts +0 -6
- package/src/workflows/rollout/rollout.feature +0 -510
- package/src/workflows/rollout/rollout.test.ts +0 -263
- package/src/workflows/rollout/rollout.ts +0 -82
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
- package/src/workflows/shared/access.test.ts +0 -62
- package/src/workflows/shared/access.ts +0 -24
- package/src/workflows/shared/config.fixture.ts +0 -12
- package/src/workflows/shared/fences.ts +0 -24
- package/src/workflows/shared/health.test.ts +0 -148
- package/src/workflows/shared/health.ts +0 -191
- package/src/workflows/shared/learn.test.ts +0 -140
- package/src/workflows/shared/learn.ts +0 -201
- package/src/workflows/shared/lenses.test.ts +0 -39
- package/src/workflows/shared/lenses.ts +0 -294
- package/src/workflows/shared/options.test.ts +0 -157
- package/src/workflows/shared/options.ts +0 -92
- package/src/workflows/shared/progress.feature +0 -124
- package/src/workflows/shared/prose.ts +0 -263
- package/src/workflows/shared/recheck.feature +0 -60
- package/src/workflows/shared/registry.test.ts +0 -345
- package/src/workflows/shared/registry.ts +0 -183
- package/src/workflows/shared/reserved.ts +0 -11
- package/src/workflows/shared/review.test.ts +0 -1062
- package/src/workflows/shared/review.ts +0 -446
- package/src/workflows/shared/skills.test.ts +0 -121
- package/src/workflows/shared/skills.ts +0 -49
- package/src/workflows/shared/standards.test.ts +0 -90
- package/src/workflows/shared/standards.ts +0 -156
- package/src/workflows/shared/steps.test.ts +0 -219
- package/src/workflows/shared/steps.ts +0 -201
- package/src/workflows/shared/tail.test.ts +0 -454
- package/src/workflows/shared/tail.ts +0 -264
- package/src/workflows/shared/text.fixture.ts +0 -128
- package/src/workflows/shared/text.test.ts +0 -1430
- package/src/workflows/shared/text.ts +0 -1700
- package/src/workflows/shared/threads.test.ts +0 -129
- package/src/workflows/shared/vars.ts +0 -47
- package/src/workflows/shared/workflows.steps.ts +0 -54
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
resolveVars,
|
|
16
16
|
seededValidateCommand,
|
|
17
17
|
} from "./index.js"
|
|
18
|
-
import { CONFIG_SCAFFOLD } from "../install/index.js"
|
|
19
18
|
|
|
20
19
|
// Every real port `ConfigService.Live` needs, scoped to `dir` — `home` is a
|
|
21
20
|
// fresh directory above the project, so the walk stops there as it would in a
|
|
@@ -71,12 +70,109 @@ const loadAndResolve = (dir: string = projectDir, vars: Env = {}) =>
|
|
|
71
70
|
let homeDir: string
|
|
72
71
|
let projectDir: string
|
|
73
72
|
|
|
74
|
-
//
|
|
73
|
+
// A config whose default workflow rests at `idle`, declaring a process
|
|
74
|
+
// setting, an environment setting and the scopes its turns run under.
|
|
75
|
+
const BASE_CONFIG = `import { human } from "@pmelab/gtd/flows"
|
|
76
|
+
|
|
77
|
+
export default async () => {
|
|
78
|
+
await human("idle", { message: "hi" })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const defaults = { qualityReviews: "" }
|
|
82
|
+
export const envDefaults = { testCommand: "npm test" }
|
|
83
|
+
|
|
84
|
+
export const skills = (vars) => ({
|
|
85
|
+
build: [],
|
|
86
|
+
"build.fix.quality": [],
|
|
87
|
+
"build.review": [],
|
|
88
|
+
...Object.fromEntries(
|
|
89
|
+
(vars.qualityReviews ?? "")
|
|
90
|
+
.split(",")
|
|
91
|
+
.map((lens) => lens.trim())
|
|
92
|
+
.filter((lens) => lens !== "")
|
|
93
|
+
.map((lens) => [\`build.quality.\${lens}\`, []]),
|
|
94
|
+
),
|
|
95
|
+
})
|
|
96
|
+
`
|
|
97
|
+
|
|
98
|
+
// Workflow factories a config imports from a sibling module: string options
|
|
99
|
+
// with defaults, an unknown or non-string option being an issue.
|
|
100
|
+
const FACTORIES = `import { defineWorkflow, human } from "@pmelab/gtd/flows"
|
|
101
|
+
|
|
102
|
+
const settings = (known) => ({
|
|
103
|
+
"~standard": {
|
|
104
|
+
version: 1,
|
|
105
|
+
vendor: "test",
|
|
106
|
+
validate: (value) => {
|
|
107
|
+
const issues = Object.entries(value).flatMap(([key, given]) =>
|
|
108
|
+
!Object.hasOwn(known, key)
|
|
109
|
+
? [{ path: [key], message: \`is not an option — options: \${Object.keys(known).join(", ")}\` }]
|
|
110
|
+
: typeof given === "string"
|
|
111
|
+
? []
|
|
112
|
+
: [{ path: [key], message: "must be a string" }],
|
|
113
|
+
)
|
|
114
|
+
return issues.length > 0 ? { issues } : { value: { ...known, ...value } }
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
export const feature = defineWorkflow(
|
|
120
|
+
settings({ qualityReviews: "", reviewNoteActionable: "0.7", testCommand: "npm test" }),
|
|
121
|
+
({ testCommand, ...defaults }) => ({
|
|
122
|
+
flow: async () => {
|
|
123
|
+
await human("idle")
|
|
124
|
+
},
|
|
125
|
+
defaults,
|
|
126
|
+
envDefaults: { testCommand },
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
export const review = defineWorkflow(settings({ reviewNoteActionable: "0.7" }), (defaults) => ({
|
|
131
|
+
flow: async () => {
|
|
132
|
+
await human("review-gate")
|
|
133
|
+
},
|
|
134
|
+
defaults: { ...defaults, reviewBase: "" },
|
|
135
|
+
doors: {
|
|
136
|
+
review: {
|
|
137
|
+
args: [{ name: "base", optional: true }],
|
|
138
|
+
vars: ({ base }) => (base === undefined ? {} : { reviewBase: base }),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
base: (_workflow, vars) => vars.reviewBase || "",
|
|
142
|
+
}))
|
|
143
|
+
|
|
144
|
+
export const fix = defineWorkflow(settings({ judgeBudgetBytes: "65536" }), (defaults) => ({
|
|
145
|
+
flow: async () => {
|
|
146
|
+
await human("fix-precheck")
|
|
147
|
+
},
|
|
148
|
+
defaults,
|
|
149
|
+
doors: { fix: {} },
|
|
150
|
+
}))
|
|
151
|
+
|
|
152
|
+
export const rollout = defineWorkflow(settings({ flagRegistry: "flags.ts" }), (defaults) => ({
|
|
153
|
+
flow: async () => {
|
|
154
|
+
await human("flipping")
|
|
155
|
+
},
|
|
156
|
+
defaults: { ...defaults, flag: "" },
|
|
157
|
+
doors: { rollout: { args: [{ name: "flag" }], vars: ({ flag }) => ({ flag: flag ?? "" }) } },
|
|
158
|
+
}))
|
|
159
|
+
|
|
160
|
+
export const unwind = async () => {
|
|
161
|
+
await human("unwinding")
|
|
162
|
+
}
|
|
163
|
+
`
|
|
164
|
+
|
|
165
|
+
/** Writes `config` as gtd.config.ts beside {@link FACTORIES} as `workflows.ts`. */
|
|
166
|
+
const withFactories = (config: string): void => {
|
|
167
|
+
writeFileSync(join(projectDir, "workflows.ts"), FACTORIES)
|
|
168
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), config)
|
|
169
|
+
}
|
|
170
|
+
|
|
75
171
|
beforeEach(() => {
|
|
76
172
|
homeDir = mkdtempSync(join(tmpdir(), "gtd-config-"))
|
|
77
173
|
projectDir = join(homeDir, "project")
|
|
78
174
|
mkdirSync(projectDir)
|
|
79
|
-
writeFileSync(join(projectDir, "gtd.config.ts"),
|
|
175
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), BASE_CONFIG)
|
|
80
176
|
})
|
|
81
177
|
|
|
82
178
|
afterEach(() => {
|
|
@@ -97,11 +193,11 @@ const minimalWorkflow = (first: string) =>
|
|
|
97
193
|
const withConfig = (config: string) =>
|
|
98
194
|
writeFileSync(
|
|
99
195
|
join(projectDir, "gtd.config.ts"),
|
|
100
|
-
`${
|
|
196
|
+
`${BASE_CONFIG}\nexport const config = ${config}\n`,
|
|
101
197
|
)
|
|
102
198
|
|
|
103
199
|
describe("ConfigService", () => {
|
|
104
|
-
it("
|
|
200
|
+
it("a config runs its default workflow, its envDefaults as environment settings", async () => {
|
|
105
201
|
const cfg = await getConfig()
|
|
106
202
|
|
|
107
203
|
expect(cfg.workflow.initial).toBe("idle")
|
|
@@ -109,11 +205,11 @@ describe("ConfigService", () => {
|
|
|
109
205
|
expect(cfg.defaultWorkflow.vars["testCommand"]).toBeUndefined()
|
|
110
206
|
})
|
|
111
207
|
|
|
112
|
-
it("with no gtd.config.ts anywhere: fails,
|
|
208
|
+
it("with no gtd.config.ts anywhere: fails, pointing at gtd install", async () => {
|
|
113
209
|
rmSync(join(projectDir, "gtd.config.ts"))
|
|
114
210
|
|
|
115
211
|
await expect(getConfig()).rejects.toThrow(
|
|
116
|
-
/no workflows configured: no gtd\.config\.ts here or above —
|
|
212
|
+
/no workflows configured: no gtd\.config\.ts here or above — gtd ships no workflow; `gtd install` says where to get one/,
|
|
117
213
|
)
|
|
118
214
|
})
|
|
119
215
|
|
|
@@ -149,7 +245,7 @@ describe("ConfigService", () => {
|
|
|
149
245
|
const others = Layer.succeed(ConfigDiscovery, {
|
|
150
246
|
legacyRcFiles: () => Effect.succeed([{ path: "/shared/.gtdrc", owned: false }]),
|
|
151
247
|
workflowModule: () =>
|
|
152
|
-
Effect.succeed({ filepath: join(projectDir, "gtd.config.ts"), source:
|
|
248
|
+
Effect.succeed({ filepath: join(projectDir, "gtd.config.ts"), source: BASE_CONFIG }),
|
|
153
249
|
})
|
|
154
250
|
const cfg = await Effect.runPromise(
|
|
155
251
|
Effect.flatMap(ConfigService, (c) => c.load).pipe(
|
|
@@ -172,7 +268,7 @@ describe("ConfigService", () => {
|
|
|
172
268
|
it("refuses a user-level rc file in home, even when the walk never reaches home", async () => {
|
|
173
269
|
writeFileSync(join(homeDir, ".gtdrc.json"), `{ "$schema": "x" }\n`)
|
|
174
270
|
const outside = mkdtempSync(join(tmpdir(), "gtd-outside-"))
|
|
175
|
-
writeFileSync(join(outside, "gtd.config.ts"),
|
|
271
|
+
writeFileSync(join(outside, "gtd.config.ts"), BASE_CONFIG)
|
|
176
272
|
|
|
177
273
|
try {
|
|
178
274
|
await expect(getConfig()).rejects.toThrow(`${join(homeDir, ".gtdrc.json")}: no longer read`)
|
|
@@ -233,7 +329,7 @@ describe("ConfigService", () => {
|
|
|
233
329
|
join(projectDir, "gtd.config.ts"),
|
|
234
330
|
[
|
|
235
331
|
`import { defineConfig } from "@pmelab/gtd/flows"`,
|
|
236
|
-
|
|
332
|
+
BASE_CONFIG,
|
|
237
333
|
`export const config = defineConfig({ ui: { port: 4173 } })`,
|
|
238
334
|
``,
|
|
239
335
|
].join("\n"),
|
|
@@ -247,7 +343,7 @@ describe("ConfigService", () => {
|
|
|
247
343
|
join(projectDir, "gtd.config.ts"),
|
|
248
344
|
[
|
|
249
345
|
`import { defineConfig } from "@pmelab/gtd/flows"`,
|
|
250
|
-
|
|
346
|
+
BASE_CONFIG,
|
|
251
347
|
`export const config = defineConfig({ env: { testCommand: "x" } })`,
|
|
252
348
|
``,
|
|
253
349
|
].join("\n"),
|
|
@@ -318,7 +414,7 @@ describe("ConfigService", () => {
|
|
|
318
414
|
expect(Object.keys(cfg.fingerprint).sort()).toEqual(["gtd.config.ts", "steps.ts"])
|
|
319
415
|
})
|
|
320
416
|
|
|
321
|
-
it("the fingerprint covers gtd.config.ts but no
|
|
417
|
+
it("the fingerprint covers gtd.config.ts but no gtd module it imports", async () => {
|
|
322
418
|
const cfg = await getConfig()
|
|
323
419
|
|
|
324
420
|
expect(Object.keys(cfg.fingerprint)).toEqual(["gtd.config.ts"])
|
|
@@ -465,7 +561,8 @@ describe("ConfigService", () => {
|
|
|
465
561
|
const flow = (first: string) => `async () => {\n await human("${first}")\n}`
|
|
466
562
|
const header = `import { human } from "@pmelab/gtd/flows"\n`
|
|
467
563
|
|
|
468
|
-
it("
|
|
564
|
+
it("re-exported factories are startable under their names, default is not", async () => {
|
|
565
|
+
withFactories(`export { feature as default, feature, review, fix } from "./workflows"\n`)
|
|
469
566
|
const cfg = await getConfig()
|
|
470
567
|
expect(cfg.workflowNames).toEqual(["feature", "fix", "review"])
|
|
471
568
|
expect(cfg.defaultWorkflow.name).toBe("feature")
|
|
@@ -512,38 +609,12 @@ describe("ConfigService", () => {
|
|
|
512
609
|
expect((await getConfig()).defaultWorkflow.name).toBe("hotfix")
|
|
513
610
|
})
|
|
514
611
|
|
|
515
|
-
it("a helper re-exported from
|
|
516
|
-
|
|
517
|
-
join(projectDir, "gtd.config.ts"),
|
|
518
|
-
[`export { feature as default, unwind } from "@pmelab/gtd/workflows"`, ``].join("\n"),
|
|
519
|
-
)
|
|
612
|
+
it("a helper re-exported from a workflow module becomes a startable workflow", async () => {
|
|
613
|
+
withFactories(`export { feature as default, unwind } from "./workflows"\n`)
|
|
520
614
|
expect((await getConfig()).workflowNames).toContain("unwind")
|
|
521
615
|
})
|
|
522
616
|
|
|
523
|
-
it("
|
|
524
|
-
writeFileSync(
|
|
525
|
-
join(projectDir, "gtd.config.ts"),
|
|
526
|
-
[`export { feature as default, feature } from "@pmelab/gtd/workflow"`, ``].join("\n"),
|
|
527
|
-
)
|
|
528
|
-
expect((await getConfig()).defaultWorkflow.name).toBe("feature")
|
|
529
|
-
})
|
|
530
|
-
|
|
531
|
-
it("each shipped workflow has its own subpath", async () => {
|
|
532
|
-
writeFileSync(
|
|
533
|
-
join(projectDir, "gtd.config.ts"),
|
|
534
|
-
[
|
|
535
|
-
`export { feature as default, feature } from "@pmelab/gtd/workflows/feature"`,
|
|
536
|
-
`export { review } from "@pmelab/gtd/workflows/review"`,
|
|
537
|
-
`export { fix } from "@pmelab/gtd/workflows/fix"`,
|
|
538
|
-
``,
|
|
539
|
-
].join("\n"),
|
|
540
|
-
)
|
|
541
|
-
const config = await getConfig()
|
|
542
|
-
expect(config.defaultWorkflow.name).toBe("feature")
|
|
543
|
-
expect(config.workflowNames).toEqual(["feature", "fix", "review"])
|
|
544
|
-
})
|
|
545
|
-
|
|
546
|
-
it("a shipped subpath only a helper file imports resolves", async () => {
|
|
617
|
+
it("a gtd subpath only a helper file imports resolves", async () => {
|
|
547
618
|
writeFileSync(
|
|
548
619
|
join(projectDir, "kit.ts"),
|
|
549
620
|
[
|
|
@@ -598,7 +669,8 @@ describe("ConfigService", () => {
|
|
|
598
669
|
].join("\n"),
|
|
599
670
|
)
|
|
600
671
|
|
|
601
|
-
it("
|
|
672
|
+
it("exported factories bring their doors, review taking an optional base", async () => {
|
|
673
|
+
withFactories(`export { feature as default, review, fix } from "./workflows"\n`)
|
|
602
674
|
const { doors } = await getConfig()
|
|
603
675
|
expect([...doors.keys()].sort()).toEqual(["fix", "review"])
|
|
604
676
|
expect(doors.get("fix")?.workflow).toBe("fix")
|
|
@@ -606,44 +678,33 @@ describe("ConfigService", () => {
|
|
|
606
678
|
})
|
|
607
679
|
|
|
608
680
|
it("a config exporting only fix gets only fix's door", async () => {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
`export default ${flow("main")}`,
|
|
614
|
-
`export { fix } from "@pmelab/gtd/workflows"`,
|
|
615
|
-
``,
|
|
616
|
-
].join("\n"),
|
|
681
|
+
withFactories(
|
|
682
|
+
[header, `export default ${flow("main")}`, `export { fix } from "./workflows"`, ``].join(
|
|
683
|
+
"\n",
|
|
684
|
+
),
|
|
617
685
|
)
|
|
618
686
|
const { doors } = await getConfig()
|
|
619
687
|
expect([...doors.keys()]).toEqual(["fix"])
|
|
620
688
|
})
|
|
621
689
|
|
|
622
690
|
it("a door follows the name its workflow is exported under", async () => {
|
|
623
|
-
|
|
624
|
-
join(projectDir, "gtd.config.ts"),
|
|
625
|
-
`export { feature as default, review as audit } from "@pmelab/gtd/workflows"\n`,
|
|
626
|
-
)
|
|
691
|
+
withFactories(`export { feature as default, review as audit } from "./workflows"\n`)
|
|
627
692
|
const door = (await getConfig()).doors.get("review")
|
|
628
693
|
expect(door?.workflow).toBe("audit")
|
|
629
694
|
expect(door?.vars?.({ base: "main" })).toEqual({ reviewBase: "main" })
|
|
630
695
|
})
|
|
631
696
|
|
|
632
697
|
it("a review factory exported as audit reviews the default branch, or an explicit base", async () => {
|
|
633
|
-
|
|
634
|
-
join(projectDir, "gtd.config.ts"),
|
|
635
|
-
`export { feature as default, review as audit } from "@pmelab/gtd/workflows"\n`,
|
|
636
|
-
)
|
|
698
|
+
withFactories(`export { feature as default, review as audit } from "./workflows"\n`)
|
|
637
699
|
const audit = (await getConfig()).workflowNamed("audit")
|
|
638
700
|
expect(audit?.def.base?.("audit", {})).toBe("")
|
|
639
701
|
expect(audit?.def.base?.("audit", { reviewBase: "v1" })).toBe("v1")
|
|
640
702
|
})
|
|
641
703
|
|
|
642
704
|
it("a config's own base export wins over the review factory's", async () => {
|
|
643
|
-
|
|
644
|
-
join(projectDir, "gtd.config.ts"),
|
|
705
|
+
withFactories(
|
|
645
706
|
[
|
|
646
|
-
`export { feature as default, review } from "
|
|
707
|
+
`export { feature as default, review } from "./workflows"`,
|
|
647
708
|
`export const base = (w: string) => (w === "review" ? "develop" : undefined)`,
|
|
648
709
|
``,
|
|
649
710
|
].join("\n"),
|
|
@@ -652,8 +713,8 @@ describe("ConfigService", () => {
|
|
|
652
713
|
})
|
|
653
714
|
|
|
654
715
|
it("a door several workflows build gets no door when none is exported under its name, until the doors export declares it", async () => {
|
|
655
|
-
const both = `export { feature as default, fix as fix2, fix as fix3 } from "
|
|
656
|
-
|
|
716
|
+
const both = `export { feature as default, fix as fix2, fix as fix3 } from "./workflows"`
|
|
717
|
+
withFactories(`${both}\n`)
|
|
657
718
|
expect((await getConfig()).doors.get("fix")).toBeUndefined()
|
|
658
719
|
writeFileSync(
|
|
659
720
|
join(projectDir, "gtd.config.ts"),
|
|
@@ -663,9 +724,8 @@ describe("ConfigService", () => {
|
|
|
663
724
|
})
|
|
664
725
|
|
|
665
726
|
it("a door clash warns once at doors.<name>, builders sorted", async () => {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
`export { feature as default, fix as fix3, fix as fix4, fix as fix2 } from "@pmelab/gtd/workflows"\n`,
|
|
727
|
+
withFactories(
|
|
728
|
+
`export { feature as default, fix as fix3, fix as fix4, fix as fix2 } from "./workflows"\n`,
|
|
669
729
|
)
|
|
670
730
|
const cfg = await getConfig()
|
|
671
731
|
const clash = cfg.warnings.filter((w) => w.path[0] === "doors")
|
|
@@ -680,70 +740,38 @@ describe("ConfigService", () => {
|
|
|
680
740
|
})
|
|
681
741
|
|
|
682
742
|
it("no clash warning when an export is named after the door, one workflow builds it, or doors declares it", async () => {
|
|
683
|
-
const both = `export { feature as default, fix as fix2, fix as fix3 } from "
|
|
743
|
+
const both = `export { feature as default, fix as fix2, fix as fix3 } from "./workflows"`
|
|
684
744
|
for (const body of [
|
|
685
|
-
`export { feature as default, fix as fix2, fix } from "
|
|
686
|
-
`export { feature as default, fix } from "
|
|
745
|
+
`export { feature as default, fix as fix2, fix } from "./workflows"\n`,
|
|
746
|
+
`export { feature as default, fix } from "./workflows"\n`,
|
|
687
747
|
`${both}\nexport const doors = { fix: { workflow: "fix3" } }\n`,
|
|
688
748
|
]) {
|
|
689
|
-
|
|
749
|
+
withFactories(body)
|
|
690
750
|
expect((await getConfig()).warnings.filter((w) => w.path[0] === "doors")).toEqual([])
|
|
691
751
|
}
|
|
692
752
|
})
|
|
693
753
|
|
|
694
754
|
it("door ownership does not depend on export order", async () => {
|
|
695
|
-
|
|
696
|
-
join(projectDir, "gtd.config.ts"),
|
|
697
|
-
`export { feature as default, fix as fix2, fix } from "@pmelab/gtd/workflows"\n`,
|
|
698
|
-
)
|
|
755
|
+
withFactories(`export { feature as default, fix as fix2, fix } from "./workflows"\n`)
|
|
699
756
|
expect((await getConfig()).doors.get("fix")?.workflow).toBe("fix")
|
|
700
757
|
})
|
|
701
758
|
|
|
702
759
|
it("rollout and a second instance of it load, door rollout running workflow rollout whichever export comes first", async () => {
|
|
703
760
|
for (const body of [
|
|
704
|
-
`export { rollout } from "
|
|
705
|
-
`import { rollout } from "
|
|
761
|
+
`export { rollout } from "./workflows"\nimport { rollout } from "./workflows"\nexport const elsewhere = rollout({ flagRegistry: "x.ts" })`,
|
|
762
|
+
`import { rollout } from "./workflows"\nexport const elsewhere = rollout({ flagRegistry: "x.ts" })\nexport { rollout }`,
|
|
706
763
|
]) {
|
|
707
|
-
|
|
708
|
-
join(projectDir, "gtd.config.ts"),
|
|
709
|
-
`export { feature as default } from "@pmelab/gtd/workflows"\n${body}\n`,
|
|
710
|
-
)
|
|
764
|
+
withFactories(`export { feature as default } from "./workflows"\n${body}\n`)
|
|
711
765
|
expect((await getConfig()).doors.get("rollout")?.workflow).toBe("rollout")
|
|
712
766
|
}
|
|
713
767
|
})
|
|
714
768
|
|
|
715
|
-
it("shipped rollout and cleanup list their doors beside fix and review", async () => {
|
|
716
|
-
writeFileSync(
|
|
717
|
-
join(projectDir, "gtd.config.ts"),
|
|
718
|
-
`export { feature as default, fix, review, rollout, cleanup } from "@pmelab/gtd/workflows"\n`,
|
|
719
|
-
)
|
|
720
|
-
const { doors } = await getConfig()
|
|
721
|
-
expect([...doors.keys()]).toEqual(["cleanup", "fix", "review", "rollout"])
|
|
722
|
-
expect(doors.get("rollout")?.args).toEqual([{ name: "flag" }])
|
|
723
|
-
expect(doors.get("cleanup")?.args).toEqual([{ name: "flag" }])
|
|
724
|
-
})
|
|
725
|
-
|
|
726
|
-
it("rollout and cleanup load from their own subpaths", async () => {
|
|
727
|
-
writeFileSync(
|
|
728
|
-
join(projectDir, "gtd.config.ts"),
|
|
729
|
-
[
|
|
730
|
-
`export { feature as default, fix, review } from "@pmelab/gtd/workflows"`,
|
|
731
|
-
`export { rollout } from "@pmelab/gtd/workflows/rollout"`,
|
|
732
|
-
`export { cleanup } from "@pmelab/gtd/workflows/cleanup"`,
|
|
733
|
-
``,
|
|
734
|
-
].join("\n"),
|
|
735
|
-
)
|
|
736
|
-
const { doors } = await getConfig()
|
|
737
|
-
expect([...doors.keys()]).toEqual(["cleanup", "fix", "review", "rollout"])
|
|
738
|
-
expect(doors.get("rollout")?.args).toEqual([{ name: "flag" }])
|
|
739
|
-
expect(doors.get("cleanup")?.args).toEqual([{ name: "flag" }])
|
|
740
|
-
})
|
|
741
|
-
|
|
742
769
|
it("a config's doors merge derived doors (exported workflows) with declared doors (doors export)", async () => {
|
|
743
770
|
withDoors(
|
|
744
771
|
`{ hot: { workflow: "hotfix", args: [{ name: "ticket" }] } }`,
|
|
745
|
-
'export { fix } from "
|
|
772
|
+
'export { fix } from "./workflows"',
|
|
746
773
|
)
|
|
774
|
+
writeFileSync(join(projectDir, "workflows.ts"), FACTORIES)
|
|
747
775
|
const { doors } = await getConfig()
|
|
748
776
|
expect([...doors.keys()].sort()).toEqual(["fix", "hot"])
|
|
749
777
|
expect(doors.get("fix")).toBeDefined()
|
|
@@ -939,10 +967,7 @@ describe("ConfigService", () => {
|
|
|
939
967
|
|
|
940
968
|
describe("ConfigService — workflow factory options", () => {
|
|
941
969
|
const configWith = (body: string) =>
|
|
942
|
-
|
|
943
|
-
join(projectDir, "gtd.config.ts"),
|
|
944
|
-
[`import { feature, fix, review } from "@pmelab/gtd/workflows"`, body, ``].join("\n"),
|
|
945
|
-
)
|
|
970
|
+
withFactories([`import { feature, fix, review } from "./workflows"`, body, ``].join("\n"))
|
|
946
971
|
|
|
947
972
|
it("an option given in gtd.config.ts becomes the workflow's setting of its kind", async () => {
|
|
948
973
|
configWith(
|
package/src/workflow/load.ts
CHANGED
|
@@ -115,7 +115,7 @@ export const load: Effect.Effect<
|
|
|
115
115
|
const missing: Diagnostic = {
|
|
116
116
|
severity: "error",
|
|
117
117
|
path: [],
|
|
118
|
-
message: `no workflows configured: no ${WORKFLOW_MODULE} here or above —
|
|
118
|
+
message: `no workflows configured: no ${WORKFLOW_MODULE} here or above — gtd ships no workflow; \`gtd install\` says where to get one`,
|
|
119
119
|
origin: host.root,
|
|
120
120
|
}
|
|
121
121
|
return yield* Effect.fail(configError([missing]))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
|
-
import { resolveStartVars, resolveVars } from "./vars.js"
|
|
2
|
+
import { ownFlagRefusal, resolveStartVars, resolveVars } from "./vars.js"
|
|
3
3
|
|
|
4
4
|
describe("resolveVars — GTD_FLAG_* belongs to feature flags", () => {
|
|
5
5
|
it.each(["flag_x", "FLAG_X", "Flag_x"])("%s keeps its default with GTD_FLAG_X set", (name) => {
|
|
@@ -30,9 +30,36 @@ describe("resolveStartVars", () => {
|
|
|
30
30
|
})
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
it("GTD_FLAG_X never overrides, while GTD_<NAME> still does for other names", () => {
|
|
33
|
+
it("GTD_FLAG_X never overrides one of gtd's own flags, while GTD_<NAME> still does for other names", () => {
|
|
34
34
|
expect(
|
|
35
|
-
resolveStartVars(
|
|
35
|
+
resolveStartVars(
|
|
36
|
+
{ flag_x: "0", a: "1" },
|
|
37
|
+
{ flag_x: "0" },
|
|
38
|
+
{},
|
|
39
|
+
{ GTD_FLAG_X: "1", GTD_A: "2" },
|
|
40
|
+
),
|
|
36
41
|
).toEqual({ flag_x: "0", a: "2" })
|
|
37
42
|
})
|
|
43
|
+
|
|
44
|
+
it("pins a workflow's own flag from GTD_FLAG_<NAME>, below --var", () => {
|
|
45
|
+
expect(resolveStartVars({ flag_own: "0" }, {}, {}, { GTD_FLAG_OWN: "true" })).toEqual({
|
|
46
|
+
flag_own: "1",
|
|
47
|
+
})
|
|
48
|
+
expect(
|
|
49
|
+
resolveStartVars({ flag_own: "0" }, {}, { flag_own: "0" }, { GTD_FLAG_OWN: "1" }),
|
|
50
|
+
).toEqual({
|
|
51
|
+
flag_own: "0",
|
|
52
|
+
})
|
|
53
|
+
expect(resolveStartVars({ flag_own: "1" }, {}, {}, {})).toEqual({ flag_own: "1" })
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe("ownFlagRefusal", () => {
|
|
58
|
+
it("refuses a workflow's own flag set to anything but 1, 0, true or false", () => {
|
|
59
|
+
expect(ownFlagRefusal({ flag_own: "0" }, {}, { GTD_FLAG_OWN: "yes" })).toBe(
|
|
60
|
+
"GTD_FLAG_OWN must be 1, 0, true or false",
|
|
61
|
+
)
|
|
62
|
+
expect(ownFlagRefusal({ flag_own: "0" }, {}, { GTD_FLAG_OWN: "TRUE" })).toBeUndefined()
|
|
63
|
+
expect(ownFlagRefusal({ flag_x: "0" }, { flag_x: "1" }, { GTD_FLAG_X: "yes" })).toBeUndefined()
|
|
64
|
+
})
|
|
38
65
|
})
|
package/src/workflow/vars.ts
CHANGED
|
@@ -29,10 +29,38 @@ export const resolveVars = (
|
|
|
29
29
|
return merged
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
const FLAG_VALUES: Readonly<Record<string, string>> = { "0": "0", false: "0", "1": "1", true: "1" }
|
|
33
|
+
|
|
34
|
+
const flagVariable = (setting: string): string =>
|
|
35
|
+
`GTD_FLAG_${setting.slice("flag_".length).toUpperCase()}`
|
|
36
|
+
|
|
37
|
+
/** The `flag_<name>` settings a workflow declares that gtd's own flags lack: the workflow's own release flags. */
|
|
38
|
+
const ownFlags = (
|
|
39
|
+
workflowVars: Readonly<Record<string, string>>,
|
|
40
|
+
flags: Readonly<Record<string, string>>,
|
|
41
|
+
): string[] =>
|
|
42
|
+
Object.keys(workflowVars).filter(
|
|
43
|
+
(name) => name.startsWith("flag_") && !Object.hasOwn(flags, name),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
/** Why a workflow's own release flag cannot start, if one is set to anything but 1, 0, true or false. */
|
|
47
|
+
export const ownFlagRefusal = (
|
|
48
|
+
workflowVars: Readonly<Record<string, string>>,
|
|
49
|
+
flags: Readonly<Record<string, string>>,
|
|
50
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
51
|
+
): string | undefined => {
|
|
52
|
+
const bad = ownFlags(workflowVars, flags).find((name) => {
|
|
53
|
+
const raw = env[flagVariable(name)] ?? ""
|
|
54
|
+
return raw !== "" && !Object.hasOwn(FLAG_VALUES, raw.toLowerCase())
|
|
55
|
+
})
|
|
56
|
+
return bad === undefined ? undefined : `${flagVariable(bad)} must be 1, 0, true or false`
|
|
57
|
+
}
|
|
58
|
+
|
|
32
59
|
/**
|
|
33
60
|
* `resolveVars` for a process being STARTED: `flags` land between the workflow
|
|
34
|
-
* defaults and `--var`, for already-declared names only
|
|
35
|
-
*
|
|
61
|
+
* defaults and `--var`, for already-declared names only, and so does
|
|
62
|
+
* `GTD_FLAG_<NAME>` for a `flag_<name>` the workflow declares itself. A running
|
|
63
|
+
* process never calls this — it replays its pinned record.
|
|
36
64
|
*/
|
|
37
65
|
export const resolveStartVars = (
|
|
38
66
|
workflowVars: Readonly<Record<string, string>>,
|
|
@@ -43,6 +71,10 @@ export const resolveStartVars = (
|
|
|
43
71
|
const declared = Object.fromEntries(
|
|
44
72
|
Object.entries(flags).filter(([name]) => Object.hasOwn(workflowVars, name)),
|
|
45
73
|
)
|
|
74
|
+
for (const name of ownFlags(workflowVars, flags)) {
|
|
75
|
+
const value = FLAG_VALUES[(env[flagVariable(name)] ?? "").toLowerCase()]
|
|
76
|
+
if (value !== undefined) declared[name] = value
|
|
77
|
+
}
|
|
46
78
|
return resolveVars({ ...workflowVars, ...declared }, entryVars, env)
|
|
47
79
|
}
|
|
48
80
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Building blocks
|
|
1
|
+
// Building blocks workflows share. Policy-free: every skill list,
|
|
2
2
|
// lens, prompt, step, path and threshold arrives as a parameter, so a new
|
|
3
3
|
// workflow composes these without inheriting the shipped one's choices.
|
|
4
4
|
export * from "./health.js"
|