Add world-class submission repair checklist

This commit is contained in:
yaojingang
2026-06-17 03:41:22 +08:00
parent 2241b0e344
commit 48a503f8f6
51 changed files with 683 additions and 401 deletions
@@ -9,6 +9,7 @@ from typing import Any
from render_world_class_evidence_intake import build_intake
from world_class_evidence_contract import DISALLOWED_REAL_ARTIFACTS
from world_class_repair_checklist import build_repair_checklist, summarize_repair_checklist
from world_class_source_checks import build_source_checklist, summarize_source_checklist
from world_class_submission_matrix import build_evidence_matrix, summarize_evidence_matrix
from world_class_submission_kit_rendering import render_html, render_readme
@@ -408,6 +409,7 @@ def build_submission_kit(
]
source_checklist = build_source_checklist(items)
evidence_matrix = build_evidence_matrix(items, files, artifact_checklist, source_checklist)
repair_checklist = build_repair_checklist(items, files, artifact_checklist, source_checklist, unknown_keys)
manifest_path = output_dir / "submission_manifest.json"
readme_path = output_dir / "README.md"
output_html = output_html or (output_dir / "index.html")
@@ -425,6 +427,7 @@ def build_submission_kit(
supporting_artifact_ready_count = sum(1 for item in supporting_rows if item.get("artifact_ref_ready"))
source_summary = summarize_source_checklist(source_checklist)
matrix_summary = summarize_evidence_matrix(evidence_matrix)
repair_summary = summarize_repair_checklist(repair_checklist)
ok = not unknown_keys and skipped_count == 0
output_dir_arg = shell_path(output_dir, skill_dir)
requested_key_args = " ".join(f"--evidence-key {shlex.quote(key)}" for key in (evidence_keys or []))
@@ -474,6 +477,7 @@ def build_submission_kit(
"artifact_ref_unfilled_count": unfilled_artifact_ref_count,
**source_summary,
**matrix_summary,
**repair_summary,
"handoff_step_count": len(handoff_steps),
"handoff_blocked_count": sum(1 for item in handoff_steps if item["status"] == "blocked"),
"handoff_fix_required_count": sum(1 for item in handoff_steps if item["status"] == "fix-required"),
@@ -487,6 +491,7 @@ def build_submission_kit(
"artifact_checklist": artifact_checklist,
"source_checklist": source_checklist,
"evidence_matrix": evidence_matrix,
"repair_checklist": repair_checklist,
"operator_handoff": handoff_steps,
"evidence_items": items,
"commands": commands,
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""Build machine-readable repair checklists for world-class submission kits."""
from collections import defaultdict
from typing import Any
SCRIPT_INTERFACE = "internal-module"
SCRIPT_INTERFACE_REASON = "Shared by submission kit generation to turn readiness blockers into actionable repair rows."
def _by_key(rows: list[dict[str, Any]], key_name: str = "evidence_key") -> dict[str, list[dict[str, Any]]]:
grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
for row in rows:
key = str(row.get(key_name, "")).strip()
if key:
grouped[key].append(row)
return dict(grouped)
def _repair_row(
*,
evidence_key: str,
repair_type: str,
target: str,
status: str,
blocking_reason: str,
next_action: str,
source: str,
) -> dict[str, Any]:
return {
"evidence_key": evidence_key,
"repair_type": repair_type,
"target": target,
"status": status,
"blocking_reason": blocking_reason,
"next_action": next_action,
"source": source,
"counts_as_completion": False,
}
def build_repair_checklist(
evidence_items: list[dict[str, Any]],
files: list[dict[str, Any]],
artifact_checklist: list[dict[str, Any]],
source_checklist: list[dict[str, Any]],
unknown_keys: list[str],
) -> list[dict[str, Any]]:
"""Return concrete repair rows for every draft, artifact, and source blocker."""
files_by_key = _by_key(files)
artifacts_by_key = _by_key(artifact_checklist)
sources_by_key = _by_key(source_checklist)
rows: list[dict[str, Any]] = []
known_keys = [str(item.get("evidence_key", "")).strip() for item in evidence_items]
for key in known_keys:
if not key:
continue
draft_rows = files_by_key.get(key, [])
if not draft_rows:
rows.append(
_repair_row(
evidence_key=key,
repair_type="draft",
target=key,
status="blocked",
blocking_reason="Submission draft was not generated.",
next_action="Run the submission kit command again for this evidence key.",
source="files",
)
)
for draft in draft_rows:
draft_status = str(draft.get("status", "")).strip()
if draft_status not in {"written", "exists"}:
target = str(draft.get("output_path") or draft.get("template_path") or key)
errors = draft.get("errors", [])
error_text = "; ".join(str(error) for error in errors) if isinstance(errors, list) else ""
rows.append(
_repair_row(
evidence_key=key,
repair_type="draft",
target=target,
status="blocked",
blocking_reason=error_text or f"Draft status is {draft_status or 'unknown'}.",
next_action="Fix template generation before asking for ledger review.",
source="files",
)
)
for artifact in artifacts_by_key.get(key, []):
if artifact.get("artifact_ref_ready"):
continue
target = str(artifact.get("path") or artifact.get("source_pattern") or key)
artifact_status = str(artifact.get("status", "missing"))
role = str(artifact.get("artifact_role", "supporting-evidence"))
if artifact.get("concrete_reference_required"):
action = "Replace the glob with concrete files, then reference the generated SHA-256 digests."
elif artifact.get("submission_ref_required"):
action = "Create the required submission artifact or update artifact_refs to a concrete existing aggregate file."
else:
action = "Add the supporting artifact if it is needed for reviewer audit."
rows.append(
_repair_row(
evidence_key=key,
repair_type="artifact",
target=target,
status="blocked",
blocking_reason=f"{role} artifact is {artifact_status}.",
next_action=action,
source="artifact_checklist",
)
)
for source in sources_by_key.get(key, []):
if source.get("status") == "pass":
continue
field = str(source.get("field") or source.get("label") or key)
rows.append(
_repair_row(
evidence_key=key,
repair_type="source-check",
target=field,
status="blocked",
blocking_reason=f"Current value {source.get('actual')!r} does not satisfy {source.get('expected')!r}.",
next_action=str(source.get("next_action") or "Collect the required source evidence."),
source="source_checklist",
)
)
for key in unknown_keys:
rows.append(
_repair_row(
evidence_key=key,
repair_type="unknown-key",
target=key,
status="blocked",
blocking_reason="Requested evidence key is not present in the operator checklist.",
next_action="Use one of the evidence keys listed by world-class-intake.",
source="unknown_evidence_keys",
)
)
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 {
"repair_checklist_count": len(rows),
"repair_blocked_count": blocked_count,
"repair_ready_count": len(rows) - blocked_count,
"repair_counts_as_completion": False,
}
@@ -78,6 +78,26 @@ def render_readme(report: dict[str, Any]) -> str:
f"`{item.get('supporting_artifact_ready_count', 0)}/{item.get('supporting_artifact_total_count', 0)}` | "
f"`{item['source_pass_count']}/{item['source_check_count']}` | {item['next_action']} |"
)
lines.extend(
[
"",
"## Repair Checklist",
"",
"This checklist turns every draft, artifact, and source blocker into a concrete repair row. Repair rows are procedural guidance and do not count as completion evidence.",
"",
"| Evidence | Type | Target | Status | Next action |",
"| --- | --- | --- | --- | --- |",
]
)
repair_rows = report.get("repair_checklist", [])
if repair_rows:
for item in repair_rows:
lines.append(
f"| `{item['evidence_key']}` | `{item['repair_type']}` | `{item['target']}` | "
f"`{item['status']}` | {item['next_action']} |"
)
else:
lines.append("| `all` | `none` | `n/a` | `ready` | No repair rows were generated. |")
lines.extend(["", "## Execution Runbook", ""])
for item in report.get("evidence_items", []):
must_collect = item.get("must_collect", {}) if isinstance(item.get("must_collect", {}), dict) else {}
@@ -276,6 +296,35 @@ def render_html_matrix(items: list[dict[str, Any]]) -> str:
)
def render_html_repair_checklist(items: list[dict[str, Any]]) -> str:
if not items:
return '<p class="muted">No repair rows were generated.</p>'
return "".join(
"""
<article class="repair-card {status}">
<header>
<span>{key} · {repair_type}</span>
<h3>{target}</h3>
</header>
<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(item.get("status", "")),
key=html_text(item.get("evidence_key", "")),
repair_type=html_text(item.get("repair_type", "")),
target=html_text(item.get("target", "")),
reason=html_text(item.get("blocking_reason", "")),
action=html_text(item.get("next_action", "")),
)
for item in items
)
def render_html_handoff(items: list[dict[str, Any]]) -> str:
if not items:
return '<p class="muted">No operator handoff steps were generated.</p>'
@@ -365,6 +414,7 @@ def render_html(report: dict[str, Any]) -> str:
)
evidence_html = "".join(render_html_item(item) for item in report.get("evidence_items", []))
matrix_html = render_html_matrix(report.get("evidence_matrix", []))
repair_html = render_html_repair_checklist(report.get("repair_checklist", []))
handoff_html = render_html_handoff(report.get("operator_handoff", []))
artifact_html = render_html_artifact_checklist(report.get("artifact_checklist", []))
source_html = render_html_source_checklist(report.get("source_checklist", []))
@@ -400,12 +450,14 @@ def render_html(report: dict[str, Any]) -> str:
.section {{ padding:32px 0; border-bottom:1px solid var(--line); }}
.panel {{ padding:20px; }}
.two-col {{ display:grid; grid-template-columns:minmax(0, .45fr) minmax(0, 1fr); gap:18px; align-items:start; }}
.draft-grid, .evidence-grid, .artifact-grid, .source-grid, .matrix-grid, .handoff-grid {{ display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:16px; }}
.draft-card, .evidence-card, .artifact-card, .source-card, .matrix-card, .handoff-card {{ padding:18px; min-width:0; border:1px solid var(--line); border-radius:8px; background:#fff; }}
.draft-grid, .evidence-grid, .artifact-grid, .source-grid, .matrix-grid, .repair-grid, .handoff-grid {{ display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:16px; }}
.draft-card, .evidence-card, .artifact-card, .source-card, .matrix-card, .repair-card, .handoff-card {{ padding:18px; min-width:0; border:1px solid var(--line); border-radius:8px; background:#fff; }}
.draft-card.written, .draft-card.exists {{ border-left:4px solid var(--pass); }}
.draft-card.skipped {{ border-left:4px solid var(--warn); }}
.matrix-card.collect-source, .matrix-card.prepare-draft, .matrix-card.fix-artifacts, .matrix-card.fix-draft {{ border-left:4px solid var(--warn); }}
.matrix-card.validate-packet {{ border-left:4px solid var(--pass); }}
.repair-card.blocked {{ border-left:4px solid var(--warn); }}
.repair-card.ready {{ border-left:4px solid var(--pass); }}
.handoff-card.blocked, .handoff-card.fix-required {{ border-left:4px solid var(--warn); }}
.handoff-card.ready {{ border-left:4px solid var(--pass); }}
.evidence-card.awaiting-submission, .evidence-card.fix-submission, .evidence-card.fix-template, .artifact-card.missing, .artifact-card.glob-no-match, .artifact-card.unsafe-path, .artifact-card.raw-content-disallowed {{ border-left:4px solid var(--warn); }}
@@ -425,11 +477,11 @@ def render_html(report: dict[str, Any]) -> str:
.mini-grid li, .runbook-panel li, .notice li {{ overflow-wrap:anywhere; }}
.notice {{ background:var(--soft); border-left:4px solid var(--ink); padding:16px; border-radius:8px; }}
.errors {{ color:var(--warn); }}
@media (max-width:820px) {{ .stats, .two-col, .draft-grid, .evidence-grid, .artifact-grid, .source-grid, .matrix-grid, .handoff-grid, .mini-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, .draft-grid, .evidence-grid, .artifact-grid, .source-grid, .matrix-grid, .repair-grid, .handoff-grid, .mini-grid {{ grid-template-columns:1fr; }} h1 {{ font-size:38px; }} .topbar-inner {{ align-items:flex-start; flex-direction:column; }} }}
</style>
</head>
<body>
<nav class="topbar"><div class="topbar-inner"><span class="brand">World-Class Kit</span><div class="links"><a href="#workflow">Workflow</a><a href="#handoff">Handoff</a><a href="#matrix">Matrix</a><a href="#drafts">Drafts</a><a href="#artifacts">Artifacts</a><a href="#source">Source</a><a href="#evidence">Evidence</a><a href="#safety">Safety</a></div></div></nav>
<nav class="topbar"><div class="topbar-inner"><span class="brand">World-Class Kit</span><div class="links"><a href="#workflow">Workflow</a><a href="#handoff">Handoff</a><a href="#matrix">Matrix</a><a href="#repair">Repair</a><a href="#drafts">Drafts</a><a href="#artifacts">Artifacts</a><a href="#source">Source</a><a href="#evidence">Evidence</a><a href="#safety">Safety</a></div></div></nav>
<main class="shell">
<section class="hero">
<span class="eyebrow">Evidence Intake</span>
@@ -443,6 +495,7 @@ def render_html(report: dict[str, Any]) -> str:
</section>
<section class="section" id="handoff"><h2>Operator Handoff</h2><p class="muted">These ordered steps make the operator-to-reviewer handoff auditable. They are procedural guardrails and never count as world-class evidence.</p><div class="handoff-grid">{handoff_html}</div></section>
<section class="section" id="matrix"><h2>Evidence Matrix</h2><p class="muted">The matrix separates submission artifact_refs from supporting assets, then combines draft status, source checks, and the next operator action. It is guidance only and never counts as accepted evidence.</p><div class="matrix-grid">{matrix_html}</div></section>
<section class="section" id="repair"><h2>Repair Checklist</h2><p class="muted">Each row names one concrete blocker and the next action required before ledger review. This checklist does not count as completion evidence.</p><div class="repair-grid">{repair_html}</div></section>
<section class="section" id="drafts"><h2>Drafts</h2><div class="draft-grid">{render_html_files(report['files'])}</div></section>
<section class="section" id="artifacts"><h2>Artifact Checklist</h2><p class="muted">Rows marked submission-ref are the paths expected in artifact_refs. Supporting-evidence rows help reviewers audit the packet but do not all need to be copied into the submission. Glob patterns are expanded for operator convenience only.</p><div class="artifact-grid">{artifact_html}</div></section>
<section class="section" id="source"><h2>Source Evidence Snapshot</h2><p class="muted">This section shows current aggregate source checks. It explains remaining blockers without changing the ledger.</p><div class="source-grid">{source_html}</div></section>