@akagilnc/pi-workflow-roles 0.1.1941 → 0.1.2004
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/dist/public-cli/main.js +2448 -75
- package/package.json +1 -1
- package/src/atomic-write.ts +26 -0
- package/src/ledger-session-read.ts +260 -0
- package/src/public-cli/cli.ts +24 -0
- package/src/public-cli/invocation.ts +352 -1
- package/src/public-cli/main.ts +21 -2
- package/src/public-cli/registry.ts +18 -1
- package/src/public-cli/settlement.ts +9 -0
- package/src/public-cli/taishi-run.ts +235 -0
- package/src/run-terminal-artifacts.ts +231 -0
- package/src/taishi-cohort.ts +232 -0
- package/src/taishi-entry.ts +429 -0
- package/src/taishi-index.ts +269 -0
- package/src/taishi-ledger.ts +466 -0
- package/src/taishi-median.ts +15 -0
- package/src/taishi-metric-families/acceptance-success-rework.ts +346 -0
- package/src/taishi-metric-families/b2-frame-buckets-actions.ts +274 -0
- package/src/taishi-metric-families/leg-wall-clock.ts +90 -0
- package/src/taishi-metric-families/round-timeline.ts +201 -0
- package/src/taishi-metric-families.ts +36 -0
- package/src/taishi-metric-family.ts +41 -0
- package/src/taishi-model-groups.ts +198 -0
- package/src/taishi-page.ts +320 -0
- package/src/ticket-trajectory.ts +9 -62
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B3 metric family: acceptance terminal mapping, rework lens, first-pass / success rates.
|
|
3
|
+
*
|
|
4
|
+
* Consumes only A2 typed per-run facts (frame span + terminal face). Drop-in
|
|
5
|
+
* registration via taishi-metric-families/ discovery — no shared skeleton edits.
|
|
6
|
+
*
|
|
7
|
+
* Denominator contract (PRD #298 + ticket #327 票面补正):
|
|
8
|
+
* - first-pass rate den = appearance lanes (no-receipt first call stays in den)
|
|
9
|
+
* - success rate den = success-eligible accepted legs (no-receipt out; planned out)
|
|
10
|
+
* - planned = plan-duty acceptance; never success numerator or denominator
|
|
11
|
+
*/
|
|
12
|
+
import type { TaishiReadableRunFacts, TaishiRunTerminalFace } from "../taishi-ledger.ts";
|
|
13
|
+
import { medianNumber } from "../taishi-median.ts";
|
|
14
|
+
import type { TaishiMetricFamilyModule } from "../taishi-metric-family.ts";
|
|
15
|
+
|
|
16
|
+
const WORKER_ROLES = new Set(["coder", "fixer"]);
|
|
17
|
+
|
|
18
|
+
/** Lawful acceptance vocabulary by role (PRD 受理终态映射). */
|
|
19
|
+
const ACCEPTED_STATUS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
20
|
+
coder: new Set(["completed", "refused", "partially_completed", "unfinished", "planned"]),
|
|
21
|
+
fixer: new Set(["completed", "refused", "partially_completed", "unfinished", "planned"]),
|
|
22
|
+
judge: new Set(["converged", "continue", "escalate"]),
|
|
23
|
+
reviewer: new Set(["completed", "refused"]),
|
|
24
|
+
doctor: new Set(["completed", "refused"]),
|
|
25
|
+
merger: new Set(["completed", "escalate"]),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Success vocabulary by role (PRD 成功集合). planned excluded for workers. */
|
|
29
|
+
const SUCCESS_STATUS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
30
|
+
coder: new Set(["completed"]),
|
|
31
|
+
fixer: new Set(["completed"]),
|
|
32
|
+
// Judge: producing any of the three verdicts completes the duty.
|
|
33
|
+
judge: new Set(["converged", "continue", "escalate"]),
|
|
34
|
+
reviewer: new Set(["completed"]),
|
|
35
|
+
doctor: new Set(["completed"]),
|
|
36
|
+
merger: new Set(["completed"]),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type TaishiTerminalMapLabel =
|
|
40
|
+
| "no-receipt"
|
|
41
|
+
| "non-accepted"
|
|
42
|
+
| "groups"
|
|
43
|
+
| string;
|
|
44
|
+
|
|
45
|
+
export type TaishiAcceptanceLeg = {
|
|
46
|
+
readonly runId: string;
|
|
47
|
+
readonly book: string;
|
|
48
|
+
readonly role: string;
|
|
49
|
+
readonly startedAt: string;
|
|
50
|
+
readonly wallMs: number;
|
|
51
|
+
/** Mapped terminal label (status token, groups, no-receipt, non-accepted). */
|
|
52
|
+
readonly terminalLabel: TaishiTerminalMapLabel;
|
|
53
|
+
readonly accepted: boolean;
|
|
54
|
+
/** In success set (and therefore success numerator when eligible). */
|
|
55
|
+
readonly success: boolean;
|
|
56
|
+
/** In success-rate denominator (accepted ∧ ¬planned-duty). */
|
|
57
|
+
readonly successEligible: boolean;
|
|
58
|
+
readonly noReceipt: boolean;
|
|
59
|
+
/** 1-based ordinal among same lane+role ordered by startedAt. */
|
|
60
|
+
readonly ordinalInLaneRole: number;
|
|
61
|
+
readonly rework: boolean;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type TaishiRoleAcceptanceStats = {
|
|
65
|
+
readonly role: string;
|
|
66
|
+
readonly acceptedCount: number;
|
|
67
|
+
readonly successEligibleCount: number;
|
|
68
|
+
readonly successCount: number;
|
|
69
|
+
readonly noReceiptCount: number;
|
|
70
|
+
/** successCount / successEligibleCount; undefined when denominator is 0. */
|
|
71
|
+
readonly successRate: number | undefined;
|
|
72
|
+
readonly appearanceLaneCount: number;
|
|
73
|
+
readonly firstPassLaneCount: number;
|
|
74
|
+
/** firstPassLaneCount / appearanceLaneCount; undefined when denominator is 0. */
|
|
75
|
+
readonly firstPassRate: number | undefined;
|
|
76
|
+
/** Per appearance-lane call counts (convergence rounds). */
|
|
77
|
+
readonly convergenceRounds: readonly number[];
|
|
78
|
+
readonly convergenceRoundsMedian: number | undefined;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type TaishiReworkLens = {
|
|
82
|
+
readonly reworkWallMs: number;
|
|
83
|
+
readonly totalWallMs: number;
|
|
84
|
+
/** reworkWallMs / totalWallMs; undefined when totalWallMs is 0. */
|
|
85
|
+
readonly reworkRatio: number | undefined;
|
|
86
|
+
readonly reworkLegCount: number;
|
|
87
|
+
readonly totalLegCount: number;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type TaishiAcceptanceSuccessReworkSection = {
|
|
91
|
+
readonly kind: "taishi-acceptance-success-rework";
|
|
92
|
+
readonly legs: readonly TaishiAcceptanceLeg[];
|
|
93
|
+
readonly byRole: readonly TaishiRoleAcceptanceStats[];
|
|
94
|
+
readonly rework: TaishiReworkLens;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
98
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function wallMsFromSpan(span: { readonly startedAt: string; readonly endedAt: string }): number {
|
|
102
|
+
return Date.parse(span.endedAt) - Date.parse(span.startedAt);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Locate collector groups array: receipt.groups or top-level groups. */
|
|
106
|
+
function findCollectorGroups(body: Record<string, unknown>): unknown {
|
|
107
|
+
if (Array.isArray(body.groups)) return body.groups;
|
|
108
|
+
const receipt = body.receipt;
|
|
109
|
+
if (isRecord(receipt) && Array.isArray(receipt.groups)) return receipt.groups;
|
|
110
|
+
const outcome = body.outcome;
|
|
111
|
+
if (isRecord(outcome)) {
|
|
112
|
+
const facts = outcome.decisiveFacts;
|
|
113
|
+
if (isRecord(facts) && Array.isArray(facts.groups)) return facts.groups;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Extract role receipt status from canonical terminal body faces. */
|
|
119
|
+
function extractStatus(body: Record<string, unknown>): string | undefined {
|
|
120
|
+
const outcome = body.outcome;
|
|
121
|
+
if (isRecord(outcome) && typeof outcome.status === "string" && outcome.status.trim() !== "") {
|
|
122
|
+
return outcome.status;
|
|
123
|
+
}
|
|
124
|
+
const receipt = body.receipt;
|
|
125
|
+
if (isRecord(receipt) && typeof receipt.status === "string" && receipt.status.trim() !== "") {
|
|
126
|
+
return receipt.status;
|
|
127
|
+
}
|
|
128
|
+
if (typeof body.status === "string" && body.status.trim() !== "") {
|
|
129
|
+
return body.status;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function mapTerminal(
|
|
135
|
+
role: string,
|
|
136
|
+
terminal: TaishiRunTerminalFace,
|
|
137
|
+
): {
|
|
138
|
+
readonly terminalLabel: TaishiTerminalMapLabel;
|
|
139
|
+
readonly accepted: boolean;
|
|
140
|
+
readonly success: boolean;
|
|
141
|
+
readonly successEligible: boolean;
|
|
142
|
+
readonly noReceipt: boolean;
|
|
143
|
+
} {
|
|
144
|
+
if (terminal.status === "absent") {
|
|
145
|
+
return {
|
|
146
|
+
terminalLabel: "no-receipt",
|
|
147
|
+
accepted: false,
|
|
148
|
+
success: false,
|
|
149
|
+
successEligible: false,
|
|
150
|
+
noReceipt: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const body = terminal.body;
|
|
155
|
+
|
|
156
|
+
// Collector: typed groups array presence is the sole acceptance discriminator.
|
|
157
|
+
if (role === "collector") {
|
|
158
|
+
const groups = findCollectorGroups(body);
|
|
159
|
+
if (Array.isArray(groups)) {
|
|
160
|
+
return {
|
|
161
|
+
terminalLabel: "groups",
|
|
162
|
+
accepted: true,
|
|
163
|
+
success: true,
|
|
164
|
+
successEligible: true,
|
|
165
|
+
noReceipt: false,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
terminalLabel: "non-accepted",
|
|
170
|
+
accepted: false,
|
|
171
|
+
success: false,
|
|
172
|
+
successEligible: false,
|
|
173
|
+
noReceipt: false,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const status = extractStatus(body);
|
|
178
|
+
if (status === undefined) {
|
|
179
|
+
return {
|
|
180
|
+
terminalLabel: "non-accepted",
|
|
181
|
+
accepted: false,
|
|
182
|
+
success: false,
|
|
183
|
+
successEligible: false,
|
|
184
|
+
noReceipt: false,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const acceptedSet = ACCEPTED_STATUS[role];
|
|
189
|
+
if (acceptedSet === undefined || !acceptedSet.has(status)) {
|
|
190
|
+
return {
|
|
191
|
+
terminalLabel: status,
|
|
192
|
+
accepted: false,
|
|
193
|
+
success: false,
|
|
194
|
+
successEligible: false,
|
|
195
|
+
noReceipt: false,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const plannedDuty = WORKER_ROLES.has(role) && status === "planned";
|
|
200
|
+
const successSet = SUCCESS_STATUS[role] ?? new Set<string>();
|
|
201
|
+
const success = !plannedDuty && successSet.has(status);
|
|
202
|
+
const successEligible = !plannedDuty;
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
terminalLabel: status,
|
|
206
|
+
accepted: true,
|
|
207
|
+
success,
|
|
208
|
+
successEligible,
|
|
209
|
+
noReceipt: false,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function projectLegs(runs: readonly TaishiReadableRunFacts[]): TaishiAcceptanceLeg[] {
|
|
214
|
+
// Order within lane+role by startedAt (then runId) to assign ordinals / rework.
|
|
215
|
+
const sorted = [...runs].sort((a, b) => {
|
|
216
|
+
if (a.book !== b.book) return a.book.localeCompare(b.book);
|
|
217
|
+
if (a.role !== b.role) return a.role.localeCompare(b.role);
|
|
218
|
+
if (a.frameSpan.startedAt !== b.frameSpan.startedAt) {
|
|
219
|
+
return a.frameSpan.startedAt.localeCompare(b.frameSpan.startedAt);
|
|
220
|
+
}
|
|
221
|
+
return a.runId.localeCompare(b.runId);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const ordinalByKey = new Map<string, number>();
|
|
225
|
+
const legs: TaishiAcceptanceLeg[] = [];
|
|
226
|
+
|
|
227
|
+
for (const run of sorted) {
|
|
228
|
+
const key = `${run.book}\0${run.role}`;
|
|
229
|
+
const ordinal = (ordinalByKey.get(key) ?? 0) + 1;
|
|
230
|
+
ordinalByKey.set(key, ordinal);
|
|
231
|
+
const mapped = mapTerminal(run.role, run.terminal);
|
|
232
|
+
legs.push({
|
|
233
|
+
runId: run.runId,
|
|
234
|
+
book: run.book,
|
|
235
|
+
role: run.role,
|
|
236
|
+
startedAt: run.frameSpan.startedAt,
|
|
237
|
+
wallMs: wallMsFromSpan(run.frameSpan),
|
|
238
|
+
terminalLabel: mapped.terminalLabel,
|
|
239
|
+
accepted: mapped.accepted,
|
|
240
|
+
success: mapped.success,
|
|
241
|
+
successEligible: mapped.successEligible,
|
|
242
|
+
noReceipt: mapped.noReceipt,
|
|
243
|
+
ordinalInLaneRole: ordinal,
|
|
244
|
+
rework: ordinal >= 2,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Stable page order: book, role, runId (match A1 leg sort).
|
|
249
|
+
return legs.sort((a, b) => {
|
|
250
|
+
if (a.book !== b.book) return a.book.localeCompare(b.book);
|
|
251
|
+
if (a.role !== b.role) return a.role.localeCompare(b.role);
|
|
252
|
+
return a.runId.localeCompare(b.runId);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function aggregateByRole(legs: readonly TaishiAcceptanceLeg[]): TaishiRoleAcceptanceStats[] {
|
|
257
|
+
const roles = [...new Set(legs.map((leg) => leg.role))].sort((a, b) => a.localeCompare(b));
|
|
258
|
+
return roles.map((role) => {
|
|
259
|
+
const roleLegs = legs.filter((leg) => leg.role === role);
|
|
260
|
+
const acceptedCount = roleLegs.filter((leg) => leg.accepted).length;
|
|
261
|
+
const successEligibleCount = roleLegs.filter((leg) => leg.successEligible).length;
|
|
262
|
+
const successCount = roleLegs.filter((leg) => leg.success).length;
|
|
263
|
+
const noReceiptCount = roleLegs.filter((leg) => leg.noReceipt).length;
|
|
264
|
+
|
|
265
|
+
// Appearance lanes + first-pass: first call by startedAt within each book.
|
|
266
|
+
const byBook = new Map<string, TaishiAcceptanceLeg[]>();
|
|
267
|
+
for (const leg of roleLegs) {
|
|
268
|
+
const list = byBook.get(leg.book) ?? [];
|
|
269
|
+
list.push(leg);
|
|
270
|
+
byBook.set(leg.book, list);
|
|
271
|
+
}
|
|
272
|
+
const books = [...byBook.keys()].sort((a, b) => a.localeCompare(b));
|
|
273
|
+
const convergenceRounds: number[] = [];
|
|
274
|
+
let firstPassLaneCount = 0;
|
|
275
|
+
for (const book of books) {
|
|
276
|
+
const laneLegs = [...byBook.get(book)!].sort((a, b) => {
|
|
277
|
+
if (a.startedAt !== b.startedAt) return a.startedAt.localeCompare(b.startedAt);
|
|
278
|
+
return a.runId.localeCompare(b.runId);
|
|
279
|
+
});
|
|
280
|
+
convergenceRounds.push(laneLegs.length);
|
|
281
|
+
const first = laneLegs[0]!;
|
|
282
|
+
if (first.accepted) firstPassLaneCount += 1;
|
|
283
|
+
}
|
|
284
|
+
const appearanceLaneCount = books.length;
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
role,
|
|
288
|
+
acceptedCount,
|
|
289
|
+
successEligibleCount,
|
|
290
|
+
successCount,
|
|
291
|
+
noReceiptCount,
|
|
292
|
+
successRate:
|
|
293
|
+
successEligibleCount === 0 ? undefined : successCount / successEligibleCount,
|
|
294
|
+
appearanceLaneCount,
|
|
295
|
+
firstPassLaneCount,
|
|
296
|
+
firstPassRate:
|
|
297
|
+
appearanceLaneCount === 0 ? undefined : firstPassLaneCount / appearanceLaneCount,
|
|
298
|
+
convergenceRounds,
|
|
299
|
+
convergenceRoundsMedian: medianNumber(convergenceRounds),
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function reworkLens(legs: readonly TaishiAcceptanceLeg[]): TaishiReworkLens {
|
|
305
|
+
let reworkWallMs = 0;
|
|
306
|
+
let totalWallMs = 0;
|
|
307
|
+
let reworkLegCount = 0;
|
|
308
|
+
for (const leg of legs) {
|
|
309
|
+
totalWallMs += leg.wallMs;
|
|
310
|
+
if (leg.rework) {
|
|
311
|
+
reworkWallMs += leg.wallMs;
|
|
312
|
+
reworkLegCount += 1;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
reworkWallMs,
|
|
317
|
+
totalWallMs,
|
|
318
|
+
reworkRatio: totalWallMs === 0 ? undefined : reworkWallMs / totalWallMs,
|
|
319
|
+
reworkLegCount,
|
|
320
|
+
totalLegCount: legs.length,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function buildAcceptanceSuccessReworkSection(
|
|
325
|
+
runs: readonly TaishiReadableRunFacts[],
|
|
326
|
+
): TaishiAcceptanceSuccessReworkSection | undefined {
|
|
327
|
+
if (runs.length === 0) return undefined;
|
|
328
|
+
const legs = projectLegs(runs);
|
|
329
|
+
return {
|
|
330
|
+
kind: "taishi-acceptance-success-rework",
|
|
331
|
+
legs,
|
|
332
|
+
byRole: aggregateByRole(legs),
|
|
333
|
+
rework: reworkLens(legs),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const acceptanceSuccessReworkFamily: TaishiMetricFamilyModule = {
|
|
338
|
+
id: "acceptance-success-rework",
|
|
339
|
+
contribute(input) {
|
|
340
|
+
const section = buildAcceptanceSuccessReworkSection(input.runs);
|
|
341
|
+
if (section === undefined) return undefined;
|
|
342
|
+
return { acceptanceSuccessRework: section };
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
export default acceptanceSuccessReworkFamily;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B2 metric family — two-bucket full partition + single-leg action board.
|
|
3
|
+
*
|
|
4
|
+
* Consumes A2 typed per-run facts only (frame span + tool intervals).
|
|
5
|
+
* Registers by drop-in under taishi-metric-families/ (A2 assembly discovery).
|
|
6
|
+
*
|
|
7
|
+
* Kernel (single frame-bounded interval core):
|
|
8
|
+
* - every closed tool interval is clipped to the leg frame [frameStart, frameEnd]
|
|
9
|
+
* - tool bucket = union duration of clipped intervals (no double-count)
|
|
10
|
+
* - model bucket = wall − tool bucket (mutual exclusion; sum ≡ wall)
|
|
11
|
+
* - actions = each clipped tool interval + every maximal continuous model gap
|
|
12
|
+
* (frame complement of tool union, including pre-first and post-last tails),
|
|
13
|
+
* sorted by duration descending
|
|
14
|
+
* - action median via shared medianNumber (even → mean of two middles)
|
|
15
|
+
* - tool observation: toolName + A2 bash first-line command summary (no re-parse)
|
|
16
|
+
*/
|
|
17
|
+
import type { SessionToolInterval } from "../ledger-session-read.ts";
|
|
18
|
+
import { medianNumber } from "../taishi-median.ts";
|
|
19
|
+
import type { TaishiReadableRunFacts } from "../taishi-ledger.ts";
|
|
20
|
+
import type { TaishiMetricFamilyModule } from "../taishi-metric-family.ts";
|
|
21
|
+
|
|
22
|
+
export type TaishiB2ToolAction = {
|
|
23
|
+
readonly kind: "tool";
|
|
24
|
+
readonly toolCallId: string;
|
|
25
|
+
readonly toolName: string;
|
|
26
|
+
readonly durationMs: number;
|
|
27
|
+
readonly startedAt: string;
|
|
28
|
+
readonly endedAt: string;
|
|
29
|
+
/** A2 bash first-line command summary when present. */
|
|
30
|
+
readonly commandSummary?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type TaishiB2ModelAction = {
|
|
34
|
+
readonly kind: "model";
|
|
35
|
+
readonly durationMs: number;
|
|
36
|
+
readonly startedAt: string;
|
|
37
|
+
readonly endedAt: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type TaishiB2Action = TaishiB2ToolAction | TaishiB2ModelAction;
|
|
41
|
+
|
|
42
|
+
export type TaishiB2RunMetrics = {
|
|
43
|
+
readonly runId: string;
|
|
44
|
+
readonly book: string;
|
|
45
|
+
readonly role: string;
|
|
46
|
+
readonly wallMs: number;
|
|
47
|
+
readonly toolBucketMs: number;
|
|
48
|
+
readonly modelBucketMs: number;
|
|
49
|
+
readonly actions: readonly TaishiB2Action[];
|
|
50
|
+
readonly actionDurationMedianMs: number | undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type TaishiB2FrameBucketsActionsSection = {
|
|
54
|
+
readonly kind: "taishi-b2-frame-buckets-actions";
|
|
55
|
+
readonly runs: readonly TaishiB2RunMetrics[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type ClosedTool = {
|
|
59
|
+
readonly toolCallId: string;
|
|
60
|
+
readonly toolName: string;
|
|
61
|
+
readonly startedAt: string;
|
|
62
|
+
readonly endedAt: string;
|
|
63
|
+
readonly startMs: number;
|
|
64
|
+
readonly endMs: number;
|
|
65
|
+
readonly command?: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function timestampMs(iso: string): number {
|
|
69
|
+
const ms = Date.parse(iso);
|
|
70
|
+
if (!Number.isFinite(ms)) {
|
|
71
|
+
throw new Error(`unparseable timestamp: ${iso}`);
|
|
72
|
+
}
|
|
73
|
+
return ms;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toIso(ms: number): string {
|
|
77
|
+
return new Date(ms).toISOString();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function closedTools(intervals: readonly SessionToolInterval[]): ClosedTool[] {
|
|
81
|
+
const out: ClosedTool[] = [];
|
|
82
|
+
for (const interval of intervals) {
|
|
83
|
+
if (interval.endedAt === undefined) continue;
|
|
84
|
+
const startMs = timestampMs(interval.startedAt);
|
|
85
|
+
const endMs = timestampMs(interval.endedAt);
|
|
86
|
+
if (endMs <= startMs) continue;
|
|
87
|
+
out.push({
|
|
88
|
+
toolCallId: interval.toolCallId,
|
|
89
|
+
toolName: interval.toolName,
|
|
90
|
+
startedAt: interval.startedAt,
|
|
91
|
+
endedAt: interval.endedAt,
|
|
92
|
+
startMs,
|
|
93
|
+
endMs,
|
|
94
|
+
...(interval.command !== undefined ? { command: interval.command } : {}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Clip closed tools to the leg frame. Empty after clip are dropped.
|
|
102
|
+
* Bucket, complement, and tool actions all consume this same bounded set.
|
|
103
|
+
*/
|
|
104
|
+
function clipToolsToFrame(
|
|
105
|
+
tools: readonly ClosedTool[],
|
|
106
|
+
frameStartMs: number,
|
|
107
|
+
frameEndMs: number,
|
|
108
|
+
): ClosedTool[] {
|
|
109
|
+
if (frameEndMs <= frameStartMs) return [];
|
|
110
|
+
const out: ClosedTool[] = [];
|
|
111
|
+
for (const tool of tools) {
|
|
112
|
+
const startMs = Math.max(tool.startMs, frameStartMs);
|
|
113
|
+
const endMs = Math.min(tool.endMs, frameEndMs);
|
|
114
|
+
if (endMs <= startMs) continue;
|
|
115
|
+
out.push({
|
|
116
|
+
toolCallId: tool.toolCallId,
|
|
117
|
+
toolName: tool.toolName,
|
|
118
|
+
startMs,
|
|
119
|
+
endMs,
|
|
120
|
+
startedAt: startMs === tool.startMs ? tool.startedAt : toIso(startMs),
|
|
121
|
+
endedAt: endMs === tool.endMs ? tool.endedAt : toIso(endMs),
|
|
122
|
+
...(tool.command !== undefined ? { command: tool.command } : {}),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Merge overlapping/adjacent [start,end) intervals; return sorted disjoint union. */
|
|
129
|
+
function mergeUnion(intervals: readonly { startMs: number; endMs: number }[]): {
|
|
130
|
+
startMs: number;
|
|
131
|
+
endMs: number;
|
|
132
|
+
}[] {
|
|
133
|
+
if (intervals.length === 0) return [];
|
|
134
|
+
const sorted = [...intervals].sort(
|
|
135
|
+
(a, b) => a.startMs - b.startMs || a.endMs - b.endMs,
|
|
136
|
+
);
|
|
137
|
+
const merged: { startMs: number; endMs: number }[] = [
|
|
138
|
+
{ startMs: sorted[0]!.startMs, endMs: sorted[0]!.endMs },
|
|
139
|
+
];
|
|
140
|
+
for (let i = 1; i < sorted.length; i += 1) {
|
|
141
|
+
const cur = sorted[i]!;
|
|
142
|
+
const last = merged[merged.length - 1]!;
|
|
143
|
+
if (cur.startMs <= last.endMs) {
|
|
144
|
+
last.endMs = Math.max(last.endMs, cur.endMs);
|
|
145
|
+
} else {
|
|
146
|
+
merged.push({ startMs: cur.startMs, endMs: cur.endMs });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return merged;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Maximal continuous complement of (already frame-clipped) tool union
|
|
154
|
+
* inside [frameStart, frameEnd]. Includes leading and trailing gaps.
|
|
155
|
+
*/
|
|
156
|
+
function modelMaximalIntervals(
|
|
157
|
+
frameStartMs: number,
|
|
158
|
+
frameEndMs: number,
|
|
159
|
+
toolUnion: readonly { startMs: number; endMs: number }[],
|
|
160
|
+
): { startMs: number; endMs: number }[] {
|
|
161
|
+
if (frameEndMs <= frameStartMs) return [];
|
|
162
|
+
const gaps: { startMs: number; endMs: number }[] = [];
|
|
163
|
+
let cursor = frameStartMs;
|
|
164
|
+
for (const interval of toolUnion) {
|
|
165
|
+
// Union is produced from frame-clipped tools, so bounds already lie in frame.
|
|
166
|
+
if (interval.startMs > cursor) {
|
|
167
|
+
gaps.push({ startMs: cursor, endMs: interval.startMs });
|
|
168
|
+
}
|
|
169
|
+
cursor = Math.max(cursor, interval.endMs);
|
|
170
|
+
}
|
|
171
|
+
if (cursor < frameEndMs) {
|
|
172
|
+
gaps.push({ startMs: cursor, endMs: frameEndMs });
|
|
173
|
+
}
|
|
174
|
+
return gaps;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function toolAction(tool: ClosedTool): TaishiB2ToolAction {
|
|
178
|
+
const action: TaishiB2ToolAction = {
|
|
179
|
+
kind: "tool",
|
|
180
|
+
toolCallId: tool.toolCallId,
|
|
181
|
+
toolName: tool.toolName,
|
|
182
|
+
durationMs: tool.endMs - tool.startMs,
|
|
183
|
+
startedAt: tool.startedAt,
|
|
184
|
+
endedAt: tool.endedAt,
|
|
185
|
+
};
|
|
186
|
+
// A2 already owns bash first-line summary; B2 only projects it.
|
|
187
|
+
if (tool.toolName === "bash" && tool.command !== undefined) {
|
|
188
|
+
return {
|
|
189
|
+
...action,
|
|
190
|
+
commandSummary: tool.command,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return action;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function modelAction(gap: { startMs: number; endMs: number }): TaishiB2ModelAction {
|
|
197
|
+
return {
|
|
198
|
+
kind: "model",
|
|
199
|
+
durationMs: gap.endMs - gap.startMs,
|
|
200
|
+
startedAt: toIso(gap.startMs),
|
|
201
|
+
endedAt: toIso(gap.endMs),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function sortActionsDescending(actions: readonly TaishiB2Action[]): TaishiB2Action[] {
|
|
206
|
+
return [...actions].sort((a, b) => {
|
|
207
|
+
if (b.durationMs !== a.durationMs) return b.durationMs - a.durationMs;
|
|
208
|
+
// Stable tie-break: earlier start first, then kind/toolCallId for determinism.
|
|
209
|
+
if (a.startedAt !== b.startedAt) return a.startedAt.localeCompare(b.startedAt);
|
|
210
|
+
if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
|
|
211
|
+
if (a.kind === "tool" && b.kind === "tool") {
|
|
212
|
+
return a.toolCallId.localeCompare(b.toolCallId);
|
|
213
|
+
}
|
|
214
|
+
return 0;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Pure B2 kernel over one readable run's A2 facts. */
|
|
219
|
+
export function computeTaishiB2RunMetrics(
|
|
220
|
+
facts: TaishiReadableRunFacts,
|
|
221
|
+
): TaishiB2RunMetrics {
|
|
222
|
+
const frameStartMs = timestampMs(facts.frameSpan.startedAt);
|
|
223
|
+
const frameEndMs = timestampMs(facts.frameSpan.endedAt);
|
|
224
|
+
const wallMs = Math.max(0, frameEndMs - frameStartMs);
|
|
225
|
+
|
|
226
|
+
// Single frame-bounded core: clip → union once → bucket/complement/actions.
|
|
227
|
+
const tools = clipToolsToFrame(closedTools(facts.toolIntervals), frameStartMs, frameEndMs);
|
|
228
|
+
const toolUnion = mergeUnion(tools);
|
|
229
|
+
const toolBucketMs = toolUnion.reduce(
|
|
230
|
+
(sum, interval) => sum + (interval.endMs - interval.startMs),
|
|
231
|
+
0,
|
|
232
|
+
);
|
|
233
|
+
// Mutual exclusion: model is exact complement duration (no independent recount).
|
|
234
|
+
const modelBucketMs = wallMs - toolBucketMs;
|
|
235
|
+
|
|
236
|
+
const modelGaps = modelMaximalIntervals(frameStartMs, frameEndMs, toolUnion);
|
|
237
|
+
const actions = sortActionsDescending([
|
|
238
|
+
...tools.map(toolAction),
|
|
239
|
+
...modelGaps.map(modelAction),
|
|
240
|
+
]);
|
|
241
|
+
const actionDurationMedianMs = medianNumber(actions.map((action) => action.durationMs));
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
runId: facts.runId,
|
|
245
|
+
book: facts.book,
|
|
246
|
+
role: facts.role,
|
|
247
|
+
wallMs,
|
|
248
|
+
toolBucketMs,
|
|
249
|
+
modelBucketMs,
|
|
250
|
+
actions,
|
|
251
|
+
actionDurationMedianMs,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const b2FrameBucketsActionsFamily: TaishiMetricFamilyModule = {
|
|
256
|
+
id: "b2-frame-buckets-actions",
|
|
257
|
+
contribute(input) {
|
|
258
|
+
if (input.runs.length === 0) return undefined;
|
|
259
|
+
const runs = [...input.runs]
|
|
260
|
+
.map(computeTaishiB2RunMetrics)
|
|
261
|
+
.sort((a, b) => {
|
|
262
|
+
if (a.book !== b.book) return a.book.localeCompare(b.book);
|
|
263
|
+
if (a.role !== b.role) return a.role.localeCompare(b.role);
|
|
264
|
+
return a.runId.localeCompare(b.runId);
|
|
265
|
+
});
|
|
266
|
+
const section: TaishiB2FrameBucketsActionsSection = {
|
|
267
|
+
kind: "taishi-b2-frame-buckets-actions",
|
|
268
|
+
runs,
|
|
269
|
+
};
|
|
270
|
+
return { b2FrameBucketsActions: section };
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export default b2FrameBucketsActionsFamily;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B1 leg wall-clock metric family (#325).
|
|
3
|
+
*
|
|
4
|
+
* Consumes A2 typed per-run frame spans only — no second ledger scan.
|
|
5
|
+
* Emits: per-leg wallMs, ranking by wall clock desc, median, total elapsed.
|
|
6
|
+
* Registers by file drop under taishi-metric-families/ (A2 discovery).
|
|
7
|
+
*/
|
|
8
|
+
import { medianNumber } from "../taishi-median.ts";
|
|
9
|
+
import type { TaishiReadableRunFacts } from "../taishi-ledger.ts";
|
|
10
|
+
import type { TaishiMetricFamilyModule } from "../taishi-metric-family.ts";
|
|
11
|
+
|
|
12
|
+
/** One readable leg's session-frame wall clock (first usable → last usable). */
|
|
13
|
+
export type TaishiLegWallClockEntry = {
|
|
14
|
+
readonly runId: string;
|
|
15
|
+
readonly book: string;
|
|
16
|
+
readonly role: string;
|
|
17
|
+
readonly wallMs: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Issue-page section: 腿墙钟 + 按墙钟降序腿总榜 + 中位数 + 完全耗时.
|
|
22
|
+
* Only readable in-scope runs (damaged already excluded by A1 scan).
|
|
23
|
+
*/
|
|
24
|
+
export type TaishiLegWallClockSection = {
|
|
25
|
+
readonly kind: "taishi-leg-wall-clock";
|
|
26
|
+
/** 腿总榜 — each row carries 腿墙钟; ordered by wallMs descending. */
|
|
27
|
+
readonly ranking: readonly TaishiLegWallClockEntry[];
|
|
28
|
+
/** 腿墙钟中位数 — even samples use shared mean-of-two-middles primitive. */
|
|
29
|
+
readonly medianWallMs: number;
|
|
30
|
+
/** 完全耗时 — Σ wallMs of every readable run on the board. */
|
|
31
|
+
readonly totalElapsedMs: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function frameSpanWallMs(span: {
|
|
35
|
+
readonly startedAt: string;
|
|
36
|
+
readonly endedAt: string;
|
|
37
|
+
}): number {
|
|
38
|
+
return Date.parse(span.endedAt) - Date.parse(span.startedAt);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function projectEntry(facts: TaishiReadableRunFacts): TaishiLegWallClockEntry {
|
|
42
|
+
return {
|
|
43
|
+
runId: facts.runId,
|
|
44
|
+
book: facts.book,
|
|
45
|
+
role: facts.role,
|
|
46
|
+
wallMs: frameSpanWallMs(facts.frameSpan),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function compareRankingDesc(
|
|
51
|
+
a: TaishiLegWallClockEntry,
|
|
52
|
+
b: TaishiLegWallClockEntry,
|
|
53
|
+
): number {
|
|
54
|
+
if (b.wallMs !== a.wallMs) return b.wallMs - a.wallMs;
|
|
55
|
+
if (a.book !== b.book) return a.book.localeCompare(b.book);
|
|
56
|
+
if (a.role !== b.role) return a.role.localeCompare(b.role);
|
|
57
|
+
return a.runId.localeCompare(b.runId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Discovered by taishi-metric-families loader (default export). */
|
|
61
|
+
const legWallClockFamily: TaishiMetricFamilyModule = {
|
|
62
|
+
id: "leg-wall-clock",
|
|
63
|
+
contribute(input) {
|
|
64
|
+
if (input.runs.length === 0) {
|
|
65
|
+
// No readable runs — omit section rather than invent zero metrics.
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ranking = input.runs.map(projectEntry).sort(compareRankingDesc);
|
|
70
|
+
const walls = ranking.map((leg) => leg.wallMs);
|
|
71
|
+
const medianWallMs = medianNumber(walls);
|
|
72
|
+
// runs.length > 0 ⇒ medianNumber returns a defined number.
|
|
73
|
+
if (medianWallMs === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let totalElapsedMs = 0;
|
|
78
|
+
for (const wallMs of walls) totalElapsedMs += wallMs;
|
|
79
|
+
|
|
80
|
+
const section: TaishiLegWallClockSection = {
|
|
81
|
+
kind: "taishi-leg-wall-clock",
|
|
82
|
+
ranking,
|
|
83
|
+
medianWallMs,
|
|
84
|
+
totalElapsedMs,
|
|
85
|
+
};
|
|
86
|
+
return { legWallClock: section };
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export default legWallClockFamily;
|