← Files Build 3D Game RoomsARCHIVED FILE

skills/build-3d-game-rooms/scripts/manage_lessons.py

3.6 KB · Oct 5, 2026 · 18:33 UTC

↓ Download file

#!/usr/bin/env python3
"""Maintain durable lessons without erasing rejected or superseded history."""

from __future__ import annotations

import argparse
import datetime as dt
import json
from pathlib import Path


REQUIRED = ("sourceRoom", "observation", "rule", "scope", "pipelineStages")


def load(path):
    data = json.loads(path.read_text(encoding="utf-8"))
    if data.get("schema") != "game-room.room-lessons.v1" or not isinstance(data.get("lessons"), list):
        raise SystemExit("invalid lesson ledger")
    return data


def save(path, data):
    path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")


def find(data, lesson_id):
    matches = [lesson for lesson in data["lessons"] if lesson.get("id") == lesson_id]
    if len(matches) != 1:
        raise SystemExit(f"expected exactly one lesson {lesson_id!r}, found {len(matches)}")
    return matches[0]


def main():
    default = Path(__file__).resolve().parent.parent / "references" / "lessons.json"
    parser = argparse.ArgumentParser()
    parser.add_argument("--ledger", type=Path, default=default)
    sub = parser.add_subparsers(dest="command", required=True)
    propose = sub.add_parser("propose")
    propose.add_argument("--id", required=True)
    for field in REQUIRED:
        propose.add_argument(f"--{field.replace('R', '-r').replace('S', '-s')}", required=True)
    propose.add_argument("--evidence", action="append", default=[])
    propose.add_argument("--metrics-json", default="{}")
    decide = sub.add_parser("decide")
    decide.add_argument("--id", required=True)
    decide.add_argument("--decision", required=True, choices=("approved", "rejected"))
    decide.add_argument("--approved-by", required=True)
    decide.add_argument("--reason", default="")
    supersede = sub.add_parser("supersede")
    supersede.add_argument("--id", required=True)
    supersede.add_argument("--by", required=True)
    args = parser.parse_args()
    ledger = args.ledger.resolve()
    data = load(ledger)
    today = dt.date.today().isoformat()
    if args.command == "propose":
        if any(item.get("id") == args.id for item in data["lessons"]):
            raise SystemExit(f"lesson already exists: {args.id}")
        stages = [item.strip() for item in args.pipeline_stages.split(",") if item.strip()]
        if not stages or any(item not in {"function", "form", "runtime"} for item in stages):
            raise SystemExit("pipeline stages must be function,form,runtime")
        data["lessons"].append({
            "id": args.id, "status": "pending", "sourceRoom": args.source_room,
            "observation": args.observation, "rule": args.rule, "evidence": args.evidence,
            "metrics": json.loads(args.metrics_json), "scope": args.scope,
            "pipelineStages": stages, "proposedAt": today, "decidedAt": None,
            "approvedBy": None, "decisionReason": "", "supersedes": None, "supersededBy": None
        })
    elif args.command == "decide":
        lesson = find(data, args.id)
        if lesson.get("status") != "pending":
            raise SystemExit("only pending lessons can be decided")
        lesson.update(status=args.decision, decidedAt=today, approvedBy=args.approved_by, decisionReason=args.reason)
    else:
        lesson = find(data, args.id)
        replacement = find(data, args.by)
        if lesson.get("status") != "approved" or replacement.get("status") != "approved":
            raise SystemExit("both lessons must be approved before superseding")
        lesson["status"] = "superseded"
        lesson["supersededBy"] = args.by
        replacement["supersedes"] = args.id
    save(ledger, data)


if __name__ == "__main__":
    main()

SHA-256: ab95babba5ce60c2e0ad1ff0858804db27fb1fd86875b78fc280be04923258b9