Skip to content

Commit e3726d4

Browse files
committed
cargo-rail-action: condense planner summaries with reason counts and
trace previews
1 parent 10d71dc commit e3726d4

4 files changed

Lines changed: 122 additions & 25 deletions

File tree

scripts/render_summary.py

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import argparse
77
import json
88
import os
9+
from collections import Counter
910

1011
INSTALL_MAP = {
1112
"binary": "Binary download",
@@ -14,6 +15,10 @@
1415
"cached": "Already installed",
1516
}
1617

18+
LIST_PREVIEW_LIMIT = 12
19+
TRACE_PREVIEW_LIMIT = 20
20+
REASON_COUNT_PREVIEW_LIMIT = 8
21+
1722
# Map reason codes to human-readable descriptions
1823
REASON_DESCRIPTIONS = {
1924
"FILE_KIND_RUST_SRC": "Rust source file changed",
@@ -101,6 +106,45 @@ def collect_active_reason_ids(surfaces: dict) -> list[int]:
101106
return sorted(reason_ids)
102107

103108

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+
104148
def render(args: argparse.Namespace, plan: dict) -> str:
105149
files = [f.get("path", "") for f in plan.get("files", []) if f.get("path")]
106150
impact = plan.get("impact", {})
@@ -140,37 +184,37 @@ def render(args: argparse.Namespace, plan: dict) -> str:
140184
lines.append("")
141185

142186
if direct:
143-
lines.append(f"**Changed direct crates:** `{ ' '.join(direct) }`")
187+
lines.append(f"**Changed direct crates ({len(direct)}):** `{preview_items(direct)}`")
144188
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)}`")
146190
elif scope_mode == "workspace":
147191
lines.append("**Execution scope:** full workspace")
148192
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)}`")
150194
lines.append(f"**Top reasons:** {top_reasons}")
151195

152196
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>")
154198
lines.append("")
155199

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.")
174218

175219
lines.append("")
176220
lines.append("</details>")

tests/golden/summary_docs_only.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212

1313
**Top reasons:** Documentation changed
1414

15-
<details><summary>Trace details (file -> crate -> surface)</summary>
15+
<details><summary>Trace summary (1 reasons)</summary>
1616

17+
**Reason counts**
18+
- Documentation changed: 1
19+
20+
**Sample trace entries (1 of 1)**
1721
- r1 FILE_KIND_DOCS file=README.md surface=docs
1822

1923
</details>

tests/golden/summary_rust_src.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@
1010
| **Direct crates** | 1 |
1111
| **Active surfaces** | build, test |
1212

13-
**Changed direct crates:** `lib-a`
13+
**Changed direct crates (1):** `lib-a`
1414
**Execution scope:** full workspace
1515
**Top reasons:** Rust source file changed; Transitive dependency of changed crate
1616

17-
<details><summary>Trace details (file -> crate -> surface)</summary>
17+
<details><summary>Trace summary (3 reasons)</summary>
1818

19+
**Reason counts**
20+
- Rust source file changed: 1
21+
- File directly owns crate: 1
22+
- Transitive dependency of changed crate: 1
23+
24+
**Sample trace entries (3 of 3)**
1925
- r1 FILE_OWNS_CRATE_DIRECT file=crates/lib-a/src/lib.rs crate=lib-a
2026
- r2 FILE_KIND_RUST_SRC file=crates/lib-a/src/lib.rs surface=build
2127
- r3 TRANSITIVE_DEPENDS_ON_DIRECT crate=lib-b depends_on=lib-a surface=build

tests/test_summary.sh

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,47 @@ diff -u "$ROOT/tests/golden/summary_rust_src.md" "$TMP_DIR/summary_rust_src.md"
2727
run_summary "$ROOT/tests/fixtures/plan_docs_only.json" "$TMP_DIR/summary_docs_only.md"
2828
diff -u "$ROOT/tests/golden/summary_docs_only.md" "$TMP_DIR/summary_docs_only.md"
2929

30+
python3 - <<'PY' > "$TMP_DIR/plan_large.json"
31+
import json
32+
33+
trace = []
34+
trace.append({"id": 1, "code": "FILE_KIND_RUST_SRC", "file": "crates/lib-01/src/lib.rs", "surface": "build"})
35+
for idx in range(2, 24):
36+
trace.append(
37+
{
38+
"id": idx,
39+
"code": "TRANSITIVE_DEPENDS_ON_DIRECT",
40+
"crate": f"lib-{idx:02d}",
41+
"depends_on": "lib-01",
42+
"surface": "build",
43+
}
44+
)
45+
46+
plan = {
47+
"files": [{"path": "crates/lib-01/src/lib.rs"}],
48+
"impact": {
49+
"direct_crates": [f"lib-{idx:02d}" for idx in range(1, 15)],
50+
"transitive_crates": [f"dep-{idx:02d}" for idx in range(1, 5)],
51+
},
52+
"surfaces": {
53+
"build": {"enabled": True, "reasons": list(range(1, 24))},
54+
"test": {"enabled": True, "reasons": list(range(1, 24))},
55+
},
56+
"scope": {
57+
"mode": "crates",
58+
"crates": [f"pkg-{idx:02d}" for idx in range(1, 17)],
59+
"surfaces": {"build": True, "test": True},
60+
},
61+
"trace": trace,
62+
}
63+
64+
print(json.dumps(plan))
65+
PY
66+
67+
run_summary "$TMP_DIR/plan_large.json" "$TMP_DIR/summary_large.md"
68+
grep -F "**Changed direct crates (14):** \`lib-01, lib-02, lib-03, lib-04, lib-05, lib-06, lib-07, lib-08, lib-09, lib-10, lib-11, lib-12, ... +2 more\`" "$TMP_DIR/summary_large.md"
69+
grep -F "**Execution crates (16):** \`pkg-01, pkg-02, pkg-03, pkg-04, pkg-05, pkg-06, pkg-07, pkg-08, pkg-09, pkg-10, pkg-11, pkg-12, ... +4 more\`" "$TMP_DIR/summary_large.md"
70+
grep -F "**Sample trace entries (20 of 23)**" "$TMP_DIR/summary_large.md"
71+
grep -F -- "- ... +3 more trace entries" "$TMP_DIR/summary_large.md"
72+
3073
echo "summary tests passed"

0 commit comments

Comments
 (0)