code-coordinator 0.5.46__py3-none-any.whl
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.
- code_coordinator-0.5.46.dist-info/METADATA +625 -0
- code_coordinator-0.5.46.dist-info/RECORD +295 -0
- code_coordinator-0.5.46.dist-info/WHEEL +5 -0
- code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
- code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
- code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
- coord/__init__.py +176 -0
- coord/_board_mapping.py +229 -0
- coord/acceptance.py +468 -0
- coord/acceptance_drivers.py +632 -0
- coord/agent.py +7517 -0
- coord/agent_app.py +1555 -0
- coord/agent_update.py +417 -0
- coord/agents/opencode/.gitignore +13 -0
- coord/agents/opencode/agents/work.md +129 -0
- coord/agents/opencode/routing.jsonc +49 -0
- coord/audit.py +301 -0
- coord/auto_loop.py +1440 -0
- coord/board_bool_guard.py +72 -0
- coord/board_service.py +141 -0
- coord/board_wire.py +309 -0
- coord/brain.py +581 -0
- coord/branch_model.py +214 -0
- coord/cargo_cache.py +258 -0
- coord/ci_github.py +386 -0
- coord/ci_store.py +560 -0
- coord/claim.py +353 -0
- coord/cli.py +454 -0
- coord/client.py +610 -0
- coord/commands/__init__.py +1 -0
- coord/commands/_common.py +329 -0
- coord/commands/acceptance.py +916 -0
- coord/commands/agent_ops.py +1339 -0
- coord/commands/audit.py +131 -0
- coord/commands/chat.py +320 -0
- coord/commands/dispatch.py +1780 -0
- coord/commands/dispatch_workers.py +4894 -0
- coord/commands/drive.py +616 -0
- coord/commands/drive_queue.py +1203 -0
- coord/commands/gate_a.py +217 -0
- coord/commands/gates.py +89 -0
- coord/commands/issues.py +681 -0
- coord/commands/lifecycle.py +513 -0
- coord/commands/merge.py +1900 -0
- coord/commands/milestone.py +2081 -0
- coord/commands/plan_followup.py +1243 -0
- coord/commands/plans.py +156 -0
- coord/commands/release.py +2232 -0
- coord/commands/report.py +341 -0
- coord/commands/review.py +1523 -0
- coord/commands/scorecard.py +252 -0
- coord/commands/sessions.py +1930 -0
- coord/commands/setup.py +576 -0
- coord/commands/status.py +2089 -0
- coord/commands/terminal.py +385 -0
- coord/commands/test_gate.py +775 -0
- coord/commands/tui.py +288 -0
- coord/comments.py +718 -0
- coord/config.py +3032 -0
- coord/conflict_fix.py +633 -0
- coord/dao.py +483 -0
- coord/dashboard/__init__.py +0 -0
- coord/dashboard/fixture.py +376 -0
- coord/dashboard/index.html +658 -0
- coord/dashboard/server.py +1894 -0
- coord/dashboard/terminal.py +382 -0
- coord/dashboard/webapp/.gitignore +9 -0
- coord/dashboard/webapp/components.json +17 -0
- coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
- coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
- coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
- coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
- coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
- coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
- coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
- coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
- coord/dashboard/webapp/dist/icons/icon.svg +5 -0
- coord/dashboard/webapp/dist/index.html +38 -0
- coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
- coord/dashboard/webapp/dist/sw.js +1 -0
- coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
- coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
- coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
- coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
- coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
- coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
- coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
- coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
- coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
- coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
- coord/dashboard/webapp/eslint.config.js +20 -0
- coord/dashboard/webapp/index.html +37 -0
- coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
- coord/dashboard/webapp/package-lock.json +10584 -0
- coord/dashboard/webapp/package.json +63 -0
- coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
- coord/dashboard/webapp/playwright.config.ts +93 -0
- coord/dashboard/webapp/postcss.config.js +6 -0
- coord/dashboard/webapp/public/icons/icon-192.png +0 -0
- coord/dashboard/webapp/public/icons/icon-512.png +0 -0
- coord/dashboard/webapp/public/icons/icon.svg +5 -0
- coord/dashboard/webapp/src/App.tsx +140 -0
- coord/dashboard/webapp/src/api/client.ts +199 -0
- coord/dashboard/webapp/src/api/generated.ts +176 -0
- coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
- coord/dashboard/webapp/src/components/Detail.tsx +800 -0
- coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
- coord/dashboard/webapp/src/components/Home.tsx +435 -0
- coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
- coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
- coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
- coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
- coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
- coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
- coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
- coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
- coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
- coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
- coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
- coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
- coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
- coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
- coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
- coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
- coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
- coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
- coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
- coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
- coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
- coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
- coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
- coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
- coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
- coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
- coord/dashboard/webapp/src/index.css +210 -0
- coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
- coord/dashboard/webapp/src/lib/utils.ts +6 -0
- coord/dashboard/webapp/src/main.tsx +46 -0
- coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
- coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
- coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
- coord/dashboard/webapp/src/realtime/connection.ts +227 -0
- coord/dashboard/webapp/src/realtime/events.ts +100 -0
- coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
- coord/dashboard/webapp/src/routes/paths.ts +92 -0
- coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
- coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
- coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
- coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
- coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
- coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
- coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
- coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
- coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
- coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
- coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
- coord/dashboard/webapp/src/shell/railItems.ts +105 -0
- coord/dashboard/webapp/src/shell/shellState.ts +174 -0
- coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
- coord/dashboard/webapp/src/test-setup.ts +41 -0
- coord/dashboard/webapp/src/vite-env.d.ts +2 -0
- coord/dashboard/webapp/tailwind.config.js +140 -0
- coord/dashboard/webapp/tsconfig.json +25 -0
- coord/dashboard/webapp/tsconfig.node.json +11 -0
- coord/dashboard/webapp/vite.config.ts +71 -0
- coord/db.py +1076 -0
- coord/dead_end.py +332 -0
- coord/deploy/README.md +33 -0
- coord/deploy/coord-agent.service +89 -0
- coord/deploy/coord-db-backup.service +60 -0
- coord/deploy/coord-db-backup.sh +74 -0
- coord/deploy/coord-db-backup.timer +18 -0
- coord/deploy/coord-drive-queue.service +117 -0
- coord/deploy/coord-drive-queue.timer +39 -0
- coord/deploy/coord-notify.service +48 -0
- coord/deploy/coord-notify.timer +24 -0
- coord/deploy/coord-release-propagate.service +83 -0
- coord/deploy/coord-release-propagate.timer +38 -0
- coord/deploy/coord-release-window.service +119 -0
- coord/deploy/coord-release-window.timer +36 -0
- coord/deploy/coord-serve.service +82 -0
- coord/deploy/coord-web-dist-build.service +43 -0
- coord/deploy/coord-web-dist-build.timer +36 -0
- coord/deploy/coord-web.service +125 -0
- coord/deploy_manifest.py +80 -0
- coord/deploy_units.py +384 -0
- coord/deps.py +115 -0
- coord/diagnose.py +1623 -0
- coord/dispatch.py +1009 -0
- coord/dist_name.py +123 -0
- coord/drive.py +3101 -0
- coord/drive_queue.py +2298 -0
- coord/drive_state.py +870 -0
- coord/events.py +381 -0
- coord/failure_class.py +914 -0
- coord/filelock.py +168 -0
- coord/fleet_config_health.py +300 -0
- coord/freshness.py +206 -0
- coord/gate_a.py +469 -0
- coord/gate_b.py +411 -0
- coord/gate_snapshot.py +385 -0
- coord/gates.py +582 -0
- coord/github_ops.py +1954 -0
- coord/goal.py +125 -0
- coord/graph_health.py +348 -0
- coord/health/__init__.py +69 -0
- coord/health/aggregate.py +129 -0
- coord/health/checks/__init__.py +13 -0
- coord/health/checks/agent_install.py +280 -0
- coord/health/checks/cargo_targets.py +171 -0
- coord/health/checks/claude_binary.py +65 -0
- coord/health/checks/deploy_lane_facts.py +458 -0
- coord/health/checks/disk.py +99 -0
- coord/health/checks/fleet_board.py +89 -0
- coord/health/checks/fleet_deploy_lanes.py +469 -0
- coord/health/checks/fleet_phantom.py +69 -0
- coord/health/checks/fleet_unit_drift.py +151 -0
- coord/health/checks/graph.py +192 -0
- coord/health/checks/plan_usage.py +88 -0
- coord/health/checks/repo_state.py +161 -0
- coord/health/checks/spawned_coord.py +465 -0
- coord/health/checks/timer_active.py +254 -0
- coord/health/checks/toolchain.py +547 -0
- coord/health/checks/unit_drift.py +648 -0
- coord/health/checks/unit_enablement.py +171 -0
- coord/health/checks/worktrees.py +96 -0
- coord/health/cli.py +121 -0
- coord/health/context.py +106 -0
- coord/health/fleet_snapshot.py +477 -0
- coord/health/models.py +250 -0
- coord/health/pypi.py +231 -0
- coord/health/registry.py +240 -0
- coord/health/render.py +82 -0
- coord/health/units.py +60 -0
- coord/hooks.py +106 -0
- coord/housekeeping.py +204 -0
- coord/interactive.py +4286 -0
- coord/issue_store.py +1496 -0
- coord/liveness_auditor.py +293 -0
- coord/machine_pause.py +755 -0
- coord/merge_queue.py +4681 -0
- coord/milestone_chat.py +600 -0
- coord/milestone_dispatch.py +943 -0
- coord/milestone_gate.py +709 -0
- coord/milestone_order.py +840 -0
- coord/mock_author.py +334 -0
- coord/models.py +891 -0
- coord/network.py +269 -0
- coord/new_issue_chat.py +229 -0
- coord/notify.py +3226 -0
- coord/openapi.py +404 -0
- coord/overlap_fence.py +133 -0
- coord/parentage.py +200 -0
- coord/parentage_github.py +58 -0
- coord/pipeline.py +481 -0
- coord/plan_parser.py +266 -0
- coord/plans.py +543 -0
- coord/platform_paths.py +43 -0
- coord/pr_body_lint.py +67 -0
- coord/prereqs.py +533 -0
- coord/progress.py +425 -0
- coord/providers/__init__.py +683 -0
- coord/providers/base.py +218 -0
- coord/providers/claude.py +284 -0
- coord/providers/claude_pty.py +610 -0
- coord/providers/opencode.py +896 -0
- coord/reconcile.py +2233 -0
- coord/refine_chat.py +485 -0
- coord/release_cordon.py +525 -0
- coord/release_propagate.py +1176 -0
- coord/release_verify.py +777 -0
- coord/release_window.py +322 -0
- coord/reports.py +1643 -0
- coord/revalidate.py +1101 -0
- coord/review.py +3317 -0
- coord/scorecard.py +484 -0
- coord/serve_app.py +7192 -0
- coord/skills/update-issue/SKILL.md +93 -0
- coord/smoke.py +1030 -0
- coord/split_work.py +210 -0
- coord/stage_projection.py +650 -0
- coord/state.py +5720 -0
- coord/test_author.py +1064 -0
- coord/test_chat.py +352 -0
- coord/test_orchestrator.py +494 -0
- coord/test_report.py +178 -0
- coord/tui_release.py +271 -0
- coord/usage.py +753 -0
- coord/usage_limits.py +358 -0
- coord/usage_rollup.py +709 -0
- coord/worker_events.py +954 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""`coord scorecard` — the dogfood scorecard (#1559): per-issue and
|
|
2
|
+
per-milestone evidence for "is this good enough to sell?", not just code.
|
|
3
|
+
|
|
4
|
+
**Why a new command, not an extension of ``coord usage``** (#1559 asks for
|
|
5
|
+
this to be justified): ``coord.usage_rollup``'s public contract (#1118) is a
|
|
6
|
+
*pure* cost/token/duration aggregator with no I/O and no notion of GitHub
|
|
7
|
+
labels or human-attended dispatch shape — that boundary is what lets
|
|
8
|
+
``coord usage``, the TUI Time view, and this command all share one rollup
|
|
9
|
+
without three reimplementations. Grafting first-pass-acceptance,
|
|
10
|
+
intervention classification, and label-sourced escaped-defect/process-bug
|
|
11
|
+
reads onto it would blow that boundary open for a use case ``coord usage``
|
|
12
|
+
callers don't need. Scorecard instead *composes* three existing read seams
|
|
13
|
+
exactly once each, at this CLI layer, and hands the results to the pure
|
|
14
|
+
:mod:`coord.scorecard` aggregator:
|
|
15
|
+
|
|
16
|
+
- :func:`coord.usage.fetch_usage_rows` — the same board-row fetch
|
|
17
|
+
``coord usage`` uses (daemon ``/board`` when configured, local DB else).
|
|
18
|
+
- :func:`coord.state.list_audit_log` — a best-effort durability cross-check
|
|
19
|
+
for "merged" (see :mod:`coord.scorecard`'s docstring). Paginated back to
|
|
20
|
+
the milestone's own creation date (see ``_AUDIT_LOG_MAX_PAGES`` below) —
|
|
21
|
+
a single newest-first page can't be trusted to reach an old milestone in
|
|
22
|
+
a repo with a long merge history.
|
|
23
|
+
- :func:`coord.github_ops.get_milestone_issues` — milestone membership
|
|
24
|
+
(open+closed) with labels, the source for the two label conventions.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json as _json
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
import click
|
|
33
|
+
|
|
34
|
+
from coord.commands._common import _CONFIG_OPTION, _load_config
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# The audit log is paginated at audit.MAX_LIMIT (500) per page, newest-first.
|
|
38
|
+
# For a repo with a long merge history, "the 500 most recent merged events"
|
|
39
|
+
# may not reach back to an older milestone at all — silently emptying the
|
|
40
|
+
# merge-durability cross-check for exactly the historical-validation case
|
|
41
|
+
# #1559 cares about. We bound the query with the milestone's own creation
|
|
42
|
+
# date (nothing can have merged for it before it existed) and paginate the
|
|
43
|
+
# rest of the way back to that bound, capped so one CLI invocation can't
|
|
44
|
+
# turn into an unbounded DB scan.
|
|
45
|
+
_AUDIT_LOG_PAGE_SIZE = 500
|
|
46
|
+
_AUDIT_LOG_MAX_PAGES = 20 # 20 * 500 = 10,000 entries
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_github_timestamp(value: object) -> float | None:
|
|
50
|
+
"""Parse a GitHub REST API ISO-8601 timestamp (e.g.
|
|
51
|
+
``"2024-01-01T00:00:00Z"``) into a Unix epoch float. Returns ``None`` for
|
|
52
|
+
missing/malformed input so callers degrade to "no time bound" instead of
|
|
53
|
+
raising — the audit cross-check is best-effort end to end."""
|
|
54
|
+
if not value:
|
|
55
|
+
return None
|
|
56
|
+
from datetime import datetime # noqa: PLC0415
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
return datetime.fromisoformat(str(value).replace("Z", "+00:00")).timestamp()
|
|
60
|
+
except (TypeError, ValueError):
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _fmt_usd(usd: float) -> str:
|
|
65
|
+
return f"${usd:.2f}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _fmt_hms(secs: float) -> str:
|
|
69
|
+
total = int(round(secs))
|
|
70
|
+
h, rem = divmod(total, 3600)
|
|
71
|
+
m, s = divmod(rem, 60)
|
|
72
|
+
if h:
|
|
73
|
+
return f"{h}h{m:02d}m"
|
|
74
|
+
if m:
|
|
75
|
+
return f"{m}m{s:02d}s"
|
|
76
|
+
return f"{s}s"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _fmt_rate(rate: float | None) -> str:
|
|
80
|
+
return "n/a" if rate is None else f"{rate * 100:.0f}%"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _fmt_first_pass(value: str) -> str:
|
|
84
|
+
return {"yes": "yes", "no": "no", "unknown": "?"}.get(value, value)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def format_scorecard(card, *, verbose: bool = False) -> str:
|
|
88
|
+
"""Human-readable rendering of a :class:`coord.scorecard.MilestoneScorecard`."""
|
|
89
|
+
t = card.totals
|
|
90
|
+
lines = [
|
|
91
|
+
f"DOGFOOD SCORECARD — {card.repo_name} milestone #{card.milestone} "
|
|
92
|
+
f"\"{card.milestone_title}\" ({t['issue_count']} issues)",
|
|
93
|
+
"",
|
|
94
|
+
f"First-pass acceptance: {t['first_pass']['yes']}/"
|
|
95
|
+
f"{t['first_pass']['yes'] + t['first_pass']['no']} = "
|
|
96
|
+
f"{_fmt_rate(t['first_pass']['rate'])}"
|
|
97
|
+
f" (unknown: {t['first_pass']['unknown']})",
|
|
98
|
+
f"Human interventions: {t['interventions']['total']}"
|
|
99
|
+
f" (fix={t['interventions']['by_kind']['fix']}"
|
|
100
|
+
f" rescue={t['interventions']['by_kind']['rescue']}"
|
|
101
|
+
f" nudge={t['interventions']['by_kind']['nudge']}"
|
|
102
|
+
f" abandon={t['interventions']['by_kind']['abandon']})"
|
|
103
|
+
f" [{t['interventions']['issues_with_unknown_data']} issues have no"
|
|
104
|
+
f" board data — excluded from the count above;"
|
|
105
|
+
f" {t['interventions']['issues_with_multiple_roots']} issues have a"
|
|
106
|
+
f" second unrelated root dispatch — first_pass=no but not counted"
|
|
107
|
+
" in any kind above]",
|
|
108
|
+
f"Cost + wall-clock: {_fmt_usd(t['cost']['total_usd'])} /"
|
|
109
|
+
f" {_fmt_hms(t['cost']['duration_secs'])}"
|
|
110
|
+
f" ({t['cost']['issues_with_data']}/{t['issue_count']} issues have"
|
|
111
|
+
f" cost data; {t['cost']['issues_without_data']} unknown)",
|
|
112
|
+
"Escaped defects: "
|
|
113
|
+
+ " ".join(
|
|
114
|
+
f"{stage}={t['escaped_defects']['by_stage'][stage]}"
|
|
115
|
+
for stage in t["escaped_defects"]["by_stage"]
|
|
116
|
+
)
|
|
117
|
+
+ f" ({t['escaped_defects']['issues_unlabeled']} issues unlabeled —"
|
|
118
|
+
" NOT the same as zero)",
|
|
119
|
+
f"Process bugs surfaced: {t['process_bugs']['count']}"
|
|
120
|
+
f" regression test: landed={t['process_bugs']['regression_test']['landed']}"
|
|
121
|
+
f" missing={t['process_bugs']['regression_test']['missing']}"
|
|
122
|
+
f" unknown={t['process_bugs']['regression_test']['unknown']}",
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
if verbose:
|
|
126
|
+
lines.append("")
|
|
127
|
+
lines.append(
|
|
128
|
+
f"{'#':<8}{'FIRST-PASS':<11}{'INTERVENTIONS':<15}{'COST':<10}"
|
|
129
|
+
f"{'TIME':<9}{'ESCAPED':<12}{'PROCESS-BUG':<14}TITLE"
|
|
130
|
+
)
|
|
131
|
+
for c in card.issues:
|
|
132
|
+
interv = sum(c.interventions.values())
|
|
133
|
+
interv_str = f"{interv}" if c.has_assignment_data else "?"
|
|
134
|
+
if c.multiple_roots:
|
|
135
|
+
interv_str += "+multiroot"
|
|
136
|
+
cost_str = _fmt_usd(c.cost_total) if c.has_cost_data else "?"
|
|
137
|
+
time_str = _fmt_hms(c.duration_secs) if c.has_cost_data else "?"
|
|
138
|
+
escaped_str = c.escaped_defect_stage or "-"
|
|
139
|
+
pb_str = (
|
|
140
|
+
f"{c.regression_test}" if c.process_bug else "-"
|
|
141
|
+
)
|
|
142
|
+
title = c.title if len(c.title) <= 40 else c.title[:39] + "…"
|
|
143
|
+
lines.append(
|
|
144
|
+
f"#{c.number:<7}{_fmt_first_pass(c.first_pass):<11}{interv_str:<15}"
|
|
145
|
+
f"{cost_str:<10}{time_str:<9}{escaped_str:<12}{pb_str:<14}{title}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return "\n".join(lines)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@click.command(
|
|
152
|
+
"scorecard",
|
|
153
|
+
help=(
|
|
154
|
+
"The dogfood scorecard (#1559): first-pass acceptance, human "
|
|
155
|
+
"interventions, cost + wall-clock, escaped defects, and process bugs "
|
|
156
|
+
"surfaced — per issue and aggregated, for one GitHub milestone.\n\n"
|
|
157
|
+
"REPO is the local repo name from coordinator.yml; MILESTONE is the "
|
|
158
|
+
"GitHub milestone NUMBER (not the tracking-issue number)."
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
@click.argument("repo")
|
|
162
|
+
@click.argument("milestone", type=int)
|
|
163
|
+
@click.option(
|
|
164
|
+
"--verbose", "-v", is_flag=True, default=False,
|
|
165
|
+
help="Also print the per-issue table (human output only; --json always includes it).",
|
|
166
|
+
)
|
|
167
|
+
@click.option("--json", "output_json", is_flag=True, default=False, help="Output machine-readable JSON.")
|
|
168
|
+
@_CONFIG_OPTION
|
|
169
|
+
def scorecard(
|
|
170
|
+
repo: str,
|
|
171
|
+
milestone: int,
|
|
172
|
+
verbose: bool,
|
|
173
|
+
output_json: bool,
|
|
174
|
+
config_path: Path,
|
|
175
|
+
) -> None:
|
|
176
|
+
from coord import github_ops # noqa: PLC0415
|
|
177
|
+
from coord.scorecard import build_milestone_scorecard, scorecard_to_dict # noqa: PLC0415
|
|
178
|
+
from coord.state import list_audit_log # noqa: PLC0415
|
|
179
|
+
from coord.usage import fetch_usage_rows, pricing_dict_from_config # noqa: PLC0415
|
|
180
|
+
|
|
181
|
+
cfg = _load_config(config_path)
|
|
182
|
+
repo_entry = cfg.repo(repo)
|
|
183
|
+
if repo_entry is None:
|
|
184
|
+
click.echo(f"error: unknown repo {repo!r} (not in coordinator.yml)", err=True)
|
|
185
|
+
raise SystemExit(2)
|
|
186
|
+
slug = repo_entry.github
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
ms = github_ops.get_milestone(slug, milestone)
|
|
190
|
+
except Exception as e: # noqa: BLE001 — surface a clean CLI error
|
|
191
|
+
click.echo(f"error: could not resolve milestone {milestone} on {slug}: {e}", err=True)
|
|
192
|
+
raise SystemExit(1) from e
|
|
193
|
+
ms_title = ms.get("title") or str(milestone)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
issues = github_ops.get_milestone_issues(slug, ms_title, state="all")
|
|
197
|
+
except Exception as e: # noqa: BLE001
|
|
198
|
+
click.echo(f"error: could not list issues for milestone {ms_title!r}: {e}", err=True)
|
|
199
|
+
raise SystemExit(1) from e
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
assignment_rows = fetch_usage_rows()
|
|
203
|
+
except Exception as e: # noqa: BLE001 — board data is best-effort here;
|
|
204
|
+
# an unreachable daemon shouldn't block the label-only metrics.
|
|
205
|
+
click.echo(f"warning: could not fetch board assignment rows: {e}", err=True)
|
|
206
|
+
assignment_rows = []
|
|
207
|
+
|
|
208
|
+
# Best-effort durability cross-check (see coord.scorecard's docstring) —
|
|
209
|
+
# never fatal; a slow/unreachable audit read just means the cross-check
|
|
210
|
+
# is skipped, board `status="merged"` remains the primary signal. Bound
|
|
211
|
+
# by the milestone's own creation date and paginate back to that bound
|
|
212
|
+
# (see _AUDIT_LOG_* constants above) rather than trusting a single
|
|
213
|
+
# newest-first page to reach an old milestone.
|
|
214
|
+
audit_entries: list[dict] = []
|
|
215
|
+
since_ts = _parse_github_timestamp(ms.get("created_at"))
|
|
216
|
+
try:
|
|
217
|
+
cursor: str | None = None
|
|
218
|
+
for _page in range(_AUDIT_LOG_MAX_PAGES):
|
|
219
|
+
audit_result = list_audit_log(
|
|
220
|
+
repo=repo, category="merge", event_type="merged",
|
|
221
|
+
since=since_ts, limit=_AUDIT_LOG_PAGE_SIZE, cursor=cursor,
|
|
222
|
+
)
|
|
223
|
+
audit_entries.extend(audit_result.get("entries") or [])
|
|
224
|
+
cursor = audit_result.get("next_cursor")
|
|
225
|
+
if not audit_result.get("has_more") or not cursor:
|
|
226
|
+
break
|
|
227
|
+
else:
|
|
228
|
+
click.echo(
|
|
229
|
+
"warning: audit log merge cross-check stopped after "
|
|
230
|
+
f"{_AUDIT_LOG_MAX_PAGES * _AUDIT_LOG_PAGE_SIZE} entries "
|
|
231
|
+
"(more were available) — durability cross-check may be "
|
|
232
|
+
"incomplete for the oldest issues in this milestone",
|
|
233
|
+
err=True,
|
|
234
|
+
)
|
|
235
|
+
except Exception as e: # noqa: BLE001
|
|
236
|
+
click.echo(f"warning: could not fetch audit log: {e}", err=True)
|
|
237
|
+
|
|
238
|
+
card = build_milestone_scorecard(
|
|
239
|
+
milestone=milestone,
|
|
240
|
+
milestone_title=ms_title,
|
|
241
|
+
repo_name=repo,
|
|
242
|
+
issues=issues,
|
|
243
|
+
assignment_rows=assignment_rows,
|
|
244
|
+
audit_entries=audit_entries,
|
|
245
|
+
pricing=pricing_dict_from_config(cfg.pricing),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if output_json:
|
|
249
|
+
click.echo(_json.dumps(scorecard_to_dict(card), indent=2, default=str))
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
click.echo(format_scorecard(card, verbose=verbose))
|