@pmelab/gtd 38.0.0 → 38.2.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 +36 -3
- package/dist/chunks/{Schema-DO5ttJaI.mjs → Schema-C6fLlQzx.mjs} +1 -1
- package/dist/chunks/{reserved-sfMdy7v7.mjs → reserved-C3PgP7XB.mjs} +56 -12
- package/dist/chunks/{tail-Cgca5Gp-.mjs → tail-Bkxki2nK.mjs} +1 -1
- package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
- package/dist/gtd.bundle.mjs +292 -53
- package/dist/testkit.mjs +257 -21
- package/dist/wire.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +158 -42
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +230 -70
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +15 -3
- package/src/cli/Cli.ts +61 -28
- package/src/cli/History.ts +1 -19
- package/src/cli/Metrics.test.ts +224 -0
- package/src/cli/Metrics.ts +310 -0
- package/src/cli/Output.ts +13 -0
- package/src/cli/Program.test.ts +3 -1
- package/src/cli/Program.ts +4 -0
- package/src/testkit/steps.ts +4 -0
- package/src/wire/ContextLimit.ts +1 -1
- package/src/workflows/feature/continuation-cap.feature +65 -0
- package/src/workflows/feature/continuation-rebuild.feature +88 -0
- package/src/workflows/feature/continuation-slices.feature +64 -0
- package/src/workflows/feature/continuation.feature +205 -0
- package/src/workflows/feature/criteria.ts +11 -0
- package/src/workflows/feature/refactor.ts +64 -30
- package/src/workflows/feature/seam.ts +102 -30
- package/src/workflows/shared/continuation.ts +92 -0
- package/src/workflows/shared/options.ts +2 -0
- package/src/workflows/shared/steps.ts +1 -0
- package/src/workflows/shared/text.test.ts +6 -2
- package/src/workflows/shared/text.ts +104 -8
- package/src/workflows/shared/vars.ts +5 -0
package/src/cli/Cli.ts
CHANGED
|
@@ -97,29 +97,33 @@ const share = (raw: string, flag: string): Either.Either<number, string> => {
|
|
|
97
97
|
: Either.left(`gtd: ${flag} must be a share of the context window in (0, 1] — got "${raw}"`)
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
const JSON_KINDS: ReadonlySet<Command["kind"] | undefined> = new Set<Command["kind"]>([
|
|
101
|
+
"next",
|
|
102
|
+
"land",
|
|
103
|
+
"judge",
|
|
104
|
+
"judgeAnswer",
|
|
105
|
+
"doors",
|
|
106
|
+
"slices",
|
|
107
|
+
"historyList",
|
|
108
|
+
"historyShow",
|
|
109
|
+
"historyMetrics",
|
|
110
|
+
"strays",
|
|
111
|
+
])
|
|
112
|
+
|
|
100
113
|
const FLAGS: readonly FlagRow[] = [
|
|
101
114
|
{
|
|
102
115
|
name: "--json",
|
|
103
116
|
arity: "optional",
|
|
104
117
|
repeatable: false,
|
|
105
|
-
scope: (kind) =>
|
|
106
|
-
kind === "next" ||
|
|
107
|
-
kind === "land" ||
|
|
108
|
-
kind === "judge" ||
|
|
109
|
-
kind === "judgeAnswer" ||
|
|
110
|
-
kind === "doors" ||
|
|
111
|
-
kind === "slices" ||
|
|
112
|
-
kind === "historyList" ||
|
|
113
|
-
kind === "historyShow" ||
|
|
114
|
-
kind === "strays",
|
|
118
|
+
scope: (kind) => JSON_KINDS.has(kind),
|
|
115
119
|
decode: ([raw]) => Either.right(raw ?? ""),
|
|
116
120
|
scopeError:
|
|
117
|
-
"gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd slices`/`gtd history list`/`gtd history show`/`gtd strays` — " +
|
|
121
|
+
"gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd slices`/`gtd history list`/`gtd history show`/`gtd history metrics`/`gtd strays` — " +
|
|
118
122
|
"every other command prints plain text; see `gtd install` for the driver protocol briefing",
|
|
119
123
|
valueHint: "<path>",
|
|
120
124
|
help: [
|
|
121
125
|
"(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd slices/gtd history",
|
|
122
|
-
"list/gtd history show/gtd strays only) output structured JSON. Bare --json prints the whole document;",
|
|
126
|
+
"list/gtd history show/gtd history metrics/gtd strays only) output structured JSON. Bare --json prints the whole document;",
|
|
123
127
|
"--json=<path> (a dotted key path into that document, e.g.",
|
|
124
128
|
"kind, content, session.id) prints just that value: a",
|
|
125
129
|
"scalar raw and unquoted, a boolean as true/false, a list",
|
|
@@ -445,6 +449,8 @@ type Arity =
|
|
|
445
449
|
| { readonly name: string; readonly variadic: string }
|
|
446
450
|
// At most one trailing positional.
|
|
447
451
|
| { readonly name: string; readonly optional: true }
|
|
452
|
+
// Any number of positionals, none included.
|
|
453
|
+
| { readonly name: string; readonly many: true }
|
|
448
454
|
|
|
449
455
|
interface CommandRow {
|
|
450
456
|
readonly token: string
|
|
@@ -824,6 +830,22 @@ const COMMAND_ROWS: readonly CommandRow[] = [
|
|
|
824
830
|
"prints the trail as one document; writes nothing",
|
|
825
831
|
],
|
|
826
832
|
},
|
|
833
|
+
{
|
|
834
|
+
token: "history metrics",
|
|
835
|
+
kind: "historyMetrics",
|
|
836
|
+
arity: { name: "ref", many: true },
|
|
837
|
+
details: [
|
|
838
|
+
"Report how the kept processes went: slices whose naive pass",
|
|
839
|
+
"landed or was rebuilt (the refactor-first rate), continuations",
|
|
840
|
+
"per phase, Gtd-Cost by model and phase with the double work of",
|
|
841
|
+
"a rebuild, and quality-lap findings per lens — per process and",
|
|
842
|
+
"in total. Each <ref> is read as by history show, but a branch",
|
|
843
|
+
"name means every process kept from it; none means every ref.",
|
|
844
|
+
"Reads commit subjects and trailers, never replays, so a",
|
|
845
|
+
"changed config is no refusal. --json prints { processes, total",
|
|
846
|
+
"} as one document; writes nothing",
|
|
847
|
+
],
|
|
848
|
+
},
|
|
827
849
|
{
|
|
828
850
|
token: "history fetch",
|
|
829
851
|
kind: "historyFetch",
|
|
@@ -883,7 +905,9 @@ export const renderHelp = (): string => {
|
|
|
883
905
|
? `${row.token} <${row.arity.name}> [${row.arity.variadic}...]`
|
|
884
906
|
: "optional" in row.arity
|
|
885
907
|
? `${row.token} [${row.arity.name}]`
|
|
886
|
-
:
|
|
908
|
+
: "many" in row.arity
|
|
909
|
+
? `${row.token} [${row.arity.name}...]`
|
|
910
|
+
: `${row.token} <${row.arity.name}>`
|
|
887
911
|
return renderBlock(header, row.details)
|
|
888
912
|
})
|
|
889
913
|
const commands = [
|
|
@@ -1051,23 +1075,23 @@ const conflictViolation = (present: ReadonlySet<string>): string | undefined =>
|
|
|
1051
1075
|
return undefined
|
|
1052
1076
|
}
|
|
1053
1077
|
|
|
1078
|
+
const tooMany = (
|
|
1079
|
+
cmd: string,
|
|
1080
|
+
rest: readonly string[],
|
|
1081
|
+
expected: string,
|
|
1082
|
+
max: number,
|
|
1083
|
+
): string | undefined =>
|
|
1084
|
+
rest.length > max
|
|
1085
|
+
? `gtd ${cmd}: too many arguments — expected ${expected}, got: ${rest.join(", ")}`
|
|
1086
|
+
: undefined
|
|
1087
|
+
|
|
1054
1088
|
const arityError = (cmd: string, rest: readonly string[], arity: Arity): string | undefined => {
|
|
1055
|
-
if (arity === "none")
|
|
1056
|
-
return rest.length > 0
|
|
1057
|
-
? `gtd ${cmd}: too many arguments — expected none, got: ${rest.join(", ")}`
|
|
1058
|
-
: undefined
|
|
1059
|
-
}
|
|
1089
|
+
if (arity === "none") return tooMany(cmd, rest, "none", 0)
|
|
1060
1090
|
if ("names" in arity) return pairArityError(cmd, rest, arity.names)
|
|
1061
|
-
if ("
|
|
1062
|
-
|
|
1063
|
-
? `gtd ${cmd}: too many arguments — expected at most one ${arity.name}, got: ${rest.join(", ")}`
|
|
1064
|
-
: undefined
|
|
1065
|
-
}
|
|
1091
|
+
if ("many" in arity) return undefined
|
|
1092
|
+
if ("optional" in arity) return tooMany(cmd, rest, `at most one ${arity.name}`, 1)
|
|
1066
1093
|
if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`
|
|
1067
|
-
|
|
1068
|
-
return `gtd ${cmd}: too many arguments — expected one ${arity.name}, got: ${rest.join(", ")}`
|
|
1069
|
-
}
|
|
1070
|
-
return undefined
|
|
1094
|
+
return "variadic" in arity ? undefined : tooMany(cmd, rest, `one ${arity.name}`, 1)
|
|
1071
1095
|
}
|
|
1072
1096
|
|
|
1073
1097
|
const pairArityError = (
|
|
@@ -1138,7 +1162,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1138
1162
|
|
|
1139
1163
|
if (isHistory && row === undefined) {
|
|
1140
1164
|
return usagePlan(
|
|
1141
|
-
"gtd history: expected a subcommand — list, show <ref
|
|
1165
|
+
"gtd history: expected a subcommand — list, show <ref>, metrics [ref...] or fetch [remote]",
|
|
1142
1166
|
jsonSeen,
|
|
1143
1167
|
)
|
|
1144
1168
|
}
|
|
@@ -1381,6 +1405,15 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1381
1405
|
}
|
|
1382
1406
|
}
|
|
1383
1407
|
|
|
1408
|
+
if (kind === "historyMetrics") {
|
|
1409
|
+
return {
|
|
1410
|
+
kind: "command",
|
|
1411
|
+
command: { kind: "historyMetrics", refs: restPositionals },
|
|
1412
|
+
json,
|
|
1413
|
+
verbose,
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1384
1417
|
if (kind === "historyFetch") {
|
|
1385
1418
|
return {
|
|
1386
1419
|
kind: "command",
|
package/src/cli/History.ts
CHANGED
|
@@ -3,12 +3,7 @@ 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 {
|
|
7
|
-
writeSelection,
|
|
8
|
-
type ArtifactOut,
|
|
9
|
-
type CommandRequirements,
|
|
10
|
-
type JsonMode,
|
|
11
|
-
} from "./Output.js"
|
|
6
|
+
import { writeJson, type ArtifactOut, type CommandRequirements, type JsonMode } from "./Output.js"
|
|
12
7
|
|
|
13
8
|
export interface HistoryEntry {
|
|
14
9
|
readonly ref: string
|
|
@@ -80,19 +75,6 @@ export const resolveHistoryRef = (
|
|
|
80
75
|
|
|
81
76
|
const seven = (hash: string): string => hash.slice(0, 7)
|
|
82
77
|
|
|
83
|
-
/** Writes `document` per `json` (bare → whole, `=path` → one value); returns false for `off`. */
|
|
84
|
-
const writeJson = (
|
|
85
|
-
json: JsonMode,
|
|
86
|
-
out: ArtifactOut,
|
|
87
|
-
document: unknown,
|
|
88
|
-
): Effect.Effect<boolean, Error> =>
|
|
89
|
-
Effect.gen(function* () {
|
|
90
|
-
if (json.kind === "off") return false
|
|
91
|
-
if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`)
|
|
92
|
-
else yield* writeSelection(out, document, json.path)
|
|
93
|
-
return true
|
|
94
|
-
})
|
|
95
|
-
|
|
96
78
|
export const runHistoryListCommand = (
|
|
97
79
|
json: JsonMode,
|
|
98
80
|
out: ArtifactOut,
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
import { applyEmittedScript, InMemRepo, makeCapturingCliIo, testLayers } from "../testkit/index.js"
|
|
4
|
+
import { runCli } from "./index.js"
|
|
5
|
+
import { processMetrics, readProcessMetrics, totalMetrics, type MetricCommit } from "./Metrics.js"
|
|
6
|
+
|
|
7
|
+
const turn = (subject: string, opts: { cost?: string; findings?: number } = {}): MetricCommit => ({
|
|
8
|
+
message: opts.cost === undefined ? subject : `${subject}\n\nGtd-Cost: ${opts.cost}`,
|
|
9
|
+
findings: opts.findings ?? 0,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe("processMetrics", () => {
|
|
13
|
+
it("counts a naive pass applied as it is as a landed slice", () => {
|
|
14
|
+
const m = processMetrics([
|
|
15
|
+
turn("gtd(human): idle → seam.naive"),
|
|
16
|
+
turn("gtd(agent): seam.naive → seam.check", { cost: "0.4 coder" }),
|
|
17
|
+
turn("gtd(check): seam.check → implement.applying"),
|
|
18
|
+
turn("gtd(check): implement.applying → idle"),
|
|
19
|
+
])
|
|
20
|
+
expect(m.slices).toEqual({ decided: 1, landed: 1, rebuilt: 0, refactored: 0 })
|
|
21
|
+
expect(m.refactorFirstRate).toBe(0)
|
|
22
|
+
expect(m.cost.byPhase).toEqual({ naive: 0.4, refactor: 0, implement: 0, other: 0 })
|
|
23
|
+
expect(m.cost.doubleWork).toEqual({ naive: 0, rebuild: 0, total: 0 })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("charges a rebuilt slice's naive pass and rebuild as double work, refactor apart", () => {
|
|
27
|
+
const m = processMetrics([
|
|
28
|
+
turn("gtd(human): idle → seam.naive"),
|
|
29
|
+
turn("gtd(agent): seam.naive → seam.continue.naive", { cost: "0.4 coder" }),
|
|
30
|
+
turn("gtd(agent): seam.continue.naive → seam.judge", { cost: "0.2 coder" }),
|
|
31
|
+
turn("gtd(judge): seam.judge → seam.note", { cost: "0.01 judge" }),
|
|
32
|
+
turn("gtd(agent): seam.note → refactor.deriving", { cost: "0.05 coder" }),
|
|
33
|
+
turn("gtd(check): refactor.deriving → refactor.building"),
|
|
34
|
+
turn("gtd(agent): refactor.building → refactor.closing", { cost: "0.3 coder" }),
|
|
35
|
+
turn("gtd(check): refactor.closing → implement.building"),
|
|
36
|
+
turn("gtd(agent): implement.building → implement.continue.building", { cost: "0.5 coder" }),
|
|
37
|
+
turn("gtd(agent): implement.continue.building → idle", { cost: "0.25 coder" }),
|
|
38
|
+
])
|
|
39
|
+
expect(m.slices).toEqual({ decided: 1, landed: 0, rebuilt: 1, refactored: 1 })
|
|
40
|
+
expect(m.refactorFirstRate).toBe(1)
|
|
41
|
+
expect(m.continuations).toEqual({ "seam.naive": 1, "implement.building": 1 })
|
|
42
|
+
expect(m.cost.total).toBe(1.71)
|
|
43
|
+
expect(m.cost.byModel).toEqual({ coder: 1.7, judge: 0.01 })
|
|
44
|
+
expect(m.cost.byPhase).toEqual({ naive: 0.6, refactor: 0.35, implement: 0.75, other: 0.01 })
|
|
45
|
+
expect(m.cost.doubleWork).toEqual({ naive: 0.6, rebuild: 0.75, total: 1.35 })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("rebuilds without a refactor when the slice leaves the seam for a build turn", () => {
|
|
49
|
+
const m = processMetrics([
|
|
50
|
+
turn("gtd(judge): seam.judge → implement.building"),
|
|
51
|
+
turn("gtd(agent): implement.building → idle"),
|
|
52
|
+
])
|
|
53
|
+
expect(m.slices).toEqual({ decided: 1, landed: 0, rebuilt: 1, refactored: 0 })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("keeps each slice's decision apart", () => {
|
|
57
|
+
const m = processMetrics([
|
|
58
|
+
turn("gtd(agent): seam.naive → seam.check", { cost: "1" }),
|
|
59
|
+
turn("gtd(check): seam.check → implement.applying"),
|
|
60
|
+
turn("gtd(agent): implement.building → implement.health.check", { cost: "1" }),
|
|
61
|
+
turn("gtd(check): implement.closing → seam.naive"),
|
|
62
|
+
turn("gtd(agent): seam.naive → seam.note", { cost: "2" }),
|
|
63
|
+
turn("gtd(agent): seam.note → refactor.deriving"),
|
|
64
|
+
turn("gtd(agent): refactor.continue.building → refactor.closing", { cost: "1" }),
|
|
65
|
+
turn("gtd(check): refactor.closing → implement.building"),
|
|
66
|
+
turn("gtd(agent): implement.building → idle", { cost: "3" }),
|
|
67
|
+
])
|
|
68
|
+
expect(m.slices).toEqual({ decided: 2, landed: 1, rebuilt: 1, refactored: 1 })
|
|
69
|
+
expect(m.refactorFirstRate).toBe(0.5)
|
|
70
|
+
expect(m.continuations).toEqual({ "refactor.building": 1 })
|
|
71
|
+
expect(m.cost.byModel).toEqual({ unspecified: 8 })
|
|
72
|
+
expect(m.cost.doubleWork).toEqual({ naive: 2, rebuild: 3, total: 5 })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("counts a naive retry in a fresh conversation as naive cost", () => {
|
|
76
|
+
const m = processMetrics([
|
|
77
|
+
turn("gtd(agent): seam.continue.naive → seam.judge", { cost: "1" }),
|
|
78
|
+
turn("gtd(judge): seam.judge → seam.fresh.naive"),
|
|
79
|
+
turn("gtd(agent): seam.fresh.naive → seam.judge", { cost: "2" }),
|
|
80
|
+
turn("gtd(judge): seam.judge → implement.building"),
|
|
81
|
+
])
|
|
82
|
+
expect(m.cost.byPhase.naive).toBe(3)
|
|
83
|
+
expect(m.cost.doubleWork.naive).toBe(3)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("turns a landed slice whose naive pass fails to apply into a rebuild", () => {
|
|
87
|
+
const m = processMetrics([
|
|
88
|
+
turn("gtd(agent): seam.naive → seam.check", { cost: "1" }),
|
|
89
|
+
turn("gtd(check): seam.check → implement.applying"),
|
|
90
|
+
turn("gtd(check): implement.applying → implement.building"),
|
|
91
|
+
turn("gtd(agent): implement.building → implement.health.check", { cost: "2" }),
|
|
92
|
+
])
|
|
93
|
+
expect(m.slices).toEqual({ decided: 1, landed: 0, rebuilt: 1, refactored: 0 })
|
|
94
|
+
expect(m.cost.doubleWork).toEqual({ naive: 1, rebuild: 2, total: 3 })
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("keeps the refactor lap's lens findings apart from the feature lap's", () => {
|
|
98
|
+
const m = processMetrics([
|
|
99
|
+
turn("gtd(agent): refactor.build.quality.correctness.reviewing → refactor.closing", {
|
|
100
|
+
findings: 3,
|
|
101
|
+
}),
|
|
102
|
+
turn("gtd(agent): build.quality.correctness.reviewing → idle", { findings: 2 }),
|
|
103
|
+
])
|
|
104
|
+
expect(m.findings.byLens).toEqual({
|
|
105
|
+
"refactor.build.quality.correctness": 3,
|
|
106
|
+
"build.quality.correctness": 2,
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("ends a rebuild's double work at the slice's closing", () => {
|
|
111
|
+
const m = processMetrics([
|
|
112
|
+
turn("gtd(judge): seam.judge → implement.building"),
|
|
113
|
+
turn("gtd(agent): implement.building → implement.closing", { cost: "1" }),
|
|
114
|
+
turn("gtd(check): implement.closing → build.quality.correctness.reviewing"),
|
|
115
|
+
turn("gtd(agent): implement.building → idle", { cost: "2" }),
|
|
116
|
+
])
|
|
117
|
+
expect(m.cost.doubleWork.rebuild).toBe(1)
|
|
118
|
+
expect(m.cost.byPhase.implement).toBe(3)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("keeps a model named like an Object member a plain key", () => {
|
|
122
|
+
const m = processMetrics([
|
|
123
|
+
turn("gtd(agent): seam.naive → seam.check", { cost: "0.4 constructor" }),
|
|
124
|
+
turn("gtd(agent): seam.naive → seam.check", { cost: "0.1 __proto__" }),
|
|
125
|
+
])
|
|
126
|
+
expect(m.cost.total).toBe(0.5)
|
|
127
|
+
expect(Object.entries(m.cost.byModel)).toEqual([
|
|
128
|
+
["constructor", 0.4],
|
|
129
|
+
["__proto__", 0.1],
|
|
130
|
+
])
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it("has no refactor-first rate without a seam decision", () => {
|
|
134
|
+
expect(processMetrics([turn("gtd(human): idle → building")]).refactorFirstRate).toBeNull()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("counts continuations only for agent turns, the bare form included", () => {
|
|
138
|
+
const m = processMetrics([
|
|
139
|
+
turn("gtd(agent): seam.continue.naive"),
|
|
140
|
+
turn("gtd(human): seam.continue.naive → seam.oversized"),
|
|
141
|
+
])
|
|
142
|
+
expect(m.continuations).toEqual({ "seam.naive": 1 })
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("sums findings per quality lens and skips commits gtd did not write", () => {
|
|
146
|
+
const m = processMetrics([
|
|
147
|
+
turn(
|
|
148
|
+
"gtd(agent): build.quality.correctness.reviewing → build.quality.conventions.reviewing",
|
|
149
|
+
{
|
|
150
|
+
findings: 2,
|
|
151
|
+
},
|
|
152
|
+
),
|
|
153
|
+
turn("gtd(agent): build.quality.conventions.reviewing → build.fix.quality.fixing", {
|
|
154
|
+
findings: 1,
|
|
155
|
+
}),
|
|
156
|
+
turn("fix: a hand-made commit", { cost: "5" }),
|
|
157
|
+
])
|
|
158
|
+
expect(m.findings).toEqual({
|
|
159
|
+
total: 3,
|
|
160
|
+
byLens: { "build.quality.correctness": 2, "build.quality.conventions": 1 },
|
|
161
|
+
})
|
|
162
|
+
expect(m.cost.total).toBe(0)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe("totalMetrics", () => {
|
|
167
|
+
it("adds processes and recomputes the rate over every decision", () => {
|
|
168
|
+
const a = processMetrics([
|
|
169
|
+
turn("gtd(agent): seam.naive → seam.check", { cost: "1 a" }),
|
|
170
|
+
turn("gtd(check): seam.check → implement.applying"),
|
|
171
|
+
])
|
|
172
|
+
const b = processMetrics([
|
|
173
|
+
turn("gtd(agent): seam.continue.naive → seam.note", { cost: "2 b" }),
|
|
174
|
+
turn("gtd(agent): seam.note → refactor.deriving"),
|
|
175
|
+
turn("gtd(agent): seam.naive → seam.note"),
|
|
176
|
+
turn("gtd(agent): seam.note → refactor.deriving"),
|
|
177
|
+
])
|
|
178
|
+
const total = totalMetrics([a, b])
|
|
179
|
+
expect(total.processes).toBe(2)
|
|
180
|
+
expect(total.slices).toEqual({ decided: 3, landed: 1, rebuilt: 2, refactored: 2 })
|
|
181
|
+
expect(total.refactorFirstRate).toBe(0.667)
|
|
182
|
+
expect(total.continuations).toEqual({ "seam.naive": 1 })
|
|
183
|
+
expect(total.cost.byModel).toEqual({ a: 1, b: 2 })
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const CONFIG = `import { agent, human, scope } from "@pmelab/gtd/flows"
|
|
188
|
+
|
|
189
|
+
export default async () => {
|
|
190
|
+
await human("idle")
|
|
191
|
+
await scope("build", () => scope("quality", () => scope("correctness", () => agent("reviewing", "review"))))
|
|
192
|
+
}
|
|
193
|
+
`
|
|
194
|
+
|
|
195
|
+
const land = async (repo: InMemRepo, files: Record<string, string>): Promise<string> => {
|
|
196
|
+
for (const [path, content] of Object.entries(files)) repo.writeFile(path, content)
|
|
197
|
+
const { io, result } = makeCapturingCliIo(repo)
|
|
198
|
+
await Effect.runPromise(runCli(["node", "gtd.js", "land", "--json"], io))
|
|
199
|
+
const { stdout, exitCode, stderr } = result()
|
|
200
|
+
if (exitCode !== 0) throw new Error(`land exited ${exitCode}: ${stderr}`)
|
|
201
|
+
const outcome = applyEmittedScript(
|
|
202
|
+
repo,
|
|
203
|
+
new Map(),
|
|
204
|
+
(JSON.parse(stdout) as { script: string }).script,
|
|
205
|
+
)
|
|
206
|
+
if (!outcome.ok) throw new Error(outcome.error)
|
|
207
|
+
return repo.resolveRef("HEAD")!
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
describe("readProcessMetrics", () => {
|
|
211
|
+
it("counts the findings a reviewing turn added to an existing QUALITY.md", async () => {
|
|
212
|
+
const repo = new InMemRepo()
|
|
213
|
+
repo.writeFile("gtd.config.ts", CONFIG)
|
|
214
|
+
repo.writeFile(".gtd/QUALITY.md", "## old finding\n")
|
|
215
|
+
repo.seedCommit("chore: add workflow")
|
|
216
|
+
const entry = await land(repo, { "NOTE.md": "go\n" })
|
|
217
|
+
const tip = await land(repo, { ".gtd/QUALITY.md": "## old finding\n\n## one\n\n## two\n" })
|
|
218
|
+
const metrics = await Effect.runPromise(
|
|
219
|
+
readProcessMetrics({ entry, tip }).pipe(Effect.provide(testLayers(repo, {}))),
|
|
220
|
+
)
|
|
221
|
+
expect(metrics.findings).toEqual({ total: 2, byLens: { "build.quality.correctness": 2 } })
|
|
222
|
+
expect(metrics.workflow).toBeNull()
|
|
223
|
+
})
|
|
224
|
+
})
|