Skip to content

Commit 7b00f25

Browse files
ctruedenclaude
andcommitted
Add smelt columns to status table
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d111e6d commit 7b00f25

5 files changed

Lines changed: 125 additions & 9 deletions

File tree

src/pombast/cli/_status.py

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from pombast.config._settings import PombastConfig, parse_repo_spec
2121
from pombast.core._filter import ComponentFilter
22+
from pombast.core._smelt_json import load_smelt_components
2223
from pombast.maven._bom import load_bom
2324
from pombast.maven._rules import RulesXML
2425
from pombast.status._drift import drift_text
@@ -82,6 +83,13 @@
8283
default=None,
8384
help="Path to vetting timestamps file (G:A <space> YYYYMMDDHHmmss per line).",
8485
)
86+
@click.option(
87+
"--smelt",
88+
"smelt_path",
89+
type=click.Path(exists=True, path_type=Path),
90+
default=None,
91+
help="Path to smelt.json to overlay binary/source compatibility columns.",
92+
)
8593
@click.option(
8694
"--no-timestamps",
8795
is_flag=True,
@@ -142,6 +150,7 @@ def status_cmd(
142150
projects: str | None,
143151
badges: str | None,
144152
timestamps: str | None,
153+
smelt_path: Path | None,
145154
no_timestamps: bool,
146155
nexus_base: str | None,
147156
html_path: Path | None,
@@ -167,6 +176,7 @@ def status_cmd(
167176
effective_projects = projects or (str(sc.projects) if sc.projects else None)
168177
effective_badges = badges or (str(sc.badges) if sc.badges else None)
169178
effective_timestamps = timestamps or (str(sc.timestamps) if sc.timestamps else None)
179+
effective_smelt = smelt_path or sc.smelt
170180
effective_html = html_path or sc.html
171181
effective_header = header or sc.header
172182
effective_footer = footer or sc.footer
@@ -200,6 +210,14 @@ def status_cmd(
200210
load_timestamps_file(effective_timestamps) if effective_timestamps else {}
201211
)
202212

213+
smelt_components: dict[str, dict] | None = None
214+
if effective_smelt:
215+
smelt_components = load_smelt_components(effective_smelt)
216+
console.print(
217+
f"Loaded smelt data: [bold]{len(smelt_components)}[/bold] components "
218+
f"([cyan]{effective_smelt}[/cyan])"
219+
)
220+
203221
cf = ComponentFilter(includes=list(include), excludes=list(exclude))
204222
total = len(cf.filter(bom_data.components))
205223

@@ -231,7 +249,7 @@ def status_cmd(
231249
description=f"{entry.component.group}:{entry.component.name}",
232250
)
233251

234-
_print_status_table(entries)
252+
_print_status_table(entries, smelt=smelt_components)
235253

236254
cuts = sum(1 for e in entries if e.action == "Cut")
237255
bumps = sum(1 for e in entries if e.action == "Bump")
@@ -251,17 +269,49 @@ def status_cmd(
251269
nexus_base=nexus_base or "",
252270
header_html=header_html,
253271
footer_html=footer_html,
272+
smelt=smelt_components,
254273
)
255274
)
256275
console.print(f"HTML report written to: [cyan]{effective_html}[/cyan]")
257276

258277

259-
def _print_status_table(entries: list[StatusEntry]) -> None:
278+
def _smelt_cells(comp_data: dict | None, bom_version: str) -> tuple[str, str]:
279+
"""Return (binary_cell, source_cell) Rich markup for a smelt component entry."""
280+
if comp_data is None:
281+
return "[dim]—[/dim]", "[dim]—[/dim]"
282+
283+
mismatch = comp_data.get("version") not in (None, bom_version)
284+
suffix = "[yellow]*[/yellow]" if mismatch else ""
285+
skipped = comp_data.get("skipped_reason")
286+
287+
def _render(status: str | None) -> str:
288+
if status == "pass":
289+
return f"[green]pass[/green]{suffix}"
290+
if status in ("fail", "error"):
291+
return f"[red]{status}[/red]{suffix}"
292+
if status == "skipped":
293+
return f"[dim]skip[/dim]{suffix}"
294+
if status is None and skipped == "prior success":
295+
return f"[green]prior[/green]{suffix}"
296+
if status is None and skipped:
297+
return f"[dim]skip[/dim]{suffix}"
298+
return f"[dim]—[/dim]{suffix}"
299+
300+
return _render(comp_data.get("binary_test")), _render(comp_data.get("source_build"))
301+
302+
303+
def _print_status_table(
304+
entries: list[StatusEntry],
305+
smelt: dict[str, dict] | None = None,
306+
) -> None:
260307
table = Table(title="BOM Status", show_lines=False)
261308
table.add_column("Component", style="cyan", no_wrap=True)
262309
table.add_column("Release", justify="right")
263310
table.add_column("Drift", justify="right")
264311
table.add_column("Action", justify="left")
312+
if smelt is not None:
313+
table.add_column("Binary", justify="center")
314+
table.add_column("Source", justify="center")
265315

266316
for e in entries:
267317
latest = e.latest_version or e.bom_version
@@ -281,11 +331,23 @@ def _print_status_table(entries: list[StatusEntry]) -> None:
281331
"Bump": "[yellow]Bump[/yellow]",
282332
"None": "[dim]—[/dim]",
283333
}[e.action]
284-
table.add_row(
334+
row: list[str] = [
285335
f"{e.component.group}:{e.component.name}",
286336
release_str,
287337
drift_cell,
288338
action_str,
289-
)
339+
]
340+
if smelt is not None:
341+
ga = f"{e.component.group}:{e.component.name}"
342+
binary_cell, source_cell = _smelt_cells(smelt.get(ga), e.bom_version)
343+
row.extend([binary_cell, source_cell])
344+
table.add_row(*row)
290345

291346
console.print(table)
347+
348+
if smelt is not None and any(
349+
smelt.get(f"{e.component.group}:{e.component.name}", {}).get("version") not in
350+
(None, e.bom_version)
351+
for e in entries
352+
):
353+
console.print("[dim][yellow]*[/yellow] smelt result is from a different version[/dim]")

src/pombast/config/_settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class StatusConfig:
4343
projects: Path | None = None
4444
badges: Path | None = None
4545
timestamps: Path | None = None
46+
smelt: Path | None = None
4647
html: Path | None = None
4748
header: Path | None = None
4849
footer: Path | None = None
@@ -109,6 +110,7 @@ def resolve(section: dict, key: str) -> Path | None:
109110
projects=resolve(status_data, "projects"),
110111
badges=resolve(status_data, "badges"),
111112
timestamps=resolve(status_data, "timestamps"),
113+
smelt=resolve(status_data, "smelt"),
112114
html=resolve(status_data, "html"),
113115
header=resolve(status_data, "header"),
114116
footer=resolve(status_data, "footer"),

src/pombast/core/_smelt_json.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,10 @@ def write_json(report: ValidationReport, path: Path) -> None:
9393
"""Write report as pretty-printed JSON to path."""
9494
data = report_to_dict(report)
9595
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
96+
97+
98+
def load_smelt_components(path: Path) -> dict[str, dict]:
99+
"""Load smelt.json, returning a mapping of G:A to per-component data."""
100+
with open(path) as f:
101+
data = json.load(f)
102+
return data.get("components", {})

src/pombast/status/_html.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
from pombast.status._entry import StatusEntry
1414

1515

16-
_COLUMNS = ["Artifact", "Release", "Drift", "Action", "Build"]
16+
_COLUMNS_BASE = ["Artifact", "Release", "Drift", "Action", "Build"]
17+
_COLUMNS_SMELT = ["Binary", "Source"]
1718

1819
_env = Environment(
1920
loader=PackageLoader("pombast.status", "templates"),
@@ -79,14 +80,47 @@ def _drift_data(entry: StatusEntry) -> dict[str, Any]:
7980
}
8081

8182

82-
def _row_data(entry: StatusEntry, nexus_base: str) -> dict[str, Any]:
83+
def _smelt_cell_data(status: str | None, skipped_reason: str | None) -> dict[str, str]:
84+
"""Return {label, css} for a single smelt result cell."""
85+
if status == "pass":
86+
return {"label": "pass", "css": "smelt-pass"}
87+
if status in ("fail", "error"):
88+
return {"label": status, "css": "smelt-fail"}
89+
if status == "skipped":
90+
return {"label": "skip", "css": "smelt-skip"}
91+
if status is None and skipped_reason == "prior success":
92+
return {"label": "prior", "css": "smelt-pass"}
93+
if status is None and skipped_reason:
94+
return {"label": "skip", "css": "smelt-skip"}
95+
return {"label": "—", "css": "smelt-none"}
96+
97+
98+
def _smelt_row_data(comp_data: dict | None, bom_version: str) -> dict[str, Any]:
99+
"""Build smelt columns data for a single row."""
100+
if comp_data is None:
101+
empty = {"label": "—", "css": "smelt-none"}
102+
return {"binary": empty, "source": empty, "version_mismatch": False}
103+
skipped = comp_data.get("skipped_reason")
104+
mismatch = comp_data.get("version") not in (None, bom_version)
105+
return {
106+
"binary": _smelt_cell_data(comp_data.get("binary_test"), skipped),
107+
"source": _smelt_cell_data(comp_data.get("source_build"), skipped),
108+
"version_mismatch": mismatch,
109+
}
110+
111+
112+
def _row_data(
113+
entry: StatusEntry,
114+
nexus_base: str,
115+
smelt: dict[str, dict] | None = None,
116+
) -> dict[str, Any]:
83117
g = entry.component.group
84118
a = entry.component.name
85119
bom_v = entry.bom_version
86120
latest_v = entry.latest_version or bom_v
87121
action_key = {"Cut": 1, "Bump": 2, "None": 3}[entry.action]
88122

89-
return {
123+
row: dict[str, Any] = {
90124
"ga": f"{g}:{a}",
91125
"group_css": _css_safe(g),
92126
"artifact_css": _css_safe(a),
@@ -103,6 +137,10 @@ def _row_data(entry: StatusEntry, nexus_base: str) -> dict[str, Any]:
103137
"action_key": action_key,
104138
"badge_html": entry.badge_html or "<td>-</td>",
105139
}
140+
if smelt is not None:
141+
comp_data = smelt.get(f"{g}:{a}")
142+
row["smelt"] = _smelt_row_data(comp_data, bom_v)
143+
return row
106144

107145

108146
def generate_html(
@@ -112,13 +150,15 @@ def generate_html(
112150
title: str = "SciJava software status",
113151
header_html: str = "",
114152
footer_html: str = "",
153+
smelt: dict[str, dict] | None = None,
115154
) -> str:
116155
"""Return a complete HTML page with the status dashboard table."""
117-
rows = [_row_data(entry, nexus_base) for entry in entries]
156+
rows = [_row_data(entry, nexus_base, smelt=smelt) for entry in entries]
157+
columns = _COLUMNS_BASE + (_COLUMNS_SMELT if smelt is not None else [])
118158
template = _env.get_template("status.html.j2")
119159
return template.render(
120160
title=title,
121-
columns=_COLUMNS,
161+
columns=columns,
122162
rows=rows,
123163
header_html=header_html,
124164
footer_html=footer_html,

src/pombast/status/templates/status.html.j2

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<!DOCTYPE html>
22
<html>
33
<head>
4+
<meta charset="utf-8">
45
<title>{{ title }}</title>
56
<link type="text/css" rel="stylesheet" href="status.css">
67
<link rel="icon" type="image/png" href="favicon.png">
@@ -36,6 +37,10 @@
3637
<td{% if row.drift.cls %} class="{{ row.drift.cls }}"{% endif %} sorttable_customkey="{{ row.drift.sort_key }}"{% if row.drift.tooltip %} title="{{ row.drift.tooltip }}"{% endif %}>{{ row.drift.html | safe }}</td>
3738
<td sorttable_customkey="{{ row.action_key }}">{{ row.action }}</td>
3839
{{ row.badge_html | safe }}
40+
{%- if row.smelt is defined %}
41+
<td class="{{ row.smelt.binary.css }}{% if row.smelt.version_mismatch %} smelt-stale{% endif %}">{{ row.smelt.binary.label }}</td>
42+
<td class="{{ row.smelt.source.css }}{% if row.smelt.version_mismatch %} smelt-stale{% endif %}">{{ row.smelt.source.label }}</td>
43+
{%- endif %}
3944
</tr>
4045
{%- endfor %}
4146
</table>

0 commit comments

Comments
 (0)