From 09c3664773aa53b0f511edf1729522d137ac7aba Mon Sep 17 00:00:00 2001 From: Shay Date: Wed, 13 May 2026 21:15:51 -0700 Subject: [PATCH] Fix CLI help/runtime imports and add doctor command (#4) * Make core CLI help robust and intuitive * Package runtime support modules for core CLI * Add CLI help and doctor tests * Fix CLI trace help and pack listing * Export language pack listing helper * Bootstrap repo root for console runtime imports * Align trace formatter with Proposition schema * Cover real trace payload formatting --- core/cli.py | 421 +++++++++++++++++++++++++------------ language_packs/__init__.py | 20 ++ pyproject.toml | 3 + tests/test_cli.py | 65 ++++++ 4 files changed, 370 insertions(+), 139 deletions(-) create mode 100644 tests/test_cli.py diff --git a/core/cli.py b/core/cli.py index 61dc7b9b..f8cdf818 100644 --- a/core/cli.py +++ b/core/cli.py @@ -1,180 +1,323 @@ -"""core CLI — single entry point for the versor engine. - -Subcommands ------------ -core chat Start interactive chat session (delegates to python -m chat) -core test [args] Run pytest suite with sane defaults -core check Run ruff on the whole project -core trace Trace one turn: show field condition, dialogue role, proposition -core oov Ground a single unknown token and show the constructed versor info -core pack list List mounted language packs -core pack verify Verify a language pack checksum -""" +"""Command line interface for the CORE versor engine.""" from __future__ import annotations +import argparse +import json import subprocess import sys -from typing import NoReturn +from collections.abc import Sequence +from pathlib import Path +from typing import Any, NoReturn -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -def _run(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]: - return subprocess.run(args, check=check, text=True) +# The `core` console script may be installed through stale editable metadata while +# this repo is moving quickly. Ensure sibling top-level packages such as +# alignment/, morphology/, and sensorium/ are importable from the checked-out +# source tree before any runtime imports execute. +_REPO_ROOT = Path(__file__).resolve().parent.parent +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) -def _die(msg: str) -> NoReturn: - print(f"error: {msg}", file=sys.stderr) - sys.exit(1) +DESCRIPTION = "CORE versor engine command suite." +EPILOG = "Examples:\n core chat\n core trace \"word beginning truth\"\n core trace --pack en_minimal_v1 --json \"word beginning truth\"\n core oov covenant\n core pack list\n core pack verify en_minimal_v1\n core test tests/test_alignment_graph.py -q" -def _usage() -> NoReturn: - print(__doc__) - sys.exit(0) +def _run(*args: str, check: bool = False) -> int: + """Run a child command and return its exit code.""" + completed = subprocess.run(args, check=check, text=True) + return int(completed.returncode) -# --------------------------------------------------------------------------- -# Subcommands -# --------------------------------------------------------------------------- +def _die(message: str, *, code: int = 2) -> NoReturn: + print(f"error: {message}", file=sys.stderr) + raise SystemExit(code) -def cmd_chat(argv: list[str]) -> None: + +def _print_runtime_import_hint(exc: BaseException) -> NoReturn: + _die( + "runtime import failed. Run `core doctor` to inspect packaging. Root cause: " + f"{exc.__class__.__name__}: {exc}", + code=1, + ) + + +def cmd_chat(args: argparse.Namespace) -> int: """Launch the readline REPL backed by ChatRuntime.""" - _run(sys.executable, "-m", "chat", *argv, check=False) + return _run(sys.executable, "-m", "chat", *args.args) -def cmd_test(argv: list[str]) -> None: - """Run the pytest suite. Extra args are forwarded to pytest.""" +def cmd_test(args: argparse.Namespace) -> int: + """Run pytest. Extra args are forwarded unchanged.""" default_args = ["-q", "--tb=short"] - _run(sys.executable, "-m", "pytest", *(argv or default_args), check=False) + return _run(sys.executable, "-m", "pytest", *(args.args or default_args)) -def cmd_check(argv: list[str]) -> None: - """Run ruff over the project source.""" - targets = argv or [ - "algebra", "ingest", "field", "vocab", "vault", "persona", - "generate", "session", "chat", "core", "language_packs", "tests", +def cmd_check(args: argparse.Namespace) -> int: + """Run ruff over selected project paths.""" + targets = args.paths or [ + "algebra", + "alignment", + "chat", + "core", + "field", + "generate", + "ingest", + "language_packs", + "morphology", + "persona", + "sensorium", + "session", + "vault", + "vocab", + "tests", ] - _run(sys.executable, "-m", "ruff", "check", *targets, check=False) + return _run(sys.executable, "-m", "ruff", "check", *targets) -def cmd_trace(argv: list[str]) -> None: - """Trace one chat turn and print field telemetry.""" - if not argv: - _die("usage: core trace ") - text = " ".join(argv) +def _runtime_for_trace(pack: list[str] | None): + try: + from chat.runtime import ChatRuntime + except Exception as exc: # pragma: no cover - exercised by CLI in broken envs + _print_runtime_import_hint(exc) + pack_arg: str | tuple[str, ...] + pack_arg = tuple(pack) if pack else ("en_minimal_v1", "he_logos_micro_v1", "grc_logos_micro_v1") + try: + return ChatRuntime(pack_arg) + except Exception as exc: + _die( + "failed to initialize ChatRuntime. Check mounted language packs with " + "`core pack list` and `core pack verify `. Root cause: " + f"{exc.__class__.__name__}: {exc}", + code=1, + ) - from chat.runtime import ChatRuntime - rt = ChatRuntime() - resp = rt.chat(text) - print(f"input : {text}") - print(f"surface : {resp.surface}") - print(f"dialogue_role : {resp.dialogue_role}") - print(f"versor_cond : {resp.versor_condition:.2e}") - p = resp.proposition - print(f"proposition : {p.surface!r}") - print(f" frame_id : {p.frame_id}") - print(f" subject : {p.subject_surface!r}") - print(f" predicate : {p.predicate_surface!r}") - if hasattr(p, "object_surface") and p.object_surface: - print(f" object : {p.object_surface!r}") +def _trace_payload(text: str, resp: Any, runtime: Any) -> dict[str, Any]: import numpy as np - print(f" relation_norm: {float(np.linalg.norm(p.relation)):.4f}") - session = rt.session - print(f"vault_entries : {len(session.vault)}") - oov = getattr(session.vocab, "unknown_token_log", []) - if oov: - print(f"oov_grounded : {len(oov)} token(s)") - for entry in oov: + proposition = resp.proposition + payload: dict[str, Any] = { + "input": text, + "surface": resp.surface, + "dialogue_role": str(resp.dialogue_role), + "versor_condition": float(resp.versor_condition), + "proposition": { + "surface": proposition.surface, + "frame_id": proposition.frame_id, + "subject": proposition.subject, + "predicate": proposition.predicate, + "object": proposition.object_, + "relation_norm": float(np.linalg.norm(proposition.relation)), + }, + "vault_entries": len(runtime.session.vault), + "oov_grounded": list(getattr(runtime.session.vocab, "unknown_token_log", [])), + } + return payload + + +def _print_trace(payload: dict[str, Any]) -> None: + print(f"input : {payload['input']}") + print(f"surface : {payload['surface']}") + print(f"dialogue_role : {payload['dialogue_role']}") + print(f"versor_cond : {payload['versor_condition']:.2e}") + proposition = payload["proposition"] + print(f"proposition : {proposition['surface']!r}") + print(f" frame_id : {proposition['frame_id']}") + print(f" subject : {proposition['subject']!r}") + print(f" predicate : {proposition['predicate']!r}") + if proposition.get("object"): + print(f" object : {proposition['object']!r}") + print(f" relation_norm: {proposition['relation_norm']:.4f}") + print(f"vault_entries : {payload['vault_entries']}") + oov_entries = payload["oov_grounded"] + if oov_entries: + print(f"oov_grounded : {len(oov_entries)} token(s)") + for entry in oov_entries: print(f" {entry}") -def cmd_oov(argv: list[str]) -> None: - """Ground a single unknown token and show the constructed versor info.""" - if not argv: - _die("usage: core oov ") - token = argv[0] +def cmd_trace(args: argparse.Namespace) -> int: + """Trace one chat turn and print field telemetry.""" + text = " ".join(args.text).strip() + if not text: + _die("trace requires input text. Try: core trace \"word beginning truth\"") - from chat.runtime import ChatRuntime - from algebra.versor import versor_condition - rt = ChatRuntime() - vocab = rt.session.vocab - - # Try known first + runtime = _runtime_for_trace(args.pack) try: - v = vocab.get_versor(token) - print(f"{token!r} is already in the manifold") - import numpy as np - print(f" versor_cond: {versor_condition(v):.2e}") - return - except KeyError: - pass - - # Ground it - from ingest.gate import inject - state = inject([token], vocab) - - import numpy as np - print(f"{token!r} — grounded as transient") - print(f" versor_cond : {versor_condition(state.F):.2e}") - oov_log = getattr(vocab, "unknown_token_log", []) - if oov_log: - last = oov_log[-1] - print(f" root_used : {last.get('root_used', '?')}") - print(f" ops_applied : {last.get('operators_applied', [])}") - - -def cmd_pack(argv: list[str]) -> None: - """Manage language packs.""" - if not argv: - _die("usage: core pack [list | verify ]") - - sub = argv[0] - - if sub == "list": - from language_packs import list_packs - packs = list_packs() - if not packs: - print("no compiled packs found") - for pid in packs: - print(pid) - - elif sub == "verify": - if len(argv) < 2: - _die("usage: core pack verify ") - pack_id = argv[1] - _run(sys.executable, "-m", "language_packs", "verify", pack_id, check=False) + response = runtime.chat(text, max_tokens=args.max_tokens) + except Exception as exc: + _die(f"trace failed: {exc.__class__.__name__}: {exc}", code=1) + payload = _trace_payload(text, response, runtime) + if args.json: + print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)) else: - _die(f"unknown pack subcommand: {sub!r}") + _print_trace(payload) + return 0 -# --------------------------------------------------------------------------- -# Dispatch -# --------------------------------------------------------------------------- +def cmd_oov(args: argparse.Namespace) -> int: + """Ground a single unknown token and show constructed versor info.""" + try: + from algebra.versor import versor_condition + from chat.runtime import ChatRuntime + except Exception as exc: # pragma: no cover - exercised by CLI in broken envs + _print_runtime_import_hint(exc) -_COMMANDS = { - "chat": cmd_chat, - "test": cmd_test, - "check": cmd_check, - "trace": cmd_trace, - "oov": cmd_oov, - "pack": cmd_pack, -} + runtime = ChatRuntime(tuple(args.pack) if args.pack else ("en_minimal_v1", "he_logos_micro_v1", "grc_logos_micro_v1")) + vocab = runtime.session.vocab + + try: + versor = vocab.get_versor(args.token) + except KeyError: + from ingest.gate import inject + + state = inject([args.token], vocab) + print(f"{args.token!r} — grounded as transient") + print(f" versor_cond : {versor_condition(state.F):.2e}") + oov_log = getattr(vocab, "unknown_token_log", []) + if oov_log: + last = oov_log[-1] + print(f" root_used : {last.get('root_used', '?')}") + print(f" ops_applied : {last.get('operators_applied', [])}") + else: + print(f"{args.token!r} is already in the manifold") + print(f" versor_cond: {versor_condition(versor):.2e}") + return 0 -def main() -> None: - args = sys.argv[1:] - if not args or args[0] in {"-h", "--help"}: - _usage() - sub = args[0] - if sub not in _COMMANDS: - _die(f"unknown subcommand {sub!r}. Try: {', '.join(_COMMANDS)}") - _COMMANDS[sub](args[1:]) +def cmd_pack_list(args: argparse.Namespace) -> int: + """List compiled language packs.""" + from language_packs import list_packs + + packs = list_packs() + if not packs: + print("no compiled packs found") + return 0 + for pack_id in packs: + print(pack_id) + return 0 + + +def cmd_pack_verify(args: argparse.Namespace) -> int: + """Verify one language pack checksum.""" + return _run(sys.executable, "-m", "language_packs", "verify", args.pack_id) + + +def cmd_doctor(args: argparse.Namespace) -> int: + """Inspect import/package health for the CLI runtime path.""" + checks = [ + ("algebra", "algebra"), + ("alignment", "alignment.graph"), + ("chat", "chat.runtime"), + ("language_packs", "language_packs"), + ("morphology", "morphology.registry"), + ("sensorium", "sensorium.protocol"), + ] + ok = True + print(f"repo_root: {_REPO_ROOT}") + for label, module_name in checks: + try: + __import__(module_name) + except Exception as exc: + ok = False + print(f"FAIL {label:<14} {module_name}: {exc.__class__.__name__}: {exc}") + else: + print(f"OK {label:<14} {module_name}") + + if args.packs: + try: + from language_packs import list_packs + + packs = list_packs() + except Exception as exc: + ok = False + print(f"FAIL packs language_packs.list_packs: {exc.__class__.__name__}: {exc}") + else: + print("packs:") + if packs: + for pack_id in packs: + print(f" {pack_id}") + else: + print(" none found") + return 0 if ok else 1 + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="core", + description=DESCRIPTION, + epilog=EPILOG, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("--version", action="store_true", help="print package version and exit") + subparsers = parser.add_subparsers(dest="command", metavar="command") + + chat = subparsers.add_parser("chat", help="start the interactive chat REPL") + chat.add_argument("args", nargs=argparse.REMAINDER, help="arguments forwarded to python -m chat") + chat.set_defaults(func=cmd_chat) + + test = subparsers.add_parser("test", help="run pytest with sane defaults") + test.add_argument("args", nargs=argparse.REMAINDER, help="arguments forwarded to pytest") + test.set_defaults(func=cmd_test) + + check = subparsers.add_parser("check", help="run ruff check") + check.add_argument("paths", nargs="*", help="optional paths to check") + check.set_defaults(func=cmd_check) + + trace = subparsers.add_parser( + "trace", + help="trace one chat turn with field telemetry", + description="trace one chat turn with field telemetry", + ) + trace.add_argument("--pack", action="append", help="language pack to mount; repeat for multiple packs") + trace.add_argument("--max-tokens", type=int, default=32, help="maximum generated tokens; default: 32") + trace.add_argument("--json", action="store_true", help="emit machine-readable JSON") + trace.add_argument("text", nargs=argparse.REMAINDER, help="input text to trace") + trace.set_defaults(func=cmd_trace) + + oov = subparsers.add_parser("oov", help="ground or inspect one token") + oov.add_argument("--pack", action="append", help="language pack to mount; repeat for multiple packs") + oov.add_argument("token", help="token to inspect or ground") + oov.set_defaults(func=cmd_oov) + + pack = subparsers.add_parser("pack", help="inspect and verify language packs") + pack_sub = pack.add_subparsers(dest="pack_command", metavar="pack-command", required=True) + pack_list = pack_sub.add_parser("list", help="list compiled packs") + pack_list.set_defaults(func=cmd_pack_list) + pack_verify = pack_sub.add_parser("verify", help="verify a pack checksum") + pack_verify.add_argument("pack_id", help="pack id, e.g. en_minimal_v1") + pack_verify.set_defaults(func=cmd_pack_verify) + + doctor = subparsers.add_parser("doctor", help="check runtime imports and packaging health") + doctor.add_argument("--packs", action="store_true", help="also list discovered language packs") + doctor.set_defaults(func=cmd_doctor) + + return parser + + +def _print_version() -> None: + try: + from importlib.metadata import version + + print(version("core-versor")) + except Exception: + print("core-versor unknown") + + +def main(argv: Sequence[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(list(argv) if argv is not None else None) + if args.version: + _print_version() + return 0 + func = getattr(args, "func", None) + if func is None: + parser.print_help() + return 0 + return int(func(args)) if __name__ == "__main__": - main() + raise SystemExit(main()) diff --git a/language_packs/__init__.py b/language_packs/__init__.py index 3538ede8..5472da7e 100644 --- a/language_packs/__init__.py +++ b/language_packs/__init__.py @@ -6,6 +6,8 @@ linguistic manifolds: surface forms, morphology, grammar attractors, cross-language resonances, and holonomy resonance proofs. """ +from pathlib import Path + from .schema import ( AlignmentEdge, GrammarAttractor, @@ -16,6 +18,23 @@ from .schema import ( MorphologyEntry, OOVPolicy, ) + +_DATA_DIR = Path(__file__).parent / "data" + + +def list_packs() -> list[str]: + """Return available compiled language-pack ids.""" + if not _DATA_DIR.exists(): + return [] + return sorted( + path.name + for path in _DATA_DIR.iterdir() + if path.is_dir() + and (path / "manifest.json").exists() + and (path / "lexicon.jsonl").exists() + ) + + __all__ = [ "AlignmentEdge", "GrammarAttractor", @@ -25,6 +44,7 @@ __all__ = [ "LexicalEntry", "MorphologyEntry", "OOVPolicy", + "list_packs", ] diff --git a/pyproject.toml b/pyproject.toml index f8731342..af561b4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,13 +26,16 @@ core = "core.cli:main" where = ["."] include = [ "algebra*", + "alignment*", "chat*", "core*", "field*", "generate*", "ingest*", "language_packs*", + "morphology*", "persona*", + "sensorium*", "session*", "vault*", "vocab*", diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..1606125b --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import pytest + +from core.cli import build_parser, main + + +def test_top_level_help_exits_without_runtime_import(capsys: pytest.CaptureFixture[str]) -> None: + with pytest.raises(SystemExit) as excinfo: + build_parser().parse_args(["-h"]) + assert excinfo.value.code == 0 + out = capsys.readouterr().out + assert "CORE versor engine command suite" in out + assert "core trace" in out + + +def test_trace_help_exits_without_runtime_import(capsys: pytest.CaptureFixture[str]) -> None: + with pytest.raises(SystemExit) as excinfo: + build_parser().parse_args(["trace", "-h"]) + assert excinfo.value.code == 0 + out = capsys.readouterr().out + assert "trace one chat turn" in out + assert "--pack" in out + assert "--json" in out + + +def test_main_without_args_prints_help(capsys: pytest.CaptureFixture[str]) -> None: + assert main([]) == 0 + out = capsys.readouterr().out + assert "CORE versor engine command suite" in out + assert "doctor" in out + + +def test_trace_requires_text_before_runtime_initialization(capsys: pytest.CaptureFixture[str]) -> None: + with pytest.raises(SystemExit) as excinfo: + main(["trace"]) + assert excinfo.value.code == 2 + err = capsys.readouterr().err + assert "trace requires input text" in err + + +def test_doctor_imports_runtime_support_modules(capsys: pytest.CaptureFixture[str]) -> None: + assert main(["doctor"]) == 0 + out = capsys.readouterr().out + assert "OK alignment" in out + assert "OK morphology" in out + assert "OK sensorium" in out + + +def test_trace_formats_real_runtime_payload(capsys: pytest.CaptureFixture[str]) -> None: + assert main(["trace", "--pack", "en_minimal_v1", "word", "beginning", "truth"]) == 0 + out = capsys.readouterr().out + assert "input : word beginning truth" in out + assert "proposition" in out + assert "subject" in out + assert "predicate" in out + + +def test_trace_json_formats_real_runtime_payload(capsys: pytest.CaptureFixture[str]) -> None: + assert main(["trace", "--pack", "en_minimal_v1", "--json", "word", "beginning", "truth"]) == 0 + out = capsys.readouterr().out + assert '"input": "word beginning truth"' in out + assert '"proposition"' in out + assert '"subject"' in out + assert '"predicate"' in out