@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,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B4 round-timeline metric family (#328).
|
|
3
|
+
*
|
|
4
|
+
* per-lane (book) run sequence sorted by start time: role, wall clock,
|
|
5
|
+
* receipt status or death channel, judge classCount when present.
|
|
6
|
+
* Unreadable runs appear as placeholder rows sorted by A2 firstFrameAt when
|
|
7
|
+
* present; only when that fact is absent do they annotate absent and sort last.
|
|
8
|
+
*
|
|
9
|
+
* Consumes only A2 typed run facts + unreadable entries — no second scan.
|
|
10
|
+
*/
|
|
11
|
+
import type { TaishiReadableRunFacts } from "../taishi-ledger.ts";
|
|
12
|
+
import type { TaishiMetricFamilyModule } from "../taishi-metric-family.ts";
|
|
13
|
+
import type {
|
|
14
|
+
TaishiFirstFrameAt,
|
|
15
|
+
TaishiMissingSource,
|
|
16
|
+
TaishiUnreadableRun,
|
|
17
|
+
} from "../taishi-page.ts";
|
|
18
|
+
|
|
19
|
+
export type TaishiRoundTimelineTerminal =
|
|
20
|
+
| {
|
|
21
|
+
readonly kind: "receipt";
|
|
22
|
+
readonly status: string;
|
|
23
|
+
readonly classCount?: number;
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
readonly kind: "death";
|
|
27
|
+
readonly channel: "no-receipt" | "error" | "audit-incomplete";
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type TaishiRoundTimelineRunRow = {
|
|
31
|
+
readonly kind: "run";
|
|
32
|
+
readonly runId: string;
|
|
33
|
+
readonly book: string;
|
|
34
|
+
readonly role: string;
|
|
35
|
+
readonly startedAt: string;
|
|
36
|
+
readonly endedAt: string;
|
|
37
|
+
readonly wallMs: number;
|
|
38
|
+
readonly terminal: TaishiRoundTimelineTerminal;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type TaishiRoundTimelineUnreadableRow = {
|
|
42
|
+
readonly kind: "unreadable";
|
|
43
|
+
readonly runId: string;
|
|
44
|
+
readonly book: string;
|
|
45
|
+
readonly missingSources: readonly TaishiMissingSource[];
|
|
46
|
+
readonly reason: string;
|
|
47
|
+
/**
|
|
48
|
+
* Projected from A2 unreadable.firstFrameAt. Present → sort by `at`;
|
|
49
|
+
* absent → lane tail.
|
|
50
|
+
*/
|
|
51
|
+
readonly firstFrameAt: TaishiFirstFrameAt;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type TaishiRoundTimelineRow =
|
|
55
|
+
| TaishiRoundTimelineRunRow
|
|
56
|
+
| TaishiRoundTimelineUnreadableRow;
|
|
57
|
+
|
|
58
|
+
export type TaishiRoundTimelineLane = {
|
|
59
|
+
readonly lane: string;
|
|
60
|
+
readonly rows: readonly TaishiRoundTimelineRow[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type TaishiRoundTimelineSection = {
|
|
64
|
+
readonly kind: "taishi-round-timeline";
|
|
65
|
+
readonly lanes: readonly TaishiRoundTimelineLane[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
69
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function wallMsFromSpan(startedAt: string, endedAt: string): number {
|
|
73
|
+
return Date.parse(endedAt) - Date.parse(startedAt);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readOutcomeStatus(body: Record<string, unknown>): string | undefined {
|
|
77
|
+
if (!isRecord(body.outcome)) return undefined;
|
|
78
|
+
const status = body.outcome.status;
|
|
79
|
+
if (typeof status !== "string" || status.trim() === "") return undefined;
|
|
80
|
+
return status;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readClassCount(body: Record<string, unknown>): number | undefined {
|
|
84
|
+
if (!isRecord(body.outcome)) return undefined;
|
|
85
|
+
if (!isRecord(body.outcome.decisiveFacts)) return undefined;
|
|
86
|
+
const classCount = body.outcome.decisiveFacts.classCount;
|
|
87
|
+
if (typeof classCount !== "number" || !Number.isFinite(classCount)) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return classCount;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function projectTerminal(facts: TaishiReadableRunFacts): TaishiRoundTimelineTerminal {
|
|
94
|
+
if (facts.terminal.status === "absent") {
|
|
95
|
+
return { kind: "death", channel: "no-receipt" };
|
|
96
|
+
}
|
|
97
|
+
if (facts.terminal.file === "error.json") {
|
|
98
|
+
return { kind: "death", channel: "error" };
|
|
99
|
+
}
|
|
100
|
+
if (facts.terminal.file === "audit-incomplete.json") {
|
|
101
|
+
return { kind: "death", channel: "audit-incomplete" };
|
|
102
|
+
}
|
|
103
|
+
// report.json — receipt face; classCount only when producer wrote a number.
|
|
104
|
+
const status = readOutcomeStatus(facts.terminal.body);
|
|
105
|
+
const classCount = readClassCount(facts.terminal.body);
|
|
106
|
+
const receiptStatus = status ?? "unparsed";
|
|
107
|
+
if (classCount === undefined) {
|
|
108
|
+
return { kind: "receipt", status: receiptStatus };
|
|
109
|
+
}
|
|
110
|
+
return { kind: "receipt", status: receiptStatus, classCount };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function projectRunRow(facts: TaishiReadableRunFacts): TaishiRoundTimelineRunRow {
|
|
114
|
+
const { startedAt, endedAt } = facts.frameSpan;
|
|
115
|
+
return {
|
|
116
|
+
kind: "run",
|
|
117
|
+
runId: facts.runId,
|
|
118
|
+
book: facts.book,
|
|
119
|
+
role: facts.role,
|
|
120
|
+
startedAt,
|
|
121
|
+
endedAt,
|
|
122
|
+
wallMs: wallMsFromSpan(startedAt, endedAt),
|
|
123
|
+
terminal: projectTerminal(facts),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function projectUnreadableRow(
|
|
128
|
+
entry: TaishiUnreadableRun,
|
|
129
|
+
): TaishiRoundTimelineUnreadableRow {
|
|
130
|
+
return {
|
|
131
|
+
kind: "unreadable",
|
|
132
|
+
runId: entry.runId,
|
|
133
|
+
book: entry.book,
|
|
134
|
+
missingSources: entry.missingSources,
|
|
135
|
+
reason: entry.reason,
|
|
136
|
+
firstFrameAt: entry.firstFrameAt,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Sort key: startedAt / present firstFrameAt ascending; absent → +∞ (lane tail). */
|
|
141
|
+
function rowSortStartedAt(row: TaishiRoundTimelineRow): string | undefined {
|
|
142
|
+
if (row.kind === "run") return row.startedAt;
|
|
143
|
+
if (row.firstFrameAt.status === "present") return row.firstFrameAt.at;
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function compareRows(a: TaishiRoundTimelineRow, b: TaishiRoundTimelineRow): number {
|
|
148
|
+
const aStart = rowSortStartedAt(a);
|
|
149
|
+
const bStart = rowSortStartedAt(b);
|
|
150
|
+
if (aStart === undefined && bStart === undefined) {
|
|
151
|
+
return a.runId.localeCompare(b.runId);
|
|
152
|
+
}
|
|
153
|
+
if (aStart === undefined) return 1;
|
|
154
|
+
if (bStart === undefined) return -1;
|
|
155
|
+
if (aStart !== bStart) return aStart.localeCompare(bStart);
|
|
156
|
+
return a.runId.localeCompare(b.runId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildLanes(
|
|
160
|
+
runs: readonly TaishiReadableRunFacts[],
|
|
161
|
+
unreadable: readonly TaishiUnreadableRun[],
|
|
162
|
+
): readonly TaishiRoundTimelineLane[] {
|
|
163
|
+
const byLane = new Map<string, TaishiRoundTimelineRow[]>();
|
|
164
|
+
|
|
165
|
+
const push = (lane: string, row: TaishiRoundTimelineRow): void => {
|
|
166
|
+
const list = byLane.get(lane);
|
|
167
|
+
if (list === undefined) byLane.set(lane, [row]);
|
|
168
|
+
else list.push(row);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
for (const facts of runs) {
|
|
172
|
+
push(facts.book, projectRunRow(facts));
|
|
173
|
+
}
|
|
174
|
+
for (const entry of unreadable) {
|
|
175
|
+
push(entry.book, projectUnreadableRow(entry));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return [...byLane.keys()]
|
|
179
|
+
.sort((a, b) => a.localeCompare(b))
|
|
180
|
+
.map((lane) => ({
|
|
181
|
+
lane,
|
|
182
|
+
rows: [...(byLane.get(lane) ?? [])].sort(compareRows),
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Discovered by taishi-metric-families loader (default export). */
|
|
187
|
+
const roundTimelineFamily: TaishiMetricFamilyModule = {
|
|
188
|
+
id: "round-timeline",
|
|
189
|
+
contribute(input) {
|
|
190
|
+
if (input.runs.length === 0 && input.unreadable.length === 0) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
const section: TaishiRoundTimelineSection = {
|
|
194
|
+
kind: "taishi-round-timeline",
|
|
195
|
+
lanes: buildLanes(input.runs, input.unreadable),
|
|
196
|
+
};
|
|
197
|
+
return { roundTimeline: section };
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export default roundTimelineFamily;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue-mode metric-family registry (page composition point).
|
|
3
|
+
*
|
|
4
|
+
* B1–B4 families are statically imported so the public single-file bundle
|
|
5
|
+
* (`dist/public-cli/main.js`) carries them. Dynamic directory discovery cannot
|
|
6
|
+
* see sibling modules inside that bundle and must not be a second compute kernel.
|
|
7
|
+
*
|
|
8
|
+
* New issue-page families register by adding a static import + list entry here
|
|
9
|
+
* (same loader seam; no parallel assembly path).
|
|
10
|
+
*/
|
|
11
|
+
import type { TaishiMetricFamilyModule } from "./taishi-metric-family.ts";
|
|
12
|
+
import acceptanceSuccessReworkFamily from "./taishi-metric-families/acceptance-success-rework.ts";
|
|
13
|
+
import b2FrameBucketsActionsFamily from "./taishi-metric-families/b2-frame-buckets-actions.ts";
|
|
14
|
+
import legWallClockFamily from "./taishi-metric-families/leg-wall-clock.ts";
|
|
15
|
+
import roundTimelineFamily from "./taishi-metric-families/round-timeline.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Production issue-page family modules in stable id order.
|
|
19
|
+
* Static graph — reachable from source and from the public CLI bundle alike.
|
|
20
|
+
*/
|
|
21
|
+
const ISSUE_METRIC_FAMILIES: readonly TaishiMetricFamilyModule[] = [
|
|
22
|
+
acceptanceSuccessReworkFamily,
|
|
23
|
+
b2FrameBucketsActionsFamily,
|
|
24
|
+
legWallClockFamily,
|
|
25
|
+
roundTimelineFamily,
|
|
26
|
+
].sort((a, b) => a.id.localeCompare(b.id));
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load the registered issue-page metric families.
|
|
30
|
+
* Always resolves the static package registry (never readdir of a sibling tree).
|
|
31
|
+
*/
|
|
32
|
+
export async function loadTaishiIssueMetricFamilies(): Promise<
|
|
33
|
+
readonly TaishiMetricFamilyModule[]
|
|
34
|
+
> {
|
|
35
|
+
return ISSUE_METRIC_FAMILIES;
|
|
36
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taishi issue-page metric-family registration seam (A2).
|
|
3
|
+
*
|
|
4
|
+
* Each B/C-wave family owns one module file under taishi-metric-families/
|
|
5
|
+
* and is listed in the static loader (taishi-metric-families.ts) so the
|
|
6
|
+
* public single-file bundle carries every family. Families only contribute
|
|
7
|
+
* optional page sections from typed run facts; they do not open a second
|
|
8
|
+
* scan, entry, or page writer.
|
|
9
|
+
*/
|
|
10
|
+
import type { TaishiReadableRunFacts } from "./taishi-ledger.ts";
|
|
11
|
+
import type { TaishiUnreadableRun } from "./taishi-page.ts";
|
|
12
|
+
|
|
13
|
+
export type TaishiMetricFamilyInput = {
|
|
14
|
+
readonly projectRoot: string;
|
|
15
|
+
readonly runs: readonly TaishiReadableRunFacts[];
|
|
16
|
+
readonly unreadable: readonly TaishiUnreadableRun[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Optional top-level page sections contributed by one family module. */
|
|
20
|
+
export type TaishiMetricFamilyContribution = {
|
|
21
|
+
readonly [sectionKey: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type TaishiMetricFamilyModule = {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
contribute(input: TaishiMetricFamilyInput): TaishiMetricFamilyContribution | undefined;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Fold discovered family modules into one section bag (Object.assign order). */
|
|
30
|
+
export function composeTaishiMetricFamilySections(
|
|
31
|
+
families: readonly TaishiMetricFamilyModule[],
|
|
32
|
+
input: TaishiMetricFamilyInput,
|
|
33
|
+
): TaishiMetricFamilyContribution {
|
|
34
|
+
const sections: Record<string, unknown> = {};
|
|
35
|
+
for (const family of families) {
|
|
36
|
+
const piece = family.contribute(input);
|
|
37
|
+
if (piece === undefined) continue;
|
|
38
|
+
Object.assign(sections, piece);
|
|
39
|
+
}
|
|
40
|
+
return sections;
|
|
41
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taishi model-group mode kernel (PRD #298 output ④ / ticket #331).
|
|
3
|
+
*
|
|
4
|
+
* Per-leg raw model grouping (single | mixed:ordered-dedup whole-leg unit)
|
|
5
|
+
* over a caller-typed issue set. Combination mapping is display-alias only:
|
|
6
|
+
* raw group identity and denominators never merge.
|
|
7
|
+
*
|
|
8
|
+
* Acceptance / success faces reuse the B3 terminal map (受理≠成功, planned
|
|
9
|
+
* out of success den) — no second outcome kernel.
|
|
10
|
+
*/
|
|
11
|
+
import { buildAcceptanceSuccessReworkSection } from "./taishi-metric-families/acceptance-success-rework.ts";
|
|
12
|
+
import type { TaishiReadableRunFacts } from "./taishi-ledger.ts";
|
|
13
|
+
import { medianNumber } from "./taishi-median.ts";
|
|
14
|
+
import type { TaishiUnreadableRun } from "./taishi-page.ts";
|
|
15
|
+
|
|
16
|
+
/** One raw model group with rates over legs in that group. */
|
|
17
|
+
export type TaishiModelGroupRow = {
|
|
18
|
+
/** Stable raw identity (single model id, or `mixed:a+b` ordered-dedup). */
|
|
19
|
+
readonly rawGroupKey: string;
|
|
20
|
+
/** Display label after optional alias map; equals rawGroupKey when unmapped. */
|
|
21
|
+
readonly displayName: string;
|
|
22
|
+
readonly legCount: number;
|
|
23
|
+
readonly acceptedCount: number;
|
|
24
|
+
/** acceptedCount / legCount; undefined when legCount is 0. */
|
|
25
|
+
readonly acceptanceRate: number | undefined;
|
|
26
|
+
readonly successCount: number;
|
|
27
|
+
/** Success-rate den (accepted ∧ ¬planned-duty), matching B3. */
|
|
28
|
+
readonly successEligibleCount: number;
|
|
29
|
+
/** successCount / successEligibleCount; undefined when den is 0. */
|
|
30
|
+
readonly successRate: number | undefined;
|
|
31
|
+
readonly noReceiptCount: number;
|
|
32
|
+
/** noReceiptCount / legCount; undefined when legCount is 0. */
|
|
33
|
+
readonly noReceiptRate: number | undefined;
|
|
34
|
+
/** Leg wall-clock median (ms); even count → mean of two middles. */
|
|
35
|
+
readonly wallClockMedianMs: number | undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Typed model-group query output (not a persisted issue page). */
|
|
39
|
+
export type TaishiModelGroupsPage = {
|
|
40
|
+
readonly kind: "taishi-model-groups";
|
|
41
|
+
readonly mode: "model-groups";
|
|
42
|
+
/** Caller-typed scope (physical identities), stable-sorted. */
|
|
43
|
+
readonly projectRoots: readonly string[];
|
|
44
|
+
readonly groups: readonly TaishiModelGroupRow[];
|
|
45
|
+
/** Model-bearing legs only (sum of group dens); model-absent listed under unreadable. */
|
|
46
|
+
readonly legCount: number;
|
|
47
|
+
readonly unreadableCount: number;
|
|
48
|
+
/** Scan damage + session-model vacancy (no empty-string group). */
|
|
49
|
+
readonly unreadable: readonly TaishiUnreadableRun[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build the raw group key for one leg from its ordered-unique model sequence.
|
|
54
|
+
* Empty sequence → undefined (no inventable group; caller lists as typed vacancy).
|
|
55
|
+
* One model → that raw id. Multi → `mixed:` + `+`-joined ordered uniques.
|
|
56
|
+
*/
|
|
57
|
+
export function taishiModelGroupKey(models: readonly string[]): string | undefined {
|
|
58
|
+
if (models.length === 0) return undefined;
|
|
59
|
+
if (models.length === 1) return models[0]!;
|
|
60
|
+
return `mixed:${models.join("+")}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function rate(numerator: number, denominator: number): number | undefined {
|
|
64
|
+
if (denominator === 0) return undefined;
|
|
65
|
+
return numerator / denominator;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function displayNameFor(
|
|
69
|
+
rawGroupKey: string,
|
|
70
|
+
combinationMapping: Readonly<Record<string, string>> | undefined,
|
|
71
|
+
): string {
|
|
72
|
+
if (combinationMapping === undefined) return rawGroupKey;
|
|
73
|
+
const aliased = combinationMapping[rawGroupKey];
|
|
74
|
+
return aliased === undefined ? rawGroupKey : aliased;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sortUnreadable(
|
|
78
|
+
unreadable: readonly TaishiUnreadableRun[],
|
|
79
|
+
): TaishiUnreadableRun[] {
|
|
80
|
+
return [...unreadable].sort((a, b) => {
|
|
81
|
+
if (a.book !== b.book) return a.book.localeCompare(b.book);
|
|
82
|
+
return a.runId.localeCompare(b.runId);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Legs with no usable session model identity cannot join any model group.
|
|
88
|
+
* Reuse the typed unreadable path (locatable missing-source) — not an empty-key group.
|
|
89
|
+
*/
|
|
90
|
+
function modelIdentityAbsentEntry(run: TaishiReadableRunFacts): TaishiUnreadableRun {
|
|
91
|
+
return {
|
|
92
|
+
runId: run.runId,
|
|
93
|
+
book: run.book,
|
|
94
|
+
missingSources: ["session-model"],
|
|
95
|
+
reason: "session has no usable model identity",
|
|
96
|
+
firstFrameAt: { status: "present", at: run.frameSpan.startedAt },
|
|
97
|
+
// Readable legs already admitted a full span — retain end edge for lastActivityAt.
|
|
98
|
+
lastFrameAt: { status: "present", at: run.frameSpan.endedAt },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Aggregate readable runs into model groups.
|
|
104
|
+
* Stats identity is always the raw key; mapping only projects displayName.
|
|
105
|
+
* Model-identity vacancy is excluded from every group den and listed as unreadable.
|
|
106
|
+
*/
|
|
107
|
+
export function buildTaishiModelGroupsPage(input: {
|
|
108
|
+
readonly projectRoots: readonly string[];
|
|
109
|
+
readonly runs: readonly TaishiReadableRunFacts[];
|
|
110
|
+
readonly unreadable: readonly TaishiUnreadableRun[];
|
|
111
|
+
readonly combinationMapping?: Readonly<Record<string, string>>;
|
|
112
|
+
}): TaishiModelGroupsPage {
|
|
113
|
+
// Split before B3 projection: only model-bearing legs enter group dens.
|
|
114
|
+
// Carry the non-empty raw key so aggregation need not re-check vacancy.
|
|
115
|
+
const groupedRuns: { run: TaishiReadableRunFacts; rawGroupKey: string }[] = [];
|
|
116
|
+
const modelAbsent: TaishiUnreadableRun[] = [];
|
|
117
|
+
for (const run of input.runs) {
|
|
118
|
+
const rawGroupKey = taishiModelGroupKey(run.models);
|
|
119
|
+
if (rawGroupKey === undefined) {
|
|
120
|
+
modelAbsent.push(modelIdentityAbsentEntry(run));
|
|
121
|
+
} else {
|
|
122
|
+
groupedRuns.push({ run, rawGroupKey });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const acceptance = buildAcceptanceSuccessReworkSection(
|
|
127
|
+
groupedRuns.map(({ run }) => run),
|
|
128
|
+
);
|
|
129
|
+
const legByRunId = new Map(
|
|
130
|
+
(acceptance?.legs ?? []).map((leg) => [leg.runId, leg] as const),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
type Acc = {
|
|
134
|
+
acceptedCount: number;
|
|
135
|
+
successCount: number;
|
|
136
|
+
successEligibleCount: number;
|
|
137
|
+
noReceiptCount: number;
|
|
138
|
+
walls: number[];
|
|
139
|
+
};
|
|
140
|
+
const byRaw = new Map<string, Acc>();
|
|
141
|
+
|
|
142
|
+
for (const { run, rawGroupKey } of groupedRuns) {
|
|
143
|
+
const leg = legByRunId.get(run.runId);
|
|
144
|
+
if (leg === undefined) {
|
|
145
|
+
// Readable run must project through B3 map; missing is an invariant break.
|
|
146
|
+
throw new Error(
|
|
147
|
+
`taishi model-groups: missing acceptance projection for run ${run.runId}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
let acc = byRaw.get(rawGroupKey);
|
|
151
|
+
if (acc === undefined) {
|
|
152
|
+
acc = {
|
|
153
|
+
acceptedCount: 0,
|
|
154
|
+
successCount: 0,
|
|
155
|
+
successEligibleCount: 0,
|
|
156
|
+
noReceiptCount: 0,
|
|
157
|
+
walls: [],
|
|
158
|
+
};
|
|
159
|
+
byRaw.set(rawGroupKey, acc);
|
|
160
|
+
}
|
|
161
|
+
if (leg.accepted) acc.acceptedCount += 1;
|
|
162
|
+
if (leg.success) acc.successCount += 1;
|
|
163
|
+
if (leg.successEligible) acc.successEligibleCount += 1;
|
|
164
|
+
if (leg.noReceipt) acc.noReceiptCount += 1;
|
|
165
|
+
acc.walls.push(leg.wallMs);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const rawKeys = [...byRaw.keys()].sort((a, b) => a.localeCompare(b));
|
|
169
|
+
const groups: TaishiModelGroupRow[] = rawKeys.map((rawGroupKey) => {
|
|
170
|
+
const acc = byRaw.get(rawGroupKey)!;
|
|
171
|
+
const legCount = acc.walls.length;
|
|
172
|
+
return {
|
|
173
|
+
rawGroupKey,
|
|
174
|
+
displayName: displayNameFor(rawGroupKey, input.combinationMapping),
|
|
175
|
+
legCount,
|
|
176
|
+
acceptedCount: acc.acceptedCount,
|
|
177
|
+
acceptanceRate: rate(acc.acceptedCount, legCount),
|
|
178
|
+
successCount: acc.successCount,
|
|
179
|
+
successEligibleCount: acc.successEligibleCount,
|
|
180
|
+
successRate: rate(acc.successCount, acc.successEligibleCount),
|
|
181
|
+
noReceiptCount: acc.noReceiptCount,
|
|
182
|
+
noReceiptRate: rate(acc.noReceiptCount, legCount),
|
|
183
|
+
wallClockMedianMs: medianNumber(acc.walls),
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const unreadable = sortUnreadable([...input.unreadable, ...modelAbsent]);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
kind: "taishi-model-groups",
|
|
191
|
+
mode: "model-groups",
|
|
192
|
+
projectRoots: [...input.projectRoots].sort((a, b) => a.localeCompare(b)),
|
|
193
|
+
groups,
|
|
194
|
+
legCount: groupedRuns.length,
|
|
195
|
+
unreadableCount: unreadable.length,
|
|
196
|
+
unreadable,
|
|
197
|
+
};
|
|
198
|
+
}
|