← Files LegalQuants TransactionalARCHIVED FILE

skills/playbook-builder/scripts/playbook_builder.py

10.3 KB · Oct 3, 2026 · 06:34 UTC

↓ Download file

#!/usr/bin/env python3
"""Deterministic command-line helpers for playbook-builder."""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

from shared.playbook_runtime import (
    PlaybookError,
    build_receipt,
    build_source_manifest,
    import_playbook_table,
    load_json,
    register_playbook,
    render_playbook_html,
    render_playbook_markdown,
    seal_playbook,
    validate_playbook,
    write_json_atomic,
    write_text_atomic,
)


def _role_map(values: list[str]) -> dict[str, str]:
    roles: dict[str, str] = {}
    for value in values:
        if "=" not in value:
            raise PlaybookError("--role must use FILE=ROLE")
        name, role = value.split("=", 1)
        roles[name] = role
    return roles


def _manifest(args: argparse.Namespace) -> int:
    manifest = build_source_manifest(
        [Path(source) for source in args.sources],
        Path(args.boundary),
        _role_map(args.role),
        max_sources=args.max_sources,
    )
    write_json_atomic(Path(args.out), manifest)
    print(f"Wrote {args.out} with {len(manifest['documents'])} documents")
    return 0


def _validate(args: argparse.Namespace) -> int:
    errors = validate_playbook(load_json(Path(args.playbook)))
    print(json.dumps({"valid": not errors, "errors": errors}, indent=2))
    return 1 if errors else 0


def _receipt(args: argparse.Namespace) -> int:
    playbook_path = Path(args.playbook)
    playbook = load_json(playbook_path)
    receipt = build_receipt(
        playbook,
        load_json(Path(args.manifest)),
    )
    write_json_atomic(Path(args.out), receipt)
    print(json.dumps(receipt, indent=2))
    if not receipt["validationErrors"] and playbook.get("status") == "approved":
        package_dir = playbook_path.parent
        try:
            register_playbook(playbook, package_dir)
            print(
                f"Registered playbook {playbook.get('playbookId')} in playbook-registry.json"
            )
        except Exception as exc:
            print(f"Warning: could not register playbook: {exc}", file=sys.stderr)
    return 1 if receipt["validationErrors"] else 0


def _register(args: argparse.Namespace) -> int:
    playbook_path = Path(args.playbook)
    package_path = (
        Path(args.package_path) if args.package_path else playbook_path.parent
    )
    registry_path = Path(args.registry) if args.registry else None
    playbook = load_json(playbook_path)
    register_playbook(playbook, package_path, registry_path)
    print(f"Registered {playbook.get('playbookId')} (v{playbook.get('version')})")
    return 0


def _render(args: argparse.Namespace) -> int:
    rendered = render_playbook_html(load_json(Path(args.playbook)))
    write_text_atomic(Path(args.out), rendered)
    print(f"Wrote {args.out}")
    return 0


def _render_md(args: argparse.Namespace) -> int:
    manifest = load_json(Path(args.manifest)) if args.manifest else None
    rendered = render_playbook_markdown(load_json(Path(args.playbook)), manifest)
    write_text_atomic(Path(args.out), rendered)
    print(f"Wrote {args.out}")
    return 0


def _seal(args: argparse.Namespace) -> int:
    playbook_path = Path(args.playbook)
    playbook = load_json(playbook_path)
    manifest = load_json(Path(args.manifest))
    package_path = (
        Path(args.package_path) if args.package_path else playbook_path.parent
    )
    registry_path = Path(args.registry) if args.registry else None

    result = seal_playbook(
        playbook,
        manifest,
        version=args.version,
        package_path=package_path,
        registry_path=registry_path,
    )

    out_playbook = Path(args.out_playbook) if args.out_playbook else playbook_path
    write_json_atomic(out_playbook, result["playbook"])
    print(f"Wrote sealed playbook to {out_playbook}")

    if args.out_receipt:
        write_json_atomic(Path(args.out_receipt), result["receipt"])
        print(f"Wrote build receipt to {args.out_receipt}")

    if args.out_markdown:
        write_text_atomic(Path(args.out_markdown), result["markdown"])
        print(f"Wrote markdown to {args.out_markdown}")

    print(
        f"Sealed {result['playbook'].get('playbookId')} (v{result['playbook'].get('version')}) successfully."
    )
    return 0


def _import_playbook(args: argparse.Namespace) -> int:
    registry_path = Path(args.registry) if args.registry else None
    result = import_playbook_table(
        source_path=args.table,
        title=args.title,
        playbook_id=args.playbook_id,
        perspective=args.perspective,
        family=args.family,
        governing_law=args.governing_law,
        version=args.version,
        out_dir=args.out_dir,
        approve_note=args.approve_all,
        seal=args.seal,
        registry_path=registry_path,
    )
    print(
        json.dumps(
            {
                "status": result["status"],
                "playbookId": result["playbookId"],
                "version": result["version"],
                "issuesCount": result["issuesCount"],
                "rowsSkipped": result["rowsSkipped"],
                "columnMap": result["columnMap"],
                "packageDir": result["packageDir"],
                "sealed": result["sealed"],
            },
            indent=2,
        )
    )
    return 0


def parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser(
        description="Hash, inventory and validate Playbook Engine build artifacts"
    )
    commands = root.add_subparsers(dest="command", required=True)

    import_cmd = commands.add_parser(
        "import-playbook",
        help="Import a playbook from a Word (.docx), Excel (.xlsx), or CSV table",
    )
    import_cmd.add_argument("table", help="Path to .docx, .xlsx, or .csv table file")
    import_cmd.add_argument("--title", required=True, help="Playbook title")
    import_cmd.add_argument(
        "--playbook-id", required=True, help="Playbook identifier (slug)"
    )
    import_cmd.add_argument(
        "--perspective",
        required=True,
        help="Represented party the table is written for (e.g. supplier, customer)",
    )
    import_cmd.add_argument(
        "--family",
        required=True,
        help="Agreement family the table covers (e.g. MSA, SaaS, NDA)",
    )
    import_cmd.add_argument(
        "--governing-law",
        default=None,
        help="Governing law the table assumes, if the lawyer states one",
    )
    import_cmd.add_argument(
        "--version", default="1.0.0", help="Playbook version (default: 1.0.0)"
    )
    import_cmd.add_argument(
        "--out-dir", help="Directory to save imported playbook package"
    )
    import_cmd.add_argument(
        "--approve-all",
        metavar="NOTE",
        help=(
            "Mark every imported row approved, recording the lawyer's confirmation "
            "that the table is approved firm policy. Without it rows are imported "
            "as candidates in a draft playbook"
        ),
    )
    import_cmd.add_argument(
        "--seal",
        action="store_true",
        help="Seal and register the imported playbook (requires --approve-all)",
    )
    import_cmd.add_argument("--registry", help="Path to playbook-registry.json")
    import_cmd.set_defaults(handler=_import_playbook)

    manifest = commands.add_parser("manifest", help="Create a frozen source manifest")
    manifest.add_argument("sources", nargs="+")
    manifest.add_argument("--boundary", required=True)
    manifest.add_argument("--role", action="append", default=[], metavar="FILE=ROLE")
    manifest.add_argument(
        "--max-sources",
        type=int,
        default=5,
        help="Maximum allowed source files (default: 5)",
    )
    manifest.add_argument("--out", required=True)
    manifest.set_defaults(handler=_manifest)

    validate = commands.add_parser("validate", help="Validate playbook invariants")
    validate.add_argument("playbook")
    validate.set_defaults(handler=_validate)

    receipt = commands.add_parser(
        "receipt", help="Create a deterministic build receipt"
    )
    receipt.add_argument("manifest")
    receipt.add_argument("playbook")
    receipt.add_argument("--out", required=True)
    receipt.set_defaults(handler=_receipt)

    seal_cmd = commands.add_parser(
        "seal", help="Validate, seal, and register an approved playbook package"
    )
    seal_cmd.add_argument("playbook", help="Path to candidate playbook.json")
    seal_cmd.add_argument(
        "--manifest", required=True, help="Path to source-manifest.json"
    )
    seal_cmd.add_argument("--version", help="Optional version to seal (e.g. 1.0.0)")
    seal_cmd.add_argument("--package-path", help="Path to playbook package directory")
    seal_cmd.add_argument("--registry", help="Path to playbook-registry.json")
    seal_cmd.add_argument("--out-playbook", help="Path to write sealed playbook.json")
    seal_cmd.add_argument("--out-receipt", help="Path to write build-receipt.json")
    seal_cmd.add_argument("--out-markdown", help="Path to write playbook.md")
    seal_cmd.set_defaults(handler=_seal)

    register_cmd = commands.add_parser(
        "register", help="Register playbook in the playbook registry"
    )
    register_cmd.add_argument("playbook", help="Path to playbook.json")
    register_cmd.add_argument(
        "--package-path", help="Path to playbook package directory"
    )
    register_cmd.add_argument("--registry", help="Path to playbook-registry.json")
    register_cmd.set_defaults(handler=_register)

    render = commands.add_parser(
        "render", help="Render a standalone local HTML playbook"
    )
    render.add_argument("playbook")
    render.add_argument("--out", required=True)
    render.set_defaults(handler=_render)

    render_md = commands.add_parser(
        "render-md", help="Render a standalone local Markdown playbook"
    )
    render_md.add_argument("playbook")
    render_md.add_argument(
        "--manifest", help="Optional path to source-manifest.json for file names"
    )
    render_md.add_argument("--out", required=True)
    render_md.set_defaults(handler=_render_md)
    return root


def main(argv: list[str] | None = None) -> int:
    try:
        args = parser().parse_args(argv)
        return args.handler(args)
    except (OSError, json.JSONDecodeError, PlaybookError) as exc:
        print(f"playbook-builder: {exc}", file=sys.stderr)
        return 2


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: e6e23dd0f927cb2d7a6de4d777f1f7b4cb44271a771d6f8a7e04dff930688dd7