tumwater 0.3.0 → 0.4.1
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 +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +20 -0
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-stream.js +170 -166
- 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 +4 -4
- package/dist/src/agent/codex/codex-rollout.js +10 -21
- package/dist/src/agent/codex/codex-stream.js +202 -179
- 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/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +4 -4
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +97 -88
- package/dist/src/agent/run-agent.js +66 -381
- package/dist/src/agent/run-settle.js +3 -2
- package/dist/src/agent/run-spawn.js +235 -0
- package/dist/src/agent/run-stream.js +193 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +88 -36
- package/dist/src/backlog/backlog-cli-view.js +92 -98
- package/dist/src/backlog/backlog-cli.js +9 -64
- package/dist/src/backlog/backlog-commit.js +17 -9
- package/dist/src/backlog/backlog-eligibility.js +45 -27
- 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 +1 -1
- package/dist/src/backlog/backlog-parts.js +23 -13
- package/dist/src/backlog/backlog-release.js +43 -33
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +0 -144
- package/dist/src/backlog/backlog-write.js +30 -17
- package/dist/src/backlog/backlog.js +9 -0
- package/dist/src/backlog/question-answer.js +3 -1
- package/dist/src/baseline/main-baseline.js +50 -15
- package/dist/src/baseline/main-guardrails.js +29 -78
- package/dist/src/baseline/main-red.js +64 -103
- package/dist/src/baseline/main-watch.js +89 -35
- package/dist/src/brief/brief-edit.js +3 -1
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +330 -0
- package/dist/src/budget/budget-wiring.js +167 -0
- 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/dep-install.js +64 -1
- package/dist/src/cli/cli-args.js +72 -0
- package/dist/src/cli/cli-flag-specs.js +38 -0
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +51 -41
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +140 -58
- package/dist/src/cli/cli-start-summary.js +10 -3
- package/dist/src/cli/cli-start.js +92 -38
- package/dist/src/cli/completion.js +21 -1
- package/dist/src/cli/config-commands.js +17 -8
- package/dist/src/cli/help.js +84 -16
- package/dist/src/cli/log-commands.js +137 -105
- package/dist/src/cli/question-commands.js +89 -64
- package/dist/src/cli.js +408 -286
- package/dist/src/collections.js +56 -6
- package/dist/src/config/config-editable-keys.js +3 -0
- package/dist/src/config/config-fallbacks.js +3 -2
- 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 +51 -241
- package/dist/src/config/config-views.js +10 -2
- package/dist/src/config/config-write.js +209 -140
- package/dist/src/config/config.js +9 -3
- package/dist/src/doctor/doctor-backlog-bugs.js +96 -53
- package/dist/src/doctor/doctor-backlog-plans.js +222 -88
- package/dist/src/doctor/doctor-backlog.js +171 -32
- package/dist/src/doctor/doctor-check-names.js +7 -1
- package/dist/src/doctor/doctor-checks.js +26 -19
- package/dist/src/doctor/doctor-claude-checks.js +6 -9
- package/dist/src/doctor/doctor-codex-checks.js +15 -29
- package/dist/src/doctor/doctor-contention-checks.js +293 -134
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +13 -15
- package/dist/src/doctor/doctor-model-checks.js +45 -200
- package/dist/src/doctor/doctor-notify.js +7 -2
- package/dist/src/doctor/doctor-opencode-checks.js +23 -9
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +108 -13
- package/dist/src/doctor/doctor-role-notes.js +51 -0
- package/dist/src/doctor/doctor.js +17 -5
- package/dist/src/doctor/probe.js +14 -1
- package/dist/src/events/event-format.js +313 -157
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-read.js +13 -0
- package/dist/src/events/event-window.js +20 -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 +127 -90
- package/dist/src/failure/failure-render.js +62 -21
- package/dist/src/failure/failure-state-change.js +174 -8
- package/dist/src/failure/time-spend.js +136 -167
- package/dist/src/files/file-queue.js +17 -0
- package/dist/src/files/files.js +7 -0
- package/dist/src/gates/budget-gates.js +116 -324
- package/dist/src/gates/gate-polls.js +123 -184
- package/dist/src/gates/maintenance-quota.js +27 -17
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/commit-message.js +12 -0
- package/dist/src/git/git.js +18 -0
- package/dist/src/git/slot-salvage.js +4 -5
- 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 +1 -1
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +21 -12
- package/dist/src/gui/gui-backlog-endpoints.js +6 -5
- package/dist/src/gui/gui-command.js +19 -1
- package/dist/src/gui/gui-config-commands.js +126 -85
- package/dist/src/gui/gui-endpoint-commands.js +5 -3
- 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 +175 -10
- package/dist/src/gui/gui-prompt-commands.js +16 -7
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +14 -4
- package/dist/src/gui/gui-server.js +20 -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-queued.js +8 -0
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +3 -6
- package/dist/src/inbox/inbox-top.js +4 -9
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +189 -161
- package/dist/src/init/init.js +43 -17
- package/dist/src/merge/merge-core.js +2 -25
- package/dist/src/merge/merge-drain.js +23 -136
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/merge-pipeline.js +1 -1
- package/dist/src/merge/merge-post-review-drain.js +214 -0
- package/dist/src/merge/merge-queue-render.js +11 -12
- package/dist/src/merge/merge-queue.js +24 -2
- package/dist/src/merge/merge-requeue.js +1 -0
- package/dist/src/merge/merge-stack.js +1 -7
- package/dist/src/merge/merge-to-main.js +2 -10
- package/dist/src/merge/merge-top.js +33 -19
- package/dist/src/merge/merge-verify.js +65 -9
- package/dist/src/notes-cli.js +8 -3
- package/dist/src/operator/operator-commands.js +16 -5
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +49 -17
- package/dist/src/orchestrator/orchestrator-scheduling.js +4 -2
- package/dist/src/orchestrator/orchestrator.js +6 -5
- package/dist/src/orchestrator/scheduling-scope.js +51 -7
- package/dist/src/orchestrator/scheduling-skip.js +15 -5
- package/dist/src/paths.js +26 -10
- package/dist/src/pi/pi-backend.js +2 -2
- 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 +65 -49
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +1 -1
- package/dist/src/process/run-marker.js +10 -2
- package/dist/src/process/supervisor.js +7 -4
- package/dist/src/prompt/gate-notes.js +35 -10
- package/dist/src/prompt/gate-prompts.js +30 -7
- package/dist/src/prompt/notes-backlog.js +24 -15
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt.js +13 -7
- 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 +322 -223
- package/dist/src/report/report-fold.js +257 -191
- package/dist/src/report/report-main-red.js +64 -0
- package/dist/src/report/report-render.js +133 -25
- package/dist/src/report/report.js +2 -5
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +32 -1
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +159 -24
- package/dist/src/review/protected-edits.js +22 -7
- package/dist/src/review/quarantine-render.js +82 -0
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/review/review.js +33 -49
- package/dist/src/roles/role-catalog.js +73 -2
- package/dist/src/roles/role-guidance.js +18 -0
- package/dist/src/roles/role-labels.js +6 -3
- package/dist/src/roles/role-render.js +62 -8
- package/dist/src/roles/role-view.js +54 -12
- package/dist/src/roles/roles.js +3 -23
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +50 -15
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +36 -16
- package/dist/src/run/run-detail-render.js +27 -9
- package/dist/src/run/run-finalize.js +76 -46
- package/dist/src/run/run-prompt.js +231 -177
- package/dist/src/run/run-stage.js +168 -70
- package/dist/src/run/run-verdict.js +127 -13
- package/dist/src/run/stage-backlog-checks.js +2 -2
- package/dist/src/run/stage-check.js +17 -11
- package/dist/src/run/stage-checks-shared.js +80 -0
- package/dist/src/run/stage-name-checks.js +194 -0
- package/dist/src/run/stage-path-checks.js +35 -537
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +61 -21
- package/dist/src/runner/revision.js +14 -25
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +4 -2
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-recovery.js +18 -3
- package/dist/src/runner/runner-run.js +36 -29
- package/dist/src/runner/runner-state.js +4 -3
- package/dist/src/scheduling/claim-entries.js +49 -0
- package/dist/src/scheduling/claims.js +1 -1
- package/dist/src/status/status-data.js +144 -86
- package/dist/src/status/status-polls.js +56 -5
- package/dist/src/status/status-role-notes.js +49 -0
- package/dist/src/status/status-roles.js +19 -2
- package/dist/src/team/reclaim.js +2 -2
- package/dist/src/text/datetime.js +3 -2
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +22 -5
- package/dist/src/text/text.js +11 -0
- package/dist/src/ui/alert-feeds.js +320 -0
- package/dist/src/ui/badges.js +35 -2
- package/dist/src/ui/gui/gui-client-backlog.js +25 -3
- package/dist/src/ui/gui/gui-client-boot.js +102 -74
- package/dist/src/ui/gui/gui-client-drawer-config.js +64 -17
- package/dist/src/ui/gui/gui-client-drawer.js +46 -5
- package/dist/src/ui/gui/gui-client-feed.js +4 -9
- package/dist/src/ui/gui/gui-client-history.js +90 -88
- package/dist/src/ui/gui/gui-client-labels.js +5 -1
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +11 -11
- package/dist/src/ui/gui/gui-client-pending.js +189 -12
- package/dist/src/ui/gui/gui-client-report-chart.js +137 -10
- package/dist/src/ui/gui/gui-client-report.js +81 -6
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +138 -4
- package/dist/src/ui/gui/gui-client-team.js +13 -2
- package/dist/src/ui/gui/gui-client.js +44 -5
- package/dist/src/ui/gui/gui-page.js +1 -0
- package/dist/src/ui/gui/gui-styles.js +4 -0
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +4 -3
- package/dist/src/ui/status-model.js +24 -14
- package/dist/src/ui/status-payload.js +168 -117
- package/dist/src/ui/status-render.js +140 -81
- package/dist/src/ui/team-alert-backlog-feed.js +181 -0
- package/dist/src/ui/team-alert-backlog.js +267 -0
- package/dist/src/ui/team-alert-merge.js +183 -0
- package/dist/src/ui/team-alert-shared.js +79 -0
- package/dist/src/ui/team-alert-status.js +321 -0
- package/dist/src/ui/team-alert-tail.js +251 -0
- package/dist/src/ui/team-alerts.js +31 -513
- package/dist/src/ui/tone.js +7 -0
- package/dist/src/ui/tui/tui-backlog.js +8 -6
- package/dist/src/ui/tui/tui-editor-attach.js +1 -7
- package/dist/src/ui/tui/tui-editor-backends.js +1 -6
- package/dist/src/ui/tui/tui-editor-brief.js +4 -24
- package/dist/src/ui/tui/tui-editor-budget.js +10 -12
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +1 -0
- package/dist/src/ui/tui/tui-editor-config.js +19 -6
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +3 -17
- package/dist/src/ui/tui/tui-editor-instances.js +9 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +51 -127
- package/dist/src/ui/tui/tui-editor-interval.js +51 -113
- package/dist/src/ui/tui/tui-editor-notes.js +29 -103
- package/dist/src/ui/tui/tui-editor-pause.js +2 -1
- package/dist/src/ui/tui/tui-editor-prompt.js +2 -1
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -13
- package/dist/src/ui/tui/tui-editor-release.js +77 -0
- package/dist/src/ui/tui/tui-editor-retire.js +1 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-file.js +75 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +3 -7
- package/dist/src/ui/tui/tui-editor-session.js +44 -0
- package/dist/src/ui/tui/tui-editor-slots.js +1 -0
- package/dist/src/ui/tui/tui-editors.js +13 -1
- package/dist/src/ui/tui/tui-frame.js +23 -7
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +14 -0
- package/dist/src/ui/tui/tui-pane-activity.js +21 -8
- package/dist/src/ui/tui/tui-pane-backlog.js +36 -9
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +153 -3
- package/dist/src/ui/tui/tui-pane-history.js +308 -228
- package/dist/src/ui/tui/tui-pane-pending.js +285 -56
- package/dist/src/ui/tui/tui-pane-queued.js +7 -2
- package/dist/src/ui/tui/tui-pane-report.js +9 -3
- package/dist/src/ui/tui/tui-pane-session.js +12 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +12 -8
- package/dist/src/ui/tui/tui-pane.js +16 -10
- package/dist/src/ui/tui/tui-panes.js +247 -134
- package/dist/src/ui/tui/tui.js +8 -5
- package/dist/src/verdict/fix-claim.js +47 -1
- package/dist/src/verdict/refusal.js +2 -1
- package/dist/src/verdict/reply-contract.js +35 -0
- package/package.json +1 -1
- package/dist/src/baseline/main-culprit.js +0 -94
- package/dist/src/baseline/main-revert.js +0 -133
- package/dist/src/merge/merge-post-review.js +0 -90
- /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
|
|
@@ -179,6 +194,11 @@ export function renderSinceReportMarkdown(data) {
|
|
|
179
194
|
// resource the budget charges, so this is the line an operator reads to find which role's
|
|
180
195
|
// prompt loop burned the window's tokens.
|
|
181
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 ?? {})));
|
|
182
202
|
// Same breakdown for spend: ranked by spend desc then name asc, zero-spend roles omitted —
|
|
183
203
|
// an all-zero window renders "-" like the runs line.
|
|
184
204
|
pushRankedRoleLine(lines, "Cost by role", rankedRoleMap(data.costByRole), usd);
|
|
@@ -202,12 +222,27 @@ export function renderSinceReportMarkdown(data) {
|
|
|
202
222
|
// a window main was never red.
|
|
203
223
|
if (data.mainRedMs > 0)
|
|
204
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
|
+
}
|
|
205
239
|
// The same breakdown for review friction: rejections per role from review_rejected events;
|
|
206
|
-
// a window with none renders "-" like the other lines.
|
|
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.
|
|
207
242
|
pushRankedRoleLine(lines, "Rejections by role", rankedRoleMap(data.rejectionsByRole));
|
|
243
|
+
pushRankedRoleLine(lines, "Post-merge rejections by role", rankedRoleMap(data.postMergeRejectionsByRole));
|
|
208
244
|
pushRankedRoleLine(lines, "Revisions by role", rankedRoleMap(data.revisionsByRole));
|
|
209
245
|
pushRankedRoleLine(lines, "Refusals by role", rankedRoleMap(data.refusalsByRole));
|
|
210
|
-
pushRankedRoleLine(lines, "Reverts by role", rankedRoleMap(data.revertsByRole));
|
|
211
246
|
// The same breakdown for outright failures: error-class run_ends per role under the
|
|
212
247
|
// digest's OUTCOME_CLASS — the runs that burned agent time and merged nothing without
|
|
213
248
|
// going through review, which rejections/revisions would miss entirely. A window with
|
|
@@ -245,10 +280,18 @@ export function renderSinceReportMarkdown(data) {
|
|
|
245
280
|
if (mergeFailureReasons.length > 0)
|
|
246
281
|
lines.push(rankedRoleLine("Merge failures by reason", mergeFailureReasons, String));
|
|
247
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)));
|
|
248
288
|
// Subscription spend is notional: its own line, only when the window has any — the same
|
|
249
289
|
// conditional/filtered `usd` form the day render's "Estimated by role" uses.
|
|
250
290
|
if (data.notionalByRole !== undefined) {
|
|
251
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)));
|
|
252
295
|
}
|
|
253
296
|
lines.push("");
|
|
254
297
|
// Exactly one final line, chosen by the collector's coverage proof: either a hedged note
|
|
@@ -264,22 +307,21 @@ export function renderSinceReportMarkdown(data) {
|
|
|
264
307
|
lines.push("backlog tallies (features done / bugs fixed / plans added / bugs filed) need the day report (--days)");
|
|
265
308
|
return lines.join("\n");
|
|
266
309
|
}
|
|
267
|
-
/**
|
|
268
|
-
*
|
|
269
|
-
|
|
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
|
+
/** Rank one cost map against a base-keyed commits map into cost-per-commit output: the shared
|
|
319
|
+
* core of the day and --since cost-per-commit lines. `cost` is expected base-role keyed (the
|
|
320
|
+
* day caller folds per day, the --since caller via `foldBaseRoles`); commits are base-role
|
|
321
|
+
* keyed. `merged` is [role, cost per commit, commits] for roles with both, ranked by cost per
|
|
270
322
|
* commit desc then name asc; `idle` is roles that spent and merged nothing, ranked by spend.
|
|
271
|
-
*
|
|
272
|
-
|
|
273
|
-
* role with commits but no spend in the picked map appears in neither list. */
|
|
274
|
-
function costPerCommitPairs(series, pick = (d) => d.costByRole) {
|
|
275
|
-
const cost = {};
|
|
276
|
-
const commits = {};
|
|
277
|
-
for (const d of series) {
|
|
278
|
-
for (const [id, c] of Object.entries(pick(d)))
|
|
279
|
-
addTo(cost, baseRoleOf(id), c);
|
|
280
|
-
for (const [role, n] of Object.entries(d.commitsByRole ?? {}))
|
|
281
|
-
addTo(commits, role, n);
|
|
282
|
-
}
|
|
323
|
+
* A role with commits but no spend appears in neither list. */
|
|
324
|
+
function costPerCommitPairsFrom(cost, commits) {
|
|
283
325
|
const mergeDone = [];
|
|
284
326
|
const idleCost = [];
|
|
285
327
|
for (const [role, c] of Object.entries(cost)) {
|
|
@@ -294,6 +336,22 @@ function costPerCommitPairs(series, pick = (d) => d.costByRole) {
|
|
|
294
336
|
sortByValueDesc(mergeDone);
|
|
295
337
|
return { mergeDone, idle: rankCountEntries(idleCost) };
|
|
296
338
|
}
|
|
339
|
+
/** The day caller of the cost-per-commit core: fold each day's role-id-keyed spend map through
|
|
340
|
+
* its base role (so `feature-2` joins `feature`) and aggregate the base-keyed commitsByRole,
|
|
341
|
+
* then hand both to `costPerCommitPairsFrom`. `pick` selects the per-role-id spend map; merge
|
|
342
|
+
* spend (the reviewer's) and notional subscription spend stay out by default, matching
|
|
343
|
+
* `**Cost by role:**`. */
|
|
344
|
+
function costPerCommitPairs(series, pick = (d) => d.costByRole) {
|
|
345
|
+
const cost = {};
|
|
346
|
+
const commits = {};
|
|
347
|
+
for (const d of series) {
|
|
348
|
+
for (const [id, c] of Object.entries(pick(d)))
|
|
349
|
+
addTo(cost, baseRoleOf(id), c);
|
|
350
|
+
for (const [role, n] of Object.entries(d.commitsByRole ?? {}))
|
|
351
|
+
addTo(commits, role, n);
|
|
352
|
+
}
|
|
353
|
+
return costPerCommitPairsFrom(cost, commits);
|
|
354
|
+
}
|
|
297
355
|
/** One `**<label>:** …` line from costPerCommitPairs output; `-` when there is nothing. */
|
|
298
356
|
function costPerCommitLine(label, pairs) {
|
|
299
357
|
const parts = [
|
|
@@ -302,11 +360,8 @@ function costPerCommitLine(label, pairs) {
|
|
|
302
360
|
];
|
|
303
361
|
return `**${label}:** ${parts.length === 0 ? "-" : parts.join(" · ")}`;
|
|
304
362
|
}
|
|
305
|
-
/**
|
|
306
|
-
|
|
307
|
-
* ReportData instead. Pure function of ReportData: no I/O, no clock reads. */
|
|
308
|
-
export function renderReportMarkdown(data) {
|
|
309
|
-
const lines = [];
|
|
363
|
+
/** The title, window line, optional role line, then the report summary line. */
|
|
364
|
+
function pushReportHeader(lines, data) {
|
|
310
365
|
lines.push("# tumwater usage report");
|
|
311
366
|
lines.push("");
|
|
312
367
|
lines.push(reportWindow(data.from, data.to, data.days, eventsRotationLabel()));
|
|
@@ -324,6 +379,9 @@ export function renderReportMarkdown(data) {
|
|
|
324
379
|
: "";
|
|
325
380
|
pushReportSummary(lines, t, tail, data.role !== undefined);
|
|
326
381
|
lines.push("");
|
|
382
|
+
}
|
|
383
|
+
/** The day-by-day window table (day, tokens out, runs, commits, cost) and its blank line. */
|
|
384
|
+
function pushReportSeriesTable(lines, data) {
|
|
327
385
|
const maxTokens = data.series.reduce((m, d) => Math.max(m, d.tokensOut), 0);
|
|
328
386
|
lines.push(...markdownTable(["day", "tokens out", "runs", "commits", "cost"], data.series.map((d) => {
|
|
329
387
|
const runs = Object.values(d.runsByRole).reduce((a, b) => a + b, 0);
|
|
@@ -332,16 +390,26 @@ export function renderReportMarkdown(data) {
|
|
|
332
390
|
return [monthDay(d.date), `${compactTokens(d.tokensOut)}${bar}`, String(runs), String(d.commits), usd(d.costUsd)];
|
|
333
391
|
}), ["left", "right", "right", "right", "right"]));
|
|
334
392
|
lines.push("");
|
|
393
|
+
}
|
|
394
|
+
/** The per-role runs/tokens/prompt/cost breakdown lines. */
|
|
395
|
+
function pushReportRoleLines(lines, data) {
|
|
335
396
|
lines.push(rankedRoleLine("Runs by role", rankedRoleTotals(data.series, (d) => d.runsByRole), String));
|
|
336
397
|
// The same breakdown for output tokens (the resource the budget charges): window totals per
|
|
337
398
|
// role from tokensByRole, ranked like the runs line. Zero-token roles are omitted; an
|
|
338
399
|
// all-zero window renders "-" like the runs line.
|
|
339
400
|
pushRankedRoleLine(lines, "Tokens by role", rankedRoleTotals(data.series, (d) => d.tokensByRole ?? {}), compactTokens);
|
|
401
|
+
// The input side of that same bill, straight after the output-token line: each role's prompt
|
|
402
|
+
// totals and, when some run reported a cache read, the share of them served from cache — so
|
|
403
|
+
// the same report that says who burned output tokens also says whose prompts are cached.
|
|
404
|
+
lines.push(promptTokensLine(rankedRoleTotals(data.series, (d) => d.promptTokensByRole ?? {}), rankedRoleTotals(data.series, (d) => d.cacheReadTokensByRole ?? {})));
|
|
340
405
|
// The same breakdown for spend: window totals per role from costByRole, ranked by spend
|
|
341
406
|
// desc then name asc — spend is what the operator acts on (an operator tuning per-role
|
|
342
407
|
// intervals wants the burning role first, not the busiest one). Zero-spend roles are
|
|
343
408
|
// omitted; an all-zero window renders "-" like the runs line.
|
|
344
409
|
pushRankedRoleLine(lines, "Cost by role", rankedRoleTotals(data.series, (d) => d.costByRole), usd);
|
|
410
|
+
}
|
|
411
|
+
/** The team-only per-provider and per-model breakdown lines. */
|
|
412
|
+
function pushReportProviderLines(lines, data) {
|
|
345
413
|
// The same breakdown per PROVIDER (plans: per-provider spend): which seam of a mixed team
|
|
346
414
|
// spent what, straight from the events' providerCost maps (run_end's own runs and the
|
|
347
415
|
// merge's reviewer seam). Gated to the team report — a provider is not a role, so a
|
|
@@ -360,12 +428,16 @@ export function renderReportMarkdown(data) {
|
|
|
360
428
|
if (modelPairs.length > 0)
|
|
361
429
|
lines.push(rankedRoleLine("Runs by model", modelPairs, String));
|
|
362
430
|
}
|
|
431
|
+
}
|
|
432
|
+
/** The per-role review-friction and outright-failure lines. */
|
|
433
|
+
function pushReportFrictionLines(lines, data) {
|
|
363
434
|
// The same breakdown for review friction: window rejections per role from review_rejected
|
|
364
|
-
// events, ranked like the runs line; a window with none renders "-".
|
|
435
|
+
// events, ranked like the runs line; a window with none renders "-". The post-merge line
|
|
436
|
+
// is the afterMerge: true subset — rejections that reached an already-merged commit.
|
|
365
437
|
pushRankedRoleLine(lines, "Rejections by role", rankedRoleTotals(data.series, (d) => d.rejectionsByRole ?? {}));
|
|
438
|
+
pushRankedRoleLine(lines, "Post-merge rejections by role", rankedRoleTotals(data.series, (d) => d.postMergeRejectionsByRole ?? {}));
|
|
366
439
|
pushRankedRoleLine(lines, "Revisions by role", rankedRoleTotals(data.series, (d) => d.revisionsByRole ?? {}));
|
|
367
440
|
pushRankedRoleLine(lines, "Refusals by role", rankedRoleTotals(data.series, (d) => d.refusalsByRole ?? {}));
|
|
368
|
-
pushRankedRoleLine(lines, "Reverts by role", rankedRoleTotals(data.series, (d) => d.revertsByRole ?? {}));
|
|
369
441
|
// The same breakdown for outright failures: error-class run_ends per role under the
|
|
370
442
|
// digest's OUTCOME_CLASS — runs that burned agent time and merged nothing without going
|
|
371
443
|
// through review, which rejections/revisions would miss entirely. Zero-means-absent, so a
|
|
@@ -373,6 +445,9 @@ export function renderReportMarkdown(data) {
|
|
|
373
445
|
pushRankedRoleLine(lines, "Failed runs by role", rankedRoleTotals(data.series, (d) => d.failedRunsByRole ?? {}));
|
|
374
446
|
pushRankedRoleLine(lines, "Fallbacks by role", rankedRoleTotals(data.series, (d) => d.fallbacksByRole ?? {}));
|
|
375
447
|
pushRankedRoleLine(lines, "Image reroutes by role", rankedRoleTotals(data.series, (d) => d.imageReroutesByRole ?? {}));
|
|
448
|
+
}
|
|
449
|
+
/** The team-only deferred-runs-by-reason line. */
|
|
450
|
+
function pushReportDeferralLines(lines, data) {
|
|
376
451
|
// Deferred maintenance runs per reason code — how many due maintenance runs stood down
|
|
377
452
|
// and why, so an idle day reads as backlog-open / no-work / plans-full rather than blank,
|
|
378
453
|
// the per-role breakdown of the deferred maintenances. Gated to the team report — a
|
|
@@ -383,6 +458,9 @@ export function renderReportMarkdown(data) {
|
|
|
383
458
|
if (deferred.length > 0)
|
|
384
459
|
lines.push(rankedRoleLine("Deferred runs by reason", deferred, String));
|
|
385
460
|
}
|
|
461
|
+
}
|
|
462
|
+
/** The Commits-by-role line plus the team-only merge/main-red/cameo block. */
|
|
463
|
+
function pushReportMergeLines(lines, data) {
|
|
386
464
|
// Work ratio 4/4: the merged commits split by base role and by tier. The totals line's
|
|
387
465
|
// `commits` carries the count; these lines say who made it and how much of it was work.
|
|
388
466
|
lines.push(rankedRoleLine("Commits by role", rankedRoleTotals(data.series, (d) => d.commitsByRole ?? {}), String));
|
|
@@ -411,9 +489,21 @@ export function renderReportMarkdown(data) {
|
|
|
411
489
|
// suite went red before passing shows here. Zero-absent: no line when no day fails.
|
|
412
490
|
pushDayCameoLine(lines, data.series, "Red merge checks", (d) => d.checkFailures, (n) => `×${n}`);
|
|
413
491
|
// Per-day main red wall-clock, beside the red-check counts above: how long each day main
|
|
414
|
-
// had
|
|
492
|
+
// had its own check red (the span main had no passing full check). Team-only (main's own
|
|
415
493
|
// red time, not any role's) and zero-absent: no line when no day was red.
|
|
416
494
|
pushDayCameoLine(lines, data.series, "Main red", (d) => d.mainRedMs, durationLabel);
|
|
495
|
+
// The day twin of the --since window's totals line: the same tests ranked across the
|
|
496
|
+
// series' per-day main-red maps (Main red by test). Zero-absent: no line when no day
|
|
497
|
+
// named a failing test.
|
|
498
|
+
const mainRedByTest = rankedRoleTotals(data.series, (d) => d.mainRedByTest ?? {}).filter(([, n]) => n > 0);
|
|
499
|
+
if (mainRedByTest.length > 0)
|
|
500
|
+
lines.push(rankedRoleLine("Main red by test", mainRedByTest, String));
|
|
501
|
+
// The day twin of the --since window's merges line: the same culprits ranked across the
|
|
502
|
+
// series' per-day main-red maps (Main red by merge). Zero-absent: no line when no day
|
|
503
|
+
// blamed a merge.
|
|
504
|
+
const mainRedByCulprit = rankedRoleTotals(data.series, (d) => d.mainRedByCulprit ?? {}).filter(([, n]) => n > 0);
|
|
505
|
+
if (mainRedByCulprit.length > 0)
|
|
506
|
+
lines.push(rankedRoleLine("Main red by merge", mainRedByCulprit, String));
|
|
417
507
|
pushDayCameoLine(lines, data.series, "Merge wait median", (d) => d.mergeWaitMedianMs);
|
|
418
508
|
// How long the merge slot itself held each change it landed, beside the merge-wait median:
|
|
419
509
|
// the queue backlog (merge wait minus slot time) apart from the slot's own review/check/
|
|
@@ -427,6 +517,10 @@ export function renderReportMarkdown(data) {
|
|
|
427
517
|
pushDayCameoLine(lines, data.series, "Workspace wait median", (d) => d.slotWaitMedianMs);
|
|
428
518
|
pushDayCameoLine(lines, data.series, "Check wait median", (d) => d.checkWaitMedianMs);
|
|
429
519
|
}
|
|
520
|
+
}
|
|
521
|
+
/** Cost/commits tail: cost per commit, notional block, lag, validation gaps, sparse note. */
|
|
522
|
+
function pushReportTail(lines, data) {
|
|
523
|
+
const t = data.totals;
|
|
430
524
|
lines.push(costPerCommitLine("Cost per commit", costPerCommitPairs(data.series)));
|
|
431
525
|
// Subscription spend is notional: its own two lines, only when the window has any.
|
|
432
526
|
if ((t.notionalCostUsd ?? 0) > 0) {
|
|
@@ -464,5 +558,19 @@ export function renderReportMarkdown(data) {
|
|
|
464
558
|
// it prints — an empty or fully covered log never claims a history it cannot see.
|
|
465
559
|
if (!data.coversFullWindow)
|
|
466
560
|
lines.push("", SPARSE_WINDOW_NOTE);
|
|
561
|
+
}
|
|
562
|
+
/** Render a report as Markdown — the pinned shape the CLI prints and the TUI usage pane
|
|
563
|
+
* (report 3/3) reuses; the GUI tab (report 2/3) renders its own SVG charts from the same
|
|
564
|
+
* ReportData instead. Pure function of ReportData: no I/O, no clock reads. */
|
|
565
|
+
export function renderReportMarkdown(data) {
|
|
566
|
+
const lines = [];
|
|
567
|
+
pushReportHeader(lines, data);
|
|
568
|
+
pushReportSeriesTable(lines, data);
|
|
569
|
+
pushReportRoleLines(lines, data);
|
|
570
|
+
pushReportProviderLines(lines, data);
|
|
571
|
+
pushReportFrictionLines(lines, data);
|
|
572
|
+
pushReportDeferralLines(lines, data);
|
|
573
|
+
pushReportMergeLines(lines, data);
|
|
574
|
+
pushReportTail(lines, data);
|
|
467
575
|
return lines.join("\n");
|
|
468
576
|
}
|
|
@@ -8,7 +8,7 @@ import { renderFailureMarkdown } from "../failure/failure-render.js";
|
|
|
8
8
|
import { renderReportMarkdown, renderSinceReportMarkdown } from "./report-render.js";
|
|
9
9
|
import { fail, say, sayJson, sayJsonOrRender } from "../cli/cli-output.js";
|
|
10
10
|
import { followView } from "../cli/follow-view.js";
|
|
11
|
-
import {
|
|
11
|
+
import { failRivalShapes, failUntilShorterThanSince, flagValue, parseCountFlag, parseRoleListScope, parseSinceFlag } from "../cli/cli-args.js";
|
|
12
12
|
import { hasFollowFlag } from "../cli/cli-flag-specs.js";
|
|
13
13
|
import { REPORT_DEFAULT_DAYS, REPORT_MAX_DAYS, REPORT_SINCE_MAX_MS } from "../events/event-window.js";
|
|
14
14
|
/** `tumwater report [--days <n>] [--failures] [--since <duration>]
|
|
@@ -20,10 +20,7 @@ export async function cmdReport(root, args, seams = {}) {
|
|
|
20
20
|
// trailing window vs a series over whole days), not a modifier of either.
|
|
21
21
|
const ms = parseSinceFlag(args, "report --since", REPORT_SINCE_MAX_MS);
|
|
22
22
|
const untilMs = parseSinceFlag(args, "report --until", REPORT_SINCE_MAX_MS, "--until");
|
|
23
|
-
|
|
24
|
-
fail("report --until requires --since");
|
|
25
|
-
if (untilMs !== null && ms !== null && untilMs >= ms)
|
|
26
|
-
fail(`report --until (${durationLabel(untilMs)}) must be shorter than --since (${durationLabel(ms)})`);
|
|
23
|
+
failUntilShorterThanSince("report", untilMs, ms);
|
|
27
24
|
// --role scopes both report shapes: the usage report (day and --since) and the failure
|
|
28
25
|
// digest, whose day and --since collectors take the same optional list of base roles.
|
|
29
26
|
const role = parseRoleListScope(root, args);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** The review gate's five deterministic diff rules, in the gate's own order and scope — the one
|
|
2
|
+
* call site both the gate (review.ts) and the main watch (main-watch.ts) use, so a finding reads
|
|
3
|
+
* the same on a changed tree and on main's tip. `baseRev` is a commit or branch name the async
|
|
4
|
+
* rules resolve with changeBaseRev (merge-base of HEAD and it), so the watch passes a commit
|
|
5
|
+
* while the gate passes the main branch. Runs conflictMarkerReason, then protectedEditReason,
|
|
6
|
+
* then secretLeakReason, then — only for an exempt (md-only) diff — falseFixReason, then
|
|
7
|
+
* backlogStructureReason for every diff, short-circuiting on the first finding exactly as the
|
|
8
|
+
* original gate did: a later rule never runs once an earlier one already rejects. Returns a
|
|
9
|
+
* one-reason array when a rule fires (the reason the gate rejects on and the watch reports) and
|
|
10
|
+
* `[]` when nothing is wrong. An empty `role` (the watch cannot name the tip's merging role) is
|
|
11
|
+
* no basis for calling a PRINCIPLES.md edit unauthorized, so the role-dependent half of the
|
|
12
|
+
* protected-edit rule is skipped — but its role-independent half, the brief-block rule
|
|
13
|
+
* (briefEditReason), still runs, so a brief edit on an unattributed head is still reported; the
|
|
14
|
+
* gate always passes a known role, so its rejections are unchanged.
|
|
15
|
+
*
|
|
16
|
+
* This module lives apart from review-precheck.ts so the main watch can share the rules without a
|
|
17
|
+
* runtime import cycle: review-precheck.ts imports src/baseline/main-watch.ts
|
|
18
|
+
* (onlyKnownMainFailures), so a main-watch -> review-precheck import edge would make the two
|
|
19
|
+
* modules mutually depend on each other at runtime (docs/code-metrics.md records "no runtime
|
|
20
|
+
* import cycles"). diff-rules.ts's own imports (merge-git, protected-edits, secret-leaks,
|
|
21
|
+
* fix-claim, backlog-structure) import nothing from main-watch or review-precheck, so it is a
|
|
22
|
+
* cycle-free home both consumers can import. */
|
|
23
|
+
import { conflictMarkerReason } from "../merge/merge-git.js";
|
|
24
|
+
import { briefEditReason, protectedEditReason } from "./protected-edits.js";
|
|
25
|
+
import { secretLeakReason } from "./secret-leaks.js";
|
|
26
|
+
import { falseFixReason } from "../verdict/fix-claim.js";
|
|
27
|
+
import { backlogStructureReason } from "../backlog/backlog-structure.js";
|
|
28
|
+
export async function diffRuleReasons(wt, baseRev, files, role, exempt) {
|
|
29
|
+
const marker = conflictMarkerReason(wt, files);
|
|
30
|
+
if (marker !== undefined)
|
|
31
|
+
return [marker];
|
|
32
|
+
// The brief-block rule is role-independent — its edit is forbidden for every role alike — so it
|
|
33
|
+
// runs even when the watch cannot name the tip's merging role (role === ""). Only the
|
|
34
|
+
// PRINCIPLES.md half of protectedEditReason needs a known role to condemn an edit.
|
|
35
|
+
const briefReason = await briefEditReason(wt, baseRev, files);
|
|
36
|
+
if (briefReason !== undefined)
|
|
37
|
+
return [briefReason];
|
|
38
|
+
const protectedReason = role === "" ? undefined : await protectedEditReason(wt, baseRev, files, role);
|
|
39
|
+
if (protectedReason !== undefined)
|
|
40
|
+
return [protectedReason];
|
|
41
|
+
const secretReason = await secretLeakReason(wt, baseRev, files);
|
|
42
|
+
if (secretReason !== undefined)
|
|
43
|
+
return [secretReason];
|
|
44
|
+
if (exempt) {
|
|
45
|
+
const falseFix = await falseFixReason(wt, baseRev, files);
|
|
46
|
+
if (falseFix)
|
|
47
|
+
return [falseFix];
|
|
48
|
+
}
|
|
49
|
+
const structure = await backlogStructureReason(wt, baseRev, files);
|
|
50
|
+
if (structure)
|
|
51
|
+
return [structure];
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* evidence of flakiness — it is exactly the attribution this memory exists to avoid making
|
|
11
11
|
* twice. */
|
|
12
12
|
import { DAY_MS } from "../text/datetime.js";
|
|
13
|
-
import { TUMWATER_SOURCE, tryOrWarn } from "../events/events.js";
|
|
13
|
+
import { TUMWATER_SOURCE, tryOrWarn, warnEventBestEffort } from "../events/events.js";
|
|
14
14
|
import { readEventsSince } from "../events/event-window.js";
|
|
15
15
|
import { increment } from "../collections.js";
|
|
16
16
|
import { normalizeClusterKey } from "../failure/failure-cluster.js";
|
|
@@ -62,6 +62,18 @@ export function flakeCounts(events) {
|
|
|
62
62
|
increment(counts, key);
|
|
63
63
|
return [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
64
64
|
}
|
|
65
|
+
/** The bar at which a normalized check counts as flapping: one warning in the window is a
|
|
66
|
+
* routine one-off the gate's single re-run already absorbed, while two means the check flapped
|
|
67
|
+
* again and pays that re-run on every merge it touches — the one threshold the doctor's `flaky
|
|
68
|
+
* checks` row and the always-on flapped-checks alert share, folded through
|
|
69
|
+
* repeatedFlakeCounts. */
|
|
70
|
+
const FLAKE_REPEAT_WARN = 2;
|
|
71
|
+
/** The normalized checks in `events` that flapped at least FLAKE_REPEAT_WARN times, most
|
|
72
|
+
* frequent first — the filter flakeCounts feeds both the doctor and the status alert, so the
|
|
73
|
+
* threshold lives in the one home. Pure. */
|
|
74
|
+
export function repeatedFlakeCounts(events) {
|
|
75
|
+
return flakeCounts(events).filter(([, n]) => n >= FLAKE_REPEAT_WARN);
|
|
76
|
+
}
|
|
65
77
|
/** A Set of the flakeWarningKeys at or after `now - windowMs` of the gate's own window. */
|
|
66
78
|
export function flakeKeys(events, now, windowMs = KNOWN_FLAKE_WINDOW_MS) {
|
|
67
79
|
return new Set(flakeWarningKeys(events, now - windowMs));
|
|
@@ -136,6 +148,25 @@ export function quarantineTests(root, tests, tail) {
|
|
|
136
148
|
return [];
|
|
137
149
|
return added;
|
|
138
150
|
}
|
|
151
|
+
/** Remove the one quarantined test named `test` at the operator's request — the manual twin
|
|
152
|
+
* of releaseQuarantine's automatic release, and `tumwater quarantine --release <n>` its only
|
|
153
|
+
* caller (which resolves the position the list prints down to the exact test first). Returns
|
|
154
|
+
* whether the entry was present and the write succeeded, so the caller can answer a concurrent
|
|
155
|
+
* removal: `false` with no warning and no write for a name not quarantined, or when a write
|
|
156
|
+
* faulted (the quarantineTests write contract: a failed write warns and changes nothing). Logs
|
|
157
|
+
* one `warning` under TUMWATER_SOURCE so the alert and the doctor's `flaky checks` row stop
|
|
158
|
+
* excusing the name. */
|
|
159
|
+
export function releaseQuarantinedTest(root, test) {
|
|
160
|
+
const entries = loadQuarantine(root);
|
|
161
|
+
const have = entries.some((e) => e.test === test);
|
|
162
|
+
if (!have)
|
|
163
|
+
return false;
|
|
164
|
+
const removed = entries.filter((e) => e.test !== test);
|
|
165
|
+
if (!tryOrWarn(root, TUMWATER_SOURCE, "write quarantine.json", () => saveQuarantine(root, removed)))
|
|
166
|
+
return false;
|
|
167
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `${test} released from the flake quarantine at the user's request — the gate no longer excuses its failures`);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
139
170
|
/** Record the BUGS.md heading filed for each of `tests`, dropping their kept output. */
|
|
140
171
|
export function markQuarantineFiled(root, bugs) {
|
|
141
172
|
const entries = loadQuarantine(root);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** The operator's deliberate acceptance of a pending post-merge review without running it
|
|
2
|
+
* (src/cli/cli-review-commands.ts `tumwater reviews --accept <n>`), split out of the CLI so
|
|
3
|
+
* the review queue stays a pure reader/editor and this module owns the destructive half —
|
|
4
|
+
* removing an unattempted entry on purpose. It is the sibling of `tumwater queue --cancel
|
|
5
|
+
* <n>` (src/merge/merge-cancel.ts): the queue's no-third-path gap that the at-risk alert's
|
|
6
|
+
* shared sentence names ("…or accept the commit unreviewed deliberately"). It reads the same
|
|
7
|
+
* FIFO list `readPostReviews` numbers 1..N and settles the entry by its commit, so the removed
|
|
8
|
+
* entry is the very one the printed position named. */
|
|
9
|
+
import { readPostReviews, settlePostReview } from "./post-merge-review.js";
|
|
10
|
+
import { warnEventBestEffort } from "../events/events.js";
|
|
11
|
+
import { shortSha } from "../text/format.js";
|
|
12
|
+
/** Remove one readable entry the operator accepts unreviewed: refuse it (writing nothing) when
|
|
13
|
+
* it carries `fixed` — the repair already merged, so accepting it unreviewed is meaningless
|
|
14
|
+
* and the false "accepted unreviewed" warning would mislead — otherwise settle it with no
|
|
15
|
+
* verdict (`settlePostReview`; its failed write already warns and never throws, INV-BT2),
|
|
16
|
+
* log one `warning` under the entry's role, and return the `accepted` outcome naming what was
|
|
17
|
+
* retired. A rejected entry waits on nothing, so it is accepted like any other pending entry.
|
|
18
|
+
* The single removal both `acceptPostReview` (by position) and `acceptPostReviewBySha`
|
|
19
|
+
* (by sha) share, so the CLI and the dashboard's post-review-accept cannot drift on what
|
|
20
|
+
* accepting means. */
|
|
21
|
+
function removeAcceptedEntry(root, entry) {
|
|
22
|
+
if (entry.fixed !== undefined)
|
|
23
|
+
return { status: "settled", commit: entry.commit };
|
|
24
|
+
settlePostReview(root, entry.commit, null);
|
|
25
|
+
const message = `post-merge review of ${shortSha(entry.commit)} accepted unreviewed at the operator's request — the commit stays on main unreviewed`; // vocabulary-ok: the accepted-unreviewed wording those reasons it is the operator's choice
|
|
26
|
+
warnEventBestEffort(root, entry.role, message);
|
|
27
|
+
return { status: "accepted", role: entry.role, commit: entry.commit, summary: entry.summary };
|
|
28
|
+
}
|
|
29
|
+
/** Retire the Nth pending post-merge review — 1-based, as `tumwater reviews` numbers it —
|
|
30
|
+
* accepting the commit unreviewed at the operator's request. `position` is resolved against
|
|
31
|
+
* `readPostReviews` (the same readable FIFO list the command renders), so the position matches
|
|
32
|
+
* the line `tumwater reviews` printed. An out-of-range position writes nothing and returns the
|
|
33
|
+
* current readable depth; otherwise it runs the same `removeAcceptedEntry` the sha variant
|
|
34
|
+
* shares, so the two never drift on what accepting means (see `removeAcceptedEntry` for the
|
|
35
|
+
* removal, the fixed-guard, the warning, and the never-throws guarantee). */
|
|
36
|
+
export function acceptPostReview(root, position) {
|
|
37
|
+
const entries = readPostReviews(root);
|
|
38
|
+
if (position < 1 || position > entries.length)
|
|
39
|
+
return { status: "out-of-range", depth: entries.length };
|
|
40
|
+
return removeAcceptedEntry(root, entries[position - 1]);
|
|
41
|
+
}
|
|
42
|
+
/** Remove the pending post-merge review whose entry's commit is `sha` (the browser twin of
|
|
43
|
+
* `tumwater reviews --accept <n>`, resolving by commit because the dashboard rows carry one).
|
|
44
|
+
* A commit that matches none is `{ status: "gone" }` and changes nothing; otherwise it runs the
|
|
45
|
+
* same `removeAcceptedEntry` the position variant shares. */
|
|
46
|
+
export function acceptPostReviewBySha(root, sha) {
|
|
47
|
+
const entry = readPostReviews(root).find((e) => e.commit.toLowerCase() === sha.toLowerCase());
|
|
48
|
+
if (!entry)
|
|
49
|
+
return { status: "gone" };
|
|
50
|
+
return removeAcceptedEntry(root, entry);
|
|
51
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** The renderer behind `tumwater reviews` — the operator-facing text for the post-merge review
|
|
2
|
+
* queue (.tumwater/state/post-review.json): one line per well-formed entry in the given order,
|
|
3
|
+
* plus a `post-merge review queue (N):` header. Pure function of the entry list — the optional
|
|
4
|
+
* `now` is the only clock input, and when it is a number and an entry carries `enqueuedAt` the
|
|
5
|
+
* line names the wait since the entry was first queued, exactly as the merge queue names its
|
|
6
|
+
* age — so a test can pin its output exactly. The `merge queue empty` convention
|
|
7
|
+
* (src/merge/merge-queue-render.ts) is reused for the empty case. */
|
|
8
|
+
import { roleLabel } from "../roles/role-labels.js";
|
|
9
|
+
import { shortSha } from "../text/format.js";
|
|
10
|
+
import { squash } from "../text/text.js";
|
|
11
|
+
import { filterKeepPositions } from "../collections.js";
|
|
12
|
+
import { plural } from "../text/phrases.js";
|
|
13
|
+
import { humanAgo } from "../text/datetime.js";
|
|
14
|
+
/** The one state every observer surface narrows on: `fixed` when the repair already merged
|
|
15
|
+
* (the terminal Forward-never-back 1a/6 record — naming both shas, it is not pending anything),
|
|
16
|
+
* `rejected` when the reviewer turned the commit back (empty `rejected` array still counts),
|
|
17
|
+
* `failing` when the review runs keep ending without a verdict (`failures` positive), else
|
|
18
|
+
* `waiting`. The precedence matches what postReviewStatusLabel already printed — an entry can
|
|
19
|
+
* never be both — so the status label and `tumwater reviews --status` share this one definition
|
|
20
|
+
* and cannot drift. */
|
|
21
|
+
export function postReviewState(entry) {
|
|
22
|
+
if (entry.fixed !== undefined)
|
|
23
|
+
return "fixed";
|
|
24
|
+
if (entry.rejected !== undefined)
|
|
25
|
+
return "rejected";
|
|
26
|
+
if (typeof entry.failures === "number" && entry.failures > 0)
|
|
27
|
+
return "failing";
|
|
28
|
+
return "waiting";
|
|
29
|
+
}
|
|
30
|
+
/** The states `tumwater reviews --status` accepts, in the order the filter's error message
|
|
31
|
+
* lists them (the terminal `fixed` last). `postReviewState` is the definition; this only names
|
|
32
|
+
* the vocabulary. */
|
|
33
|
+
export const POST_REVIEW_STATES = ["waiting", "failing", "rejected", "fixed"];
|
|
34
|
+
/** The bracketed status suffix one post-merge review entry carries on every observer surface
|
|
35
|
+
* (`tumwater reviews`, the dashboard's Post-merge reviews card, and the TUI Pending pane's
|
|
36
|
+
* review block): the reviewer's recorded rejected reasons when `rejected` is set; else the
|
|
37
|
+
* number of verdict-less review runs when `failures` is positive, naming the
|
|
38
|
+
* last failure's reason beside the count when `lastFailure` is a non-empty string; else `""`.
|
|
39
|
+
* Reasons are squashed to 60 as `tumwater reviews` has always printed them, and the failure
|
|
40
|
+
* detail to 80. Branches on `postReviewState` so the label and the `--status` filter share one
|
|
41
|
+
* state definition; extracted so the
|
|
42
|
+
* CLI, the dashboard and the TUI share one status wording and cannot drift. */
|
|
43
|
+
export function postReviewStatusLabel(entry) {
|
|
44
|
+
switch (postReviewState(entry)) {
|
|
45
|
+
case "fixed":
|
|
46
|
+
return `[fixed — the repair ${shortSha(entry.fixed)} already merged on main]`;
|
|
47
|
+
case "rejected":
|
|
48
|
+
return `[rejected — ${entry.rejected.map((r) => squash(r, 60)).join("; ")}]`;
|
|
49
|
+
case "failing": {
|
|
50
|
+
const failures = entry.failures; // postReviewState only says "failing" when failures > 0
|
|
51
|
+
if (typeof entry.lastFailure === "string" && entry.lastFailure.length > 0) {
|
|
52
|
+
return `[${plural(failures, "failed review run")} — last: ${squash(entry.lastFailure, 80)}]`;
|
|
53
|
+
}
|
|
54
|
+
return `[${plural(failures, "failed review run")}]`;
|
|
55
|
+
}
|
|
56
|
+
default:
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Narrows a whole post-merge review list to the entries `keep` accepts, and returns
|
|
61
|
+
* the `scope` renderPostReviews takes: the kept roles, each kept entry's 1-based place in the
|
|
62
|
+
* whole list, and the whole list's entry count — plus, when a status filter is active, the
|
|
63
|
+
* `filter` text the header and no-match line print. The filtering-and-positioning step is the
|
|
64
|
+
* shared `filterKeepPositions` (src/collections.ts) that `scopeMergeQueue` (src/merge/
|
|
65
|
+
* merge-queue-render.ts) also uses, so `tumwater reviews --role` keeps the same whole-queue
|
|
66
|
+
* positions `--accept <n>` still addresses. */
|
|
67
|
+
export function scopePostReviews(entries, roles, keep, filter) {
|
|
68
|
+
const { kept, positions } = filterKeepPositions(entries, keep);
|
|
69
|
+
return {
|
|
70
|
+
entries: kept,
|
|
71
|
+
scope: { roles, positions, total: entries.length, ...(filter !== undefined ? { filter } : {}) },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** The text `tumwater reviews` prints. A totally empty queue renders `post-merge review queue
|
|
75
|
+
* empty`; otherwise a header and one line per entry — position, role, short commit, and, when
|
|
76
|
+
* `now` and the entry's `enqueuedAt` are both numbers, the wait since the commit was queued
|
|
77
|
+
* (`<age> ago`, between the short commit and the summary) — then the squashed summary and the
|
|
78
|
+
* status suffix from `postReviewStatusLabel`, appended as `" · " + label` only when it is
|
|
79
|
+
* non-empty. Without a `now`, or for an entry with no `enqueuedAt`, the line is byte-identical
|
|
80
|
+
* to before, so an older `post-review.json` and every existing renderer test read unchanged.
|
|
81
|
+
* With the optional `scope` (`reviews --role`/`--status`: `entries` already holds only the kept
|
|
82
|
+
* entries', `positions` their 1-based places in the whole queue, `total` the whole queue's
|
|
83
|
+
* entry count, and `filter` the `--role`/`--status` filter text when one is active) each line
|
|
84
|
+
* keeps its whole-queue number and the header reads
|
|
85
|
+
* `post-merge review queue (<kept> of <total>, <filter>):`, where `<filter>` is `scope.filter`
|
|
86
|
+
* when set else `--role <ids>`. A scope that keeps nothing of a non-empty queue prints
|
|
87
|
+
* `no post-merge reviews matching <filter> (post-merge review queue holds <total>)` when a
|
|
88
|
+
* filter is set, else `no post-merge reviews for <ids> (…)`. Without `scope` and `now` the
|
|
89
|
+
* output is byte-identical to before. */
|
|
90
|
+
export function renderPostReviews(entries, scope, now) {
|
|
91
|
+
const total = scope ? scope.total : entries.length;
|
|
92
|
+
if (total === 0)
|
|
93
|
+
return "post-merge review queue empty";
|
|
94
|
+
if (scope && entries.length === 0) {
|
|
95
|
+
const reason = scope.filter !== undefined ? `matching ${scope.filter}` : `for ${scope.roles.join(",")}`;
|
|
96
|
+
return `no post-merge reviews ${reason} (post-merge review queue holds ${total})`;
|
|
97
|
+
}
|
|
98
|
+
const lines = [
|
|
99
|
+
scope
|
|
100
|
+
? `post-merge review queue (${entries.length} of ${total}, ${scope.filter ?? `--role ${scope.roles.join(",")}`}):`
|
|
101
|
+
: `post-merge review queue (${plural(total, "entry", "entries")}):`,
|
|
102
|
+
];
|
|
103
|
+
entries.forEach((entry, i) => {
|
|
104
|
+
const label = postReviewStatusLabel(entry);
|
|
105
|
+
const suffix = label === "" ? "" : ` · ${label}`;
|
|
106
|
+
const age = typeof now === "number" && typeof entry.enqueuedAt === "number"
|
|
107
|
+
? ` · ${humanAgo(entry.enqueuedAt, now)}`
|
|
108
|
+
: "";
|
|
109
|
+
lines.push(` ${scope ? scope.positions[i] : i + 1}. ${roleLabel(entry.role)} ${shortSha(entry.commit)}${age} · ${squash(entry.summary, 80)}${suffix}`);
|
|
110
|
+
});
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|