tumwater 0.4.0 → 0.4.2
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 +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- 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 +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -38,6 +38,21 @@ function rankedRoleTotals(series, pick) {
|
|
|
38
38
|
function rankedRoleLine(label, pairs, fmt) {
|
|
39
39
|
return `**${label}:** ${pairs.length === 0 ? "-" : pairs.map(([r, n]) => `${r} — ${fmt(n)}`).join(" · ")}`;
|
|
40
40
|
}
|
|
41
|
+
/** The "**Prompt tokens by role:**" line — each role's prompt-token total and, when its
|
|
42
|
+
* backend reported a cache read, the share of it served from cache (`12000 (80% cached)`)
|
|
43
|
+
* — with zero-total roles omitted (the same zero-means-absent rule the sibling lines apply)
|
|
44
|
+
* and "-" when no role sent a prompt. `prompt` arrives already ranked (prompt desc then
|
|
45
|
+
* name asc). The one home of the share format, so the day and `--since` lines cannot drift. */
|
|
46
|
+
function promptTokensLine(prompt, cache) {
|
|
47
|
+
const cacheByRole = new Map(cache);
|
|
48
|
+
const cells = prompt
|
|
49
|
+
.filter(([, n]) => n > 0)
|
|
50
|
+
.map(([role, n]) => {
|
|
51
|
+
const c = cacheByRole.get(role) ?? 0;
|
|
52
|
+
return `${role} — ${compactTokens(n)}${c > 0 ? ` (${Math.round((100 * c) / n)}% cached)` : ""}`;
|
|
53
|
+
});
|
|
54
|
+
return `**Prompt tokens by role:** ${cells.length === 0 ? "-" : cells.join(" · ")}`;
|
|
55
|
+
}
|
|
41
56
|
/** Push one "**Label:** …" ranked-role line, dropping roles with a non-positive value (zero
|
|
42
57
|
* count/spend) before rendering, so an all-zero window still renders "-" exactly like the
|
|
43
58
|
* unfiltered lines. fmt formats each role's number (default String for count lines; pass
|
|
@@ -46,6 +61,18 @@ function rankedRoleLine(label, pairs, fmt) {
|
|
|
46
61
|
function pushRankedRoleLine(lines, label, pairs, fmt = String) {
|
|
47
62
|
lines.push(rankedRoleLine(label, pairs.filter(([, n]) => n > 0), fmt));
|
|
48
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
|
+
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
|
+
}
|
|
49
76
|
/** The per-day cameo line of the team day report — "MMM D <cell>" cells for each day that
|
|
50
77
|
* carries a value, joined under a bold title, or undefined when no day has one (zero means
|
|
51
78
|
* absent). The one home of that cameo rendering, shared by the per-day medians, the red-check
|
|
@@ -159,6 +186,20 @@ function pushReportSummary(lines, t, tail = "", scoped = false) {
|
|
|
159
186
|
* collector's captured cutoff, so the printed window matches the aggregated one). */
|
|
160
187
|
export function renderSinceReportMarkdown(data) {
|
|
161
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) {
|
|
162
203
|
lines.push("# tumwater usage report");
|
|
163
204
|
lines.push("");
|
|
164
205
|
lines.push(data.untilMs !== undefined && data.toIso !== undefined
|
|
@@ -167,21 +208,36 @@ export function renderSinceReportMarkdown(data) {
|
|
|
167
208
|
if (data.role !== undefined)
|
|
168
209
|
lines.push(`role: ${data.role}`);
|
|
169
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) {
|
|
170
215
|
const t = data.totals;
|
|
171
216
|
// Same totals voice as the day report, minus the features/bugs and filing cells: those tallies
|
|
172
217
|
// come from day-granular backlog-file dates that cannot subdivide a sub-day window, so the
|
|
173
218
|
// final line below states the omission instead of showing a day-rounded number.
|
|
174
219
|
pushReportSummary(lines, t, "", data.role !== undefined);
|
|
175
220
|
lines.push("");
|
|
221
|
+
}
|
|
222
|
+
/** Runs / tokens / prompt-tokens / cost breakdowns by role. */
|
|
223
|
+
function pushSinceRoleLines(lines, data) {
|
|
176
224
|
lines.push(rankedRoleLine("Runs by role", rankedRoleMap(data.runsByRole), String));
|
|
177
225
|
// Same breakdown for token spend: ranked by tokens desc then name asc, zero-token roles
|
|
178
226
|
// omitted — an all-zero window renders "-" like the other lines. Output tokens are the
|
|
179
227
|
// resource the budget charges, so this is the line an operator reads to find which role's
|
|
180
228
|
// prompt loop burned the window's tokens.
|
|
181
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 ?? {})));
|
|
182
235
|
// Same breakdown for spend: ranked by spend desc then name asc, zero-spend roles omitted —
|
|
183
236
|
// an all-zero window renders "-" like the runs line.
|
|
184
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) {
|
|
185
241
|
// The same breakdown per PROVIDER (plans: per-provider spend), gated to the team report
|
|
186
242
|
// like its day sibling. SinceReport.costByProvider is always present (empty when none), so
|
|
187
243
|
// the zero-means-absent gate prints no line for a window with no provider spend.
|
|
@@ -193,15 +249,29 @@ export function renderSinceReportMarkdown(data) {
|
|
|
193
249
|
// omits it. SinceReport.runsByModel is always present, so a window with no modelled
|
|
194
250
|
// start prints no line (keeping a model-less day's output unchanged), exactly as the
|
|
195
251
|
// provider lines stay absent when no provider spent.
|
|
196
|
-
|
|
197
|
-
if (modelPairs.length > 0)
|
|
198
|
-
lines.push(rankedRoleLine("Runs by model", modelPairs, String));
|
|
252
|
+
pushPresentRankedLine(lines, "Runs by model", rankedRoleMap(data.runsByModel));
|
|
199
253
|
}
|
|
254
|
+
}
|
|
255
|
+
/** Main's red time and the test/merge culprits behind it, gated like the day sibling. */
|
|
256
|
+
function pushSinceMainRedLines(lines, data) {
|
|
200
257
|
// Main's red wall-clock over the window (main's own, not any role's — a role-scoped
|
|
201
258
|
// collector carries 0, so this never prints for a scoped window). Zero-absent: no line for
|
|
202
259
|
// a window main was never red.
|
|
203
260
|
if (data.mainRedMs > 0)
|
|
204
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) {
|
|
205
275
|
// The same breakdown for review friction: rejections per role from review_rejected events;
|
|
206
276
|
// a window with none renders "-" like the other lines. The post-merge line is the
|
|
207
277
|
// afterMerge: true subset — rejections that reached an already-merged commit.
|
|
@@ -215,16 +285,19 @@ export function renderSinceReportMarkdown(data) {
|
|
|
215
285
|
// none renders "-" like the other lines.
|
|
216
286
|
pushRankedRoleLine(lines, "Failed runs by role", rankedRoleMap(data.failedRunsByRole));
|
|
217
287
|
pushRankedRoleLine(lines, "Fallbacks by role", rankedRoleMap(data.fallbacksByRole));
|
|
218
|
-
|
|
288
|
+
}
|
|
289
|
+
/** Deferred maintenance runs by reason code, gated to the team report. */
|
|
290
|
+
function pushSinceDeferralLines(lines, data) {
|
|
219
291
|
// Deferred maintenance runs per reason code, the --since twin of the day render's line:
|
|
220
292
|
// how many due maintenance runs stood down and why (backlog / no-work / plans-full).
|
|
221
293
|
// Gated to the team report — a reason is not a role, so a role-scoped window omits it.
|
|
222
294
|
// Zero-means-absent: a window with no deferral prints no line.
|
|
223
295
|
if (data.role === undefined) {
|
|
224
|
-
|
|
225
|
-
if (deferred.length > 0)
|
|
226
|
-
lines.push(rankedRoleLine("Deferred runs by reason", deferred, String));
|
|
296
|
+
pushPresentRankedLine(lines, "Deferred runs by reason", rankedRoleMap(data.deferredByReason ?? {}));
|
|
227
297
|
}
|
|
298
|
+
}
|
|
299
|
+
/** Commits by role and the merged/merge-failure split, gated to the team report. */
|
|
300
|
+
function pushSinceCommitLines(lines, data) {
|
|
228
301
|
// The merged-commits split by base role, matching the day render's unfiltered line: the
|
|
229
302
|
// window's totals.commits carries the count; this line says which role's commits it was.
|
|
230
303
|
lines.push(rankedRoleLine("Commits by role", rankedRoleMap(data.commitsByRole), String));
|
|
@@ -235,22 +308,32 @@ export function renderSinceReportMarkdown(data) {
|
|
|
235
308
|
// some role has one; a merge-free window renders `-` like the other lines.
|
|
236
309
|
if (data.role === undefined) {
|
|
237
310
|
lines.push(rankedRoleLine("Merged by role", rankedRoleMap(data.mergedByRole), String));
|
|
238
|
-
|
|
239
|
-
if (mergeFailures.length > 0)
|
|
240
|
-
lines.push(rankedRoleLine("Merge failures by role", mergeFailures, String));
|
|
311
|
+
pushPresentRankedLine(lines, "Merge failures by role", rankedRoleMap(data.mergeFailedByRole));
|
|
241
312
|
// WHY the window's merges failed, from each merge_failed's result code — the day
|
|
242
313
|
// report's reason twin of the role line above, so an operator can tell a review
|
|
243
314
|
// rejection (fix the change) from a conflict/block (rebase) from a shutdown abort
|
|
244
315
|
// (capacity). Ranked the same way (count desc then code asc), reason-free only.
|
|
245
|
-
|
|
246
|
-
if (mergeFailureReasons.length > 0)
|
|
247
|
-
lines.push(rankedRoleLine("Merge failures by reason", mergeFailureReasons, String));
|
|
316
|
+
pushPresentRankedLine(lines, "Merge failures by reason", rankedRoleMap(data.mergeFailedByReason ?? {}));
|
|
248
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)));
|
|
249
326
|
// Subscription spend is notional: its own line, only when the window has any — the same
|
|
250
327
|
// conditional/filtered `usd` form the day render's "Estimated by role" uses.
|
|
251
328
|
if (data.notionalByRole !== undefined) {
|
|
252
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)));
|
|
253
333
|
}
|
|
334
|
+
}
|
|
335
|
+
/** The trailing blank and the exactly-one coverage/backlog-omission note. */
|
|
336
|
+
function pushSinceCoverageLines(lines, data) {
|
|
254
337
|
lines.push("");
|
|
255
338
|
// Exactly one final line, chosen by the collector's coverage proof: either a hedged note
|
|
256
339
|
// that stays true whenever it prints (the oldest retained event lies inside the window —
|
|
@@ -263,24 +346,22 @@ export function renderSinceReportMarkdown(data) {
|
|
|
263
346
|
lines.push(SPARSE_WINDOW_NOTE);
|
|
264
347
|
else if (data.role === undefined)
|
|
265
348
|
lines.push("backlog tallies (features done / bugs fixed / plans added / bugs filed) need the day report (--days)");
|
|
266
|
-
return lines.join("\n");
|
|
267
349
|
}
|
|
268
|
-
/**
|
|
269
|
-
*
|
|
270
|
-
|
|
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
|
+
/** Rank one cost map against a base-keyed commits map into cost-per-commit output: the shared
|
|
359
|
+
* core of the day and --since cost-per-commit lines. `cost` is expected base-role keyed (the
|
|
360
|
+
* day caller folds per day, the --since caller via `foldBaseRoles`); commits are base-role
|
|
361
|
+
* keyed. `merged` is [role, cost per commit, commits] for roles with both, ranked by cost per
|
|
271
362
|
* commit desc then name asc; `idle` is roles that spent and merged nothing, ranked by spend.
|
|
272
|
-
*
|
|
273
|
-
|
|
274
|
-
* role with commits but no spend in the picked map appears in neither list. */
|
|
275
|
-
function costPerCommitPairs(series, pick = (d) => d.costByRole) {
|
|
276
|
-
const cost = {};
|
|
277
|
-
const commits = {};
|
|
278
|
-
for (const d of series) {
|
|
279
|
-
for (const [id, c] of Object.entries(pick(d)))
|
|
280
|
-
addTo(cost, baseRoleOf(id), c);
|
|
281
|
-
for (const [role, n] of Object.entries(d.commitsByRole ?? {}))
|
|
282
|
-
addTo(commits, role, n);
|
|
283
|
-
}
|
|
363
|
+
* A role with commits but no spend appears in neither list. */
|
|
364
|
+
function costPerCommitPairsFrom(cost, commits) {
|
|
284
365
|
const mergeDone = [];
|
|
285
366
|
const idleCost = [];
|
|
286
367
|
for (const [role, c] of Object.entries(cost)) {
|
|
@@ -295,6 +376,22 @@ function costPerCommitPairs(series, pick = (d) => d.costByRole) {
|
|
|
295
376
|
sortByValueDesc(mergeDone);
|
|
296
377
|
return { mergeDone, idle: rankCountEntries(idleCost) };
|
|
297
378
|
}
|
|
379
|
+
/** The day caller of the cost-per-commit core: fold each day's role-id-keyed spend map through
|
|
380
|
+
* its base role (so `feature-2` joins `feature`) and aggregate the base-keyed commitsByRole,
|
|
381
|
+
* then hand both to `costPerCommitPairsFrom`. `pick` selects the per-role-id spend map; merge
|
|
382
|
+
* spend (the reviewer's) and notional subscription spend stay out by default, matching
|
|
383
|
+
* `**Cost by role:**`. */
|
|
384
|
+
function costPerCommitPairs(series, pick = (d) => d.costByRole) {
|
|
385
|
+
const cost = {};
|
|
386
|
+
const commits = {};
|
|
387
|
+
for (const d of series) {
|
|
388
|
+
for (const [id, c] of Object.entries(pick(d)))
|
|
389
|
+
addTo(cost, baseRoleOf(id), c);
|
|
390
|
+
for (const [role, n] of Object.entries(d.commitsByRole ?? {}))
|
|
391
|
+
addTo(commits, role, n);
|
|
392
|
+
}
|
|
393
|
+
return costPerCommitPairsFrom(cost, commits);
|
|
394
|
+
}
|
|
298
395
|
/** One `**<label>:** …` line from costPerCommitPairs output; `-` when there is nothing. */
|
|
299
396
|
function costPerCommitLine(label, pairs) {
|
|
300
397
|
const parts = [
|
|
@@ -303,11 +400,8 @@ function costPerCommitLine(label, pairs) {
|
|
|
303
400
|
];
|
|
304
401
|
return `**${label}:** ${parts.length === 0 ? "-" : parts.join(" · ")}`;
|
|
305
402
|
}
|
|
306
|
-
/**
|
|
307
|
-
|
|
308
|
-
* ReportData instead. Pure function of ReportData: no I/O, no clock reads. */
|
|
309
|
-
export function renderReportMarkdown(data) {
|
|
310
|
-
const lines = [];
|
|
403
|
+
/** The title, window line, optional role line, then the report summary line. */
|
|
404
|
+
function pushReportHeader(lines, data) {
|
|
311
405
|
lines.push("# tumwater usage report");
|
|
312
406
|
lines.push("");
|
|
313
407
|
lines.push(reportWindow(data.from, data.to, data.days, eventsRotationLabel()));
|
|
@@ -325,6 +419,9 @@ export function renderReportMarkdown(data) {
|
|
|
325
419
|
: "";
|
|
326
420
|
pushReportSummary(lines, t, tail, data.role !== undefined);
|
|
327
421
|
lines.push("");
|
|
422
|
+
}
|
|
423
|
+
/** The day-by-day window table (day, tokens out, runs, commits, cost) and its blank line. */
|
|
424
|
+
function pushReportSeriesTable(lines, data) {
|
|
328
425
|
const maxTokens = data.series.reduce((m, d) => Math.max(m, d.tokensOut), 0);
|
|
329
426
|
lines.push(...markdownTable(["day", "tokens out", "runs", "commits", "cost"], data.series.map((d) => {
|
|
330
427
|
const runs = Object.values(d.runsByRole).reduce((a, b) => a + b, 0);
|
|
@@ -333,16 +430,26 @@ export function renderReportMarkdown(data) {
|
|
|
333
430
|
return [monthDay(d.date), `${compactTokens(d.tokensOut)}${bar}`, String(runs), String(d.commits), usd(d.costUsd)];
|
|
334
431
|
}), ["left", "right", "right", "right", "right"]));
|
|
335
432
|
lines.push("");
|
|
433
|
+
}
|
|
434
|
+
/** The per-role runs/tokens/prompt/cost breakdown lines. */
|
|
435
|
+
function pushReportRoleLines(lines, data) {
|
|
336
436
|
lines.push(rankedRoleLine("Runs by role", rankedRoleTotals(data.series, (d) => d.runsByRole), String));
|
|
337
437
|
// The same breakdown for output tokens (the resource the budget charges): window totals per
|
|
338
438
|
// role from tokensByRole, ranked like the runs line. Zero-token roles are omitted; an
|
|
339
439
|
// all-zero window renders "-" like the runs line.
|
|
340
440
|
pushRankedRoleLine(lines, "Tokens by role", rankedRoleTotals(data.series, (d) => d.tokensByRole ?? {}), compactTokens);
|
|
441
|
+
// The input side of that same bill, straight after the output-token line: each role's prompt
|
|
442
|
+
// totals and, when some run reported a cache read, the share of them served from cache — so
|
|
443
|
+
// the same report that says who burned output tokens also says whose prompts are cached.
|
|
444
|
+
lines.push(promptTokensLine(rankedRoleTotals(data.series, (d) => d.promptTokensByRole ?? {}), rankedRoleTotals(data.series, (d) => d.cacheReadTokensByRole ?? {})));
|
|
341
445
|
// The same breakdown for spend: window totals per role from costByRole, ranked by spend
|
|
342
446
|
// desc then name asc — spend is what the operator acts on (an operator tuning per-role
|
|
343
447
|
// intervals wants the burning role first, not the busiest one). Zero-spend roles are
|
|
344
448
|
// omitted; an all-zero window renders "-" like the runs line.
|
|
345
449
|
pushRankedRoleLine(lines, "Cost by role", rankedRoleTotals(data.series, (d) => d.costByRole), usd);
|
|
450
|
+
}
|
|
451
|
+
/** The team-only per-provider and per-model breakdown lines. */
|
|
452
|
+
function pushReportProviderLines(lines, data) {
|
|
346
453
|
// The same breakdown per PROVIDER (plans: per-provider spend): which seam of a mixed team
|
|
347
454
|
// spent what, straight from the events' providerCost maps (run_end's own runs and the
|
|
348
455
|
// merge's reviewer seam). Gated to the team report — a provider is not a role, so a
|
|
@@ -357,10 +464,11 @@ export function renderReportMarkdown(data) {
|
|
|
357
464
|
// omits it (ROLE_SCOPE_NOTE covers the omission), and a window with no modelled start
|
|
358
465
|
// prints no line (keeping a model-less day's output unchanged), exactly as the provider
|
|
359
466
|
// lines stay absent when no provider spent. Ranked and formatted like Cost by role.
|
|
360
|
-
|
|
361
|
-
if (modelPairs.length > 0)
|
|
362
|
-
lines.push(rankedRoleLine("Runs by model", modelPairs, String));
|
|
467
|
+
pushPresentRankedLine(lines, "Runs by model", rankedRoleTotals(data.series, (d) => d.runsByModel ?? {}));
|
|
363
468
|
}
|
|
469
|
+
}
|
|
470
|
+
/** The per-role review-friction and outright-failure lines. */
|
|
471
|
+
function pushReportFrictionLines(lines, data) {
|
|
364
472
|
// The same breakdown for review friction: window rejections per role from review_rejected
|
|
365
473
|
// events, ranked like the runs line; a window with none renders "-". The post-merge line
|
|
366
474
|
// is the afterMerge: true subset — rejections that reached an already-merged commit.
|
|
@@ -374,17 +482,20 @@ export function renderReportMarkdown(data) {
|
|
|
374
482
|
// failure-free window renders no line and an all-zero window renders "-" like the others.
|
|
375
483
|
pushRankedRoleLine(lines, "Failed runs by role", rankedRoleTotals(data.series, (d) => d.failedRunsByRole ?? {}));
|
|
376
484
|
pushRankedRoleLine(lines, "Fallbacks by role", rankedRoleTotals(data.series, (d) => d.fallbacksByRole ?? {}));
|
|
377
|
-
|
|
485
|
+
}
|
|
486
|
+
/** The team-only deferred-runs-by-reason line. */
|
|
487
|
+
function pushReportDeferralLines(lines, data) {
|
|
378
488
|
// Deferred maintenance runs per reason code — how many due maintenance runs stood down
|
|
379
489
|
// and why, so an idle day reads as backlog-open / no-work / plans-full rather than blank,
|
|
380
490
|
// the per-role breakdown of the deferred maintenances. Gated to the team report — a
|
|
381
491
|
// reason is not a role (ROLE_SCOPE_NOTE covers the omission). Zero-means-absent: a window
|
|
382
492
|
// with no deferral prints no line, so a deferral-free day's output is unchanged.
|
|
383
493
|
if (data.role === undefined) {
|
|
384
|
-
|
|
385
|
-
if (deferred.length > 0)
|
|
386
|
-
lines.push(rankedRoleLine("Deferred runs by reason", deferred, String));
|
|
494
|
+
pushPresentRankedLine(lines, "Deferred runs by reason", rankedRoleTotals(data.series, (d) => d.deferredByReason ?? {}));
|
|
387
495
|
}
|
|
496
|
+
}
|
|
497
|
+
/** The Commits-by-role line plus the team-only merge/main-red/cameo block. */
|
|
498
|
+
function pushReportMergeLines(lines, data) {
|
|
388
499
|
// Work ratio 4/4: the merged commits split by base role and by tier. The totals line's
|
|
389
500
|
// `commits` carries the count; these lines say who made it and how much of it was work.
|
|
390
501
|
lines.push(rankedRoleLine("Commits by role", rankedRoleTotals(data.series, (d) => d.commitsByRole ?? {}), String));
|
|
@@ -398,14 +509,10 @@ export function renderReportMarkdown(data) {
|
|
|
398
509
|
if (data.role === undefined) {
|
|
399
510
|
const mergeDone = rankedRoleTotals(data.series, (d) => d.mergedByRole ?? {});
|
|
400
511
|
lines.push(rankedRoleLine("Merged by role", mergeDone, String));
|
|
401
|
-
|
|
402
|
-
if (mergeFailures.length > 0)
|
|
403
|
-
lines.push(rankedRoleLine("Merge failures by role", mergeFailures, String));
|
|
512
|
+
pushPresentRankedLine(lines, "Merge failures by role", rankedRoleTotals(data.series, (d) => d.mergeFailedByRole ?? {}));
|
|
404
513
|
// The --since reason twin, folded across the series the same way; a reason-free window
|
|
405
514
|
// prints no line, so its output is byte-for-byte unchanged.
|
|
406
|
-
|
|
407
|
-
if (mergeFailureReasons.length > 0)
|
|
408
|
-
lines.push(rankedRoleLine("Merge failures by reason", mergeFailureReasons, String));
|
|
515
|
+
pushPresentRankedLine(lines, "Merge failures by reason", rankedRoleTotals(data.series, (d) => d.mergeFailedByReason ?? {}));
|
|
409
516
|
// Per-day median passing check time, so a suite slowing over days shows; days without a
|
|
410
517
|
// check are left out, and no line prints when no day has one (zero means absent).
|
|
411
518
|
pushDayCameoLine(lines, data.series, "Merge check median", (d) => d.checkMedianMs);
|
|
@@ -413,9 +520,17 @@ export function renderReportMarkdown(data) {
|
|
|
413
520
|
// suite went red before passing shows here. Zero-absent: no line when no day fails.
|
|
414
521
|
pushDayCameoLine(lines, data.series, "Red merge checks", (d) => d.checkFailures, (n) => `×${n}`);
|
|
415
522
|
// Per-day main red wall-clock, beside the red-check counts above: how long each day main
|
|
416
|
-
// had
|
|
523
|
+
// had its own check red (the span main had no passing full check). Team-only (main's own
|
|
417
524
|
// red time, not any role's) and zero-absent: no line when no day was red.
|
|
418
525
|
pushDayCameoLine(lines, data.series, "Main red", (d) => d.mainRedMs, durationLabel);
|
|
526
|
+
// The day twin of the --since window's totals line: the same tests ranked across the
|
|
527
|
+
// series' per-day main-red maps (Main red by test). Zero-absent: no line when no day
|
|
528
|
+
// named a failing test.
|
|
529
|
+
pushPresentRankedLine(lines, "Main red by test", rankedRoleTotals(data.series, (d) => d.mainRedByTest ?? {}));
|
|
530
|
+
// The day twin of the --since window's merges line: the same culprits ranked across the
|
|
531
|
+
// series' per-day main-red maps (Main red by merge). Zero-absent: no line when no day
|
|
532
|
+
// blamed a merge.
|
|
533
|
+
pushPresentRankedLine(lines, "Main red by merge", rankedRoleTotals(data.series, (d) => d.mainRedByCulprit ?? {}));
|
|
419
534
|
pushDayCameoLine(lines, data.series, "Merge wait median", (d) => d.mergeWaitMedianMs);
|
|
420
535
|
// How long the merge slot itself held each change it landed, beside the merge-wait median:
|
|
421
536
|
// the queue backlog (merge wait minus slot time) apart from the slot's own review/check/
|
|
@@ -429,6 +544,10 @@ export function renderReportMarkdown(data) {
|
|
|
429
544
|
pushDayCameoLine(lines, data.series, "Workspace wait median", (d) => d.slotWaitMedianMs);
|
|
430
545
|
pushDayCameoLine(lines, data.series, "Check wait median", (d) => d.checkWaitMedianMs);
|
|
431
546
|
}
|
|
547
|
+
}
|
|
548
|
+
/** Cost/commits tail: cost per commit, notional block, lag, validation gaps, sparse note. */
|
|
549
|
+
function pushReportTail(lines, data) {
|
|
550
|
+
const t = data.totals;
|
|
432
551
|
lines.push(costPerCommitLine("Cost per commit", costPerCommitPairs(data.series)));
|
|
433
552
|
// Subscription spend is notional: its own two lines, only when the window has any.
|
|
434
553
|
if ((t.notionalCostUsd ?? 0) > 0) {
|
|
@@ -466,5 +585,19 @@ export function renderReportMarkdown(data) {
|
|
|
466
585
|
// it prints — an empty or fully covered log never claims a history it cannot see.
|
|
467
586
|
if (!data.coversFullWindow)
|
|
468
587
|
lines.push("", SPARSE_WINDOW_NOTE);
|
|
588
|
+
}
|
|
589
|
+
/** Render a report as Markdown — the pinned shape the CLI prints and the TUI usage pane
|
|
590
|
+
* (report 3/3) reuses; the GUI tab (report 2/3) renders its own SVG charts from the same
|
|
591
|
+
* ReportData instead. Pure function of ReportData: no I/O, no clock reads. */
|
|
592
|
+
export function renderReportMarkdown(data) {
|
|
593
|
+
const lines = [];
|
|
594
|
+
pushReportHeader(lines, data);
|
|
595
|
+
pushReportSeriesTable(lines, data);
|
|
596
|
+
pushReportRoleLines(lines, data);
|
|
597
|
+
pushReportProviderLines(lines, data);
|
|
598
|
+
pushReportFrictionLines(lines, data);
|
|
599
|
+
pushReportDeferralLines(lines, data);
|
|
600
|
+
pushReportMergeLines(lines, data);
|
|
601
|
+
pushReportTail(lines, data);
|
|
469
602
|
return lines.join("\n");
|
|
470
603
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
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-render.ts. */
|
|
11
|
+
import { assertReportSinceMs, eventWindowCovers, readWindowEvents, sinceWindowTiming } from "../events/event-window.js";
|
|
12
|
+
import { dayKey } from "../text/datetime.js";
|
|
13
|
+
import { addTo } from "../collections.js";
|
|
14
|
+
import { nonemptyObj, roleSum, scopeBases, scopedByRole, zeroCommitCounts, zeroMergeCounts, } from "./report-fold.js";
|
|
15
|
+
import { foldUsageEvent } from "./report-usage-fold.js";
|
|
16
|
+
import { mainRedCulpritsByDay, mainRedFailingTestsByDay, mainRedMinutesByDay } from "./report-main-red.js";
|
|
17
|
+
/** Aggregate team usage over the trailing `sinceMs` window ending now (or `untilMs` ago, which
|
|
18
|
+
* drops events at or after that end; `coversFullWindow` still judges the far edge
|
|
19
|
+
* only), from the event log only (run_end/merged — see SinceReport for why the backlog files
|
|
20
|
+
* are not read). Kept a
|
|
21
|
+
* separate function rather than a `collectReport` variant flag: the day collector's
|
|
22
|
+
* zero-filled series and backlog-file reads have no place in a window totals view. */
|
|
23
|
+
export function collectReportSince(root, sinceMs, role, untilMs) {
|
|
24
|
+
assertReportSinceMs(sinceMs);
|
|
25
|
+
const { now, cutoff, end, slice } = sinceWindowTiming(sinceMs, untilMs);
|
|
26
|
+
const { raw, acc } = foldSinceWindow(root, cutoff, now, end, untilMs);
|
|
27
|
+
// The empty-window branch of the shared proof matters here: with no retained events nothing
|
|
28
|
+
// can have rotated away, and printing a rotation note over a fresh install would claim a
|
|
29
|
+
// history that never existed. When neither proof holds, the oldest retained event lies inside
|
|
30
|
+
// the window and the render notes the possible truncation.
|
|
31
|
+
const coversFullWindow = eventWindowCovers(raw, cutoff);
|
|
32
|
+
// Role scope: every map and total below derives from the fold's per-role maps for the named
|
|
33
|
+
// base roles alone — merge usage (the merge slot's, not the role's) and the team-only
|
|
34
|
+
// fields stay out, so a scoped --since report sums exactly the same events the scoped day
|
|
35
|
+
// report would. The scope is the base roles, so instances (`feature-2`) aggregate into their
|
|
36
|
+
// group as the cap does.
|
|
37
|
+
const bases = scopeBases(role);
|
|
38
|
+
return {
|
|
39
|
+
...slice,
|
|
40
|
+
...(bases === undefined ? {} : { role: bases.join(",") }),
|
|
41
|
+
totals: sinceScopedTotals(acc, bases),
|
|
42
|
+
...sinceRoleMaps(acc, bases),
|
|
43
|
+
// Main's red time is main's own (G1's rule), not any role's, so a role-scoped window
|
|
44
|
+
// carries 0 — the type's always-present default for the team-only field.
|
|
45
|
+
mainRedMs: bases === undefined
|
|
46
|
+
? [...mainRedMinutesByDay(raw.events, cutoff, end).values()].reduce((a, b) => a + b, 0)
|
|
47
|
+
: 0,
|
|
48
|
+
...nonemptyObj("mainRedByTest", sinceMainRedByTest(raw, cutoff, end, bases)),
|
|
49
|
+
...nonemptyObj("mainRedByCulprit", sinceMainRedByCulprit(raw, cutoff, end, bases)),
|
|
50
|
+
coversFullWindow,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** The window's event read and fold, one function so the read happens exactly once per window
|
|
54
|
+
* and every downstream builder — the coverage proof and the main-red by-test/by-culprit maps
|
|
55
|
+
* — consumes the same `raw`. `raw` is the one day-keyed log read the whole report is built on
|
|
56
|
+
* (its `events` feed both the `acc` fold below and the red maps), so its retention-backed
|
|
57
|
+
* coverage and its event set cannot disagree across the builders. */
|
|
58
|
+
function foldSinceWindow(root, cutoff, now, end, untilMs) {
|
|
59
|
+
// The window key is the cutoff's local calendar day, from the shared dayKey helper
|
|
60
|
+
// eventDayKey buckets events with, so the read's day keys cannot disagree with the ts
|
|
61
|
+
// filter below; the day-keyed read may include earlier hours of that day, which the ts
|
|
62
|
+
// filter removes (over-read is at most one day's events). Future-dated events are dropped
|
|
63
|
+
// here too, matching collectReport (its day map only holds the window's days).
|
|
64
|
+
const fromKey = dayKey(cutoff);
|
|
65
|
+
const raw = readWindowEvents(root, fromKey);
|
|
66
|
+
const acc = {
|
|
67
|
+
runs: 0,
|
|
68
|
+
runsByRole: {},
|
|
69
|
+
rejectionsByRole: {},
|
|
70
|
+
postMergeRejectionsByRole: {},
|
|
71
|
+
revisionsByRole: {},
|
|
72
|
+
refusalsByRole: {},
|
|
73
|
+
checkDurationsMs: [],
|
|
74
|
+
checkFailures: 0,
|
|
75
|
+
mergeWaitsMs: [],
|
|
76
|
+
mergeDurationsMs: [],
|
|
77
|
+
runDurationsMs: [],
|
|
78
|
+
providerWaitsMs: [],
|
|
79
|
+
slotWaitsMs: [],
|
|
80
|
+
checkWaitsMs: [],
|
|
81
|
+
reviewDurationsMs: [],
|
|
82
|
+
failedRunsByRole: {},
|
|
83
|
+
fallbacksByRole: {},
|
|
84
|
+
tokensByRole: {},
|
|
85
|
+
promptTokensByRole: {},
|
|
86
|
+
cacheReadTokensByRole: {},
|
|
87
|
+
costByRole: {},
|
|
88
|
+
tokensOut: 0,
|
|
89
|
+
...zeroCommitCounts(),
|
|
90
|
+
costUsd: 0,
|
|
91
|
+
notionalCostUsd: 0,
|
|
92
|
+
notionalByRole: {},
|
|
93
|
+
...zeroMergeCounts(),
|
|
94
|
+
mergedByRole: {},
|
|
95
|
+
mergeFailedByRole: {},
|
|
96
|
+
mergeFailedByReason: {},
|
|
97
|
+
costByProvider: {},
|
|
98
|
+
notionalByProvider: {},
|
|
99
|
+
notionalByBackend: {},
|
|
100
|
+
runsByModel: {},
|
|
101
|
+
deferredByReason: {},
|
|
102
|
+
};
|
|
103
|
+
for (const ev of raw.events) {
|
|
104
|
+
if (typeof ev.ts !== "number" || ev.ts < cutoff || ev.ts > now || (untilMs !== undefined && ev.ts >= end))
|
|
105
|
+
continue;
|
|
106
|
+
foldUsageEvent(acc, ev);
|
|
107
|
+
}
|
|
108
|
+
return { raw, acc };
|
|
109
|
+
}
|
|
110
|
+
/** The window's `totals` group under both scope rules, named so the team-vs-role branch reads
|
|
111
|
+
* as one function instead of a ternary buried in the return: the team figure sums the merge-slot
|
|
112
|
+
* fields in (the merge slot's dash tokens/cost are the same team spend the budget charges), while
|
|
113
|
+
* the role branch sums just the base roles through `roleSum` and zeroes the merge counts — a
|
|
114
|
+
* role's merge usage is the merge slot's, not the role's. */
|
|
115
|
+
function sinceScopedTotals(acc, bases) {
|
|
116
|
+
return bases === undefined
|
|
117
|
+
? {
|
|
118
|
+
tokensOut: acc.tokensOut + (acc.mergeTokens ?? 0),
|
|
119
|
+
runs: acc.runs ?? 0,
|
|
120
|
+
commits: acc.commits,
|
|
121
|
+
costUsd: acc.costUsd + (acc.mergeCostUsd ?? 0),
|
|
122
|
+
...(acc.notionalCostUsd > 0 ? { notionalCostUsd: acc.notionalCostUsd } : {}),
|
|
123
|
+
...nonemptyObj("notionalByBackend", acc.notionalByBackend),
|
|
124
|
+
mergeRuns: acc.mergeRuns ?? 0,
|
|
125
|
+
mergeTokens: acc.mergeTokens ?? 0,
|
|
126
|
+
mergeCostUsd: acc.mergeCostUsd ?? 0,
|
|
127
|
+
...((acc.mergeFailed ?? 0) > 0 ? { mergeFailed: acc.mergeFailed } : {}),
|
|
128
|
+
}
|
|
129
|
+
: (() => {
|
|
130
|
+
// The role's own notional spend, from its run_end events; the merge slot's
|
|
131
|
+
// notional is team-wide and stays out like its cost.
|
|
132
|
+
const notional = roleSum(acc.notionalByRole, bases);
|
|
133
|
+
return {
|
|
134
|
+
tokensOut: roleSum(acc.tokensByRole, bases),
|
|
135
|
+
runs: roleSum(acc.runsByRole, bases),
|
|
136
|
+
commits: roleSum(acc.commitsByRole, bases),
|
|
137
|
+
costUsd: roleSum(acc.costByRole, bases),
|
|
138
|
+
...(notional > 0 ? { notionalCostUsd: notional } : {}),
|
|
139
|
+
...zeroMergeCounts(),
|
|
140
|
+
};
|
|
141
|
+
})();
|
|
142
|
+
}
|
|
143
|
+
/** Flatten a per-day main-red record map (day → label → count) into one flat label→count map,
|
|
144
|
+
* the shared body of sinceMainRedByTest and sinceMainRedByCulprit. Main's red state is main's
|
|
145
|
+
* own, not any role's, so a role-scoped window carries none — the empty map `nonemptyObj` then
|
|
146
|
+
* omits from the JSON, keeping a red-free --since byte-identical to today. */
|
|
147
|
+
function flattenMainRedByDay(perDay, bases) {
|
|
148
|
+
if (bases !== undefined)
|
|
149
|
+
return {};
|
|
150
|
+
const flat = {};
|
|
151
|
+
for (const record of perDay.values()) {
|
|
152
|
+
for (const [label, n] of Object.entries(record))
|
|
153
|
+
addTo(flat, label, n);
|
|
154
|
+
}
|
|
155
|
+
return flat;
|
|
156
|
+
}
|
|
157
|
+
/** Main's red failing tests over the window, flattened across the per-day records of the same
|
|
158
|
+
* red `main_check` events the red time uses (G1's rule) into one map. Main's failing tests are
|
|
159
|
+
* main's own, not any role's, so a role-scoped window carries none (see flattenMainRedByDay). */
|
|
160
|
+
function sinceMainRedByTest(raw, cutoff, end, bases) {
|
|
161
|
+
return flattenMainRedByDay(mainRedFailingTestsByDay(raw.events, cutoff, end), bases);
|
|
162
|
+
}
|
|
163
|
+
/** Main's red culprits over the window, flattened across the per-day records of the same red
|
|
164
|
+
* `main_culprit` events the live `mainred` alert's `red.culprit` reads (the historical twin)
|
|
165
|
+
* into one map. Main's own blame, not any role's, so a role-scoped window carries none (see
|
|
166
|
+
* flattenMainRedByDay). */
|
|
167
|
+
function sinceMainRedByCulprit(raw, cutoff, end, bases) {
|
|
168
|
+
return flattenMainRedByDay(mainRedCulpritsByDay(raw.events, cutoff, end), bases);
|
|
169
|
+
}
|
|
170
|
+
/** The `SinceReport` return's role-scoped map group — every field a reader names when asking
|
|
171
|
+
* "which fields are team-only?": each `scopedByRole` call applies the base-role scope (the
|
|
172
|
+
* rule `collectReportSince`'s `bases` comment spells), so a role-scoped window shows exactly
|
|
173
|
+
* the maps the scoped day report would, and the team-only fields (providers, backends, models,
|
|
174
|
+
* deferred reasons) carry full maps for the team or empty maps for a scoped role. */
|
|
175
|
+
function sinceRoleMaps(acc, bases) {
|
|
176
|
+
// The --since render/JSON's Notional by role line is conditional on the window's own
|
|
177
|
+
// notional spend, scoped to base when a role is given — the same conditional the day
|
|
178
|
+
// collector applies to ReportDay.notionalByRole (line ~573), so a role-scoped window
|
|
179
|
+
// whose role spent no notional carries no notionalByRole instead of an empty map.
|
|
180
|
+
const notionalByRole = scopedByRole(acc.notionalByRole, bases);
|
|
181
|
+
// Deferred runs are bucketed by a reason code (not a role), so a role-scoped window
|
|
182
|
+
// carries no entries — the same team-only rule runsByModel follows. Only set when the
|
|
183
|
+
// window actually deferred (zero means absent), so a deferral-free --since JSON omits it.
|
|
184
|
+
const deferred = scopedByRole(acc.deferredByReason, bases);
|
|
185
|
+
return {
|
|
186
|
+
runsByRole: scopedByRole(acc.runsByRole, bases),
|
|
187
|
+
costByRole: scopedByRole(acc.costByRole, bases),
|
|
188
|
+
rejectionsByRole: scopedByRole(acc.rejectionsByRole, bases),
|
|
189
|
+
postMergeRejectionsByRole: scopedByRole(acc.postMergeRejectionsByRole, bases),
|
|
190
|
+
revisionsByRole: scopedByRole(acc.revisionsByRole, bases),
|
|
191
|
+
refusalsByRole: scopedByRole(acc.refusalsByRole, bases),
|
|
192
|
+
failedRunsByRole: scopedByRole(acc.failedRunsByRole, bases),
|
|
193
|
+
fallbacksByRole: scopedByRole(acc.fallbacksByRole, bases),
|
|
194
|
+
tokensByRole: scopedByRole(acc.tokensByRole, bases),
|
|
195
|
+
...nonemptyObj("promptTokensByRole", scopedByRole(acc.promptTokensByRole, bases)),
|
|
196
|
+
...nonemptyObj("cacheReadTokensByRole", scopedByRole(acc.cacheReadTokensByRole, bases)),
|
|
197
|
+
commitsByRole: scopedByRole(acc.commitsByRole, bases),
|
|
198
|
+
mergedByRole: scopedByRole(acc.mergedByRole, bases),
|
|
199
|
+
mergeFailedByRole: scopedByRole(acc.mergeFailedByRole, bases),
|
|
200
|
+
// A result code is not a role, so a role-scoped window carries an empty map (a code
|
|
201
|
+
// never baseRoleOf to the role) — the render gates the reason line to the team report.
|
|
202
|
+
mergeFailedByReason: scopedByRole(acc.mergeFailedByReason, bases),
|
|
203
|
+
...nonemptyObj("notionalByRole", notionalByRole),
|
|
204
|
+
// Provider spend is a team attribution (a provider is not a role), so a role-scoped
|
|
205
|
+
// window carries empty maps — providers never baseRoleOf to the role — and the render
|
|
206
|
+
// gates the provider lines to the team report anyway. Always present, empty when none.
|
|
207
|
+
costByProvider: scopedByRole(acc.costByProvider, bases),
|
|
208
|
+
notionalByProvider: scopedByRole(acc.notionalByProvider, bases),
|
|
209
|
+
// Backend spend is a team attribution (a backend is not a role), so a role-scoped window
|
|
210
|
+
// omits it — the same absent-when-empty rule the day collector applies.
|
|
211
|
+
...nonemptyObj("notionalByBackend", scopedByRole(acc.notionalByBackend, bases)),
|
|
212
|
+
// Model attribution is team-only (a model is not a role, so no model folds to the
|
|
213
|
+
// scoped role), the same full-map-for-team / empty-for-scoped rule costByProvider
|
|
214
|
+
// follows. Always present so the render can gate on the window's own events.
|
|
215
|
+
runsByModel: scopedByRole(acc.runsByModel, bases),
|
|
216
|
+
...nonemptyObj("deferredByReason", deferred),
|
|
217
|
+
};
|
|
218
|
+
}
|