Files
yao-meta-skill/scripts/prepare_world_class_submission_kit.py
T
2026-06-16 03:58:05 +08:00

725 lines
32 KiB
Python

#!/usr/bin/env python3
import argparse
import hashlib
import json
import shlex
from datetime import date
from pathlib import Path
from typing import Any
from html_rendering import html_text
from render_world_class_evidence_intake import build_intake
from world_class_evidence_contract import DISALLOWED_REAL_ARTIFACTS
from world_class_source_checks import build_source_checklist, summarize_source_checklist
ROOT = Path(__file__).resolve().parent.parent
SCRIPT_INTERFACE = "cli"
SCRIPT_INTERFACE_REASON = "Prepares editable world-class evidence intake packets without counting drafts as accepted evidence."
def rel_path(path: Path, root: Path) -> str:
try:
return str(path.resolve().relative_to(root.resolve()))
except ValueError:
return str(path.resolve())
def shell_path(path: Path, root: Path) -> str:
return shlex.quote(rel_path(path, root))
def write_json(path: Path, payload: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def has_glob_pattern(value: str) -> bool:
return any(token in value for token in ("*", "?", "["))
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def requested_checklist_items(intake: dict[str, Any], evidence_keys: list[str]) -> list[dict[str, Any]]:
items = intake.get("operator_checklist", [])
if not evidence_keys:
return items
requested = set(evidence_keys)
return [item for item in items if item.get("evidence_key") in requested]
def template_result_by_key(intake: dict[str, Any]) -> dict[str, dict[str, Any]]:
return {str(item.get("evidence_key")): item for item in intake.get("templates", [])}
def artifact_rows_by_key_and_path(rows: list[dict[str, Any]]) -> dict[tuple[str, str], dict[str, Any]]:
ready: dict[tuple[str, str], dict[str, Any]] = {}
for row in rows:
key = str(row.get("evidence_key", ""))
path = str(row.get("path", ""))
if key and path and row.get("artifact_ref_ready"):
ready[(key, path)] = row
return ready
def load_template_payload(path: Path, errors: list[str]) -> dict[str, Any] | None:
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
errors.append("template file is not valid JSON")
return None
return payload if isinstance(payload, dict) else None
def prefill_artifact_refs(
payload: dict[str, Any],
evidence_key: str,
ready_rows: dict[tuple[str, str], dict[str, Any]],
) -> dict[str, int]:
refs = payload.get("artifact_refs", [])
stats = {
"prefilled_artifact_ref_count": 0,
"unfilled_artifact_ref_count": 0,
}
if not isinstance(refs, list):
return stats
for ref in refs:
if not isinstance(ref, dict):
stats["unfilled_artifact_ref_count"] += 1
continue
path = str(ref.get("path", "")).strip()
row = ready_rows.get((evidence_key, path))
if row and row.get("sha256"):
ref["sha256"] = row["sha256"]
ref["contains_raw_content"] = False
stats["prefilled_artifact_ref_count"] += 1
else:
stats["unfilled_artifact_ref_count"] += 1
return stats
def copy_template(
skill_dir: Path,
output_dir: Path,
item: dict[str, Any],
template_results: dict[str, dict[str, Any]],
ready_rows: dict[tuple[str, str], dict[str, Any]],
overwrite: bool,
prefill_artifacts: bool,
) -> dict[str, Any]:
key = str(item.get("evidence_key", ""))
template_result = template_results.get(key, {})
template_path = skill_dir / str(item.get("template_path", ""))
output_path = output_dir / f"{key}.json"
errors: list[str] = []
if template_result.get("status") != "pass":
errors.append("template failed intake validation")
if not template_path.exists():
errors.append("template file is missing")
if errors:
return {
"evidence_key": key,
"status": "skipped",
"template_path": rel_path(template_path, skill_dir),
"output_path": rel_path(output_path, skill_dir),
"prefilled_artifact_ref_count": 0,
"unfilled_artifact_ref_count": 0,
"errors": errors,
}
if output_path.exists() and not overwrite:
return {
"evidence_key": key,
"status": "exists",
"template_path": rel_path(template_path, skill_dir),
"output_path": rel_path(output_path, skill_dir),
"prefilled_artifact_ref_count": 0,
"unfilled_artifact_ref_count": 0,
"errors": [],
}
payload = load_template_payload(template_path, errors)
if errors or payload is None:
return {
"evidence_key": key,
"status": "skipped",
"template_path": rel_path(template_path, skill_dir),
"output_path": rel_path(output_path, skill_dir),
"prefilled_artifact_ref_count": 0,
"unfilled_artifact_ref_count": 0,
"errors": errors or ["template file is not a JSON object"],
}
prefill_stats = (
prefill_artifact_refs(payload, key, ready_rows)
if prefill_artifacts
else {"prefilled_artifact_ref_count": 0, "unfilled_artifact_ref_count": 0}
)
output_path.parent.mkdir(parents=True, exist_ok=True)
write_json(output_path, payload)
return {
"evidence_key": key,
"status": "written",
"template_path": rel_path(template_path, skill_dir),
"output_path": rel_path(output_path, skill_dir),
**prefill_stats,
"errors": [],
}
def artifact_row(skill_dir: Path, evidence_key: str, pattern: str, path: Path, status: str) -> dict[str, Any]:
exists = path.exists()
is_file = exists and path.is_file()
relative = rel_path(path, skill_dir) if exists else pattern
contains_raw_content = relative in DISALLOWED_REAL_ARTIFACTS
digest = sha256_file(path) if is_file and not contains_raw_content else ""
row_status = status if status else ("ready" if is_file else "missing")
if contains_raw_content:
row_status = "raw-content-disallowed"
return {
"evidence_key": evidence_key,
"source_pattern": pattern,
"path": relative,
"status": row_status,
"exists": exists,
"is_file": is_file,
"sha256": digest,
"artifact_ref_ready": bool(is_file and digest and not contains_raw_content),
"copy_path": relative if is_file else "",
"copy_sha256": digest,
"contains_raw_content": contains_raw_content,
"concrete_reference_required": has_glob_pattern(pattern),
}
def artifact_checklist_for_item(skill_dir: Path, item: dict[str, Any]) -> list[dict[str, Any]]:
key = str(item.get("evidence_key", ""))
must_collect = item.get("must_collect", {}) if isinstance(item.get("must_collect", {}), dict) else {}
artifacts = must_collect.get("evidence_artifacts", [])
if not isinstance(artifacts, list):
return []
rows: list[dict[str, Any]] = []
for artifact in artifacts:
pattern = str(artifact or "").strip()
if not pattern:
continue
if Path(pattern).is_absolute() or ".." in Path(pattern).parts:
rows.append(
{
"evidence_key": key,
"source_pattern": pattern,
"path": pattern,
"status": "unsafe-path",
"exists": False,
"is_file": False,
"sha256": "",
"artifact_ref_ready": False,
"copy_path": "",
"copy_sha256": "",
"contains_raw_content": False,
"concrete_reference_required": True,
}
)
continue
if has_glob_pattern(pattern):
matches = sorted(path for path in skill_dir.glob(pattern) if path.is_file())
if not matches:
rows.append(
{
"evidence_key": key,
"source_pattern": pattern,
"path": pattern,
"status": "glob-no-match",
"exists": False,
"is_file": False,
"sha256": "",
"artifact_ref_ready": False,
"copy_path": "",
"copy_sha256": "",
"contains_raw_content": False,
"concrete_reference_required": True,
}
)
continue
for match in matches:
rows.append(artifact_row(skill_dir, key, pattern, match, "ready"))
continue
rows.append(artifact_row(skill_dir, key, pattern, skill_dir / pattern, ""))
return rows
def build_artifact_checklist(skill_dir: Path, items: list[dict[str, Any]]) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for item in items:
rows.extend(artifact_checklist_for_item(skill_dir, item))
return rows
def render_readme(report: dict[str, Any]) -> str:
commands = report["commands"]
lines = [
"# World-Class Evidence Submission Kit",
"",
f"Generated at: `{report['generated_at']}`",
"",
"This kit contains editable drafts for human and external evidence packets. Drafts are not accepted evidence.",
"",
"## Workflow",
"",
"1. Run the real provider, human review, native permission, or native client telemetry work first.",
"2. Edit the matching JSON draft with only aggregate artifact references and provenance metadata.",
"3. Set `template_only` to `false` only after real evidence exists.",
"4. Set attestation booleans truthfully; do not include credentials, raw prompts, raw outputs, transcripts, notes, or private user content.",
"5. Validate the packet before asking the ledger reviewer to accept it.",
"6. Optional artifact prefill only inserts SHA-256 digests for current local aggregate artifacts; it does not mark a draft as real evidence.",
"",
"## Commands",
"",
f"- validate intake: `{commands['validate_intake']}`",
f"- refresh ledger: `{commands['refresh_ledger']}`",
f"- guard public claims: `{commands['guard_claim']}`",
"",
"## Drafts",
"",
"| Evidence | Draft | Status | Prefilled refs |",
"| --- | --- | --- | ---: |",
]
for item in report["files"]:
lines.append(
f"| `{item['evidence_key']}` | `{item['output_path']}` | `{item['status']}` | `{item.get('prefilled_artifact_ref_count', 0)}` |"
)
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 {}
runbook = must_collect.get("runbook", [])
lines.extend(["", f"### {item.get('label', item.get('evidence_key', 'Evidence'))}", ""])
if runbook:
for step in runbook:
lines.append(f"- `{step}`" if str(step).startswith("python3 ") or "=" in str(step) else f"- {step}")
else:
lines.append("- No source runbook listed.")
lines.extend(
[
"",
"## Artifact Checklist",
"",
"Use these paths and SHA-256 digests when filling `artifact_refs`. Glob patterns are expanded into concrete files; submissions must reference concrete paths, not globs.",
"",
"| Evidence | Path | Status | SHA-256 |",
"| --- | --- | --- | --- |",
]
)
for item in report.get("artifact_checklist", []):
digest = item.get("sha256") or "n/a"
lines.append(
f"| `{item['evidence_key']}` | `{item['path']}` | `{item['status']}` | `{digest}` |"
)
lines.extend(
[
"",
"## Source Evidence Snapshot",
"",
"These checks explain why a draft is not ready for ledger acceptance yet. They mirror current aggregate reports and do not accept evidence by themselves.",
"",
"| Evidence | Check | Current | Expected | Status |",
"| --- | --- | --- | --- | --- |",
]
)
for item in report.get("source_checklist", []):
lines.append(
f"| `{item['evidence_key']}` | {item['label']} | `{item['actual']}` | `{item['expected']}` | `{item['status']}` |"
)
lines.extend(
[
"",
"## Anti-Overclaim",
"",
"- This kit never marks ledger evidence as accepted.",
"- Planned work, metadata fallback, pending review, and local command-runner output remain non-evidence.",
"- A valid intake packet means ready for ledger review, not world-class completion.",
]
)
return "\n".join(lines).rstrip() + "\n"
def render_html_list(values: list[Any], empty: str) -> str:
if not values:
return f"<li>{html_text(empty)}</li>"
return "".join(f"<li>{html_text(value)}</li>" for value in values)
def render_html_commands(commands: dict[str, str]) -> str:
return "".join(
f"<li><span>{html_text(label.replace('_', ' '))}</span><code>{html_text(command)}</code></li>"
for label, command in commands.items()
)
def render_html_files(files: list[dict[str, Any]]) -> str:
if not files:
return "<p class=\"muted\">No submission drafts were requested.</p>"
return "".join(
"""
<article class="draft-card {status}">
<div>
<span>{status}</span>
<h3>{key}</h3>
</div>
<dl>
<dt>Template</dt><dd><code>{template}</code></dd>
<dt>Draft</dt><dd><code>{output}</code></dd>
<dt>Prefill</dt><dd>{prefill} artifact refs</dd>
</dl>
{errors}
</article>
""".format(
status=html_text(item.get("status", "")),
key=html_text(item.get("evidence_key", "")),
template=html_text(item.get("template_path", "")),
output=html_text(item.get("output_path", "")),
prefill=html_text(item.get("prefilled_artifact_ref_count", 0)),
errors=(
"<ul class=\"errors\">"
+ render_html_list(item.get("errors", []), "No errors.")
+ "</ul>"
if item.get("errors")
else ""
),
)
for item in files
)
def render_html_artifact_checklist(items: list[dict[str, Any]]) -> str:
if not items:
return "<p class=\"muted\">No required artifacts were listed for the requested evidence.</p>"
return "".join(
"""
<article class="artifact-card {status}">
<div>
<span>{key}</span>
<h3>{path}</h3>
</div>
<dl>
<dt>Pattern</dt><dd><code>{pattern}</code></dd>
<dt>Status</dt><dd>{status}</dd>
<dt>SHA-256</dt><dd><code>{sha}</code></dd>
</dl>
</article>
""".format(
status=html_text(item.get("status", "")),
key=html_text(item.get("evidence_key", "")),
path=html_text(item.get("path", "")),
pattern=html_text(item.get("source_pattern", "")),
sha=html_text(item.get("sha256") or "n/a"),
)
for item in items
)
def render_html_source_checklist(items: list[dict[str, Any]]) -> str:
if not items:
return "<p class=\"muted\">No source checks were listed for the requested evidence.</p>"
return "".join(
"""
<article class="source-card {status}">
<div>
<span>{key}</span>
<h3>{label}</h3>
</div>
<dl>
<dt>Field</dt><dd><code>{field}</code></dd>
<dt>Current</dt><dd><code>{actual}</code></dd>
<dt>Expected</dt><dd><code>{expected}</code></dd>
<dt>Action</dt><dd>{action}</dd>
</dl>
</article>
""".format(
status=html_text(item.get("status", "")),
key=html_text(item.get("evidence_key", "")),
label=html_text(item.get("label", "")),
field=html_text(item.get("field", "")),
actual=html_text(item.get("actual", "")),
expected=html_text(item.get("expected", "")),
action=html_text(item.get("next_action", "")),
)
for item in items
)
def render_html_item(item: dict[str, Any]) -> str:
must_collect = item.get("must_collect", {}) if isinstance(item.get("must_collect", {}), dict) else {}
runbook = must_collect.get("runbook", [])
return f"""
<article class="evidence-card {html_text(item.get('readiness', ''))}">
<header>
<span>{html_text(item.get('category', ''))} · {html_text(item.get('readiness', ''))}</span>
<h3>{html_text(item.get('label', item.get('evidence_key', '')))}</h3>
</header>
<p>{html_text(item.get('blocking_reason', ''))}</p>
<dl>
<dt>Owner</dt><dd>{html_text(item.get('owner', ''))}</dd>
<dt>Evidence</dt><dd><code>{html_text(item.get('evidence_key', ''))}</code></dd>
<dt>Submission</dt><dd><code>{html_text(item.get('submission_path', ''))}</code></dd>
</dl>
<section class="runbook-panel">
<h4>Execution Runbook</h4>
<ul>{render_html_list(runbook, 'No source runbook listed.')}</ul>
</section>
<div class="mini-grid">
<section>
<h4>Must Collect</h4>
<ul>{render_html_list(must_collect.get('provenance_requirements', []), 'No provenance requirements listed.')}</ul>
</section>
<section>
<h4>Pass Checks</h4>
<ul>{render_html_list(must_collect.get('success_checks', []), 'No success checks listed.')}</ul>
</section>
<section>
<h4>Privacy</h4>
<ul>{render_html_list(must_collect.get('privacy_contract', []), 'No privacy contract listed.')}</ul>
</section>
</div>
</article>
"""
def render_html(report: dict[str, Any]) -> str:
summary = report["summary"]
artifact_ready = summary.get("artifact_ready_count", 0)
artifact_total = summary.get("artifact_checklist_count", 0)
stats = [
("Requested", summary["requested_count"]),
("Written", summary["written_count"]),
("Existing", summary["existing_count"]),
("Skipped", summary["skipped_count"]),
("Artifacts", f"{artifact_ready}/{artifact_total}"),
("Prefilled", summary.get("artifact_ref_prefill_count", 0)),
]
stat_html = "".join(f"<article><span>{html_text(label)}</span><strong>{html_text(value)}</strong></article>" for label, value in stats)
evidence_html = "".join(render_html_item(item) for item in report.get("evidence_items", []))
artifact_html = render_html_artifact_checklist(report.get("artifact_checklist", []))
source_html = render_html_source_checklist(report.get("source_checklist", []))
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>World-Class Evidence Submission Kit</title>
<style>
:root {{ --ink:#1B365D; --text:#202124; --muted:#6f6a63; --line:#e8e1d8; --soft:#f8f6f2; --warn:#9b4d0f; --pass:#1f6f43; }}
* {{ box-sizing:border-box; }}
body {{ margin:0; background:#fff; color:var(--text); font:16px/1.55 Georgia, "Times New Roman", serif; }}
.topbar {{ position:sticky; top:0; z-index:10; background:rgba(255,255,255,.96); border-bottom:1px solid var(--line); }}
.topbar-inner {{ max-width:1180px; margin:0 auto; padding:12px 24px; display:flex; justify-content:space-between; gap:16px; align-items:center; }}
.brand, a {{ color:var(--ink); }}
.links {{ display:flex; gap:14px; flex-wrap:wrap; }}
.links a {{ text-decoration:none; }}
.shell {{ max-width:1180px; margin:0 auto; padding:36px 24px 72px; }}
.hero {{ border-bottom:1px solid var(--line); padding:32px 0 28px; }}
.eyebrow {{ color:var(--ink); font-size:12px; text-transform:uppercase; font-weight:700; letter-spacing:0; }}
h1 {{ margin:8px 0 12px; color:var(--ink); font-size:56px; line-height:1.04; letter-spacing:0; }}
h2, h3, h4 {{ color:var(--ink); letter-spacing:0; }}
h2 {{ margin:0 0 14px; font-size:30px; }}
h3 {{ margin:4px 0 10px; font-size:22px; }}
h4 {{ margin:0 0 8px; font-size:16px; }}
.lede {{ max-width:800px; color:var(--muted); font-size:20px; }}
.stats {{ display:grid; grid-template-columns:repeat(6, minmax(0,1fr)); gap:12px; margin:26px 0 0; }}
.stats article, .panel, .draft-card, .evidence-card {{ border:1px solid var(--line); border-radius:8px; background:#fff; }}
.stats article {{ padding:16px; }}
.stats span, .draft-card span, .evidence-card span, .muted {{ color:var(--muted); }}
.stats strong {{ display:block; color:var(--ink); font-size:34px; line-height:1; }}
.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 {{ display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:16px; }}
.draft-card, .evidence-card, .artifact-card, .source-card {{ padding:18px; min-width:0; }}
.draft-card.written, .draft-card.exists {{ border-left:4px solid var(--pass); }}
.draft-card.skipped {{ border-left:4px solid var(--warn); }}
.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); }}
.artifact-card.ready {{ border-left:4px solid var(--pass); }}
.source-card.blocked {{ border-left:4px solid var(--warn); }}
.source-card.pass {{ border-left:4px solid var(--pass); }}
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; }}
ul, ol {{ padding-left:20px; }}
.commands {{ list-style:none; padding:0; margin:0; display:grid; gap:10px; }}
.commands li {{ padding:12px; background:var(--soft); border-radius:8px; }}
.commands span {{ display:block; color:var(--ink); font-weight:700; margin-bottom:4px; }}
.mini-grid {{ display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:12px; margin-top:14px; }}
.mini-grid section, .runbook-panel {{ background:var(--soft); border-radius:8px; padding:14px; min-width:0; }}
.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, .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="#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>
<h1>World-Class Evidence Submission Kit</h1>
<p class="lede">Use this cockpit to prepare human and external evidence packets. Drafts are not accepted evidence, artifact prefill only inserts local SHA-256 digests, and this page never changes the ledger result.</p>
<div class="stats">{stat_html}</div>
</section>
<section class="section two-col" id="workflow">
<article class="panel"><h2>Workflow</h2><ol><li>Run the real provider, human review, native permission, or native client telemetry work first.</li><li>Edit the matching JSON draft with aggregate artifact references and provenance metadata.</li><li>Set template_only to false only after real evidence exists.</li><li>Use prefilled SHA-256 values as convenience data, not evidence acceptance.</li><li>Validate intake, refresh the ledger, then guard public claims.</li></ol></article>
<aside class="panel"><h2>Commands</h2><ul class="commands">{render_html_commands(report['commands'])}</ul></aside>
</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">Copy concrete paths and SHA-256 digests from here into artifact_refs after real evidence exists. 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>
<section class="section" id="evidence"><h2>Evidence Requirements</h2><div class="evidence-grid">{evidence_html}</div></section>
<section class="section" id="safety"><h2>Safety Boundary</h2><div class="notice"><ul><li>Drafts never count as accepted ledger evidence.</li><li>Valid intake means ready for ledger review, not world-class completion.</li><li>Do not include credentials, raw prompts, raw outputs, transcripts, notes, or private user content.</li></ul></div></section>
</main>
</body>
</html>
"""
def build_submission_kit(
skill_dir: Path,
output_dir: Path,
generated_at: str,
evidence_keys: list[str] | None = None,
overwrite: bool = False,
prefill_artifacts: bool = False,
output_html: Path | None = None,
) -> dict[str, Any]:
intake = build_intake(skill_dir, generated_at, submissions_dir=output_dir)
items = requested_checklist_items(intake, evidence_keys or [])
valid_keys = {str(item.get("evidence_key")) for item in intake.get("operator_checklist", [])}
unknown_keys = sorted(set(evidence_keys or []) - valid_keys)
template_results = template_result_by_key(intake)
artifact_checklist = build_artifact_checklist(skill_dir, items)
ready_rows = artifact_rows_by_key_and_path(artifact_checklist)
files = [
copy_template(
skill_dir,
output_dir,
item,
template_results,
ready_rows,
overwrite,
prefill_artifacts,
)
for item in items
]
source_checklist = build_source_checklist(items)
manifest_path = output_dir / "submission_manifest.json"
readme_path = output_dir / "README.md"
output_html = output_html or (output_dir / "index.html")
written_count = sum(1 for item in files if item["status"] == "written")
existing_count = sum(1 for item in files if item["status"] == "exists")
skipped_count = sum(1 for item in files if item["status"] == "skipped")
prefilled_artifact_ref_count = sum(item.get("prefilled_artifact_ref_count", 0) for item in files)
unfilled_artifact_ref_count = sum(item.get("unfilled_artifact_ref_count", 0) for item in files)
artifact_ready_count = sum(1 for item in artifact_checklist if item.get("artifact_ref_ready"))
artifact_missing_count = sum(1 for item in artifact_checklist if not item.get("artifact_ref_ready"))
artifact_glob_count = sum(1 for item in artifact_checklist if item.get("concrete_reference_required"))
source_summary = summarize_source_checklist(source_checklist)
ok = not unknown_keys and skipped_count == 0
report = {
"schema_version": "1.0",
"ok": ok,
"generated_at": generated_at,
"skill_dir": rel_path(skill_dir, ROOT),
"output_dir": rel_path(output_dir, skill_dir),
"summary": {
"requested_count": len(items) + len(unknown_keys),
"prepared_count": len(files),
"written_count": written_count,
"existing_count": existing_count,
"skipped_count": skipped_count,
"unknown_key_count": len(unknown_keys),
"artifact_checklist_count": len(artifact_checklist),
"artifact_ready_count": artifact_ready_count,
"artifact_missing_count": artifact_missing_count,
"artifact_glob_expansion_count": artifact_glob_count,
"artifact_prefill_enabled": prefill_artifacts,
"artifact_ref_prefill_count": prefilled_artifact_ref_count,
"artifact_ref_unfilled_count": unfilled_artifact_ref_count,
**source_summary,
"drafts_count_as_evidence": False,
"ledger_counts_submission_as_completion": False,
"decision": "submission-kit-ready" if ok else "fix-submission-kit",
},
"unknown_evidence_keys": unknown_keys,
"files": files,
"artifact_checklist": artifact_checklist,
"source_checklist": source_checklist,
"evidence_items": items,
"commands": {
"validate_intake": f"python3 scripts/yao.py world-class-intake . --submissions-dir {shell_path(output_dir, skill_dir)}",
"refresh_ledger": f"python3 scripts/yao.py world-class-ledger . --submissions-dir {shell_path(output_dir, skill_dir)}",
"guard_claim": "python3 scripts/yao.py world-class-claim-guard .",
},
"safety": {
"template_only_drafts": True,
"artifact_prefill_counts_as_evidence": False,
"real_evidence_required_before_template_only_false": True,
"raw_content_allowed": False,
"credentials_allowed": False,
"drafts_count_as_evidence": False,
},
"artifacts": {
"manifest": rel_path(manifest_path, skill_dir),
"readme": rel_path(readme_path, skill_dir),
"html": rel_path(output_html, skill_dir),
},
}
output_dir.mkdir(parents=True, exist_ok=True)
write_json(manifest_path, report)
readme_path.write_text(render_readme(report), encoding="utf-8")
output_html.parent.mkdir(parents=True, exist_ok=True)
output_html.write_text(render_html(report), encoding="utf-8")
return report
def main() -> None:
parser = argparse.ArgumentParser(description="Prepare editable world-class evidence submission drafts.")
parser.add_argument("skill_dir", nargs="?", default=".")
parser.add_argument("--output-dir", default="evidence/world_class/submission-kit")
parser.add_argument("--evidence-key", action="append", default=[])
parser.add_argument("--overwrite", action="store_true")
parser.add_argument(
"--prefill-artifacts",
action="store_true",
help="Insert SHA-256 digests for currently available aggregate artifacts while keeping drafts template-only.",
)
parser.add_argument("--generated-at", default=date.today().isoformat())
parser.add_argument("--output-html")
args = parser.parse_args()
skill_dir = Path(args.skill_dir).resolve()
output_dir = Path(args.output_dir)
if not output_dir.is_absolute():
output_dir = skill_dir / output_dir
report = build_submission_kit(
skill_dir,
output_dir.resolve(),
args.generated_at,
evidence_keys=args.evidence_key,
overwrite=args.overwrite,
prefill_artifacts=args.prefill_artifacts,
output_html=Path(args.output_html).resolve() if args.output_html else None,
)
print(json.dumps(report, ensure_ascii=False, indent=2))
if not report["ok"]:
raise SystemExit(2)
if __name__ == "__main__":
main()