@pmelab/gtd 59.0.0 → 60.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 +42 -38
- package/claude/hooks/doors.ts +8 -4
- package/dist/chunks/flows-DENt0uWV.mjs +707 -0
- package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
- package/dist/chunks/{testkit-CMBAAyRK.mjs → testkit-BvecVjQ0.mjs} +937 -772
- package/dist/chunks/{wire-C12dahrH.mjs → wire-DZ8R1RU9.mjs} +9 -2
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +1101 -1025
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +371 -82
- package/dist/testkit.mjs +3 -2
- package/dist/web/main.js +234 -232
- package/dist/wire.mjs +2 -2
- package/dist/workflow-kit.mjs +3 -3
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +68 -61
- package/src/cli/Cli.test.ts +311 -229
- package/src/cli/Cli.ts +486 -585
- package/src/cli/Help.test.ts +47 -0
- package/src/cli/Help.ts +70 -0
- package/src/cli/History.ts +9 -4
- package/src/cli/Judges.ts +1 -1
- package/src/cli/Output.test.ts +35 -0
- package/src/cli/Output.ts +31 -1
- package/src/cli/Program.test.ts +131 -82
- package/src/cli/Program.ts +197 -111
- package/src/edge/Edge.test.ts +64 -56
- package/src/edge/Edge.ts +4 -4
- package/src/emit/Emit.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/flows/factory.test.ts +14 -0
- package/src/flows/factory.ts +25 -6
- package/src/flows/helpers.test.ts +35 -0
- package/src/flows/helpers.ts +19 -10
- package/src/flows/index.test.ts +66 -0
- package/src/flows/index.ts +109 -5
- package/src/flows/internal.ts +7 -0
- package/src/flows/runtime.test.ts +192 -1
- package/src/flows/runtime.ts +188 -54
- package/src/gherkin/git-control.feature +6 -6
- package/src/gherkin/index.test.ts +127 -0
- package/src/gherkin/index.ts +481 -98
- package/src/gherkin/messages.test.ts +124 -0
- package/src/gherkin/messages.ts +101 -0
- package/src/gherkin/more-steps.feature +170 -0
- package/src/gherkin/scenario-end.feature +101 -0
- package/src/gherkin/testkit.feature +83 -7
- package/src/install/Install.test.ts +7 -7
- package/src/install/Install.ts +8 -8
- package/src/replay/Replay.test.ts +13 -2
- package/src/replay/Replay.ts +19 -30
- package/src/replay/judgeDiffShape.test.ts +1 -1
- package/src/steering/Footnotes.test.ts +1 -1
- package/src/steering/SteeringFormats.test.ts +1 -1
- package/src/steering/freeform.test.ts +1 -1
- package/src/steering/index.test.ts +27 -1
- package/src/steering/index.ts +6 -104
- package/src/steering/internal.ts +11 -0
- package/src/steering/qa.test.ts +7 -2
- package/src/steering/reads.test.ts +42 -0
- package/src/steering/reads.ts +43 -0
- package/src/steering/registry.ts +93 -0
- package/src/steering/review.test.ts +2 -2
- package/src/step/planStart.test.ts +18 -16
- package/src/step/planStart.ts +9 -9
- package/src/testkit/TestRepo.test.ts +289 -6
- package/src/testkit/TestRepo.ts +317 -41
- package/src/testkit/imports.test.ts +58 -0
- package/src/testkit/imports.ts +102 -0
- package/src/testkit/index.test.ts +28 -0
- package/src/testkit/index.ts +14 -2
- package/src/twins/EmittedScriptRecognizer.ts +7 -14
- package/src/twins/EmittedScripts.test.ts +4 -2
- package/src/twins/InMemRepo.ts +1 -0
- package/src/wire/Land.ts +1 -1
- package/src/wire/Script.ts +11 -0
- package/src/wire/WireSchema.test.ts +3 -0
- package/src/wire/golden/1/script.json +4 -0
- package/src/wire/index.ts +1 -0
- package/src/workflow/SteeringFormats.test.ts +1 -1
- package/src/workflow/compile.ts +2 -2
- package/src/workflow/discovery.test.ts +3 -3
- package/src/workflow/load.test.ts +256 -116
- package/src/workflow/load.ts +175 -152
- package/src/workflow/skills.test.ts +5 -5
- package/src/workflow/skills.ts +1 -1
- package/src/workflow-kit/index.test.ts +25 -0
- package/src/workflow-kit/index.ts +37 -4
- package/src/workflow-kit/review.ts +2 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { closest, definition, wrap } from "./Help.js"
|
|
3
|
+
|
|
4
|
+
describe("wrap", () => {
|
|
5
|
+
it("fills lines up to the width, never past it", () => {
|
|
6
|
+
expect(wrap("aaa bbb ccc ddd", 7)).toEqual(["aaa bbb", "ccc ddd"])
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it("keeps a code span on one line", () => {
|
|
10
|
+
expect(wrap("run `gtd help land` now", 12)).toEqual(["run", "`gtd help land`", "now"])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it("keeps an indented example paragraph verbatim", () => {
|
|
14
|
+
expect(wrap('text\n\n s=$(x) && sh -c "$s"', 4)).toEqual([
|
|
15
|
+
"text",
|
|
16
|
+
"",
|
|
17
|
+
' s=$(x) && sh -c "$s"',
|
|
18
|
+
])
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe("definition", () => {
|
|
23
|
+
it("puts a short term beside its description", () => {
|
|
24
|
+
expect(definition("next", "Print it", 10)).toEqual([" next Print it"])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("gives a term too wide for the column a line of its own", () => {
|
|
28
|
+
expect(definition("a-very-long-term", "Text", 10)).toEqual([
|
|
29
|
+
" a-very-long-term",
|
|
30
|
+
" Text",
|
|
31
|
+
])
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe("closest", () => {
|
|
36
|
+
const commands = ["next", "land", "history", "help"]
|
|
37
|
+
|
|
38
|
+
it("finds a dropped letter and a swapped pair", () => {
|
|
39
|
+
expect(closest("nxt", commands)).toBe("next")
|
|
40
|
+
expect(closest("lnad", commands)).toBe("land")
|
|
41
|
+
expect(closest("histroy", commands)).toBe("history")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("suggests nothing for a word unlike every candidate", () => {
|
|
45
|
+
expect(closest("frobnicate", commands)).toBeUndefined()
|
|
46
|
+
})
|
|
47
|
+
})
|
package/src/cli/Help.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const HELP_WIDTH = 80
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Word-wraps prose to `width` columns, every line prefixed by `indent`.
|
|
5
|
+
* Paragraphs are separated by a blank line; a paragraph whose lines start
|
|
6
|
+
* with two spaces is an example and is kept verbatim (indented, not wrapped).
|
|
7
|
+
*/
|
|
8
|
+
const wrapParagraph = (paragraph: string, width: number, indent: string): string[] => {
|
|
9
|
+
const lines: string[] = []
|
|
10
|
+
let line = ""
|
|
11
|
+
// A `code span` is one word: a command split across lines can't be copied.
|
|
12
|
+
for (const word of paragraph.match(/\S*`[^`]*`\S*|\S+/g) ?? []) {
|
|
13
|
+
if (line !== "" && indent.length + line.length + 1 + word.length > width) {
|
|
14
|
+
lines.push(indent + line)
|
|
15
|
+
line = word
|
|
16
|
+
} else {
|
|
17
|
+
line = line === "" ? word : `${line} ${word}`
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (line !== "") lines.push(indent + line)
|
|
21
|
+
return lines
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const wrap = (text: string, width = HELP_WIDTH, indent = ""): string[] =>
|
|
25
|
+
text
|
|
26
|
+
.split("\n\n")
|
|
27
|
+
.flatMap((paragraph, i) => [
|
|
28
|
+
...(i > 0 ? [""] : []),
|
|
29
|
+
...(paragraph.startsWith(" ")
|
|
30
|
+
? paragraph.split("\n").map((l) => indent + l)
|
|
31
|
+
: wrapParagraph(paragraph, width, indent)),
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
/** `term` and `description` side by side, the description wrapped into the column after `column`; a term too wide for it gets a line of its own. */
|
|
35
|
+
export const definition = (term: string, description: string, column: number): string[] => {
|
|
36
|
+
const cell = ` ${term}`
|
|
37
|
+
const body = wrap(description, HELP_WIDTH, " ".repeat(column))
|
|
38
|
+
if (cell.length + 1 >= column) return [cell, ...body]
|
|
39
|
+
const [first = "", ...rest] = body
|
|
40
|
+
return [cell.padEnd(column) + first.trimStart(), ...rest]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Optimal-string-alignment distance: Levenshtein plus adjacent transpositions, so `nxet` is one edit from `next`. */
|
|
44
|
+
const distance = (a: string, b: string): number => {
|
|
45
|
+
const d: number[][] = Array.from({ length: a.length + 1 }, (_, i) =>
|
|
46
|
+
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
|
|
47
|
+
)
|
|
48
|
+
for (let i = 1; i <= a.length; i++) {
|
|
49
|
+
for (let j = 1; j <= b.length; j++) {
|
|
50
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
51
|
+
let best = Math.min(d[i - 1]![j]! + 1, d[i]![j - 1]! + 1, d[i - 1]![j - 1]! + cost)
|
|
52
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
53
|
+
best = Math.min(best, d[i - 2]![j - 2]! + 1)
|
|
54
|
+
}
|
|
55
|
+
d[i]![j] = best
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return d[a.length]![b.length]!
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The candidate closest to `word`, when it is a plausible typo of it: at most one edit per three characters, and at least one. */
|
|
62
|
+
export const closest = (word: string, candidates: readonly string[]): string | undefined => {
|
|
63
|
+
const limit = Math.max(1, Math.floor(word.length / 3))
|
|
64
|
+
let best: { readonly name: string; readonly d: number } | undefined
|
|
65
|
+
for (const name of candidates) {
|
|
66
|
+
const d = distance(word, name)
|
|
67
|
+
if (d <= limit && (best === undefined || d < best.d)) best = { name, d }
|
|
68
|
+
}
|
|
69
|
+
return best?.name
|
|
70
|
+
}
|
package/src/cli/History.ts
CHANGED
|
@@ -3,7 +3,13 @@ import { historyTrail } from "../edge/index.js"
|
|
|
3
3
|
import { combinedScript, fetchHistoryRefs } from "../emit/index.js"
|
|
4
4
|
import { GitService, Narrator, PROCESS_HISTORY_PREFIX } from "../platform/index.js"
|
|
5
5
|
import { parseCommitMessage } from "../replay/index.js"
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
writeJson,
|
|
8
|
+
writeScript,
|
|
9
|
+
type ArtifactOut,
|
|
10
|
+
type CommandRequirements,
|
|
11
|
+
type JsonMode,
|
|
12
|
+
} from "./Output.js"
|
|
7
13
|
|
|
8
14
|
export interface HistoryEntry {
|
|
9
15
|
readonly ref: string
|
|
@@ -138,8 +144,7 @@ export const runHistoryShowCommand = (
|
|
|
138
144
|
/** Prints the script and never runs git: gtd's promise that it only decides and prints. */
|
|
139
145
|
export const runHistoryFetchCommand = (
|
|
140
146
|
remote: string,
|
|
147
|
+
json: JsonMode,
|
|
141
148
|
out: ArtifactOut,
|
|
142
149
|
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
143
|
-
|
|
144
|
-
out.write(`${combinedScript(fetchHistoryRefs(remote), "")}\n`)
|
|
145
|
-
})
|
|
150
|
+
writeScript(json, out, `${combinedScript(fetchHistoryRefs(remote), "")}\n`)
|
package/src/cli/Judges.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Effect, Option } from "effect"
|
|
|
2
2
|
import { answered, numeric } from "../flows/index.js"
|
|
3
3
|
import { GitService } from "../platform/index.js"
|
|
4
4
|
import { parseCommitMessage, type JudgeVerdict } from "../replay/index.js"
|
|
5
|
-
import { headingSectionBodies } from "../steering/
|
|
5
|
+
import { headingSectionBodies } from "../steering/internal.js"
|
|
6
6
|
import { selectHistoryEntries, type HistoryEntry } from "./History.js"
|
|
7
7
|
import { printable } from "./Metrics.js"
|
|
8
8
|
import { writeJson, type ArtifactOut, type CommandRequirements, type JsonMode } from "./Output.js"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Effect, Exit } from "effect"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
import { WIRE_SCHEMA } from "../wire/index.js"
|
|
4
|
+
import { writeScript, type JsonMode } from "./Output.js"
|
|
5
|
+
|
|
6
|
+
const run = (json: JsonMode, script: string, plain?: string) => {
|
|
7
|
+
let written = ""
|
|
8
|
+
const out = { write: (chunk: string) => (written += chunk), flush: () => {} }
|
|
9
|
+
const exit = Effect.runSyncExit(writeScript(json, out, script, plain))
|
|
10
|
+
return { exit, written }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("writeScript", () => {
|
|
14
|
+
it("plain prints the script, or the plain text when given", () => {
|
|
15
|
+
expect(run({ kind: "off" }, "git reset\n").written).toBe("git reset\n")
|
|
16
|
+
expect(run({ kind: "off" }, "", "nothing to do\n").written).toBe("nothing to do\n")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("--json prints { schema, script }, the script ending in exactly one newline", () => {
|
|
20
|
+
expect(JSON.parse(run({ kind: "document" }, "git reset\n\n").written)).toEqual({
|
|
21
|
+
schema: WIRE_SCHEMA,
|
|
22
|
+
script: "git reset\n",
|
|
23
|
+
})
|
|
24
|
+
expect(JSON.parse(run({ kind: "document" }, "", "nothing\n").written)).toEqual({
|
|
25
|
+
schema: WIRE_SCHEMA,
|
|
26
|
+
script: "",
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("--json=script prints the script alone; an unknown path fails", () => {
|
|
31
|
+
expect(run({ kind: "select", path: "script" }, "git reset").written).toBe("git reset\n\n")
|
|
32
|
+
expect(run({ kind: "select", path: "schema" }, "x").written).toBe(`${WIRE_SCHEMA}\n`)
|
|
33
|
+
expect(Exit.isFailure(run({ kind: "select", path: "nope" }, "x").exit)).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
})
|
package/src/cli/Output.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import { CommandRunner, GitService, Host, Narrator, Workspace } from "../platform/index.js"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
renderScriptJson,
|
|
5
|
+
scriptFields,
|
|
6
|
+
selectPath,
|
|
7
|
+
type EXIT_OK,
|
|
8
|
+
type EXIT_STRAYS_FOUND,
|
|
9
|
+
} from "../wire/index.js"
|
|
4
10
|
import { ConfigDiscovery, ConfigService } from "../workflow/index.js"
|
|
5
11
|
import type { ToolCommands } from "./ToolCommands.js"
|
|
6
12
|
|
|
@@ -81,3 +87,27 @@ export const writeJson = (
|
|
|
81
87
|
else yield* writeSelection(out, document, json.path)
|
|
82
88
|
return true
|
|
83
89
|
})
|
|
90
|
+
|
|
91
|
+
/** Exactly one trailing newline: a script embedded in a `--json` document is never re-normalized at its tail. */
|
|
92
|
+
export const normalizeScriptNewline = (script: string): string =>
|
|
93
|
+
script.length === 0 ? script : `${script.replace(/\n+$/, "")}\n`
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A script-printing command's output: the script itself (or `plain`, when
|
|
97
|
+
* there is none to run), `{ schema, script }` under `--json`, or one field.
|
|
98
|
+
*/
|
|
99
|
+
export const writeScript = (
|
|
100
|
+
json: JsonMode,
|
|
101
|
+
out: ArtifactOut,
|
|
102
|
+
script: string,
|
|
103
|
+
plain: string = script,
|
|
104
|
+
): Effect.Effect<void, Error> =>
|
|
105
|
+
Effect.gen(function* () {
|
|
106
|
+
if (json.kind === "off") {
|
|
107
|
+
out.write(plain)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
const fields = scriptFields(normalizeScriptNewline(script))
|
|
111
|
+
if (json.kind === "document") out.write(renderScriptJson(fields))
|
|
112
|
+
else yield* writeSelection(out, fields, json.path)
|
|
113
|
+
})
|