Improve plan review UX with enriched rows, TUI filters, and execute gate.
Make review-plan easier to act on: Chinese risk labels, relative paths, verdict/next-step footer, optional identity/analysis enrichment, grouping, spot-check sampling, and structure preview. Extend the TUI with filters, duplicate-group reject, and quality context. Persist review_context on plan operations and add --require-review for confirmed execute. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+136
-5
@@ -17,7 +17,14 @@ from click.core import ParameterSource
|
||||
from vlm.config import Config, load_config, create_default_config, validate_config
|
||||
from vlm.context import CLIContext, pass_context
|
||||
from vlm.logging_config import setup_logging, get_logger
|
||||
from vlm.plan_render import fallback_plan_summary, preferred_plan_summary, render_review_preview
|
||||
from vlm.plan_render import (
|
||||
fallback_plan_summary,
|
||||
preferred_plan_summary,
|
||||
render_review_footer,
|
||||
render_review_preview,
|
||||
render_review_verdict,
|
||||
duplicate_groups_from_plan,
|
||||
)
|
||||
from vlm.utils import format_size
|
||||
|
||||
|
||||
@@ -538,6 +545,38 @@ def plan(ctx: CLIContext, input: Path, output: Path, analysis: Path | None):
|
||||
default=False,
|
||||
help='Interactive Textual UI (requires: uv pip install -e ".[tui]")'
|
||||
)
|
||||
@click.option(
|
||||
'--identities',
|
||||
type=click.Path(path_type=Path),
|
||||
default=None,
|
||||
help='Identities JSON for enriched review rows (default: artifacts/identities.json if present)'
|
||||
)
|
||||
@click.option(
|
||||
'--analysis',
|
||||
type=click.Path(path_type=Path),
|
||||
default=None,
|
||||
help='Analysis JSON for duplicate grouping and TUI quality pane'
|
||||
)
|
||||
@click.option(
|
||||
'--group-by',
|
||||
type=click.Choice(['none', 'reason', 'title', 'duplicate'], case_sensitive=False),
|
||||
default='none',
|
||||
show_default=True,
|
||||
help='Reorder review rows for display/export'
|
||||
)
|
||||
@click.option(
|
||||
'--sample-safe',
|
||||
type=int,
|
||||
default=0,
|
||||
show_default=True,
|
||||
help='Include N random non-high-risk move operations for spot-checking'
|
||||
)
|
||||
@click.option(
|
||||
'--structure-preview',
|
||||
type=click.Path(path_type=Path),
|
||||
default=None,
|
||||
help='Write target library tree preview to this file (e.g. artifacts/plan_structure.txt)'
|
||||
)
|
||||
@pass_context
|
||||
def review_plan_cmd(
|
||||
ctx: CLIContext,
|
||||
@@ -548,10 +587,17 @@ def review_plan_cmd(
|
||||
preview_limit: int,
|
||||
show_all: bool,
|
||||
tui: bool,
|
||||
identities: Optional[Path],
|
||||
analysis: Optional[Path],
|
||||
group_by: str,
|
||||
sample_safe: int,
|
||||
structure_preview: Optional[Path],
|
||||
):
|
||||
"""Review a plan and export high-risk operations for manual confirmation."""
|
||||
from vlm.io import load_analysis_json, load_identities_json
|
||||
from vlm.planner import load_plan
|
||||
from vlm.plan_review import review_plan, save_review_csv
|
||||
from vlm.plan_review import GROUP_BY_CHOICES, prepare_review_rows, save_review_csv
|
||||
from vlm.plan_structure_preview import write_structure_preview
|
||||
|
||||
logger = ctx.logger
|
||||
|
||||
@@ -563,6 +609,13 @@ def review_plan_cmd(
|
||||
if preview_limit < 1:
|
||||
click.echo("Error: --preview-limit must be >= 1", err=True)
|
||||
sys.exit(1)
|
||||
if sample_safe < 0:
|
||||
click.echo("Error: --sample-safe must be >= 0", err=True)
|
||||
sys.exit(1)
|
||||
group_by = group_by.lower()
|
||||
if group_by not in GROUP_BY_CHOICES:
|
||||
click.echo(f"Error: invalid --group-by {group_by}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
if tui:
|
||||
if not _review_plan_tui_streams_ok():
|
||||
@@ -571,15 +624,46 @@ def review_plan_cmd(
|
||||
|
||||
click.echo(f"Loading plan: {input}")
|
||||
execution_plan = load_plan(input)
|
||||
rows, counters = review_plan(
|
||||
|
||||
identities_data = None
|
||||
identities_path = identities
|
||||
if identities_path is None:
|
||||
default_id = default_artifact_path("identities.json")
|
||||
if default_id.is_file():
|
||||
identities_path = default_id
|
||||
if identities_path is not None and identities_path.is_file():
|
||||
identities_data = load_identities_json(identities_path)
|
||||
click.echo(f"Loaded identities: {identities_path}")
|
||||
|
||||
analysis_data = None
|
||||
if analysis is not None and analysis.is_file():
|
||||
analysis_data = load_analysis_json(analysis)
|
||||
click.echo(f"Loaded analysis: {analysis}")
|
||||
|
||||
rows, counters = prepare_review_rows(
|
||||
execution_plan,
|
||||
season_threshold=season_threshold,
|
||||
episode_threshold=episode_threshold,
|
||||
library_root=ctx.config.library_root,
|
||||
identities_data=identities_data,
|
||||
analysis_data=analysis_data,
|
||||
sample_safe=sample_safe,
|
||||
group_by=group_by,
|
||||
)
|
||||
|
||||
if structure_preview is not None:
|
||||
write_structure_preview(
|
||||
execution_plan,
|
||||
ctx.config.library_root,
|
||||
structure_preview,
|
||||
)
|
||||
click.echo(f"Wrote structure preview: {structure_preview}")
|
||||
|
||||
if tui:
|
||||
from vlm.plan_review import build_duplicate_path_maps
|
||||
from vlm.review_tui import ReviewTUIContext, run_plan_review_tui
|
||||
|
||||
_, path_to_quality = build_duplicate_path_maps(analysis_data)
|
||||
tui_ctx = ReviewTUIContext(
|
||||
rows=rows,
|
||||
counters=counters,
|
||||
@@ -587,6 +671,7 @@ def review_plan_cmd(
|
||||
output_csv=output,
|
||||
plan_input=input,
|
||||
summary_text=preferred_plan_summary(execution_plan),
|
||||
path_to_quality=path_to_quality,
|
||||
)
|
||||
rc = run_plan_review_tui(tui_ctx)
|
||||
if rc != 0:
|
||||
@@ -617,6 +702,9 @@ def review_plan_cmd(
|
||||
click.echo(f" high_episode: {counters['high_episode']}")
|
||||
click.echo(f" conflicts: {counters['conflicts']}")
|
||||
|
||||
click.echo()
|
||||
click.echo(render_review_verdict(counters))
|
||||
|
||||
click.echo()
|
||||
click.echo("High-risk operations preview:")
|
||||
if rows:
|
||||
@@ -624,6 +712,7 @@ def review_plan_cmd(
|
||||
rows,
|
||||
preview_limit=preview_limit,
|
||||
show_all=show_all,
|
||||
library_root=ctx.config.library_root,
|
||||
)
|
||||
for line in preview_lines:
|
||||
click.echo(line)
|
||||
@@ -638,6 +727,16 @@ def review_plan_cmd(
|
||||
click.echo()
|
||||
click.echo(f"Saved manual review CSV to: {output}")
|
||||
|
||||
dup_groups = duplicate_groups_from_plan(execution_plan)
|
||||
click.echo()
|
||||
for line in render_review_footer(
|
||||
counters=counters,
|
||||
output_csv=output,
|
||||
plan_input=input,
|
||||
duplicate_groups=dup_groups,
|
||||
):
|
||||
click.echo(line)
|
||||
|
||||
logger.info(
|
||||
"Plan review completed: total=%s high_risk=%s output=%s",
|
||||
counters["total_operations"],
|
||||
@@ -764,8 +863,30 @@ def apply_review_cmd(
|
||||
default=False,
|
||||
help='Enable safe mode: prevent any operations that would destroy directories'
|
||||
)
|
||||
@click.option(
|
||||
'--require-review',
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help='With --confirm, require a current plan_manual_review.csv when high-risk ops exist'
|
||||
)
|
||||
@click.option(
|
||||
'--review-csv',
|
||||
type=click.Path(path_type=Path),
|
||||
default=None,
|
||||
help='Review CSV path for --require-review (default: beside plan file)'
|
||||
)
|
||||
@pass_context
|
||||
def execute(ctx: CLIContext, plan: Path, confirm: bool, yes: bool, verbose_ops: bool, preserve_directories: bool, safe_mode: bool):
|
||||
def execute(
|
||||
ctx: CLIContext,
|
||||
plan: Path,
|
||||
confirm: bool,
|
||||
yes: bool,
|
||||
verbose_ops: bool,
|
||||
preserve_directories: bool,
|
||||
safe_mode: bool,
|
||||
require_review: bool,
|
||||
review_csv: Optional[Path],
|
||||
):
|
||||
"""Execute plan (defaults to dry-run, requires --confirm).
|
||||
|
||||
Executes file operations from a plan. Defaults to dry-run mode which
|
||||
@@ -782,7 +903,17 @@ def execute(ctx: CLIContext, plan: Path, confirm: bool, yes: bool, verbose_ops:
|
||||
try:
|
||||
plan = resolve_legacy_default_input_path(plan, "plan", "plan.json", "--plan")
|
||||
from vlm.commands.execute import execute_cmd
|
||||
execute_cmd(ctx, plan, confirm, yes, verbose_ops, preserve_directories, safe_mode)
|
||||
execute_cmd(
|
||||
ctx,
|
||||
plan,
|
||||
confirm,
|
||||
yes,
|
||||
verbose_ops,
|
||||
preserve_directories,
|
||||
safe_mode,
|
||||
require_review=require_review,
|
||||
review_csv=review_csv,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
_command_error(
|
||||
ctx,
|
||||
|
||||
Reference in New Issue
Block a user