feat: add review bundle audit skill

This commit is contained in:
wantsong 2026-06-19 03:15:05 +08:00
parent 625ab2e516
commit fdda95aea9
8 changed files with 737 additions and 0 deletions

View File

@ -25,6 +25,7 @@ powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill bund
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill lifecycle-status-guard-scan
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill repair-markdown-citations
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill regression-validation-gate-runner
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill review-bundle-audit
```
Use `-Force` to back up and replace an existing installed copy:
@ -34,6 +35,7 @@ powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill bund
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill lifecycle-status-guard-scan -Force
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill repair-markdown-citations -Force
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill regression-validation-gate-runner -Force
powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill review-bundle-audit -Force
```
`-All` installs only registry rows with an installable status and `default` install policy. Optional Skills must be named explicitly.

View File

@ -18,6 +18,7 @@ Existing CCPE content is not migrated here.
| `lifecycle-status-guard-scan` | Scan configured Markdown, JSON, YAML, and text files for lifecycle/status overclaim candidates and missing local evidence markers. | `C:\Users\wangq\Documents\Codex\skills-vault\skills\lifecycle-status-guard-scan` | `installed` | `default` | `C:\Users\wangq\.agents\skills\lifecycle-status-guard-scan` | none |
| `repair-markdown-citations` | Repair ChatGPT/Deep Research Markdown citation tokens into standard footnotes and a deduplicated reference section using exact turn-ID metadata mapping. | `C:\Users\wangq\Documents\Codex\skills-vault\skills\repair-markdown-citations` | `installed` | `default` | `C:\Users\wangq\.agents\skills\repair-markdown-citations` | none |
| `regression-validation-gate-runner` | Dry-run or execute project-declared regression and validation gates from a manifest while capturing logs, exit codes, durations, skipped gates, and changed-file notes. | `C:\Users\wangq\Documents\Codex\skills-vault\skills\regression-validation-gate-runner` | `installed` | `default` | `C:\Users\wangq\.agents\skills\regression-validation-gate-runner` | none |
| `review-bundle-audit` | Preflight review, handoff, release, CCRA, Web upload, or code-review bundle directories for required files, manifests, sidecars, reports, zip readability, warnings, and blockers. | `C:\Users\wangq\Documents\Codex\skills-vault\skills\review-bundle-audit` | `installed` | `default` | `C:\Users\wangq\.agents\skills\review-bundle-audit` | none |
| `voice-generation` | Generate spoken audio from Markdown scripts using the local `voice-gen` CLI and MiniMax `mmx`, including custom voice clone registration and voice registry management. | `D:\AI\ClaudeCode\voice-ge` | `installed` | `conditional` | `C:\Users\wangq\.agents\skills\voice-generation` | none |
## Status Values

View File

@ -0,0 +1,41 @@
# Review Bundle Audit
Source Skill for auditing review bundle directories before human or Agent review.
The canonical entry point is `SKILL.md`; the deterministic auditor is in `scripts/review_bundle_audit.py`.
## Original Source
```text
Original source: C:\Users\wangq\Documents\Codex\ccpe-system\requirements\skills-vault\2026-06-19-review-bundle-audit.md
Migration date: 2026-06-19
Migration status: first public Skill implementation
Behavior preserved: deterministic package audit only; no review judgment or bundle repair
Known gaps: built-in profile is generic and intentionally conservative
```
## Layout
```text
SKILL.md
README.md
agents/openai.yaml
fixtures/profile.example.yaml
scripts/review_bundle_audit.py
tests/test_review_bundle_audit.py
```
## Usage
```powershell
conda run -n skills-vault python .\scripts\review_bundle_audit.py `
--bundle-root C:\path\review-bundle `
--output-dir C:\path\helper-outputs `
--profile generic
```
## Tests
```powershell
conda run -n skills-vault python -B -m unittest discover -s skills/review-bundle-audit/tests -v
```

View File

@ -0,0 +1,103 @@
---
name: review-bundle-audit
description: Use when Codex needs to preflight a review, handoff, release, CCRA, Web upload, article, video, Skill, or code-review bundle directory for required files, manifests, validation sidecars, reports, zip readability, missing materials, extra files, UTF-8 paths, and review-package completeness before a human or Agent review.
---
# Review Bundle Audit
## Purpose
Audit a review bundle directory before reviewer judgment is spent. Use the bundled script to produce deterministic Markdown and JSON reports about bundle structure, required files, readable manifests, validation sidecars, reports, zips, warnings, and blockers.
This Skill is a package audit only. It must not repair bundles, edit source files, or decide review acceptance.
## Command
```powershell
conda run -n skills-vault python .\scripts\review_bundle_audit.py `
--bundle-root C:\path\review-bundle `
--output-dir C:\path\helper-outputs `
--profile generic
```
Use `--profile-config C:\path\profile.yaml` when a project has its own required file names.
When installed under `C:\Users\wangq\.agents\skills\review-bundle-audit`, run from that Skill directory or use the installed script path.
## Built-In Profile
`generic` requires:
- `brief`: `*brief*.md`, `*brief*.markdown`
- `manifest`: `*manifest*.json`, `*manifest*.yaml`, `*manifest*.yml`
- `validation_sidecar`: `*validation*sidecar*.json`, `*validation*sidecar*.yaml`, `*validation*sidecar*.yml`
It optionally recognizes:
- `report`: `*report*.md`, `*report*.markdown`
- `zip`: `*.zip`
- `changed_files`: changed-file summary names
Extra files produce warnings, not blockers. Missing report or zip produces warnings. Unreadable manifest, sidecar, or zip produces a blocker.
## Profile Config
```yaml
profiles:
project-review:
required_files:
brief:
- review-brief.md
manifest:
- review-manifest.json
validation_sidecar:
- validation-sidecar.json
optional_files:
report:
- "*report*.md"
zip:
- "*.zip"
allowed_extra_patterns:
- notes/*.md
```
## Outputs
The script writes under `output_dir`:
```text
bundle-audit.md
bundle-audit.json
```
Reports include:
- bundle root and profile
- files discovered
- required files present and missing
- optional files present
- manifest status
- zip status
- validation sidecar status
- report status
- warnings and blocking errors
- machine-readable summary
Exit code is `0` when no blockers exist, `1` for audit blockers, and `2` for input/profile errors.
## Safety
- Read only the specified bundle directory and optional profile config.
- Write only audit reports under `output_dir`.
- Do not create, repair, normalize, or delete bundle files.
- Do not edit model cards, selector rules, regression cases, project docs, or source files.
- Treat the audit as evidence for a reviewer, not as review acceptance.
## Validation
After changing audit behavior, run:
```powershell
conda run -n skills-vault python -B -m unittest discover -s skills/review-bundle-audit/tests -v
conda run -n skills-vault python -B C:\Users\wangq\.codex\skills\.system\skill-creator\scripts\quick_validate.py skills/review-bundle-audit
```

View File

@ -0,0 +1,4 @@
interface:
display_name: "Review Bundle Audit"
short_description: "Preflight review bundle completeness"
default_prompt: "Use $review-bundle-audit to preflight a review bundle and write Markdown and JSON audit reports."

View File

@ -0,0 +1,23 @@
profiles:
project-review:
required_files:
brief:
- review-brief.md
- "*brief*.md"
manifest:
- review-manifest.json
- "*manifest*.json"
validation_sidecar:
- validation-sidecar.json
- "*validation*sidecar*.json"
optional_files:
report:
- "*report*.md"
zip:
- "*.zip"
changed_files:
- "*changed*files*.md"
- "*changed-files*.json"
allowed_extra_patterns:
- notes/*.md
- README.md

View File

@ -0,0 +1,367 @@
from __future__ import annotations
import argparse
import fnmatch
import json
import pathlib
import sys
import zipfile
from collections.abc import Iterable, Mapping
from typing import Any
import yaml
REPORT_JSON = "bundle-audit.json"
REPORT_MD = "bundle-audit.md"
BUILTIN_PROFILES: dict[str, dict[str, Any]] = {
"generic": {
"required_files": {
"brief": ["*brief*.md", "*brief*.markdown"],
"manifest": ["*manifest*.json", "*manifest*.yaml", "*manifest*.yml"],
"validation_sidecar": [
"*validation*sidecar*.json",
"*validation*sidecar*.yaml",
"*validation*sidecar*.yml",
],
},
"optional_files": {
"report": ["*report*.md", "*report*.markdown"],
"zip": ["*.zip"],
"changed_files": ["*changed*files*.md", "*changed-files*.json", "*changed*files*.json"],
},
"allowed_extra_patterns": [],
}
}
def as_list(value: Any) -> list[str]:
if value is None:
return []
if isinstance(value, str):
return [value]
if isinstance(value, Iterable):
return [str(item) for item in value if item is not None]
return [str(value)]
def load_structured(path: pathlib.Path) -> Any:
text = path.read_text(encoding="utf-8")
suffix = path.suffix.casefold()
if suffix == ".json":
return json.loads(text)
if suffix in {".yaml", ".yml"}:
return yaml.safe_load(text)
return text
def load_profile_config(path: pathlib.Path | None) -> dict[str, Any]:
if path is None:
return {}
parsed = load_structured(path)
if not isinstance(parsed, Mapping):
raise ValueError("Profile config must be a JSON or YAML object.")
return dict(parsed)
def get_profile(profile_name: str, config: Mapping[str, Any]) -> dict[str, Any]:
profiles = dict(BUILTIN_PROFILES)
configured_profiles = config.get("profiles") if isinstance(config, Mapping) else None
if isinstance(configured_profiles, Mapping):
for name, profile in configured_profiles.items():
if isinstance(profile, Mapping):
profiles[str(name)] = dict(profile)
if profile_name not in profiles:
raise ValueError(f"Unknown profile: {profile_name}")
return normalize_profile(profiles[profile_name])
def normalize_pattern_map(raw: Any) -> dict[str, list[str]]:
if not isinstance(raw, Mapping):
return {}
return {str(key): as_list(value) for key, value in raw.items()}
def normalize_profile(raw_profile: Mapping[str, Any]) -> dict[str, Any]:
return {
"required_files": normalize_pattern_map(raw_profile.get("required_files")),
"optional_files": normalize_pattern_map(raw_profile.get("optional_files")),
"allowed_extra_patterns": as_list(raw_profile.get("allowed_extra_patterns")),
}
def rel_path(path: pathlib.Path, root: pathlib.Path) -> str:
return path.relative_to(root).as_posix()
def discover_files(bundle_root: pathlib.Path) -> list[pathlib.Path]:
return sorted(path for path in bundle_root.rglob("*") if path.is_file())
def match_patterns(file_rel: str, patterns: list[str]) -> bool:
basename = pathlib.PurePosixPath(file_rel).name
return any(fnmatch.fnmatch(file_rel, pattern) or fnmatch.fnmatch(basename, pattern) for pattern in patterns)
def files_for_patterns(files_rel: list[str], patterns: list[str]) -> list[str]:
return sorted(file_rel for file_rel in files_rel if match_patterns(file_rel, patterns))
def status_for_category(files: list[str], missing_status: str = "missing") -> dict[str, Any]:
return {"status": "present" if files else missing_status, "files": files}
def inspect_structured_files(
bundle_root: pathlib.Path,
file_rels: list[str],
missing_status: str = "missing",
) -> dict[str, Any]:
if not file_rels:
return {"status": missing_status, "files": []}
errors: list[str] = []
for file_rel in file_rels:
try:
load_structured(bundle_root / file_rel)
except Exception as exc:
errors.append(f"{file_rel}: {exc}")
if errors:
return {"status": "unreadable", "files": file_rels, "errors": errors}
return {"status": "present", "files": file_rels}
def inspect_zip_files(bundle_root: pathlib.Path, zip_rels: list[str]) -> dict[str, Any]:
if not zip_rels:
return {"status": "missing", "files": []}
errors: list[str] = []
entry_counts: dict[str, int] = {}
for file_rel in zip_rels:
path = bundle_root / file_rel
try:
with zipfile.ZipFile(path) as archive:
bad_member = archive.testzip()
if bad_member is not None:
errors.append(f"{file_rel}: unreadable member {bad_member}")
entry_counts[file_rel] = len(archive.namelist())
except Exception as exc:
errors.append(f"{file_rel}: {exc}")
if errors:
return {"status": "unreadable", "files": zip_rels, "errors": errors, "entry_counts": entry_counts}
return {"status": "readable", "files": zip_rels, "entry_counts": entry_counts}
def category_matches(profile: Mapping[str, Any], files_rel: list[str]) -> tuple[dict[str, list[str]], dict[str, list[str]]]:
required_matches = {
category: files_for_patterns(files_rel, patterns)
for category, patterns in profile["required_files"].items()
}
optional_matches = {
category: files_for_patterns(files_rel, patterns)
for category, patterns in profile["optional_files"].items()
}
return required_matches, optional_matches
def allowed_patterns(profile: Mapping[str, Any]) -> list[str]:
patterns: list[str] = []
for pattern_map_name in ("required_files", "optional_files"):
for category_patterns in profile[pattern_map_name].values():
patterns.extend(category_patterns)
patterns.extend(profile.get("allowed_extra_patterns", []))
return patterns
def extra_files(files_rel: list[str], profile: Mapping[str, Any]) -> list[str]:
patterns = allowed_patterns(profile)
return [file_rel for file_rel in files_rel if not match_patterns(file_rel, patterns)]
def empty_report(bundle_root: pathlib.Path, profile_name: str) -> dict[str, Any]:
return {
"bundle_root": str(bundle_root),
"profile": profile_name,
"files_discovered": [],
"required_files_present": {},
"required_files_missing": [],
"optional_files_present": {},
"manifest_status": {"status": "missing", "files": []},
"zip_status": {"status": "missing", "files": []},
"validation_sidecar_status": {"status": "missing", "files": []},
"report_status": {"status": "missing", "files": []},
"warnings": [],
"blocking_errors": [],
"machine_readable_summary": {},
}
def audit_bundle(bundle_root: pathlib.Path, profile_name: str, profile: Mapping[str, Any]) -> dict[str, Any]:
report = empty_report(bundle_root, profile_name)
files = discover_files(bundle_root)
files_rel = [rel_path(path, bundle_root) for path in files]
report["files_discovered"] = files_rel
required_matches, optional_matches = category_matches(profile, files_rel)
report["required_files_present"] = {
category: matches for category, matches in required_matches.items() if matches
}
report["optional_files_present"] = {
category: matches for category, matches in optional_matches.items() if matches
}
for category, matches in required_matches.items():
if not matches:
report["required_files_missing"].append(category)
report["blocking_errors"].append(f"Missing required {category} file.")
manifest_files = required_matches.get("manifest", [])
sidecar_files = required_matches.get("validation_sidecar", [])
report_files = optional_matches.get("report", [])
zip_files = optional_matches.get("zip", [])
report["manifest_status"] = inspect_structured_files(bundle_root, manifest_files)
report["validation_sidecar_status"] = inspect_structured_files(bundle_root, sidecar_files)
report["report_status"] = status_for_category(report_files)
report["zip_status"] = inspect_zip_files(bundle_root, zip_files)
if report["manifest_status"]["status"] == "unreadable":
report["blocking_errors"].append("Manifest file is unreadable.")
if report["validation_sidecar_status"]["status"] == "unreadable":
report["blocking_errors"].append("Validation sidecar file is unreadable.")
if report["zip_status"]["status"] == "unreadable":
report["blocking_errors"].append("Zip file is unreadable.")
if not zip_files:
report["warnings"].append("No zip file matched the profile.")
if not report_files:
report["warnings"].append("No review report file matched the profile.")
for file_rel in extra_files(files_rel, profile):
report["warnings"].append(f"Extra file not matched by profile: {file_rel}")
report["required_files_missing"] = sorted(report["required_files_missing"])
report["warnings"] = sorted(report["warnings"])
report["blocking_errors"] = sorted(set(report["blocking_errors"]))
finalize_report(report)
return report
def finalize_report(report: dict[str, Any]) -> None:
status = "FAIL" if report["blocking_errors"] else "PASS"
report["machine_readable_summary"] = {
"status": status,
"files_discovered_count": len(report["files_discovered"]),
"required_missing_count": len(report["required_files_missing"]),
"warning_count": len(report["warnings"]),
"blocking_error_count": len(report["blocking_errors"]),
"manifest_status": report["manifest_status"]["status"],
"zip_status": report["zip_status"]["status"],
"validation_sidecar_status": report["validation_sidecar_status"]["status"],
"report_status": report["report_status"]["status"],
}
def write_json_report(report: Mapping[str, Any], output_dir: pathlib.Path) -> None:
(output_dir / REPORT_JSON).write_text(
json.dumps(report, ensure_ascii=False, indent=2),
encoding="utf-8",
)
def write_markdown_report(report: Mapping[str, Any], output_dir: pathlib.Path) -> None:
summary = report["machine_readable_summary"]
lines = [
"# Review Bundle Audit",
"",
"## Summary",
"",
f"- Status: `{summary['status']}`",
f"- Bundle root: `{report['bundle_root']}`",
f"- Profile: `{report['profile']}`",
f"- Files discovered: {summary['files_discovered_count']}",
f"- Required files missing: {summary['required_missing_count']}",
f"- Warnings: {summary['warning_count']}",
f"- Blocking errors: {summary['blocking_error_count']}",
"",
"## Required Files",
"",
]
for category, files in report["required_files_present"].items():
lines.append(f"- `{category}`: {', '.join(f'`{file}`' for file in files)}")
if report["required_files_missing"]:
lines.append("")
lines.append("Missing:")
for category in report["required_files_missing"]:
lines.append(f"- `{category}`")
lines.extend(["", "## Package Status", ""])
for key in ("manifest_status", "validation_sidecar_status", "zip_status", "report_status"):
status = report[key]["status"]
files = report[key].get("files", [])
rendered_files = ", ".join(f"`{file}`" for file in files) if files else "none"
lines.append(f"- `{key}`: `{status}` ({rendered_files})")
lines.extend(["", "## Blocking Errors", ""])
if report["blocking_errors"]:
for error in report["blocking_errors"]:
lines.append(f"- {error}")
else:
lines.append("- None")
lines.extend(["", "## Warnings", ""])
if report["warnings"]:
for warning in report["warnings"]:
lines.append(f"- {warning}")
else:
lines.append("- None")
lines.extend(["", "## Files Discovered", ""])
for file_rel in report["files_discovered"]:
lines.append(f"- `{file_rel}`")
(output_dir / REPORT_MD).write_text("\n".join(lines) + "\n", encoding="utf-8")
def write_reports(report: Mapping[str, Any], output_dir: pathlib.Path) -> None:
output_dir.mkdir(parents=True, exist_ok=True)
write_json_report(report, output_dir)
write_markdown_report(report, output_dir)
def run(
bundle_root: pathlib.Path,
output_dir: pathlib.Path,
profile_name: str,
profile_config: pathlib.Path | None,
) -> int:
bundle_root = bundle_root.resolve()
output_dir = output_dir.resolve()
output_dir.mkdir(parents=True, exist_ok=True)
report = empty_report(bundle_root, profile_name)
if not bundle_root.is_dir():
report["blocking_errors"].append(f"Bundle root is not a directory: {bundle_root}")
finalize_report(report)
write_reports(report, output_dir)
return 2
try:
config = load_profile_config(profile_config)
profile = get_profile(profile_name, config)
report = audit_bundle(bundle_root, profile_name, profile)
except Exception as exc:
report["blocking_errors"].append(str(exc))
finalize_report(report)
write_reports(report, output_dir)
return 2
write_reports(report, output_dir)
return 1 if report["blocking_errors"] else 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Audit a review bundle directory.")
parser.add_argument("--bundle-root", required=True, type=pathlib.Path)
parser.add_argument("--output-dir", required=True, type=pathlib.Path)
parser.add_argument("--profile", default="generic")
parser.add_argument("--profile-config", type=pathlib.Path)
args = parser.parse_args(argv)
return run(args.bundle_root, args.output_dir, args.profile, args.profile_config)
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,196 @@
from __future__ import annotations
import json
import pathlib
import shutil
import subprocess
import sys
import tempfile
import unittest
import zipfile
SKILL_ROOT = pathlib.Path(__file__).resolve().parents[1]
REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT = SKILL_ROOT / "scripts" / "review_bundle_audit.py"
TMP_ROOT = REPO_ROOT / "tmp" / "review-bundle-audit-tests"
def run_cli(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
text=True,
capture_output=True,
check=False,
)
def write_text(path: pathlib.Path, text: str = "content") -> pathlib.Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
return path
def write_json(path: pathlib.Path, data: object) -> pathlib.Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
return path
def write_zip(path: pathlib.Path) -> pathlib.Path:
path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(path, "w") as archive:
archive.writestr("included.txt", "ok")
return path
def load_report(output_dir: pathlib.Path) -> dict[str, object]:
return json.loads((output_dir / "bundle-audit.json").read_text(encoding="utf-8"))
class ReviewBundleAuditTest(unittest.TestCase):
def setUp(self) -> None:
TMP_ROOT.mkdir(parents=True, exist_ok=True)
self.tmp_dir = tempfile.TemporaryDirectory(dir=TMP_ROOT)
self.bundle_root = pathlib.Path(self.tmp_dir.name) / "ccra_review_bundle" / "round-05"
self.output_dir = pathlib.Path(self.tmp_dir.name) / "outputs"
self.bundle_root.mkdir(parents=True)
def tearDown(self) -> None:
self.tmp_dir.cleanup()
@classmethod
def tearDownClass(cls) -> None:
if TMP_ROOT.exists():
shutil.rmtree(TMP_ROOT)
def write_complete_bundle(self) -> None:
write_text(self.bundle_root / "review-brief.md", "# Brief\n")
write_json(self.bundle_root / "review-manifest.json", {"files": ["review-brief.md"]})
write_json(self.bundle_root / "validation-sidecar.json", {"status": "pass"})
write_text(self.bundle_root / "local-review-report.md", "# Report\n")
write_text(self.bundle_root / "changed-files.md", "- src/model.json\n")
write_zip(self.bundle_root / "raw-review-bundle.zip")
def run_audit(self, *extra_args: str) -> tuple[subprocess.CompletedProcess[str], dict[str, object]]:
result = run_cli(
"--bundle-root",
str(self.bundle_root),
"--output-dir",
str(self.output_dir),
*extra_args,
)
return result, load_report(self.output_dir)
def test_complete_bundle_passes_and_reports_present_files(self) -> None:
self.write_complete_bundle()
result, report = self.run_audit()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(report["machine_readable_summary"]["status"], "PASS")
self.assertEqual(report["required_files_missing"], [])
self.assertEqual(report["manifest_status"]["status"], "present")
self.assertEqual(report["zip_status"]["status"], "readable")
self.assertEqual(report["validation_sidecar_status"]["status"], "present")
self.assertEqual(report["report_status"]["status"], "present")
self.assertIn("review-brief.md", report["files_discovered"])
def test_missing_required_brief_is_blocking(self) -> None:
self.write_complete_bundle()
(self.bundle_root / "review-brief.md").unlink()
result, report = self.run_audit()
self.assertEqual(result.returncode, 1, result.stderr)
self.assertEqual(report["machine_readable_summary"]["status"], "FAIL")
self.assertIn("brief", report["required_files_missing"])
self.assertTrue(any("brief" in error for error in report["blocking_errors"]))
def test_missing_manifest_is_blocking(self) -> None:
self.write_complete_bundle()
(self.bundle_root / "review-manifest.json").unlink()
result, report = self.run_audit()
self.assertEqual(result.returncode, 1, result.stderr)
self.assertEqual(report["manifest_status"]["status"], "missing")
self.assertIn("manifest", report["required_files_missing"])
def test_missing_validation_sidecar_is_blocking(self) -> None:
self.write_complete_bundle()
(self.bundle_root / "validation-sidecar.json").unlink()
result, report = self.run_audit()
self.assertEqual(result.returncode, 1, result.stderr)
self.assertEqual(report["validation_sidecar_status"]["status"], "missing")
self.assertIn("validation_sidecar", report["required_files_missing"])
def test_unreadable_zip_is_blocking_when_zip_is_present(self) -> None:
self.write_complete_bundle()
write_text(self.bundle_root / "raw-review-bundle.zip", "not a real zip")
result, report = self.run_audit()
self.assertEqual(result.returncode, 1, result.stderr)
self.assertEqual(report["zip_status"]["status"], "unreadable")
self.assertTrue(any("zip" in error.casefold() for error in report["blocking_errors"]))
def test_extra_files_are_warnings_not_blockers(self) -> None:
self.write_complete_bundle()
write_text(self.bundle_root / "scratch.tmp", "operator note")
result, report = self.run_audit()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(report["machine_readable_summary"]["status"], "PASS")
self.assertEqual(report["blocking_errors"], [])
self.assertTrue(any("scratch.tmp" in warning for warning in report["warnings"]))
def test_utf8_paths_and_chinese_filenames_are_reported(self) -> None:
self.bundle_root = pathlib.Path(self.tmp_dir.name) / "审查包"
self.bundle_root.mkdir()
write_text(self.bundle_root / "审查-brief.md", "# Brief\n")
write_json(self.bundle_root / "审查-manifest.json", {"files": ["审查-brief.md"]})
write_json(self.bundle_root / "验证-validation-sidecar.json", {"status": "pass"})
write_text(self.bundle_root / "审查-report.md", "# Report\n")
write_zip(self.bundle_root / "原始-review-bundle.zip")
result, report = self.run_audit()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("审查-brief.md", report["files_discovered"])
self.assertEqual(report["zip_status"]["files"][0], "原始-review-bundle.zip")
def test_profile_config_can_define_custom_required_patterns(self) -> None:
profile_config = pathlib.Path(self.tmp_dir.name) / "profile.json"
write_text(self.bundle_root / "handoff.txt", "handoff")
write_json(self.bundle_root / "index.json", {"files": ["handoff.txt"]})
write_json(self.bundle_root / "checks.json", {"status": "pass"})
write_json(
profile_config,
{
"profiles": {
"custom": {
"required_files": {
"brief": ["handoff.txt"],
"manifest": ["index.json"],
"validation_sidecar": ["checks.json"],
},
"optional_files": {"report": ["notes.md"], "zip": ["*.zip"]},
"allowed_extra_patterns": ["handoff.txt", "index.json", "checks.json"],
}
}
},
)
result, report = self.run_audit("--profile", "custom", "--profile-config", str(profile_config))
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(report["profile"], "custom")
self.assertEqual(report["required_files_missing"], [])
if __name__ == "__main__":
unittest.main()