@akagilnc/pi-workflow-roles 0.1.5303 → 0.1.5307

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.
@@ -30042,11 +30042,21 @@ function admitFrameSpanFromRows(rows) {
30042
30042
  }
30043
30043
  return {
30044
30044
  models,
30045
- frameSpan: { startedAt: span.startedAt, endedAt: span.endedAt },
30045
+ frameSpan: { startedAt: span.startedAt, endedAt: span.endedAt, wallMs: endedMs - startedMs },
30046
30046
  partialFirstFrameAt: { status: "present", at: span.startedAt },
30047
30047
  partialLastFrameAt: { status: "present", at: span.endedAt }
30048
30048
  };
30049
30049
  }
30050
+ function sumAnalystRunWallMs(runs) {
30051
+ let total = 0;
30052
+ for (const run of runs) total += run.frameSpan.wallMs;
30053
+ return total;
30054
+ }
30055
+ function compareAnalystLegs(a, b) {
30056
+ if (a.book !== b.book) return a.book.localeCompare(b.book);
30057
+ if (a.role !== b.role) return a.role.localeCompare(b.role);
30058
+ return a.runId.localeCompare(b.runId);
30059
+ }
30050
30060
  async function classifyScopedRun(input) {
30051
30061
  const missingSources = [];
30052
30062
  const reasons = [];
@@ -30327,9 +30337,6 @@ var init_analyst_ledger = __esm({
30327
30337
  function isRecord11(value) {
30328
30338
  return typeof value === "object" && value !== null && !Array.isArray(value);
30329
30339
  }
30330
- function wallMsFromSpan(span) {
30331
- return Date.parse(span.endedAt) - Date.parse(span.startedAt);
30332
- }
30333
30340
  function findCollectorGroups(body) {
30334
30341
  if (Array.isArray(body.groups)) return body.groups;
30335
30342
  const receipt = body.receipt;
@@ -30438,7 +30445,7 @@ function projectLegs(runs) {
30438
30445
  book: run.book,
30439
30446
  role: run.role,
30440
30447
  startedAt: run.frameSpan.startedAt,
30441
- wallMs: wallMsFromSpan(run.frameSpan),
30448
+ wallMs: run.frameSpan.wallMs,
30442
30449
  terminalLabel: mapped.terminalLabel,
30443
30450
  accepted: mapped.accepted,
30444
30451
  success: mapped.success,
@@ -30448,11 +30455,7 @@ function projectLegs(runs) {
30448
30455
  rework: ordinal >= 2
30449
30456
  });
30450
30457
  }
30451
- return legs.sort((a, b) => {
30452
- if (a.book !== b.book) return a.book.localeCompare(b.book);
30453
- if (a.role !== b.role) return a.role.localeCompare(b.role);
30454
- return a.runId.localeCompare(b.runId);
30455
- });
30458
+ return legs.sort(compareAnalystLegs);
30456
30459
  }
30457
30460
  function aggregateByRole(legs) {
30458
30461
  const roles = [...new Set(legs.map((leg) => leg.role))].sort((a, b) => a.localeCompare(b));
@@ -30496,12 +30499,10 @@ function aggregateByRole(legs) {
30496
30499
  };
30497
30500
  });
30498
30501
  }
30499
- function reworkLens(legs) {
30502
+ function reworkLens(legs, totalWallMs) {
30500
30503
  let reworkWallMs = 0;
30501
- let totalWallMs = 0;
30502
30504
  let reworkLegCount = 0;
30503
30505
  for (const leg of legs) {
30504
- totalWallMs += leg.wallMs;
30505
30506
  if (leg.rework) {
30506
30507
  reworkWallMs += leg.wallMs;
30507
30508
  reworkLegCount += 1;
@@ -30522,13 +30523,14 @@ function buildAcceptanceSuccessReworkSection(runs) {
30522
30523
  kind: "analyst-acceptance-success-rework",
30523
30524
  legs,
30524
30525
  byRole: aggregateByRole(legs),
30525
- rework: reworkLens(legs)
30526
+ rework: reworkLens(legs, sumAnalystRunWallMs(runs))
30526
30527
  };
30527
30528
  }
30528
30529
  var WORKER_ROLES, ACCEPTED_STATUS, SUCCESS_STATUS, acceptanceSuccessReworkFamily, acceptance_success_rework_default;
30529
30530
  var init_acceptance_success_rework = __esm({
30530
30531
  "src/analyst-metric-families/acceptance-success-rework.ts"() {
30531
30532
  "use strict";
30533
+ init_analyst_ledger();
30532
30534
  init_analyst_median();
30533
30535
  init_packaged_role_registry();
30534
30536
  WORKER_ROLES = /* @__PURE__ */ new Set(["coder", "fixer"]);
@@ -30561,117 +30563,6 @@ var init_acceptance_success_rework = __esm({
30561
30563
  }
30562
30564
  });
30563
30565
 
30564
- // src/analyst-model-groups.ts
30565
- function analystModelGroupKey(models) {
30566
- if (models.length === 0) return void 0;
30567
- if (models.length === 1) return models[0];
30568
- return `mixed:${models.join("+")}`;
30569
- }
30570
- function rate(numerator, denominator) {
30571
- if (denominator === 0) return void 0;
30572
- return numerator / denominator;
30573
- }
30574
- function displayNameFor(rawGroupKey, combinationMapping) {
30575
- if (combinationMapping === void 0) return rawGroupKey;
30576
- const aliased = combinationMapping[rawGroupKey];
30577
- return aliased === void 0 ? rawGroupKey : aliased;
30578
- }
30579
- function sortUnreadable(unreadable) {
30580
- return [...unreadable].sort((a, b) => {
30581
- if (a.book !== b.book) return a.book.localeCompare(b.book);
30582
- return a.runId.localeCompare(b.runId);
30583
- });
30584
- }
30585
- function modelIdentityAbsentEntry(run) {
30586
- return {
30587
- runId: run.runId,
30588
- book: run.book,
30589
- missingSources: ["session-model"],
30590
- reason: "session has no usable model identity",
30591
- firstFrameAt: { status: "present", at: run.frameSpan.startedAt },
30592
- // Readable legs already admitted a full span — retain end edge for lastActivityAt.
30593
- lastFrameAt: { status: "present", at: run.frameSpan.endedAt }
30594
- };
30595
- }
30596
- function buildAnalystModelGroupsPage(input) {
30597
- const groupedRuns = [];
30598
- const modelAbsent = [];
30599
- for (const run of input.runs) {
30600
- const rawGroupKey = analystModelGroupKey(run.models);
30601
- if (rawGroupKey === void 0) {
30602
- modelAbsent.push(modelIdentityAbsentEntry(run));
30603
- } else {
30604
- groupedRuns.push({ run, rawGroupKey });
30605
- }
30606
- }
30607
- const acceptance = buildAcceptanceSuccessReworkSection(
30608
- groupedRuns.map(({ run }) => run)
30609
- );
30610
- const legByRunId = new Map(
30611
- (acceptance?.legs ?? []).map((leg) => [leg.runId, leg])
30612
- );
30613
- const byRaw = /* @__PURE__ */ new Map();
30614
- for (const { run, rawGroupKey } of groupedRuns) {
30615
- const leg = legByRunId.get(run.runId);
30616
- if (leg === void 0) {
30617
- throw new Error(
30618
- `analyst model-groups: missing acceptance projection for run ${run.runId}`
30619
- );
30620
- }
30621
- let acc = byRaw.get(rawGroupKey);
30622
- if (acc === void 0) {
30623
- acc = {
30624
- acceptedCount: 0,
30625
- successCount: 0,
30626
- successEligibleCount: 0,
30627
- noReceiptCount: 0,
30628
- walls: []
30629
- };
30630
- byRaw.set(rawGroupKey, acc);
30631
- }
30632
- if (leg.accepted) acc.acceptedCount += 1;
30633
- if (leg.success) acc.successCount += 1;
30634
- if (leg.successEligible) acc.successEligibleCount += 1;
30635
- if (leg.noReceipt) acc.noReceiptCount += 1;
30636
- acc.walls.push(leg.wallMs);
30637
- }
30638
- const rawKeys = [...byRaw.keys()].sort((a, b) => a.localeCompare(b));
30639
- const groups = rawKeys.map((rawGroupKey) => {
30640
- const acc = byRaw.get(rawGroupKey);
30641
- const legCount = acc.walls.length;
30642
- return {
30643
- rawGroupKey,
30644
- displayName: displayNameFor(rawGroupKey, input.combinationMapping),
30645
- legCount,
30646
- acceptedCount: acc.acceptedCount,
30647
- acceptanceRate: rate(acc.acceptedCount, legCount),
30648
- successCount: acc.successCount,
30649
- successEligibleCount: acc.successEligibleCount,
30650
- successRate: rate(acc.successCount, acc.successEligibleCount),
30651
- noReceiptCount: acc.noReceiptCount,
30652
- noReceiptRate: rate(acc.noReceiptCount, legCount),
30653
- wallClockMedianMs: medianNumber(acc.walls)
30654
- };
30655
- });
30656
- const unreadable = sortUnreadable([...input.unreadable, ...modelAbsent]);
30657
- return {
30658
- kind: "analyst-model-groups",
30659
- mode: "model-groups",
30660
- projectRoots: [...input.projectRoots].sort((a, b) => a.localeCompare(b)),
30661
- groups,
30662
- legCount: groupedRuns.length,
30663
- unreadableCount: unreadable.length,
30664
- unreadable
30665
- };
30666
- }
30667
- var init_analyst_model_groups = __esm({
30668
- "src/analyst-model-groups.ts"() {
30669
- "use strict";
30670
- init_acceptance_success_rework();
30671
- init_analyst_median();
30672
- }
30673
- });
30674
-
30675
30566
  // src/analyst-metric-families/b2-frame-buckets-actions.ts
30676
30567
  function timestampMs(iso) {
30677
30568
  const ms = Date.parse(iso);
@@ -30794,7 +30685,7 @@ function sortActionsDescending(actions) {
30794
30685
  function computeAnalystB2RunMetrics(facts) {
30795
30686
  const frameStartMs = timestampMs(facts.frameSpan.startedAt);
30796
30687
  const frameEndMs = timestampMs(facts.frameSpan.endedAt);
30797
- const wallMs = Math.max(0, frameEndMs - frameStartMs);
30688
+ const wallMs = facts.frameSpan.wallMs;
30798
30689
  const tools = clipToolsToFrame(closedTools(facts.toolIntervals), frameStartMs, frameEndMs);
30799
30690
  const toolUnion = mergeUnion(tools);
30800
30691
  const toolBucketMs = toolUnion.reduce(
@@ -30824,15 +30715,12 @@ var init_b2_frame_buckets_actions = __esm({
30824
30715
  "src/analyst-metric-families/b2-frame-buckets-actions.ts"() {
30825
30716
  "use strict";
30826
30717
  init_analyst_median();
30718
+ init_analyst_ledger();
30827
30719
  b2FrameBucketsActionsFamily = {
30828
30720
  id: "b2-frame-buckets-actions",
30829
30721
  contribute(input) {
30830
30722
  if (input.runs.length === 0) return void 0;
30831
- const runs = [...input.runs].map(computeAnalystB2RunMetrics).sort((a, b) => {
30832
- if (a.book !== b.book) return a.book.localeCompare(b.book);
30833
- if (a.role !== b.role) return a.role.localeCompare(b.role);
30834
- return a.runId.localeCompare(b.runId);
30835
- });
30723
+ const runs = [...input.runs].map(computeAnalystB2RunMetrics).sort(compareAnalystLegs);
30836
30724
  const section = {
30837
30725
  kind: "analyst-b2-frame-buckets-actions",
30838
30726
  runs
@@ -30865,11 +30753,6 @@ function projectLeg(facts) {
30865
30753
  rounds
30866
30754
  };
30867
30755
  }
30868
- function compareLegs(a, b) {
30869
- if (a.book !== b.book) return a.book.localeCompare(b.book);
30870
- if (a.role !== b.role) return a.role.localeCompare(b.role);
30871
- return a.runId.localeCompare(b.runId);
30872
- }
30873
30756
  function emptyAccum() {
30874
30757
  return { rounds: 0, bounceCount: 0, passCount: 0, wallSum: 0 };
30875
30758
  }
@@ -30905,11 +30788,12 @@ var gateCyclesFamily, gate_cycles_default;
30905
30788
  var init_gate_cycles = __esm({
30906
30789
  "src/analyst-metric-families/gate-cycles.ts"() {
30907
30790
  "use strict";
30791
+ init_analyst_ledger();
30908
30792
  gateCyclesFamily = {
30909
30793
  id: "gate-cycles",
30910
30794
  contribute(input) {
30911
30795
  if (input.runs.length === 0) return void 0;
30912
- const legs = input.runs.map(projectLeg).sort(compareLegs);
30796
+ const legs = input.runs.map(projectLeg).sort(compareAnalystLegs);
30913
30797
  const section = {
30914
30798
  kind: "analyst-gate-cycles",
30915
30799
  legs,
@@ -30923,28 +30807,24 @@ var init_gate_cycles = __esm({
30923
30807
  });
30924
30808
 
30925
30809
  // src/analyst-metric-families/leg-wall-clock.ts
30926
- function frameSpanWallMs(span) {
30927
- return Date.parse(span.endedAt) - Date.parse(span.startedAt);
30928
- }
30929
30810
  function projectEntry(facts) {
30930
30811
  return {
30931
30812
  runId: facts.runId,
30932
30813
  book: facts.book,
30933
30814
  role: facts.role,
30934
- wallMs: frameSpanWallMs(facts.frameSpan)
30815
+ wallMs: facts.frameSpan.wallMs
30935
30816
  };
30936
30817
  }
30937
30818
  function compareRankingDesc(a, b) {
30938
30819
  if (b.wallMs !== a.wallMs) return b.wallMs - a.wallMs;
30939
- if (a.book !== b.book) return a.book.localeCompare(b.book);
30940
- if (a.role !== b.role) return a.role.localeCompare(b.role);
30941
- return a.runId.localeCompare(b.runId);
30820
+ return compareAnalystLegs(a, b);
30942
30821
  }
30943
30822
  var legWallClockFamily, leg_wall_clock_default;
30944
30823
  var init_leg_wall_clock = __esm({
30945
30824
  "src/analyst-metric-families/leg-wall-clock.ts"() {
30946
30825
  "use strict";
30947
30826
  init_analyst_median();
30827
+ init_analyst_ledger();
30948
30828
  legWallClockFamily = {
30949
30829
  id: "leg-wall-clock",
30950
30830
  contribute(input) {
@@ -30957,13 +30837,11 @@ var init_leg_wall_clock = __esm({
30957
30837
  if (medianWallMs === void 0) {
30958
30838
  return void 0;
30959
30839
  }
30960
- let totalElapsedMs = 0;
30961
- for (const wallMs of walls) totalElapsedMs += wallMs;
30962
30840
  const section = {
30963
30841
  kind: "analyst-leg-wall-clock",
30964
30842
  ranking,
30965
30843
  medianWallMs,
30966
- totalElapsedMs
30844
+ totalElapsedMs: sumAnalystRunWallMs(input.runs)
30967
30845
  };
30968
30846
  return { legWallClock: section };
30969
30847
  }
@@ -30976,9 +30854,6 @@ var init_leg_wall_clock = __esm({
30976
30854
  function isRecord12(value) {
30977
30855
  return typeof value === "object" && value !== null && !Array.isArray(value);
30978
30856
  }
30979
- function wallMsFromSpan2(startedAt, endedAt) {
30980
- return Date.parse(endedAt) - Date.parse(startedAt);
30981
- }
30982
30857
  function readClassCount(body) {
30983
30858
  if (!isRecord12(body.outcome)) return void 0;
30984
30859
  if (!isRecord12(body.outcome.decisiveFacts)) return void 0;
@@ -31015,7 +30890,7 @@ function projectRunRow(facts) {
31015
30890
  role: facts.role,
31016
30891
  startedAt,
31017
30892
  endedAt,
31018
- wallMs: wallMsFromSpan2(startedAt, endedAt),
30893
+ wallMs: facts.frameSpan.wallMs,
31019
30894
  terminal: projectTerminal(facts)
31020
30895
  };
31021
30896
  }
@@ -31147,13 +31022,9 @@ function analystIssuePageAddressFromPage(page) {
31147
31022
  };
31148
31023
  }
31149
31024
  function sortLegs(legs) {
31150
- return [...legs].sort((a, b) => {
31151
- if (a.book !== b.book) return a.book.localeCompare(b.book);
31152
- if (a.role !== b.role) return a.role.localeCompare(b.role);
31153
- return a.runId.localeCompare(b.runId);
31154
- });
31025
+ return [...legs].sort(compareAnalystLegs);
31155
31026
  }
31156
- function sortUnreadable2(unreadable) {
31027
+ function sortUnreadable(unreadable) {
31157
31028
  return [...unreadable].sort((a, b) => {
31158
31029
  if (a.book !== b.book) return a.book.localeCompare(b.book);
31159
31030
  return a.runId.localeCompare(b.runId);
@@ -31189,14 +31060,10 @@ function computeAnalystMsPerKLines(totalElapsedMs, changedLines) {
31189
31060
  value: totalElapsedMs / (changedLines.value / 1e3)
31190
31061
  };
31191
31062
  }
31192
- function frameSpanWallMs2(span) {
31193
- return Date.parse(span.endedAt) - Date.parse(span.startedAt);
31194
- }
31195
31063
  function summarizeAnalystRunEfficiency(runs, unreadable = []) {
31196
- let totalElapsedMs = 0;
31064
+ const totalElapsedMs = sumAnalystRunWallMs(runs);
31197
31065
  let latestEndedAt;
31198
31066
  for (const run of runs) {
31199
- totalElapsedMs += frameSpanWallMs2(run.frameSpan);
31200
31067
  const endedAt = run.frameSpan.endedAt;
31201
31068
  if (latestEndedAt === void 0 || endedAt > latestEndedAt) {
31202
31069
  latestEndedAt = endedAt;
@@ -31221,7 +31088,7 @@ async function buildAnalystIssueMetricsPage(input) {
31221
31088
  role: run.role
31222
31089
  }))
31223
31090
  );
31224
- const unreadable = sortUnreadable2(input.unreadable);
31091
+ const unreadable = sortUnreadable(input.unreadable);
31225
31092
  const scopeConflicts = sortScopeConflicts(input.scopeConflicts ?? []);
31226
31093
  const projectRoot = physicalPathIdentity(input.projectRoot);
31227
31094
  const { totalElapsedMs, lastActivityAt } = summarizeAnalystRunEfficiency(
@@ -31267,11 +31134,118 @@ var init_analyst_page = __esm({
31267
31134
  "use strict";
31268
31135
  init_atomic_write();
31269
31136
  init_activation_ledger_topology();
31137
+ init_analyst_ledger();
31270
31138
  init_analyst_metric_families();
31271
31139
  init_analyst_metric_family();
31272
31140
  }
31273
31141
  });
31274
31142
 
31143
+ // src/analyst-model-groups.ts
31144
+ function analystModelGroupKey(models) {
31145
+ if (models.length === 0) return void 0;
31146
+ if (models.length === 1) return models[0];
31147
+ return `mixed:${models.join("+")}`;
31148
+ }
31149
+ function rate(numerator, denominator) {
31150
+ if (denominator === 0) return void 0;
31151
+ return numerator / denominator;
31152
+ }
31153
+ function displayNameFor(rawGroupKey, combinationMapping) {
31154
+ if (combinationMapping === void 0) return rawGroupKey;
31155
+ const aliased = combinationMapping[rawGroupKey];
31156
+ return aliased === void 0 ? rawGroupKey : aliased;
31157
+ }
31158
+ function modelIdentityAbsentEntry(run) {
31159
+ return {
31160
+ runId: run.runId,
31161
+ book: run.book,
31162
+ missingSources: ["session-model"],
31163
+ reason: "session has no usable model identity",
31164
+ firstFrameAt: { status: "present", at: run.frameSpan.startedAt },
31165
+ // Readable legs already admitted a full span — retain end edge for lastActivityAt.
31166
+ lastFrameAt: { status: "present", at: run.frameSpan.endedAt }
31167
+ };
31168
+ }
31169
+ function buildAnalystModelGroupsPage(input) {
31170
+ const groupedRuns = [];
31171
+ const modelAbsent = [];
31172
+ for (const run of input.runs) {
31173
+ const rawGroupKey = analystModelGroupKey(run.models);
31174
+ if (rawGroupKey === void 0) {
31175
+ modelAbsent.push(modelIdentityAbsentEntry(run));
31176
+ } else {
31177
+ groupedRuns.push({ run, rawGroupKey });
31178
+ }
31179
+ }
31180
+ const acceptance = buildAcceptanceSuccessReworkSection(
31181
+ groupedRuns.map(({ run }) => run)
31182
+ );
31183
+ const legByRunId = new Map(
31184
+ (acceptance?.legs ?? []).map((leg) => [leg.runId, leg])
31185
+ );
31186
+ const byRaw = /* @__PURE__ */ new Map();
31187
+ for (const { run, rawGroupKey } of groupedRuns) {
31188
+ const leg = legByRunId.get(run.runId);
31189
+ if (leg === void 0) {
31190
+ throw new Error(
31191
+ `analyst model-groups: missing acceptance projection for run ${run.runId}`
31192
+ );
31193
+ }
31194
+ let acc = byRaw.get(rawGroupKey);
31195
+ if (acc === void 0) {
31196
+ acc = {
31197
+ acceptedCount: 0,
31198
+ successCount: 0,
31199
+ successEligibleCount: 0,
31200
+ noReceiptCount: 0,
31201
+ walls: []
31202
+ };
31203
+ byRaw.set(rawGroupKey, acc);
31204
+ }
31205
+ if (leg.accepted) acc.acceptedCount += 1;
31206
+ if (leg.success) acc.successCount += 1;
31207
+ if (leg.successEligible) acc.successEligibleCount += 1;
31208
+ if (leg.noReceipt) acc.noReceiptCount += 1;
31209
+ acc.walls.push(leg.wallMs);
31210
+ }
31211
+ const rawKeys = [...byRaw.keys()].sort((a, b) => a.localeCompare(b));
31212
+ const groups = rawKeys.map((rawGroupKey) => {
31213
+ const acc = byRaw.get(rawGroupKey);
31214
+ const legCount = acc.walls.length;
31215
+ return {
31216
+ rawGroupKey,
31217
+ displayName: displayNameFor(rawGroupKey, input.combinationMapping),
31218
+ legCount,
31219
+ acceptedCount: acc.acceptedCount,
31220
+ acceptanceRate: rate(acc.acceptedCount, legCount),
31221
+ successCount: acc.successCount,
31222
+ successEligibleCount: acc.successEligibleCount,
31223
+ successRate: rate(acc.successCount, acc.successEligibleCount),
31224
+ noReceiptCount: acc.noReceiptCount,
31225
+ noReceiptRate: rate(acc.noReceiptCount, legCount),
31226
+ wallClockMedianMs: medianNumber(acc.walls)
31227
+ };
31228
+ });
31229
+ const unreadable = sortUnreadable([...input.unreadable, ...modelAbsent]);
31230
+ return {
31231
+ kind: "analyst-model-groups",
31232
+ mode: "model-groups",
31233
+ projectRoots: [...input.projectRoots].sort((a, b) => a.localeCompare(b)),
31234
+ groups,
31235
+ legCount: groupedRuns.length,
31236
+ unreadableCount: unreadable.length,
31237
+ unreadable
31238
+ };
31239
+ }
31240
+ var init_analyst_model_groups = __esm({
31241
+ "src/analyst-model-groups.ts"() {
31242
+ "use strict";
31243
+ init_acceptance_success_rework();
31244
+ init_analyst_median();
31245
+ init_analyst_page();
31246
+ }
31247
+ });
31248
+
31275
31249
  // src/analyst-entry.ts
31276
31250
  import { readFile as readFile20 } from "node:fs/promises";
31277
31251
  function isMissingPathError7(error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5303",
3
+ "version": "0.1.5307",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -319,7 +319,7 @@ function admitFrameSpanFromRows(rows: readonly LedgerSessionRow[]): {
319
319
  }
320
320
  return {
321
321
  models,
322
- frameSpan: { startedAt: span.startedAt, endedAt: span.endedAt },
322
+ frameSpan: { startedAt: span.startedAt, endedAt: span.endedAt, wallMs: endedMs - startedMs },
323
323
  partialFirstFrameAt: { status: "present", at: span.startedAt },
324
324
  partialLastFrameAt: { status: "present", at: span.endedAt },
325
325
  };
@@ -329,6 +329,8 @@ function admitFrameSpanFromRows(rows: readonly LedgerSessionRow[]): {
329
329
  export type AnalystRunFrameSpan = {
330
330
  readonly startedAt: string;
331
331
  readonly endedAt: string;
332
+ /** Validated wall clock, computed once when the session span is admitted. */
333
+ readonly wallMs: number;
332
334
  };
333
335
 
334
336
  /**
@@ -371,6 +373,23 @@ export type AnalystReadableRunFacts = {
371
373
  readonly gateCycles: readonly AnalystGateCycleRound[];
372
374
  };
373
375
 
376
+ /** Total wall clock for exactly the readable runs supplied by the caller. */
377
+ export function sumAnalystRunWallMs(runs: readonly AnalystReadableRunFacts[]): number {
378
+ let total = 0;
379
+ for (const run of runs) total += run.frameSpan.wallMs;
380
+ return total;
381
+ }
382
+
383
+ /** Shared identity order for readable legs and their page/metric projections. */
384
+ export function compareAnalystLegs(
385
+ a: Pick<AnalystReadableRunFacts, "book" | "role" | "runId">,
386
+ b: Pick<AnalystReadableRunFacts, "book" | "role" | "runId">,
387
+ ): number {
388
+ if (a.book !== b.book) return a.book.localeCompare(b.book);
389
+ if (a.role !== b.role) return a.role.localeCompare(b.role);
390
+ return a.runId.localeCompare(b.runId);
391
+ }
392
+
374
393
  export type AnalystScopedRunScan = {
375
394
  /** Readable in-scope runs with retained typed facts (A2). */
376
395
  readonly runs: readonly AnalystReadableRunFacts[];
@@ -9,7 +9,7 @@
9
9
  * - success rate den = success-eligible accepted legs (no-receipt out; planned out)
10
10
  * - planned = plan-duty acceptance; never success numerator or denominator
11
11
  */
12
- import type { AnalystReadableRunFacts, AnalystRunTerminalFace } from "../analyst-ledger.ts";
12
+ import { compareAnalystLegs, sumAnalystRunWallMs, type AnalystReadableRunFacts, type AnalystRunTerminalFace } from "../analyst-ledger.ts";
13
13
  import { medianNumber } from "../analyst-median.ts";
14
14
  import type { AnalystMetricFamilyModule } from "../analyst-metric-family.ts";
15
15
  import { packagedAnalystTerminal } from "../packaged-role-registry.ts";
@@ -99,10 +99,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
99
99
  return typeof value === "object" && value !== null && !Array.isArray(value);
100
100
  }
101
101
 
102
- function wallMsFromSpan(span: { readonly startedAt: string; readonly endedAt: string }): number {
103
- return Date.parse(span.endedAt) - Date.parse(span.startedAt);
104
- }
105
-
106
102
  /** Locate collector groups array: receipt.groups or top-level groups. */
107
103
  function findCollectorGroups(body: Record<string, unknown>): unknown {
108
104
  if (Array.isArray(body.groups)) return body.groups;
@@ -235,7 +231,7 @@ function projectLegs(runs: readonly AnalystReadableRunFacts[]): AnalystAcceptanc
235
231
  book: run.book,
236
232
  role: run.role,
237
233
  startedAt: run.frameSpan.startedAt,
238
- wallMs: wallMsFromSpan(run.frameSpan),
234
+ wallMs: run.frameSpan.wallMs,
239
235
  terminalLabel: mapped.terminalLabel,
240
236
  accepted: mapped.accepted,
241
237
  success: mapped.success,
@@ -247,11 +243,7 @@ function projectLegs(runs: readonly AnalystReadableRunFacts[]): AnalystAcceptanc
247
243
  }
248
244
 
249
245
  // Stable page order: book, role, runId (match A1 leg sort).
250
- return legs.sort((a, b) => {
251
- if (a.book !== b.book) return a.book.localeCompare(b.book);
252
- if (a.role !== b.role) return a.role.localeCompare(b.role);
253
- return a.runId.localeCompare(b.runId);
254
- });
246
+ return legs.sort(compareAnalystLegs);
255
247
  }
256
248
 
257
249
  function aggregateByRole(legs: readonly AnalystAcceptanceLeg[]): AnalystRoleAcceptanceStats[] {
@@ -302,12 +294,10 @@ function aggregateByRole(legs: readonly AnalystAcceptanceLeg[]): AnalystRoleAcce
302
294
  });
303
295
  }
304
296
 
305
- function reworkLens(legs: readonly AnalystAcceptanceLeg[]): AnalystReworkLens {
297
+ function reworkLens(legs: readonly AnalystAcceptanceLeg[], totalWallMs: number): AnalystReworkLens {
306
298
  let reworkWallMs = 0;
307
- let totalWallMs = 0;
308
299
  let reworkLegCount = 0;
309
300
  for (const leg of legs) {
310
- totalWallMs += leg.wallMs;
311
301
  if (leg.rework) {
312
302
  reworkWallMs += leg.wallMs;
313
303
  reworkLegCount += 1;
@@ -331,7 +321,7 @@ export function buildAcceptanceSuccessReworkSection(
331
321
  kind: "analyst-acceptance-success-rework",
332
322
  legs,
333
323
  byRole: aggregateByRole(legs),
334
- rework: reworkLens(legs),
324
+ rework: reworkLens(legs, sumAnalystRunWallMs(runs)),
335
325
  };
336
326
  }
337
327
 
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import type { SessionToolInterval } from "../ledger-session-read.ts";
18
18
  import { medianNumber } from "../analyst-median.ts";
19
- import type { AnalystReadableRunFacts } from "../analyst-ledger.ts";
19
+ import { compareAnalystLegs, type AnalystReadableRunFacts } from "../analyst-ledger.ts";
20
20
  import type { AnalystMetricFamilyModule } from "../analyst-metric-family.ts";
21
21
 
22
22
  export type AnalystB2ToolAction = {
@@ -221,7 +221,7 @@ export function computeAnalystB2RunMetrics(
221
221
  ): AnalystB2RunMetrics {
222
222
  const frameStartMs = timestampMs(facts.frameSpan.startedAt);
223
223
  const frameEndMs = timestampMs(facts.frameSpan.endedAt);
224
- const wallMs = Math.max(0, frameEndMs - frameStartMs);
224
+ const wallMs = facts.frameSpan.wallMs;
225
225
 
226
226
  // Single frame-bounded core: clip → union once → bucket/complement/actions.
227
227
  const tools = clipToolsToFrame(closedTools(facts.toolIntervals), frameStartMs, frameEndMs);
@@ -258,11 +258,7 @@ const b2FrameBucketsActionsFamily: AnalystMetricFamilyModule = {
258
258
  if (input.runs.length === 0) return undefined;
259
259
  const runs = [...input.runs]
260
260
  .map(computeAnalystB2RunMetrics)
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
- });
261
+ .sort(compareAnalystLegs);
266
262
  const section: AnalystB2FrameBucketsActionsSection = {
267
263
  kind: "analyst-b2-frame-buckets-actions",
268
264
  runs,
@@ -9,7 +9,7 @@ import type {
9
9
  AnalystGateCycleOrigin,
10
10
  AnalystGateCycleRound,
11
11
  } from "../analyst-gate-cycles-read.ts";
12
- import type { AnalystReadableRunFacts } from "../analyst-ledger.ts";
12
+ import { compareAnalystLegs, type AnalystReadableRunFacts } from "../analyst-ledger.ts";
13
13
  import type { AnalystMetricFamilyModule } from "../analyst-metric-family.ts";
14
14
 
15
15
  export type AnalystGateCyclesRoundRow = {
@@ -73,12 +73,6 @@ function projectLeg(facts: AnalystReadableRunFacts): AnalystGateCyclesLeg {
73
73
  };
74
74
  }
75
75
 
76
- function compareLegs(a: AnalystGateCyclesLeg, b: AnalystGateCyclesLeg): number {
77
- if (a.book !== b.book) return a.book.localeCompare(b.book);
78
- if (a.role !== b.role) return a.role.localeCompare(b.role);
79
- return a.runId.localeCompare(b.runId);
80
- }
81
-
82
76
  type OfficerAccum = {
83
77
  rounds: number;
84
78
  bounceCount: number;
@@ -132,7 +126,7 @@ const gateCyclesFamily: AnalystMetricFamilyModule = {
132
126
  id: "gate-cycles",
133
127
  contribute(input) {
134
128
  if (input.runs.length === 0) return undefined;
135
- const legs = input.runs.map(projectLeg).sort(compareLegs);
129
+ const legs = input.runs.map(projectLeg).sort(compareAnalystLegs);
136
130
  const section: AnalystGateCyclesSection = {
137
131
  kind: "analyst-gate-cycles",
138
132
  legs,