|
6 | 6 | import argparse |
7 | 7 | import json |
8 | 8 | import os |
| 9 | +from collections import Counter |
9 | 10 |
|
10 | 11 | INSTALL_MAP = { |
11 | 12 | "binary": "Binary download", |
|
14 | 15 | "cached": "Already installed", |
15 | 16 | } |
16 | 17 |
|
| 18 | +LIST_PREVIEW_LIMIT = 12 |
| 19 | +TRACE_PREVIEW_LIMIT = 20 |
| 20 | +REASON_COUNT_PREVIEW_LIMIT = 8 |
| 21 | + |
17 | 22 | # Map reason codes to human-readable descriptions |
18 | 23 | REASON_DESCRIPTIONS = { |
19 | 24 | "FILE_KIND_RUST_SRC": "Rust source file changed", |
@@ -101,6 +106,45 @@ def collect_active_reason_ids(surfaces: dict) -> list[int]: |
101 | 106 | return sorted(reason_ids) |
102 | 107 |
|
103 | 108 |
|
| 109 | +def preview_items(items: list[str], limit: int = LIST_PREVIEW_LIMIT) -> str: |
| 110 | + """Render a stable preview for a potentially large list.""" |
| 111 | + if not items: |
| 112 | + return "none" |
| 113 | + |
| 114 | + preview = items[: max(limit, 1)] |
| 115 | + rendered = ", ".join(preview) |
| 116 | + if len(items) <= limit: |
| 117 | + return rendered |
| 118 | + return f"{rendered}, ... +{len(items) - limit} more" |
| 119 | + |
| 120 | + |
| 121 | +def summarize_reason_counts(trace: list[dict]) -> list[tuple[str, int]]: |
| 122 | + """Count reason codes across the full trace.""" |
| 123 | + counts = Counter(item.get("code", "UNKNOWN") for item in trace) |
| 124 | + return sorted(counts.items(), key=lambda item: (-item[1], item[0])) |
| 125 | + |
| 126 | + |
| 127 | +def render_trace_entry(item: dict) -> str: |
| 128 | + """Render a single raw trace entry.""" |
| 129 | + rid = item.get("id") |
| 130 | + code = item.get("code", "") |
| 131 | + file_path = item.get("file") |
| 132 | + crate_name = item.get("crate") |
| 133 | + depends_on = item.get("depends_on") |
| 134 | + surface = item.get("surface") |
| 135 | + |
| 136 | + parts = [f"r{rid}", code] |
| 137 | + if file_path: |
| 138 | + parts.append(f"file={file_path}") |
| 139 | + if crate_name: |
| 140 | + parts.append(f"crate={crate_name}") |
| 141 | + if depends_on: |
| 142 | + parts.append(f"depends_on={depends_on}") |
| 143 | + if surface: |
| 144 | + parts.append(f"surface={surface}") |
| 145 | + return f"- {' '.join(parts)}" |
| 146 | + |
| 147 | + |
104 | 148 | def render(args: argparse.Namespace, plan: dict) -> str: |
105 | 149 | files = [f.get("path", "") for f in plan.get("files", []) if f.get("path")] |
106 | 150 | impact = plan.get("impact", {}) |
@@ -140,37 +184,37 @@ def render(args: argparse.Namespace, plan: dict) -> str: |
140 | 184 | lines.append("") |
141 | 185 |
|
142 | 186 | if direct: |
143 | | - lines.append(f"**Changed direct crates:** `{ ' '.join(direct) }`") |
| 187 | + lines.append(f"**Changed direct crates ({len(direct)}):** `{preview_items(direct)}`") |
144 | 188 | if scope_mode == "crates" and scope_crates: |
145 | | - lines.append(f"**Execution crates:** `{ ' '.join(scope_crates) }`") |
| 189 | + lines.append(f"**Execution crates ({len(scope_crates)}):** `{preview_items(scope_crates)}`") |
146 | 190 | elif scope_mode == "workspace": |
147 | 191 | lines.append("**Execution scope:** full workspace") |
148 | 192 | if active_custom_surfaces: |
149 | | - lines.append(f"**Active custom surfaces:** `{ ' '.join(active_custom_surfaces) }`") |
| 193 | + lines.append(f"**Active custom surfaces:** `{preview_items(active_custom_surfaces)}`") |
150 | 194 | lines.append(f"**Top reasons:** {top_reasons}") |
151 | 195 |
|
152 | 196 | lines.append("") |
153 | | - lines.append("<details><summary>Trace details (file -> crate -> surface)</summary>") |
| 197 | + lines.append(f"<details><summary>Trace summary ({len(trace)} reasons)</summary>") |
154 | 198 | lines.append("") |
155 | 199 |
|
156 | | - for item in trace: |
157 | | - rid = item.get("id") |
158 | | - code = item.get("code", "") |
159 | | - file_path = item.get("file") |
160 | | - crate_name = item.get("crate") |
161 | | - depends_on = item.get("depends_on") |
162 | | - surface = item.get("surface") |
163 | | - |
164 | | - parts = [f"r{rid}", code] |
165 | | - if file_path: |
166 | | - parts.append(f"file={file_path}") |
167 | | - if crate_name: |
168 | | - parts.append(f"crate={crate_name}") |
169 | | - if depends_on: |
170 | | - parts.append(f"depends_on={depends_on}") |
171 | | - if surface: |
172 | | - parts.append(f"surface={surface}") |
173 | | - lines.append(f"- {' '.join(parts)}") |
| 200 | + reason_counts = summarize_reason_counts(trace) |
| 201 | + if reason_counts: |
| 202 | + lines.append("**Reason counts**") |
| 203 | + for code, count in reason_counts[:REASON_COUNT_PREVIEW_LIMIT]: |
| 204 | + desc = REASON_DESCRIPTIONS.get(code, code) |
| 205 | + lines.append(f"- {desc}: {count}") |
| 206 | + if len(reason_counts) > REASON_COUNT_PREVIEW_LIMIT: |
| 207 | + lines.append(f"- ... +{len(reason_counts) - REASON_COUNT_PREVIEW_LIMIT} more reason types") |
| 208 | + lines.append("") |
| 209 | + |
| 210 | + if trace: |
| 211 | + lines.append(f"**Sample trace entries ({min(len(trace), TRACE_PREVIEW_LIMIT)} of {len(trace)})**") |
| 212 | + for item in trace[:TRACE_PREVIEW_LIMIT]: |
| 213 | + lines.append(render_trace_entry(item)) |
| 214 | + if len(trace) > TRACE_PREVIEW_LIMIT: |
| 215 | + lines.append(f"- ... +{len(trace) - TRACE_PREVIEW_LIMIT} more trace entries") |
| 216 | + else: |
| 217 | + lines.append("No trace reasons.") |
174 | 218 |
|
175 | 219 | lines.append("") |
176 | 220 | lines.append("</details>") |
|
0 commit comments