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

1"""CLI command for auditing project integrity.""" 

2 

3from pathlib import Path 

4 

5import click 

6 

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 

14 

15 

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)') 

22 

23 

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') 

30 

31 

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') 

38 

39 

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') 

46 

47 

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() 

55 

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() 

62 

63 # Execute use case 

64 interactor = AuditBinder( 

65 binder_repo=binder_repo, 

66 node_repo=node_repo, 

67 logger=logger, 

68 ) 

69 

70 report = interactor.execute() 

71 

72 # Always report placeholders if they exist (informational) 

73 if report.placeholders: 

74 _report_placeholders(report) 

75 

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') 

92 

93 click.echo('\nProject integrity check completed') 

94 

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) 

102 

103 except FileSystemError as err: 

104 click.echo(f'Error: {err}', err=True) 

105 raise SystemExit(2) from err