Extract review-plan, report, quarantine, state, and config handlers into commands/ with shared cli_helpers; remove unused exceptions and duplicate plan summary wrappers. Archive superseded review markdown, sync docs to 517-test baseline, and fix empty series titles when only a quality tag remains. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
240 lines
8.1 KiB
Python
240 lines
8.1 KiB
Python
"""Review-plan and apply-review command implementations."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import click
|
|
|
|
from vlm.cli_helpers import command_error, default_artifact_path, resolve_legacy_default_input_path, review_plan_tui_streams_ok
|
|
from vlm.context import CLIContext
|
|
from vlm.io import load_analysis_json, load_identities_json
|
|
from vlm.plan_render import (
|
|
duplicate_groups_from_plan,
|
|
preferred_plan_summary,
|
|
render_review_footer,
|
|
render_review_preview,
|
|
render_review_verdict,
|
|
)
|
|
from vlm.plan_review import GROUP_BY_CHOICES, prepare_review_rows, save_review_csv
|
|
from vlm.plan_structure_preview import write_structure_preview
|
|
from vlm.planner import apply_review_to_plan, load_plan, save_plan
|
|
|
|
|
|
def review_plan_cmd(
|
|
ctx: CLIContext,
|
|
input: Path,
|
|
output: Path,
|
|
season_threshold: int,
|
|
episode_threshold: int,
|
|
preview_limit: int,
|
|
show_all: bool,
|
|
tui: bool,
|
|
identities: Optional[Path],
|
|
analysis: Optional[Path],
|
|
group_by: str,
|
|
sample_safe: int,
|
|
structure_preview: Optional[Path],
|
|
) -> None:
|
|
"""Review a plan and export high-risk operations for manual confirmation."""
|
|
logger = ctx.logger
|
|
|
|
try:
|
|
input = resolve_legacy_default_input_path(input, "input", "plan.json", "--input")
|
|
if season_threshold < 1 or episode_threshold < 1:
|
|
click.echo("Error: thresholds must be >= 1", err=True)
|
|
sys.exit(1)
|
|
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():
|
|
click.echo("Error: --tui requires an interactive terminal (TTY)", err=True)
|
|
sys.exit(1)
|
|
|
|
click.echo(f"Loading plan: {input}")
|
|
execution_plan = load_plan(input)
|
|
|
|
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,
|
|
library_root=ctx.config.library_root,
|
|
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:
|
|
click.echo("Plan review aborted (no CSV written).", err=True)
|
|
sys.exit(rc)
|
|
click.echo(f"Saved manual review CSV to: {output}")
|
|
logger.info(
|
|
"Plan review TUI completed: total=%s high_risk=%s output=%s",
|
|
counters["total_operations"],
|
|
counters["high_risk_operations"],
|
|
output,
|
|
)
|
|
return
|
|
|
|
save_review_csv(rows, output)
|
|
|
|
click.echo()
|
|
click.echo("Plan overview:")
|
|
click.echo(preferred_plan_summary(execution_plan))
|
|
|
|
click.echo()
|
|
click.echo("Plan review summary:")
|
|
click.echo(f" Total operations: {counters['total_operations']}")
|
|
click.echo(f" High-risk operations: {counters['high_risk_operations']}")
|
|
click.echo(f" manual_review: {counters['manual_review']}")
|
|
click.echo(f" sample_source: {counters['sample_source']}")
|
|
click.echo(f" high_season: {counters['high_season']}")
|
|
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:
|
|
preview_lines, hidden_count = render_review_preview(
|
|
rows,
|
|
preview_limit=preview_limit,
|
|
show_all=show_all,
|
|
library_root=ctx.config.library_root,
|
|
)
|
|
for line in preview_lines:
|
|
click.echo(line)
|
|
if hidden_count > 0:
|
|
click.echo(
|
|
f" ... and {hidden_count} more high-risk operations "
|
|
"(use --show-all to display all)"
|
|
)
|
|
else:
|
|
click.echo(" (none)")
|
|
|
|
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"],
|
|
counters["high_risk_operations"],
|
|
output,
|
|
)
|
|
except FileNotFoundError:
|
|
command_error(ctx, f"Error: Plan file not found: {input}", f"Plan file not found: {input}")
|
|
except json.JSONDecodeError as e:
|
|
command_error(
|
|
ctx,
|
|
f"Error: Failed to parse plan JSON: {e}",
|
|
f"Plan review JSON parsing failed: {e}",
|
|
exc_info=True,
|
|
)
|
|
except Exception as e:
|
|
command_error(
|
|
ctx,
|
|
f"Error during plan review: {e}",
|
|
f"Plan review failed: {e}",
|
|
exc_info=True,
|
|
)
|
|
|
|
|
|
def apply_review_cmd(
|
|
ctx: CLIContext,
|
|
plan: Path,
|
|
csv: Path,
|
|
output: Optional[Path],
|
|
) -> None:
|
|
"""Apply modifications from a manual review CSV back to the plan JSON."""
|
|
logger = ctx.logger
|
|
output_path = output or plan
|
|
|
|
try:
|
|
click.echo(f"Loading plan: {plan}")
|
|
execution_plan = load_plan(plan)
|
|
|
|
click.echo(f"Applying review from: {csv}")
|
|
updated_plan = apply_review_to_plan(execution_plan, csv)
|
|
|
|
save_plan(updated_plan, output_path)
|
|
click.echo(f"Successfully updated plan saved to: {output_path}")
|
|
|
|
modified = 0
|
|
for i, op in enumerate(updated_plan.operations):
|
|
if op.operation_type != execution_plan.operations[i].operation_type:
|
|
modified += 1
|
|
|
|
click.echo(f"Total operations modified: {modified}")
|
|
logger.info("Applied review from %s to %s, modified %s ops", csv, output_path, modified)
|
|
|
|
except Exception as e:
|
|
command_error(
|
|
ctx,
|
|
f"Error applying review: {e}",
|
|
f"Apply review failed: {e}",
|
|
exc_info=True,
|
|
)
|