Coverage for src/prosemark/cli/audit.py: 100%
66 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
1"""CLI command for auditing project integrity."""
3from pathlib import Path
5import click
7from prosemark.adapters.binder_repo_fs import BinderRepoFs
8from prosemark.adapters.clock_system import ClockSystem
9from prosemark.adapters.editor_launcher_system import EditorLauncherSystem
10from prosemark.adapters.logger_stdout import LoggerStdout
11from prosemark.adapters.node_repo_fs import NodeRepoFs
12from prosemark.app.use_cases import AuditBinder, AuditReport
13from prosemark.exceptions import FileSystemError
16def _report_placeholders(report: AuditReport) -> None:
17 """Report placeholder nodes."""
18 if report.placeholders:
19 for placeholder in report.placeholders:
20 if hasattr(placeholder, 'display_title'):
21 click.echo(f'⚠ PLACEHOLDER: "{placeholder.display_title}" (no associated files)')
24def _report_missing_nodes(report: AuditReport) -> None:
25 """Report missing nodes."""
26 if report.missing:
27 for missing in report.missing:
28 if hasattr(missing, 'node_id'):
29 click.echo(f'⚠ MISSING: Node {missing.node_id} referenced but files not found')
32def _report_orphans(report: AuditReport) -> None:
33 """Report orphaned files."""
34 if report.orphans:
35 for orphan in report.orphans:
36 if hasattr(orphan, 'file_path'):
37 click.echo(f'⚠ ORPHAN: File {orphan.file_path} exists but not in binder')
40def _report_mismatches(report: AuditReport) -> None:
41 """Report file mismatches."""
42 if report.mismatches:
43 for mismatch in report.mismatches:
44 if hasattr(mismatch, 'file_path'):
45 click.echo(f'⚠ MISMATCH: File {mismatch.file_path} ID mismatch')
48@click.command()
49@click.option('--fix/--no-fix', default=False, help='Attempt to fix discovered issues')
50@click.option('--path', '-p', type=click.Path(path_type=Path), help='Project directory')
51def audit_command(*, fix: bool, path: Path | None) -> None:
52 """Check project integrity."""
53 try:
54 project_root = path or Path.cwd()
56 # Wire up dependencies
57 binder_repo = BinderRepoFs(project_root)
58 clock = ClockSystem()
59 editor = EditorLauncherSystem()
60 node_repo = NodeRepoFs(project_root, editor, clock)
61 logger = LoggerStdout()
63 # Execute use case
64 interactor = AuditBinder(
65 binder_repo=binder_repo,
66 node_repo=node_repo,
67 logger=logger,
68 )
70 report = interactor.execute()
72 # Always report placeholders if they exist (informational)
73 if report.placeholders:
74 _report_placeholders(report)
76 # Report actual issues if they exist
77 has_real_issues = report.missing or report.orphans or report.mismatches
78 if has_real_issues:
79 if report.placeholders:
80 click.echo('') # Add spacing after placeholders
81 click.echo('Project integrity issues found:')
82 _report_missing_nodes(report)
83 _report_orphans(report)
84 _report_mismatches(report)
85 else:
86 # Show success messages for real issues when none exist
87 if report.placeholders:
88 click.echo('') # Add spacing after placeholders
89 click.echo('✓ All nodes have valid files')
90 click.echo('✓ All references are consistent')
91 click.echo('✓ No orphaned files found')
93 click.echo('\nProject integrity check completed')
95 # Only exit with error code for real issues, not placeholders
96 if has_real_issues:
97 if fix:
98 click.echo('\nNote: Auto-fix not implemented in MVP')
99 raise SystemExit(2)
100 # Exit with code 1 when issues are found (standard audit behavior)
101 raise SystemExit(1)
103 except FileSystemError as err:
104 click.echo(f'Error: {err}', err=True)
105 raise SystemExit(2) from err