tumwater 0.4.2 → 0.4.3
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/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +25 -12
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +1 -8
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +9 -0
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +50 -27
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +1 -1
- package/dist/src/agent/subscription-account.js +3 -3
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +58 -102
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +1 -1
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-scoped.js +40 -16
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +34 -12
- package/dist/src/cli/cli-query-commands.js +1 -1
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-start.js +91 -58
- package/dist/src/cli/completion.js +82 -46
- package/dist/src/cli/config-commands.js +82 -71
- package/dist/src/cli/help.js +4 -6
- package/dist/src/cli/log-commands.js +43 -23
- package/dist/src/cli/notify-command.js +2 -2
- package/dist/src/cli/question-commands.js +86 -60
- package/dist/src/cli.js +181 -135
- package/dist/src/collections.js +31 -16
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +22 -0
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +5 -20
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +153 -102
- package/dist/src/config/config-schema.js +9 -11
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +4 -52
- package/dist/src/config/config-validation.js +4 -38
- package/dist/src/config/config-views.js +9 -34
- package/dist/src/config/config-write.js +6 -23
- package/dist/src/config/config.js +4 -5
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +24 -24
- package/dist/src/doctor/doctor-backlog.js +7 -5
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +5 -5
- package/dist/src/doctor/doctor-claude-checks.js +44 -28
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +44 -112
- package/dist/src/doctor/doctor-fallback-checks.js +2 -2
- package/dist/src/doctor/doctor-holds.js +6 -45
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +120 -20
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +3 -3
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +30 -54
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +1 -3
- package/dist/src/events/{notify.js → event-notify.js} +2 -2
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/failure/failure-cluster.js +6 -6
- package/dist/src/failure/failure-data.js +8 -4
- package/dist/src/failure/failure-render.js +44 -34
- package/dist/src/failure/failure-state-change.js +4 -34
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +27 -68
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +22 -23
- package/dist/src/gui/gui-prompt-commands.js +23 -23
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +51 -33
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +8 -11
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +10 -13
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-verify.js +8 -18
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +76 -44
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +27 -32
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +1 -1
- package/dist/src/orchestrator/scheduling-skip.js +1 -1
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +8 -7
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +1 -1
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +32 -6
- package/dist/src/pi/pi-watchdogs.js +12 -5
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +49 -5
- package/dist/src/process/process.js +8 -8
- package/dist/src/process/run-marker.js +31 -13
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +1 -1
- package/dist/src/review/review-precheck.js +11 -1
- package/dist/src/review/review.js +40 -153
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +1 -14
- package/dist/src/roles/role-view.js +9 -21
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/run-apply.js +12 -6
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +135 -84
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +119 -50
- package/dist/src/run/stage-check.js +49 -32
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/runner-outcome.js +6 -1
- package/dist/src/runner/runner-pi.js +3 -10
- package/dist/src/runner/runner-recovery.js +1 -1
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
- package/dist/src/runner/runner-run.js +97 -35
- package/dist/src/runner/runner-state-load.js +12 -9
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-data.js +2 -2
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -17
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +98 -65
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
- package/dist/src/team/team-polls.js +28 -126
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +20 -15
- package/dist/src/text/text.js +3 -3
- package/dist/src/ui/alert-feeds.js +49 -73
- package/dist/src/ui/badges.js +3 -13
- package/dist/src/ui/gui/gui-client-boot.js +2 -2
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +4 -4
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +53 -40
- package/dist/src/ui/run-progress-model.js +9 -2
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +3 -7
- package/dist/src/ui/status-render.js +5 -9
- package/dist/src/ui/team-alert-backlog.js +6 -6
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +7 -8
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +1 -7
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +159 -92
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +3 -3
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -228
- package/dist/src/ui/tui/tui-pane-history.js +61 -70
- package/dist/src/ui/tui/tui-pane-pending.js +284 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +11 -7
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +29 -32
- package/dist/src/ui/tui/tui.js +3 -4
- package/dist/src/verdict/fix-claim.js +76 -54
- package/dist/src/verdict/refusal.js +1 -1
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
- /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
|
@@ -88,7 +88,7 @@ function applyTeamDay(day, fold) {
|
|
|
88
88
|
setLowerMedian(day, "reviewMedianMs", fold.reviewDurationsMs);
|
|
89
89
|
// Only set when nonzero, so a failure-free day keeps its exact pre-feature shape.
|
|
90
90
|
setMapIfNonempty(day, "failedRunsByRole", fold.failedRunsByRole);
|
|
91
|
-
setMapIfNonempty(day, "
|
|
91
|
+
setMapIfNonempty(day, "imageReroutesByRole", fold.imageReroutesByRole);
|
|
92
92
|
day.commits = fold.commits;
|
|
93
93
|
day.commitsByRole = fold.commitsByRole;
|
|
94
94
|
day.workCommits = fold.workCommits;
|
|
@@ -152,7 +152,7 @@ function applyRoleDay(day, fold, bases) {
|
|
|
152
152
|
setMapIfNonempty(day, "revisionsByRole", narrowToRole(fold.revisionsByRole, bases));
|
|
153
153
|
setMapIfNonempty(day, "refusalsByRole", narrowToRole(fold.refusalsByRole, bases));
|
|
154
154
|
setMapIfNonempty(day, "failedRunsByRole", narrowToRole(fold.failedRunsByRole, bases));
|
|
155
|
-
setMapIfNonempty(day, "
|
|
155
|
+
setMapIfNonempty(day, "imageReroutesByRole", narrowToRole(fold.imageReroutesByRole, bases));
|
|
156
156
|
day.commits = commits;
|
|
157
157
|
day.commitsByRole = narrowToRole(fold.commitsByRole, bases);
|
|
158
158
|
const tierCommits = (tier) => bases.filter((b) => commitTier(b) === tier).reduce((n, b) => n + roleSum(fold.commitsByRole, b), 0);
|
|
@@ -297,51 +297,67 @@ function applyBacklogLags(root, byDate, role) {
|
|
|
297
297
|
byDate.get(date).backlogLagMedianDays = lowerMedian(lags);
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
|
+
/** Fold one day's run, token, commit, cost and per-role commit counts into `acc.totals`. */
|
|
301
|
+
function foldRunAndCommitCounts(acc, d) {
|
|
302
|
+
acc.totals.tokensOut += d.tokensOut;
|
|
303
|
+
for (const n of Object.values(d.runsByRole))
|
|
304
|
+
acc.totals.runs += n;
|
|
305
|
+
acc.totals.commits += d.commits;
|
|
306
|
+
for (const [role, n] of Object.entries(d.commitsByRole ?? {}))
|
|
307
|
+
addTo(acc.totals.commitsByRole, role, n);
|
|
308
|
+
acc.totals.workCommits += d.workCommits ?? 0;
|
|
309
|
+
acc.totals.maintenanceCommits += d.maintenanceCommits ?? 0;
|
|
310
|
+
acc.totals.costUsd += d.costUsd;
|
|
311
|
+
}
|
|
312
|
+
/** Fold one day's notional spend and its per-backend attribution into `acc`. */
|
|
313
|
+
function foldNotionalSpend(acc, d) {
|
|
314
|
+
acc.notionalCostUsd += d.notionalCostUsd ?? 0;
|
|
315
|
+
for (const [backend, c] of Object.entries(d.notionalByBackend ?? {}))
|
|
316
|
+
addTo(acc.notionalByBackend, backend, c);
|
|
317
|
+
}
|
|
318
|
+
/** Fold one day's features-done, bugs-fixed, plan/bug filing and merge counts into `acc`. */
|
|
319
|
+
function foldOutcomeCounts(acc, d) {
|
|
320
|
+
acc.totals.featuresDone += d.featuresDone;
|
|
321
|
+
acc.totals.bugsFixed += d.bugsFixed;
|
|
322
|
+
acc.plansAdded += d.plansAdded ?? 0;
|
|
323
|
+
acc.bugsFiled += d.bugsFiled ?? 0;
|
|
324
|
+
acc.totals.mergeRuns += d.mergeRuns ?? 0;
|
|
325
|
+
acc.totals.mergeTokens += d.mergeTokens ?? 0;
|
|
326
|
+
acc.totals.mergeCostUsd += d.mergeCostUsd ?? 0;
|
|
327
|
+
acc.mergeFailed += d.mergeFailed ?? 0;
|
|
328
|
+
}
|
|
300
329
|
/** Reduce the filled day series to the report's totals, plus the nonzero-only fields the
|
|
301
330
|
* return literal folds into `ReportData.totals` (notional spend, merge failures, plans/bugs
|
|
302
|
-
* filings, backend attribution).
|
|
331
|
+
* filings, backend attribution). One named helper per accumulator phase folds each day:
|
|
332
|
+
* `foldRunAndCommitCounts` for the run/commit counts, `foldNotionalSpend` for the notional
|
|
333
|
+
* spend and its backend attribution, and `foldOutcomeCounts` for the outcome/merge counts. */
|
|
303
334
|
function foldSeriesTotals(series) {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
335
|
+
const acc = {
|
|
336
|
+
totals: {
|
|
337
|
+
tokensOut: 0,
|
|
338
|
+
runs: 0,
|
|
339
|
+
...zeroCommitCounts(),
|
|
340
|
+
costUsd: 0,
|
|
341
|
+
featuresDone: 0,
|
|
342
|
+
bugsFixed: 0,
|
|
343
|
+
...zeroMergeCounts(),
|
|
344
|
+
},
|
|
345
|
+
notionalCostUsd: 0,
|
|
346
|
+
mergeFailed: 0,
|
|
347
|
+
plansAdded: 0,
|
|
348
|
+
bugsFiled: 0,
|
|
349
|
+
notionalByBackend: {},
|
|
312
350
|
};
|
|
313
351
|
// Notional spend is kept out of the totals object until it is nonzero, so a pre-feature
|
|
314
352
|
// window's totals keep their exact shape. Merge failures likewise: a failure-free window
|
|
315
353
|
// carries no mergeFailed (zero means absent, like the merge-share line's other fields
|
|
316
354
|
// are folded in below).
|
|
317
|
-
let notionalCostUsd = 0;
|
|
318
|
-
let mergeFailed = 0;
|
|
319
|
-
let plansAdded = 0;
|
|
320
|
-
let bugsFiled = 0;
|
|
321
|
-
const notionalByBackend = {};
|
|
322
355
|
for (const d of series) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
totals.commits += d.commits;
|
|
327
|
-
for (const [role, n] of Object.entries(d.commitsByRole ?? {}))
|
|
328
|
-
addTo(totals.commitsByRole, role, n);
|
|
329
|
-
totals.workCommits += d.workCommits ?? 0;
|
|
330
|
-
totals.maintenanceCommits += d.maintenanceCommits ?? 0;
|
|
331
|
-
totals.costUsd += d.costUsd;
|
|
332
|
-
notionalCostUsd += d.notionalCostUsd ?? 0;
|
|
333
|
-
for (const [backend, c] of Object.entries(d.notionalByBackend ?? {}))
|
|
334
|
-
addTo(notionalByBackend, backend, c);
|
|
335
|
-
totals.featuresDone += d.featuresDone;
|
|
336
|
-
totals.bugsFixed += d.bugsFixed;
|
|
337
|
-
plansAdded += d.plansAdded ?? 0;
|
|
338
|
-
bugsFiled += d.bugsFiled ?? 0;
|
|
339
|
-
totals.mergeRuns += d.mergeRuns ?? 0;
|
|
340
|
-
totals.mergeTokens += d.mergeTokens ?? 0;
|
|
341
|
-
totals.mergeCostUsd += d.mergeCostUsd ?? 0;
|
|
342
|
-
mergeFailed += d.mergeFailed ?? 0;
|
|
356
|
+
foldRunAndCommitCounts(acc, d);
|
|
357
|
+
foldNotionalSpend(acc, d);
|
|
358
|
+
foldOutcomeCounts(acc, d);
|
|
343
359
|
}
|
|
344
|
-
return
|
|
360
|
+
return acc;
|
|
345
361
|
}
|
|
346
362
|
/** The shared coverage proof (event-window.ts), rebuilt from the memoized read's pinned
|
|
347
363
|
* parts: the day-keyed proof (fileCovers — a property of the retained bytes and fromKey,
|
|
@@ -14,7 +14,7 @@ import { eventDayKey, parseEventLine } from "../events/event-read.js";
|
|
|
14
14
|
import { readCompleteLines } from "../files/tail.js";
|
|
15
15
|
import { eventsLogPath } from "../paths.js";
|
|
16
16
|
import { isPassingMainCheck, isRedMain } from "../baseline/main-guardrails.js";
|
|
17
|
-
import { memoSetLru } from "../collections.js";
|
|
17
|
+
import { addTo, distinctOrdered, memoSetLru } from "../collections.js";
|
|
18
18
|
import { asList } from "../text/text.js";
|
|
19
19
|
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
20
20
|
import { foldUsageEvent } from "./report-usage-fold.js";
|
|
@@ -53,13 +53,20 @@ export function scopeBases(role) {
|
|
|
53
53
|
const ids = asList(role);
|
|
54
54
|
if (ids.length === 0)
|
|
55
55
|
return undefined;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
return distinctOrdered(ids.map((id) => baseRoleOf(id)));
|
|
57
|
+
}
|
|
58
|
+
/** Fold every key of a per-role-id count/cost map through its base role, so `feature-2` joins
|
|
59
|
+
* `feature` and the map is keyed by base role. The single home of the fold — the day
|
|
60
|
+
* renderer's cost-per-commit path folds each day's raw map and then folds it once (previously
|
|
61
|
+
* inline per entry), and the --since renderer folds an already-aggregated window map — so the
|
|
62
|
+
* two cannot drift on what an instance key joins into. Addition is associative and
|
|
63
|
+
* `baseRoleOf` is deterministic, so accumulating per-id then folding once (or per entry) is
|
|
64
|
+
* identical; the helper takes an already-aggregated map either way. */
|
|
65
|
+
export function foldBaseRoles(map) {
|
|
66
|
+
const folded = {};
|
|
67
|
+
for (const [id, c] of Object.entries(map))
|
|
68
|
+
addTo(folded, baseRoleOf(id), c);
|
|
69
|
+
return folded;
|
|
63
70
|
}
|
|
64
71
|
/** Sum a per-role-id count/cost map over the scoped base roles: instance keys (`feature-2`)
|
|
65
72
|
* fold into their base (`feature`), so a scoped report aggregates a whole role group exactly
|
|
@@ -123,7 +130,7 @@ function dayFoldFor(entry, dayKey) {
|
|
|
123
130
|
checkWaitsMs: [],
|
|
124
131
|
reviewDurationsMs: [],
|
|
125
132
|
failedRunsByRole: {},
|
|
126
|
-
|
|
133
|
+
imageReroutesByRole: {},
|
|
127
134
|
tokensByRole: {},
|
|
128
135
|
promptTokensByRole: {},
|
|
129
136
|
cacheReadTokensByRole: {},
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { addTo, rankCountEntries, sortByValueDesc } from "../collections.js";
|
|
2
|
+
import { foldBaseRoles } from "./report-fold.js";
|
|
2
3
|
import { compactTokens, usd } from "../text/format.js";
|
|
3
4
|
import { durationLabel } from "../cli/cli-args.js";
|
|
4
|
-
import {
|
|
5
|
+
import { monthDay, reportWindow } from "../text/datetime.js";
|
|
5
6
|
import { eventsRotationLabel } from "../events/events.js";
|
|
6
7
|
import { SPARSE_WINDOW_NOTE } from "../events/event-window.js";
|
|
7
8
|
import { shortSpanPhrase } from "../text/phrases.js";
|
|
8
9
|
import { markdownTable } from "../text/markdown.js";
|
|
9
|
-
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
10
10
|
import { backendDisplayName } from "../agent/backend-traits.js";
|
|
11
11
|
/** Bar width for one day: up to 20 blocks scaled to the window's max tokensOut —
|
|
12
12
|
* round(20·v/max), min 1 when v > 0. */
|
|
@@ -19,7 +19,7 @@ function barWidth(v, max) {
|
|
|
19
19
|
* collections.ts's rank rule, so the report cannot drift from the other counters that rank the
|
|
20
20
|
* same way.
|
|
21
21
|
* Callers filter/format the ranked pairs. */
|
|
22
|
-
function rankedRoleMap(totals) {
|
|
22
|
+
export function rankedRoleMap(totals) {
|
|
23
23
|
return rankCountEntries(Object.entries(totals));
|
|
24
24
|
}
|
|
25
25
|
/** Window totals per role: fold the per-day role maps (any `Record<string, number>` day field,
|
|
@@ -35,7 +35,7 @@ function rankedRoleTotals(series, pick) {
|
|
|
35
35
|
}
|
|
36
36
|
/** One "**Label:** role — n · role — n" Markdown line from already-ranked pairs; an empty
|
|
37
37
|
* pair list (an all-zero window) renders "-". fmt formats each role's number. */
|
|
38
|
-
function rankedRoleLine(label, pairs, fmt) {
|
|
38
|
+
export function rankedRoleLine(label, pairs, fmt) {
|
|
39
39
|
return `**${label}:** ${pairs.length === 0 ? "-" : pairs.map(([r, n]) => `${r} — ${fmt(n)}`).join(" · ")}`;
|
|
40
40
|
}
|
|
41
41
|
/** The "**Prompt tokens by role:**" line — each role's prompt-token total and, when its
|
|
@@ -43,7 +43,7 @@ function rankedRoleLine(label, pairs, fmt) {
|
|
|
43
43
|
* — with zero-total roles omitted (the same zero-means-absent rule the sibling lines apply)
|
|
44
44
|
* and "-" when no role sent a prompt. `prompt` arrives already ranked (prompt desc then
|
|
45
45
|
* name asc). The one home of the share format, so the day and `--since` lines cannot drift. */
|
|
46
|
-
function promptTokensLine(prompt, cache) {
|
|
46
|
+
export function promptTokensLine(prompt, cache) {
|
|
47
47
|
const cacheByRole = new Map(cache);
|
|
48
48
|
const cells = prompt
|
|
49
49
|
.filter(([, n]) => n > 0)
|
|
@@ -58,7 +58,7 @@ function promptTokensLine(prompt, cache) {
|
|
|
58
58
|
* unfiltered lines. fmt formats each role's number (default String for count lines; pass
|
|
59
59
|
* usd/compactTokens for spend/token lines). All the "X by role" lines share this filter-rule
|
|
60
60
|
* home so the zero-means-render-dash guard cannot drift between the --since and day renders. */
|
|
61
|
-
function pushRankedRoleLine(lines, label, pairs, fmt = String) {
|
|
61
|
+
export function pushRankedRoleLine(lines, label, pairs, fmt = String) {
|
|
62
62
|
lines.push(rankedRoleLine(label, pairs.filter(([, n]) => n > 0), fmt));
|
|
63
63
|
}
|
|
64
64
|
/** Push a "**Label:** …" ranked-role line only when some role carries a positive value — the
|
|
@@ -68,7 +68,7 @@ function pushRankedRoleLine(lines, label, pairs, fmt = String) {
|
|
|
68
68
|
* formats each role's number (default String for count lines). Shared by the since and day
|
|
69
69
|
* renders so the filter-and-if-present guard cannot drift between them — a field that flipped
|
|
70
70
|
* one render to always-print would show an absent "-" line in one report but not the other. */
|
|
71
|
-
function pushPresentRankedLine(lines, label, pairs, fmt = String) {
|
|
71
|
+
export function pushPresentRankedLine(lines, label, pairs, fmt = String) {
|
|
72
72
|
const present = pairs.filter(([, n]) => n > 0);
|
|
73
73
|
if (present.length > 0)
|
|
74
74
|
lines.push(rankedRoleLine(label, present, fmt));
|
|
@@ -102,7 +102,7 @@ function pushDayCameoLine(lines, series, title, pick, fmt = shortSpanPhrase) {
|
|
|
102
102
|
* renders. Zero-means-absent: a provider with no spend prints no line, and both lanes format
|
|
103
103
|
* with usd so the day and --since windows cannot drift (the --since path ranks via rankedRoleMap,
|
|
104
104
|
* the day path via rankedRoleTotals). */
|
|
105
|
-
function pushProviderCostLines(lines, rankCost, rankNotional) {
|
|
105
|
+
export function pushProviderCostLines(lines, rankCost, rankNotional) {
|
|
106
106
|
const providers = rankCost().filter(([, c]) => c > 0);
|
|
107
107
|
if (providers.length > 0)
|
|
108
108
|
lines.push(rankedRoleLine("Cost by provider", providers, usd));
|
|
@@ -171,7 +171,7 @@ const ROLE_SCOPE_NOTE = "role scope: merge runs and backlog tallies (features do
|
|
|
171
171
|
* nonzero (zero means absent, so an API-only or merge-free window renders as before), and the
|
|
172
172
|
* role-scope note when the report is scoped. One home so the day report and the --since window
|
|
173
173
|
* cannot drift on the order, the omission predicates, or the scope note. */
|
|
174
|
-
function pushReportSummary(lines, t, tail = "", scoped = false) {
|
|
174
|
+
export function pushReportSummary(lines, t, tail = "", scoped = false) {
|
|
175
175
|
lines.push(totalsLine(t, tail));
|
|
176
176
|
if (t.mergeRuns > 0)
|
|
177
177
|
lines.push(mergeShareLine(t));
|
|
@@ -180,188 +180,13 @@ function pushReportSummary(lines, t, tail = "", scoped = false) {
|
|
|
180
180
|
if (scoped)
|
|
181
181
|
lines.push(ROLE_SCOPE_NOTE);
|
|
182
182
|
}
|
|
183
|
-
/** Render a trailing-window totals report as Markdown — the aggregate sibling of the
|
|
184
|
-
* `logs --since` raw stream ("how much" here, "what exactly" there). Pure function of
|
|
185
|
-
* SinceReport: no I/O, no clock reads (the window line's local time is rendered from the
|
|
186
|
-
* collector's captured cutoff, so the printed window matches the aggregated one). */
|
|
187
|
-
export function renderSinceReportMarkdown(data) {
|
|
188
|
-
const lines = [];
|
|
189
|
-
pushSinceWindowHeader(lines, data);
|
|
190
|
-
pushSinceSummary(lines, data);
|
|
191
|
-
pushSinceRoleLines(lines, data);
|
|
192
|
-
pushSinceProviderModelLines(lines, data);
|
|
193
|
-
pushSinceMainRedLines(lines, data);
|
|
194
|
-
pushSinceFrictionLines(lines, data);
|
|
195
|
-
pushSinceDeferralLines(lines, data);
|
|
196
|
-
pushSinceCommitLines(lines, data);
|
|
197
|
-
pushSinceCostLines(lines, data);
|
|
198
|
-
pushSinceCoverageLines(lines, data);
|
|
199
|
-
return lines.join("\n");
|
|
200
|
-
}
|
|
201
|
-
/** The window header: the title, the bounded-or-since window line and the role scope line. */
|
|
202
|
-
function pushSinceWindowHeader(lines, data) {
|
|
203
|
-
lines.push("# tumwater usage report");
|
|
204
|
-
lines.push("");
|
|
205
|
-
lines.push(data.untilMs !== undefined && data.toIso !== undefined
|
|
206
|
-
? boundedWindowLine(durationLabel(data.sinceMs), durationLabel(data.untilMs), data.fromIso, data.toIso)
|
|
207
|
-
: sinceWindowLine(durationLabel(data.sinceMs), data.fromIso));
|
|
208
|
-
if (data.role !== undefined)
|
|
209
|
-
lines.push(`role: ${data.role}`);
|
|
210
|
-
lines.push("");
|
|
211
|
-
}
|
|
212
|
-
/** The shared totals voice (minus features/bugs/filing cells, stated at the tail), gated
|
|
213
|
-
* to the team report like the day sibling's. */
|
|
214
|
-
function pushSinceSummary(lines, data) {
|
|
215
|
-
const t = data.totals;
|
|
216
|
-
// Same totals voice as the day report, minus the features/bugs and filing cells: those tallies
|
|
217
|
-
// come from day-granular backlog-file dates that cannot subdivide a sub-day window, so the
|
|
218
|
-
// final line below states the omission instead of showing a day-rounded number.
|
|
219
|
-
pushReportSummary(lines, t, "", data.role !== undefined);
|
|
220
|
-
lines.push("");
|
|
221
|
-
}
|
|
222
|
-
/** Runs / tokens / prompt-tokens / cost breakdowns by role. */
|
|
223
|
-
function pushSinceRoleLines(lines, data) {
|
|
224
|
-
lines.push(rankedRoleLine("Runs by role", rankedRoleMap(data.runsByRole), String));
|
|
225
|
-
// Same breakdown for token spend: ranked by tokens desc then name asc, zero-token roles
|
|
226
|
-
// omitted — an all-zero window renders "-" like the other lines. Output tokens are the
|
|
227
|
-
// resource the budget charges, so this is the line an operator reads to find which role's
|
|
228
|
-
// prompt loop burned the window's tokens.
|
|
229
|
-
pushRankedRoleLine(lines, "Tokens by role", rankedRoleMap(data.tokensByRole), compactTokens);
|
|
230
|
-
// The prompt side of the same breakdown, straight after the output-token line: how many
|
|
231
|
-
// prompt tokens each role sent and the share of them its backend served from cache — the
|
|
232
|
-
// input side of the bill an operator cannot see from Tokens by role. Zero-prompt roles are
|
|
233
|
-
// omitted, and an all-zero window renders "-" like the sibling lines.
|
|
234
|
-
lines.push(promptTokensLine(rankedRoleMap(data.promptTokensByRole ?? {}), rankedRoleMap(data.cacheReadTokensByRole ?? {})));
|
|
235
|
-
// Same breakdown for spend: ranked by spend desc then name asc, zero-spend roles omitted —
|
|
236
|
-
// an all-zero window renders "-" like the runs line.
|
|
237
|
-
pushRankedRoleLine(lines, "Cost by role", rankedRoleMap(data.costByRole), usd);
|
|
238
|
-
}
|
|
239
|
-
/** The provider cost lines and the per-model line, gated to the team report. */
|
|
240
|
-
function pushSinceProviderModelLines(lines, data) {
|
|
241
|
-
// The same breakdown per PROVIDER (plans: per-provider spend), gated to the team report
|
|
242
|
-
// like its day sibling. SinceReport.costByProvider is always present (empty when none), so
|
|
243
|
-
// the zero-means-absent gate prints no line for a window with no provider spend.
|
|
244
|
-
if (data.role === undefined) {
|
|
245
|
-
pushProviderCostLines(lines, () => rankedRoleMap(data.costByProvider), () => rankedRoleMap(data.notionalByProvider));
|
|
246
|
-
// The same breakdown per MODEL (each run_start's agent-qualified selector — the model a
|
|
247
|
-
// team mixing providers, per-role overrides and fallbacks actually ran on), gated to the
|
|
248
|
-
// team report like the provider lines: a model is not a role, so a role-scoped window
|
|
249
|
-
// omits it. SinceReport.runsByModel is always present, so a window with no modelled
|
|
250
|
-
// start prints no line (keeping a model-less day's output unchanged), exactly as the
|
|
251
|
-
// provider lines stay absent when no provider spent.
|
|
252
|
-
pushPresentRankedLine(lines, "Runs by model", rankedRoleMap(data.runsByModel));
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
/** Main's red time and the test/merge culprits behind it, gated like the day sibling. */
|
|
256
|
-
function pushSinceMainRedLines(lines, data) {
|
|
257
|
-
// Main's red wall-clock over the window (main's own, not any role's — a role-scoped
|
|
258
|
-
// collector carries 0, so this never prints for a scoped window). Zero-absent: no line for
|
|
259
|
-
// a window main was never red.
|
|
260
|
-
if (data.mainRedMs > 0)
|
|
261
|
-
lines.push(`**Main red:** ${durationLabel(data.mainRedMs)}`);
|
|
262
|
-
// The same "what kept main red" detail named by failing test — which tests main's own red
|
|
263
|
-
// checks named over the window. Team-only like main's red time, so a role-scoped window
|
|
264
|
-
// omits it; zero-absent: no line when the window's red checks named no test.
|
|
265
|
-
if (data.role === undefined) {
|
|
266
|
-
pushPresentRankedLine(lines, "Main red by test", rankedRoleMap(data.mainRedByTest ?? {}));
|
|
267
|
-
// The same "what kept main red" detail named by merge — the landed merges the watch's
|
|
268
|
-
// own main_culprit events blamed, mirroring the failing-test line right above. Team-only
|
|
269
|
-
// like main's red time and zero-absent: no line when the window blamed no merge.
|
|
270
|
-
pushPresentRankedLine(lines, "Main red by merge", rankedRoleMap(data.mainRedByCulprit ?? {}));
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
/** The review-friction and failed-run breakdowns by role. */
|
|
274
|
-
function pushSinceFrictionLines(lines, data) {
|
|
275
|
-
// The same breakdown for review friction: rejections per role from review_rejected events;
|
|
276
|
-
// a window with none renders "-" like the other lines. The post-merge line is the
|
|
277
|
-
// afterMerge: true subset — rejections that reached an already-merged commit.
|
|
278
|
-
pushRankedRoleLine(lines, "Rejections by role", rankedRoleMap(data.rejectionsByRole));
|
|
279
|
-
pushRankedRoleLine(lines, "Post-merge rejections by role", rankedRoleMap(data.postMergeRejectionsByRole));
|
|
280
|
-
pushRankedRoleLine(lines, "Revisions by role", rankedRoleMap(data.revisionsByRole));
|
|
281
|
-
pushRankedRoleLine(lines, "Refusals by role", rankedRoleMap(data.refusalsByRole));
|
|
282
|
-
// The same breakdown for outright failures: error-class run_ends per role under the
|
|
283
|
-
// digest's OUTCOME_CLASS — the runs that burned agent time and merged nothing without
|
|
284
|
-
// going through review, which rejections/revisions would miss entirely. A window with
|
|
285
|
-
// none renders "-" like the other lines.
|
|
286
|
-
pushRankedRoleLine(lines, "Failed runs by role", rankedRoleMap(data.failedRunsByRole));
|
|
287
|
-
pushRankedRoleLine(lines, "Fallbacks by role", rankedRoleMap(data.fallbacksByRole));
|
|
288
|
-
}
|
|
289
|
-
/** Deferred maintenance runs by reason code, gated to the team report. */
|
|
290
|
-
function pushSinceDeferralLines(lines, data) {
|
|
291
|
-
// Deferred maintenance runs per reason code, the --since twin of the day render's line:
|
|
292
|
-
// how many due maintenance runs stood down and why (backlog / no-work / plans-full).
|
|
293
|
-
// Gated to the team report — a reason is not a role, so a role-scoped window omits it.
|
|
294
|
-
// Zero-means-absent: a window with no deferral prints no line.
|
|
295
|
-
if (data.role === undefined) {
|
|
296
|
-
pushPresentRankedLine(lines, "Deferred runs by reason", rankedRoleMap(data.deferredByReason ?? {}));
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
/** Commits by role and the merged/merge-failure split, gated to the team report. */
|
|
300
|
-
function pushSinceCommitLines(lines, data) {
|
|
301
|
-
// The merged-commits split by base role, matching the day render's unfiltered line: the
|
|
302
|
-
// window's totals.commits carries the count; this line says which role's commits it was.
|
|
303
|
-
lines.push(rankedRoleLine("Commits by role", rankedRoleMap(data.commitsByRole), String));
|
|
304
|
-
// The merged-outcomes split by base role, mirroring the day render's line: which roles'
|
|
305
|
-
// changes reach main and which bounce off the gate (merge_failed). Gated to the team
|
|
306
|
-
// report — a role-scoped window omits merge (ROLE_SCOPE_NOTE), so the per-role
|
|
307
|
-
// attribution would contradict it. Zero-means-absent: the failures line prints only when
|
|
308
|
-
// some role has one; a merge-free window renders `-` like the other lines.
|
|
309
|
-
if (data.role === undefined) {
|
|
310
|
-
lines.push(rankedRoleLine("Merged by role", rankedRoleMap(data.mergedByRole), String));
|
|
311
|
-
pushPresentRankedLine(lines, "Merge failures by role", rankedRoleMap(data.mergeFailedByRole));
|
|
312
|
-
// WHY the window's merges failed, from each merge_failed's result code — the day
|
|
313
|
-
// report's reason twin of the role line above, so an operator can tell a review
|
|
314
|
-
// rejection (fix the change) from a conflict/block (rebase) from a shutdown abort
|
|
315
|
-
// (capacity). Ranked the same way (count desc then code asc), reason-free only.
|
|
316
|
-
pushPresentRankedLine(lines, "Merge failures by reason", rankedRoleMap(data.mergeFailedByReason ?? {}));
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
/** Cost/estimated per commit and the notional Estimated by role lines. */
|
|
320
|
-
function pushSinceCostLines(lines, data) {
|
|
321
|
-
// Spend per merged commit, the --since twin of the day render's `Cost per commit:` line:
|
|
322
|
-
// costByRole is keyed by role id (instances not folded, so `feature-2` joins `feature`),
|
|
323
|
-
// commitsByRole is already base-role keyed. Prints for a spend-free window as `-` and for a
|
|
324
|
-
// role-scoped window from its scoped maps, matching the day render.
|
|
325
|
-
lines.push(costPerCommitLine("Cost per commit", costPerCommitPairsFrom(foldBaseRoles(data.costByRole), data.commitsByRole)));
|
|
326
|
-
// Subscription spend is notional: its own line, only when the window has any — the same
|
|
327
|
-
// conditional/filtered `usd` form the day render's "Estimated by role" uses.
|
|
328
|
-
if (data.notionalByRole !== undefined) {
|
|
329
|
-
pushRankedRoleLine(lines, "Estimated by role", rankedRoleMap(data.notionalByRole), usd);
|
|
330
|
-
// The day render's `Estimated per commit:` twin, after `Estimated by role`, from the same
|
|
331
|
-
// notionalByRole source folded to base roles and the base-keyed commitsByRole.
|
|
332
|
-
lines.push(costPerCommitLine("Estimated per commit", costPerCommitPairsFrom(foldBaseRoles(data.notionalByRole), data.commitsByRole)));
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
/** The trailing blank and the exactly-one coverage/backlog-omission note. */
|
|
336
|
-
function pushSinceCoverageLines(lines, data) {
|
|
337
|
-
lines.push("");
|
|
338
|
-
// Exactly one final line, chosen by the collector's coverage proof: either a hedged note
|
|
339
|
-
// that stays true whenever it prints (the oldest retained event lies inside the window —
|
|
340
|
-
// whether from rotation or a log born inside the window), or the one-time statement of the
|
|
341
|
-
// backlog-tally omission. A flat "rotated out" claim would be false for a log that never
|
|
342
|
-
// had the older events, so the wording names only what is provably the case. A role-scoped
|
|
343
|
-
// report already states the omission in ROLE_SCOPE_NOTE, so it does not repeat the --days
|
|
344
|
-
// pointer, which would imply a scoped day report shows the tallies it too omits.
|
|
345
|
-
if (!data.coversFullWindow)
|
|
346
|
-
lines.push(SPARSE_WINDOW_NOTE);
|
|
347
|
-
else if (data.role === undefined)
|
|
348
|
-
lines.push("backlog tallies (features done / bugs fixed / plans added / bugs filed) need the day report (--days)");
|
|
349
|
-
}
|
|
350
|
-
/** Fold every key of a per-role-id map through its base role, so `feature-2` joins `feature`.
|
|
351
|
-
* The same fold the day caller does per day, applied to an already-aggregated window map. */
|
|
352
|
-
function foldBaseRoles(map) {
|
|
353
|
-
const folded = {};
|
|
354
|
-
for (const [id, c] of Object.entries(map))
|
|
355
|
-
addTo(folded, baseRoleOf(id), c);
|
|
356
|
-
return folded;
|
|
357
|
-
}
|
|
358
183
|
/** Rank one cost map against a base-keyed commits map into cost-per-commit output: the shared
|
|
359
184
|
* core of the day and --since cost-per-commit lines. `cost` is expected base-role keyed (the
|
|
360
185
|
* day caller folds per day, the --since caller via `foldBaseRoles`); commits are base-role
|
|
361
186
|
* keyed. `merged` is [role, cost per commit, commits] for roles with both, ranked by cost per
|
|
362
187
|
* commit desc then name asc; `idle` is roles that spent and merged nothing, ranked by spend.
|
|
363
188
|
* A role with commits but no spend appears in neither list. */
|
|
364
|
-
function costPerCommitPairsFrom(cost, commits) {
|
|
189
|
+
export function costPerCommitPairsFrom(cost, commits) {
|
|
365
190
|
const mergeDone = [];
|
|
366
191
|
const idleCost = [];
|
|
367
192
|
for (const [role, c] of Object.entries(cost)) {
|
|
@@ -376,24 +201,26 @@ function costPerCommitPairsFrom(cost, commits) {
|
|
|
376
201
|
sortByValueDesc(mergeDone);
|
|
377
202
|
return { mergeDone, idle: rankCountEntries(idleCost) };
|
|
378
203
|
}
|
|
379
|
-
/** The day caller of the cost-per-commit core:
|
|
380
|
-
* its base role (so `feature-2` joins
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
204
|
+
/** The day caller of the cost-per-commit core: aggregate each day's role-id-keyed spend map
|
|
205
|
+
* under its raw role id, then fold the whole map through its base role (so `feature-2` joins
|
|
206
|
+
* `feature`) once via the shared foldBaseRoles — addition is associative and the fold is
|
|
207
|
+
* deterministic, so this is the same total the --since caller's single fold reaches — and
|
|
208
|
+
* aggregate the base-keyed commitsByRole, then hand both to `costPerCommitPairsFrom`. `pick`
|
|
209
|
+
* selects the per-role-id spend map; merge spend (the reviewer's) and notional subscription
|
|
210
|
+
* spend stay out by default, matching `**Cost by role:**`. */
|
|
384
211
|
function costPerCommitPairs(series, pick = (d) => d.costByRole) {
|
|
385
212
|
const cost = {};
|
|
386
213
|
const commits = {};
|
|
387
214
|
for (const d of series) {
|
|
388
215
|
for (const [id, c] of Object.entries(pick(d)))
|
|
389
|
-
addTo(cost,
|
|
216
|
+
addTo(cost, id, c);
|
|
390
217
|
for (const [role, n] of Object.entries(d.commitsByRole ?? {}))
|
|
391
218
|
addTo(commits, role, n);
|
|
392
219
|
}
|
|
393
|
-
return costPerCommitPairsFrom(cost, commits);
|
|
220
|
+
return costPerCommitPairsFrom(foldBaseRoles(cost), commits);
|
|
394
221
|
}
|
|
395
222
|
/** One `**<label>:** …` line from costPerCommitPairs output; `-` when there is nothing. */
|
|
396
|
-
function costPerCommitLine(label, pairs) {
|
|
223
|
+
export function costPerCommitLine(label, pairs) {
|
|
397
224
|
const parts = [
|
|
398
225
|
...pairs.mergeDone.map(([r, per, n]) => `${r} — ${usd(per)} (${n})`),
|
|
399
226
|
...pairs.idle.map(([r, c]) => `${r} — ${usd(c)}, no commits`),
|
|
@@ -481,7 +308,7 @@ function pushReportFrictionLines(lines, data) {
|
|
|
481
308
|
// through review, which rejections/revisions would miss entirely. Zero-means-absent, so a
|
|
482
309
|
// failure-free window renders no line and an all-zero window renders "-" like the others.
|
|
483
310
|
pushRankedRoleLine(lines, "Failed runs by role", rankedRoleTotals(data.series, (d) => d.failedRunsByRole ?? {}));
|
|
484
|
-
pushRankedRoleLine(lines, "
|
|
311
|
+
pushRankedRoleLine(lines, "Image reroutes by role", rankedRoleTotals(data.series, (d) => d.imageReroutesByRole ?? {}));
|
|
485
312
|
}
|
|
486
313
|
/** The team-only deferred-runs-by-reason line. */
|
|
487
314
|
function pushReportDeferralLines(lines, data) {
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* for different reasons. The event-log folding both collectors share lives in
|
|
8
8
|
* src/report/report-usage-fold.ts (foldUsageEvent) and src/report/report-fold.ts (the windowed
|
|
9
9
|
* cache and scope helpers), the shapes they produce in src/report/report-types.ts, and the
|
|
10
|
-
* Markdown rendering in src/report/report-render.ts
|
|
10
|
+
* Markdown rendering in src/report/report-since-render.ts (which imports the shared formatting
|
|
11
|
+
* voice from src/report/report-render.ts). */
|
|
11
12
|
import { assertReportSinceMs, eventWindowCovers, readWindowEvents, sinceWindowTiming } from "../events/event-window.js";
|
|
12
13
|
import { dayKey } from "../text/datetime.js";
|
|
13
14
|
import { addTo } from "../collections.js";
|
|
@@ -80,7 +81,7 @@ function foldSinceWindow(root, cutoff, now, end, untilMs) {
|
|
|
80
81
|
checkWaitsMs: [],
|
|
81
82
|
reviewDurationsMs: [],
|
|
82
83
|
failedRunsByRole: {},
|
|
83
|
-
|
|
84
|
+
imageReroutesByRole: {},
|
|
84
85
|
tokensByRole: {},
|
|
85
86
|
promptTokensByRole: {},
|
|
86
87
|
cacheReadTokensByRole: {},
|
|
@@ -190,7 +191,7 @@ function sinceRoleMaps(acc, bases) {
|
|
|
190
191
|
revisionsByRole: scopedByRole(acc.revisionsByRole, bases),
|
|
191
192
|
refusalsByRole: scopedByRole(acc.refusalsByRole, bases),
|
|
192
193
|
failedRunsByRole: scopedByRole(acc.failedRunsByRole, bases),
|
|
193
|
-
|
|
194
|
+
imageReroutesByRole: scopedByRole(acc.imageReroutesByRole, bases),
|
|
194
195
|
tokensByRole: scopedByRole(acc.tokensByRole, bases),
|
|
195
196
|
...nonemptyObj("promptTokensByRole", scopedByRole(acc.promptTokensByRole, bases)),
|
|
196
197
|
...nonemptyObj("cacheReadTokensByRole", scopedByRole(acc.cacheReadTokensByRole, bases)),
|