229c381c3e
* docs: import zacus conversation bundle v3 and close ZIP intake TODO * tools: add reproducible validator installer and document next gates * docs: remove ZIP terminology and align intake docs to conversation bundle * tools: add runtime bundle validator and update docs workflow * feat: promote gameplay scenario and enforce G3 bundle checks in CI * docs: add deep repo analysis and next-step TODOs * Initial plan (#107) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
114 lines
4.7 KiB
Python
Executable File
114 lines
4.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Generate a functional diff report between canonical and conversation gameplay YAML files."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from datetime import date
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
try:
|
|
import yaml
|
|
except ImportError: # pragma: no cover
|
|
sys.exit("Missing dependency: install PyYAML (pip install pyyaml) to diff scenario YAML files.")
|
|
|
|
|
|
def load_yaml(path: Path) -> dict:
|
|
data = yaml.safe_load(path.read_text())
|
|
if not isinstance(data, dict):
|
|
raise ValueError(f"{path} must contain a YAML mapping")
|
|
return data
|
|
|
|
|
|
def _safe_len(value) -> int:
|
|
return len(value) if isinstance(value, list) else 0
|
|
|
|
|
|
def build_report(base_path: Path, target_path: Path, base: dict, target: dict) -> str:
|
|
lines: list[str] = []
|
|
lines.append("# Rapport de diff fonctionnel — scénarios Zacus")
|
|
lines.append("")
|
|
lines.append(f"- Date: {date.today().isoformat()}")
|
|
lines.append(f"- Base canon: `{base_path}`")
|
|
lines.append(f"- Cible gameplay: `{target_path}`")
|
|
lines.append("")
|
|
|
|
lines.append("## Identité")
|
|
lines.append(f"- id: `{base.get('id')}` -> `{target.get('id')}`")
|
|
lines.append(f"- version: `{base.get('version')}` -> `{target.get('version')}`")
|
|
lines.append(f"- title: `{base.get('title')}` -> `{target.get('title')}`")
|
|
lines.append("")
|
|
|
|
lines.append("## Cadre de session")
|
|
base_players = base.get("players", {})
|
|
target_players = target.get("players", {})
|
|
lines.append(
|
|
f"- players: `{base_players.get('min')}-{base_players.get('max')}` -> `{target_players.get('min')}-{target_players.get('max')}`"
|
|
)
|
|
base_duration = base.get("duration_minutes", {})
|
|
target_duration = target.get("duration_minutes", {})
|
|
lines.append(
|
|
f"- duration_minutes: `{base_duration.get('min')}-{base_duration.get('max')}` -> `{target_duration.get('min')}-{target_duration.get('max')}`"
|
|
)
|
|
lines.append("")
|
|
|
|
lines.append("## Structure narrative")
|
|
lines.append(f"- stations: `{_safe_len(base.get('stations'))}` -> `{_safe_len(target.get('stations'))}`")
|
|
lines.append(f"- puzzles: `{_safe_len(base.get('puzzles'))}` -> `{_safe_len(target.get('puzzles'))}`")
|
|
lines.append(f"- solution_unique: `{base.get('solution_unique')}` -> `{target.get('solution_unique')}`")
|
|
lines.append("")
|
|
|
|
lines.append("## Changements clés détectés")
|
|
base_station_names = [s.get("name") for s in base.get("stations", []) if isinstance(s, dict)]
|
|
target_station_names = [s.get("name") for s in target.get("stations", []) if isinstance(s, dict)]
|
|
lines.append(f"- Stations cibles: {', '.join([str(x) for x in target_station_names])}")
|
|
|
|
base_puzzle_ids = [p.get("id") for p in base.get("puzzles", []) if isinstance(p, dict)]
|
|
target_puzzle_ids = [p.get("id") for p in target.get("puzzles", []) if isinstance(p, dict)]
|
|
lines.append(f"- IDs puzzles base: {', '.join([str(x) for x in base_puzzle_ids])}")
|
|
lines.append(f"- IDs puzzles cible: {', '.join([str(x) for x in target_puzzle_ids])}")
|
|
|
|
if base_station_names != target_station_names:
|
|
lines.append("- ✅ Les stations ont été réalignées avec le gameplay U-SON / Zone 4 / Archives.")
|
|
if base_puzzle_ids != target_puzzle_ids:
|
|
lines.append("- ✅ Les puzzles ont été remplacés pour refléter LA 440 → LEFOU → QR WIN.")
|
|
|
|
lines.append("")
|
|
lines.append("## Impact")
|
|
lines.append("- Source de vérité gameplay maintenue dans `game/scenarios/*.yaml`.")
|
|
lines.append("- Bundle conversationnel conservé comme matériau de travail parallèle.")
|
|
lines.append("- Validation G3 requise via validateurs scénario/audio/printables + runtime bundle.")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Generate functional diff report between two scenario YAML files.")
|
|
parser.add_argument("--base", default="scenario-ai-coherence/zacus_conversation_bundle_v3/zacus_v2.yaml")
|
|
parser.add_argument("--target", default="game/scenarios/zacus_v1.yaml")
|
|
parser.add_argument("--out", default=f"artifacts/qa-test/{date.today().isoformat()}/gameplay-functional-diff.md")
|
|
args = parser.parse_args()
|
|
|
|
base_path = Path(args.base)
|
|
target_path = Path(args.target)
|
|
out_path = Path(args.out)
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
try:
|
|
base = load_yaml(base_path)
|
|
target = load_yaml(target_path)
|
|
except (OSError, ValueError, yaml.YAMLError) as exc:
|
|
print(f"[scenario-diff] error -> {exc}")
|
|
return 1
|
|
|
|
report = build_report(base_path, target_path, base, target)
|
|
out_path.write_text(report)
|
|
print(f"[scenario-diff] ok report={out_path}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|