From 146cb395a8ca2ea9a3b4fe1a8b2b83a149c2665b Mon Sep 17 00:00:00 2001 From: Drew Schuyler <262217085+Amidwestnoob@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:30:53 -0500 Subject: [PATCH 1/4] feat(domain): add category descriptions --- src/modeldock/cli/commands/install_category.py | 9 ++++++++- src/modeldock/domain/model.py | 12 ++++++++++++ tests/unit/test_cli_wiring.py | 12 +++++++++++- tests/unit/test_domain.py | 14 ++++++++++++++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/modeldock/cli/commands/install_category.py b/src/modeldock/cli/commands/install_category.py index 5b6bf18..1690830 100644 --- a/src/modeldock/cli/commands/install_category.py +++ b/src/modeldock/cli/commands/install_category.py @@ -6,10 +6,17 @@ from modeldock.cli.console import print_error from modeldock.cli.factory import manager_for +from modeldock.domain.model import Category + + +def _category_help() -> str: + """Build help text from the domain category descriptions.""" + descriptions = "; ".join(f"{item.value} ({item.description})" for item in Category) + return f"Category name. Available: {descriptions}" def install_category_cmd( - category: str = typer.Argument(..., help="Category name (e.g. coding)"), + category: str = typer.Argument(..., help=_category_help()), backend: str = typer.Option(None, "--backend", help="Runtime backend"), debug: bool = typer.Option(False, "--debug", help="Show traceback"), ) -> None: diff --git a/src/modeldock/domain/model.py b/src/modeldock/domain/model.py index cec63b5..533a2a1 100644 --- a/src/modeldock/domain/model.py +++ b/src/modeldock/domain/model.py @@ -45,6 +45,18 @@ class Category(str, Enum): REASONING = "reasoning" INSTRUCT = "instruct" + @property + def description(self) -> str: + """Return a human-readable description for CLI help and documentation.""" + return { + Category.CHAT: "General-purpose conversational models", + Category.CODING: "Models optimized for code generation and completion", + Category.EMBEDDING: "Models that convert text into vector representations", + Category.VISION: "Models that understand images and text", + Category.REASONING: "Models optimized for multi-step reasoning", + Category.INSTRUCT: "Models tuned to follow instructions", + }[self] + @classmethod def from_value(cls, value: str) -> Category: """Resolve a category from a string, case-insensitively.""" diff --git a/tests/unit/test_cli_wiring.py b/tests/unit/test_cli_wiring.py index 18d14e5..5b22d22 100644 --- a/tests/unit/test_cli_wiring.py +++ b/tests/unit/test_cli_wiring.py @@ -12,7 +12,7 @@ import modeldock.cli.factory as factory from modeldock.cli.app import app from modeldock.common.errors import ConfigError -from modeldock.domain.model import ModelRef, RuntimeBackend +from modeldock.domain.model import Category, ModelRef, RuntimeBackend runner = CliRunner() @@ -85,6 +85,16 @@ def test_unknown_backend_exits_nonzero(recording_manager: type[_RecordingManager assert "Unknown backend" in result.output +def test_install_category_help_includes_descriptions() -> None: + result = runner.invoke(app, ["install-category", "--help"]) + output = " ".join(result.output.replace("│", " ").split()) + + assert result.exit_code == 0 + for category in Category: + assert category.value in output + assert category.description in output + + def test_global_backend_reaches_subcommands(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("MODELDOCK_DEFAULT_BACKEND", raising=False) result = runner.invoke(app, ["--backend", "lmstudio", "config", "show"]) diff --git a/tests/unit/test_domain.py b/tests/unit/test_domain.py index 18be1e8..3bc2584 100644 --- a/tests/unit/test_domain.py +++ b/tests/unit/test_domain.py @@ -32,6 +32,20 @@ def test_category_from_value() -> None: Category.from_value("nonsense") +def test_category_descriptions() -> None: + expected = { + Category.CHAT: "General-purpose conversational models", + Category.CODING: "Models optimized for code generation and completion", + Category.EMBEDDING: "Models that convert text into vector representations", + Category.VISION: "Models that understand images and text", + Category.REASONING: "Models optimized for multi-step reasoning", + Category.INSTRUCT: "Models tuned to follow instructions", + } + + assert {category: category.description for category in Category} == expected + assert len(Category) == 6 + + def test_backend_from_value() -> None: assert RuntimeBackend.from_value("OLLAMA") == RuntimeBackend.OLLAMA assert RuntimeBackend.from_value("vllm") == RuntimeBackend.VLLM From 56fe73b9ba3782e6e19e9296064b45bcad6302f7 Mon Sep 17 00:00:00 2001 From: Drew Schuyler <262217085+Amidwestnoob@users.noreply.github.com> Date: Wed, 2 Sep 2026 07:11:55 -0500 Subject: [PATCH 2/4] fix(domain): store category descriptions on members --- src/modeldock/domain/model.py | 29 +++++++++++++++-------------- tests/unit/test_cli_wiring.py | 4 +++- tests/unit/test_domain.py | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/modeldock/domain/model.py b/src/modeldock/domain/model.py index 533a2a1..c526c27 100644 --- a/src/modeldock/domain/model.py +++ b/src/modeldock/domain/model.py @@ -38,24 +38,25 @@ def from_value(cls, value: str) -> Capability: class Category(str, Enum): """High-level model categories used for discovery and bulk install.""" - CHAT = "chat" - CODING = "coding" - EMBEDDING = "embedding" - VISION = "vision" - REASONING = "reasoning" - INSTRUCT = "instruct" + _description: str + + def __new__(cls, value: str, description: str) -> Category: + obj = str.__new__(cls, value) + obj._value_ = value + obj._description = description + return obj + + CHAT = ("chat", "General-purpose conversational models") + CODING = ("coding", "Models optimized for code generation and completion") + EMBEDDING = ("embedding", "Models that convert text into vector representations") + VISION = ("vision", "Models that understand images and text") + REASONING = ("reasoning", "Models optimized for multi-step reasoning") + INSTRUCT = ("instruct", "Models tuned to follow instructions") @property def description(self) -> str: """Return a human-readable description for CLI help and documentation.""" - return { - Category.CHAT: "General-purpose conversational models", - Category.CODING: "Models optimized for code generation and completion", - Category.EMBEDDING: "Models that convert text into vector representations", - Category.VISION: "Models that understand images and text", - Category.REASONING: "Models optimized for multi-step reasoning", - Category.INSTRUCT: "Models tuned to follow instructions", - }[self] + return self._description @classmethod def from_value(cls, value: str) -> Category: diff --git a/tests/unit/test_cli_wiring.py b/tests/unit/test_cli_wiring.py index 5b22d22..c8d42ab 100644 --- a/tests/unit/test_cli_wiring.py +++ b/tests/unit/test_cli_wiring.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from pathlib import Path from typing import Any, Optional @@ -87,7 +88,8 @@ def test_unknown_backend_exits_nonzero(recording_manager: type[_RecordingManager def test_install_category_help_includes_descriptions() -> None: result = runner.invoke(app, ["install-category", "--help"]) - output = " ".join(result.output.replace("│", " ").split()) + stripped = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", result.output) + output = " ".join(stripped.replace("│", " ").split()) assert result.exit_code == 0 for category in Category: diff --git a/tests/unit/test_domain.py b/tests/unit/test_domain.py index 3bc2584..3b2505c 100644 --- a/tests/unit/test_domain.py +++ b/tests/unit/test_domain.py @@ -33,6 +33,7 @@ def test_category_from_value() -> None: def test_category_descriptions() -> None: + # Pin exact wording so accidental rewording is caught in CI. expected = { Category.CHAT: "General-purpose conversational models", Category.CODING: "Models optimized for code generation and completion", @@ -43,7 +44,6 @@ def test_category_descriptions() -> None: } assert {category: category.description for category in Category} == expected - assert len(Category) == 6 def test_backend_from_value() -> None: From abd579b863feff1210bc6177357a76d4f497c596 Mon Sep 17 00:00:00 2001 From: Drew Schuyler <262217085+Amidwestnoob@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:52:58 -0500 Subject: [PATCH 3/4] fix(tests): make category iteration explicit for CodeQL --- tests/unit/test_cli_wiring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_cli_wiring.py b/tests/unit/test_cli_wiring.py index c8d42ab..b577a75 100644 --- a/tests/unit/test_cli_wiring.py +++ b/tests/unit/test_cli_wiring.py @@ -92,7 +92,7 @@ def test_install_category_help_includes_descriptions() -> None: output = " ".join(stripped.replace("│", " ").split()) assert result.exit_code == 0 - for category in Category: + for category in list(Category): assert category.value in output assert category.description in output From 1a0ec9c01fb14e3b96fc386a2bcbb6763e94191a Mon Sep 17 00:00:00 2001 From: Amidwestnoob Date: Thu, 17 Sep 2026 13:11:55 +0000 Subject: [PATCH 4/4] refactor(cli): hoist category help to a module constant install-category help is built once from Category descriptions, so keep it as _CATEGORY_HELP instead of a one-shot helper function. --- src/modeldock/cli/commands/install_category.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/modeldock/cli/commands/install_category.py b/src/modeldock/cli/commands/install_category.py index 1690830..fa045a5 100644 --- a/src/modeldock/cli/commands/install_category.py +++ b/src/modeldock/cli/commands/install_category.py @@ -8,15 +8,13 @@ from modeldock.cli.factory import manager_for from modeldock.domain.model import Category - -def _category_help() -> str: - """Build help text from the domain category descriptions.""" - descriptions = "; ".join(f"{item.value} ({item.description})" for item in Category) - return f"Category name. Available: {descriptions}" +_CATEGORY_HELP: str = "Category name. Available: " + "; ".join( + f"{item.value} ({item.description})" for item in Category +) def install_category_cmd( - category: str = typer.Argument(..., help=_category_help()), + category: str = typer.Argument(..., help=_CATEGORY_HELP), backend: str = typer.Option(None, "--backend", help="Runtime backend"), debug: bool = typer.Option(False, "--debug", help="Show traceback"), ) -> None: