tumwater 0.1.2 → 0.2.0
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 +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,211 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** Collection half of the usage report: read the harness's event log (through the shared
|
|
3
|
+
* event-folding layer in src/report/report-fold.ts) and the backlog history over a window of
|
|
4
|
+
* local calendar days and distill it into a `ReportData` / `SinceReport` — per-day
|
|
5
|
+
* tick/token/commit/cost counts plus features-done and bugs-fixed tallies from PLANS.md and
|
|
6
|
+
* BUGS.md. The Markdown rendering of this data lives in src/report/report-render.ts, a pure
|
|
7
|
+
* function of it; the split mirrors the failure report's (src/failure/failure-data.ts /
|
|
6
8
|
* src/failure/failure-render.ts) and keeps "what happened" (window math, aggregation, bounds)
|
|
7
9
|
* apart from "how it prints" (bars, cell wording), which change for different reasons — and
|
|
8
10
|
* keeps core data collection out of the presentation layer, so a core consumer (as
|
|
9
|
-
* /api/report already is) never forces a core→ui import.
|
|
10
|
-
|
|
11
|
+
* /api/report already is) never forces a core→ui import. This file holds the two window
|
|
12
|
+
* collectors and the report shapes they produce; the event-log folding and its memoized
|
|
13
|
+
* per-window cache live in src/report/report-fold.ts (its own test, report-fold-cache). */
|
|
11
14
|
import { assertReportSinceMs, eventWindowCovers, readWindowEvents } from "../events/event-window.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { eventsLogPath } from "../paths.js";
|
|
15
|
-
import { sectionCompletionDates } from "../backlog/backlog.js";
|
|
15
|
+
import { DONE_DATE_RE, FIXED_DATE_RE, completionDate, finishedFiles, fixedBugEntries, sectionCompletionDates, sectionFilingDates, } from "../backlog/backlog.js";
|
|
16
|
+
import { validationGapTag } from "../backlog/backlog-md.js";
|
|
16
17
|
import { dayAt, dayKey, dayWindow, formatDate } from "../text/datetime.js";
|
|
17
|
-
import { addTo } from "../collections.js";
|
|
18
|
+
import { addTo, lowerMedian } from "../collections.js";
|
|
18
19
|
import { baseRoleOf } from "../roles/loop-ids.js";
|
|
19
20
|
import { commitTier } from "../roles/roles.js";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
* omitted" rule true on the aggregation itself, not just at display time; merged events add
|
|
24
|
-
* a commit; landed/land_failed events add a landing run and
|
|
25
|
-
* accumulate their tokens/cost into the landing fields only — costByRole stays a ticks-only
|
|
26
|
-
* breakdown, and the landing line is how the reviewer's share is named. Both collectors fold
|
|
27
|
-
* through this one place, so the two windows' aggregation rules cannot drift apart. */
|
|
28
|
-
function foldUsageEvent(target, ev) {
|
|
29
|
-
if (ev.type === "tick_end") {
|
|
30
|
-
const role = eventRole(ev);
|
|
31
|
-
addTo(target.ticksByRole, role, 1);
|
|
32
|
-
if (target.ticks !== undefined)
|
|
33
|
-
target.ticks++;
|
|
34
|
-
const { tokens, costUsd, notionalCostUsd } = eventUsage(ev);
|
|
35
|
-
addTo(target.tokensByRole, role, tokens);
|
|
36
|
-
target.tokensOut += tokens;
|
|
37
|
-
target.costUsd += costUsd;
|
|
38
|
-
target.notionalCostUsd += notionalCostUsd;
|
|
39
|
-
if (costUsd !== 0)
|
|
40
|
-
addTo(target.costByRole, role, costUsd);
|
|
41
|
-
if (notionalCostUsd !== 0)
|
|
42
|
-
addTo(target.notionalByRole, role, notionalCostUsd);
|
|
43
|
-
}
|
|
44
|
-
else if (ev.type === "merged") {
|
|
45
|
-
target.commits++;
|
|
46
|
-
// Work ratio 4/4: split landed commits by base role and by tier. Instance suffixes fold
|
|
47
|
-
// into their role (`feature-2` counts as feature) so the split does not fragment.
|
|
48
|
-
const role = baseRoleOf(eventRole(ev));
|
|
49
|
-
addTo(target.commitsByRole, role, 1);
|
|
50
|
-
const tier = commitTier(role);
|
|
51
|
-
if (tier === "work")
|
|
52
|
-
target.workCommits++;
|
|
53
|
-
else if (tier === "maintenance")
|
|
54
|
-
target.maintenanceCommits++;
|
|
55
|
-
}
|
|
56
|
-
else if (ev.type === "landed" || ev.type === "land_failed") {
|
|
57
|
-
target.landingRuns = (target.landingRuns ?? 0) + 1;
|
|
58
|
-
const { tokens, costUsd, notionalCostUsd } = eventUsage(ev);
|
|
59
|
-
target.landingTokens = (target.landingTokens ?? 0) + tokens;
|
|
60
|
-
target.landingCostUsd = (target.landingCostUsd ?? 0) + costUsd;
|
|
61
|
-
// A subscription landing's API-equivalent spend joins the notional line, never the
|
|
62
|
-
// budgeted cost totals (plans/agent-backends.md part 6a).
|
|
63
|
-
target.notionalCostUsd += notionalCostUsd;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/** Bounded memo of window folds. Eight entries cover a dashboard flipping among a few day
|
|
67
|
-
* counts plus the midnight rollover's new fromKey; oldest-insertion eviction (Map preserves
|
|
68
|
-
* insertion order) keeps the memory bound to a handful of per-day accumulator maps. */
|
|
69
|
-
const reportFoldCache = new Map();
|
|
70
|
-
const REPORT_FOLD_CACHE_MAX = 8;
|
|
71
|
-
/** The empty commit-family counters every accumulator starts from: the total and per-base-role
|
|
72
|
-
* landed-commit count plus the work/maintenance split (Work ratio 4/4). Four shapes carry them —
|
|
73
|
-
* DayFold, UsageFold, the ReportDay series slots, and the window totals — so the empty quartet
|
|
74
|
-
* lives once here and a new commit counter is added in one place. */
|
|
75
|
-
function zeroCommitCounts() {
|
|
76
|
-
return { commits: 0, commitsByRole: {}, workCommits: 0, maintenanceCommits: 0 };
|
|
77
|
-
}
|
|
78
|
-
/** The empty landing-run counters every accumulator starts from: the landing slot's run count
|
|
79
|
-
* and its own token/cost share (foldUsageEvent's landed/land_failed branch). Four shapes carry
|
|
80
|
-
* them — DayFold, UsageFold, the scoped window totals, and the window totals — so the empty
|
|
81
|
-
* triple lives once here and a new landing counter is added in one place. */
|
|
82
|
-
function zeroLandingCounts() {
|
|
83
|
-
return { landingRuns: 0, landingTokens: 0, landingCostUsd: 0 };
|
|
84
|
-
}
|
|
85
|
-
/** Sum a per-loop-id count/cost map over one base role: instance keys (`feature-2`) fold into
|
|
86
|
-
* their base (`feature`), so a scoped report aggregates a whole role group exactly as the
|
|
87
|
-
* per-role cap's group does. Ticks/tokens/cost are keyed by exact loop id while commits are
|
|
88
|
-
* already base-role keyed; running every key through `baseRoleOf` reads both shapes the same
|
|
89
|
-
* and returns 0 when nothing matches. */
|
|
90
|
-
function roleSum(record, role) {
|
|
91
|
-
let sum = 0;
|
|
92
|
-
for (const [key, n] of Object.entries(record)) {
|
|
93
|
-
if (baseRoleOf(key) === role)
|
|
94
|
-
sum += n;
|
|
95
|
-
}
|
|
96
|
-
return sum;
|
|
97
|
-
}
|
|
98
|
-
/** Narrow a per-loop-id count/cost map to one base role, keyed by the base role; a role absent
|
|
99
|
-
* from the map (no ticks, or no nonzero cost) yields an empty map, never a zero entry — the
|
|
100
|
-
* same zero-means-absent rule the fleet render applies. Used by the role-scoped collectors so a
|
|
101
|
-
* scoped series derives from the fold's per-role maps without inventing entries. */
|
|
102
|
-
function narrowToRole(record, role) {
|
|
103
|
-
const n = roleSum(record, role);
|
|
104
|
-
return n === 0 ? {} : { [role]: n };
|
|
105
|
-
}
|
|
106
|
-
function dayFoldFor(entry, dayKey) {
|
|
107
|
-
let fold = entry.days.get(dayKey);
|
|
108
|
-
if (!fold) {
|
|
109
|
-
fold = {
|
|
110
|
-
tokensOut: 0,
|
|
111
|
-
...zeroCommitCounts(),
|
|
112
|
-
costUsd: 0,
|
|
113
|
-
notionalCostUsd: 0,
|
|
114
|
-
ticksByRole: {},
|
|
115
|
-
tokensByRole: {},
|
|
116
|
-
costByRole: {},
|
|
117
|
-
notionalByRole: {},
|
|
118
|
-
...zeroLandingCounts(),
|
|
119
|
-
};
|
|
120
|
-
entry.days.set(dayKey, fold);
|
|
121
|
-
}
|
|
122
|
-
return fold;
|
|
123
|
-
}
|
|
124
|
-
/** Fold one already-parsed event into a cache entry: only in-window events (a non-null day
|
|
125
|
-
* on or after fromKey) count, matching readWindowEvents's event list; the first such event
|
|
126
|
-
* seen takes firstEventTs. The one home of the fold body, shared by the incremental append
|
|
127
|
-
* path (foldLineInto) and the full re-read path, so the two can never drift. */
|
|
128
|
-
function foldEventInto(entry, ev) {
|
|
129
|
-
const dayKey = eventDayKey(ev);
|
|
130
|
-
if (dayKey === null || dayKey < entry.fromKey)
|
|
131
|
-
return;
|
|
132
|
-
if (!entry.hadEvents) {
|
|
133
|
-
entry.hadEvents = true;
|
|
134
|
-
entry.firstEventTs = typeof ev.ts === "number" ? ev.ts : undefined;
|
|
135
|
-
}
|
|
136
|
-
foldUsageEvent(dayFoldFor(entry, dayKey), ev);
|
|
137
|
-
}
|
|
138
|
-
/** Parse one appended log line and fold it; a torn or non-event line folds nothing. */
|
|
139
|
-
function foldLineInto(entry, line) {
|
|
140
|
-
const ev = parseEventLine(line);
|
|
141
|
-
if (ev)
|
|
142
|
-
foldEventInto(entry, ev);
|
|
143
|
-
}
|
|
144
|
-
/** The event-log half of collectReport, memoized: a dashboard re-fetching /api/report every
|
|
145
|
-
* poll re-read and re-parsed the whole window's log bytes each time (~6.8 MB and tens of ms
|
|
146
|
-
* on this repo's own day log, growing toward the 16 MB rotation cap). Now a request whose
|
|
147
|
-
* log has not changed costs one stat; growth folds only the appended complete lines
|
|
148
|
-
* (readCompleteLines holds back a torn trailing write, so it is folded whole next call).
|
|
149
|
-
* Same size with a changed mtime, a shrunken file (rotation or rewrite), or a new fromKey
|
|
150
|
-
* falls back to the full readWindowEvents pass — the fallback re-derives everything, so a
|
|
151
|
-
* mis-fold can only come from the append assumption, and rotation's whole-file replace
|
|
152
|
-
* always hits the shrink arm. The entry's coverage parts are pinned by the cache key:
|
|
153
|
-
* fromKey and file bytes decide them, so an advancing window re-derives fresh instead of
|
|
154
|
-
* carrying yesterday's proof forward. */
|
|
155
|
-
function foldWindowEvents(root, fromKey) {
|
|
156
|
-
const key = `${root}\0${fromKey}`;
|
|
157
|
-
const liveStat = statOrNull(eventsLogPath(root));
|
|
158
|
-
const size = liveStat?.size ?? 0;
|
|
159
|
-
const mtimeMs = liveStat?.mtimeMs ?? 0;
|
|
160
|
-
const dev = liveStat?.dev ?? 0;
|
|
161
|
-
const ino = liveStat?.ino ?? 0;
|
|
162
|
-
const cached = reportFoldCache.get(key);
|
|
163
|
-
if (cached &&
|
|
164
|
-
sameStatIdentity({ dev, ino, mtimeMs, size }, { dev: cached.dev, ino: cached.ino, mtimeMs: cached.mtimeMs, size: cached.offset })) {
|
|
165
|
-
reportFoldCache.delete(key); // Refresh LRU position.
|
|
166
|
-
reportFoldCache.set(key, cached);
|
|
167
|
-
return cached;
|
|
168
|
-
}
|
|
169
|
-
if (cached && size > cached.offset && dev === cached.dev && ino === cached.ino) {
|
|
170
|
-
const { lines, end } = readCompleteLines(eventsLogPath(root), cached.offset, size);
|
|
171
|
-
if (end > cached.offset) {
|
|
172
|
-
for (const line of lines)
|
|
173
|
-
foldLineInto(cached, line);
|
|
174
|
-
cached.offset = end;
|
|
175
|
-
cached.mtimeMs = mtimeMs;
|
|
176
|
-
return cached;
|
|
177
|
-
}
|
|
178
|
-
// Grown but no complete line yet (a torn write in flight): reuse the folds; the next
|
|
179
|
-
// request sees the completed line and folds it then.
|
|
180
|
-
return cached;
|
|
181
|
-
}
|
|
182
|
-
const raw = readWindowEvents(root, fromKey);
|
|
183
|
-
const entry = {
|
|
184
|
-
root,
|
|
185
|
-
fromKey,
|
|
186
|
-
// The scan's own end, not the outer `size`: an event appended between that stat and the
|
|
187
|
-
// read is already folded, and seeding the offset from the stale size would fold it again
|
|
188
|
-
// on the next append (readTailTextWithEnd's coveredEnd is exactly where the scan stopped).
|
|
189
|
-
offset: raw.liveEnd,
|
|
190
|
-
mtimeMs,
|
|
191
|
-
dev,
|
|
192
|
-
ino,
|
|
193
|
-
fileCovers: raw.coversFullWindow,
|
|
194
|
-
hadEvents: false,
|
|
195
|
-
days: new Map(),
|
|
196
|
-
};
|
|
197
|
-
for (const ev of raw.events)
|
|
198
|
-
foldEventInto(entry, ev);
|
|
199
|
-
if (reportFoldCache.size >= REPORT_FOLD_CACHE_MAX) {
|
|
200
|
-
const oldest = reportFoldCache.keys().next().value;
|
|
201
|
-
if (oldest !== undefined)
|
|
202
|
-
reportFoldCache.delete(oldest);
|
|
203
|
-
}
|
|
204
|
-
reportFoldCache.set(key, entry);
|
|
205
|
-
return entry;
|
|
206
|
-
}
|
|
207
|
-
/** Aggregate fleet usage over the trailing `sinceMs` window ending now, from the event log
|
|
208
|
-
* only (tick_end/merged — see SinceReport for why the backlog files are not read). Kept a
|
|
21
|
+
import { foldUsageEvent, foldWindowEvents, narrowToRole, roleSum, scopedByRole, zeroCommitCounts, zeroMergeCounts, } from "./report-fold.js";
|
|
22
|
+
/** Aggregate team usage over the trailing `sinceMs` window ending now, from the event log
|
|
23
|
+
* only (run_end/merged — see SinceReport for why the backlog files are not read). Kept a
|
|
209
24
|
* separate function rather than a `collectReport` variant flag: the day collector's
|
|
210
25
|
* zero-filled series and backlog-file reads have no place in a window totals view. */
|
|
211
26
|
export function collectReportSince(root, sinceMs, role) {
|
|
@@ -220,8 +35,19 @@ export function collectReportSince(root, sinceMs, role) {
|
|
|
220
35
|
const fromKey = dayKey(cutoff);
|
|
221
36
|
const raw = readWindowEvents(root, fromKey);
|
|
222
37
|
const acc = {
|
|
223
|
-
|
|
224
|
-
|
|
38
|
+
runs: 0,
|
|
39
|
+
runsByRole: {},
|
|
40
|
+
rejectionsByRole: {},
|
|
41
|
+
revisionsByRole: {},
|
|
42
|
+
refusalsByRole: {},
|
|
43
|
+
checkDurationsMs: [],
|
|
44
|
+
checkFailures: 0,
|
|
45
|
+
mergeWaitsMs: [],
|
|
46
|
+
providerWaitsMs: [],
|
|
47
|
+
slotWaitsMs: [],
|
|
48
|
+
failedRunsByRole: {},
|
|
49
|
+
fallbacksByRole: {},
|
|
50
|
+
imageReroutesByRole: {},
|
|
225
51
|
tokensByRole: {},
|
|
226
52
|
costByRole: {},
|
|
227
53
|
tokensOut: 0,
|
|
@@ -229,7 +55,14 @@ export function collectReportSince(root, sinceMs, role) {
|
|
|
229
55
|
costUsd: 0,
|
|
230
56
|
notionalCostUsd: 0,
|
|
231
57
|
notionalByRole: {},
|
|
232
|
-
...
|
|
58
|
+
...zeroMergeCounts(),
|
|
59
|
+
mergedByRole: {},
|
|
60
|
+
mergeFailedByRole: {},
|
|
61
|
+
mergeFailedByReason: {},
|
|
62
|
+
costByProvider: {},
|
|
63
|
+
notionalByProvider: {},
|
|
64
|
+
runsByModel: {},
|
|
65
|
+
deferredByReason: {},
|
|
233
66
|
};
|
|
234
67
|
for (const ev of raw.events) {
|
|
235
68
|
if (typeof ev.ts !== "number" || ev.ts < cutoff || ev.ts > now)
|
|
@@ -242,46 +75,81 @@ export function collectReportSince(root, sinceMs, role) {
|
|
|
242
75
|
// the window and the render notes the possible truncation.
|
|
243
76
|
const coversFullWindow = eventWindowCovers(raw, cutoff);
|
|
244
77
|
// Role scope: every total derives from the fold's per-role maps for the named base role alone
|
|
245
|
-
// —
|
|
78
|
+
// — merge usage (the merge slot's, not the role's) and the team-only fields stay out, so
|
|
246
79
|
// a scoped --since report sums exactly the same events the scoped day report would. The scope
|
|
247
80
|
// is the base role, so instances (`feature-2`) aggregate into their group as the cap does.
|
|
248
81
|
const base = role === undefined ? undefined : baseRoleOf(role);
|
|
249
82
|
const scopedTotals = base === undefined
|
|
250
83
|
? {
|
|
251
|
-
tokensOut: acc.tokensOut + (acc.
|
|
252
|
-
|
|
84
|
+
tokensOut: acc.tokensOut + (acc.mergeTokens ?? 0),
|
|
85
|
+
runs: acc.runs ?? 0,
|
|
253
86
|
commits: acc.commits,
|
|
254
|
-
costUsd: acc.costUsd + (acc.
|
|
87
|
+
costUsd: acc.costUsd + (acc.mergeCostUsd ?? 0),
|
|
255
88
|
...(acc.notionalCostUsd > 0 ? { notionalCostUsd: acc.notionalCostUsd } : {}),
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
89
|
+
mergeRuns: acc.mergeRuns ?? 0,
|
|
90
|
+
mergeTokens: acc.mergeTokens ?? 0,
|
|
91
|
+
mergeCostUsd: acc.mergeCostUsd ?? 0,
|
|
92
|
+
...((acc.mergeFailed ?? 0) > 0 ? { mergeFailed: acc.mergeFailed } : {}),
|
|
259
93
|
}
|
|
260
94
|
: (() => {
|
|
261
|
-
// The role's own notional spend, from its
|
|
262
|
-
// notional is
|
|
95
|
+
// The role's own notional spend, from its run_end events; the merge slot's
|
|
96
|
+
// notional is team-wide and stays out like its cost.
|
|
263
97
|
const notional = roleSum(acc.notionalByRole, base);
|
|
264
98
|
return {
|
|
265
99
|
tokensOut: roleSum(acc.tokensByRole, base),
|
|
266
|
-
|
|
100
|
+
runs: roleSum(acc.runsByRole, base),
|
|
267
101
|
commits: roleSum(acc.commitsByRole, base),
|
|
268
102
|
costUsd: roleSum(acc.costByRole, base),
|
|
269
103
|
...(notional > 0 ? { notionalCostUsd: notional } : {}),
|
|
270
|
-
...
|
|
104
|
+
...zeroMergeCounts(),
|
|
271
105
|
};
|
|
272
106
|
})();
|
|
107
|
+
// The --since render/JSON's Notional by role line is conditional on the window's own
|
|
108
|
+
// notional spend, scoped to base when a role is given — the same conditional the day
|
|
109
|
+
// collector applies to ReportDay.notionalByRole (line ~573), so a role-scoped window
|
|
110
|
+
// whose role spent no notional carries no notionalByRole instead of an empty map.
|
|
111
|
+
const notionalByRole = scopedByRole(acc.notionalByRole, base);
|
|
112
|
+
// Deferred runs are bucketed by a reason code (not a role), so a role-scoped window
|
|
113
|
+
// carries no entries — the same team-only rule runsByModel follows. Only set when the
|
|
114
|
+
// window actually deferred (zero means absent), so a deferral-free --since JSON omits it.
|
|
115
|
+
const deferred = scopedByRole(acc.deferredByReason, base);
|
|
273
116
|
return {
|
|
274
117
|
sinceMs,
|
|
275
118
|
fromIso: new Date(cutoff).toISOString(),
|
|
276
119
|
...(base === undefined ? {} : { role: base }),
|
|
277
120
|
totals: scopedTotals,
|
|
278
|
-
|
|
279
|
-
costByRole:
|
|
121
|
+
runsByRole: scopedByRole(acc.runsByRole, base),
|
|
122
|
+
costByRole: scopedByRole(acc.costByRole, base),
|
|
123
|
+
rejectionsByRole: scopedByRole(acc.rejectionsByRole, base),
|
|
124
|
+
revisionsByRole: scopedByRole(acc.revisionsByRole, base),
|
|
125
|
+
refusalsByRole: scopedByRole(acc.refusalsByRole, base),
|
|
126
|
+
failedRunsByRole: scopedByRole(acc.failedRunsByRole, base),
|
|
127
|
+
fallbacksByRole: scopedByRole(acc.fallbacksByRole, base),
|
|
128
|
+
imageReroutesByRole: scopedByRole(acc.imageReroutesByRole, base),
|
|
129
|
+
tokensByRole: scopedByRole(acc.tokensByRole, base),
|
|
130
|
+
commitsByRole: scopedByRole(acc.commitsByRole, base),
|
|
131
|
+
mergedByRole: scopedByRole(acc.mergedByRole, base),
|
|
132
|
+
mergeFailedByRole: scopedByRole(acc.mergeFailedByRole, base),
|
|
133
|
+
// A result code is not a role, so a role-scoped window carries an empty map (a code
|
|
134
|
+
// never baseRoleOf to the role) — the render gates the reason line to the team report.
|
|
135
|
+
mergeFailedByReason: scopedByRole(acc.mergeFailedByReason, base),
|
|
136
|
+
...(Object.keys(notionalByRole).length > 0 ? { notionalByRole } : {}),
|
|
137
|
+
// Provider spend is a team attribution (a provider is not a role), so a role-scoped
|
|
138
|
+
// window carries empty maps — providers never baseRoleOf to the role — and the render
|
|
139
|
+
// gates the provider lines to the team report anyway. Always present, empty when none.
|
|
140
|
+
costByProvider: scopedByRole(acc.costByProvider, base),
|
|
141
|
+
notionalByProvider: scopedByRole(acc.notionalByProvider, base),
|
|
142
|
+
// Model attribution is team-only (a model is not a role, so no model folds to the
|
|
143
|
+
// scoped role), the same full-map-for-team / empty-for-scoped rule costByProvider
|
|
144
|
+
// follows. Always present so the render can gate on the window's own events.
|
|
145
|
+
runsByModel: scopedByRole(acc.runsByModel, base),
|
|
146
|
+
...(Object.keys(deferred).length > 0 ? { deferredByReason: deferred } : {}),
|
|
280
147
|
coversFullWindow,
|
|
281
148
|
};
|
|
282
149
|
}
|
|
283
|
-
/** Aggregate
|
|
284
|
-
* log (
|
|
150
|
+
/** Aggregate team usage over exactly `days` local calendar days ending today, from the event
|
|
151
|
+
* log (run_end/merged) and the backlog history (`## Done` in PLANS.md and PLANS-DONE.md,
|
|
152
|
+
* `## Fixed` in BUGS.md and BUGS-FIXED.md). */
|
|
285
153
|
export function collectReport(root, days, role) {
|
|
286
154
|
const now = new Date();
|
|
287
155
|
// One captured instant for the whole window, so every day key derives from the same day.
|
|
@@ -291,7 +159,7 @@ export function collectReport(root, days, role) {
|
|
|
291
159
|
series.push({
|
|
292
160
|
date: formatDate(dayAt(i, now)),
|
|
293
161
|
tokensOut: 0,
|
|
294
|
-
|
|
162
|
+
runsByRole: {},
|
|
295
163
|
costByRole: {},
|
|
296
164
|
...zeroCommitCounts(),
|
|
297
165
|
costUsd: 0,
|
|
@@ -309,8 +177,36 @@ export function collectReport(root, days, role) {
|
|
|
309
177
|
continue; // Out-of-window day key (e.g. future-dated) — no series slot to fill.
|
|
310
178
|
if (role === undefined) {
|
|
311
179
|
day.tokensOut = fold.tokensOut;
|
|
312
|
-
day.
|
|
180
|
+
day.runsByRole = fold.runsByRole;
|
|
181
|
+
day.tokensByRole = fold.tokensByRole;
|
|
313
182
|
day.costByRole = fold.costByRole;
|
|
183
|
+
// Only set when nonzero, so a rejection-free day keeps its exact pre-feature shape.
|
|
184
|
+
if (Object.keys(fold.rejectionsByRole).length > 0)
|
|
185
|
+
day.rejectionsByRole = fold.rejectionsByRole;
|
|
186
|
+
if (Object.keys(fold.revisionsByRole).length > 0)
|
|
187
|
+
day.revisionsByRole = fold.revisionsByRole;
|
|
188
|
+
if (Object.keys(fold.refusalsByRole).length > 0)
|
|
189
|
+
day.refusalsByRole = fold.refusalsByRole;
|
|
190
|
+
// Checks belong to the merge slot, so only the team series carries the median.
|
|
191
|
+
if (fold.checkDurationsMs.length > 0) {
|
|
192
|
+
day.checkMedianMs = lowerMedian(fold.checkDurationsMs);
|
|
193
|
+
}
|
|
194
|
+
if (fold.checkFailures > 0)
|
|
195
|
+
day.checkFailures = fold.checkFailures;
|
|
196
|
+
if (fold.mergeWaitsMs.length > 0)
|
|
197
|
+
day.mergeWaitMedianMs = lowerMedian(fold.mergeWaitsMs);
|
|
198
|
+
if (fold.providerWaitsMs.length > 0)
|
|
199
|
+
day.providerWaitMedianMs = lowerMedian(fold.providerWaitsMs);
|
|
200
|
+
// A workspace wait is the pool's, not a role's, so only the team series carries it.
|
|
201
|
+
if (fold.slotWaitsMs.length > 0)
|
|
202
|
+
day.slotWaitMedianMs = lowerMedian(fold.slotWaitsMs);
|
|
203
|
+
// Only set when nonzero, so a failure-free day keeps its exact pre-feature shape.
|
|
204
|
+
if (Object.keys(fold.failedRunsByRole).length > 0)
|
|
205
|
+
day.failedRunsByRole = fold.failedRunsByRole;
|
|
206
|
+
if (Object.keys(fold.fallbacksByRole).length > 0)
|
|
207
|
+
day.fallbacksByRole = fold.fallbacksByRole;
|
|
208
|
+
if (Object.keys(fold.imageReroutesByRole).length > 0)
|
|
209
|
+
day.imageReroutesByRole = fold.imageReroutesByRole;
|
|
314
210
|
day.commits = fold.commits;
|
|
315
211
|
day.commitsByRole = fold.commitsByRole;
|
|
316
212
|
day.workCommits = fold.workCommits;
|
|
@@ -319,75 +215,178 @@ export function collectReport(root, days, role) {
|
|
|
319
215
|
// Only set when nonzero, so zero-notional days keep their exact pre-feature shape.
|
|
320
216
|
if (fold.notionalCostUsd > 0)
|
|
321
217
|
day.notionalCostUsd = fold.notionalCostUsd;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
218
|
+
if (Object.keys(fold.notionalByRole).length > 0)
|
|
219
|
+
day.notionalByRole = fold.notionalByRole;
|
|
220
|
+
// Only set when a provider actually spent (zero means absent), so a provider-free day
|
|
221
|
+
// keeps its exact pre-feature shape and `report --json` omits the maps when empty.
|
|
222
|
+
if (Object.keys(fold.costByProvider).length > 0)
|
|
223
|
+
day.costByProvider = fold.costByProvider;
|
|
224
|
+
if (Object.keys(fold.notionalByProvider).length > 0)
|
|
225
|
+
day.notionalByProvider = fold.notionalByProvider;
|
|
226
|
+
// Models are a team attribution (a model is not a role), so only the team series
|
|
227
|
+
// carries the map; only set when a modelled start happened (zero means absent), so a
|
|
228
|
+
// model-less day keeps its exact pre-feature shape.
|
|
229
|
+
if (Object.keys(fold.runsByModel).length > 0)
|
|
230
|
+
day.runsByModel = fold.runsByModel;
|
|
231
|
+
// Deferred runs are a team attribution (a reason is not a role), so only the team
|
|
232
|
+
// series carries the map; only set when a day actually deferred (zero means absent),
|
|
233
|
+
// so a deferral-free day keeps its exact pre-feature shape.
|
|
234
|
+
if (Object.keys(fold.deferredByReason).length > 0)
|
|
235
|
+
day.deferredByReason = fold.deferredByReason;
|
|
236
|
+
day.mergeRuns = fold.mergeRuns;
|
|
237
|
+
day.mergeTokens = fold.mergeTokens;
|
|
238
|
+
day.mergeCostUsd = fold.mergeCostUsd;
|
|
239
|
+
// Only set when nonzero, so a failure-free day keeps its exact pre-feature shape.
|
|
240
|
+
if ((fold.mergeFailed ?? 0) > 0)
|
|
241
|
+
day.mergeFailed = fold.mergeFailed;
|
|
242
|
+
// Only set when a role actually merged/failed that day (zero means absent), so a
|
|
243
|
+
// merge-free day keeps its exact pre-feature shape.
|
|
244
|
+
if (Object.keys(fold.mergedByRole).length > 0)
|
|
245
|
+
day.mergedByRole = fold.mergedByRole;
|
|
246
|
+
if (Object.keys(fold.mergeFailedByRole).length > 0)
|
|
247
|
+
day.mergeFailedByRole = fold.mergeFailedByRole;
|
|
248
|
+
// Reason codes are a team attribution (a result is not a role), so only the team
|
|
249
|
+
// series carries the map; only set when nonzero, so a failure-free day keeps its
|
|
250
|
+
// exact pre-feature shape. The role branch below leaves it unset — merge runs are
|
|
251
|
+
// the merge slot's.
|
|
252
|
+
if (Object.keys(fold.mergeFailedByReason).length > 0)
|
|
253
|
+
day.mergeFailedByReason = fold.mergeFailedByReason;
|
|
325
254
|
}
|
|
326
255
|
else {
|
|
327
256
|
// Role scope: derive this day from the fold's per-role maps, so the series carries only
|
|
328
257
|
// the named role's ticks/tokens/cost and (base-role-keyed) commits. The scope is the
|
|
329
258
|
// base role, so an instance id's usage and its siblings' commits fold into one group and
|
|
330
|
-
// the numbers match the per-role cap, which is base-role keyed too. The
|
|
331
|
-
// stay unset —
|
|
259
|
+
// the numbers match the per-role cap, which is base-role keyed too. The merge fields
|
|
260
|
+
// stay unset — merge runs are the merge slot's, not this role's — and the work /
|
|
332
261
|
// maintenance split follows the base role's own tier rather than the day's whole split.
|
|
333
262
|
const base = baseRoleOf(role);
|
|
334
263
|
const commits = roleSum(fold.commitsByRole, base);
|
|
335
264
|
const tier = commitTier(base);
|
|
336
265
|
day.tokensOut = roleSum(fold.tokensByRole, base);
|
|
337
|
-
day.
|
|
266
|
+
day.runsByRole = narrowToRole(fold.runsByRole, base);
|
|
267
|
+
day.tokensByRole = narrowToRole(fold.tokensByRole, base);
|
|
338
268
|
day.costByRole = narrowToRole(fold.costByRole, base);
|
|
269
|
+
const rejs = narrowToRole(fold.rejectionsByRole, base);
|
|
270
|
+
if (Object.keys(rejs).length > 0)
|
|
271
|
+
day.rejectionsByRole = rejs;
|
|
272
|
+
const revs = narrowToRole(fold.revisionsByRole, base);
|
|
273
|
+
if (Object.keys(revs).length > 0)
|
|
274
|
+
day.revisionsByRole = revs;
|
|
275
|
+
const refusals = narrowToRole(fold.refusalsByRole, base);
|
|
276
|
+
if (Object.keys(refusals).length > 0)
|
|
277
|
+
day.refusalsByRole = refusals;
|
|
278
|
+
const fails = narrowToRole(fold.failedRunsByRole, base);
|
|
279
|
+
if (Object.keys(fails).length > 0)
|
|
280
|
+
day.failedRunsByRole = fails;
|
|
281
|
+
const falls = narrowToRole(fold.fallbacksByRole, base);
|
|
282
|
+
if (Object.keys(falls).length > 0)
|
|
283
|
+
day.fallbacksByRole = falls;
|
|
284
|
+
const reroutes = narrowToRole(fold.imageReroutesByRole, base);
|
|
285
|
+
if (Object.keys(reroutes).length > 0)
|
|
286
|
+
day.imageReroutesByRole = reroutes;
|
|
339
287
|
day.commits = commits;
|
|
340
288
|
day.commitsByRole = narrowToRole(fold.commitsByRole, base);
|
|
341
289
|
day.workCommits = tier === "work" ? commits : 0;
|
|
342
290
|
day.maintenanceCommits = tier === "maintenance" ? commits : 0;
|
|
343
291
|
day.costUsd = roleSum(fold.costByRole, base);
|
|
344
292
|
const notional = roleSum(fold.notionalByRole, base);
|
|
345
|
-
if (notional > 0)
|
|
293
|
+
if (notional > 0) {
|
|
346
294
|
day.notionalCostUsd = notional;
|
|
295
|
+
day.notionalByRole = narrowToRole(fold.notionalByRole, base);
|
|
296
|
+
}
|
|
297
|
+
// Merge runs belong to the merge slot, not this role, so the role-scoped series
|
|
298
|
+
// keeps them unset (matching the merge-share fields) — the render omits the merge
|
|
299
|
+
// line for a scoped report, so these are carried only for shape consistency.
|
|
300
|
+
const mergeDone = narrowToRole(fold.mergedByRole, base);
|
|
301
|
+
if (Object.keys(mergeDone).length > 0)
|
|
302
|
+
day.mergedByRole = mergeDone;
|
|
303
|
+
const mergeDoneFailed = narrowToRole(fold.mergeFailedByRole, base);
|
|
304
|
+
if (Object.keys(mergeDoneFailed).length > 0)
|
|
305
|
+
day.mergeFailedByRole = mergeDoneFailed;
|
|
347
306
|
}
|
|
348
307
|
}
|
|
349
|
-
// The
|
|
308
|
+
// The merge share joins its day's tokensOut/costUsd (the budget charges these same
|
|
350
309
|
// events, so the day series must sum to the totals the budget agrees with) while the
|
|
351
|
-
//
|
|
310
|
+
// merge fields keep the reviewer's part of it visible on its own.
|
|
352
311
|
for (const d of series) {
|
|
353
|
-
d.tokensOut += d.
|
|
354
|
-
d.costUsd += d.
|
|
312
|
+
d.tokensOut += d.mergeTokens ?? 0;
|
|
313
|
+
d.costUsd += d.mergeCostUsd ?? 0;
|
|
355
314
|
}
|
|
356
315
|
const countOn = (date, field) => {
|
|
357
316
|
const day = byDate.get(date);
|
|
358
317
|
if (day)
|
|
359
|
-
day[field]
|
|
318
|
+
day[field] = (day[field] ?? 0) + 1; // Out-of-window dates drop out here.
|
|
360
319
|
};
|
|
361
|
-
// Role scope leaves
|
|
362
|
-
// role, so a scoped report cannot attribute a completion
|
|
320
|
+
// Role scope leaves the day-granular backlog tallies at zero: their dates are day-granular
|
|
321
|
+
// file metadata with no role, so a scoped report cannot attribute a completion or a filing
|
|
322
|
+
// (the render states the omission).
|
|
323
|
+
if (role === undefined) {
|
|
324
|
+
// Finished entries sit in the live file's finished section and in its archive file
|
|
325
|
+
// (plans/backlog-archive.md); each per-file read is stat-cached.
|
|
326
|
+
for (const file of finishedFiles("PLANS.md")) {
|
|
327
|
+
for (const d of sectionCompletionDates(root, file, "Done", DONE_DATE_RE))
|
|
328
|
+
countOn(d, "featuresDone");
|
|
329
|
+
}
|
|
330
|
+
for (const file of finishedFiles("BUGS.md")) {
|
|
331
|
+
for (const d of sectionCompletionDates(root, file, "Fixed", FIXED_DATE_RE))
|
|
332
|
+
countOn(d, "bugsFixed");
|
|
333
|
+
}
|
|
334
|
+
// Fold the filing stamps too: plans/bugs that opened this day, over the same entry
|
|
335
|
+
// detection `entryDates` applies to the completion reads, so an entry's opened and done
|
|
336
|
+
// days are read the same way.
|
|
337
|
+
for (const d of sectionFilingDates(root, "PLANS.md", "Planned"))
|
|
338
|
+
countOn(d, "plansAdded");
|
|
339
|
+
for (const file of finishedFiles("PLANS.md")) {
|
|
340
|
+
for (const d of sectionFilingDates(root, file, "Done"))
|
|
341
|
+
countOn(d, "plansAdded");
|
|
342
|
+
}
|
|
343
|
+
for (const d of sectionFilingDates(root, "BUGS.md", "Open"))
|
|
344
|
+
countOn(d, "bugsFiled");
|
|
345
|
+
for (const file of finishedFiles("BUGS.md")) {
|
|
346
|
+
for (const d of sectionFilingDates(root, file, "Fixed"))
|
|
347
|
+
countOn(d, "bugsFiled");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Tally the fixed bugs' `Validation gap` tags over the window, so the team can see where
|
|
351
|
+
// its own tests were weakest. An entry counts only when it is in the window (the same
|
|
352
|
+
// out-of-window drop countOn applies: its completion date must be a key of byDate), and
|
|
353
|
+
// only the team report carries the tally — a tag has no role (the render gates on the
|
|
354
|
+
// same `role === undefined`). Zero-means-absent: a window with no tagged fixed bug spreads
|
|
355
|
+
// no key, so the byte-for-byte assertions on pre-feature windows hold.
|
|
356
|
+
const validationGaps = {};
|
|
363
357
|
if (role === undefined) {
|
|
364
|
-
for (const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
358
|
+
for (const e of fixedBugEntries(root)) {
|
|
359
|
+
const date = completionDate(e.title, FIXED_DATE_RE);
|
|
360
|
+
if (date === null || !byDate.has(date))
|
|
361
|
+
continue;
|
|
362
|
+
const tag = validationGapTag(e.body);
|
|
363
|
+
if (tag !== null) {
|
|
364
|
+
addTo(validationGaps, tag, 1);
|
|
365
|
+
addTo((byDate.get(date).validationGapsByTag ??= {}), tag, 1);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
374
368
|
}
|
|
375
369
|
const totals = {
|
|
376
370
|
tokensOut: 0,
|
|
377
|
-
|
|
371
|
+
runs: 0,
|
|
378
372
|
...zeroCommitCounts(),
|
|
379
373
|
costUsd: 0,
|
|
380
374
|
featuresDone: 0,
|
|
381
375
|
bugsFixed: 0,
|
|
382
|
-
...
|
|
376
|
+
...zeroMergeCounts(),
|
|
383
377
|
};
|
|
384
378
|
// Notional spend is kept out of the totals object until it is nonzero, so a pre-feature
|
|
385
|
-
// window's totals keep their exact shape.
|
|
379
|
+
// window's totals keep their exact shape. Merge failures likewise: a failure-free window
|
|
380
|
+
// carries no mergeFailed (zero means absent, like the merge-share line's other fields
|
|
381
|
+
// are folded in below).
|
|
386
382
|
let notionalCostUsd = 0;
|
|
383
|
+
let mergeFailed = 0;
|
|
384
|
+
let plansAdded = 0;
|
|
385
|
+
let bugsFiled = 0;
|
|
387
386
|
for (const d of series) {
|
|
388
387
|
totals.tokensOut += d.tokensOut;
|
|
389
|
-
for (const n of Object.values(d.
|
|
390
|
-
totals.
|
|
388
|
+
for (const n of Object.values(d.runsByRole))
|
|
389
|
+
totals.runs += n;
|
|
391
390
|
totals.commits += d.commits;
|
|
392
391
|
for (const [role, n] of Object.entries(d.commitsByRole ?? {}))
|
|
393
392
|
addTo(totals.commitsByRole, role, n);
|
|
@@ -397,9 +396,12 @@ export function collectReport(root, days, role) {
|
|
|
397
396
|
notionalCostUsd += d.notionalCostUsd ?? 0;
|
|
398
397
|
totals.featuresDone += d.featuresDone;
|
|
399
398
|
totals.bugsFixed += d.bugsFixed;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
totals.
|
|
399
|
+
plansAdded += d.plansAdded ?? 0;
|
|
400
|
+
bugsFiled += d.bugsFiled ?? 0;
|
|
401
|
+
totals.mergeRuns += d.mergeRuns ?? 0;
|
|
402
|
+
totals.mergeTokens += d.mergeTokens ?? 0;
|
|
403
|
+
totals.mergeCostUsd += d.mergeCostUsd ?? 0;
|
|
404
|
+
mergeFailed += d.mergeFailed ?? 0;
|
|
403
405
|
}
|
|
404
406
|
// The shared coverage proof (event-window.ts), rebuilt from the memoized read's pinned
|
|
405
407
|
// parts: the day-keyed proof (fileCovers — a property of the retained bytes and fromKey,
|
|
@@ -408,7 +410,7 @@ export function collectReport(root, days, role) {
|
|
|
408
410
|
// earliest the window could contain, so the same-day arm degenerates harmlessly. Without
|
|
409
411
|
// this field the day report was the one windowed consumer that could undercount in
|
|
410
412
|
// silence — the --since report and the failure digest both carry the note, and a silent
|
|
411
|
-
// number invites an operator to trust a truncated window as an idle
|
|
413
|
+
// number invites an operator to trust a truncated window as an idle team.
|
|
412
414
|
const cutoffMs = dayAt(days - 1, now).getTime();
|
|
413
415
|
const coversFullWindow = foldEntry.fileCovers ||
|
|
414
416
|
!foldEntry.hadEvents ||
|
|
@@ -419,7 +421,14 @@ export function collectReport(root, days, role) {
|
|
|
419
421
|
to,
|
|
420
422
|
...(role === undefined ? {} : { role: baseRoleOf(role) }),
|
|
421
423
|
series,
|
|
422
|
-
totals: {
|
|
424
|
+
totals: {
|
|
425
|
+
...totals,
|
|
426
|
+
...(notionalCostUsd > 0 ? { notionalCostUsd } : {}),
|
|
427
|
+
...(mergeFailed > 0 ? { mergeFailed } : {}),
|
|
428
|
+
...(plansAdded > 0 ? { plansAdded } : {}),
|
|
429
|
+
...(bugsFiled > 0 ? { bugsFiled } : {}),
|
|
430
|
+
...(Object.keys(validationGaps).length > 0 ? { validationGapsByTag: validationGaps } : {}),
|
|
431
|
+
},
|
|
423
432
|
coversFullWindow,
|
|
424
433
|
};
|
|
425
434
|
}
|