chore: add drift-cli fix, calibrate, validate command compatibility stubs (ADR-100 phase 5b-stubs-b)#723
Open
mick-gsk wants to merge 1 commit into
Open
chore: add drift-cli fix, calibrate, validate command compatibility stubs (ADR-100 phase 5b-stubs-b)#723mick-gsk wants to merge 1 commit into
mick-gsk wants to merge 1 commit into
Conversation
…tubs (ADR-100 phase 5b-stubs-b)
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR replaces several drift.commands.* command implementations with compatibility re-export stubs that forward to the new drift_cli.commands.* modules as part of the ADR-100 monorepo migration (phase 5b stubs).
Changes:
- Removed legacy Click command implementations from
src/drift/commands/*and replaced them with re-export shims todrift_cli.commands.*. - Added
sys.modulesaliasing so imports ofdrift.commands.<cmd>resolve to the correspondingdrift_cli.commands.<cmd>module. - Kept public command entrypoints (e.g.,
validate,fix_plan,config, etc.) available under the old import paths for compatibility.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/drift/commands/verify.py | Re-export shim forwarding verify to drift_cli.commands.verify |
| src/drift/commands/validate_cmd.py | Re-export shim forwarding validate to drift_cli.commands.validate_cmd |
| src/drift/commands/suppress.py | Re-export shim forwarding suppress subcommands to drift_cli.commands.suppress |
| src/drift/commands/setup.py | Re-export shim forwarding setup to drift_cli.commands.setup |
| src/drift/commands/self_improve.py | Re-export shim forwarding self-improve commands to drift_cli.commands.self_improve |
| src/drift/commands/self_analyze.py | Re-export shim forwarding self_analyze to drift_cli.commands.self_analyze |
| src/drift/commands/preset.py | Re-export shim forwarding preset commands to drift_cli.commands.preset |
| src/drift/commands/patch_cmd.py | Re-export shim forwarding patch commands to drift_cli.commands.patch_cmd |
| src/drift/commands/init_cmd.py | Re-export shim forwarding init to drift_cli.commands.init_cmd |
| src/drift/commands/fix_plan.py | Re-export shim forwarding fix_plan to drift_cli.commands.fix_plan |
| src/drift/commands/feedback.py | Re-export shim forwarding feedback commands to drift_cli.commands.feedback |
| src/drift/commands/config_cmd.py | Re-export shim forwarding config commands to drift_cli.commands.config_cmd |
| src/drift/commands/completions.py | Re-export shim forwarding completions to drift_cli.commands.completions |
| src/drift/commands/calibrate.py | Re-export shim forwarding calibrate commands to drift_cli.commands.calibrate |
Comment on lines
+6
to
17
| from drift_cli.commands.suppress import ( | ||
| audit_suppressions as audit_suppressions, | ||
| ) | ||
| @click.option( | ||
| "--config", | ||
| "-c", | ||
| type=click.Path(path_type=Path), | ||
| default=None, | ||
| help="Config file path.", | ||
| from drift_cli.commands.suppress import ( | ||
| interactive as interactive, | ||
| ) | ||
| @click.option( | ||
| "--since", | ||
| default=90, | ||
| type=int, | ||
| help="Days of git history to consider (default: 90).", | ||
| from drift_cli.commands.suppress import ( | ||
| list_suppressions as list_suppressions, | ||
| ) | ||
| @click.option( | ||
| "--dry-run", | ||
| is_flag=True, | ||
| default=False, | ||
| help="Show decisions without writing any changes to disk.", | ||
| from drift_cli.commands.suppress import ( # noqa: F401 | ||
| suppress as suppress, | ||
| ) |
Comment on lines
+6
to
20
| from drift_cli.commands.self_improve import ( | ||
| apply as apply, | ||
| ) | ||
| @click.option( | ||
| "--dry-run", | ||
| is_flag=True, | ||
| default=False, | ||
| help="Print planned actions without writing any files.", | ||
| from drift_cli.commands.self_improve import ( | ||
| close as close, | ||
| ) | ||
| @click.option( | ||
| "--output-dir", | ||
| type=click.Path(file_okay=False, path_type=Path), | ||
| default=None, | ||
| help="Directory for action artefacts (default: <proposals_dir>/../../dsol_actions).", | ||
| from drift_cli.commands.self_improve import ( | ||
| ledger as ledger, | ||
| ) | ||
| @click.option( | ||
| "--format", | ||
| "output_format", | ||
| type=click.Choice(["text", "json"]), | ||
| default="text", | ||
| from drift_cli.commands.self_improve import ( | ||
| run as run, | ||
| ) | ||
| def apply( | ||
| proposals_path: Path, | ||
| dry_run: bool, | ||
| output_dir: Path | None, | ||
| output_format: str, | ||
| ) -> None: | ||
| """Write action-artefacts for each proposal (ADR-098 write-back). | ||
|
|
||
| Produces human-reviewable Markdown files (ADR stubs, triage guides, | ||
| audit action notes) — never modifies source code or scoring config. | ||
| """ | ||
| raw = json.loads(proposals_path.read_text(encoding="utf-8")) | ||
| report = ImprovementReport.model_validate(raw) | ||
|
|
||
| if output_dir is None: | ||
| # Default: work_artifacts/dsol_actions/<cycle_ts>/ | ||
| output_dir = proposals_path.parent.parent.parent / "dsol_actions" / report.cycle_ts | ||
|
|
||
| created: list[str] = [] | ||
|
|
||
| for p in report.proposals: | ||
| kind = p.kind | ||
| if kind == "stale_audit": | ||
| filename = f"stale_audit_action_{_safe_stem(report.cycle_ts)}.md" | ||
| content = ( | ||
| f"# Stale Audit Action — {report.cycle_ts}\n\n" | ||
| f"**Proposal:** `{p.proposal_id}`\n\n" | ||
| f"**Rationale:** {p.rationale}\n\n" | ||
| f"## Required Action\n\n" | ||
| f"{p.suggested_action}\n\n" | ||
| f"```sh\nmake audit-diff\n```\n" | ||
| ) | ||
| elif kind == "regressive_signal": | ||
| stem = _safe_stem(p.signal_type or p.proposal_id) | ||
| filename = f"adr_stub_{stem}.md" | ||
| content = ( | ||
| f"# ADR Draft — Regressive Signal: {p.signal_type or p.proposal_id}\n\n" | ||
| f"- Status: proposed\n" | ||
| f"- Generated by DSOL cycle: {report.cycle_ts}\n\n" | ||
| f"## Context\n\n{p.rationale}\n\n" | ||
| f"## Suggested Action\n\n{p.suggested_action}\n\n" | ||
| f"## Decision\n\n_[Maintainer fills in]_\n\n" | ||
| f"## Consequences\n\n_[Maintainer fills in]_\n" | ||
| ) | ||
| elif kind == "fp_rate_exceeded": | ||
| stem = _safe_stem(p.signal_type or p.proposal_id) | ||
| filename = f"fp_triage_{stem}.md" | ||
| content = ( | ||
| f"# FP Triage — {p.signal_type or p.proposal_id}\n\n" | ||
| f"**Generated by DSOL cycle:** {report.cycle_ts}\n\n" | ||
| f"## Finding\n\n{p.rationale}\n\n" | ||
| f"## Triage Steps\n\n{p.suggested_action}\n\n" | ||
| f"1. Open `benchmark_results/oracle_fp_report.json`\n" | ||
| f"2. Review labeled samples for signal `{p.signal_type}`\n" | ||
| f"3. Classify root cause (threshold / scope / semantic)\n" | ||
| f"4. Decide: adjust threshold, add suppression, or file ADR\n" | ||
| ) | ||
| elif kind == "hotspot_finding": | ||
| stem = _safe_stem(p.proposal_id) | ||
| filename = f"hotspot_{stem}.md" | ||
| content = ( | ||
| f"# Hotspot Finding Action — {report.cycle_ts}\n\n" | ||
| f"**Proposal:** `{p.proposal_id}`\n" | ||
| f"**Signal:** `{p.signal_type}`\n" | ||
| f"**File:** `{p.file_path}`\n" | ||
| f"**Severity:** `{p.severity}`\n\n" | ||
| f"## Rationale\n\n{p.rationale}\n\n" | ||
| f"## Suggested Action\n\n{p.suggested_action}\n" | ||
| ) | ||
| else: | ||
| stem = _safe_stem(p.proposal_id) | ||
| filename = f"action_{stem}.md" | ||
| content = ( | ||
| f"# Action — {p.proposal_id}\n\n" | ||
| f"**Cycle:** {report.cycle_ts}\n\n" | ||
| f"## Rationale\n\n{p.rationale}\n\n" | ||
| f"## Suggested Action\n\n{p.suggested_action}\n" | ||
| ) | ||
|
|
||
| if dry_run: | ||
| click.echo(f"[dry-run] would write: {output_dir / filename}") | ||
| else: | ||
| path = _write_action(output_dir, filename, content) | ||
| created.append(str(path)) | ||
|
|
||
| if output_format == "json": | ||
| click.echo(json.dumps({"dry_run": dry_run, "created": created})) | ||
| else: | ||
| if dry_run: | ||
| click.echo(f"Dry-run complete. {len(report.proposals)} action(s) planned.") | ||
| else: | ||
| click.echo(f"Applied {len(created)} action artefact(s) to {output_dir}") | ||
|
|
||
|
|
||
| @self_improve.command() | ||
| @click.argument("proposal_id") | ||
| @click.option( | ||
| "--note", | ||
| "outcome_note", | ||
| default="", | ||
| help="Short outcome note (e.g. 'implemented in PR #42').", | ||
| from drift_cli.commands.self_improve import ( # noqa: F401 | ||
| self_improve as self_improve, | ||
| ) |
Comment on lines
+6
to
+10
| from drift_cli.commands.verify import ( # noqa: F401 | ||
| verify as verify, | ||
| ) | ||
| @click.option( | ||
| "--scope", | ||
| default=None, | ||
| help="Comma-separated file paths to restrict verification scope.", | ||
| ) | ||
| def verify( | ||
| repo: Path, | ||
| ref: str | None, | ||
| uncommitted: bool, | ||
| staged_only: bool, | ||
| fail_on: str, | ||
| baseline: Path | None, | ||
| output_format: str, | ||
| exit_zero: bool, | ||
| output_file: Path | None, | ||
| scope: str | None, | ||
| ) -> None: | ||
| """Verify structural coherence after edits — binary pass/fail verdict. | ||
|
|
||
| Designed for CI pipelines and agent workflows. Returns PASS when no | ||
| new findings above the severity threshold are introduced and the | ||
| drift score has not degraded. | ||
|
|
||
| \b | ||
| Examples: | ||
| drift verify # Quick check, rich output | ||
| drift verify --format json # Machine-readable verdict | ||
| drift verify --ref main --no-uncommitted # Compare against explicit ref | ||
| drift verify --fail-on medium # Stricter threshold | ||
| drift verify --scope src/api.py,src/db.py # Restrict to specific files | ||
| drift verify --exit-zero # Report-only mode | ||
| """ | ||
| from drift.api.verify import verify as api_verify | ||
|
|
||
| scope_files = [s.strip() for s in scope.split(",") if s.strip()] if scope else None | ||
|
|
||
| result = api_verify( | ||
| path=str(repo), | ||
| ref=ref, | ||
| uncommitted=uncommitted if not staged_only else False, | ||
| staged_only=staged_only, | ||
| fail_on=fail_on, | ||
| baseline=str(baseline) if baseline else None, | ||
| scope_files=scope_files, | ||
| ) | ||
|
|
||
| # Handle error responses. | ||
| if result.get("type") == "error": | ||
| if output_format == "json": | ||
| _emit_machine_output(json.dumps(result, indent=2, default=str), output_file) | ||
| else: | ||
| console.print( | ||
| f"[bold red]{fail_glyph(console)} Verify error:[/bold red]" | ||
| f" {result.get('message', '')}" | ||
| ) | ||
| if not exit_zero: | ||
| sys.exit(EXIT_FINDINGS_ABOVE_THRESHOLD) | ||
| return | ||
|
|
||
| passed = result.get("pass", False) | ||
|
|
||
| if output_format == "json": | ||
| _emit_machine_output(json.dumps(result, indent=2, default=str), output_file) | ||
| else: | ||
| _render_rich_verdict(result, console) | ||
|
|
||
| if not passed and not exit_zero: | ||
| sys.exit(EXIT_FINDINGS_ABOVE_THRESHOLD) | ||
|
|
||
|
|
||
| def _render_rich_verdict(result: dict, con: Console) -> None: | ||
| """Render a human-readable pass/fail verdict.""" | ||
| from rich.panel import Panel | ||
| from rich.table import Table | ||
|
|
||
| passed = result.get("pass", False) | ||
| delta = result.get("score_delta", 0.0) | ||
| direction = result.get("direction", "stable") | ||
| introduced = result.get("findings_introduced_count", 0) | ||
| resolved = result.get("findings_resolved_count", 0) | ||
| blocking = result.get("blocking_reasons", []) | ||
|
|
||
| if passed: | ||
| con.print( | ||
| Panel( | ||
| f"[bold green]{ok_glyph(con)} PASS[/bold green] — " | ||
| f"No structural coherence degradation detected.\n\n" | ||
| f" Score delta: {delta:+.4f} ({direction})\n" | ||
| f" New findings: {introduced} | Resolved: {resolved}", | ||
| title="[bold green]drift verify[/bold green]", | ||
| border_style="green", | ||
| ) | ||
| ) | ||
| else: | ||
| # Build blocking reasons table. | ||
| table = Table(show_header=True, header_style="bold red") | ||
| table.add_column("Type", style="dim") | ||
| table.add_column("Reason") | ||
| table.add_column("File", style="dim") | ||
|
|
||
| for reason in blocking[:10]: | ||
| table.add_row( | ||
| reason.get("type", ""), | ||
| reason.get("reason", ""), | ||
| reason.get("file", ""), | ||
| ) | ||
|
|
||
| con.print( | ||
| Panel( | ||
| f"[bold red]{fail_glyph(con)} FAIL[/bold red] — " | ||
| f"{len(blocking)} blocking reason(s) detected.\n\n" | ||
| f" Score delta: {delta:+.4f} ({direction})\n" | ||
| f" New findings: {introduced} | Resolved: {resolved}", | ||
| title="[bold red]drift verify[/bold red]", | ||
| border_style="red", | ||
| ) | ||
| ) | ||
| con.print(table) | ||
| con.print( | ||
| "\n[dim]Run [bold]drift fix-plan[/bold] for a prioritized repair plan.[/dim]" | ||
| ) | ||
| _sys.modules[__name__] = _importlib.import_module("drift_cli.commands.verify") |
Comment on lines
+24
to
+26
| from drift_cli.commands.calibrate import ( | ||
| run as run, | ||
| ) |
Comment on lines
+6
to
+8
| from drift_cli.commands.completions import ( # noqa: F401 | ||
| completions as completions, | ||
| ) |
Comment on lines
+9
to
+11
| from drift_cli.commands.config_cmd import ( | ||
| schema as schema, | ||
| ) |
Comment on lines
+12
to
+14
| from drift_cli.commands.config_cmd import ( | ||
| show as show, | ||
| ) |
Comment on lines
+6
to
+8
| from drift_cli.commands.preset import ( # noqa: F401 | ||
| preset as preset, | ||
| ) |
Comment on lines
+9
to
+11
| from drift_cli.commands.preset import ( | ||
| preset_list as preset_list, | ||
| ) |
Comment on lines
+12
to
+14
| from drift_cli.commands.preset import ( | ||
| preset_show as preset_show, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Split from #576 (ADR-100 monorepo migration). Part of the PR decomposition into atomic, reviewable units.