@pmelab/gtd 60.0.0 → 60.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.
Files changed (46) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +14 -2
  3. package/dist/chunks/testkit-BhTey1Do.mjs +30517 -0
  4. package/dist/gtd.bundle.mjs +10545 -19230
  5. package/dist/gtd.core.mjs +593 -0
  6. package/dist/testkit/gherkin.mjs +2 -2
  7. package/dist/testkit.mjs +1 -1
  8. package/package.json +7 -7
  9. package/src/cli/Cli.test.ts +2 -2
  10. package/src/cli/Cli.ts +68 -51
  11. package/src/cli/Output.test.ts +1 -1
  12. package/src/cli/Output.ts +3 -2
  13. package/src/cli/Program.test.ts +14 -6
  14. package/src/cli/Program.ts +35 -10
  15. package/src/cli/ToolCommands.ts +2 -2
  16. package/src/edge/Edge.test.ts +8 -6
  17. package/src/edge/Edge.ts +3 -3
  18. package/src/flows/runtime.ts +1 -1
  19. package/src/install/Version.ts +9 -3
  20. package/src/install/index.ts +1 -1
  21. package/src/platform/CommandRunner.ts +13 -11
  22. package/src/platform/Commentary.ts +2 -2
  23. package/src/platform/Git.test.ts +4 -5
  24. package/src/platform/Git.ts +42 -38
  25. package/src/platform/Host.ts +1 -1
  26. package/src/platform/NodeServices.ts +11 -0
  27. package/src/platform/Workspace.test.ts +4 -5
  28. package/src/platform/Workspace.ts +3 -3
  29. package/src/platform/WorktreeState.test.ts +2 -3
  30. package/src/platform/WorktreeState.ts +1 -1
  31. package/src/platform/index.ts +2 -1
  32. package/src/step/planStart.ts +3 -3
  33. package/src/twins/EmittedScripts.test.ts +1 -1
  34. package/src/twins/GitTiers.ts +6 -7
  35. package/src/twins/Layers.test.ts +1 -1
  36. package/src/twins/Layers.ts +10 -3
  37. package/src/twins/cliIo.ts +9 -4
  38. package/src/workflow/discovery.test.ts +24 -7
  39. package/src/workflow/discovery.ts +5 -6
  40. package/src/workflow/index.ts +1 -0
  41. package/src/workflow/load.test.ts +60 -3
  42. package/src/workflow/load.ts +316 -166
  43. package/src/workflow/pin.test.ts +159 -0
  44. package/src/workflow/pin.ts +72 -0
  45. package/src/workflow/vars.ts +3 -3
  46. package/dist/chunks/testkit-BvecVjQ0.mjs +0 -32355
@@ -12,10 +12,9 @@ import { mkdtempSync, realpathSync } from "node:fs"
12
12
  import { execSync } from "node:child_process"
13
13
  import { tmpdir } from "node:os"
14
14
  import { Effect, Layer } from "effect"
15
- import { NodeContext } from "@effect/platform-node"
16
15
  import { describe, expect, it } from "vitest"
17
16
  import { loopLogPath, worktreeGitDir } from "./WorktreeState.js"
18
- import { GitService, Host, Workspace } from "./index.js"
17
+ import { GitService, Host, Workspace, NodeServices } from "./index.js"
19
18
  import { InMemRepo, testLayers } from "../twins/index.js"
20
19
 
21
20
  const provide = <A>(
@@ -112,7 +111,7 @@ describe("worktreeGitDir [real git]", () => {
112
111
 
113
112
  const runIn = (root: string) => {
114
113
  const hostLayer = Host.layer({ root, home: root, env: {} })
115
- const gitLayer = GitService.Live.pipe(Layer.provide(Layer.merge(hostLayer, NodeContext.layer)))
114
+ const gitLayer = GitService.Live.pipe(Layer.provide(Layer.merge(hostLayer, NodeServices.layer)))
116
115
  const workspaceLayer = Workspace.Live.pipe(Layer.provide(Layer.merge(hostLayer, gitLayer)))
117
116
  return Effect.runPromise(
118
117
  worktreeGitDir.pipe(Effect.provide(Layer.mergeAll(hostLayer, workspaceLayer))),
@@ -17,7 +17,7 @@ export const worktreeGitDir: Effect.Effect<string, never, Host | Workspace> = Ef
17
17
  const workspace = yield* Workspace
18
18
  const content = yield* workspace
19
19
  .read(".git")
20
- .pipe(Effect.catchAll(() => Effect.succeed(undefined)))
20
+ .pipe(Effect.catch(() => Effect.succeed(undefined)))
21
21
  const pointer = content === undefined ? null : GITDIR_POINTER.exec(content)
22
22
  if (pointer === null) return ".git"
23
23
  const gitdir = pointer[1]!.trim()
@@ -4,7 +4,7 @@
4
4
  * home, env, scratch). `GitService` moved in alongside `Workspace` since
5
5
  * `committed` IS `git.readFileAtRef` — one module owns every read.
6
6
  *
7
- * Seam rule, stated once: a seam is a `Context.Tag` unless the consuming
7
+ * Seam rule, stated once: a seam is a `Context.Service` unless the consuming
8
8
  * code is synchronous, non-Effect code that cannot `yield*` — replay's flow
9
9
  * reads are that exception, and use the `*Sync` members.
10
10
  */
@@ -19,6 +19,7 @@ export {
19
19
  } from "./Git.js"
20
20
  export { Narrator, GtdError, GtdUsageError, renderFailure } from "./Commentary.js"
21
21
  export { CommandRunner } from "./CommandRunner.js"
22
+ export * as NodeServices from "./NodeServices.js"
22
23
  export { loopLogPath } from "./WorktreeState.js"
23
24
  export {
24
25
  HISTORY_REF,
@@ -158,11 +158,11 @@ const resolveBase = (
158
158
  const git = yield* GitService
159
159
  const rendered = value.trim()
160
160
  const target = rendered === "" ? yield* git.defaultBranch() : rendered
161
- const resolvedBase = yield* Effect.either(git.resolveRef(target))
162
- if (resolvedBase._tag === "Left") {
161
+ const resolvedBase = yield* Effect.result(git.resolveRef(target))
162
+ if (resolvedBase._tag === "Failure") {
163
163
  return refusal(`${commandLabel}: "${target}" does not resolve to a commit`)
164
164
  }
165
- const mergeBase = yield* git.mergeBase(resolvedBase.right, "HEAD")
165
+ const mergeBase = yield* git.mergeBase(resolvedBase.success, "HEAD")
166
166
  if (Option.isNone(mergeBase)) {
167
167
  return refusal(`${commandLabel}: "${target}" shares no common ancestor with HEAD`)
168
168
  }
@@ -180,7 +180,7 @@ for (const makeTier of gitTiers) {
180
180
  // As a landing renders it: under `set -eu`, which a failing update-ref relies on to stop.
181
181
  const keepTurn = (ref: string, message: string) =>
182
182
  ScriptSurface.render([{ kind: "gitWrite", write: { kind: "keepTurn", ref, message } }])
183
- const git = <A>(f: (g: GitService["Type"]) => Effect.Effect<A, Error>) =>
183
+ const git = <A>(f: (g: GitService["Service"]) => Effect.Effect<A, Error>) =>
184
184
  t.provide(Effect.flatMap(GitService, f))
185
185
  const keptTip = async () => {
186
186
  const marker = await git((g) => g.lastCommitMessage())
@@ -18,8 +18,7 @@ import { execFileSync, execSync, spawnSync } from "node:child_process"
18
18
  // `TEST_DOUBLE_SENTINEL` check.
19
19
  import { afterEach, beforeEach, describe, expect, it } from "vitest"
20
20
  import { Effect, Exit, Option } from "effect"
21
- import { NodeContext } from "@effect/platform-node"
22
- import { Narrator, GitService, Host, type GitOperations } from "../platform/index.js"
21
+ import { Narrator, GitService, Host, type GitOperations, NodeServices } from "../platform/index.js"
23
22
  import {
24
23
  commitAll,
25
24
  commitAsIs,
@@ -165,7 +164,7 @@ const makeLiveTier = (initialCommit = true): GitTier => {
165
164
  eff.pipe(
166
165
  Effect.provide(GitService.Live),
167
166
  Effect.provide(Host.layer({ root, home: root, env: {} })),
168
- Effect.provide(NodeContext.layer),
167
+ Effect.provide(NodeServices.layer),
169
168
  Effect.provide(noopNarratorLayer),
170
169
  ),
171
170
  )
@@ -177,7 +176,7 @@ const makeLiveTier = (initialCommit = true): GitTier => {
177
176
  eff.pipe(
178
177
  Effect.provide(GitService.Live),
179
178
  Effect.provide(Host.layer({ root, home: root, env: {} })),
180
- Effect.provide(NodeContext.layer),
179
+ Effect.provide(NodeServices.layer),
181
180
  Effect.provide(noopNarratorLayer),
182
181
  ),
183
182
  )
@@ -1266,7 +1265,7 @@ export const runGitServiceContract = (makeTier: () => GitTier): void => {
1266
1265
  Effect.flatMap(GitService, (g) => g.gitDir()).pipe(
1267
1266
  Effect.provide(GitService.Live),
1268
1267
  Effect.provide(Host.layer({ root: siblingDir, home: siblingDir, env: {} })),
1269
- Effect.provide(NodeContext.layer),
1268
+ Effect.provide(NodeServices.layer),
1270
1269
  ),
1271
1270
  )
1272
1271
  expect(siblingGitDir).not.toBe(mainGitDir)
@@ -1297,14 +1296,14 @@ export const runGitServiceContract = (makeTier: () => GitTier): void => {
1297
1296
  Effect.flatMap(GitService, (g) => g.gitCommonDir()).pipe(
1298
1297
  Effect.provide(GitService.Live),
1299
1298
  Effect.provide(Host.layer({ root: siblingDir, home: siblingDir, env: {} })),
1300
- Effect.provide(NodeContext.layer),
1299
+ Effect.provide(NodeServices.layer),
1301
1300
  ),
1302
1301
  )
1303
1302
  const siblingGitDir = await Effect.runPromise(
1304
1303
  Effect.flatMap(GitService, (g) => g.gitDir()).pipe(
1305
1304
  Effect.provide(GitService.Live),
1306
1305
  Effect.provide(Host.layer({ root: siblingDir, home: siblingDir, env: {} })),
1307
- Effect.provide(NodeContext.layer),
1306
+ Effect.provide(NodeServices.layer),
1308
1307
  ),
1309
1308
  )
1310
1309
  expect(siblingCommonDir).toBe(mainCommonDir)
@@ -23,7 +23,7 @@ const seed = (repo: InMemRepo, dir: string, name: string, content: string): void
23
23
 
24
24
  const runDiscovery = <A>(
25
25
  repo: InMemRepo,
26
- f: (d: ConfigDiscovery["Type"]) => Effect.Effect<A, Error>,
26
+ f: (d: ConfigDiscovery["Service"]) => Effect.Effect<A, Error>,
27
27
  ) =>
28
28
  Effect.runPromise(
29
29
  Effect.gen(function* () {
@@ -21,6 +21,7 @@ import {
21
21
  import {
22
22
  ConfigDiscovery,
23
23
  ConfigService,
24
+ EngineCore,
24
25
  LEGACY_RC_FILES,
25
26
  legacyRcDirs,
26
27
  WORKFLOW_MODULE,
@@ -30,7 +31,7 @@ import { fakeGitOperations } from "./FakeGitOperations.js"
30
31
  import { InMemRepo } from "./InMemRepo.js"
31
32
 
32
33
  // Read off the CLI's own port rather than imported from the composition root.
33
- type CommandRequirements = Layer.Layer.Success<ReturnType<CliIo["layers"]>>
34
+ type CommandRequirements = Layer.Success<ReturnType<CliIo["layers"]>>
34
35
  import { ToolCommands, type CliIo } from "../cli/index.js"
35
36
 
36
37
  const toError = (e: unknown): Error => (e instanceof Error ? e : new Error(String(e)))
@@ -167,6 +168,11 @@ const makeInMemoryConfigDiscovery = (
167
168
  })
168
169
  }
169
170
 
171
+ /** The in-memory counterpart to the shipped `gtd.core.mjs`: an ES module that needs nothing but Node. */
172
+ const IN_MEMORY_ENGINE = `#!/usr/bin/env node
173
+ import "node:process"
174
+ `
175
+
170
176
  /** One scripted `bash` command's canned behavior, keyed by the RENDERED command string a scenario's `Given` step declares. */
171
177
  export type ScriptedCommand =
172
178
  | {
@@ -207,13 +213,13 @@ export interface TestWorldOptions {
207
213
  /** 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. */
208
214
  readonly verbose?: boolean
209
215
  /** Real or fake driver and tool commands — each one left out fails when called. */
210
- readonly tools?: Partial<ToolCommands["Type"]>
216
+ readonly tools?: Partial<ToolCommands["Service"]>
211
217
  }
212
218
 
213
219
  const missingTool = (name: string) => () =>
214
220
  Effect.fail(new Error(`gtd ${name}: ToolCommands has no test double wired into testLayers()`))
215
221
 
216
- const missingToolCommands: ToolCommands["Type"] = {
222
+ const missingToolCommands: ToolCommands["Service"] = {
217
223
  ui: missingTool("ui"),
218
224
  lsp: missingTool("lsp"),
219
225
  judge: () =>
@@ -264,6 +270,7 @@ export function testLayers(
264
270
  }),
265
271
  ConfigService.Live,
266
272
  makeInMemoryConfigDiscovery(repo, root),
273
+ Layer.succeed(EngineCore, { source: Effect.succeed(IN_MEMORY_ENGINE) }),
267
274
  Narrator.layer(opts.narrate ?? (() => {}), opts.verbose ?? true),
268
275
  // Drivers and tools are unit-tested in their own folders; a call reaching
269
276
  // one of these is a test gap, not silence. A test opts into one via `tools`.
@@ -4,6 +4,8 @@ import { testLayers } from "./Layers.js"
4
4
 
5
5
  export interface CapturedCliResult {
6
6
  readonly stdout: string
7
+ /** stdout as written, for a binary artifact `stdout` would decode. */
8
+ readonly stdoutBytes: Uint8Array
7
9
  readonly stderr: string
8
10
  readonly exitCode: number
9
11
  }
@@ -11,14 +13,14 @@ export interface CapturedCliResult {
11
13
  export const makeCapturingCliIo = (
12
14
  repo: InMemRepo,
13
15
  env: Readonly<Record<string, string | undefined>> = {},
14
- tools: Partial<ToolCommands["Type"]> = {},
16
+ tools: Partial<ToolCommands["Service"]> = {},
15
17
  ): { readonly io: CliIo; readonly result: () => CapturedCliResult } => {
16
- let stdout = ""
18
+ let stdout: Buffer = Buffer.alloc(0)
17
19
  let stderr = ""
18
20
  let exitCode = 0
19
21
  const io: CliIo = {
20
22
  stdout: (chunk) => {
21
- stdout += chunk
23
+ stdout = Buffer.concat([stdout, Buffer.from(chunk)])
22
24
  },
23
25
  stderr: (chunk) => {
24
26
  stderr += chunk
@@ -32,5 +34,8 @@ export const makeCapturingCliIo = (
32
34
  layers: (verbose) =>
33
35
  testLayers(repo, { env, narrate: (line) => (stderr += line), verbose, tools }),
34
36
  }
35
- return { io, result: () => ({ stdout, stderr, exitCode }) }
37
+ return {
38
+ io,
39
+ result: () => ({ stdout: stdout.toString("utf8"), stdoutBytes: stdout, stderr, exitCode }),
40
+ }
36
41
  }
@@ -2,15 +2,14 @@ import { execSync } from "node:child_process"
2
2
  import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"
3
3
  import { tmpdir } from "node:os"
4
4
  import { dirname, join } from "node:path"
5
- import { NodeContext } from "@effect/platform-node"
6
5
  import { Effect, Layer } from "effect"
7
6
  import { afterEach, describe, expect, it } from "vitest"
8
- import { GitService, Host, Narrator, Workspace } from "../platform/index.js"
7
+ import { GitService, Host, Narrator, Workspace, NodeServices } from "../platform/index.js"
9
8
  import { InMemRepo, testLayers } from "../twins/index.js"
10
9
  import { ConfigDiscovery, ConfigService } from "./index.js"
11
10
 
12
11
  // A config and the local modules it imports load the same from disk, where
13
- // jiti resolves them, and from an in-memory discovery, where the loader
12
+ // rolldown resolves them, and from an in-memory discovery, where the loader
14
13
  // resolves them itself — the same workflows, the same fingerprint, the same
15
14
  // failure for a module that is not there.
16
15
  type Files = Readonly<Record<string, string>>
@@ -63,7 +62,7 @@ const live = (): Tier => {
63
62
  writeFileSync(join(root, path), content)
64
63
  }
65
64
  const host = Host.layer({ root, home: root, env: {} })
66
- const git = GitService.Live.pipe(Layer.provide(Layer.merge(host, NodeContext.layer)))
65
+ const git = GitService.Live.pipe(Layer.provide(Layer.merge(host, NodeServices.layer)))
67
66
  const workspace = Workspace.Live.pipe(Layer.provide(Layer.merge(host, git)))
68
67
  return loadWith(
69
68
  Layer.mergeAll(
@@ -121,7 +120,24 @@ for (const make of [live, inMemory]) {
121
120
  })
122
121
  })
123
122
 
124
- it("leaves a specifier in a string alone, and loads a skipped require never", async () => {
123
+ it("imports a local module and a gtd subpath dynamically as the flow runs", async () => {
124
+ t = make()
125
+ const loaded = await t.load({
126
+ "gtd.config.ts": [
127
+ 'import { human } from "@pmelab/gtd/flows"',
128
+ "export default async () => {",
129
+ ' const { WIRE_SCHEMA } = await import("@pmelab/gtd/wire")',
130
+ ' const { first } = await import("./first.js")',
131
+ " await human(`${first}-${WIRE_SCHEMA}`)",
132
+ "}",
133
+ "",
134
+ ].join("\n"),
135
+ "first.ts": 'export const first = "idle"\n',
136
+ })
137
+ expect(Object.keys(loaded.fingerprint).sort()).toEqual(["first.ts", "gtd.config.ts"])
138
+ })
139
+
140
+ it("leaves a specifier in a string alone, and runs a skipped require never", async () => {
125
141
  t = make()
126
142
  const loaded = await t.load({
127
143
  "gtd.config.ts": config(
@@ -134,14 +150,15 @@ for (const make of [live, inMemory]) {
134
150
  "optional.ts": 'throw new Error("optional ran")\n',
135
151
  })
136
152
  expect(loaded.names).toEqual(["named"])
137
- expect(Object.keys(loaded.fingerprint)).toEqual(["gtd.config.ts"])
153
+ // Bundled, so watched, though it never runs.
154
+ expect(Object.keys(loaded.fingerprint).sort()).toEqual(["gtd.config.ts", "optional.ts"])
138
155
  })
139
156
 
140
157
  it("fails on an import that is not there, naming it", async () => {
141
158
  t = make()
142
159
  await expect(
143
160
  t.load({ "gtd.config.ts": config('import { brief } from "./missing.js"') }),
144
- ).rejects.toThrow(/Cannot find module '\.\/missing\.js'/)
161
+ ).rejects.toThrow(/Could not resolve '\.\/missing\.js'/)
145
162
  })
146
163
  })
147
164
  }
@@ -110,18 +110,17 @@ const workflowModule = (
110
110
  })
111
111
 
112
112
  /**
113
- * The discovery seam — a `Context.Tag`, not part of `WorkflowFiles` (that
113
+ * The discovery seam — a `Context.Service`, not part of `WorkflowFiles` (that
114
114
  * port is a plain sync record for `compileWorkflow`'s own content-file-ref
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
118
  * cannot back it. `src/twins/Layers.ts` provides the in-memory
119
119
  * counterpart instead, mirroring how `Workspace`/`Host`/`GitService` already
120
- * each have a production and a fake layer behind one Tag.
120
+ * each have a production and a fake layer behind one service.
121
121
  */
122
- export class ConfigDiscovery extends Context.Tag("ConfigDiscovery")<
123
- ConfigDiscovery,
124
- ConfigDiscoveryOps
125
- >() {
122
+ export class ConfigDiscovery extends Context.Service<ConfigDiscovery, ConfigDiscoveryOps>()(
123
+ "ConfigDiscovery",
124
+ ) {
126
125
  static Live = Layer.succeed(ConfigDiscovery, { legacyRcFiles, workflowModule })
127
126
  }
@@ -34,3 +34,4 @@ export {
34
34
  type StateName,
35
35
  } from "./Workflow.js"
36
36
  export { changedConfigFiles, contentHash, type ConfigFingerprint } from "./fingerprint.js"
37
+ export { EngineCore, pin, readPin } from "./pin.js"
@@ -4,8 +4,7 @@ import { join } from "node:path"
4
4
  import { tmpdir } from "node:os"
5
5
  import { afterEach, beforeEach, describe, expect, it } from "vitest"
6
6
  import { Cause, Effect, Exit, Layer } from "effect"
7
- import { NodeContext } from "@effect/platform-node"
8
- import { Narrator, GitService, Host, Workspace } from "../platform/index.js"
7
+ import { Narrator, GitService, Host, Workspace, NodeServices } from "../platform/index.js"
9
8
  import {
10
9
  ConfigDiscovery,
11
10
  ConfigService,
@@ -23,7 +22,7 @@ type Env = Readonly<Record<string, string>>
23
22
 
24
23
  const baseLayer = (dir: string, env: Env = {}) => {
25
24
  const hostLayer = Host.layer({ root: dir, home: homeDir, env })
26
- const gitLayer = GitService.Live.pipe(Layer.provide(Layer.merge(hostLayer, NodeContext.layer)))
25
+ const gitLayer = GitService.Live.pipe(Layer.provide(Layer.merge(hostLayer, NodeServices.layer)))
27
26
  const workspaceLayer = Workspace.Live.pipe(Layer.provide(Layer.merge(hostLayer, gitLayer)))
28
27
  return Layer.mergeAll(hostLayer, gitLayer, workspaceLayer)
29
28
  }
@@ -442,6 +441,64 @@ describe("ConfigService", () => {
442
441
  expect(Object.keys(cfg.fingerprint).sort()).toEqual(["gtd.config.ts", "steps.ts"])
443
442
  })
444
443
 
444
+ it("an ES module package gets the shipped gtd, a CommonJS one stays Node's, each module keeps its location", async () => {
445
+ const vendor = join(projectDir, "vendor", "wf")
446
+ const pkg = (name: string, manifest: object, file: string, source: string) => {
447
+ const dir = join(vendor, "node_modules", name)
448
+ mkdirSync(dir, { recursive: true })
449
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name, ...manifest }))
450
+ writeFileSync(join(dir, file), source)
451
+ }
452
+ // No @pmelab/gtd installed: only the shipped copy can serve this import.
453
+ pkg(
454
+ "dep",
455
+ { type: "module", exports: { import: "./index.js" } },
456
+ "index.js",
457
+ `import { human } from "@pmelab/gtd/flows"\nexport const file = typeof human\n`,
458
+ )
459
+ pkg("ts-dep", { main: "index.ts" }, "index.ts", `export const suffix: string = "ts"\n`)
460
+ pkg("cjs-dep", {}, "index.js", `module.exports = require("node:path").basename(__dirname)\n`)
461
+ writeFileSync(
462
+ join(vendor, "index.ts"),
463
+ [
464
+ `import { basename, dirname } from "node:path"`,
465
+ `import { fileURLToPath } from "node:url"`,
466
+ `import { file as base } from "dep"`,
467
+ `import { suffix } from "ts-dep"`,
468
+ `import cjs from "cjs-dep"`,
469
+ `const where = basename(dirname(fileURLToPath(import.meta.url)))`,
470
+ `export const file = [base, suffix, cjs, where].join("-")`,
471
+ ``,
472
+ ].join("\n"),
473
+ )
474
+ writeFileSync(
475
+ join(projectDir, "gtd.config.ts"),
476
+ [
477
+ `import { file } from "./vendor/wf/index.js"`,
478
+ optionedWorkflow("first", "{ steering: { [`.gtd/${file}.md`]: 'qa' } }"),
479
+ ].join("\n"),
480
+ )
481
+
482
+ const cfg = await getConfig()
483
+
484
+ expect(cfg.workflow.steering[".gtd/function-ts-cjs-dep-wf.md"]).toBe("qa")
485
+ expect(Object.keys(cfg.fingerprint).sort()).toEqual(["gtd.config.ts", "vendor/wf/index.ts"])
486
+ })
487
+
488
+ it("a config may require and require.resolve a local module", async () => {
489
+ writeFileSync(join(projectDir, "steps.ts"), `export const file = "required"\n`)
490
+ writeFileSync(
491
+ join(projectDir, "gtd.config.ts"),
492
+ [
493
+ `const { file } = require("./steps.js")`,
494
+ `const found = require("node:path").basename(require.resolve("./steps.ts"))`,
495
+ optionedWorkflow("first", "{ steering: { [`.gtd/${file}-${found}.md`]: 'qa' } }"),
496
+ ].join("\n"),
497
+ )
498
+
499
+ expect((await getConfig()).workflow.steering[".gtd/required-steps.ts.md"]).toBe("qa")
500
+ })
501
+
445
502
  it("the fingerprint covers gtd.config.ts but no gtd module it imports", async () => {
446
503
  const cfg = await getConfig()
447
504