tumwater 0.4.1 → 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 +3 -5
- 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} +35 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +70 -37
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +24 -24
- package/dist/src/agent/{run-stream.js → agent-stream.js} +6 -14
- package/dist/src/agent/backend-traits.js +16 -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 +131 -78
- 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 +120 -69
- package/dist/src/agent/codex/codex-stream.js +72 -39
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +21 -33
- 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 +52 -29
- 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 +51 -29
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/subscription-account.js +3 -3
- package/dist/src/backlog/backlog-archive.js +64 -165
- 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 +84 -136
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +2 -2
- package/dist/src/backlog/backlog-md.js +42 -31
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +30 -14
- package/dist/src/backlog/backlog-release.js +15 -23
- package/dist/src/backlog/backlog-structure.js +128 -62
- package/dist/src/backlog/backlog-write.js +1 -1
- package/dist/src/backlog/backlog.js +5 -4
- package/dist/src/baseline/main-baseline.js +178 -104
- package/dist/src/baseline/main-guardrails.js +54 -17
- package/dist/src/baseline/main-watch.js +1 -1
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +24 -65
- package/dist/src/budget/budget.js +2 -1
- 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 +103 -50
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-command-args.js +275 -137
- package/dist/src/cli/cli-flag-specs.js +43 -26
- 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-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +27 -50
- package/dist/src/cli/cli-start.js +150 -67
- package/dist/src/cli/completion.js +82 -46
- package/dist/src/cli/config-commands.js +82 -71
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +6 -9
- package/dist/src/cli/log-commands.js +46 -24
- package/dist/src/cli/notify-command.js +2 -2
- package/dist/src/cli/question-commands.js +86 -60
- package/dist/src/cli.js +183 -141
- package/dist/src/collections.js +52 -23
- 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 +79 -103
- 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 +161 -153
- 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 +4 -34
- package/dist/src/config/config-write.js +22 -46
- package/dist/src/config/config.js +4 -5
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -121
- package/dist/src/doctor/doctor-backlog-plans.js +27 -27
- package/dist/src/doctor/doctor-backlog.js +29 -41
- package/dist/src/doctor/doctor-check-names.js +4 -7
- package/dist/src/doctor/doctor-checks.js +5 -5
- package/dist/src/doctor/doctor-claude-checks.js +46 -30
- 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 +19 -56
- 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 +181 -273
- package/dist/src/doctor/doctor-notify.js +4 -7
- package/dist/src/doctor/doctor-opencode-checks.js +90 -52
- 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/{probe.js → doctor-probe.js} +1 -1
- package/dist/src/doctor/doctor-project-checks.js +69 -20
- package/dist/src/doctor/doctor-role-notes.js +7 -25
- package/dist/src/doctor/doctor.js +12 -15
- package/dist/src/events/event-format.js +19 -53
- package/dist/src/events/event-labels.js +0 -5
- package/dist/src/events/event-legacy.js +0 -2
- package/dist/src/events/{notify.js → event-notify.js} +2 -2
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- 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 +177 -92
- 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 +21 -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} +34 -34
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/slot-salvage.js +5 -4
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +5 -6
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +62 -48
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +22 -35
- package/dist/src/gui/gui-prompt-commands.js +48 -42
- package/dist/src/gui/gui-report-endpoints.js +3 -2
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +138 -85
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/prompt-commands.js +82 -60
- package/dist/src/init/init.js +156 -55
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-core.js +2 -2
- 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 +11 -14
- 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 +143 -90
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/operator/{retire.js → operator-retire.js} +2 -1
- 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/retention.js +6 -5
- 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 -13
- 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 +29 -3
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +87 -51
- 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 +31 -8
- package/dist/src/process/run-marker.js +31 -13
- package/dist/src/process/supervisor.js +57 -29
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +11 -11
- package/dist/src/prompt/notes-backlog.js +73 -57
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt.js +92 -43
- package/dist/src/report/report-data.js +61 -260
- package/dist/src/report/report-fold.js +34 -302
- package/dist/src/report/report-main-red.js +45 -42
- package/dist/src/report/report-render.js +37 -183
- package/dist/src/report/report-since-data.js +219 -0
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +48 -23
- package/dist/src/review/known-flakes.js +3 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +10 -10
- package/dist/src/review/quarantine-render.js +2 -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 +28 -32
- package/dist/src/roles/role-guidance.js +25 -46
- package/dist/src/roles/role-render.js +1 -14
- package/dist/src/roles/role-view.js +10 -23
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +12 -6
- package/dist/src/run/run-detail-data.js +137 -68
- package/dist/src/run/run-detail-render.js +97 -34
- package/dist/src/run/run-detail.js +106 -53
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-prompt.js +150 -93
- package/dist/src/run/run-resume.js +85 -37
- package/dist/src/run/run-stage.js +139 -105
- package/dist/src/run/run-timing.js +26 -6
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +71 -67
- package/dist/src/run/stage-backlog-checks.js +2 -29
- package/dist/src/run/stage-check.js +52 -43
- 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 +2 -2
- 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 +23 -38
- package/dist/src/runner/runner-state-load.js +2 -9
- package/dist/src/runner/runner.js +21 -36
- package/dist/src/scheduling/claims.js +3 -14
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-data.js +7 -11
- package/dist/src/status/status-role-notes.js +20 -16
- package/dist/src/status/status-roles.js +11 -52
- 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/datetime.js +8 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +20 -15
- package/dist/src/text/text.js +15 -3
- package/dist/src/ui/alert-feeds.js +52 -281
- package/dist/src/ui/badges.js +3 -14
- package/dist/src/ui/gui/gui-client-boot.js +21 -6
- 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 +2 -2
- 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 +12 -10
- package/dist/src/ui/gui/gui-client-report.js +1 -2
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -107
- package/dist/src/ui/gui/gui-client-team.js +30 -4
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/gui/gui-icons.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 +4 -26
- package/dist/src/ui/status-payload.js +4 -8
- package/dist/src/ui/status-render.js +6 -10
- package/dist/src/ui/team-alert-backlog.js +29 -60
- package/dist/src/ui/team-alert-shared.js +23 -10
- package/dist/src/ui/team-alert-status.js +17 -173
- package/dist/src/ui/team-alert-tail.js +19 -228
- package/dist/src/ui/team-alerts.js +12 -24
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-editor-answer.js +66 -54
- package/dist/src/ui/tui/tui-editor-attach.js +77 -65
- package/dist/src/ui/tui/tui-editor-backends.js +65 -58
- package/dist/src/ui/tui/tui-editor-brief.js +64 -57
- package/dist/src/ui/tui/tui-editor-budget.js +109 -96
- package/dist/src/ui/tui/tui-editor-config.js +81 -71
- package/dist/src/ui/tui/tui-editor-entry.js +167 -157
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-pause.js +208 -188
- package/dist/src/ui/tui/tui-editor-prompt.js +50 -44
- package/dist/src/ui/tui/tui-editor-queued.js +74 -67
- package/dist/src/ui/tui/tui-editor-quiet.js +124 -110
- package/dist/src/ui/tui/tui-editor-release.js +53 -44
- package/dist/src/ui/tui/tui-editor-retire.js +60 -49
- package/dist/src/ui/tui/tui-editor-role-enabled.js +93 -82
- package/dist/src/ui/tui/tui-editor-role-file.js +61 -49
- package/dist/src/ui/tui/tui-editor-role-key.js +52 -44
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +73 -60
- 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 +2 -2
- package/dist/src/ui/tui/tui-pane-activity.js +65 -53
- package/dist/src/ui/tui/tui-pane-backlog.js +78 -40
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +254 -251
- package/dist/src/ui/tui/tui-pane-history.js +81 -94
- 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 +76 -58
- package/dist/src/ui/tui/tui-pane-session.js +14 -9
- package/dist/src/ui/tui/tui-pane-transcript.js +152 -141
- package/dist/src/ui/tui/tui-pane.js +205 -175
- package/dist/src/ui/tui/tui-panes.js +31 -35
- package/dist/src/ui/tui/tui.js +4 -5
- package/dist/src/verdict/fix-claim.js +87 -53
- package/dist/src/verdict/refusal.js +31 -7
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/doctor/image-probe.js +0 -257
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/team-alert-backlog-feed.js +0 -181
- package/dist/src/ui/team-alert-merge.js +0 -183
- 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/gates/{disk-gate.js → disk-gates.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
|
@@ -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,9 +58,21 @@ 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
|
+
/** Push a "**Label:** …" ranked-role line only when some role carries a positive value — the
|
|
65
|
+
* zero-means-absent idiom: a window or day with no evidence prints no line at all, unlike
|
|
66
|
+
* pushRankedRoleLine's always-printed "-", because "no model ran" is an absence, not a zero
|
|
67
|
+
* (the same reason pushProviderCostLines and pushDayCameoLine stay silent when empty). fmt
|
|
68
|
+
* formats each role's number (default String for count lines). Shared by the since and day
|
|
69
|
+
* renders so the filter-and-if-present guard cannot drift between them — a field that flipped
|
|
70
|
+
* one render to always-print would show an absent "-" line in one report but not the other. */
|
|
71
|
+
export function pushPresentRankedLine(lines, label, pairs, fmt = String) {
|
|
72
|
+
const present = pairs.filter(([, n]) => n > 0);
|
|
73
|
+
if (present.length > 0)
|
|
74
|
+
lines.push(rankedRoleLine(label, present, fmt));
|
|
75
|
+
}
|
|
64
76
|
/** The per-day cameo line of the team day report — "MMM D <cell>" cells for each day that
|
|
65
77
|
* carries a value, joined under a bold title, or undefined when no day has one (zero means
|
|
66
78
|
* absent). The one home of that cameo rendering, shared by the per-day medians, the red-check
|
|
@@ -90,7 +102,7 @@ function pushDayCameoLine(lines, series, title, pick, fmt = shortSpanPhrase) {
|
|
|
90
102
|
* renders. Zero-means-absent: a provider with no spend prints no line, and both lanes format
|
|
91
103
|
* with usd so the day and --since windows cannot drift (the --since path ranks via rankedRoleMap,
|
|
92
104
|
* the day path via rankedRoleTotals). */
|
|
93
|
-
function pushProviderCostLines(lines, rankCost, rankNotional) {
|
|
105
|
+
export function pushProviderCostLines(lines, rankCost, rankNotional) {
|
|
94
106
|
const providers = rankCost().filter(([, c]) => c > 0);
|
|
95
107
|
if (providers.length > 0)
|
|
96
108
|
lines.push(rankedRoleLine("Cost by provider", providers, usd));
|
|
@@ -159,7 +171,7 @@ const ROLE_SCOPE_NOTE = "role scope: merge runs and backlog tallies (features do
|
|
|
159
171
|
* nonzero (zero means absent, so an API-only or merge-free window renders as before), and the
|
|
160
172
|
* role-scope note when the report is scoped. One home so the day report and the --since window
|
|
161
173
|
* cannot drift on the order, the omission predicates, or the scope note. */
|
|
162
|
-
function pushReportSummary(lines, t, tail = "", scoped = false) {
|
|
174
|
+
export function pushReportSummary(lines, t, tail = "", scoped = false) {
|
|
163
175
|
lines.push(totalsLine(t, tail));
|
|
164
176
|
if (t.mergeRuns > 0)
|
|
165
177
|
lines.push(mergeShareLine(t));
|
|
@@ -168,160 +180,13 @@ function pushReportSummary(lines, t, tail = "", scoped = false) {
|
|
|
168
180
|
if (scoped)
|
|
169
181
|
lines.push(ROLE_SCOPE_NOTE);
|
|
170
182
|
}
|
|
171
|
-
/** Render a trailing-window totals report as Markdown — the aggregate sibling of the
|
|
172
|
-
* `logs --since` raw stream ("how much" here, "what exactly" there). Pure function of
|
|
173
|
-
* SinceReport: no I/O, no clock reads (the window line's local time is rendered from the
|
|
174
|
-
* collector's captured cutoff, so the printed window matches the aggregated one). */
|
|
175
|
-
export function renderSinceReportMarkdown(data) {
|
|
176
|
-
const lines = [];
|
|
177
|
-
lines.push("# tumwater usage report");
|
|
178
|
-
lines.push("");
|
|
179
|
-
lines.push(data.untilMs !== undefined && data.toIso !== undefined
|
|
180
|
-
? boundedWindowLine(durationLabel(data.sinceMs), durationLabel(data.untilMs), data.fromIso, data.toIso)
|
|
181
|
-
: sinceWindowLine(durationLabel(data.sinceMs), data.fromIso));
|
|
182
|
-
if (data.role !== undefined)
|
|
183
|
-
lines.push(`role: ${data.role}`);
|
|
184
|
-
lines.push("");
|
|
185
|
-
const t = data.totals;
|
|
186
|
-
// Same totals voice as the day report, minus the features/bugs and filing cells: those tallies
|
|
187
|
-
// come from day-granular backlog-file dates that cannot subdivide a sub-day window, so the
|
|
188
|
-
// final line below states the omission instead of showing a day-rounded number.
|
|
189
|
-
pushReportSummary(lines, t, "", data.role !== undefined);
|
|
190
|
-
lines.push("");
|
|
191
|
-
lines.push(rankedRoleLine("Runs by role", rankedRoleMap(data.runsByRole), String));
|
|
192
|
-
// Same breakdown for token spend: ranked by tokens desc then name asc, zero-token roles
|
|
193
|
-
// omitted — an all-zero window renders "-" like the other lines. Output tokens are the
|
|
194
|
-
// resource the budget charges, so this is the line an operator reads to find which role's
|
|
195
|
-
// prompt loop burned the window's tokens.
|
|
196
|
-
pushRankedRoleLine(lines, "Tokens by role", rankedRoleMap(data.tokensByRole), compactTokens);
|
|
197
|
-
// The prompt side of the same breakdown, straight after the output-token line: how many
|
|
198
|
-
// prompt tokens each role sent and the share of them its backend served from cache — the
|
|
199
|
-
// input side of the bill an operator cannot see from Tokens by role. Zero-prompt roles are
|
|
200
|
-
// omitted, and an all-zero window renders "-" like the sibling lines.
|
|
201
|
-
lines.push(promptTokensLine(rankedRoleMap(data.promptTokensByRole ?? {}), rankedRoleMap(data.cacheReadTokensByRole ?? {})));
|
|
202
|
-
// Same breakdown for spend: ranked by spend desc then name asc, zero-spend roles omitted —
|
|
203
|
-
// an all-zero window renders "-" like the runs line.
|
|
204
|
-
pushRankedRoleLine(lines, "Cost by role", rankedRoleMap(data.costByRole), usd);
|
|
205
|
-
// The same breakdown per PROVIDER (plans: per-provider spend), gated to the team report
|
|
206
|
-
// like its day sibling. SinceReport.costByProvider is always present (empty when none), so
|
|
207
|
-
// the zero-means-absent gate prints no line for a window with no provider spend.
|
|
208
|
-
if (data.role === undefined) {
|
|
209
|
-
pushProviderCostLines(lines, () => rankedRoleMap(data.costByProvider), () => rankedRoleMap(data.notionalByProvider));
|
|
210
|
-
// The same breakdown per MODEL (each run_start's agent-qualified selector — the model a
|
|
211
|
-
// team mixing providers, per-role overrides and fallbacks actually ran on), gated to the
|
|
212
|
-
// team report like the provider lines: a model is not a role, so a role-scoped window
|
|
213
|
-
// omits it. SinceReport.runsByModel is always present, so a window with no modelled
|
|
214
|
-
// start prints no line (keeping a model-less day's output unchanged), exactly as the
|
|
215
|
-
// provider lines stay absent when no provider spent.
|
|
216
|
-
const modelPairs = rankedRoleMap(data.runsByModel).filter(([, n]) => n > 0);
|
|
217
|
-
if (modelPairs.length > 0)
|
|
218
|
-
lines.push(rankedRoleLine("Runs by model", modelPairs, String));
|
|
219
|
-
}
|
|
220
|
-
// Main's red wall-clock over the window (main's own, not any role's — a role-scoped
|
|
221
|
-
// collector carries 0, so this never prints for a scoped window). Zero-absent: no line for
|
|
222
|
-
// a window main was never red.
|
|
223
|
-
if (data.mainRedMs > 0)
|
|
224
|
-
lines.push(`**Main red:** ${durationLabel(data.mainRedMs)}`);
|
|
225
|
-
// The same "what kept main red" detail named by failing test — which tests main's own red
|
|
226
|
-
// checks named over the window. Team-only like main's red time, so a role-scoped window
|
|
227
|
-
// omits it; zero-absent: no line when the window's red checks named no test.
|
|
228
|
-
if (data.role === undefined) {
|
|
229
|
-
const mainRedByTest = rankedRoleMap(data.mainRedByTest ?? {}).filter(([, n]) => n > 0);
|
|
230
|
-
if (mainRedByTest.length > 0)
|
|
231
|
-
lines.push(rankedRoleLine("Main red by test", mainRedByTest, String));
|
|
232
|
-
// The same "what kept main red" detail named by merge — the landed merges the watch's
|
|
233
|
-
// own main_culprit events blamed, mirroring the failing-test line right above. Team-only
|
|
234
|
-
// like main's red time and zero-absent: no line when the window blamed no merge.
|
|
235
|
-
const mainRedByCulprit = rankedRoleMap(data.mainRedByCulprit ?? {}).filter(([, n]) => n > 0);
|
|
236
|
-
if (mainRedByCulprit.length > 0)
|
|
237
|
-
lines.push(rankedRoleLine("Main red by merge", mainRedByCulprit, String));
|
|
238
|
-
}
|
|
239
|
-
// The same breakdown for review friction: rejections per role from review_rejected events;
|
|
240
|
-
// a window with none renders "-" like the other lines. The post-merge line is the
|
|
241
|
-
// afterMerge: true subset — rejections that reached an already-merged commit.
|
|
242
|
-
pushRankedRoleLine(lines, "Rejections by role", rankedRoleMap(data.rejectionsByRole));
|
|
243
|
-
pushRankedRoleLine(lines, "Post-merge rejections by role", rankedRoleMap(data.postMergeRejectionsByRole));
|
|
244
|
-
pushRankedRoleLine(lines, "Revisions by role", rankedRoleMap(data.revisionsByRole));
|
|
245
|
-
pushRankedRoleLine(lines, "Refusals by role", rankedRoleMap(data.refusalsByRole));
|
|
246
|
-
// The same breakdown for outright failures: error-class run_ends per role under the
|
|
247
|
-
// digest's OUTCOME_CLASS — the runs that burned agent time and merged nothing without
|
|
248
|
-
// going through review, which rejections/revisions would miss entirely. A window with
|
|
249
|
-
// none renders "-" like the other lines.
|
|
250
|
-
pushRankedRoleLine(lines, "Failed runs by role", rankedRoleMap(data.failedRunsByRole));
|
|
251
|
-
pushRankedRoleLine(lines, "Fallbacks by role", rankedRoleMap(data.fallbacksByRole));
|
|
252
|
-
pushRankedRoleLine(lines, "Image reroutes by role", rankedRoleMap(data.imageReroutesByRole));
|
|
253
|
-
// Deferred maintenance runs per reason code, the --since twin of the day render's line:
|
|
254
|
-
// how many due maintenance runs stood down and why (backlog / no-work / plans-full).
|
|
255
|
-
// Gated to the team report — a reason is not a role, so a role-scoped window omits it.
|
|
256
|
-
// Zero-means-absent: a window with no deferral prints no line.
|
|
257
|
-
if (data.role === undefined) {
|
|
258
|
-
const deferred = rankedRoleMap(data.deferredByReason ?? {}).filter(([, n]) => n > 0);
|
|
259
|
-
if (deferred.length > 0)
|
|
260
|
-
lines.push(rankedRoleLine("Deferred runs by reason", deferred, String));
|
|
261
|
-
}
|
|
262
|
-
// The merged-commits split by base role, matching the day render's unfiltered line: the
|
|
263
|
-
// window's totals.commits carries the count; this line says which role's commits it was.
|
|
264
|
-
lines.push(rankedRoleLine("Commits by role", rankedRoleMap(data.commitsByRole), String));
|
|
265
|
-
// The merged-outcomes split by base role, mirroring the day render's line: which roles'
|
|
266
|
-
// changes reach main and which bounce off the gate (merge_failed). Gated to the team
|
|
267
|
-
// report — a role-scoped window omits merge (ROLE_SCOPE_NOTE), so the per-role
|
|
268
|
-
// attribution would contradict it. Zero-means-absent: the failures line prints only when
|
|
269
|
-
// some role has one; a merge-free window renders `-` like the other lines.
|
|
270
|
-
if (data.role === undefined) {
|
|
271
|
-
lines.push(rankedRoleLine("Merged by role", rankedRoleMap(data.mergedByRole), String));
|
|
272
|
-
const mergeFailures = rankedRoleMap(data.mergeFailedByRole).filter(([, n]) => n > 0);
|
|
273
|
-
if (mergeFailures.length > 0)
|
|
274
|
-
lines.push(rankedRoleLine("Merge failures by role", mergeFailures, String));
|
|
275
|
-
// WHY the window's merges failed, from each merge_failed's result code — the day
|
|
276
|
-
// report's reason twin of the role line above, so an operator can tell a review
|
|
277
|
-
// rejection (fix the change) from a conflict/block (rebase) from a shutdown abort
|
|
278
|
-
// (capacity). Ranked the same way (count desc then code asc), reason-free only.
|
|
279
|
-
const mergeFailureReasons = rankedRoleMap(data.mergeFailedByReason ?? {}).filter(([, n]) => n > 0);
|
|
280
|
-
if (mergeFailureReasons.length > 0)
|
|
281
|
-
lines.push(rankedRoleLine("Merge failures by reason", mergeFailureReasons, String));
|
|
282
|
-
}
|
|
283
|
-
// Spend per merged commit, the --since twin of the day render's `Cost per commit:` line:
|
|
284
|
-
// costByRole is keyed by role id (instances not folded, so `feature-2` joins `feature`),
|
|
285
|
-
// commitsByRole is already base-role keyed. Prints for a spend-free window as `-` and for a
|
|
286
|
-
// role-scoped window from its scoped maps, matching the day render.
|
|
287
|
-
lines.push(costPerCommitLine("Cost per commit", costPerCommitPairsFrom(foldBaseRoles(data.costByRole), data.commitsByRole)));
|
|
288
|
-
// Subscription spend is notional: its own line, only when the window has any — the same
|
|
289
|
-
// conditional/filtered `usd` form the day render's "Estimated by role" uses.
|
|
290
|
-
if (data.notionalByRole !== undefined) {
|
|
291
|
-
pushRankedRoleLine(lines, "Estimated by role", rankedRoleMap(data.notionalByRole), usd);
|
|
292
|
-
// The day render's `Estimated per commit:` twin, after `Estimated by role`, from the same
|
|
293
|
-
// notionalByRole source folded to base roles and the base-keyed commitsByRole.
|
|
294
|
-
lines.push(costPerCommitLine("Estimated per commit", costPerCommitPairsFrom(foldBaseRoles(data.notionalByRole), data.commitsByRole)));
|
|
295
|
-
}
|
|
296
|
-
lines.push("");
|
|
297
|
-
// Exactly one final line, chosen by the collector's coverage proof: either a hedged note
|
|
298
|
-
// that stays true whenever it prints (the oldest retained event lies inside the window —
|
|
299
|
-
// whether from rotation or a log born inside the window), or the one-time statement of the
|
|
300
|
-
// backlog-tally omission. A flat "rotated out" claim would be false for a log that never
|
|
301
|
-
// had the older events, so the wording names only what is provably the case. A role-scoped
|
|
302
|
-
// report already states the omission in ROLE_SCOPE_NOTE, so it does not repeat the --days
|
|
303
|
-
// pointer, which would imply a scoped day report shows the tallies it too omits.
|
|
304
|
-
if (!data.coversFullWindow)
|
|
305
|
-
lines.push(SPARSE_WINDOW_NOTE);
|
|
306
|
-
else if (data.role === undefined)
|
|
307
|
-
lines.push("backlog tallies (features done / bugs fixed / plans added / bugs filed) need the day report (--days)");
|
|
308
|
-
return lines.join("\n");
|
|
309
|
-
}
|
|
310
|
-
/** Fold every key of a per-role-id map through its base role, so `feature-2` joins `feature`.
|
|
311
|
-
* The same fold the day caller does per day, applied to an already-aggregated window map. */
|
|
312
|
-
function foldBaseRoles(map) {
|
|
313
|
-
const folded = {};
|
|
314
|
-
for (const [id, c] of Object.entries(map))
|
|
315
|
-
addTo(folded, baseRoleOf(id), c);
|
|
316
|
-
return folded;
|
|
317
|
-
}
|
|
318
183
|
/** Rank one cost map against a base-keyed commits map into cost-per-commit output: the shared
|
|
319
184
|
* core of the day and --since cost-per-commit lines. `cost` is expected base-role keyed (the
|
|
320
185
|
* day caller folds per day, the --since caller via `foldBaseRoles`); commits are base-role
|
|
321
186
|
* keyed. `merged` is [role, cost per commit, commits] for roles with both, ranked by cost per
|
|
322
187
|
* commit desc then name asc; `idle` is roles that spent and merged nothing, ranked by spend.
|
|
323
188
|
* A role with commits but no spend appears in neither list. */
|
|
324
|
-
function costPerCommitPairsFrom(cost, commits) {
|
|
189
|
+
export function costPerCommitPairsFrom(cost, commits) {
|
|
325
190
|
const mergeDone = [];
|
|
326
191
|
const idleCost = [];
|
|
327
192
|
for (const [role, c] of Object.entries(cost)) {
|
|
@@ -336,24 +201,26 @@ function costPerCommitPairsFrom(cost, commits) {
|
|
|
336
201
|
sortByValueDesc(mergeDone);
|
|
337
202
|
return { mergeDone, idle: rankCountEntries(idleCost) };
|
|
338
203
|
}
|
|
339
|
-
/** The day caller of the cost-per-commit core:
|
|
340
|
-
* its base role (so `feature-2` joins
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
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:**`. */
|
|
344
211
|
function costPerCommitPairs(series, pick = (d) => d.costByRole) {
|
|
345
212
|
const cost = {};
|
|
346
213
|
const commits = {};
|
|
347
214
|
for (const d of series) {
|
|
348
215
|
for (const [id, c] of Object.entries(pick(d)))
|
|
349
|
-
addTo(cost,
|
|
216
|
+
addTo(cost, id, c);
|
|
350
217
|
for (const [role, n] of Object.entries(d.commitsByRole ?? {}))
|
|
351
218
|
addTo(commits, role, n);
|
|
352
219
|
}
|
|
353
|
-
return costPerCommitPairsFrom(cost, commits);
|
|
220
|
+
return costPerCommitPairsFrom(foldBaseRoles(cost), commits);
|
|
354
221
|
}
|
|
355
222
|
/** One `**<label>:** …` line from costPerCommitPairs output; `-` when there is nothing. */
|
|
356
|
-
function costPerCommitLine(label, pairs) {
|
|
223
|
+
export function costPerCommitLine(label, pairs) {
|
|
357
224
|
const parts = [
|
|
358
225
|
...pairs.mergeDone.map(([r, per, n]) => `${r} — ${usd(per)} (${n})`),
|
|
359
226
|
...pairs.idle.map(([r, c]) => `${r} — ${usd(c)}, no commits`),
|
|
@@ -424,9 +291,7 @@ function pushReportProviderLines(lines, data) {
|
|
|
424
291
|
// omits it (ROLE_SCOPE_NOTE covers the omission), and a window with no modelled start
|
|
425
292
|
// prints no line (keeping a model-less day's output unchanged), exactly as the provider
|
|
426
293
|
// lines stay absent when no provider spent. Ranked and formatted like Cost by role.
|
|
427
|
-
|
|
428
|
-
if (modelPairs.length > 0)
|
|
429
|
-
lines.push(rankedRoleLine("Runs by model", modelPairs, String));
|
|
294
|
+
pushPresentRankedLine(lines, "Runs by model", rankedRoleTotals(data.series, (d) => d.runsByModel ?? {}));
|
|
430
295
|
}
|
|
431
296
|
}
|
|
432
297
|
/** The per-role review-friction and outright-failure lines. */
|
|
@@ -443,7 +308,6 @@ function pushReportFrictionLines(lines, data) {
|
|
|
443
308
|
// through review, which rejections/revisions would miss entirely. Zero-means-absent, so a
|
|
444
309
|
// failure-free window renders no line and an all-zero window renders "-" like the others.
|
|
445
310
|
pushRankedRoleLine(lines, "Failed runs by role", rankedRoleTotals(data.series, (d) => d.failedRunsByRole ?? {}));
|
|
446
|
-
pushRankedRoleLine(lines, "Fallbacks by role", rankedRoleTotals(data.series, (d) => d.fallbacksByRole ?? {}));
|
|
447
311
|
pushRankedRoleLine(lines, "Image reroutes by role", rankedRoleTotals(data.series, (d) => d.imageReroutesByRole ?? {}));
|
|
448
312
|
}
|
|
449
313
|
/** The team-only deferred-runs-by-reason line. */
|
|
@@ -454,9 +318,7 @@ function pushReportDeferralLines(lines, data) {
|
|
|
454
318
|
// reason is not a role (ROLE_SCOPE_NOTE covers the omission). Zero-means-absent: a window
|
|
455
319
|
// with no deferral prints no line, so a deferral-free day's output is unchanged.
|
|
456
320
|
if (data.role === undefined) {
|
|
457
|
-
|
|
458
|
-
if (deferred.length > 0)
|
|
459
|
-
lines.push(rankedRoleLine("Deferred runs by reason", deferred, String));
|
|
321
|
+
pushPresentRankedLine(lines, "Deferred runs by reason", rankedRoleTotals(data.series, (d) => d.deferredByReason ?? {}));
|
|
460
322
|
}
|
|
461
323
|
}
|
|
462
324
|
/** The Commits-by-role line plus the team-only merge/main-red/cameo block. */
|
|
@@ -474,14 +336,10 @@ function pushReportMergeLines(lines, data) {
|
|
|
474
336
|
if (data.role === undefined) {
|
|
475
337
|
const mergeDone = rankedRoleTotals(data.series, (d) => d.mergedByRole ?? {});
|
|
476
338
|
lines.push(rankedRoleLine("Merged by role", mergeDone, String));
|
|
477
|
-
|
|
478
|
-
if (mergeFailures.length > 0)
|
|
479
|
-
lines.push(rankedRoleLine("Merge failures by role", mergeFailures, String));
|
|
339
|
+
pushPresentRankedLine(lines, "Merge failures by role", rankedRoleTotals(data.series, (d) => d.mergeFailedByRole ?? {}));
|
|
480
340
|
// The --since reason twin, folded across the series the same way; a reason-free window
|
|
481
341
|
// prints no line, so its output is byte-for-byte unchanged.
|
|
482
|
-
|
|
483
|
-
if (mergeFailureReasons.length > 0)
|
|
484
|
-
lines.push(rankedRoleLine("Merge failures by reason", mergeFailureReasons, String));
|
|
342
|
+
pushPresentRankedLine(lines, "Merge failures by reason", rankedRoleTotals(data.series, (d) => d.mergeFailedByReason ?? {}));
|
|
485
343
|
// Per-day median passing check time, so a suite slowing over days shows; days without a
|
|
486
344
|
// check are left out, and no line prints when no day has one (zero means absent).
|
|
487
345
|
pushDayCameoLine(lines, data.series, "Merge check median", (d) => d.checkMedianMs);
|
|
@@ -495,15 +353,11 @@ function pushReportMergeLines(lines, data) {
|
|
|
495
353
|
// The day twin of the --since window's totals line: the same tests ranked across the
|
|
496
354
|
// series' per-day main-red maps (Main red by test). Zero-absent: no line when no day
|
|
497
355
|
// named a failing test.
|
|
498
|
-
|
|
499
|
-
if (mainRedByTest.length > 0)
|
|
500
|
-
lines.push(rankedRoleLine("Main red by test", mainRedByTest, String));
|
|
356
|
+
pushPresentRankedLine(lines, "Main red by test", rankedRoleTotals(data.series, (d) => d.mainRedByTest ?? {}));
|
|
501
357
|
// The day twin of the --since window's merges line: the same culprits ranked across the
|
|
502
358
|
// series' per-day main-red maps (Main red by merge). Zero-absent: no line when no day
|
|
503
359
|
// blamed a merge.
|
|
504
|
-
|
|
505
|
-
if (mainRedByCulprit.length > 0)
|
|
506
|
-
lines.push(rankedRoleLine("Main red by merge", mainRedByCulprit, String));
|
|
360
|
+
pushPresentRankedLine(lines, "Main red by merge", rankedRoleTotals(data.series, (d) => d.mainRedByCulprit ?? {}));
|
|
507
361
|
pushDayCameoLine(lines, data.series, "Merge wait median", (d) => d.mergeWaitMedianMs);
|
|
508
362
|
// How long the merge slot itself held each change it landed, beside the merge-wait median:
|
|
509
363
|
// the queue backlog (merge wait minus slot time) apart from the slot's own review/check/
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/** The trailing-window half of the usage report's collection: read the harness's event log
|
|
2
|
+
* over the `sinceMs` window and distill it into a `SinceReport` — run/token/commit/cost
|
|
3
|
+
* totals plus features-done/cost maps and main-red tallies. Split out of
|
|
4
|
+
* src/report/report-data.ts (which keeps the per-day `ReportData` series collector) so each
|
|
5
|
+
* window collector owns its own helpers and its own shape: the day collector's zero-filled
|
|
6
|
+
* series and backlog-file reads have no place in a window totals view, and the two change
|
|
7
|
+
* for different reasons. The event-log folding both collectors share lives in
|
|
8
|
+
* src/report/report-usage-fold.ts (foldUsageEvent) and src/report/report-fold.ts (the windowed
|
|
9
|
+
* cache and scope helpers), the shapes they produce in src/report/report-types.ts, and the
|
|
10
|
+
* Markdown rendering in src/report/report-since-render.ts (which imports the shared formatting
|
|
11
|
+
* voice from src/report/report-render.ts). */
|
|
12
|
+
import { assertReportSinceMs, eventWindowCovers, readWindowEvents, sinceWindowTiming } from "../events/event-window.js";
|
|
13
|
+
import { dayKey } from "../text/datetime.js";
|
|
14
|
+
import { addTo } from "../collections.js";
|
|
15
|
+
import { nonemptyObj, roleSum, scopeBases, scopedByRole, zeroCommitCounts, zeroMergeCounts, } from "./report-fold.js";
|
|
16
|
+
import { foldUsageEvent } from "./report-usage-fold.js";
|
|
17
|
+
import { mainRedCulpritsByDay, mainRedFailingTestsByDay, mainRedMinutesByDay } from "./report-main-red.js";
|
|
18
|
+
/** Aggregate team usage over the trailing `sinceMs` window ending now (or `untilMs` ago, which
|
|
19
|
+
* drops events at or after that end; `coversFullWindow` still judges the far edge
|
|
20
|
+
* only), from the event log only (run_end/merged — see SinceReport for why the backlog files
|
|
21
|
+
* are not read). Kept a
|
|
22
|
+
* separate function rather than a `collectReport` variant flag: the day collector's
|
|
23
|
+
* zero-filled series and backlog-file reads have no place in a window totals view. */
|
|
24
|
+
export function collectReportSince(root, sinceMs, role, untilMs) {
|
|
25
|
+
assertReportSinceMs(sinceMs);
|
|
26
|
+
const { now, cutoff, end, slice } = sinceWindowTiming(sinceMs, untilMs);
|
|
27
|
+
const { raw, acc } = foldSinceWindow(root, cutoff, now, end, untilMs);
|
|
28
|
+
// The empty-window branch of the shared proof matters here: with no retained events nothing
|
|
29
|
+
// can have rotated away, and printing a rotation note over a fresh install would claim a
|
|
30
|
+
// history that never existed. When neither proof holds, the oldest retained event lies inside
|
|
31
|
+
// the window and the render notes the possible truncation.
|
|
32
|
+
const coversFullWindow = eventWindowCovers(raw, cutoff);
|
|
33
|
+
// Role scope: every map and total below derives from the fold's per-role maps for the named
|
|
34
|
+
// base roles alone — merge usage (the merge slot's, not the role's) and the team-only
|
|
35
|
+
// fields stay out, so a scoped --since report sums exactly the same events the scoped day
|
|
36
|
+
// report would. The scope is the base roles, so instances (`feature-2`) aggregate into their
|
|
37
|
+
// group as the cap does.
|
|
38
|
+
const bases = scopeBases(role);
|
|
39
|
+
return {
|
|
40
|
+
...slice,
|
|
41
|
+
...(bases === undefined ? {} : { role: bases.join(",") }),
|
|
42
|
+
totals: sinceScopedTotals(acc, bases),
|
|
43
|
+
...sinceRoleMaps(acc, bases),
|
|
44
|
+
// Main's red time is main's own (G1's rule), not any role's, so a role-scoped window
|
|
45
|
+
// carries 0 — the type's always-present default for the team-only field.
|
|
46
|
+
mainRedMs: bases === undefined
|
|
47
|
+
? [...mainRedMinutesByDay(raw.events, cutoff, end).values()].reduce((a, b) => a + b, 0)
|
|
48
|
+
: 0,
|
|
49
|
+
...nonemptyObj("mainRedByTest", sinceMainRedByTest(raw, cutoff, end, bases)),
|
|
50
|
+
...nonemptyObj("mainRedByCulprit", sinceMainRedByCulprit(raw, cutoff, end, bases)),
|
|
51
|
+
coversFullWindow,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** The window's event read and fold, one function so the read happens exactly once per window
|
|
55
|
+
* and every downstream builder — the coverage proof and the main-red by-test/by-culprit maps
|
|
56
|
+
* — consumes the same `raw`. `raw` is the one day-keyed log read the whole report is built on
|
|
57
|
+
* (its `events` feed both the `acc` fold below and the red maps), so its retention-backed
|
|
58
|
+
* coverage and its event set cannot disagree across the builders. */
|
|
59
|
+
function foldSinceWindow(root, cutoff, now, end, untilMs) {
|
|
60
|
+
// The window key is the cutoff's local calendar day, from the shared dayKey helper
|
|
61
|
+
// eventDayKey buckets events with, so the read's day keys cannot disagree with the ts
|
|
62
|
+
// filter below; the day-keyed read may include earlier hours of that day, which the ts
|
|
63
|
+
// filter removes (over-read is at most one day's events). Future-dated events are dropped
|
|
64
|
+
// here too, matching collectReport (its day map only holds the window's days).
|
|
65
|
+
const fromKey = dayKey(cutoff);
|
|
66
|
+
const raw = readWindowEvents(root, fromKey);
|
|
67
|
+
const acc = {
|
|
68
|
+
runs: 0,
|
|
69
|
+
runsByRole: {},
|
|
70
|
+
rejectionsByRole: {},
|
|
71
|
+
postMergeRejectionsByRole: {},
|
|
72
|
+
revisionsByRole: {},
|
|
73
|
+
refusalsByRole: {},
|
|
74
|
+
checkDurationsMs: [],
|
|
75
|
+
checkFailures: 0,
|
|
76
|
+
mergeWaitsMs: [],
|
|
77
|
+
mergeDurationsMs: [],
|
|
78
|
+
runDurationsMs: [],
|
|
79
|
+
providerWaitsMs: [],
|
|
80
|
+
slotWaitsMs: [],
|
|
81
|
+
checkWaitsMs: [],
|
|
82
|
+
reviewDurationsMs: [],
|
|
83
|
+
failedRunsByRole: {},
|
|
84
|
+
imageReroutesByRole: {},
|
|
85
|
+
tokensByRole: {},
|
|
86
|
+
promptTokensByRole: {},
|
|
87
|
+
cacheReadTokensByRole: {},
|
|
88
|
+
costByRole: {},
|
|
89
|
+
tokensOut: 0,
|
|
90
|
+
...zeroCommitCounts(),
|
|
91
|
+
costUsd: 0,
|
|
92
|
+
notionalCostUsd: 0,
|
|
93
|
+
notionalByRole: {},
|
|
94
|
+
...zeroMergeCounts(),
|
|
95
|
+
mergedByRole: {},
|
|
96
|
+
mergeFailedByRole: {},
|
|
97
|
+
mergeFailedByReason: {},
|
|
98
|
+
costByProvider: {},
|
|
99
|
+
notionalByProvider: {},
|
|
100
|
+
notionalByBackend: {},
|
|
101
|
+
runsByModel: {},
|
|
102
|
+
deferredByReason: {},
|
|
103
|
+
};
|
|
104
|
+
for (const ev of raw.events) {
|
|
105
|
+
if (typeof ev.ts !== "number" || ev.ts < cutoff || ev.ts > now || (untilMs !== undefined && ev.ts >= end))
|
|
106
|
+
continue;
|
|
107
|
+
foldUsageEvent(acc, ev);
|
|
108
|
+
}
|
|
109
|
+
return { raw, acc };
|
|
110
|
+
}
|
|
111
|
+
/** The window's `totals` group under both scope rules, named so the team-vs-role branch reads
|
|
112
|
+
* as one function instead of a ternary buried in the return: the team figure sums the merge-slot
|
|
113
|
+
* fields in (the merge slot's dash tokens/cost are the same team spend the budget charges), while
|
|
114
|
+
* the role branch sums just the base roles through `roleSum` and zeroes the merge counts — a
|
|
115
|
+
* role's merge usage is the merge slot's, not the role's. */
|
|
116
|
+
function sinceScopedTotals(acc, bases) {
|
|
117
|
+
return bases === undefined
|
|
118
|
+
? {
|
|
119
|
+
tokensOut: acc.tokensOut + (acc.mergeTokens ?? 0),
|
|
120
|
+
runs: acc.runs ?? 0,
|
|
121
|
+
commits: acc.commits,
|
|
122
|
+
costUsd: acc.costUsd + (acc.mergeCostUsd ?? 0),
|
|
123
|
+
...(acc.notionalCostUsd > 0 ? { notionalCostUsd: acc.notionalCostUsd } : {}),
|
|
124
|
+
...nonemptyObj("notionalByBackend", acc.notionalByBackend),
|
|
125
|
+
mergeRuns: acc.mergeRuns ?? 0,
|
|
126
|
+
mergeTokens: acc.mergeTokens ?? 0,
|
|
127
|
+
mergeCostUsd: acc.mergeCostUsd ?? 0,
|
|
128
|
+
...((acc.mergeFailed ?? 0) > 0 ? { mergeFailed: acc.mergeFailed } : {}),
|
|
129
|
+
}
|
|
130
|
+
: (() => {
|
|
131
|
+
// The role's own notional spend, from its run_end events; the merge slot's
|
|
132
|
+
// notional is team-wide and stays out like its cost.
|
|
133
|
+
const notional = roleSum(acc.notionalByRole, bases);
|
|
134
|
+
return {
|
|
135
|
+
tokensOut: roleSum(acc.tokensByRole, bases),
|
|
136
|
+
runs: roleSum(acc.runsByRole, bases),
|
|
137
|
+
commits: roleSum(acc.commitsByRole, bases),
|
|
138
|
+
costUsd: roleSum(acc.costByRole, bases),
|
|
139
|
+
...(notional > 0 ? { notionalCostUsd: notional } : {}),
|
|
140
|
+
...zeroMergeCounts(),
|
|
141
|
+
};
|
|
142
|
+
})();
|
|
143
|
+
}
|
|
144
|
+
/** Flatten a per-day main-red record map (day → label → count) into one flat label→count map,
|
|
145
|
+
* the shared body of sinceMainRedByTest and sinceMainRedByCulprit. Main's red state is main's
|
|
146
|
+
* own, not any role's, so a role-scoped window carries none — the empty map `nonemptyObj` then
|
|
147
|
+
* omits from the JSON, keeping a red-free --since byte-identical to today. */
|
|
148
|
+
function flattenMainRedByDay(perDay, bases) {
|
|
149
|
+
if (bases !== undefined)
|
|
150
|
+
return {};
|
|
151
|
+
const flat = {};
|
|
152
|
+
for (const record of perDay.values()) {
|
|
153
|
+
for (const [label, n] of Object.entries(record))
|
|
154
|
+
addTo(flat, label, n);
|
|
155
|
+
}
|
|
156
|
+
return flat;
|
|
157
|
+
}
|
|
158
|
+
/** Main's red failing tests over the window, flattened across the per-day records of the same
|
|
159
|
+
* red `main_check` events the red time uses (G1's rule) into one map. Main's failing tests are
|
|
160
|
+
* main's own, not any role's, so a role-scoped window carries none (see flattenMainRedByDay). */
|
|
161
|
+
function sinceMainRedByTest(raw, cutoff, end, bases) {
|
|
162
|
+
return flattenMainRedByDay(mainRedFailingTestsByDay(raw.events, cutoff, end), bases);
|
|
163
|
+
}
|
|
164
|
+
/** Main's red culprits over the window, flattened across the per-day records of the same red
|
|
165
|
+
* `main_culprit` events the live `mainred` alert's `red.culprit` reads (the historical twin)
|
|
166
|
+
* into one map. Main's own blame, not any role's, so a role-scoped window carries none (see
|
|
167
|
+
* flattenMainRedByDay). */
|
|
168
|
+
function sinceMainRedByCulprit(raw, cutoff, end, bases) {
|
|
169
|
+
return flattenMainRedByDay(mainRedCulpritsByDay(raw.events, cutoff, end), bases);
|
|
170
|
+
}
|
|
171
|
+
/** The `SinceReport` return's role-scoped map group — every field a reader names when asking
|
|
172
|
+
* "which fields are team-only?": each `scopedByRole` call applies the base-role scope (the
|
|
173
|
+
* rule `collectReportSince`'s `bases` comment spells), so a role-scoped window shows exactly
|
|
174
|
+
* the maps the scoped day report would, and the team-only fields (providers, backends, models,
|
|
175
|
+
* deferred reasons) carry full maps for the team or empty maps for a scoped role. */
|
|
176
|
+
function sinceRoleMaps(acc, bases) {
|
|
177
|
+
// The --since render/JSON's Notional by role line is conditional on the window's own
|
|
178
|
+
// notional spend, scoped to base when a role is given — the same conditional the day
|
|
179
|
+
// collector applies to ReportDay.notionalByRole (line ~573), so a role-scoped window
|
|
180
|
+
// whose role spent no notional carries no notionalByRole instead of an empty map.
|
|
181
|
+
const notionalByRole = scopedByRole(acc.notionalByRole, bases);
|
|
182
|
+
// Deferred runs are bucketed by a reason code (not a role), so a role-scoped window
|
|
183
|
+
// carries no entries — the same team-only rule runsByModel follows. Only set when the
|
|
184
|
+
// window actually deferred (zero means absent), so a deferral-free --since JSON omits it.
|
|
185
|
+
const deferred = scopedByRole(acc.deferredByReason, bases);
|
|
186
|
+
return {
|
|
187
|
+
runsByRole: scopedByRole(acc.runsByRole, bases),
|
|
188
|
+
costByRole: scopedByRole(acc.costByRole, bases),
|
|
189
|
+
rejectionsByRole: scopedByRole(acc.rejectionsByRole, bases),
|
|
190
|
+
postMergeRejectionsByRole: scopedByRole(acc.postMergeRejectionsByRole, bases),
|
|
191
|
+
revisionsByRole: scopedByRole(acc.revisionsByRole, bases),
|
|
192
|
+
refusalsByRole: scopedByRole(acc.refusalsByRole, bases),
|
|
193
|
+
failedRunsByRole: scopedByRole(acc.failedRunsByRole, bases),
|
|
194
|
+
imageReroutesByRole: scopedByRole(acc.imageReroutesByRole, bases),
|
|
195
|
+
tokensByRole: scopedByRole(acc.tokensByRole, bases),
|
|
196
|
+
...nonemptyObj("promptTokensByRole", scopedByRole(acc.promptTokensByRole, bases)),
|
|
197
|
+
...nonemptyObj("cacheReadTokensByRole", scopedByRole(acc.cacheReadTokensByRole, bases)),
|
|
198
|
+
commitsByRole: scopedByRole(acc.commitsByRole, bases),
|
|
199
|
+
mergedByRole: scopedByRole(acc.mergedByRole, bases),
|
|
200
|
+
mergeFailedByRole: scopedByRole(acc.mergeFailedByRole, bases),
|
|
201
|
+
// A result code is not a role, so a role-scoped window carries an empty map (a code
|
|
202
|
+
// never baseRoleOf to the role) — the render gates the reason line to the team report.
|
|
203
|
+
mergeFailedByReason: scopedByRole(acc.mergeFailedByReason, bases),
|
|
204
|
+
...nonemptyObj("notionalByRole", notionalByRole),
|
|
205
|
+
// Provider spend is a team attribution (a provider is not a role), so a role-scoped
|
|
206
|
+
// window carries empty maps — providers never baseRoleOf to the role — and the render
|
|
207
|
+
// gates the provider lines to the team report anyway. Always present, empty when none.
|
|
208
|
+
costByProvider: scopedByRole(acc.costByProvider, bases),
|
|
209
|
+
notionalByProvider: scopedByRole(acc.notionalByProvider, bases),
|
|
210
|
+
// Backend spend is a team attribution (a backend is not a role), so a role-scoped window
|
|
211
|
+
// omits it — the same absent-when-empty rule the day collector applies.
|
|
212
|
+
...nonemptyObj("notionalByBackend", scopedByRole(acc.notionalByBackend, bases)),
|
|
213
|
+
// Model attribution is team-only (a model is not a role, so no model folds to the
|
|
214
|
+
// scoped role), the same full-map-for-team / empty-for-scoped rule costByProvider
|
|
215
|
+
// follows. Always present so the render can gate on the window's own events.
|
|
216
|
+
runsByModel: scopedByRole(acc.runsByModel, bases),
|
|
217
|
+
...nonemptyObj("deferredByReason", deferred),
|
|
218
|
+
};
|
|
219
|
+
}
|