@pmelab/gtd 42.0.0 → 44.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 +96 -48
- package/claude/hooks/sketch.ts +1 -1
- package/dist/chunks/{reserved-D7MNY9QJ.mjs → reserved-DJk_6-ju.mjs} +75 -46
- package/dist/chunks/review-X7Jfz0dY.mjs +249 -0
- package/dist/chunks/{steering-oCDdQlpP.mjs → steering-D4LUQ-av.mjs} +95 -12
- package/dist/chunks/{tail-D2o2h5cc.mjs → tail-5w5mxGTq.mjs} +3 -3
- package/dist/gtd.bundle.mjs +488 -30
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +447 -27
- package/dist/web/main.js +42 -13
- package/dist/workflow-kit.mjs +61 -25
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +7 -290
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +27 -7
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +287 -161
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +33 -1
- package/src/cli/Cli.ts +70 -6
- package/src/cli/History.ts +15 -0
- package/src/cli/Judges.test.ts +373 -0
- package/src/cli/Judges.ts +624 -0
- package/src/cli/Metrics.ts +3 -16
- package/src/cli/Program.test.ts +3 -1
- package/src/cli/Program.ts +9 -0
- package/src/steering/FORMATS.md +4 -4
- package/src/steering/index.ts +12 -1
- package/src/steering/review.test.ts +107 -0
- package/src/steering/review.ts +159 -9
- package/src/testkit/EmittedScriptRecognizer.ts +0 -14
- package/src/testkit/steps.ts +4 -0
- package/src/workflow-kit/review.test.ts +170 -26
- package/src/workflow-kit/review.ts +95 -19
- package/src/workflows/feature/coverage.ts +1 -1
- package/src/workflows/feature/feature.test.ts +8 -0
- package/src/workflows/feature/feature.ts +2 -2
- package/src/workflows/feature/index.ts +1 -1
- package/src/workflows/feature/lens-review.feature +16 -4
- package/src/workflows/feature/refactor.ts +1 -1
- package/src/workflows/feature/review-answers.feature +179 -0
- package/src/workflows/feature/seam.ts +1 -1
- package/src/workflows/feature/steps.test.ts +2 -76
- package/src/workflows/feature/steps.ts +5 -48
- package/src/workflows/fix/fix.feature +20 -21
- package/src/workflows/index.ts +1 -1
- package/src/workflows/review/review-door.feature +149 -0
- package/src/workflows/review/review.test.ts +1 -73
- package/src/workflows/review/review.ts +34 -6
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +6 -5
- package/src/workflows/shared/access.test.ts +2 -11
- package/src/workflows/shared/access.ts +0 -2
- package/src/workflows/{feature → shared}/review.test.ts +9 -61
- package/src/workflows/{feature → shared}/review.ts +40 -48
- package/src/workflows/shared/skills.test.ts +2 -0
- package/src/workflows/shared/skills.ts +2 -0
- package/src/workflows/shared/steps.test.ts +64 -2
- package/src/workflows/shared/steps.ts +40 -1
- package/src/workflows/shared/tail.ts +2 -2
- package/src/workflows/shared/text.test.ts +40 -89
- package/src/workflows/shared/text.ts +63 -54
package/package.json
CHANGED
package/src/cli/Cli.test.ts
CHANGED
|
@@ -171,7 +171,8 @@ describe("parseArgv — scope", () => {
|
|
|
171
171
|
it("--var without --workflow is rejected", () => {
|
|
172
172
|
const plan = parseArgv(["node", "gtd.js", "land", "--var", "a=1"])
|
|
173
173
|
expect(plan.kind).toBe("usage")
|
|
174
|
-
if (plan.kind === "usage")
|
|
174
|
+
if (plan.kind === "usage")
|
|
175
|
+
expect(plan.message).toBe("gtd: --var requires --workflow (or `gtd history judges`)")
|
|
175
176
|
})
|
|
176
177
|
|
|
177
178
|
it("--cost with --workflow is rejected (landing and starting are different verbs)", () => {
|
|
@@ -1027,6 +1028,34 @@ describe("parseArgv — gtd history", () => {
|
|
|
1027
1028
|
expect(parse("history", "metrics", "a", "feat/b")).toMatchObject({
|
|
1028
1029
|
command: { kind: "historyMetrics", refs: ["a", "feat/b"] },
|
|
1029
1030
|
})
|
|
1031
|
+
expect(parse("history", "judges", "a", "--var", "qualityTriageMinP=0.9")).toMatchObject({
|
|
1032
|
+
command: {
|
|
1033
|
+
kind: "historyJudges",
|
|
1034
|
+
refs: ["a"],
|
|
1035
|
+
vars: { qualityTriageMinP: "0.9" },
|
|
1036
|
+
jsonl: false,
|
|
1037
|
+
},
|
|
1038
|
+
})
|
|
1039
|
+
expect(parse("history", "judges", "--jsonl")).toMatchObject({
|
|
1040
|
+
command: { kind: "historyJudges", refs: [], vars: {}, jsonl: true },
|
|
1041
|
+
})
|
|
1042
|
+
})
|
|
1043
|
+
|
|
1044
|
+
it("judges takes only judge thresholds as --var, and --jsonl alone", () => {
|
|
1045
|
+
expect(usage(parse("history", "judges", "--var", "reviewBase=main"))).toContain(
|
|
1046
|
+
"no judge threshold",
|
|
1047
|
+
)
|
|
1048
|
+
expect(usage(parse("history", "judges", "--var", "toString=0.9"))).toContain(
|
|
1049
|
+
"no judge threshold",
|
|
1050
|
+
)
|
|
1051
|
+
expect(usage(parse("history", "judges", "--var", "qualityTriageMinP=0,9"))).toContain(
|
|
1052
|
+
"must be a number",
|
|
1053
|
+
)
|
|
1054
|
+
expect(usage(parse("history", "judges", "--jsonl", "--json"))).toContain("mutually exclusive")
|
|
1055
|
+
expect(usage(parse("history", "judges", "--jsonl", "--var", "qualityTriageMinP=1"))).toContain(
|
|
1056
|
+
"mutually exclusive",
|
|
1057
|
+
)
|
|
1058
|
+
expect(usage(parse("history", "metrics", "--jsonl"))).toContain("only valid for")
|
|
1030
1059
|
})
|
|
1031
1060
|
|
|
1032
1061
|
it("bare history and an unknown subcommand are usage errors naming list, show, metrics, fetch", () => {
|
|
@@ -1036,6 +1065,7 @@ describe("parseArgv — gtd history", () => {
|
|
|
1036
1065
|
expect(message).toContain("show")
|
|
1037
1066
|
expect(message).toContain("fetch")
|
|
1038
1067
|
expect(message).toContain("metrics")
|
|
1068
|
+
expect(message).toContain("judges")
|
|
1039
1069
|
}
|
|
1040
1070
|
})
|
|
1041
1071
|
|
|
@@ -1063,6 +1093,7 @@ describe("parseArgv — gtd history", () => {
|
|
|
1063
1093
|
expect(help).toContain("history show <ref>")
|
|
1064
1094
|
expect(help).toContain("history fetch [remote]")
|
|
1065
1095
|
expect(help).toContain("history metrics [ref...]")
|
|
1096
|
+
expect(help).toContain("history judges [ref...]")
|
|
1066
1097
|
})
|
|
1067
1098
|
})
|
|
1068
1099
|
|
|
@@ -1148,6 +1179,7 @@ describe("standaloneKinds / needsOf", () => {
|
|
|
1148
1179
|
"historyList",
|
|
1149
1180
|
"historyShow",
|
|
1150
1181
|
"historyMetrics",
|
|
1182
|
+
"historyJudges",
|
|
1151
1183
|
"historyFetch",
|
|
1152
1184
|
] as const) {
|
|
1153
1185
|
expect(needsOf(kind)).toBe("state")
|
package/src/cli/Cli.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type JsonMode,
|
|
36
36
|
} from "./Program.js"
|
|
37
37
|
import type { ToolCommands } from "./ToolCommands.js"
|
|
38
|
+
import { THRESHOLDS } from "./Judges.js"
|
|
38
39
|
import { EXIT_RUNTIME_ERROR, EXIT_USAGE_ERROR, parseContextWindow } from "../wire/index.js"
|
|
39
40
|
|
|
40
41
|
export type { Command, CommandRequirements } from "./Program.js"
|
|
@@ -107,6 +108,7 @@ const JSON_KINDS: ReadonlySet<Command["kind"] | undefined> = new Set<Command["ki
|
|
|
107
108
|
"historyList",
|
|
108
109
|
"historyShow",
|
|
109
110
|
"historyMetrics",
|
|
111
|
+
"historyJudges",
|
|
110
112
|
"strays",
|
|
111
113
|
])
|
|
112
114
|
|
|
@@ -118,12 +120,12 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
118
120
|
scope: (kind) => JSON_KINDS.has(kind),
|
|
119
121
|
decode: ([raw]) => Either.right(raw ?? ""),
|
|
120
122
|
scopeError:
|
|
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` — " +
|
|
123
|
+
"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 history judges`/`gtd strays` — " +
|
|
122
124
|
"every other command prints plain text; see `gtd install` for the driver protocol briefing",
|
|
123
125
|
valueHint: "<path>",
|
|
124
126
|
help: [
|
|
125
127
|
"(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd slices/gtd history",
|
|
126
|
-
"list/gtd history show/gtd history metrics/gtd strays only) output structured JSON. Bare --json prints the whole document;",
|
|
128
|
+
"list/gtd history show/gtd history metrics/gtd history judges/gtd strays only) output structured JSON. Bare --json prints the whole document;",
|
|
127
129
|
"--json=<path> (a dotted key path into that document, e.g.",
|
|
128
130
|
"kind, content, session.id) prints just that value: a",
|
|
129
131
|
"scalar raw and unquoted, a boolean as true/false, a list",
|
|
@@ -325,7 +327,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
325
327
|
name: "--var",
|
|
326
328
|
arity: 1,
|
|
327
329
|
repeatable: true,
|
|
328
|
-
scope: (kind) => kind === "start",
|
|
330
|
+
scope: (kind) => kind === "start" || kind === "historyJudges",
|
|
329
331
|
decode: (raws) => {
|
|
330
332
|
const vars: Record<string, string> = {}
|
|
331
333
|
const seen = new Set<string>()
|
|
@@ -362,15 +364,28 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
362
364
|
}
|
|
363
365
|
return Either.right(vars)
|
|
364
366
|
},
|
|
365
|
-
scopeError: "gtd: --var requires --workflow",
|
|
367
|
+
scopeError: "gtd: --var requires --workflow (or `gtd history judges`)",
|
|
366
368
|
valueHint: "<name>=<value>",
|
|
367
369
|
help: [
|
|
368
370
|
"(with --workflow; repeatable) pin a process setting for the",
|
|
369
371
|
"new process; the name must already be declared by the",
|
|
370
372
|
"workflow's defaults (environment settings are not",
|
|
371
|
-
"pinnable)",
|
|
373
|
+
"pinnable). (gtd history judges) re-decide past verdicts",
|
|
374
|
+
"under this threshold: judgeIdenticalMinP or",
|
|
375
|
+
"qualityTriageMinP",
|
|
372
376
|
],
|
|
373
377
|
},
|
|
378
|
+
{
|
|
379
|
+
name: "--jsonl",
|
|
380
|
+
arity: 0,
|
|
381
|
+
repeatable: false,
|
|
382
|
+
scope: (kind) => kind === "historyJudges",
|
|
383
|
+
decode: () => Either.right(true),
|
|
384
|
+
scopeError: "gtd: --jsonl is only valid for `gtd history judges`",
|
|
385
|
+
valueHint: "",
|
|
386
|
+
help: ["(gtd history judges only) print one JSON line per verdict", "instead of the report"],
|
|
387
|
+
conflicts: ["--json", "--var"],
|
|
388
|
+
},
|
|
374
389
|
{
|
|
375
390
|
name: "--open-questions",
|
|
376
391
|
arity: 0,
|
|
@@ -847,6 +862,23 @@ const COMMAND_ROWS: readonly CommandRow[] = [
|
|
|
847
862
|
"} as one document; writes nothing",
|
|
848
863
|
],
|
|
849
864
|
},
|
|
865
|
+
{
|
|
866
|
+
token: "history judges",
|
|
867
|
+
kind: "historyJudges",
|
|
868
|
+
arity: { name: "ref", many: true },
|
|
869
|
+
details: [
|
|
870
|
+
"Calibrate the judges of the kept processes from their",
|
|
871
|
+
"recorded Gtd-Judge verdicts: per judge (retry, triage, clear)",
|
|
872
|
+
"and p bucket, how many verdicts later events overturned.",
|
|
873
|
+
"--var <name>=<value> re-decides every past verdict under",
|
|
874
|
+
"another threshold and prints what would have changed;",
|
|
875
|
+
"--jsonl prints one { process, judge, commit, step, question,",
|
|
876
|
+
"state, answer, p, outcome } line per verdict instead. <ref>",
|
|
877
|
+
"is read as by history metrics. Never replays and asks no",
|
|
878
|
+
"model. --json prints { processes, total, whatIf }; writes",
|
|
879
|
+
"nothing",
|
|
880
|
+
],
|
|
881
|
+
},
|
|
850
882
|
{
|
|
851
883
|
token: "history fetch",
|
|
852
884
|
kind: "historyFetch",
|
|
@@ -1163,7 +1195,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1163
1195
|
|
|
1164
1196
|
if (isHistory && row === undefined) {
|
|
1165
1197
|
return usagePlan(
|
|
1166
|
-
"gtd history: expected a subcommand — list, show <ref>, metrics [ref...] or fetch [remote]",
|
|
1198
|
+
"gtd history: expected a subcommand — list, show <ref>, metrics [ref...], judges [ref...] or fetch [remote]",
|
|
1167
1199
|
jsonSeen,
|
|
1168
1200
|
)
|
|
1169
1201
|
}
|
|
@@ -1268,6 +1300,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1268
1300
|
readonly "--soft"?: number
|
|
1269
1301
|
readonly "--hard"?: number
|
|
1270
1302
|
readonly "--window"?: number
|
|
1303
|
+
readonly "--jsonl"?: boolean
|
|
1271
1304
|
}
|
|
1272
1305
|
|
|
1273
1306
|
// `present.has("--json")` alone can't distinguish bare `--json` (no value
|
|
@@ -1415,6 +1448,37 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1415
1448
|
}
|
|
1416
1449
|
}
|
|
1417
1450
|
|
|
1451
|
+
if (kind === "historyJudges") {
|
|
1452
|
+
const vars = bag["--var"] ?? {}
|
|
1453
|
+
const unknown = Object.keys(vars).find((name) => !Object.hasOwn(THRESHOLDS, name))
|
|
1454
|
+
if (unknown !== undefined) {
|
|
1455
|
+
return usagePlan(
|
|
1456
|
+
`gtd history judges: --var ${unknown} is no judge threshold — use ${Object.keys(THRESHOLDS).join(" or ")}`,
|
|
1457
|
+
jsonSeen,
|
|
1458
|
+
)
|
|
1459
|
+
}
|
|
1460
|
+
const nan = Object.entries(vars).find(
|
|
1461
|
+
([, value]) => value.trim() === "" || !Number.isFinite(Number(value)),
|
|
1462
|
+
)
|
|
1463
|
+
if (nan !== undefined) {
|
|
1464
|
+
return usagePlan(
|
|
1465
|
+
`gtd history judges: --var ${nan[0]} must be a number — got "${nan[1]}"`,
|
|
1466
|
+
jsonSeen,
|
|
1467
|
+
)
|
|
1468
|
+
}
|
|
1469
|
+
return {
|
|
1470
|
+
kind: "command",
|
|
1471
|
+
command: {
|
|
1472
|
+
kind: "historyJudges",
|
|
1473
|
+
refs: restPositionals,
|
|
1474
|
+
vars,
|
|
1475
|
+
jsonl: bag["--jsonl"] ?? false,
|
|
1476
|
+
},
|
|
1477
|
+
json,
|
|
1478
|
+
verbose,
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1418
1482
|
if (kind === "historyFetch") {
|
|
1419
1483
|
return {
|
|
1420
1484
|
kind: "command",
|
package/src/cli/History.ts
CHANGED
|
@@ -73,6 +73,21 @@ export const resolveHistoryRef = (
|
|
|
73
73
|
return Effect.fail(new Error(`gtd history: no history ref matches "${arg}"`))
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** Every kept process, or those `refs` name — a branch name meaning every process kept from it, not just its newest. */
|
|
77
|
+
export const selectHistoryEntries = (
|
|
78
|
+
refs: readonly string[],
|
|
79
|
+
): Effect.Effect<readonly HistoryEntry[], Error, GitService> =>
|
|
80
|
+
Effect.gen(function* () {
|
|
81
|
+
const entries = yield* historyEntries
|
|
82
|
+
if (refs.length === 0) return entries
|
|
83
|
+
const chosen = (yield* Effect.forEach(refs, (ref) =>
|
|
84
|
+
resolveHistoryRef(ref, entries).pipe(
|
|
85
|
+
Effect.map((hit) => (hit.branch === ref ? entries.filter((e) => e.branch === ref) : [hit])),
|
|
86
|
+
),
|
|
87
|
+
)).flat()
|
|
88
|
+
return chosen.filter((e, i) => chosen.findIndex((o) => o.ref === e.ref) === i)
|
|
89
|
+
})
|
|
90
|
+
|
|
76
91
|
const seven = (hash: string): string => hash.slice(0, 7)
|
|
77
92
|
|
|
78
93
|
export const runHistoryListCommand = (
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
calibrate,
|
|
4
|
+
criteriaChanged,
|
|
5
|
+
findingsOf,
|
|
6
|
+
processJudgments,
|
|
7
|
+
whatIf,
|
|
8
|
+
type JudgeCommit,
|
|
9
|
+
type Judgment,
|
|
10
|
+
} from "./Judges.js"
|
|
11
|
+
|
|
12
|
+
let n = 0
|
|
13
|
+
const c = (
|
|
14
|
+
subject: string,
|
|
15
|
+
facts: Omit<JudgeCommit, "hash" | "message"> & { verdicts?: readonly object[] } = {},
|
|
16
|
+
): JudgeCommit => {
|
|
17
|
+
const { verdicts = [], ...rest } = facts
|
|
18
|
+
const trailers = verdicts.map((v) => `Gtd-Judge: ${JSON.stringify(v)}`).join("\n")
|
|
19
|
+
return {
|
|
20
|
+
hash: `c${++n}`,
|
|
21
|
+
message: trailers === "" ? subject : `${subject}\n\n${trailers}`,
|
|
22
|
+
...rest,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const identical = (p = 0.9) => ({ verdicts: [{ id: "verdict", answer: "identical", p }] })
|
|
27
|
+
|
|
28
|
+
describe("processJudgments — retry", () => {
|
|
29
|
+
const red = (subject: string, report: string) => c(subject, { feedback: report })
|
|
30
|
+
|
|
31
|
+
it("overturns identical when the next fix turn's check goes green", () => {
|
|
32
|
+
const [j] = processJudgments([
|
|
33
|
+
red("gtd(check): build.health.check → build.fix", "1 failing"),
|
|
34
|
+
c("gtd(agent): build.fix → build.health.check"),
|
|
35
|
+
red("gtd(check): build.health.check → build.health.judge", "1 failing again"),
|
|
36
|
+
c("gtd(judge): build.health.judge → build.health.recheck", identical()),
|
|
37
|
+
red("gtd(check): build.health.recheck → build.health.describe", "still"),
|
|
38
|
+
c("gtd(agent): build.health.describe → build.health.stop"),
|
|
39
|
+
c("gtd(human): build.health.stop → build.fix"),
|
|
40
|
+
c("gtd(agent): build.fix → build.health.check"),
|
|
41
|
+
c("gtd(check): build.health.check → build.quality"),
|
|
42
|
+
])
|
|
43
|
+
expect(j).toMatchObject({
|
|
44
|
+
judge: "retry",
|
|
45
|
+
step: "build.health.judge",
|
|
46
|
+
question: "verdict",
|
|
47
|
+
outcome: "overturned",
|
|
48
|
+
state: { previous: "1 failing", current: "1 failing again" },
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("holds identical when that check stays red", () => {
|
|
53
|
+
const [j] = processJudgments([
|
|
54
|
+
red("gtd(check): health.check → fix", "x"),
|
|
55
|
+
red("gtd(check): health.check → health.judge", "x"),
|
|
56
|
+
c("gtd(judge): health.judge → health.recheck", identical()),
|
|
57
|
+
red("gtd(check): health.recheck → health.describe", "x"),
|
|
58
|
+
c("gtd(agent): fix → health.check"),
|
|
59
|
+
red("gtd(check): health.check → health.judge", "x"),
|
|
60
|
+
])
|
|
61
|
+
expect(j?.outcome).toBe("held")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("has no outcome when a person edited code before the deciding check", () => {
|
|
65
|
+
const [j] = processJudgments([
|
|
66
|
+
c("gtd(judge): build.health.judge → build.health.recheck", identical()),
|
|
67
|
+
red("gtd(check): build.health.recheck → build.health.describe", "x"),
|
|
68
|
+
c("gtd(human): build.health.stop → build.fix", { code: true }),
|
|
69
|
+
c("gtd(agent): build.fix → build.health.check"),
|
|
70
|
+
c("gtd(check): build.health.check → idle"),
|
|
71
|
+
])
|
|
72
|
+
expect(j?.outcome).toBeNull()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("hands an empty report on as the flow does, and scores none of it", () => {
|
|
76
|
+
const js = processJudgments([
|
|
77
|
+
red("gtd(check): health.check → fix", " "),
|
|
78
|
+
red("gtd(check): health.check → health.judge", "x"),
|
|
79
|
+
c("gtd(judge): health.judge → fix", identical()),
|
|
80
|
+
c("gtd(agent): fix → health.check"),
|
|
81
|
+
c("gtd(check): health.check → idle"),
|
|
82
|
+
])
|
|
83
|
+
expect(js[0]).toMatchObject({ state: { tailsNotComparable: "true" }, outcome: null })
|
|
84
|
+
const w = whatIf(
|
|
85
|
+
[{ vars: { judgeIdenticalMinP: "0.99" }, judgments: js }],
|
|
86
|
+
"judgeIdenticalMinP",
|
|
87
|
+
"0.5",
|
|
88
|
+
)
|
|
89
|
+
expect(w.changes).toEqual([])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("has no outcome when history shows fewer than two reports", () => {
|
|
93
|
+
const [j] = processJudgments([
|
|
94
|
+
c("gtd(judge): build.health.judge → build.fix", identical()),
|
|
95
|
+
c("gtd(agent): build.fix → build.health.check"),
|
|
96
|
+
c("gtd(check): build.health.check → idle"),
|
|
97
|
+
])
|
|
98
|
+
expect(j?.outcome).toBeNull()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("has no outcome when the re-check passes before any fix, or for another answer", () => {
|
|
102
|
+
const js = processJudgments([
|
|
103
|
+
c("gtd(judge): build.health.judge → build.health.recheck", identical()),
|
|
104
|
+
c("gtd(check): build.health.recheck → build.quality"),
|
|
105
|
+
c("gtd(judge): build.health.judge → build.fix", {
|
|
106
|
+
verdicts: [{ id: "verdict", answer: "progress", p: 0.9 }],
|
|
107
|
+
}),
|
|
108
|
+
c("gtd(agent): build.fix → build.health.check"),
|
|
109
|
+
c("gtd(check): build.health.check → idle"),
|
|
110
|
+
])
|
|
111
|
+
expect(js.map((j) => j.outcome)).toEqual([null, null])
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe("processJudgments — triage", () => {
|
|
116
|
+
const findings = findingsOf("## a stray export\n\nexport it\n\n## a missing test\n\nadd one\n")
|
|
117
|
+
const composed = [
|
|
118
|
+
"# Review: abc",
|
|
119
|
+
"",
|
|
120
|
+
"## Fixed automatically",
|
|
121
|
+
"",
|
|
122
|
+
"- a stray export (abc1234)",
|
|
123
|
+
"",
|
|
124
|
+
"## Dropped as noise",
|
|
125
|
+
"",
|
|
126
|
+
"- a missing test (p=0.85)",
|
|
127
|
+
"",
|
|
128
|
+
].join("\n")
|
|
129
|
+
const triage = c("gtd(judge): build.quality.triage → build.quality.sorting", {
|
|
130
|
+
findings,
|
|
131
|
+
verdicts: [
|
|
132
|
+
{ id: "Q1", answer: "mechanical", p: 0.95 },
|
|
133
|
+
{ id: "Q2", answer: "noise", p: 0.85 },
|
|
134
|
+
],
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("overturns a dropped finding the human wrote back on, holds the untouched fix", () => {
|
|
138
|
+
const js = processJudgments([
|
|
139
|
+
triage,
|
|
140
|
+
c("gtd(check): build.review.composing → build.review.await", {
|
|
141
|
+
review: { before: "", after: composed },
|
|
142
|
+
}),
|
|
143
|
+
c("gtd(human): build.review.await → build.review.triage", {
|
|
144
|
+
review: {
|
|
145
|
+
before: composed,
|
|
146
|
+
after: composed.replace("(p=0.85)", "(p=0.85) — it is not noise"),
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
])
|
|
150
|
+
expect(js.map((j) => [j.question, j.outcome])).toEqual([
|
|
151
|
+
["Q1", "held"],
|
|
152
|
+
["Q2", "overturned"],
|
|
153
|
+
])
|
|
154
|
+
expect(js[1]?.state).toEqual({ Q2: "## a missing test\n\nadd one" })
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it("credits a written-back line to its own finding only", () => {
|
|
158
|
+
const listed = [
|
|
159
|
+
"# Review",
|
|
160
|
+
"",
|
|
161
|
+
"## Dropped as noise",
|
|
162
|
+
"",
|
|
163
|
+
"- a test (p=0.8)",
|
|
164
|
+
"- a test for x (p=0.8)",
|
|
165
|
+
"",
|
|
166
|
+
].join("\n")
|
|
167
|
+
const js = processJudgments([
|
|
168
|
+
c("gtd(judge): build.quality.triage → build.quality.sorting", {
|
|
169
|
+
findings: findingsOf("## a test\n\n## a test for x\n"),
|
|
170
|
+
verdicts: [
|
|
171
|
+
{ id: "Q1", answer: "noise", p: 0.8 },
|
|
172
|
+
{ id: "Q2", answer: "noise", p: 0.8 },
|
|
173
|
+
],
|
|
174
|
+
}),
|
|
175
|
+
c("gtd(human): build.review.await → build.review.triage", {
|
|
176
|
+
review: { before: listed, after: listed.replace("x (p=0.8)", "x (p=0.8) — needed") },
|
|
177
|
+
}),
|
|
178
|
+
])
|
|
179
|
+
expect(js.map((j) => j.outcome)).toEqual(["held", "overturned"])
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it("overturns a note written under the line, or a rewrite of it", () => {
|
|
183
|
+
const listed = "# R\n\n## Dropped as noise\n\n- a test (p=0.8)\n- b (p=0.8)\n"
|
|
184
|
+
const triaged = (after: string) =>
|
|
185
|
+
processJudgments([
|
|
186
|
+
c("gtd(judge): build.quality.triage → build.quality.sorting", {
|
|
187
|
+
findings: findingsOf("## a test\n\n## b\n"),
|
|
188
|
+
verdicts: [
|
|
189
|
+
{ id: "Q1", answer: "noise", p: 0.8 },
|
|
190
|
+
{ id: "Q2", answer: "noise", p: 0.8 },
|
|
191
|
+
],
|
|
192
|
+
}),
|
|
193
|
+
c("gtd(human): build.review.await → build.review.triage", {
|
|
194
|
+
review: { before: listed, after },
|
|
195
|
+
}),
|
|
196
|
+
]).map((j) => j.outcome)
|
|
197
|
+
expect(triaged(listed.replace("(p=0.8)\n", "(p=0.8)\n we need it\n"))).toEqual([
|
|
198
|
+
"overturned",
|
|
199
|
+
"held",
|
|
200
|
+
])
|
|
201
|
+
expect(triaged(listed.replace("- a test (p=0.8)", "- a test: not noise"))).toEqual([
|
|
202
|
+
"overturned",
|
|
203
|
+
"held",
|
|
204
|
+
])
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it("never reads a longer title's edited line as a rewrite of a deleted shorter one", () => {
|
|
208
|
+
const listed = "# R\n\n## Dropped as noise\n\n- a test (p=0.8)\n- a test for x (p=0.8)\n"
|
|
209
|
+
const js = processJudgments([
|
|
210
|
+
c("gtd(judge): build.quality.triage → build.quality.sorting", {
|
|
211
|
+
findings: findingsOf("## a test\n\n## a test for x\n"),
|
|
212
|
+
verdicts: [
|
|
213
|
+
{ id: "Q1", answer: "noise", p: 0.8 },
|
|
214
|
+
{ id: "Q2", answer: "noise", p: 0.8 },
|
|
215
|
+
],
|
|
216
|
+
}),
|
|
217
|
+
c("gtd(human): build.review.await → build.review.triage", {
|
|
218
|
+
review: {
|
|
219
|
+
before: listed,
|
|
220
|
+
after: "# R\n\n## Dropped as noise\n\n- a test for x (p=0.8) — needed\n",
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
])
|
|
224
|
+
expect(js.map((j) => j.outcome)).toEqual(["held", "overturned"])
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it("scores only a finding the review listed as fixed or dropped", () => {
|
|
228
|
+
const js = processJudgments([
|
|
229
|
+
c("gtd(judge): build.quality.triage → build.quality.sorting", {
|
|
230
|
+
findings,
|
|
231
|
+
verdicts: [
|
|
232
|
+
{ id: "Q1", answer: "mechanical", p: 0.95 },
|
|
233
|
+
{ id: "Q2", answer: "design", p: 0.95 },
|
|
234
|
+
],
|
|
235
|
+
}),
|
|
236
|
+
c("gtd(human): build.review.await → idle", {
|
|
237
|
+
review: { before: "# Review\n\n## a stray export\n\nleft\n", after: "# Review\n" },
|
|
238
|
+
}),
|
|
239
|
+
])
|
|
240
|
+
expect(js.map((j) => j.outcome)).toEqual([null, null])
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe("processJudgments — clear", () => {
|
|
245
|
+
const clear = (answer: unknown) =>
|
|
246
|
+
c("gtd(judge): design.clear → design.normalise", {
|
|
247
|
+
sketch: "# TODO\n",
|
|
248
|
+
verdicts: [{ id: "clear", answer, p: 0.9 }],
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it("overturns a skip a later design gate edited the criteria after", () => {
|
|
252
|
+
const [j] = processJudgments([
|
|
253
|
+
clear("yes"),
|
|
254
|
+
c("gtd(agent): design.triage → design.answer-product-questions", { criteriaChanged: true }),
|
|
255
|
+
c("gtd(human): design.answer-product-questions → design.triage", { criteriaChanged: true }),
|
|
256
|
+
])
|
|
257
|
+
expect(j).toMatchObject({
|
|
258
|
+
judge: "clear",
|
|
259
|
+
outcome: "overturned",
|
|
260
|
+
state: { sketch: "# TODO\n" },
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it("holds a skip no gate edited after, reading a boolean verdict as yes", () => {
|
|
265
|
+
const [j] = processJudgments([
|
|
266
|
+
clear(true),
|
|
267
|
+
c("gtd(agent): design.triage → design.answer-product-questions", { criteriaChanged: true }),
|
|
268
|
+
c("gtd(human): build.review.await → idle"),
|
|
269
|
+
])
|
|
270
|
+
expect(j?.outcome).toBe("held")
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it("has no outcome for a no, or a yes below the skip floor", () => {
|
|
274
|
+
expect(processJudgments([clear("no")])[0]?.outcome).toBeNull()
|
|
275
|
+
const low = c("gtd(judge): design.clear → design.triage", {
|
|
276
|
+
verdicts: [{ id: "clear", answer: "yes", p: 0.75 }],
|
|
277
|
+
})
|
|
278
|
+
expect(processJudgments([low])[0]?.outcome).toBeNull()
|
|
279
|
+
// A cut sketch: a confident yes the flow did not act on.
|
|
280
|
+
const cut = c("gtd(judge): design.clear → design.triage", {
|
|
281
|
+
verdicts: [{ id: "clear", answer: "yes", p: 0.9 }],
|
|
282
|
+
})
|
|
283
|
+
expect(processJudgments([cut])[0]?.outcome).toBeNull()
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
describe("findingsOf", () => {
|
|
288
|
+
it("numbers stray prose first, as the quality lap does", () => {
|
|
289
|
+
expect(findingsOf("just prose\n\n## one\n\nbody\n").map((f) => [f.id, f.title])).toEqual([
|
|
290
|
+
["Q1", "Quality findings"],
|
|
291
|
+
["Q2", "one"],
|
|
292
|
+
])
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
describe("criteriaChanged", () => {
|
|
297
|
+
const doc = (items: string) => `# R\n\n## Acceptance Criteria\n\n${items}\n\n## Notes\n\nx\n`
|
|
298
|
+
|
|
299
|
+
it("ignores rewrapping and edits outside the criteria", () => {
|
|
300
|
+
expect(criteriaChanged(doc("- AC-1: a b"), doc("- AC-1: a\n b").replace("x", "y"))).toBe(false)
|
|
301
|
+
expect(criteriaChanged(doc("- AC-1: a"), doc("- AC-1: a\n- AC-2: b"))).toBe(true)
|
|
302
|
+
expect(criteriaChanged(undefined, doc("- AC-1: a"))).toBe(true)
|
|
303
|
+
expect(criteriaChanged(doc("- AC-1: a"), doc("Settled.\n\n- AC-1: a"))).toBe(false)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
const judgment = (over: Partial<Judgment>): Judgment => ({
|
|
308
|
+
judge: "triage",
|
|
309
|
+
commit: "c",
|
|
310
|
+
step: "build.quality.triage",
|
|
311
|
+
question: "Q1",
|
|
312
|
+
state: {},
|
|
313
|
+
answer: "noise",
|
|
314
|
+
p: 0.8,
|
|
315
|
+
outcome: null,
|
|
316
|
+
...over,
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
describe("calibrate", () => {
|
|
320
|
+
it("buckets scored verdicts by tenths of p, 1 in the top bucket", () => {
|
|
321
|
+
const { triage, retry } = calibrate([
|
|
322
|
+
judgment({ p: 0.85, outcome: "overturned" }),
|
|
323
|
+
judgment({ p: 0.8, outcome: "held" }),
|
|
324
|
+
judgment({ p: 1, outcome: "held" }),
|
|
325
|
+
judgment({ p: 0.5 }),
|
|
326
|
+
])
|
|
327
|
+
expect(triage).toEqual({
|
|
328
|
+
verdicts: 4,
|
|
329
|
+
scored: 3,
|
|
330
|
+
overturned: 1,
|
|
331
|
+
buckets: [
|
|
332
|
+
{ p: 0.8, scored: 2, overturned: 1, rate: 0.5 },
|
|
333
|
+
{ p: 0.9, scored: 1, overturned: 0, rate: 0 },
|
|
334
|
+
],
|
|
335
|
+
})
|
|
336
|
+
expect(retry).toEqual({ verdicts: 0, scored: 0, overturned: 0, buckets: [] })
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
describe("whatIf", () => {
|
|
341
|
+
it("re-decides each verdict under its process's recorded threshold and the new one", () => {
|
|
342
|
+
const w = whatIf(
|
|
343
|
+
[
|
|
344
|
+
{
|
|
345
|
+
vars: { qualityTriageMinP: "0.7" },
|
|
346
|
+
judgments: [
|
|
347
|
+
judgment({ p: 0.85, outcome: "overturned" }),
|
|
348
|
+
judgment({ p: 0.8 }),
|
|
349
|
+
judgment({ answer: "mechanical", p: 0.95 }),
|
|
350
|
+
judgment({ judge: "retry", answer: "identical", p: 0.8 }),
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
"qualityTriageMinP",
|
|
355
|
+
"0.9",
|
|
356
|
+
)
|
|
357
|
+
expect(w).toEqual({
|
|
358
|
+
var: "qualityTriageMinP",
|
|
359
|
+
value: "0.9",
|
|
360
|
+
judge: "triage",
|
|
361
|
+
changes: [{ from: "noise", to: "design", count: 2, overturned: 1 }],
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it("leaves out a process that recorded no such threshold", () => {
|
|
366
|
+
const w = whatIf(
|
|
367
|
+
[{ vars: {}, judgments: [judgment({ judge: "retry", answer: "identical", p: 0.8 })] }],
|
|
368
|
+
"judgeIdenticalMinP",
|
|
369
|
+
"0.7",
|
|
370
|
+
)
|
|
371
|
+
expect(w.changes).toEqual([])
|
|
372
|
+
})
|
|
373
|
+
})
|