2828import de .splatgames .aether .datafixers .api .bootstrap .DataFixerBootstrap ;
2929import de .splatgames .aether .datafixers .api .dynamic .Dynamic ;
3030import de .splatgames .aether .datafixers .api .dynamic .TaggedDynamic ;
31+ import de .splatgames .aether .datafixers .api .diagnostic .DiagnosticContext ;
32+ import de .splatgames .aether .datafixers .api .diagnostic .DiagnosticOptions ;
33+ import de .splatgames .aether .datafixers .api .diagnostic .MigrationReport ;
3134import de .splatgames .aether .datafixers .cli .bootstrap .BootstrapLoader ;
3235import de .splatgames .aether .datafixers .cli .format .FormatHandler ;
3336import de .splatgames .aether .datafixers .cli .format .FormatRegistry ;
3639import de .splatgames .aether .datafixers .core .AetherDataFixer ;
3740import de .splatgames .aether .datafixers .core .bootstrap .DataFixerRuntimeFactory ;
3841import org .jetbrains .annotations .NotNull ;
42+ import org .jetbrains .annotations .Nullable ;
3943import picocli .CommandLine .Command ;
4044import picocli .CommandLine .Option ;
4145import picocli .CommandLine .Parameters ;
@@ -396,6 +400,32 @@ public class MigrateCommand implements Callable<Integer> {
396400 )
397401 private boolean verbose ;
398402
403+ /**
404+ * Whether to enable field-level diagnostics output.
405+ *
406+ * <p>When {@code true}, a detailed diagnostic report is generated for each
407+ * migrated file, including information about every fix execution, rule
408+ * application, and field-level operation (renames, removals, additions,
409+ * transforms, etc.).</p>
410+ *
411+ * <p>Diagnostics output is written alongside the migration report: to
412+ * {@link #reportFile} if specified, or to stderr otherwise. The output
413+ * format follows the selected {@link #reportFormat}.</p>
414+ *
415+ * <p>Default value: {@code false}</p>
416+ *
417+ * <p>CLI usage: {@code --diagnostics}</p>
418+ *
419+ * @see DiagnosticContext
420+ * @see de.splatgames.aether.datafixers.api.diagnostic.FieldOperation
421+ * @since 1.0.0
422+ */
423+ @ Option (
424+ names = {"--diagnostics" },
425+ description = "Enable field-level diagnostics output showing detailed fix and field operation information."
426+ )
427+ private boolean diagnostics ;
428+
399429 /**
400430 * Whether to pretty-print the output JSON.
401431 *
@@ -474,6 +504,8 @@ public Integer call() {
474504 int errorCount = 0 ;
475505 final StringBuilder reportBuilder = new StringBuilder ();
476506
507+ final StringBuilder diagnosticBuilder = new StringBuilder ();
508+
477509 for (final File inputFile : this .inputFiles ) {
478510 try {
479511 final MigrationResult result = processFile (
@@ -483,6 +515,9 @@ public Integer call() {
483515 if (this .generateReport ) {
484516 reportBuilder .append (result .report ).append ("\n " );
485517 }
518+ if (this .diagnostics && !result .diagnosticOutput .isEmpty ()) {
519+ diagnosticBuilder .append (result .diagnosticOutput ).append ("\n " );
520+ }
486521 } catch (final Exception e ) {
487522 errorCount ++;
488523 System .err .println ("Error processing " + inputFile + ": " + e .getMessage ());
@@ -505,6 +540,19 @@ public Integer call() {
505540 }
506541 }
507542
543+ // Write diagnostic output
544+ if (this .diagnostics && !diagnosticBuilder .isEmpty ()) {
545+ final String diagnosticContent = diagnosticBuilder .toString ();
546+ if (this .reportFile != null ) {
547+ Files .writeString (this .reportFile .toPath (), diagnosticContent ,
548+ StandardCharsets .UTF_8 ,
549+ java .nio .file .StandardOpenOption .CREATE ,
550+ java .nio .file .StandardOpenOption .APPEND );
551+ } else {
552+ System .err .println (diagnosticContent );
553+ }
554+ }
555+
508556 // Summary
509557 if (this .inputFiles .size () > 1 || this .verbose ) {
510558 System .err .println ("Completed: " + successCount + " migrated, " + errorCount + " errors" );
@@ -590,15 +638,24 @@ private <T> MigrationResult processFile(
590638 System .err .println ("Skipping " + inputFile + " (already at v"
591639 + sourceVersion .getVersion () + ")" );
592640 }
593- return new MigrationResult ("" , Duration .ZERO );
641+ return new MigrationResult ("" , Duration .ZERO , "" );
594642 }
595643
596644 // Create dynamic and migrate
597645 final Dynamic <T > dynamic = new Dynamic <>(handler .ops (), data );
598646 final TaggedDynamic tagged = new TaggedDynamic (typeRef , dynamic );
599647
600- // Perform migration
601- final TaggedDynamic migrated = fixer .update (tagged , sourceVersion , targetVersion );
648+ // Perform migration (with diagnostics if enabled)
649+ final DiagnosticContext diagCtx = this .diagnostics
650+ ? DiagnosticContext .create (DiagnosticOptions .builder ()
651+ .captureSnapshots (false )
652+ .captureRuleDetails (true )
653+ .captureFieldDetails (true )
654+ .build ())
655+ : null ;
656+ final TaggedDynamic migrated = diagCtx != null
657+ ? fixer .update (tagged , sourceVersion , targetVersion , diagCtx )
658+ : fixer .update (tagged , sourceVersion , targetVersion );
602659
603660 // Extract result
604661 @ SuppressWarnings ("unchecked" )
@@ -631,7 +688,19 @@ private <T> MigrationResult processFile(
631688 );
632689 }
633690
634- return new MigrationResult (report , duration );
691+ // Generate diagnostic report
692+ final MigrationReport diagnosticReport = diagCtx != null ? diagCtx .getReport () : null ;
693+ String diagnosticOutput = "" ;
694+ if (diagnosticReport != null ) {
695+ final ReportFormatter formatter = ReportFormatter .forFormat (this .reportFormat );
696+ diagnosticOutput = formatter .formatDiagnostic (
697+ inputFile .getName (),
698+ typeRef .getId (),
699+ diagnosticReport
700+ );
701+ }
702+
703+ return new MigrationResult (report , duration , diagnosticOutput );
635704 }
636705
637706 /**
@@ -719,6 +788,21 @@ private void writeOutput(@NotNull final File inputFile, @NotNull final String co
719788 * @see #processFile(File, AetherDataFixer, FormatHandler, TypeReference, DataVersion)
720789 * @see ReportFormatter
721790 */
722- private record MigrationResult (String report , Duration duration ) {
791+ /**
792+ * Holds the result of a single file migration operation.
793+ *
794+ * <p>This record captures the outcome of migrating one file, including
795+ * the formatted report string (if reporting is enabled), the time
796+ * taken for the migration, and the optional diagnostic output (if
797+ * {@code --diagnostics} was enabled).</p>
798+ *
799+ * @param report the formatted migration report string, empty if reporting is disabled
800+ * or if the file was skipped (already at target version)
801+ * @param duration the time elapsed during the migration process, including file I/O
802+ * @param diagnosticOutput the formatted diagnostic report string, empty if diagnostics are disabled
803+ * @see #processFile(File, AetherDataFixer, FormatHandler, TypeReference, DataVersion)
804+ * @see ReportFormatter
805+ */
806+ private record MigrationResult (String report , Duration duration , String diagnosticOutput ) {
723807 }
724808}
0 commit comments