master
barrett-land Publish 0.0.42 7b8ee75 2mo ago
#!/usr/bin/env python3
"""check_wiring — prove the three halves of this bridge agree, before publishing.

The bridge is three files that each carry their own list of what exists:

    bridge.json   verbs[]            what Adom Desktop advertises
    server.py     COMMAND_HANDLERS   what the HTTP server routes
    AdomAltium.pas  Dispatch         what Altium actually implements

Nothing enforced that they matched, and they drifted: the shipped package routed
verbs its own script could not run, and the repo's readable source was two verbs
behind the zip that was actually installed. Run this in deploy.sh so a mismatch
fails the build instead of shipping.

Pure static parsing (regex over the sources) so it runs in the Linux container,
where the Windows-only bridge modules cannot be imported.

Usage:  python3 check_wiring.py [bridge-src-dir]   -> exit 0 clean, 1 on mismatch
"""

import json
import re
import sys
from pathlib import Path

# Verbs the server answers itself, with no in-Altium counterpart to match against.
HOST_ONLY = {"altium_ping", "altium_detect", "altium_script_state",
             "altium_install_extension"}


def routed_verbs(server_py: Path) -> dict:
    """{verb: pas_command_or_None} from COMMAND_HANDLERS plus each handler's body."""
    text = server_py.read_text(encoding="utf-8")

    table = re.search(r"COMMAND_HANDLERS\s*=\s*\{(.*?)\n\}", text, re.S)
    if not table:
        sys.exit("check_wiring: COMMAND_HANDLERS table not found in server.py")
    pairs = re.findall(r'"(altium_[a-z_]+)"\s*:\s*(\w+)', table.group(1))

    # Map handler function -> the addin_call command it issues (if any).
    bodies = re.split(r"\ndef ", text)
    handler_cmd = {}
    for body in bodies:
        name = re.match(r"(\w+)\s*\(", body)
        if not name:
            continue
        call = re.search(r'addin_call\(\s*"([a-z_]+)"', body)
        handler_cmd[name.group(1)] = call.group(1) if call else None

    return {verb: handler_cmd.get(fn) for verb, fn in pairs}


def pas_commands(pas: Path) -> set:
    """Command strings the DelphiScript Dispatch procedure compares against."""
    text = pas.read_text(encoding="utf-8", errors="replace")
    block = re.search(r"procedure Dispatch;(.*?)\nend;", text, re.S)
    if not block:
        sys.exit("check_wiring: Dispatch procedure not found in AdomAltium.pas")
    return set(re.findall(r"Cmd\s*=\s*'([a-z_]+)'", block.group(1)))


def main() -> int:
    root = Path(sys.argv[1] if len(sys.argv) > 1 else Path(__file__).parent)
    routed = routed_verbs(root / "server.py")
    implemented = pas_commands(root / "addin" / "AdomAltium" / "AdomAltium.pas")
    advertised = set(json.loads((root / "bridge.json").read_text())["verbs"])

    problems = []

    # 1. Every routed verb must reach a command the script implements.
    for verb, cmd in sorted(routed.items()):
        if verb in HOST_ONLY:
            continue
        if cmd is None:
            problems.append(f"{verb}: handler issues no addin_call (host-only? add to HOST_ONLY)")
        elif cmd not in implemented:
            problems.append(f"{verb}: routes to '{cmd}', which AdomAltium.pas does not dispatch")

    # 2. bridge.json is what Adom Desktop shows the user; an omission hides a verb.
    for verb in sorted(set(routed) - advertised):
        problems.append(f"{verb}: routed by server.py but missing from bridge.json verbs[]")
    for verb in sorted(advertised - set(routed)):
        problems.append(f"{verb}: advertised in bridge.json but not routed by server.py")

    # 3. Script commands with no verb are dead weight — a warning, not a failure:
    #    'add_lcsc_param' is legitimately reached via the altium_add_component verb.
    unreachable = implemented - {c for c in routed.values() if c}
    if unreachable:
        print(f"note: script commands with no verb: {', '.join(sorted(unreachable))}")

    if problems:
        print(f"\nFAIL — {len(problems)} wiring mismatch(es):")
        for p in problems:
            print(f"  - {p}")
        return 1

    print(f"OK — {len(routed)} verbs routed, {len(implemented)} script commands, bridge.json agrees.")
    return 0


if __name__ == "__main__":
    sys.exit(main())