code-maat-python 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,822 @@
1
+ """Command-line interface for code-maat-python.
2
+
3
+ Modern CLI using click framework with subcommands for each analysis type.
4
+ """
5
+
6
+ import sys
7
+ from collections.abc import Callable
8
+ from functools import wraps
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import click
13
+ import pandas as pd
14
+
15
+ from code_maat_python.analyses import (
16
+ analyze_authors,
17
+ analyze_coupling,
18
+ analyze_entities,
19
+ analyze_revisions,
20
+ analyze_soc,
21
+ analyze_summary,
22
+ )
23
+ from code_maat_python.analyses.age import code_age
24
+ from code_maat_python.analyses.churn import (
25
+ abs_churn,
26
+ author_churn,
27
+ entity_churn,
28
+ entity_ownership,
29
+ main_dev,
30
+ refactoring_main_dev,
31
+ )
32
+ from code_maat_python.analyses.communication import communication
33
+ from code_maat_python.analyses.effort import (
34
+ entity_effort,
35
+ fragmentation,
36
+ main_dev_by_revs,
37
+ )
38
+ from code_maat_python.parser import parse_git_log
39
+
40
+
41
+ def validate_logfile(ctx: click.Context, param: click.Parameter, value: str) -> Path:
42
+ """Validate that the logfile exists and is readable.
43
+
44
+ Supports regular files, stdin, pipes, and process substitution (e.g., <(git log ...)).
45
+
46
+ Args:
47
+ ctx: Click context
48
+ param: Click parameter
49
+ value: Path string
50
+
51
+ Returns:
52
+ Path object if valid
53
+
54
+ Raises:
55
+ click.BadParameter: If file doesn't exist or isn't readable
56
+ """
57
+ path = Path(value)
58
+
59
+ # Check if path exists (regular files, symlinks, etc.)
60
+ # or if it's a special file (pipes, character devices like /dev/fd/N)
61
+ if not path.exists() and not (path.is_fifo() or path.is_char_device()):
62
+ raise click.BadParameter(f"File not found: {value}")
63
+
64
+ # Try to open and read to verify it's actually readable
65
+ try:
66
+ with open(path, 'r') as f:
67
+ # Just verify we can open it; don't read anything yet
68
+ pass
69
+ except (OSError, IOError) as e:
70
+ raise click.BadParameter(f"Cannot read file: {value} ({e})")
71
+
72
+ return path
73
+
74
+
75
+ def common_options(func: Callable[..., Any]) -> Callable[..., Any]:
76
+ """Add common options to all analysis commands.
77
+
78
+ Adds --group, --team-map-file, --rows, and --output options.
79
+ """
80
+
81
+ @click.option(
82
+ "--group",
83
+ "-g",
84
+ type=str,
85
+ help="Architectural grouping specification file",
86
+ )
87
+ @click.option(
88
+ "--team-map-file",
89
+ "-p",
90
+ type=str,
91
+ help="Team mapping CSV file",
92
+ )
93
+ @click.option(
94
+ "--rows",
95
+ "-r",
96
+ type=int,
97
+ help="Maximum number of rows to output",
98
+ )
99
+ @click.option(
100
+ "--output",
101
+ "-o",
102
+ type=str,
103
+ help="Output CSV file (default: stdout)",
104
+ )
105
+ @wraps(func)
106
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
107
+ return func(*args, **kwargs)
108
+
109
+ return wrapper
110
+
111
+
112
+ def apply_transformers(
113
+ df: pd.DataFrame, group_file: str | None, team_map_file: str | None
114
+ ) -> pd.DataFrame:
115
+ """Apply transformers in correct order.
116
+
117
+ Args:
118
+ df: DataFrame with parsed git log data
119
+ group_file: Path to architectural grouping specification file
120
+ team_map_file: Path to team mapping CSV file
121
+
122
+ Returns:
123
+ Transformed DataFrame
124
+ """
125
+ result = df
126
+
127
+ # Apply architectural grouping first (reduces entity granularity)
128
+ if group_file:
129
+ from code_maat_python.transformers.grouper import (
130
+ load_group_specification_file,
131
+ map_entities_to_groups,
132
+ )
133
+
134
+ patterns = load_group_specification_file(group_file)
135
+ result = map_entities_to_groups(result, patterns)
136
+
137
+ # Apply team mapping second (reduces author granularity)
138
+ if team_map_file:
139
+ from code_maat_python.transformers.team_mapper import (
140
+ load_team_mapping_file,
141
+ map_authors_to_teams,
142
+ )
143
+
144
+ mapping = load_team_mapping_file(team_map_file)
145
+ result = map_authors_to_teams(result, mapping)
146
+
147
+ return result
148
+
149
+
150
+ def output_results(df: pd.DataFrame, output: str | None, max_rows: int | None = None) -> None:
151
+ """Output analysis results to stdout or file.
152
+
153
+ Args:
154
+ df: DataFrame with analysis results
155
+ output: Output file path or None for stdout
156
+ max_rows: Maximum number of rows to output (None for all)
157
+ """
158
+ # Limit rows if specified
159
+ if max_rows and len(df) > max_rows:
160
+ df = df.head(max_rows)
161
+
162
+ if output:
163
+ df.to_csv(output, index=False)
164
+ click.echo(f"Results written to {output}", err=True)
165
+ else:
166
+ # Output to stdout
167
+ click.echo(df.to_csv(index=False))
168
+
169
+
170
+ def handle_analysis_error(e: Exception) -> None:
171
+ """Handle analysis errors with helpful messages.
172
+
173
+ Args:
174
+ e: Exception that occurred
175
+ """
176
+ click.echo(f"Error during analysis: {str(e)}", err=True)
177
+ sys.exit(1)
178
+
179
+
180
+ # Main command group
181
+ @click.group()
182
+ @click.version_option(version="0.1.0", prog_name="code-maat-python")
183
+ def main() -> None:
184
+ """Code Maat Pandas - Modern Python tool for mining version control data.
185
+
186
+ Analyzes git repository logs to identify patterns, coupling, churn,
187
+ and other metrics useful for understanding code evolution.
188
+
189
+ \b
190
+ Example workflow:
191
+ 1. Generate a git log:
192
+ git log --all -M -C --numstat --date=short \\
193
+ --pretty=format:'--%h--%cd--%cn' > git.log
194
+
195
+ 2. Run an analysis:
196
+ code-maat-python coupling git.log --min-coupling 50
197
+
198
+ 3. Save results to CSV:
199
+ code-maat-python revisions git.log --output results.csv
200
+ """
201
+ pass
202
+
203
+
204
+ @main.command()
205
+ @click.argument("logfile", type=str, callback=validate_logfile)
206
+ @common_options
207
+ def authors(
208
+ logfile: Path,
209
+ group: str | None,
210
+ team_map_file: str | None,
211
+ rows: int | None,
212
+ output: str | None,
213
+ ) -> None:
214
+ """Count distinct authors per entity with revision counts.
215
+
216
+ Shows how many authors have worked on each file and the number
217
+ of revisions, useful for identifying knowledge distribution.
218
+
219
+ \b
220
+ Example:
221
+ code-maat-python authors git.log
222
+ code-maat-python authors git.log --output authors.csv
223
+ code-maat-python authors git.log --group layers.txt --rows 20
224
+ """
225
+ try:
226
+ df = parse_git_log(logfile)
227
+ df = apply_transformers(df, group, team_map_file)
228
+ result = analyze_authors(df)
229
+ output_results(result, output, rows)
230
+ except Exception as e:
231
+ handle_analysis_error(e)
232
+
233
+
234
+ @main.command()
235
+ @click.argument("logfile", type=str, callback=validate_logfile)
236
+ @common_options
237
+ def revisions(
238
+ logfile: Path,
239
+ group: str | None,
240
+ team_map_file: str | None,
241
+ rows: int | None,
242
+ output: str | None,
243
+ ) -> None:
244
+ """Sort entities by revision frequency.
245
+
246
+ Lists all files sorted by number of revisions, useful for
247
+ identifying hotspots and frequently changed code.
248
+
249
+ \b
250
+ Example:
251
+ code-maat-python revisions git.log
252
+ code-maat-python revisions git.log --output revisions.csv
253
+ code-maat-python revisions git.log --group layers.txt --rows 10
254
+ """
255
+ try:
256
+ df = parse_git_log(logfile)
257
+ df = apply_transformers(df, group, team_map_file)
258
+ result = analyze_revisions(df)
259
+ output_results(result, output, rows)
260
+ except Exception as e:
261
+ handle_analysis_error(e)
262
+
263
+
264
+ @main.command()
265
+ @click.argument("logfile", type=str, callback=validate_logfile)
266
+ @common_options
267
+ def entities(
268
+ logfile: Path,
269
+ group: str | None,
270
+ team_map_file: str | None,
271
+ rows: int | None,
272
+ output: str | None,
273
+ ) -> None:
274
+ """List all entities with basic statistics.
275
+
276
+ Shows all files in the repository with commit counts,
277
+ useful for understanding the scope of the codebase.
278
+
279
+ \b
280
+ Example:
281
+ code-maat-python entities git.log
282
+ code-maat-python entities git.log --output entities.csv
283
+ code-maat-python entities git.log --group layers.txt
284
+ """
285
+ try:
286
+ df = parse_git_log(logfile)
287
+ df = apply_transformers(df, group, team_map_file)
288
+ result = analyze_entities(df)
289
+ output_results(result, output, rows)
290
+ except Exception as e:
291
+ handle_analysis_error(e)
292
+
293
+
294
+ @main.command()
295
+ @click.argument("logfile", type=str, callback=validate_logfile)
296
+ @common_options
297
+ def summary(
298
+ logfile: Path,
299
+ group: str | None,
300
+ team_map_file: str | None,
301
+ rows: int | None,
302
+ output: str | None,
303
+ ) -> None:
304
+ """Generate overview statistics for the repository.
305
+
306
+ Provides high-level statistics including number of commits,
307
+ entities, authors, and date range of the repository history.
308
+
309
+ \b
310
+ Example:
311
+ code-maat-python summary git.log
312
+ code-maat-python summary git.log --output summary.csv
313
+ code-maat-python summary git.log --group layers.txt
314
+ """
315
+ try:
316
+ df = parse_git_log(logfile)
317
+ df = apply_transformers(df, group, team_map_file)
318
+ result = analyze_summary(df)
319
+ output_results(result, output, rows)
320
+ except Exception as e:
321
+ handle_analysis_error(e)
322
+
323
+
324
+ @main.command()
325
+ @click.argument("logfile", type=str, callback=validate_logfile)
326
+ @click.option(
327
+ "--min-revs",
328
+ type=int,
329
+ default=5,
330
+ show_default=True,
331
+ help="Minimum number of revisions for a module to be included",
332
+ )
333
+ @click.option(
334
+ "--min-shared-revs",
335
+ type=int,
336
+ default=5,
337
+ show_default=True,
338
+ help="Minimum number of shared revisions between modules",
339
+ )
340
+ @click.option(
341
+ "--min-coupling",
342
+ type=int,
343
+ default=30,
344
+ show_default=True,
345
+ help="Minimum coupling percentage (0-100)",
346
+ )
347
+ @click.option(
348
+ "--max-coupling",
349
+ type=int,
350
+ default=100,
351
+ show_default=True,
352
+ help="Maximum coupling percentage (0-100)",
353
+ )
354
+ @click.option(
355
+ "--max-changeset-size",
356
+ type=int,
357
+ default=30,
358
+ show_default=True,
359
+ help="Maximum number of files in a commit to consider",
360
+ )
361
+ @common_options
362
+ def coupling(
363
+ logfile: Path,
364
+ min_revs: int,
365
+ min_shared_revs: int,
366
+ min_coupling: int,
367
+ max_coupling: int,
368
+ max_changeset_size: int,
369
+ group: str | None,
370
+ team_map_file: str | None,
371
+ rows: int | None,
372
+ output: str | None,
373
+ ) -> None:
374
+ """Calculate logical coupling between files.
375
+
376
+ Identifies files that frequently change together, which may
377
+ indicate hidden dependencies or architectural issues.
378
+
379
+ Logical coupling is calculated as:
380
+ (shared_revisions / average_revisions) * 100
381
+
382
+ Large commits (> max-changeset-size) are filtered out to avoid
383
+ noise from bulk refactorings or automated changes.
384
+
385
+ \b
386
+ Example:
387
+ code-maat-python coupling git.log
388
+ code-maat-python coupling git.log --min-coupling 50
389
+ code-maat-python coupling git.log --min-revs 10 --min-shared-revs 5
390
+ code-maat-python coupling git.log --group layers.txt --rows 20
391
+ code-maat-python coupling git.log --output coupling.csv
392
+ """
393
+ try:
394
+ # Validate parameters
395
+ if not (0 <= min_coupling <= 100):
396
+ raise click.BadParameter("min-coupling must be between 0 and 100")
397
+ if not (0 <= max_coupling <= 100):
398
+ raise click.BadParameter("max-coupling must be between 0 and 100")
399
+ if min_coupling > max_coupling:
400
+ raise click.BadParameter("min-coupling must be <= max-coupling")
401
+
402
+ df = parse_git_log(logfile)
403
+ df = apply_transformers(df, group, team_map_file)
404
+ result = analyze_coupling(
405
+ df,
406
+ min_revs=min_revs,
407
+ min_shared_revs=min_shared_revs,
408
+ min_coupling=min_coupling,
409
+ max_coupling=max_coupling,
410
+ max_changeset_size=max_changeset_size,
411
+ )
412
+ output_results(result, output, rows)
413
+ except Exception as e:
414
+ handle_analysis_error(e)
415
+
416
+
417
+ @main.command()
418
+ @click.argument("logfile", type=str, callback=validate_logfile)
419
+ @click.option(
420
+ "--max-changeset-size",
421
+ type=int,
422
+ default=30,
423
+ show_default=True,
424
+ help="Maximum number of files in a commit to consider",
425
+ )
426
+ @common_options
427
+ def soc(
428
+ logfile: Path,
429
+ max_changeset_size: int,
430
+ group: str | None,
431
+ team_map_file: str | None,
432
+ rows: int | None,
433
+ output: str | None,
434
+ ) -> None:
435
+ """Calculate sum of coupling (SOC) for each entity.
436
+
437
+ A simpler metric than full logical coupling. For each commit
438
+ with m files, each file gets a SOC score of (m-1).
439
+
440
+ This provides a quick way to identify files that are often
441
+ changed together with other files.
442
+
443
+ \b
444
+ Example:
445
+ code-maat-python soc git.log
446
+ code-maat-python soc git.log --max-changeset-size 50
447
+ code-maat-python soc git.log --group layers.txt --rows 10
448
+ code-maat-python soc git.log --output soc.csv
449
+ """
450
+ try:
451
+ df = parse_git_log(logfile)
452
+ df = apply_transformers(df, group, team_map_file)
453
+ result = analyze_soc(df, max_changeset_size=max_changeset_size)
454
+ output_results(result, output, rows)
455
+ except Exception as e:
456
+ handle_analysis_error(e)
457
+
458
+
459
+ @main.command()
460
+ @click.argument("logfile", type=str, callback=validate_logfile)
461
+ @common_options
462
+ def abs_churn_cmd(
463
+ logfile: Path,
464
+ group: str | None,
465
+ team_map_file: str | None,
466
+ rows: int | None,
467
+ output: str | None,
468
+ ) -> None:
469
+ """Calculate absolute code churn per date.
470
+
471
+ Shows total lines added and deleted for each date in the
472
+ commit history, useful for identifying periods of high activity.
473
+
474
+ \b
475
+ Example:
476
+ code-maat-python abs-churn git.log
477
+ code-maat-python abs-churn git.log --output churn.csv
478
+ code-maat-python abs-churn git.log --rows 30
479
+ """
480
+ try:
481
+ df = parse_git_log(logfile)
482
+ df = apply_transformers(df, group, team_map_file)
483
+ result = abs_churn(df)
484
+ output_results(result, output, rows)
485
+ except Exception as e:
486
+ handle_analysis_error(e)
487
+
488
+
489
+ @main.command()
490
+ @click.argument("logfile", type=str, callback=validate_logfile)
491
+ @common_options
492
+ def author_churn_cmd(
493
+ logfile: Path,
494
+ group: str | None,
495
+ team_map_file: str | None,
496
+ rows: int | None,
497
+ output: str | None,
498
+ ) -> None:
499
+ """Calculate total churn per author.
500
+
501
+ Shows total lines added and deleted by each author across
502
+ all entities, useful for understanding individual contributions.
503
+
504
+ \b
505
+ Example:
506
+ code-maat-python author-churn git.log
507
+ code-maat-python author-churn git.log --team-map-file teams.csv
508
+ code-maat-python author-churn git.log --output author-churn.csv
509
+ """
510
+ try:
511
+ df = parse_git_log(logfile)
512
+ df = apply_transformers(df, group, team_map_file)
513
+ result = author_churn(df)
514
+ output_results(result, output, rows)
515
+ except Exception as e:
516
+ handle_analysis_error(e)
517
+
518
+
519
+ @main.command()
520
+ @click.argument("logfile", type=str, callback=validate_logfile)
521
+ @common_options
522
+ def entity_churn_cmd(
523
+ logfile: Path,
524
+ group: str | None,
525
+ team_map_file: str | None,
526
+ rows: int | None,
527
+ output: str | None,
528
+ ) -> None:
529
+ """Calculate absolute churn per entity.
530
+
531
+ Shows total lines added and deleted for each file, sorted by
532
+ lines added (a better predictor of post-release defects).
533
+
534
+ \b
535
+ Example:
536
+ code-maat-python entity-churn git.log
537
+ code-maat-python entity-churn git.log --group layers.txt --rows 20
538
+ code-maat-python entity-churn git.log --output entity-churn.csv
539
+ """
540
+ try:
541
+ df = parse_git_log(logfile)
542
+ df = apply_transformers(df, group, team_map_file)
543
+ result = entity_churn(df)
544
+ output_results(result, output, rows)
545
+ except Exception as e:
546
+ handle_analysis_error(e)
547
+
548
+
549
+ @main.command()
550
+ @click.argument("logfile", type=str, callback=validate_logfile)
551
+ @common_options
552
+ def entity_ownership_cmd(
553
+ logfile: Path,
554
+ group: str | None,
555
+ team_map_file: str | None,
556
+ rows: int | None,
557
+ output: str | None,
558
+ ) -> None:
559
+ """Calculate ownership of each entity by author based on churn.
560
+
561
+ Shows how much each author has contributed to each entity
562
+ in terms of lines added and deleted, identifying code ownership.
563
+
564
+ \b
565
+ Example:
566
+ code-maat-python entity-ownership git.log
567
+ code-maat-python entity-ownership git.log --group layers.txt
568
+ code-maat-python entity-ownership git.log --output ownership.csv
569
+ """
570
+ try:
571
+ df = parse_git_log(logfile)
572
+ df = apply_transformers(df, group, team_map_file)
573
+ result = entity_ownership(df)
574
+ output_results(result, output, rows)
575
+ except Exception as e:
576
+ handle_analysis_error(e)
577
+
578
+
579
+ @main.command()
580
+ @click.argument("logfile", type=str, callback=validate_logfile)
581
+ @common_options
582
+ def main_dev_cmd(
583
+ logfile: Path,
584
+ group: str | None,
585
+ team_map_file: str | None,
586
+ rows: int | None,
587
+ output: str | None,
588
+ ) -> None:
589
+ """Identify the main developer of each entity by lines added.
590
+
591
+ The main developer is the author who contributed the most lines
592
+ of code to each entity. Returns ownership percentage.
593
+
594
+ \b
595
+ Example:
596
+ code-maat-python main-dev git.log
597
+ code-maat-python main-dev git.log --group layers.txt --rows 15
598
+ code-maat-python main-dev git.log --output main-dev.csv
599
+ """
600
+ try:
601
+ df = parse_git_log(logfile)
602
+ df = apply_transformers(df, group, team_map_file)
603
+ result = main_dev(df)
604
+ output_results(result, output, rows)
605
+ except Exception as e:
606
+ handle_analysis_error(e)
607
+
608
+
609
+ @main.command()
610
+ @click.argument("logfile", type=str, callback=validate_logfile)
611
+ @common_options
612
+ def refactoring_main_dev_cmd(
613
+ logfile: Path,
614
+ group: str | None,
615
+ team_map_file: str | None,
616
+ rows: int | None,
617
+ output: str | None,
618
+ ) -> None:
619
+ """Identify the main developer of each entity by lines removed.
620
+
621
+ Alternative calculation identifying main developer as the author
622
+ who removed the most lines (representing refactoring effort).
623
+
624
+ \b
625
+ Example:
626
+ code-maat-python refactoring-main-dev git.log
627
+ code-maat-python refactoring-main-dev git.log --rows 10
628
+ code-maat-python refactoring-main-dev git.log --output refactoring.csv
629
+ """
630
+ try:
631
+ df = parse_git_log(logfile)
632
+ df = apply_transformers(df, group, team_map_file)
633
+ result = refactoring_main_dev(df)
634
+ output_results(result, output, rows)
635
+ except Exception as e:
636
+ handle_analysis_error(e)
637
+
638
+
639
+ @main.command()
640
+ @click.argument("logfile", type=str, callback=validate_logfile)
641
+ @common_options
642
+ def entity_effort_cmd(
643
+ logfile: Path,
644
+ group: str | None,
645
+ team_map_file: str | None,
646
+ rows: int | None,
647
+ output: str | None,
648
+ ) -> None:
649
+ """Calculate author contribution to each entity by revision count.
650
+
651
+ Identifies how many revisions each author contributed to each
652
+ entity, providing a measure of effort.
653
+
654
+ \b
655
+ Example:
656
+ code-maat-python entity-effort git.log
657
+ code-maat-python entity-effort git.log --group layers.txt
658
+ code-maat-python entity-effort git.log --output effort.csv
659
+ """
660
+ try:
661
+ df = parse_git_log(logfile)
662
+ df = apply_transformers(df, group, team_map_file)
663
+ result = entity_effort(df)
664
+ output_results(result, output, rows)
665
+ except Exception as e:
666
+ handle_analysis_error(e)
667
+
668
+
669
+ @main.command()
670
+ @click.argument("logfile", type=str, callback=validate_logfile)
671
+ @common_options
672
+ def main_dev_by_revs_cmd(
673
+ logfile: Path,
674
+ group: str | None,
675
+ team_map_file: str | None,
676
+ rows: int | None,
677
+ output: str | None,
678
+ ) -> None:
679
+ """Identify the main developer of each entity by revision count.
680
+
681
+ The main developer is the author who contributed the most
682
+ revisions to each entity. Returns ownership percentage.
683
+
684
+ \b
685
+ Example:
686
+ code-maat-python main-dev-by-revs git.log
687
+ code-maat-python main-dev-by-revs git.log --rows 15
688
+ code-maat-python main-dev-by-revs git.log --output main-dev-revs.csv
689
+ """
690
+ try:
691
+ df = parse_git_log(logfile)
692
+ df = apply_transformers(df, group, team_map_file)
693
+ result = main_dev_by_revs(df)
694
+ output_results(result, output, rows)
695
+ except Exception as e:
696
+ handle_analysis_error(e)
697
+
698
+
699
+ @main.command()
700
+ @click.argument("logfile", type=str, callback=validate_logfile)
701
+ @common_options
702
+ def fragmentation_cmd(
703
+ logfile: Path,
704
+ group: str | None,
705
+ team_map_file: str | None,
706
+ rows: int | None,
707
+ output: str | None,
708
+ ) -> None:
709
+ """Calculate fragmentation for each entity using fractal value.
710
+
711
+ The fractal value measures how fragmented contributions are
712
+ across authors. 0 = single author, approaching 1 = highly fragmented.
713
+
714
+ \b
715
+ Example:
716
+ code-maat-python fragmentation git.log
717
+ code-maat-python fragmentation git.log --group layers.txt --rows 20
718
+ code-maat-python fragmentation git.log --output fragmentation.csv
719
+ """
720
+ try:
721
+ df = parse_git_log(logfile)
722
+ df = apply_transformers(df, group, team_map_file)
723
+ result = fragmentation(df)
724
+ output_results(result, output, rows)
725
+ except Exception as e:
726
+ handle_analysis_error(e)
727
+
728
+
729
+ @main.command()
730
+ @click.argument("logfile", type=str, callback=validate_logfile)
731
+ @click.option(
732
+ "--reference-date",
733
+ "-d",
734
+ type=str,
735
+ help="Reference date for age calculation (default: today, format: YYYY-MM-DD)",
736
+ )
737
+ @common_options
738
+ def age(
739
+ logfile: Path,
740
+ reference_date: str | None,
741
+ group: str | None,
742
+ team_map_file: str | None,
743
+ rows: int | None,
744
+ output: str | None,
745
+ ) -> None:
746
+ """Calculate age of entities in months since last modification.
747
+
748
+ Identifies stale code that hasn't been modified recently, which
749
+ may indicate technical debt, abandoned features, or stable components.
750
+
751
+ \b
752
+ Example:
753
+ code-maat-python age git.log
754
+ code-maat-python age git.log --reference-date 2023-12-31
755
+ code-maat-python age git.log --group layers.txt --rows 25
756
+ code-maat-python age git.log --output age.csv
757
+ """
758
+ try:
759
+ df = parse_git_log(logfile)
760
+ df = apply_transformers(df, group, team_map_file)
761
+
762
+ # Parse reference date if provided
763
+ ref_date = None
764
+ if reference_date:
765
+ ref_date = pd.Timestamp(reference_date)
766
+
767
+ result = code_age(df, reference_date=ref_date)
768
+ output_results(result, output, rows)
769
+ except Exception as e:
770
+ handle_analysis_error(e)
771
+
772
+
773
+ @main.command()
774
+ @click.argument("logfile", type=str, callback=validate_logfile)
775
+ @click.option(
776
+ "--min-shared",
777
+ type=int,
778
+ default=5,
779
+ show_default=True,
780
+ help="Minimum number of shared entities between developers",
781
+ )
782
+ @click.option(
783
+ "--min-coupling",
784
+ type=int,
785
+ default=30,
786
+ show_default=True,
787
+ help="Minimum coupling strength percentage (0-100)",
788
+ )
789
+ @common_options
790
+ def communication_cmd(
791
+ logfile: Path,
792
+ min_shared: int,
793
+ min_coupling: int,
794
+ group: str | None,
795
+ team_map_file: str | None,
796
+ rows: int | None,
797
+ output: str | None,
798
+ ) -> None:
799
+ """Calculate communication needs between developers.
800
+
801
+ Identifies developers who work on the same code files, indicating
802
+ a need for communication and coordination. Strength is normalized
803
+ by each developer's total workload.
804
+
805
+ \b
806
+ Example:
807
+ code-maat-python communication git.log
808
+ code-maat-python communication git.log --min-shared 10 --min-coupling 50
809
+ code-maat-python communication git.log --team-map-file teams.csv --rows 15
810
+ code-maat-python communication git.log --output communication.csv
811
+ """
812
+ try:
813
+ df = parse_git_log(logfile)
814
+ df = apply_transformers(df, group, team_map_file)
815
+ result = communication(df, min_shared=min_shared, min_coupling=min_coupling)
816
+ output_results(result, output, rows)
817
+ except Exception as e:
818
+ handle_analysis_error(e)
819
+
820
+
821
+ if __name__ == "__main__":
822
+ main()