Expose world-class preflight repair rows
This commit is contained in:
@@ -13,6 +13,7 @@ from render_world_class_evidence_ledger import build_ledger
|
||||
from render_world_class_submission_review import build_submission_review
|
||||
from world_class_evidence_contract import rel_path
|
||||
from world_class_preflight_layout import render_html
|
||||
from world_class_repair_checklist import build_preflight_repair_checklist, summarize_repair_checklist
|
||||
from world_class_source_checks import summarize_source_checklist
|
||||
|
||||
|
||||
@@ -346,11 +347,19 @@ def build_preflight(skill_dir: Path, generated_at: str, submissions_dir: Path |
|
||||
items.append(item)
|
||||
precheck_rows.extend(prechecks)
|
||||
source_rows.extend(item_source_rows)
|
||||
repair_checklist = build_preflight_repair_checklist(items)
|
||||
repairs_by_key: dict[str, list[dict[str, Any]]] = {}
|
||||
for row in repair_checklist:
|
||||
key = str(row.get("evidence_key", ""))
|
||||
repairs_by_key.setdefault(key, []).append(row)
|
||||
for item in items:
|
||||
item["repair_checklist"] = repairs_by_key.get(str(item.get("evidence_key", "")), [])
|
||||
precheck_status_counts: dict[str, int] = {}
|
||||
for row in precheck_rows:
|
||||
status = str(row.get("status", "unknown"))
|
||||
precheck_status_counts[status] = precheck_status_counts.get(status, 0) + 1
|
||||
source_summary = summarize_source_checklist(source_rows)
|
||||
repair_summary = summarize_repair_checklist(repair_checklist)
|
||||
collection_ready_count = sum(1 for item in items if item["status"] in {"ready-to-collect", "ready-for-human-review", "ready-for-submission"})
|
||||
blocked_count = sum(1 for item in items if item["status"] == "blocked")
|
||||
ready_to_claim = ledger.get("summary", {}).get("ready_to_claim_world_class") is True
|
||||
@@ -365,6 +374,7 @@ def build_preflight(skill_dir: Path, generated_at: str, submissions_dir: Path |
|
||||
"collection_ready_count": collection_ready_count,
|
||||
"collection_blocked_count": blocked_count,
|
||||
**source_summary,
|
||||
**repair_summary,
|
||||
"pending_count": ledger.get("summary", {}).get("pending_count", 0),
|
||||
"ready_to_claim_world_class": ready_to_claim,
|
||||
"credential_value_exposed": False,
|
||||
@@ -382,6 +392,7 @@ def build_preflight(skill_dir: Path, generated_at: str, submissions_dir: Path |
|
||||
"items": items,
|
||||
"prechecks": precheck_rows,
|
||||
"source_checklist": source_rows,
|
||||
"repair_checklist": repair_checklist,
|
||||
"submissions": {
|
||||
"directory": rel_path(submissions_dir, skill_dir),
|
||||
"commands": build_submission_commands(skill_dir, submissions_dir),
|
||||
@@ -425,6 +436,7 @@ def render_markdown(report: dict[str, Any]) -> str:
|
||||
f"- collection ready: `{summary['collection_ready_count']}`",
|
||||
f"- collection blocked: `{summary['collection_blocked_count']}`",
|
||||
f"- source checks: `{summary['source_pass_count']}` pass / `{summary['source_check_count']}` total",
|
||||
f"- repair rows: `{summary['repair_blocked_count']}` blocked / `{summary['repair_checklist_count']}` total",
|
||||
"",
|
||||
"This preflight report checks whether an operator can start collecting the remaining external or human evidence. It never accepts evidence, prints secret values, or changes the world-class ledger.",
|
||||
"",
|
||||
@@ -475,6 +487,22 @@ def render_markdown(report: dict[str, Any]) -> str:
|
||||
lines.append(
|
||||
f"| `{item['evidence_key']}` | `{item['status']}` | `{item['intake_readiness']}` | `{item['review_state']}` | {next_action} |"
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Repair Checklist",
|
||||
"",
|
||||
"Repair rows convert preflight and source blockers into concrete operator actions. They are guidance only and do not count as completion evidence.",
|
||||
"",
|
||||
"| Evidence | Type | Target | Status | Next action |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
]
|
||||
)
|
||||
for row in report.get("repair_checklist", []):
|
||||
lines.append(
|
||||
f"| `{row['evidence_key']}` | `{row['repair_type']}` | `{row['target']}` | "
|
||||
f"`{row['status']}` | {md_cell(row['next_action'])} |"
|
||||
)
|
||||
for item in report["items"]:
|
||||
lines.extend(
|
||||
[
|
||||
|
||||
@@ -79,6 +79,34 @@ def render_html_source_checks(rows: list[dict[str, Any]]) -> str:
|
||||
)
|
||||
|
||||
|
||||
def render_html_repair_rows(rows: list[dict[str, Any]]) -> str:
|
||||
if not rows:
|
||||
return "<p class=\"muted\">No repair rows listed.</p>"
|
||||
return "".join(
|
||||
"""
|
||||
<article class="repair-row {status}">
|
||||
<div>
|
||||
<span>{repair_type}</span>
|
||||
<strong>{target}</strong>
|
||||
</div>
|
||||
<dl>
|
||||
<dt>Status</dt><dd>{status}</dd>
|
||||
<dt>Reason</dt><dd>{reason}</dd>
|
||||
<dt>Action</dt><dd>{action}</dd>
|
||||
<dt>Evidence</dt><dd>does not count as completion</dd>
|
||||
</dl>
|
||||
</article>
|
||||
""".format(
|
||||
status=html_text(row.get("status", "")),
|
||||
repair_type=html_text(row.get("repair_type", "")),
|
||||
target=html_text(row.get("target", "")),
|
||||
reason=html_text(row.get("blocking_reason", "")),
|
||||
action=html_text(row.get("next_action", "")),
|
||||
)
|
||||
for row in rows
|
||||
)
|
||||
|
||||
|
||||
def render_html_artifact_roles(contract: dict[str, Any]) -> str:
|
||||
cards = []
|
||||
for role in contract.get("roles", []):
|
||||
@@ -143,6 +171,10 @@ def render_html_item(item: dict[str, Any]) -> str:
|
||||
<h4>Source Checks</h4>
|
||||
<div class="check-grid">{render_html_source_checks(item.get('source_checklist', []))}</div>
|
||||
</section>
|
||||
<section class="check-section">
|
||||
<h4>Repair Rows</h4>
|
||||
<div class="repair-grid compact">{render_html_repair_rows(item.get('repair_checklist', []))}</div>
|
||||
</section>
|
||||
<section class="runbook">
|
||||
<h4>Runbook</h4>
|
||||
<ul>{html_list(item.get('runbook', []), 'No runbook steps listed.')}</ul>
|
||||
@@ -159,6 +191,7 @@ def render_html(report: dict[str, Any]) -> str:
|
||||
("Ready", summary["collection_ready_count"]),
|
||||
("Blocked", summary["collection_blocked_count"]),
|
||||
("Source", f"{summary['source_pass_count']}/{summary['source_check_count']}"),
|
||||
("Repairs", f"{summary.get('repair_blocked_count', 0)}/{summary.get('repair_checklist_count', 0)}"),
|
||||
]
|
||||
stat_html = "".join(
|
||||
f"<article><span>{html_text(label)}</span><strong>{html_text(value)}</strong></article>"
|
||||
@@ -189,10 +222,10 @@ def render_html(report: dict[str, Any]) -> str:
|
||||
h3 {{ margin:4px 0 10px; font-size:22px; letter-spacing:0; }}
|
||||
h4 {{ margin:0 0 8px; font-size:16px; letter-spacing:0; }}
|
||||
.lede {{ max-width:820px; color:var(--muted); font-size:20px; }}
|
||||
.stats {{ display:grid; grid-template-columns:repeat(5, minmax(0,1fr)); gap:12px; margin:26px 0 0; }}
|
||||
.stats article, .panel, .evidence-card, .check-row {{ border:1px solid var(--line); border-radius:8px; background:#fff; }}
|
||||
.stats {{ display:grid; grid-template-columns:repeat(6, minmax(0,1fr)); gap:12px; margin:26px 0 0; }}
|
||||
.stats article, .panel, .evidence-card, .check-row, .repair-row {{ border:1px solid var(--line); border-radius:8px; background:#fff; }}
|
||||
.stats article {{ padding:16px; }}
|
||||
.stats span, .muted, .evidence-card header span, .check-row span {{ color:var(--muted); }}
|
||||
.stats span, .muted, .evidence-card header span, .check-row span, .repair-row span {{ color:var(--muted); }}
|
||||
.stats strong {{ display:block; color:var(--ink); font-size:28px; line-height:1.15; overflow-wrap:anywhere; }}
|
||||
.section {{ padding:32px 0; border-bottom:1px solid var(--line); }}
|
||||
.two-col {{ display:grid; grid-template-columns:minmax(0,.45fr) minmax(0,1fr); gap:18px; align-items:start; }}
|
||||
@@ -209,9 +242,9 @@ def render_html(report: dict[str, Any]) -> str:
|
||||
.evidence-grid {{ display:grid; gap:18px; }}
|
||||
.evidence-card {{ padding:20px; min-width:0; }}
|
||||
.evidence-card.blocked {{ border-left:4px solid var(--block); }}
|
||||
.evidence-card.ready-for-human-review, .evidence-card.ready-to-collect, .check-row.human-required, .check-row.external-required, .check-row.missing, .check-row.blocked {{ border-left:4px solid var(--warn); }}
|
||||
.evidence-card.ready-for-submission, .check-row.pass {{ border-left:4px solid var(--pass); }}
|
||||
.meta, .check-row dl {{ display:grid; grid-template-columns:96px minmax(0,1fr); gap:8px 12px; }}
|
||||
.evidence-card.ready-for-human-review, .evidence-card.ready-to-collect, .check-row.human-required, .check-row.external-required, .check-row.missing, .check-row.blocked, .repair-row.blocked {{ border-left:4px solid var(--warn); }}
|
||||
.evidence-card.ready-for-submission, .check-row.pass, .repair-row.ready {{ border-left:4px solid var(--pass); }}
|
||||
.meta, .check-row dl, .repair-row dl {{ display:grid; grid-template-columns:96px minmax(0,1fr); gap:8px 12px; }}
|
||||
dt {{ color:var(--ink); }}
|
||||
dd {{ margin:0; min-width:0; overflow-wrap:anywhere; }}
|
||||
code {{ font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:13px; overflow-wrap:anywhere; }}
|
||||
@@ -219,11 +252,12 @@ def render_html(report: dict[str, Any]) -> str:
|
||||
.next-action p {{ margin-top:0; }}
|
||||
.next-action code {{ display:block; margin-top:8px; }}
|
||||
.check-grid {{ display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:12px; }}
|
||||
.check-row {{ padding:14px; min-width:0; }}
|
||||
.repair-grid {{ display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:12px; }}
|
||||
.check-row, .repair-row {{ padding:14px; min-width:0; }}
|
||||
.check-section {{ margin-top:16px; }}
|
||||
.notice {{ background:var(--soft); border-left:4px solid var(--ink); padding:16px; border-radius:8px; }}
|
||||
li {{ overflow-wrap:anywhere; }}
|
||||
@media (max-width:820px) {{ .stats, .two-col, .check-grid, .role-grid {{ grid-template-columns:1fr; }} h1 {{ font-size:38px; }} .topbar-inner {{ align-items:flex-start; flex-direction:column; }} }}
|
||||
@media (max-width:820px) {{ .stats, .two-col, .check-grid, .repair-grid, .role-grid {{ grid-template-columns:1fr; }} h1 {{ font-size:38px; }} .topbar-inner {{ align-items:flex-start; flex-direction:column; }} }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -144,6 +144,56 @@ def build_repair_checklist(
|
||||
return rows
|
||||
|
||||
|
||||
def build_preflight_repair_checklist(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Return repair rows from preflight precheck and source-check blockers."""
|
||||
rows: list[dict[str, Any]] = []
|
||||
for item in items:
|
||||
key = str(item.get("evidence_key", "")).strip()
|
||||
if not key:
|
||||
continue
|
||||
for precheck in item.get("prechecks", []):
|
||||
if not isinstance(precheck, dict):
|
||||
continue
|
||||
if precheck.get("required") is not True or precheck.get("status") == "pass":
|
||||
continue
|
||||
target = str(precheck.get("key") or precheck.get("label") or key)
|
||||
rows.append(
|
||||
_repair_row(
|
||||
evidence_key=key,
|
||||
repair_type="precheck",
|
||||
target=target,
|
||||
status="blocked",
|
||||
blocking_reason=(
|
||||
f"Required {precheck.get('kind', 'precheck')} precheck is "
|
||||
f"{precheck.get('status', 'unknown')}."
|
||||
),
|
||||
next_action=str(precheck.get("next_action") or "Complete the required preflight action."),
|
||||
source="prechecks",
|
||||
)
|
||||
)
|
||||
for source in item.get("source_checklist", []):
|
||||
if not isinstance(source, dict):
|
||||
continue
|
||||
if source.get("status") == "pass":
|
||||
continue
|
||||
target = str(source.get("field") or source.get("label") or key)
|
||||
rows.append(
|
||||
_repair_row(
|
||||
evidence_key=key,
|
||||
repair_type="source-check",
|
||||
target=target,
|
||||
status="blocked",
|
||||
blocking_reason=(
|
||||
f"Current value {source.get('actual')!r} does not satisfy "
|
||||
f"{source.get('expected')!r}."
|
||||
),
|
||||
next_action=str(source.get("next_action") or "Collect the required source evidence."),
|
||||
source="source_checklist",
|
||||
)
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def summarize_repair_checklist(rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
blocked_count = sum(1 for row in rows if row.get("status") != "ready")
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user