diff --git a/README.md b/README.md index 90fa9bf1..11553ebe 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,8 @@ python3 scripts/yao.py world-class-evidence . SUBMISSIONS_DIR="${SUBMISSIONS_DIR:-evidence/world_class/submissions}" python3 scripts/yao.py world-class-preflight . --submissions-dir "$SUBMISSIONS_DIR" python3 scripts/yao.py world-class-submission-kit . --output-dir "$SUBMISSIONS_DIR" +# Alternative: prefill artifact SHA-256 digests while keeping drafts template-only. +python3 scripts/yao.py world-class-submission-kit . --output-dir "$SUBMISSIONS_DIR" --prefill-artifacts python3 scripts/yao.py world-class-intake . --submissions-dir "$SUBMISSIONS_DIR" python3 scripts/yao.py world-class-submission-review . --submissions-dir "$SUBMISSIONS_DIR" python3 scripts/yao.py world-class-ledger . --submissions-dir "$SUBMISSIONS_DIR" diff --git a/evidence/world_class/README.md b/evidence/world_class/README.md index 337df467..16a35ac5 100644 --- a/evidence/world_class/README.md +++ b/evidence/world_class/README.md @@ -10,6 +10,8 @@ Run: SUBMISSIONS_DIR="${SUBMISSIONS_DIR:-evidence/world_class/submissions}" python3 scripts/yao.py world-class-preflight . --submissions-dir "$SUBMISSIONS_DIR" python3 scripts/yao.py world-class-submission-kit . --output-dir "$SUBMISSIONS_DIR" +# Alternative: prefill artifact SHA-256 digests while keeping drafts template-only. +python3 scripts/yao.py world-class-submission-kit . --output-dir "$SUBMISSIONS_DIR" --prefill-artifacts python3 scripts/yao.py world-class-intake . --submissions-dir "$SUBMISSIONS_DIR" python3 scripts/yao.py world-class-submission-review . --submissions-dir "$SUBMISSIONS_DIR" python3 scripts/yao.py world-class-ledger . --submissions-dir "$SUBMISSIONS_DIR" @@ -28,7 +30,7 @@ Run `world-class-preflight` before assigning external or human work. It checks l The generated intake report also includes an `operator_checklist` for each pending evidence item. Use it to find the template path, target submission path, preparation command, validation command, required provenance, success checks, and privacy boundary before asking a reviewer or external operator to submit evidence. -The submission kit command creates editable JSON drafts plus a local README for an external operator or human reviewer. Those drafts keep `template_only: true` and do not count as evidence until the real run or review exists, the packet is edited truthfully, every artifact ref points to a local aggregate evidence file with a matching `sha256`, and `world-class-intake` validates it. +The submission kit command creates editable JSON drafts plus a local README for an external operator or human reviewer. Use `--prefill-artifacts` when you want the kit to insert SHA-256 digests for currently available local aggregate artifacts. Prefill is operator convenience only: those drafts still keep `template_only: true` and do not count as evidence until the real run or review exists, the packet is edited truthfully, every artifact ref points to a local aggregate evidence file with a matching `sha256`, and `world-class-intake` validates it. The submission review command renders a read-only queue that compares valid packets with the source evidence checks and current ledger state. It is for reviewer triage only; it does not accept evidence or make the world-class claim true. diff --git a/scripts/prepare_world_class_submission_kit.py b/scripts/prepare_world_class_submission_kit.py index f7afd6f7..fd051185 100644 --- a/scripts/prepare_world_class_submission_kit.py +++ b/scripts/prepare_world_class_submission_kit.py @@ -3,7 +3,6 @@ import argparse import hashlib import json import shlex -import shutil from datetime import date from pathlib import Path from typing import Any @@ -59,12 +58,60 @@ 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, {}) @@ -83,6 +130,8 @@ def copy_template( "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, } @@ -92,16 +141,36 @@ def copy_template( "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) - shutil.copyfile(template_path, output_path) + 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": [], } @@ -210,6 +279,7 @@ def render_readme(report: dict[str, Any]) -> str: "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", "", @@ -219,11 +289,13 @@ def render_readme(report: dict[str, Any]) -> str: "", "## Drafts", "", - "| Evidence | Draft | Status |", - "| --- | --- | --- |", + "| Evidence | Draft | Status | Prefilled refs |", + "| --- | --- | --- | ---: |", ] for item in report["files"]: - lines.append(f"| `{item['evidence_key']}` | `{item['output_path']}` | `{item['status']}` |") + 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 {} @@ -304,6 +376,7 @@ def render_html_files(files: list[dict[str, Any]]) -> str:
Template
{template}
Draft
{output}
+
Prefill
{prefill} artifact refs
{errors} @@ -312,6 +385,7 @@ def render_html_files(files: list[dict[str, Any]]) -> str: 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=( "