@akagilnc/pi-workflow-roles 0.1.4800 → 0.1.4870

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.
Files changed (143) hide show
  1. package/dist/acp-host/production-host.js +19434 -18218
  2. package/dist/analyst-book-key.js +63 -0
  3. package/dist/analyst-cohort.js +167 -0
  4. package/dist/analyst-entry.js +320 -0
  5. package/dist/analyst-index.js +247 -0
  6. package/dist/analyst-ledger.js +561 -0
  7. package/dist/analyst-median.js +16 -0
  8. package/dist/analyst-metric-families/acceptance-success-rework.js +249 -0
  9. package/dist/analyst-metric-families/b2-frame-buckets-actions.js +189 -0
  10. package/dist/analyst-metric-families/gate-cycles.js +77 -0
  11. package/dist/analyst-metric-families/leg-wall-clock.js +56 -0
  12. package/dist/analyst-metric-families/round-timeline.js +126 -0
  13. package/dist/analyst-metric-families.js +23 -0
  14. package/dist/analyst-metric-family.js +11 -0
  15. package/dist/analyst-model-groups.js +135 -0
  16. package/dist/analyst-page.js +182 -0
  17. package/dist/atomic-write.js +23 -0
  18. package/dist/auditor-soul.js +1 -1
  19. package/dist/countersign-contracts.js +0 -2
  20. package/dist/diarist-contracts.js +0 -1
  21. package/dist/doctor-contracts.js +2 -2
  22. package/dist/gatekeeper-role.js +15 -16
  23. package/dist/gleaner-left-contracts.js +0 -2
  24. package/dist/headless-host/production-host.js +16967 -15749
  25. package/dist/host-contracts.js +0 -4
  26. package/dist/inspector-contracts.js +0 -2
  27. package/dist/institutional-resolution.js +4 -2
  28. package/dist/merger-contracts.js +0 -2
  29. package/dist/migrate-book-topology.js +3908 -3089
  30. package/dist/navigator-public-session.js +4 -3
  31. package/dist/notary-contracts.js +0 -2
  32. package/dist/package-contracts/auditor-output.js +0 -1
  33. package/dist/package-contracts/collector-output.js +0 -2
  34. package/dist/package-contracts/fixer-output.js +0 -2
  35. package/dist/package-contracts/gatekeeper-output.js +0 -2
  36. package/dist/package-contracts/judge-output.js +0 -2
  37. package/dist/package-contracts/navigator-output.js +0 -2
  38. package/dist/package-contracts/reviewer-output.js +0 -2
  39. package/dist/package-contracts/terminating-tools.js +32 -92
  40. package/dist/package-contracts/worker-output.js +0 -4
  41. package/dist/packaged-role-registry.js +518 -36
  42. package/dist/pi/role-turn-host.js +2 -1
  43. package/dist/public-cli/analyst-run.js +185 -0
  44. package/dist/public-cli/cli.js +885 -0
  45. package/dist/public-cli/config.js +12 -9
  46. package/dist/public-cli/countersign-run.js +4 -311
  47. package/dist/public-cli/instruction-seat-run.js +628 -158
  48. package/dist/public-cli/invocation.js +806 -1550
  49. package/dist/public-cli/main.js +6962 -9882
  50. package/dist/public-cli/option-definitions.js +51 -99
  51. package/dist/public-cli/post-admission.js +30 -2
  52. package/dist/public-cli/run-lifecycle.js +234 -353
  53. package/dist/public-cli/seat-ticket-binding.js +6 -0
  54. package/dist/public-cli/settlement.js +261 -675
  55. package/dist/public-cli/turn-request.js +76 -0
  56. package/dist/public-role-summons.js +38 -112
  57. package/dist/role-activation-flags.js +23 -45
  58. package/dist/secretariat-contracts.js +0 -2
  59. package/dist/session-identity.js +17 -8
  60. package/dist/session-opening-materials.js +3 -2
  61. package/extensions/role-runtime.ts +6 -80
  62. package/package.json +1 -1
  63. package/souls/coder.md +1 -2
  64. package/src/acp-host/role-turn-host.ts +4 -1
  65. package/src/analyst-metric-families/acceptance-success-rework.ts +3 -2
  66. package/src/auditor-role.ts +1 -2
  67. package/src/auditor-soul.ts +1 -1
  68. package/src/collector-role.ts +2 -2
  69. package/src/countersign-contracts.ts +0 -1
  70. package/src/countersign-role.ts +1 -1
  71. package/src/diarist-contracts.ts +0 -1
  72. package/src/diarist-role.ts +0 -1
  73. package/src/doctor-auditor.ts +1 -2
  74. package/src/doctor-contracts.ts +1 -1
  75. package/src/doctor-role.ts +3 -2
  76. package/src/external-host-turn-loop.ts +1 -1
  77. package/src/factory-board.ts +10 -7
  78. package/src/gatekeeper-role.ts +16 -10
  79. package/src/gleaner-left-contracts.ts +0 -1
  80. package/src/gleaner-left-role.ts +0 -1
  81. package/src/headless-host/role-turn-host.ts +7 -1
  82. package/src/host-contracts.ts +10 -6
  83. package/src/inspector-contracts.ts +0 -1
  84. package/src/inspector-role.ts +0 -2
  85. package/src/institutional-resolution.ts +6 -4
  86. package/src/judge-role.ts +3 -3
  87. package/src/merger-contracts.ts +0 -1
  88. package/src/merger-role.ts +2 -2
  89. package/src/navigator-public-session.ts +7 -6
  90. package/src/navigator-work-context.ts +6 -8
  91. package/src/notary-contracts.ts +0 -1
  92. package/src/notary-role.ts +2 -3
  93. package/src/package-contracts/auditor-output.ts +0 -1
  94. package/src/package-contracts/collector-output.ts +0 -1
  95. package/src/package-contracts/fixer-output.ts +0 -1
  96. package/src/package-contracts/gatekeeper-output.ts +0 -1
  97. package/src/package-contracts/judge-output.ts +0 -1
  98. package/src/package-contracts/navigator-output.ts +0 -1
  99. package/src/package-contracts/reviewer-output.ts +0 -1
  100. package/src/package-contracts/terminating-tools.ts +27 -99
  101. package/src/package-contracts/worker-output.ts +0 -2
  102. package/src/packaged-role-registry.ts +604 -35
  103. package/src/pi/role-turn-host.ts +2 -1
  104. package/src/public-cli/cli.ts +64 -259
  105. package/src/public-cli/config.ts +18 -10
  106. package/src/public-cli/countersign-run.ts +14 -449
  107. package/src/public-cli/instruction-seat-run.ts +758 -277
  108. package/src/public-cli/invocation.ts +1097 -2051
  109. package/src/public-cli/option-definitions.ts +62 -127
  110. package/src/public-cli/post-admission.ts +47 -2
  111. package/src/public-cli/run-lifecycle.ts +303 -613
  112. package/src/public-cli/seat-ticket-binding.ts +6 -0
  113. package/src/public-cli/settlement.ts +399 -1185
  114. package/src/public-cli/turn-request.ts +92 -1
  115. package/src/public-role-summons.ts +44 -128
  116. package/src/reviewer-role.ts +3 -2
  117. package/src/role-activation-flags.ts +20 -41
  118. package/src/role-runtime-dependencies.ts +25 -26
  119. package/src/role-runtime.ts +121 -232
  120. package/src/secretariat-contracts.ts +0 -1
  121. package/src/secretariat-role.ts +0 -2
  122. package/src/session-identity.ts +23 -8
  123. package/src/session-opening-materials.ts +2 -1
  124. package/src/worker-role.ts +3 -4
  125. package/dist/doctor-auditor.js +0 -25
  126. package/dist/public-cli/diarist-run.js +0 -130
  127. package/dist/public-cli/doctor-run.js +0 -87
  128. package/dist/public-cli/inspector-run.js +0 -151
  129. package/dist/public-cli/judge-run.js +0 -114
  130. package/dist/public-cli/notary-run.js +0 -174
  131. package/dist/public-cli/reviewer-run.js +0 -339
  132. package/src/public-cli/coder-run.ts +0 -255
  133. package/src/public-cli/collector-run.ts +0 -189
  134. package/src/public-cli/diarist-run.ts +0 -233
  135. package/src/public-cli/doctor-run.ts +0 -167
  136. package/src/public-cli/fixer-run.ts +0 -265
  137. package/src/public-cli/gleaner-left-run.ts +0 -167
  138. package/src/public-cli/inspector-run.ts +0 -261
  139. package/src/public-cli/judge-run.ts +0 -196
  140. package/src/public-cli/merger-run.ts +0 -294
  141. package/src/public-cli/notary-run.ts +0 -279
  142. package/src/public-cli/reviewer-run.ts +0 -480
  143. package/src/public-cli/secretariat-run.ts +0 -265
@@ -0,0 +1,249 @@
1
+ import { medianNumber } from "../analyst-median.js";
2
+ import { packagedAnalystTerminal } from "../packaged-role-registry.js";
3
+ const WORKER_ROLES = new Set(["coder", "fixer"]);
4
+ /** Lawful acceptance vocabulary by role (PRD 受理终态映射). */
5
+ const ACCEPTED_STATUS = {
6
+ coder: new Set(["completed", "refused", "partially_completed", "unfinished", "planned"]),
7
+ fixer: new Set(["completed", "refused", "partially_completed", "unfinished", "planned"]),
8
+ judge: new Set(["converged", "continue", "escalate"]),
9
+ reviewer: new Set(["completed", "refused"]),
10
+ doctor: new Set(["completed", "refused"]),
11
+ merger: new Set(["completed", "escalate"]),
12
+ };
13
+ /** Success vocabulary by role (PRD 成功集合). planned excluded for workers. */
14
+ const SUCCESS_STATUS = {
15
+ coder: new Set(["completed"]),
16
+ fixer: new Set(["completed"]),
17
+ // Judge: producing any of the three verdicts completes the duty.
18
+ judge: new Set(["converged", "continue", "escalate"]),
19
+ reviewer: new Set(["completed"]),
20
+ doctor: new Set(["completed"]),
21
+ merger: new Set(["completed"]),
22
+ };
23
+ function isRecord(value) {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+ function wallMsFromSpan(span) {
27
+ return Date.parse(span.endedAt) - Date.parse(span.startedAt);
28
+ }
29
+ /** Locate collector groups array: receipt.groups or top-level groups. */
30
+ function findCollectorGroups(body) {
31
+ if (Array.isArray(body.groups))
32
+ return body.groups;
33
+ const receipt = body.receipt;
34
+ if (isRecord(receipt) && Array.isArray(receipt.groups))
35
+ return receipt.groups;
36
+ const outcome = body.outcome;
37
+ if (isRecord(outcome)) {
38
+ const facts = outcome.decisiveFacts;
39
+ if (isRecord(facts) && Array.isArray(facts.groups))
40
+ return facts.groups;
41
+ }
42
+ return undefined;
43
+ }
44
+ /** Extract role receipt status from canonical terminal body faces. */
45
+ function extractStatus(body) {
46
+ const outcome = body.outcome;
47
+ if (isRecord(outcome) && typeof outcome.status === "string" && outcome.status.trim() !== "") {
48
+ return outcome.status;
49
+ }
50
+ const receipt = body.receipt;
51
+ if (isRecord(receipt) && typeof receipt.status === "string" && receipt.status.trim() !== "") {
52
+ return receipt.status;
53
+ }
54
+ if (typeof body.status === "string" && body.status.trim() !== "") {
55
+ return body.status;
56
+ }
57
+ return undefined;
58
+ }
59
+ function mapTerminal(role, terminal) {
60
+ if (terminal.status === "absent") {
61
+ return {
62
+ terminalLabel: "no-receipt",
63
+ accepted: false,
64
+ success: false,
65
+ successEligible: false,
66
+ noReceipt: true,
67
+ };
68
+ }
69
+ const body = terminal.body;
70
+ // Seat-declared terminal: typed groups array presence is the sole acceptance discriminator.
71
+ if (packagedAnalystTerminal(role) === "groups") {
72
+ const groups = findCollectorGroups(body);
73
+ if (Array.isArray(groups)) {
74
+ return {
75
+ terminalLabel: "groups",
76
+ accepted: true,
77
+ success: true,
78
+ successEligible: true,
79
+ noReceipt: false,
80
+ };
81
+ }
82
+ return {
83
+ terminalLabel: "non-accepted",
84
+ accepted: false,
85
+ success: false,
86
+ successEligible: false,
87
+ noReceipt: false,
88
+ };
89
+ }
90
+ const status = extractStatus(body);
91
+ if (status === undefined) {
92
+ return {
93
+ terminalLabel: "non-accepted",
94
+ accepted: false,
95
+ success: false,
96
+ successEligible: false,
97
+ noReceipt: false,
98
+ };
99
+ }
100
+ const acceptedSet = ACCEPTED_STATUS[role];
101
+ if (acceptedSet === undefined || !acceptedSet.has(status)) {
102
+ return {
103
+ terminalLabel: status,
104
+ accepted: false,
105
+ success: false,
106
+ successEligible: false,
107
+ noReceipt: false,
108
+ };
109
+ }
110
+ const plannedDuty = WORKER_ROLES.has(role) && status === "planned";
111
+ const successSet = SUCCESS_STATUS[role] ?? new Set();
112
+ const success = !plannedDuty && successSet.has(status);
113
+ const successEligible = !plannedDuty;
114
+ return {
115
+ terminalLabel: status,
116
+ accepted: true,
117
+ success,
118
+ successEligible,
119
+ noReceipt: false,
120
+ };
121
+ }
122
+ function projectLegs(runs) {
123
+ // Order within lane+role by startedAt (then runId) to assign ordinals / rework.
124
+ const sorted = [...runs].sort((a, b) => {
125
+ if (a.book !== b.book)
126
+ return a.book.localeCompare(b.book);
127
+ if (a.role !== b.role)
128
+ return a.role.localeCompare(b.role);
129
+ if (a.frameSpan.startedAt !== b.frameSpan.startedAt) {
130
+ return a.frameSpan.startedAt.localeCompare(b.frameSpan.startedAt);
131
+ }
132
+ return a.runId.localeCompare(b.runId);
133
+ });
134
+ const ordinalByKey = new Map();
135
+ const legs = [];
136
+ for (const run of sorted) {
137
+ const key = `${run.book}\0${run.role}`;
138
+ const ordinal = (ordinalByKey.get(key) ?? 0) + 1;
139
+ ordinalByKey.set(key, ordinal);
140
+ const mapped = mapTerminal(run.role, run.terminal);
141
+ legs.push({
142
+ runId: run.runId,
143
+ book: run.book,
144
+ role: run.role,
145
+ startedAt: run.frameSpan.startedAt,
146
+ wallMs: wallMsFromSpan(run.frameSpan),
147
+ terminalLabel: mapped.terminalLabel,
148
+ accepted: mapped.accepted,
149
+ success: mapped.success,
150
+ successEligible: mapped.successEligible,
151
+ noReceipt: mapped.noReceipt,
152
+ ordinalInLaneRole: ordinal,
153
+ rework: ordinal >= 2,
154
+ });
155
+ }
156
+ // Stable page order: book, role, runId (match A1 leg sort).
157
+ return legs.sort((a, b) => {
158
+ if (a.book !== b.book)
159
+ return a.book.localeCompare(b.book);
160
+ if (a.role !== b.role)
161
+ return a.role.localeCompare(b.role);
162
+ return a.runId.localeCompare(b.runId);
163
+ });
164
+ }
165
+ function aggregateByRole(legs) {
166
+ const roles = [...new Set(legs.map((leg) => leg.role))].sort((a, b) => a.localeCompare(b));
167
+ return roles.map((role) => {
168
+ const roleLegs = legs.filter((leg) => leg.role === role);
169
+ const acceptedCount = roleLegs.filter((leg) => leg.accepted).length;
170
+ const successEligibleCount = roleLegs.filter((leg) => leg.successEligible).length;
171
+ const successCount = roleLegs.filter((leg) => leg.success).length;
172
+ const noReceiptCount = roleLegs.filter((leg) => leg.noReceipt).length;
173
+ // Appearance lanes + first-pass: first call by startedAt within each book.
174
+ const byBook = new Map();
175
+ for (const leg of roleLegs) {
176
+ const list = byBook.get(leg.book) ?? [];
177
+ list.push(leg);
178
+ byBook.set(leg.book, list);
179
+ }
180
+ const books = [...byBook.keys()].sort((a, b) => a.localeCompare(b));
181
+ const convergenceRounds = [];
182
+ let firstPassLaneCount = 0;
183
+ for (const book of books) {
184
+ const laneLegs = [...byBook.get(book)].sort((a, b) => {
185
+ if (a.startedAt !== b.startedAt)
186
+ return a.startedAt.localeCompare(b.startedAt);
187
+ return a.runId.localeCompare(b.runId);
188
+ });
189
+ convergenceRounds.push(laneLegs.length);
190
+ const first = laneLegs[0];
191
+ if (first.accepted)
192
+ firstPassLaneCount += 1;
193
+ }
194
+ const appearanceLaneCount = books.length;
195
+ return {
196
+ role,
197
+ acceptedCount,
198
+ successEligibleCount,
199
+ successCount,
200
+ noReceiptCount,
201
+ successRate: successEligibleCount === 0 ? undefined : successCount / successEligibleCount,
202
+ appearanceLaneCount,
203
+ firstPassLaneCount,
204
+ firstPassRate: appearanceLaneCount === 0 ? undefined : firstPassLaneCount / appearanceLaneCount,
205
+ convergenceRounds,
206
+ convergenceRoundsMedian: medianNumber(convergenceRounds),
207
+ };
208
+ });
209
+ }
210
+ function reworkLens(legs) {
211
+ let reworkWallMs = 0;
212
+ let totalWallMs = 0;
213
+ let reworkLegCount = 0;
214
+ for (const leg of legs) {
215
+ totalWallMs += leg.wallMs;
216
+ if (leg.rework) {
217
+ reworkWallMs += leg.wallMs;
218
+ reworkLegCount += 1;
219
+ }
220
+ }
221
+ return {
222
+ reworkWallMs,
223
+ totalWallMs,
224
+ reworkRatio: totalWallMs === 0 ? undefined : reworkWallMs / totalWallMs,
225
+ reworkLegCount,
226
+ totalLegCount: legs.length,
227
+ };
228
+ }
229
+ export function buildAcceptanceSuccessReworkSection(runs) {
230
+ if (runs.length === 0)
231
+ return undefined;
232
+ const legs = projectLegs(runs);
233
+ return {
234
+ kind: "analyst-acceptance-success-rework",
235
+ legs,
236
+ byRole: aggregateByRole(legs),
237
+ rework: reworkLens(legs),
238
+ };
239
+ }
240
+ const acceptanceSuccessReworkFamily = {
241
+ id: "acceptance-success-rework",
242
+ contribute(input) {
243
+ const section = buildAcceptanceSuccessReworkSection(input.runs);
244
+ if (section === undefined)
245
+ return undefined;
246
+ return { acceptanceSuccessRework: section };
247
+ },
248
+ };
249
+ export default acceptanceSuccessReworkFamily;
@@ -0,0 +1,189 @@
1
+ import { medianNumber } from "../analyst-median.js";
2
+ function timestampMs(iso) {
3
+ const ms = Date.parse(iso);
4
+ if (!Number.isFinite(ms)) {
5
+ throw new Error(`unparseable timestamp: ${iso}`);
6
+ }
7
+ return ms;
8
+ }
9
+ function toIso(ms) {
10
+ return new Date(ms).toISOString();
11
+ }
12
+ function closedTools(intervals) {
13
+ const out = [];
14
+ for (const interval of intervals) {
15
+ if (interval.endedAt === undefined)
16
+ continue;
17
+ const startMs = timestampMs(interval.startedAt);
18
+ const endMs = timestampMs(interval.endedAt);
19
+ if (endMs <= startMs)
20
+ continue;
21
+ out.push({
22
+ toolCallId: interval.toolCallId,
23
+ toolName: interval.toolName,
24
+ startedAt: interval.startedAt,
25
+ endedAt: interval.endedAt,
26
+ startMs,
27
+ endMs,
28
+ ...(interval.command !== undefined ? { command: interval.command } : {}),
29
+ });
30
+ }
31
+ return out;
32
+ }
33
+ /**
34
+ * Clip closed tools to the leg frame. Empty after clip are dropped.
35
+ * Bucket, complement, and tool actions all consume this same bounded set.
36
+ */
37
+ function clipToolsToFrame(tools, frameStartMs, frameEndMs) {
38
+ if (frameEndMs <= frameStartMs)
39
+ return [];
40
+ const out = [];
41
+ for (const tool of tools) {
42
+ const startMs = Math.max(tool.startMs, frameStartMs);
43
+ const endMs = Math.min(tool.endMs, frameEndMs);
44
+ if (endMs <= startMs)
45
+ continue;
46
+ out.push({
47
+ toolCallId: tool.toolCallId,
48
+ toolName: tool.toolName,
49
+ startMs,
50
+ endMs,
51
+ startedAt: startMs === tool.startMs ? tool.startedAt : toIso(startMs),
52
+ endedAt: endMs === tool.endMs ? tool.endedAt : toIso(endMs),
53
+ ...(tool.command !== undefined ? { command: tool.command } : {}),
54
+ });
55
+ }
56
+ return out;
57
+ }
58
+ /** Merge overlapping/adjacent [start,end) intervals; return sorted disjoint union. */
59
+ function mergeUnion(intervals) {
60
+ if (intervals.length === 0)
61
+ return [];
62
+ const sorted = [...intervals].sort((a, b) => a.startMs - b.startMs || a.endMs - b.endMs);
63
+ const merged = [
64
+ { startMs: sorted[0].startMs, endMs: sorted[0].endMs },
65
+ ];
66
+ for (let i = 1; i < sorted.length; i += 1) {
67
+ const cur = sorted[i];
68
+ const last = merged[merged.length - 1];
69
+ if (cur.startMs <= last.endMs) {
70
+ last.endMs = Math.max(last.endMs, cur.endMs);
71
+ }
72
+ else {
73
+ merged.push({ startMs: cur.startMs, endMs: cur.endMs });
74
+ }
75
+ }
76
+ return merged;
77
+ }
78
+ /**
79
+ * Maximal continuous complement of (already frame-clipped) tool union
80
+ * inside [frameStart, frameEnd]. Includes leading and trailing gaps.
81
+ */
82
+ function modelMaximalIntervals(frameStartMs, frameEndMs, toolUnion) {
83
+ if (frameEndMs <= frameStartMs)
84
+ return [];
85
+ const gaps = [];
86
+ let cursor = frameStartMs;
87
+ for (const interval of toolUnion) {
88
+ // Union is produced from frame-clipped tools, so bounds already lie in frame.
89
+ if (interval.startMs > cursor) {
90
+ gaps.push({ startMs: cursor, endMs: interval.startMs });
91
+ }
92
+ cursor = Math.max(cursor, interval.endMs);
93
+ }
94
+ if (cursor < frameEndMs) {
95
+ gaps.push({ startMs: cursor, endMs: frameEndMs });
96
+ }
97
+ return gaps;
98
+ }
99
+ function toolAction(tool) {
100
+ const action = {
101
+ kind: "tool",
102
+ toolCallId: tool.toolCallId,
103
+ toolName: tool.toolName,
104
+ durationMs: tool.endMs - tool.startMs,
105
+ startedAt: tool.startedAt,
106
+ endedAt: tool.endedAt,
107
+ };
108
+ // A2 already owns bash first-line summary; B2 only projects it.
109
+ if (tool.toolName === "bash" && tool.command !== undefined) {
110
+ return {
111
+ ...action,
112
+ commandSummary: tool.command,
113
+ };
114
+ }
115
+ return action;
116
+ }
117
+ function modelAction(gap) {
118
+ return {
119
+ kind: "model",
120
+ durationMs: gap.endMs - gap.startMs,
121
+ startedAt: toIso(gap.startMs),
122
+ endedAt: toIso(gap.endMs),
123
+ };
124
+ }
125
+ function sortActionsDescending(actions) {
126
+ return [...actions].sort((a, b) => {
127
+ if (b.durationMs !== a.durationMs)
128
+ return b.durationMs - a.durationMs;
129
+ // Stable tie-break: earlier start first, then kind/toolCallId for determinism.
130
+ if (a.startedAt !== b.startedAt)
131
+ return a.startedAt.localeCompare(b.startedAt);
132
+ if (a.kind !== b.kind)
133
+ return a.kind.localeCompare(b.kind);
134
+ if (a.kind === "tool" && b.kind === "tool") {
135
+ return a.toolCallId.localeCompare(b.toolCallId);
136
+ }
137
+ return 0;
138
+ });
139
+ }
140
+ /** Pure B2 kernel over one readable run's A2 facts. */
141
+ export function computeAnalystB2RunMetrics(facts) {
142
+ const frameStartMs = timestampMs(facts.frameSpan.startedAt);
143
+ const frameEndMs = timestampMs(facts.frameSpan.endedAt);
144
+ const wallMs = Math.max(0, frameEndMs - frameStartMs);
145
+ // Single frame-bounded core: clip → union once → bucket/complement/actions.
146
+ const tools = clipToolsToFrame(closedTools(facts.toolIntervals), frameStartMs, frameEndMs);
147
+ const toolUnion = mergeUnion(tools);
148
+ const toolBucketMs = toolUnion.reduce((sum, interval) => sum + (interval.endMs - interval.startMs), 0);
149
+ // Mutual exclusion: model is exact complement duration (no independent recount).
150
+ const modelBucketMs = wallMs - toolBucketMs;
151
+ const modelGaps = modelMaximalIntervals(frameStartMs, frameEndMs, toolUnion);
152
+ const actions = sortActionsDescending([
153
+ ...tools.map(toolAction),
154
+ ...modelGaps.map(modelAction),
155
+ ]);
156
+ const actionDurationMedianMs = medianNumber(actions.map((action) => action.durationMs));
157
+ return {
158
+ runId: facts.runId,
159
+ book: facts.book,
160
+ role: facts.role,
161
+ wallMs,
162
+ toolBucketMs,
163
+ modelBucketMs,
164
+ actions,
165
+ actionDurationMedianMs,
166
+ };
167
+ }
168
+ const b2FrameBucketsActionsFamily = {
169
+ id: "b2-frame-buckets-actions",
170
+ contribute(input) {
171
+ if (input.runs.length === 0)
172
+ return undefined;
173
+ const runs = [...input.runs]
174
+ .map(computeAnalystB2RunMetrics)
175
+ .sort((a, b) => {
176
+ if (a.book !== b.book)
177
+ return a.book.localeCompare(b.book);
178
+ if (a.role !== b.role)
179
+ return a.role.localeCompare(b.role);
180
+ return a.runId.localeCompare(b.runId);
181
+ });
182
+ const section = {
183
+ kind: "analyst-b2-frame-buckets-actions",
184
+ runs,
185
+ };
186
+ return { b2FrameBucketsActions: section };
187
+ },
188
+ };
189
+ export default b2FrameBucketsActionsFamily;
@@ -0,0 +1,77 @@
1
+ function projectRound(round) {
2
+ return {
3
+ roundIndex: round.roundIndex,
4
+ officer: round.officer,
5
+ status: round.status,
6
+ officerWallMs: round.officerWallMs,
7
+ findingsCount: round.findingsCount,
8
+ origin: round.origin,
9
+ };
10
+ }
11
+ function projectLeg(facts) {
12
+ const rounds = facts.gateCycles.map(projectRound);
13
+ return {
14
+ runId: facts.runId,
15
+ book: facts.book,
16
+ role: facts.role,
17
+ roundCount: rounds.length,
18
+ rounds,
19
+ };
20
+ }
21
+ function compareLegs(a, b) {
22
+ if (a.book !== b.book)
23
+ return a.book.localeCompare(b.book);
24
+ if (a.role !== b.role)
25
+ return a.role.localeCompare(b.role);
26
+ return a.runId.localeCompare(b.runId);
27
+ }
28
+ function emptyAccum() {
29
+ return { rounds: 0, bounceCount: 0, passCount: 0, wallSum: 0 };
30
+ }
31
+ function absorbRound(accum, round) {
32
+ accum.rounds += 1;
33
+ accum.wallSum += round.officerWallMs;
34
+ if (round.status === "bounce")
35
+ accum.bounceCount += 1;
36
+ if (round.status === "pass")
37
+ accum.passCount += 1;
38
+ }
39
+ function finishOfficer(officer, accum) {
40
+ return {
41
+ officer,
42
+ rounds: accum.rounds,
43
+ bounceCount: accum.bounceCount,
44
+ passCount: accum.passCount,
45
+ bounceRate: accum.rounds === 0 ? undefined : accum.bounceCount / accum.rounds,
46
+ meanOfficerWallMs: accum.rounds === 0 ? undefined : accum.wallSum / accum.rounds,
47
+ };
48
+ }
49
+ function summarizeByOfficer(legs) {
50
+ const byOfficer = new Map();
51
+ for (const leg of legs) {
52
+ for (const round of leg.rounds) {
53
+ const accum = byOfficer.get(round.officer) ?? emptyAccum();
54
+ absorbRound(accum, round);
55
+ byOfficer.set(round.officer, accum);
56
+ }
57
+ }
58
+ // Stable officer order; only officers that appeared on the board.
59
+ const officers = ["inspector", "notary"].filter((o) => byOfficer.has(o));
60
+ return officers.map((officer) => finishOfficer(officer, byOfficer.get(officer)));
61
+ }
62
+ /** Discovered by analyst-metric-families loader (default export). */
63
+ const gateCyclesFamily = {
64
+ id: "gate-cycles",
65
+ contribute(input) {
66
+ if (input.runs.length === 0)
67
+ return undefined;
68
+ const legs = input.runs.map(projectLeg).sort(compareLegs);
69
+ const section = {
70
+ kind: "analyst-gate-cycles",
71
+ legs,
72
+ byOfficer: summarizeByOfficer(legs),
73
+ };
74
+ return { gateCycles: section };
75
+ },
76
+ };
77
+ export default gateCyclesFamily;
@@ -0,0 +1,56 @@
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 analyst-metric-families/ (A2 discovery).
7
+ */
8
+ import { medianNumber } from "../analyst-median.js";
9
+ function frameSpanWallMs(span) {
10
+ return Date.parse(span.endedAt) - Date.parse(span.startedAt);
11
+ }
12
+ function projectEntry(facts) {
13
+ return {
14
+ runId: facts.runId,
15
+ book: facts.book,
16
+ role: facts.role,
17
+ wallMs: frameSpanWallMs(facts.frameSpan),
18
+ };
19
+ }
20
+ function compareRankingDesc(a, b) {
21
+ if (b.wallMs !== a.wallMs)
22
+ return b.wallMs - a.wallMs;
23
+ if (a.book !== b.book)
24
+ return a.book.localeCompare(b.book);
25
+ if (a.role !== b.role)
26
+ return a.role.localeCompare(b.role);
27
+ return a.runId.localeCompare(b.runId);
28
+ }
29
+ /** Discovered by analyst-metric-families loader (default export). */
30
+ const legWallClockFamily = {
31
+ id: "leg-wall-clock",
32
+ contribute(input) {
33
+ if (input.runs.length === 0) {
34
+ // No readable runs — omit section rather than invent zero metrics.
35
+ return undefined;
36
+ }
37
+ const ranking = input.runs.map(projectEntry).sort(compareRankingDesc);
38
+ const walls = ranking.map((leg) => leg.wallMs);
39
+ const medianWallMs = medianNumber(walls);
40
+ // runs.length > 0 ⇒ medianNumber returns a defined number.
41
+ if (medianWallMs === undefined) {
42
+ return undefined;
43
+ }
44
+ let totalElapsedMs = 0;
45
+ for (const wallMs of walls)
46
+ totalElapsedMs += wallMs;
47
+ const section = {
48
+ kind: "analyst-leg-wall-clock",
49
+ ranking,
50
+ medianWallMs,
51
+ totalElapsedMs,
52
+ };
53
+ return { legWallClock: section };
54
+ },
55
+ };
56
+ export default legWallClockFamily;
@@ -0,0 +1,126 @@
1
+ function isRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ function wallMsFromSpan(startedAt, endedAt) {
5
+ return Date.parse(endedAt) - Date.parse(startedAt);
6
+ }
7
+ function readOutcomeStatus(body) {
8
+ if (!isRecord(body.outcome))
9
+ return undefined;
10
+ const status = body.outcome.status;
11
+ if (typeof status !== "string" || status.trim() === "")
12
+ return undefined;
13
+ return status;
14
+ }
15
+ function readClassCount(body) {
16
+ if (!isRecord(body.outcome))
17
+ return undefined;
18
+ if (!isRecord(body.outcome.decisiveFacts))
19
+ return undefined;
20
+ const classCount = body.outcome.decisiveFacts.classCount;
21
+ if (typeof classCount !== "number" || !Number.isFinite(classCount)) {
22
+ return undefined;
23
+ }
24
+ return classCount;
25
+ }
26
+ function projectTerminal(facts) {
27
+ if (facts.terminal.status === "absent") {
28
+ return { kind: "death", channel: "no-receipt" };
29
+ }
30
+ if (facts.terminal.file === "error.json") {
31
+ return { kind: "death", channel: "error" };
32
+ }
33
+ if (facts.terminal.file === "audit-incomplete.json") {
34
+ return { kind: "death", channel: "audit-incomplete" };
35
+ }
36
+ // report.json — receipt face; classCount only when producer wrote a number.
37
+ const status = readOutcomeStatus(facts.terminal.body);
38
+ const classCount = readClassCount(facts.terminal.body);
39
+ const receiptStatus = status ?? "unparsed";
40
+ if (classCount === undefined) {
41
+ return { kind: "receipt", status: receiptStatus };
42
+ }
43
+ return { kind: "receipt", status: receiptStatus, classCount };
44
+ }
45
+ function projectRunRow(facts) {
46
+ const { startedAt, endedAt } = facts.frameSpan;
47
+ return {
48
+ kind: "run",
49
+ runId: facts.runId,
50
+ book: facts.book,
51
+ role: facts.role,
52
+ startedAt,
53
+ endedAt,
54
+ wallMs: wallMsFromSpan(startedAt, endedAt),
55
+ terminal: projectTerminal(facts),
56
+ };
57
+ }
58
+ function projectUnreadableRow(entry) {
59
+ return {
60
+ kind: "unreadable",
61
+ runId: entry.runId,
62
+ book: entry.book,
63
+ missingSources: entry.missingSources,
64
+ reason: entry.reason,
65
+ firstFrameAt: entry.firstFrameAt,
66
+ };
67
+ }
68
+ /** Sort key: startedAt / present firstFrameAt ascending; absent → +∞ (lane tail). */
69
+ function rowSortStartedAt(row) {
70
+ if (row.kind === "run")
71
+ return row.startedAt;
72
+ if (row.firstFrameAt.status === "present")
73
+ return row.firstFrameAt.at;
74
+ return undefined;
75
+ }
76
+ function compareRows(a, b) {
77
+ const aStart = rowSortStartedAt(a);
78
+ const bStart = rowSortStartedAt(b);
79
+ if (aStart === undefined && bStart === undefined) {
80
+ return a.runId.localeCompare(b.runId);
81
+ }
82
+ if (aStart === undefined)
83
+ return 1;
84
+ if (bStart === undefined)
85
+ return -1;
86
+ if (aStart !== bStart)
87
+ return aStart.localeCompare(bStart);
88
+ return a.runId.localeCompare(b.runId);
89
+ }
90
+ function buildLanes(runs, unreadable) {
91
+ const byLane = new Map();
92
+ const push = (lane, row) => {
93
+ const list = byLane.get(lane);
94
+ if (list === undefined)
95
+ byLane.set(lane, [row]);
96
+ else
97
+ list.push(row);
98
+ };
99
+ for (const facts of runs) {
100
+ push(facts.book, projectRunRow(facts));
101
+ }
102
+ for (const entry of unreadable) {
103
+ push(entry.book, projectUnreadableRow(entry));
104
+ }
105
+ return [...byLane.keys()]
106
+ .sort((a, b) => a.localeCompare(b))
107
+ .map((lane) => ({
108
+ lane,
109
+ rows: [...(byLane.get(lane) ?? [])].sort(compareRows),
110
+ }));
111
+ }
112
+ /** Discovered by analyst-metric-families loader (default export). */
113
+ const roundTimelineFamily = {
114
+ id: "round-timeline",
115
+ contribute(input) {
116
+ if (input.runs.length === 0 && input.unreadable.length === 0) {
117
+ return undefined;
118
+ }
119
+ const section = {
120
+ kind: "analyst-round-timeline",
121
+ lanes: buildLanes(input.runs, input.unreadable),
122
+ };
123
+ return { roundTimeline: section };
124
+ },
125
+ };
126
+ export default roundTimelineFamily;