tumwater 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -1,168 +1,24 @@
|
|
|
1
|
-
/** Collection half of the usage report: read the harness's event log
|
|
2
|
-
* event-folding
|
|
3
|
-
* local calendar days and distill it into a `ReportData`
|
|
1
|
+
/** Collection half of the usage report's per-day series: read the harness's event log
|
|
2
|
+
* (through the shared event-folding core in src/report/report-usage-fold.ts) and the backlog
|
|
3
|
+
* history over a window of local calendar days and distill it into a `ReportData` — per-day
|
|
4
4
|
* run/token/commit/cost counts plus features-done and bugs-fixed tallies from PLANS.md and
|
|
5
5
|
* BUGS.md. The Markdown rendering of this data lives in src/report/report-render.ts, a pure
|
|
6
6
|
* function of it; the split mirrors the failure report's (src/failure/failure-data.ts /
|
|
7
7
|
* src/failure/failure-render.ts) and keeps "what happened" (window math, aggregation, bounds)
|
|
8
8
|
* apart from "how it prints" (bars, cell wording), which change for different reasons — and
|
|
9
9
|
* keeps core data collection out of the presentation layer, so a core consumer (as
|
|
10
|
-
* /api/report already is) never forces a core→ui import.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
10
|
+
* /api/report already is) never forces a core→ui import. The trailing-window `SinceReport`
|
|
11
|
+
* collector lives in src/report/report-since-data.ts beside it; both fold events through the
|
|
12
|
+
* shared one-event core in src/report/report-usage-fold.ts and its memoized per-window cache
|
|
13
|
+
* in src/report/report-fold.ts, and the report shapes they produce live in
|
|
14
|
+
* src/report/report-types.ts (report-fold has its own test, report-fold-cache). */
|
|
15
15
|
import { DONE_DATE_RE, FIXED_DATE_RE, completionDate, finishedFiles, fixedBugEntries, sectionCompletionDates, sectionCompletionLags, sectionFilingDates, } from "../backlog/backlog.js";
|
|
16
16
|
import { validationGapTag } from "../backlog/backlog-md.js";
|
|
17
|
-
import { dayAt,
|
|
17
|
+
import { dayAt, dayWindow, formatDate } from "../text/datetime.js";
|
|
18
18
|
import { addTo, lowerMedian } from "../collections.js";
|
|
19
19
|
import { commitTier } from "../roles/roles.js";
|
|
20
|
-
import {
|
|
21
|
-
import { mainRedMinutesByDay } from "./report-main-red.js";
|
|
22
|
-
/** Aggregate team usage over the trailing `sinceMs` window ending now (or `untilMs` ago, which
|
|
23
|
-
* drops events at or after that end; `coversFullWindow` still judges the far edge
|
|
24
|
-
* only), from the event log only (run_end/merged — see SinceReport for why the backlog files
|
|
25
|
-
* are not read). Kept a
|
|
26
|
-
* separate function rather than a `collectReport` variant flag: the day collector's
|
|
27
|
-
* zero-filled series and backlog-file reads have no place in a window totals view. */
|
|
28
|
-
export function collectReportSince(root, sinceMs, role, untilMs) {
|
|
29
|
-
assertReportSinceMs(sinceMs);
|
|
30
|
-
const { now, cutoff, end, slice } = sinceWindowTiming(sinceMs, untilMs);
|
|
31
|
-
// The window key is the cutoff's local calendar day, from the shared dayKey helper
|
|
32
|
-
// eventDayKey buckets events with, so the read's day keys cannot disagree with the ts
|
|
33
|
-
// filter below; the day-keyed read may include earlier hours of that day, which the ts
|
|
34
|
-
// filter removes (over-read is at most one day's events). Future-dated events are dropped
|
|
35
|
-
// here too, matching collectReport (its day map only holds the window's days).
|
|
36
|
-
const fromKey = dayKey(cutoff);
|
|
37
|
-
const raw = readWindowEvents(root, fromKey);
|
|
38
|
-
const acc = {
|
|
39
|
-
runs: 0,
|
|
40
|
-
runsByRole: {},
|
|
41
|
-
rejectionsByRole: {},
|
|
42
|
-
postMergeRejectionsByRole: {},
|
|
43
|
-
revisionsByRole: {},
|
|
44
|
-
refusalsByRole: {},
|
|
45
|
-
checkDurationsMs: [],
|
|
46
|
-
checkFailures: 0,
|
|
47
|
-
mergeWaitsMs: [],
|
|
48
|
-
mergeDurationsMs: [],
|
|
49
|
-
runDurationsMs: [],
|
|
50
|
-
providerWaitsMs: [],
|
|
51
|
-
slotWaitsMs: [],
|
|
52
|
-
checkWaitsMs: [],
|
|
53
|
-
reviewDurationsMs: [],
|
|
54
|
-
failedRunsByRole: {},
|
|
55
|
-
fallbacksByRole: {},
|
|
56
|
-
imageReroutesByRole: {},
|
|
57
|
-
tokensByRole: {},
|
|
58
|
-
costByRole: {},
|
|
59
|
-
tokensOut: 0,
|
|
60
|
-
...zeroCommitCounts(),
|
|
61
|
-
costUsd: 0,
|
|
62
|
-
notionalCostUsd: 0,
|
|
63
|
-
notionalByRole: {},
|
|
64
|
-
...zeroMergeCounts(),
|
|
65
|
-
mergedByRole: {},
|
|
66
|
-
mergeFailedByRole: {},
|
|
67
|
-
mergeFailedByReason: {},
|
|
68
|
-
costByProvider: {},
|
|
69
|
-
notionalByProvider: {},
|
|
70
|
-
notionalByBackend: {},
|
|
71
|
-
runsByModel: {},
|
|
72
|
-
deferredByReason: {},
|
|
73
|
-
};
|
|
74
|
-
for (const ev of raw.events) {
|
|
75
|
-
if (typeof ev.ts !== "number" || ev.ts < cutoff || ev.ts > now || (untilMs !== undefined && ev.ts >= end))
|
|
76
|
-
continue;
|
|
77
|
-
foldUsageEvent(acc, ev);
|
|
78
|
-
}
|
|
79
|
-
// The empty-window branch of the shared proof matters here: with no retained events nothing
|
|
80
|
-
// can have rotated away, and printing a rotation note over a fresh install would claim a
|
|
81
|
-
// history that never existed. When neither proof holds, the oldest retained event lies inside
|
|
82
|
-
// the window and the render notes the possible truncation.
|
|
83
|
-
const coversFullWindow = eventWindowCovers(raw, cutoff);
|
|
84
|
-
// Role scope: every total derives from the fold's per-role maps for the named base roles alone
|
|
85
|
-
// — merge usage (the merge slot's, not the role's) and the team-only fields stay out, so
|
|
86
|
-
// a scoped --since report sums exactly the same events the scoped day report would. The scope
|
|
87
|
-
// is the base roles, so instances (`feature-2`) aggregate into their group as the cap does.
|
|
88
|
-
const bases = scopeBases(role);
|
|
89
|
-
const scopedTotals = bases === undefined
|
|
90
|
-
? {
|
|
91
|
-
tokensOut: acc.tokensOut + (acc.mergeTokens ?? 0),
|
|
92
|
-
runs: acc.runs ?? 0,
|
|
93
|
-
commits: acc.commits,
|
|
94
|
-
costUsd: acc.costUsd + (acc.mergeCostUsd ?? 0),
|
|
95
|
-
...(acc.notionalCostUsd > 0 ? { notionalCostUsd: acc.notionalCostUsd } : {}),
|
|
96
|
-
...nonemptyObj("notionalByBackend", acc.notionalByBackend),
|
|
97
|
-
mergeRuns: acc.mergeRuns ?? 0,
|
|
98
|
-
mergeTokens: acc.mergeTokens ?? 0,
|
|
99
|
-
mergeCostUsd: acc.mergeCostUsd ?? 0,
|
|
100
|
-
...((acc.mergeFailed ?? 0) > 0 ? { mergeFailed: acc.mergeFailed } : {}),
|
|
101
|
-
}
|
|
102
|
-
: (() => {
|
|
103
|
-
// The role's own notional spend, from its run_end events; the merge slot's
|
|
104
|
-
// notional is team-wide and stays out like its cost.
|
|
105
|
-
const notional = roleSum(acc.notionalByRole, bases);
|
|
106
|
-
return {
|
|
107
|
-
tokensOut: roleSum(acc.tokensByRole, bases),
|
|
108
|
-
runs: roleSum(acc.runsByRole, bases),
|
|
109
|
-
commits: roleSum(acc.commitsByRole, bases),
|
|
110
|
-
costUsd: roleSum(acc.costByRole, bases),
|
|
111
|
-
...(notional > 0 ? { notionalCostUsd: notional } : {}),
|
|
112
|
-
...zeroMergeCounts(),
|
|
113
|
-
};
|
|
114
|
-
})();
|
|
115
|
-
// The --since render/JSON's Notional by role line is conditional on the window's own
|
|
116
|
-
// notional spend, scoped to base when a role is given — the same conditional the day
|
|
117
|
-
// collector applies to ReportDay.notionalByRole (line ~573), so a role-scoped window
|
|
118
|
-
// whose role spent no notional carries no notionalByRole instead of an empty map.
|
|
119
|
-
const notionalByRole = scopedByRole(acc.notionalByRole, bases);
|
|
120
|
-
// Deferred runs are bucketed by a reason code (not a role), so a role-scoped window
|
|
121
|
-
// carries no entries — the same team-only rule runsByModel follows. Only set when the
|
|
122
|
-
// window actually deferred (zero means absent), so a deferral-free --since JSON omits it.
|
|
123
|
-
const deferred = scopedByRole(acc.deferredByReason, bases);
|
|
124
|
-
return {
|
|
125
|
-
...slice,
|
|
126
|
-
...(bases === undefined ? {} : { role: bases.join(",") }),
|
|
127
|
-
totals: scopedTotals,
|
|
128
|
-
runsByRole: scopedByRole(acc.runsByRole, bases),
|
|
129
|
-
costByRole: scopedByRole(acc.costByRole, bases),
|
|
130
|
-
rejectionsByRole: scopedByRole(acc.rejectionsByRole, bases),
|
|
131
|
-
postMergeRejectionsByRole: scopedByRole(acc.postMergeRejectionsByRole, bases),
|
|
132
|
-
revisionsByRole: scopedByRole(acc.revisionsByRole, bases),
|
|
133
|
-
refusalsByRole: scopedByRole(acc.refusalsByRole, bases),
|
|
134
|
-
failedRunsByRole: scopedByRole(acc.failedRunsByRole, bases),
|
|
135
|
-
fallbacksByRole: scopedByRole(acc.fallbacksByRole, bases),
|
|
136
|
-
imageReroutesByRole: scopedByRole(acc.imageReroutesByRole, bases),
|
|
137
|
-
tokensByRole: scopedByRole(acc.tokensByRole, bases),
|
|
138
|
-
commitsByRole: scopedByRole(acc.commitsByRole, bases),
|
|
139
|
-
mergedByRole: scopedByRole(acc.mergedByRole, bases),
|
|
140
|
-
mergeFailedByRole: scopedByRole(acc.mergeFailedByRole, bases),
|
|
141
|
-
// A result code is not a role, so a role-scoped window carries an empty map (a code
|
|
142
|
-
// never baseRoleOf to the role) — the render gates the reason line to the team report.
|
|
143
|
-
mergeFailedByReason: scopedByRole(acc.mergeFailedByReason, bases),
|
|
144
|
-
...nonemptyObj("notionalByRole", notionalByRole),
|
|
145
|
-
// Provider spend is a team attribution (a provider is not a role), so a role-scoped
|
|
146
|
-
// window carries empty maps — providers never baseRoleOf to the role — and the render
|
|
147
|
-
// gates the provider lines to the team report anyway. Always present, empty when none.
|
|
148
|
-
costByProvider: scopedByRole(acc.costByProvider, bases),
|
|
149
|
-
notionalByProvider: scopedByRole(acc.notionalByProvider, bases),
|
|
150
|
-
// Backend spend is a team attribution (a backend is not a role), so a role-scoped window
|
|
151
|
-
// omits it — the same absent-when-empty rule the day collector applies.
|
|
152
|
-
...nonemptyObj("notionalByBackend", scopedByRole(acc.notionalByBackend, bases)),
|
|
153
|
-
// Model attribution is team-only (a model is not a role, so no model folds to the
|
|
154
|
-
// scoped role), the same full-map-for-team / empty-for-scoped rule costByProvider
|
|
155
|
-
// follows. Always present so the render can gate on the window's own events.
|
|
156
|
-
runsByModel: scopedByRole(acc.runsByModel, bases),
|
|
157
|
-
...nonemptyObj("deferredByReason", deferred),
|
|
158
|
-
// Main's red time is main's own (G1's rule), not any role's, so a role-scoped window
|
|
159
|
-
// carries 0 — the type's always-present default for the team-only field.
|
|
160
|
-
mainRedMs: bases === undefined
|
|
161
|
-
? [...mainRedMinutesByDay(raw.events, cutoff, end).values()].reduce((a, b) => a + b, 0)
|
|
162
|
-
: 0,
|
|
163
|
-
coversFullWindow,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
20
|
+
import { foldWindowEvents, narrowToRole, nonemptyObj, roleSum, scopeBases, zeroCommitCounts, zeroMergeCounts, } from "./report-fold.js";
|
|
21
|
+
import { mainRedCulpritsByDay, mainRedFailingTestsByDay, mainRedMinutesByDay } from "./report-main-red.js";
|
|
166
22
|
/** Set one per-day median cell only when the day actually has samples (`ms` non-empty), so a
|
|
167
23
|
* medians-free day keeps its exact pre-feature shape (the field stays absent). The single home
|
|
168
24
|
* of that guard — the day collector's eight `…MedianMs` cells all followed it inline before. */
|
|
@@ -179,21 +35,9 @@ function setMapIfNonempty(day, key, map) {
|
|
|
179
35
|
day[key] = map;
|
|
180
36
|
}
|
|
181
37
|
}
|
|
182
|
-
/**
|
|
183
|
-
*
|
|
184
|
-
|
|
185
|
-
* followed it inline before. */
|
|
186
|
-
function nonemptyObj(key, map) {
|
|
187
|
-
return Object.keys(map).length > 0 ? { [key]: map } : {};
|
|
188
|
-
}
|
|
189
|
-
/** Aggregate team usage over exactly `days` local calendar days ending today, from the event
|
|
190
|
-
* log (run_end/merged) and the backlog history (`## Done` in PLANS.md and PLANS-DONE.md,
|
|
191
|
-
* `## Fixed` in BUGS.md and BUGS-FIXED.md). */
|
|
192
|
-
export function collectReport(root, days, role) {
|
|
193
|
-
const bases = scopeBases(role);
|
|
194
|
-
const now = new Date();
|
|
195
|
-
// One captured instant for the whole window, so every day key derives from the same day.
|
|
196
|
-
const { from, to } = dayWindow(days, now);
|
|
38
|
+
/** Zero-fill the per-day series across `days` calendar days ending `now` and index it by
|
|
39
|
+
* date, so the fold's day keys can address their slots and the out-of-window keys drop out. */
|
|
40
|
+
function buildEmptySeries(days, now) {
|
|
197
41
|
const series = [];
|
|
198
42
|
for (let i = days - 1; i >= 0; i--) {
|
|
199
43
|
series.push({
|
|
@@ -210,138 +54,159 @@ export function collectReport(root, days, role) {
|
|
|
210
54
|
const byDate = new Map();
|
|
211
55
|
for (const d of series)
|
|
212
56
|
byDate.set(d.date, d);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
setMapIfNonempty(day, "mergeFailedByRole", narrowToRole(fold.mergeFailedByRole, bases));
|
|
323
|
-
}
|
|
57
|
+
return { series, byDate };
|
|
58
|
+
}
|
|
59
|
+
/** Fill one team day from its folded events: every field the fold carries, with the
|
|
60
|
+
* nonzero-or-absent and median-guard rules the day's exact pre-feature shape relies on. */
|
|
61
|
+
function applyTeamDay(day, fold) {
|
|
62
|
+
day.tokensOut = fold.tokensOut;
|
|
63
|
+
day.runsByRole = fold.runsByRole;
|
|
64
|
+
day.tokensByRole = fold.tokensByRole;
|
|
65
|
+
// Only set when nonempty, so a prompt-free day keeps its exact pre-feature shape.
|
|
66
|
+
setMapIfNonempty(day, "promptTokensByRole", fold.promptTokensByRole);
|
|
67
|
+
setMapIfNonempty(day, "cacheReadTokensByRole", fold.cacheReadTokensByRole);
|
|
68
|
+
day.costByRole = fold.costByRole;
|
|
69
|
+
// Only set when nonzero, so a rejection-free day keeps its exact pre-feature shape.
|
|
70
|
+
setMapIfNonempty(day, "rejectionsByRole", fold.rejectionsByRole);
|
|
71
|
+
setMapIfNonempty(day, "postMergeRejectionsByRole", fold.postMergeRejectionsByRole);
|
|
72
|
+
setMapIfNonempty(day, "revisionsByRole", fold.revisionsByRole);
|
|
73
|
+
setMapIfNonempty(day, "refusalsByRole", fold.refusalsByRole);
|
|
74
|
+
// Checks belong to the merge slot, so only the team series carries the median.
|
|
75
|
+
setLowerMedian(day, "checkMedianMs", fold.checkDurationsMs);
|
|
76
|
+
if (fold.checkFailures > 0)
|
|
77
|
+
day.checkFailures = fold.checkFailures;
|
|
78
|
+
setLowerMedian(day, "mergeWaitMedianMs", fold.mergeWaitsMs);
|
|
79
|
+
setLowerMedian(day, "mergeSlotMedianMs", fold.mergeDurationsMs);
|
|
80
|
+
setLowerMedian(day, "runMedianMs", fold.runDurationsMs);
|
|
81
|
+
setLowerMedian(day, "providerWaitMedianMs", fold.providerWaitsMs);
|
|
82
|
+
// A workspace wait is the pool's, not a role's, so only the team series carries it.
|
|
83
|
+
setLowerMedian(day, "slotWaitMedianMs", fold.slotWaitsMs);
|
|
84
|
+
// A check permit is the team's, not a role's, so only the team series carries it.
|
|
85
|
+
setLowerMedian(day, "checkWaitMedianMs", fold.checkWaitsMs);
|
|
86
|
+
// A review belongs to the merge slot/reviewer, not the author role, so only the team
|
|
87
|
+
// series carries the median, mirroring checkMedianMs.
|
|
88
|
+
setLowerMedian(day, "reviewMedianMs", fold.reviewDurationsMs);
|
|
89
|
+
// Only set when nonzero, so a failure-free day keeps its exact pre-feature shape.
|
|
90
|
+
setMapIfNonempty(day, "failedRunsByRole", fold.failedRunsByRole);
|
|
91
|
+
setMapIfNonempty(day, "fallbacksByRole", fold.fallbacksByRole);
|
|
92
|
+
day.commits = fold.commits;
|
|
93
|
+
day.commitsByRole = fold.commitsByRole;
|
|
94
|
+
day.workCommits = fold.workCommits;
|
|
95
|
+
day.maintenanceCommits = fold.maintenanceCommits;
|
|
96
|
+
day.costUsd = fold.costUsd;
|
|
97
|
+
// Only set when nonzero, so zero-notional days keep their exact pre-feature shape.
|
|
98
|
+
if (fold.notionalCostUsd > 0)
|
|
99
|
+
day.notionalCostUsd = fold.notionalCostUsd;
|
|
100
|
+
setMapIfNonempty(day, "notionalByRole", fold.notionalByRole);
|
|
101
|
+
// Only set when a provider actually spent (zero means absent), so a provider-free day
|
|
102
|
+
// keeps its exact pre-feature shape and `report --json` omits the maps when empty.
|
|
103
|
+
setMapIfNonempty(day, "costByProvider", fold.costByProvider);
|
|
104
|
+
setMapIfNonempty(day, "notionalByProvider", fold.notionalByProvider);
|
|
105
|
+
// Backends are a team attribution (a backend is not a role), so only the team series
|
|
106
|
+
// carries the map; only set when a backend actually spent (zero means absent), so a
|
|
107
|
+
// backend-free day keeps its exact pre-feature shape.
|
|
108
|
+
setMapIfNonempty(day, "notionalByBackend", fold.notionalByBackend);
|
|
109
|
+
// Models are a team attribution (a model is not a role), so only the team series
|
|
110
|
+
// carries the map; only set when a modelled start happened (zero means absent), so a
|
|
111
|
+
// model-less day keeps its exact pre-feature shape.
|
|
112
|
+
setMapIfNonempty(day, "runsByModel", fold.runsByModel);
|
|
113
|
+
// Deferred runs are a team attribution (a reason is not a role), so only the team
|
|
114
|
+
// series carries the map; only set when a day actually deferred (zero means absent),
|
|
115
|
+
// so a deferral-free day keeps its exact pre-feature shape.
|
|
116
|
+
setMapIfNonempty(day, "deferredByReason", fold.deferredByReason);
|
|
117
|
+
day.mergeRuns = fold.mergeRuns;
|
|
118
|
+
day.mergeTokens = fold.mergeTokens;
|
|
119
|
+
day.mergeCostUsd = fold.mergeCostUsd;
|
|
120
|
+
// Only set when nonzero, so a failure-free day keeps its exact pre-feature shape.
|
|
121
|
+
if ((fold.mergeFailed ?? 0) > 0)
|
|
122
|
+
day.mergeFailed = fold.mergeFailed;
|
|
123
|
+
// Only set when a role actually merged/failed that day (zero means absent), so a
|
|
124
|
+
// merge-free day keeps its exact pre-feature shape.
|
|
125
|
+
setMapIfNonempty(day, "mergedByRole", fold.mergedByRole);
|
|
126
|
+
setMapIfNonempty(day, "mergeFailedByRole", fold.mergeFailedByRole);
|
|
127
|
+
// Reason codes are a team attribution (a result is not a role), so only the team
|
|
128
|
+
// series carries the map; only set when nonzero, so a failure-free day keeps its
|
|
129
|
+
// exact pre-feature shape. The role branch below leaves it unset — merge runs are
|
|
130
|
+
// the merge slot's.
|
|
131
|
+
setMapIfNonempty(day, "mergeFailedByReason", fold.mergeFailedByReason);
|
|
132
|
+
}
|
|
133
|
+
/** Fill one role-scoped day from the fold's per-role maps, so the series carries only the
|
|
134
|
+
* named base role's usage, folded across an instance id's siblings, and keeps the merge
|
|
135
|
+
* fields and team-only maps unset. */
|
|
136
|
+
function applyRoleDay(day, fold, bases) {
|
|
137
|
+
// Role scope: derive this day from the fold's per-role maps, so the series carries only
|
|
138
|
+
// the named role's runs/tokens/cost and (base-role-keyed) commits. The scope is the
|
|
139
|
+
// base role, so an instance id's usage and its siblings' commits fold into one group and
|
|
140
|
+
// the numbers match the per-role cap, which is base-role keyed too. The merge fields
|
|
141
|
+
// stay unset — merge runs are the merge slot's, not this role's — and the work /
|
|
142
|
+
// maintenance split follows the base role's own tier rather than the day's whole split.
|
|
143
|
+
const commits = roleSum(fold.commitsByRole, bases);
|
|
144
|
+
day.tokensOut = roleSum(fold.tokensByRole, bases);
|
|
145
|
+
day.runsByRole = narrowToRole(fold.runsByRole, bases);
|
|
146
|
+
day.tokensByRole = narrowToRole(fold.tokensByRole, bases);
|
|
147
|
+
setMapIfNonempty(day, "promptTokensByRole", narrowToRole(fold.promptTokensByRole, bases));
|
|
148
|
+
setMapIfNonempty(day, "cacheReadTokensByRole", narrowToRole(fold.cacheReadTokensByRole, bases));
|
|
149
|
+
day.costByRole = narrowToRole(fold.costByRole, bases);
|
|
150
|
+
setMapIfNonempty(day, "rejectionsByRole", narrowToRole(fold.rejectionsByRole, bases));
|
|
151
|
+
setMapIfNonempty(day, "postMergeRejectionsByRole", narrowToRole(fold.postMergeRejectionsByRole, bases));
|
|
152
|
+
setMapIfNonempty(day, "revisionsByRole", narrowToRole(fold.revisionsByRole, bases));
|
|
153
|
+
setMapIfNonempty(day, "refusalsByRole", narrowToRole(fold.refusalsByRole, bases));
|
|
154
|
+
setMapIfNonempty(day, "failedRunsByRole", narrowToRole(fold.failedRunsByRole, bases));
|
|
155
|
+
setMapIfNonempty(day, "fallbacksByRole", narrowToRole(fold.fallbacksByRole, bases));
|
|
156
|
+
day.commits = commits;
|
|
157
|
+
day.commitsByRole = narrowToRole(fold.commitsByRole, bases);
|
|
158
|
+
const tierCommits = (tier) => bases.filter((b) => commitTier(b) === tier).reduce((n, b) => n + roleSum(fold.commitsByRole, b), 0);
|
|
159
|
+
day.workCommits = tierCommits("work");
|
|
160
|
+
day.maintenanceCommits = tierCommits("maintenance");
|
|
161
|
+
day.costUsd = roleSum(fold.costByRole, bases);
|
|
162
|
+
const notional = roleSum(fold.notionalByRole, bases);
|
|
163
|
+
if (notional > 0) {
|
|
164
|
+
day.notionalCostUsd = notional;
|
|
165
|
+
day.notionalByRole = narrowToRole(fold.notionalByRole, bases);
|
|
324
166
|
}
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
167
|
+
// Merge runs belong to the merge slot, not this role, so the role-scoped series
|
|
168
|
+
// keeps them unset (matching the merge-share fields) — the render omits the merge
|
|
169
|
+
// line for a scoped report, so these are carried only for shape consistency.
|
|
170
|
+
setMapIfNonempty(day, "mergedByRole", narrowToRole(fold.mergedByRole, bases));
|
|
171
|
+
setMapIfNonempty(day, "mergeFailedByRole", narrowToRole(fold.mergeFailedByRole, bases));
|
|
172
|
+
}
|
|
173
|
+
/** Dispatch a day's fill on the role scope: team days read the fold whole, scoped days
|
|
174
|
+
* narrow to the named base roles. */
|
|
175
|
+
function fillDayFromFold(day, fold, bases) {
|
|
176
|
+
if (bases === undefined)
|
|
177
|
+
applyTeamDay(day, fold);
|
|
178
|
+
else
|
|
179
|
+
applyRoleDay(day, fold, bases);
|
|
180
|
+
}
|
|
181
|
+
/** The merge share joins its day's tokensOut/costUsd (the budget charges these same
|
|
182
|
+
* events, so the day series must sum to the totals the budget agrees with) while the
|
|
183
|
+
* merge fields keep the reviewer's part of it visible on its own. */
|
|
184
|
+
function addMergeShareToSeries(series) {
|
|
328
185
|
for (const d of series) {
|
|
329
186
|
d.tokensOut += d.mergeTokens ?? 0;
|
|
330
187
|
d.costUsd += d.mergeCostUsd ?? 0;
|
|
331
188
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
189
|
+
}
|
|
190
|
+
/** Main's red wall-clock per day, from G1's rule (main_red_hold / failed ran main_check open a
|
|
191
|
+
* spell, a passing ran main_check closes it, an open spell runs to now). Team-only: main's
|
|
192
|
+
* red time is main's own, not any role's, so a role-scoped series leaves every day unset —
|
|
193
|
+
* exactly as checkFailures is team-only. Only set when a day actually was red (zero-absent,
|
|
194
|
+
* like checkMedianMs). */
|
|
195
|
+
function applyMainRedToSeries(series, foldEntry, days, now) {
|
|
196
|
+
const redByDay = mainRedMinutesByDay(foldEntry.redEvents, dayAt(days - 1, now).getTime(), now.getTime());
|
|
197
|
+
const redTestsByDay = mainRedFailingTestsByDay(foldEntry.redEvents, dayAt(days - 1, now).getTime(), now.getTime());
|
|
198
|
+
const redCulpritsByDay = mainRedCulpritsByDay(foldEntry.redEvents, dayAt(days - 1, now).getTime(), now.getTime());
|
|
199
|
+
for (const d of series) {
|
|
200
|
+
const ms = redByDay.get(d.date);
|
|
201
|
+
if (ms !== undefined)
|
|
202
|
+
d.mainRedMs = ms;
|
|
203
|
+
setMapIfNonempty(d, "mainRedByTest", redTestsByDay.get(d.date));
|
|
204
|
+
setMapIfNonempty(d, "mainRedByCulprit", redCulpritsByDay.get(d.date));
|
|
344
205
|
}
|
|
206
|
+
}
|
|
207
|
+
/** Count the window's backlog completions and filings onto their days. Day-granular, so a
|
|
208
|
+
* role-scoped report keeps these at zero (a date is file metadata with no role). */
|
|
209
|
+
function applyCompletionAndFilingCounts(root, byDate, role) {
|
|
345
210
|
const countOn = (date, field) => {
|
|
346
211
|
const day = byDate.get(date);
|
|
347
212
|
if (day)
|
|
@@ -377,12 +242,14 @@ export function collectReport(root, days, role) {
|
|
|
377
242
|
countOn(d, "bugsFiled");
|
|
378
243
|
}
|
|
379
244
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
245
|
+
}
|
|
246
|
+
/** Tally the fixed bugs' `Validation gap` tags over the window, so the team can see where
|
|
247
|
+
* its own tests were weakest. An entry counts only when it is in the window (the same
|
|
248
|
+
* out-of-window drop countOn applies: its completion date must be a key of byDate), and
|
|
249
|
+
* only the team report carries the tally — a tag has no role (the render gates on the
|
|
250
|
+
* same `role === undefined`). Zero-means-absent: a window with no tagged fixed bug spreads
|
|
251
|
+
* no key, so the byte-for-byte assertions on pre-feature windows hold. */
|
|
252
|
+
function applyValidationGaps(root, byDate, role) {
|
|
386
253
|
const validationGaps = {};
|
|
387
254
|
if (role === undefined) {
|
|
388
255
|
for (const e of fixedBugEntries(root)) {
|
|
@@ -396,14 +263,17 @@ export function collectReport(root, days, role) {
|
|
|
396
263
|
}
|
|
397
264
|
}
|
|
398
265
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
266
|
+
return validationGaps;
|
|
267
|
+
}
|
|
268
|
+
/** The per-day backlog lag median: how long each finished plan or bug waited from its filing
|
|
269
|
+
* stamp to its done/fixed stamp, folded per completion day — the median backlog age at
|
|
270
|
+
* which the day's items closed. Team-only (a lag has no role, like the validation tally
|
|
271
|
+
* and the filing stamps above) and zero-means-absent: a day with no lag-carrying completion
|
|
272
|
+
* keeps its exact pre-feature shape, and an out-of-window completion date drops out (it
|
|
273
|
+
* must be a byDate key, the same drop countOn applies). The same entry detection and date
|
|
274
|
+
* rules (sectionCompletionLags / entryLags) the completion and filing reads use, so the
|
|
275
|
+
* spans pair the days `featuresDone`/`bugsFixed` count. */
|
|
276
|
+
function applyBacklogLags(root, byDate, role) {
|
|
407
277
|
if (role === undefined) {
|
|
408
278
|
const lagsByDay = new Map();
|
|
409
279
|
const foldLag = (date, lagDays) => {
|
|
@@ -426,6 +296,11 @@ export function collectReport(root, days, role) {
|
|
|
426
296
|
for (const [date, lags] of lagsByDay)
|
|
427
297
|
byDate.get(date).backlogLagMedianDays = lowerMedian(lags);
|
|
428
298
|
}
|
|
299
|
+
}
|
|
300
|
+
/** Reduce the filled day series to the report's totals, plus the nonzero-only fields the
|
|
301
|
+
* return literal folds into `ReportData.totals` (notional spend, merge failures, plans/bugs
|
|
302
|
+
* filings, backend attribution). */
|
|
303
|
+
function foldSeriesTotals(series) {
|
|
429
304
|
const totals = {
|
|
430
305
|
tokensOut: 0,
|
|
431
306
|
runs: 0,
|
|
@@ -466,18 +341,47 @@ export function collectReport(root, days, role) {
|
|
|
466
341
|
totals.mergeCostUsd += d.mergeCostUsd ?? 0;
|
|
467
342
|
mergeFailed += d.mergeFailed ?? 0;
|
|
468
343
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
344
|
+
return { totals, notionalCostUsd, mergeFailed, plansAdded, bugsFiled, notionalByBackend };
|
|
345
|
+
}
|
|
346
|
+
/** The shared coverage proof (event-window.ts), rebuilt from the memoized read's pinned
|
|
347
|
+
* parts: the day-keyed proof (fileCovers — a property of the retained bytes and fromKey,
|
|
348
|
+
* both in the cache key), the vacuous empty-log arm, and the oldest retained event's ts.
|
|
349
|
+
* The window's first local midnight is the cutoff: an event at that instant is the
|
|
350
|
+
* earliest the window could contain, so the same-day arm degenerates harmlessly. Without
|
|
351
|
+
* this field the day report was the one windowed consumer that could undercount in
|
|
352
|
+
* silence — the --since report and the failure digest both carry the note, and a silent
|
|
353
|
+
* number invites an operator to trust a truncated window as an idle team. */
|
|
354
|
+
function windowCoverage(foldEntry, days, now) {
|
|
477
355
|
const cutoffMs = dayAt(days - 1, now).getTime();
|
|
478
|
-
|
|
356
|
+
return (foldEntry.fileCovers ||
|
|
479
357
|
!foldEntry.hadEvents ||
|
|
480
|
-
(foldEntry.firstEventTs !== undefined && foldEntry.firstEventTs <= cutoffMs);
|
|
358
|
+
(foldEntry.firstEventTs !== undefined && foldEntry.firstEventTs <= cutoffMs));
|
|
359
|
+
}
|
|
360
|
+
/** Aggregate team usage over exactly `days` local calendar days ending today, from the event
|
|
361
|
+
* log (run_end/merged) and the backlog history (`## Done` in PLANS.md and PLANS-DONE.md,
|
|
362
|
+
* `## Fixed` in BUGS.md and BUGS-FIXED.md). */
|
|
363
|
+
export function collectReport(root, days, role) {
|
|
364
|
+
const bases = scopeBases(role);
|
|
365
|
+
const now = new Date();
|
|
366
|
+
// One captured instant for the whole window, so every day key derives from the same day.
|
|
367
|
+
const { from, to } = dayWindow(days, now);
|
|
368
|
+
const { series, byDate } = buildEmptySeries(days, now);
|
|
369
|
+
const foldEntry = foldWindowEvents(root, from);
|
|
370
|
+
for (const [dayKey, fold] of foldEntry.days) {
|
|
371
|
+
const day = byDate.get(dayKey);
|
|
372
|
+
if (!day)
|
|
373
|
+
continue; // Out-of-window day key (e.g. future-dated) — no series slot to fill.
|
|
374
|
+
fillDayFromFold(day, fold, bases);
|
|
375
|
+
}
|
|
376
|
+
addMergeShareToSeries(series);
|
|
377
|
+
// Team-only: main's red time is main's own, not any role's (see applyMainRedToSeries).
|
|
378
|
+
if (bases === undefined)
|
|
379
|
+
applyMainRedToSeries(series, foldEntry, days, now);
|
|
380
|
+
applyCompletionAndFilingCounts(root, byDate, role);
|
|
381
|
+
const validationGaps = applyValidationGaps(root, byDate, role);
|
|
382
|
+
applyBacklogLags(root, byDate, role);
|
|
383
|
+
const { totals, notionalCostUsd, mergeFailed, plansAdded, bugsFiled, notionalByBackend } = foldSeriesTotals(series);
|
|
384
|
+
const coversFullWindow = windowCoverage(foldEntry, days, now);
|
|
481
385
|
return {
|
|
482
386
|
days,
|
|
483
387
|
from,
|