From 4eddbcf48951df97e75ef8e6673b84bef9e4fc5e Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Sun, 29 Mar 2026 03:35:30 +0000 Subject: [PATCH] fix(cli): add --depth-gradient flag to project context set Add the missing --depth-gradient repeatable flag to `project context set` per specification. The flag accepts HOP:INT_OR_NAME format to control context detail depth degradation with graph distance. Parsing validates hop as integer and value as integer or recognized detail level name. ISSUES CLOSED: #889 --- .../project_context_depth_gradient.feature | 49 +++++++ .../project_context_depth_gradient_steps.py | 132 ++++++++++++++++++ .../cli/commands/project_context.py | 49 +++++++ 3 files changed, 230 insertions(+) create mode 100644 features/project_context_depth_gradient.feature create mode 100644 features/steps/project_context_depth_gradient_steps.py diff --git a/features/project_context_depth_gradient.feature b/features/project_context_depth_gradient.feature new file mode 100644 index 00000000..b81f551c --- /dev/null +++ b/features/project_context_depth_gradient.feature @@ -0,0 +1,49 @@ +Feature: Project context depth-gradient flag (B2.cli) + As a CleverAgents user + I want to set per-hop depth gradients via ``project context set --depth-gradient`` + So that context detail depth degrades with graph distance from focus nodes + + Background: + Given a project context CLI in-memory database is initialized + And a project "local/ctx-app" exists for context CLI + + # ── single depth-gradient value ───────────────────────────── + + Scenario: Set a single depth-gradient with integer value + When I run context set on "local/ctx-app" with depth-gradient "0:9" + Then the context set command should succeed + And the stored ACMS depth gradient should have hop "0" with value 9 + + Scenario: Set a single depth-gradient with named level value + When I run context set on "local/ctx-app" with depth-gradient "1:SIGNATURES" + Then the context set command should succeed + And the stored ACMS depth gradient should have hop "1" with string value "SIGNATURES" + + # ── multiple depth-gradient values ────────────────────────── + + Scenario: Set multiple depth-gradient values + When I run context set on "local/ctx-app" with depth-gradients "0:9,1:4,2:MODULE_LISTING" + Then the context set command should succeed + And the stored ACMS depth gradient should have hop "0" with value 9 + And the stored ACMS depth gradient should have hop "1" with value 4 + And the stored ACMS depth gradient should have hop "2" with string value "MODULE_LISTING" + + # ── invalid format ────────────────────────────────────────── + + Scenario: Invalid depth-gradient missing colon fails + When I run context set on "local/ctx-app" with depth-gradient "bad" + Then the context set command should fail + + Scenario: Invalid depth-gradient non-integer hop fails + When I run context set on "local/ctx-app" with depth-gradient "abc:5" + Then the context set command should fail + + # ── display in show command ───────────────────────────────── + + Scenario: Depth gradient is displayed in context show output + Given I have set depth-gradient "0:9,1:SIGNATURES" on "local/ctx-app" + When I run context show on "local/ctx-app" without options + Then the context show command should succeed + And the context show output should contain "Depth gradient" + And the context show output should contain "0:9" + And the context show output should contain "1:SIGNATURES" diff --git a/features/steps/project_context_depth_gradient_steps.py b/features/steps/project_context_depth_gradient_steps.py new file mode 100644 index 00000000..5bae1086 --- /dev/null +++ b/features/steps/project_context_depth_gradient_steps.py @@ -0,0 +1,132 @@ +"""Step definitions for ``project_context_depth_gradient.feature``. + +Tests the ``--depth-gradient`` flag on ``agents project context set`` +and its display in ``agents project context show``. +""" + +from __future__ import annotations + +from typing import Any + +from behave import given, then, when # type: ignore[import-untyped] + +# ------------------------------------------------------------------ +# Helpers — reuse the shared infrastructure from +# project_context_cli_steps (background steps, _run_with_container, +# _mock_container, etc. are already registered). +# ------------------------------------------------------------------ + + +def _run_with_container_local( + context: Any, func: Any, *args: Any, **kwargs: Any +) -> None: + """Delegate to the shared test runner from project_context_cli_steps.""" + from features.steps.project_context_cli_steps import ( + _run_with_container, + ) + + _run_with_container(context, func, *args, **kwargs) + + +# ------------------------------------------------------------------ +# When steps +# ------------------------------------------------------------------ + + +@when( + 'I run context set on "{project}" with depth-gradient "{gradient}"', +) +def step_ctx_set_depth_gradient_single( + context: Any, project: str, gradient: str +) -> None: + from cleveragents.cli.commands.project_context import ( + context_set, + ) + + _run_with_container_local( + context, + context_set, + project=project, + view="default", + depth_gradient=[gradient], + ) + + +@when( + 'I run context set on "{project}" with depth-gradients "{gradients}"', +) +def step_ctx_set_depth_gradient_multi( + context: Any, project: str, gradients: str +) -> None: + from cleveragents.cli.commands.project_context import ( + context_set, + ) + + gradient_list = [g.strip() for g in gradients.split(",")] + _run_with_container_local( + context, + context_set, + project=project, + view="default", + depth_gradient=gradient_list, + ) + + +# ------------------------------------------------------------------ +# Given steps +# ------------------------------------------------------------------ + + +@given('I have set depth-gradient "{gradients}" on "{project}"') +def step_given_depth_gradient(context: Any, gradients: str, project: str) -> None: + from cleveragents.cli.commands.project_context import ( + context_set, + ) + + gradient_list = [g.strip() for g in gradients.split(",")] + _run_with_container_local( + context, + context_set, + project=project, + view="default", + depth_gradient=gradient_list, + ) + + +# ------------------------------------------------------------------ +# Then steps +# ------------------------------------------------------------------ + + +def _read_stored_acms(context: Any) -> dict[str, Any]: + """Read the stored ACMS config from the test DB.""" + from cleveragents.cli.commands.project_context import ( + _read_acms_config, + ) + + return _read_acms_config(context.ctx_session_factory, "local/ctx-app") + + +@then('the stored ACMS depth gradient should have hop "{hop}" with value {value:d}') +def step_depth_gradient_int(context: Any, hop: str, value: int) -> None: + acms = _read_stored_acms(context) + dg = acms.get("depth_gradient", {}) + assert hop in dg, f"Hop '{hop}' not in depth_gradient: {dg}" + assert dg[hop] == value, f"Expected {value} for hop {hop}, got {dg[hop]}" + + +@then( + 'the stored ACMS depth gradient should have hop "{hop}" with string value "{value}"' +) +def step_depth_gradient_str(context: Any, hop: str, value: str) -> None: + acms = _read_stored_acms(context) + dg = acms.get("depth_gradient", {}) + assert hop in dg, f"Hop '{hop}' not in depth_gradient: {dg}" + assert dg[hop] == value, f"Expected '{value}' for hop {hop}, got {dg[hop]}" + + +@then('the context show output should contain "{text}"') +def step_show_output_contains(context: Any, text: str) -> None: + assert text in context.ctx_output, ( + f"Expected '{text}' in output, got: {context.ctx_output!r}" + ) diff --git a/src/cleveragents/cli/commands/project_context.py b/src/cleveragents/cli/commands/project_context.py index a675a331..5df06680 100644 --- a/src/cleveragents/cli/commands/project_context.py +++ b/src/cleveragents/cli/commands/project_context.py @@ -543,6 +543,16 @@ def context_set( help="Temporal scope: current, recent, or all", ), ] = None, + depth_gradient: Annotated[ + list[str] | None, + typer.Option( + "--depth-gradient", + help=( + "Per-hop detail depth override (repeatable). " + "Format: HOP:INT_OR_NAME, e.g. 0:9, 1:SIGNATURES" + ), + ), + ] = None, auto_refresh: Annotated[ bool | None, typer.Option( @@ -642,6 +652,37 @@ def context_set( acms["temporal_scope"] = temporal_scope if auto_refresh is not None: acms["auto_refresh"] = auto_refresh + if depth_gradient is not None: + parsed_gradient: dict[str, int | str] = {} + for entry in depth_gradient: + if ":" not in entry: + err_console.print( + f"[red]Invalid depth-gradient '{entry}': " + f"expected HOP:INT_OR_NAME format[/red]" + ) + raise typer.Exit(1) + hop_str, _, value_str = entry.partition(":") + try: + hop = int(hop_str) + except ValueError: + err_console.print( + f"[red]Invalid depth-gradient hop '{hop_str}': " + f"hop must be a non-negative integer[/red]" + ) + raise typer.Exit(1) from None + if hop < 0: + err_console.print( + f"[red]Invalid depth-gradient hop '{hop}': " + f"hop must be a non-negative integer[/red]" + ) + raise typer.Exit(1) + # Value: try int first, else keep as named level string + try: + parsed_value: int | str = int(value_str) + except ValueError: + parsed_value = value_str + parsed_gradient[str(hop)] = parsed_value + acms["depth_gradient"] = parsed_gradient _write_policy(session_factory, project, policy, acms) @@ -852,6 +893,14 @@ def context_show( f" Default breadth: {acms.get('default_breadth', _DEFAULT_BREADTH)}" ) lines.append(f" Default depth: {acms.get('default_depth', _DEFAULT_DEPTH)}") + dg = acms.get("depth_gradient", {}) + if dg: + gradient_parts = [ + f"{h}:{v}" for h, v in sorted(dg.items(), key=lambda x: int(x[0])) + ] + lines.append(f" Depth gradient: {', '.join(gradient_parts)}") + else: + lines.append(" Depth gradient: (none)") lines.append( f" Skeleton ratio: {acms.get('skeleton_ratio', _DEFAULT_SKELETON_RATIO)}" )