attune-ai 2.1.5__py3-none-any.whl → 2.2.1__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.
Files changed (125) hide show
  1. attune/cli/__init__.py +3 -59
  2. attune/cli/commands/batch.py +4 -12
  3. attune/cli/commands/cache.py +8 -16
  4. attune/cli/commands/provider.py +17 -0
  5. attune/cli/commands/routing.py +3 -1
  6. attune/cli/commands/setup.py +122 -0
  7. attune/cli/commands/tier.py +1 -3
  8. attune/cli/commands/workflow.py +31 -0
  9. attune/cli/parsers/cache.py +1 -0
  10. attune/cli/parsers/help.py +1 -3
  11. attune/cli/parsers/provider.py +7 -0
  12. attune/cli/parsers/routing.py +1 -3
  13. attune/cli/parsers/setup.py +7 -0
  14. attune/cli/parsers/status.py +1 -3
  15. attune/cli/parsers/tier.py +1 -3
  16. attune/cli_minimal.py +9 -3
  17. attune/cli_router.py +9 -7
  18. attune/cli_unified.py +3 -0
  19. attune/dashboard/app.py +3 -1
  20. attune/dashboard/simple_server.py +3 -1
  21. attune/dashboard/standalone_server.py +7 -3
  22. attune/mcp/server.py +54 -102
  23. attune/memory/long_term.py +0 -2
  24. attune/memory/short_term/__init__.py +84 -0
  25. attune/memory/short_term/base.py +465 -0
  26. attune/memory/short_term/batch.py +219 -0
  27. attune/memory/short_term/caching.py +227 -0
  28. attune/memory/short_term/conflicts.py +265 -0
  29. attune/memory/short_term/cross_session.py +122 -0
  30. attune/memory/short_term/facade.py +653 -0
  31. attune/memory/short_term/pagination.py +207 -0
  32. attune/memory/short_term/patterns.py +271 -0
  33. attune/memory/short_term/pubsub.py +286 -0
  34. attune/memory/short_term/queues.py +244 -0
  35. attune/memory/short_term/security.py +300 -0
  36. attune/memory/short_term/sessions.py +250 -0
  37. attune/memory/short_term/streams.py +242 -0
  38. attune/memory/short_term/timelines.py +234 -0
  39. attune/memory/short_term/transactions.py +184 -0
  40. attune/memory/short_term/working.py +252 -0
  41. attune/meta_workflows/cli_commands/__init__.py +3 -0
  42. attune/meta_workflows/cli_commands/agent_commands.py +0 -4
  43. attune/meta_workflows/cli_commands/analytics_commands.py +0 -6
  44. attune/meta_workflows/cli_commands/config_commands.py +0 -5
  45. attune/meta_workflows/cli_commands/memory_commands.py +0 -5
  46. attune/meta_workflows/cli_commands/template_commands.py +0 -5
  47. attune/meta_workflows/cli_commands/workflow_commands.py +0 -6
  48. attune/meta_workflows/plan_generator.py +2 -4
  49. attune/models/adaptive_routing.py +4 -8
  50. attune/models/auth_cli.py +3 -9
  51. attune/models/auth_strategy.py +2 -4
  52. attune/models/telemetry/analytics.py +0 -2
  53. attune/models/telemetry/backend.py +0 -3
  54. attune/models/telemetry/storage.py +0 -2
  55. attune/monitoring/alerts.py +6 -10
  56. attune/orchestration/_strategies/__init__.py +156 -0
  57. attune/orchestration/_strategies/base.py +227 -0
  58. attune/orchestration/_strategies/conditional_strategies.py +365 -0
  59. attune/orchestration/_strategies/conditions.py +369 -0
  60. attune/orchestration/_strategies/core_strategies.py +479 -0
  61. attune/orchestration/_strategies/data_classes.py +64 -0
  62. attune/orchestration/_strategies/nesting.py +233 -0
  63. attune/orchestration/execution_strategies.py +58 -1567
  64. attune/orchestration/meta_orchestrator.py +1 -3
  65. attune/project_index/scanner.py +1 -3
  66. attune/project_index/scanner_parallel.py +7 -5
  67. attune/socratic/storage.py +2 -4
  68. attune/socratic_router.py +1 -3
  69. attune/telemetry/agent_coordination.py +9 -3
  70. attune/telemetry/agent_tracking.py +16 -3
  71. attune/telemetry/approval_gates.py +22 -5
  72. attune/telemetry/cli.py +1 -3
  73. attune/telemetry/commands/dashboard_commands.py +24 -8
  74. attune/telemetry/event_streaming.py +8 -2
  75. attune/telemetry/feedback_loop.py +10 -2
  76. attune/tools.py +2 -1
  77. attune/workflow_commands.py +1 -3
  78. attune/workflow_patterns/structural.py +4 -8
  79. attune/workflows/__init__.py +54 -10
  80. attune/workflows/autonomous_test_gen.py +158 -102
  81. attune/workflows/base.py +48 -672
  82. attune/workflows/batch_processing.py +1 -3
  83. attune/workflows/compat.py +156 -0
  84. attune/workflows/cost_mixin.py +141 -0
  85. attune/workflows/data_classes.py +92 -0
  86. attune/workflows/document_gen/workflow.py +11 -14
  87. attune/workflows/history.py +16 -9
  88. attune/workflows/llm_base.py +1 -3
  89. attune/workflows/migration.py +432 -0
  90. attune/workflows/output.py +2 -7
  91. attune/workflows/parsing_mixin.py +427 -0
  92. attune/workflows/perf_audit.py +3 -1
  93. attune/workflows/progress.py +9 -11
  94. attune/workflows/release_prep.py +5 -1
  95. attune/workflows/routing.py +0 -2
  96. attune/workflows/secure_release.py +4 -1
  97. attune/workflows/security_audit.py +20 -14
  98. attune/workflows/security_audit_phase3.py +28 -22
  99. attune/workflows/seo_optimization.py +27 -27
  100. attune/workflows/test_gen/test_templates.py +1 -4
  101. attune/workflows/test_gen/workflow.py +0 -2
  102. attune/workflows/test_gen_behavioral.py +6 -19
  103. attune/workflows/test_gen_parallel.py +8 -6
  104. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/METADATA +4 -3
  105. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/RECORD +121 -96
  106. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/entry_points.txt +0 -2
  107. attune_healthcare/monitors/monitoring/__init__.py +9 -9
  108. attune_llm/agent_factory/__init__.py +6 -6
  109. attune_llm/agent_factory/adapters/haystack_adapter.py +1 -4
  110. attune_llm/commands/__init__.py +10 -10
  111. attune_llm/commands/models.py +3 -3
  112. attune_llm/config/__init__.py +8 -8
  113. attune_llm/learning/__init__.py +3 -3
  114. attune_llm/learning/extractor.py +5 -3
  115. attune_llm/learning/storage.py +5 -3
  116. attune_llm/security/__init__.py +17 -17
  117. attune_llm/utils/tokens.py +3 -1
  118. attune/cli_legacy.py +0 -3978
  119. attune/memory/short_term.py +0 -2192
  120. attune/workflows/manage_docs.py +0 -87
  121. attune/workflows/test5.py +0 -125
  122. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/WHEEL +0 -0
  123. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/licenses/LICENSE +0 -0
  124. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
  125. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,432 @@
1
+ """Workflow Migration System
2
+
3
+ Environment-aware workflow migration with config-driven preferences.
4
+ Helps users transition from deprecated workflows to consolidated versions.
5
+
6
+ Behavior:
7
+ - CI/Non-interactive: Silent aliasing with log message
8
+ - First interactive use: Show dialog with "remember" option
9
+ - Subsequent uses: Follow stored preference
10
+ - Config override: Users can set migration mode globally
11
+
12
+ Usage:
13
+ from attune.workflows.migration import resolve_workflow_migration
14
+
15
+ # Returns (canonical_name, kwargs, was_migrated)
16
+ name, kwargs, migrated = resolve_workflow_migration("test-gen-behavioral")
17
+
18
+ Copyright 2026 Smart-AI-Memory
19
+ Licensed under Fair Source License 0.9
20
+ """
21
+
22
+ import json
23
+ import logging
24
+ import os
25
+ import sys
26
+ from dataclasses import dataclass, field
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # Migration mode options
33
+ MIGRATION_MODE_PROMPT = "prompt" # Ask user on first use
34
+ MIGRATION_MODE_AUTO = "auto" # Silently use new syntax
35
+ MIGRATION_MODE_LEGACY = "legacy" # Keep using old syntax (deprecated)
36
+
37
+ # Workflow aliases: old_name -> (new_name, default_kwargs)
38
+ WORKFLOW_ALIASES: dict[str, tuple[str, dict[str, Any]]] = {
39
+ # Code Review consolidation
40
+ "pro-review": ("code-review", {"mode": "premium"}),
41
+ # Test Generation consolidation
42
+ "test-gen-behavioral": ("test-gen", {"style": "behavioral"}),
43
+ "test-coverage-boost": ("test-gen", {"target": "coverage"}),
44
+ "autonomous-test-gen": ("test-gen-parallel", {"autonomous": True}),
45
+ "progressive-test-gen": ("test-gen-parallel", {"progressive": True}),
46
+ # Release consolidation
47
+ "secure-release": ("release-prep", {"mode": "secure"}),
48
+ "orchestrated-release-prep": ("release-prep", {"mode": "full"}),
49
+ # Deprecated (show warning, map to replacement)
50
+ "release-prep-legacy": ("release-prep", {"mode": "standard", "_deprecated": True}),
51
+ "document-manager": ("doc-gen", {"_deprecated": True}),
52
+ # Removed (show error with migration path)
53
+ "test5": (
54
+ None,
55
+ {"_removed": True, "_message": "test5 was a test artifact and has been removed"},
56
+ ),
57
+ "manage-docs": (
58
+ None,
59
+ {
60
+ "_removed": True,
61
+ "_message": "manage-docs was incomplete. Use doc-gen or doc-orchestrator",
62
+ },
63
+ ),
64
+ }
65
+
66
+ # Human-readable descriptions for the dialog
67
+ WORKFLOW_DESCRIPTIONS: dict[str, str] = {
68
+ "code-review": "Comprehensive code analysis with optional premium mode",
69
+ "test-gen": "Generate tests with style (standard/behavioral) and target (gaps/coverage) options",
70
+ "test-gen-parallel": "Batch test generation with autonomous and progressive modes",
71
+ "release-prep": "Release readiness with modes: standard, secure, or full",
72
+ "doc-gen": "Generate documentation for code",
73
+ }
74
+
75
+
76
+ @dataclass
77
+ class MigrationConfig:
78
+ """Configuration for workflow migration behavior.
79
+
80
+ Attributes:
81
+ mode: How to handle migrations (prompt, auto, legacy)
82
+ remembered_choices: User's choices for specific workflows
83
+ show_tips: Whether to show migration tips after workflow runs
84
+ last_prompted: Track when workflows were last prompted
85
+ """
86
+
87
+ mode: str = MIGRATION_MODE_PROMPT
88
+ remembered_choices: dict[str, str] = field(default_factory=dict)
89
+ show_tips: bool = True
90
+ last_prompted: dict[str, str] = field(default_factory=dict)
91
+
92
+ @classmethod
93
+ def load(cls) -> "MigrationConfig":
94
+ """Load migration config from .attune/migration.json"""
95
+ config_path = Path.cwd() / ".attune" / "migration.json"
96
+
97
+ if config_path.exists():
98
+ try:
99
+ with config_path.open() as f:
100
+ data = json.load(f)
101
+ return cls(
102
+ mode=data.get("mode", MIGRATION_MODE_PROMPT),
103
+ remembered_choices=data.get("remembered_choices", {}),
104
+ show_tips=data.get("show_tips", True),
105
+ last_prompted=data.get("last_prompted", {}),
106
+ )
107
+ except (json.JSONDecodeError, OSError) as e:
108
+ logger.warning(f"Failed to load migration config: {e}")
109
+
110
+ return cls()
111
+
112
+ def save(self) -> None:
113
+ """Save migration config to .attune/migration.json"""
114
+ config_dir = Path.cwd() / ".attune"
115
+ config_dir.mkdir(exist_ok=True)
116
+ config_path = config_dir / "migration.json"
117
+
118
+ try:
119
+ with config_path.open("w") as f:
120
+ json.dump(
121
+ {
122
+ "mode": self.mode,
123
+ "remembered_choices": self.remembered_choices,
124
+ "show_tips": self.show_tips,
125
+ "last_prompted": self.last_prompted,
126
+ },
127
+ f,
128
+ indent=2,
129
+ )
130
+ except OSError as e:
131
+ logger.warning(f"Failed to save migration config: {e}")
132
+
133
+ def remember_choice(self, old_name: str, choice: str) -> None:
134
+ """Remember user's migration choice for a workflow."""
135
+ self.remembered_choices[old_name] = choice
136
+ self.save()
137
+
138
+
139
+ def is_interactive() -> bool:
140
+ """Check if we're running in an interactive terminal.
141
+
142
+ Returns False in CI environments or when stdin is not a TTY.
143
+ """
144
+ # Check common CI environment variables
145
+ ci_vars = ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL", "CIRCLECI", "TRAVIS"]
146
+ if any(os.getenv(var) for var in ci_vars):
147
+ return False
148
+
149
+ # Check for explicit non-interactive flag
150
+ if os.getenv("ATTUNE_NO_INTERACTIVE"):
151
+ return False
152
+
153
+ # Check if stdin is a TTY
154
+ return sys.stdin.isatty()
155
+
156
+
157
+ def show_migration_dialog(
158
+ old_name: str, new_name: str, kwargs: dict[str, Any]
159
+ ) -> tuple[str, dict[str, Any], bool]:
160
+ """Show interactive migration dialog to user.
161
+
162
+ Args:
163
+ old_name: The deprecated workflow name
164
+ new_name: The new canonical workflow name
165
+ kwargs: Default kwargs for the new workflow
166
+
167
+ Returns:
168
+ Tuple of (chosen_name, chosen_kwargs, should_remember)
169
+ """
170
+ # Build the new command string
171
+ flag_parts = []
172
+ for key, value in kwargs.items():
173
+ if key.startswith("_"):
174
+ continue
175
+ if isinstance(value, bool) and value:
176
+ flag_parts.append(f"--{key.replace('_', '-')}")
177
+ else:
178
+ flag_parts.append(f"--{key.replace('_', '-')} {value}")
179
+
180
+ new_command = f"attune workflow run {new_name}"
181
+ if flag_parts:
182
+ new_command += " " + " ".join(flag_parts)
183
+
184
+ description = WORKFLOW_DESCRIPTIONS.get(new_name, "")
185
+
186
+ # Print dialog
187
+ print()
188
+ print("┌" + "─" * 68 + "┐")
189
+ print("│" + " Workflow Migration ".center(68) + "│")
190
+ print("├" + "─" * 68 + "┤")
191
+ print(f"│ '{old_name}' has been consolidated into '{new_name}'".ljust(69) + "│")
192
+ if description:
193
+ print(f"│ {description[:64]}".ljust(69) + "│")
194
+ print("│" + " " * 68 + "│")
195
+ print("│ New syntax:".ljust(69) + "│")
196
+ print(f"│ {new_command[:62]}".ljust(69) + "│")
197
+ print("│" + " " * 68 + "│")
198
+ print("│ How would you like to proceed?".ljust(69) + "│")
199
+ print("│" + " " * 68 + "│")
200
+ print("│ 1. Use new syntax (recommended)".ljust(69) + "│")
201
+ print("│ 2. Continue with legacy behavior (deprecated)".ljust(69) + "│")
202
+ print("│ 3. Always use new syntax (don't ask again)".ljust(69) + "│")
203
+ print("│ 4. Always use legacy (don't ask again)".ljust(69) + "│")
204
+ print("└" + "─" * 68 + "┘")
205
+ print()
206
+
207
+ while True:
208
+ try:
209
+ choice = input("Enter choice [1-4]: ").strip()
210
+
211
+ if choice == "1":
212
+ return new_name, kwargs, False
213
+ elif choice == "2":
214
+ return old_name, {}, False
215
+ elif choice == "3":
216
+ return new_name, kwargs, True # Remember: use new
217
+ elif choice == "4":
218
+ return old_name, {}, True # Remember: use legacy
219
+ else:
220
+ print("Please enter 1, 2, 3, or 4")
221
+ except (EOFError, KeyboardInterrupt):
222
+ # User cancelled - default to new syntax
223
+ print("\nDefaulting to new syntax...")
224
+ return new_name, kwargs, False
225
+
226
+
227
+ def show_removed_workflow_error(old_name: str, message: str) -> None:
228
+ """Show error for removed workflows with migration guidance."""
229
+ print()
230
+ print("┌" + "─" * 68 + "┐")
231
+ print("│" + " Workflow Removed ".center(68) + "│")
232
+ print("├" + "─" * 68 + "┤")
233
+ print(f"│ '{old_name}' is no longer available.".ljust(69) + "│")
234
+ print("│" + " " * 68 + "│")
235
+ # Word wrap the message
236
+ words = message.split()
237
+ line = "│ "
238
+ for word in words:
239
+ if len(line) + len(word) + 1 > 67:
240
+ print(line.ljust(69) + "│")
241
+ line = "│ " + word + " "
242
+ else:
243
+ line += word + " "
244
+ if line.strip() != "│":
245
+ print(line.ljust(69) + "│")
246
+ print("│" + " " * 68 + "│")
247
+ print("│ Run 'attune workflow list' to see available workflows.".ljust(69) + "│")
248
+ print("└" + "─" * 68 + "┘")
249
+ print()
250
+
251
+
252
+ def show_deprecation_warning(old_name: str, new_name: str, kwargs: dict[str, Any]) -> None:
253
+ """Show deprecation warning (non-blocking)."""
254
+ flag_parts = []
255
+ for key, value in kwargs.items():
256
+ if key.startswith("_"):
257
+ continue
258
+ if isinstance(value, bool) and value:
259
+ flag_parts.append(f"--{key.replace('_', '-')}")
260
+ else:
261
+ flag_parts.append(f"--{key.replace('_', '-')} {value}")
262
+
263
+ new_command = f"attune workflow run {new_name}"
264
+ if flag_parts:
265
+ new_command += " " + " ".join(flag_parts)
266
+
267
+ logger.warning(
268
+ f"'{old_name}' is deprecated. Use '{new_command}' instead. "
269
+ f"Set ATTUNE_NO_INTERACTIVE=1 to suppress this warning in CI."
270
+ )
271
+
272
+
273
+ def resolve_workflow_migration(
274
+ workflow_name: str,
275
+ config: MigrationConfig | None = None,
276
+ ) -> tuple[str, dict[str, Any], bool]:
277
+ """Resolve a workflow name, handling migrations as needed.
278
+
279
+ This is the main entry point for the migration system.
280
+
281
+ Args:
282
+ workflow_name: The workflow name from user input
283
+ config: Optional migration config (loads from file if not provided)
284
+
285
+ Returns:
286
+ Tuple of:
287
+ - resolved_name: The canonical workflow name to use
288
+ - kwargs: Additional kwargs to pass to the workflow
289
+ - was_migrated: True if the workflow was migrated
290
+
291
+ Raises:
292
+ SystemExit: If workflow has been removed
293
+ """
294
+ # Not an aliased workflow - return as-is
295
+ if workflow_name not in WORKFLOW_ALIASES:
296
+ return workflow_name, {}, False
297
+
298
+ new_name, kwargs = WORKFLOW_ALIASES[workflow_name]
299
+
300
+ # Handle removed workflows
301
+ if kwargs.get("_removed"):
302
+ message = kwargs.get("_message", "This workflow has been removed.")
303
+ if is_interactive():
304
+ show_removed_workflow_error(workflow_name, message)
305
+ else:
306
+ logger.error(f"Workflow '{workflow_name}' has been removed: {message}")
307
+ raise SystemExit(1)
308
+
309
+ # Load config if not provided
310
+ if config is None:
311
+ config = MigrationConfig.load()
312
+
313
+ # Check if user has a remembered choice
314
+ if workflow_name in config.remembered_choices:
315
+ remembered = config.remembered_choices[workflow_name]
316
+ if remembered == "new":
317
+ logger.debug(f"Using remembered choice: {new_name}")
318
+ return new_name, kwargs, True
319
+ elif remembered == "legacy":
320
+ logger.debug(f"Using remembered choice: {workflow_name} (legacy)")
321
+ return workflow_name, {}, True
322
+
323
+ # Handle based on mode and environment
324
+ is_deprecated = kwargs.get("_deprecated", False)
325
+
326
+ if not is_interactive():
327
+ # CI/non-interactive: silent aliasing with log
328
+ if config.mode == MIGRATION_MODE_LEGACY:
329
+ logger.info(f"Using legacy workflow '{workflow_name}' (migration mode: legacy)")
330
+ return workflow_name, {}, False
331
+ else:
332
+ show_deprecation_warning(workflow_name, new_name, kwargs)
333
+ return new_name, kwargs, True
334
+
335
+ # Interactive mode
336
+ if config.mode == MIGRATION_MODE_AUTO:
337
+ # Auto-migrate without prompting
338
+ show_deprecation_warning(workflow_name, new_name, kwargs)
339
+ return new_name, kwargs, True
340
+
341
+ elif config.mode == MIGRATION_MODE_LEGACY:
342
+ # Use legacy behavior
343
+ if is_deprecated:
344
+ show_deprecation_warning(workflow_name, new_name, kwargs)
345
+ return workflow_name, {}, False
346
+
347
+ else: # MIGRATION_MODE_PROMPT
348
+ # Show interactive dialog
349
+ chosen_name, chosen_kwargs, remember = show_migration_dialog(
350
+ workflow_name, new_name, kwargs
351
+ )
352
+
353
+ if remember:
354
+ if chosen_name == new_name:
355
+ config.remember_choice(workflow_name, "new")
356
+ else:
357
+ config.remember_choice(workflow_name, "legacy")
358
+
359
+ was_migrated = chosen_name == new_name
360
+ return chosen_name, chosen_kwargs, was_migrated
361
+
362
+
363
+ def show_migration_tip(old_name: str, new_name: str, kwargs: dict[str, Any]) -> None:
364
+ """Show a migration tip after workflow completion.
365
+
366
+ Called at the end of a workflow run to educate users.
367
+ """
368
+ config = MigrationConfig.load()
369
+ if not config.show_tips:
370
+ return
371
+
372
+ if not is_interactive():
373
+ return
374
+
375
+ # Build command
376
+ flag_parts = []
377
+ for key, value in kwargs.items():
378
+ if key.startswith("_"):
379
+ continue
380
+ if isinstance(value, bool) and value:
381
+ flag_parts.append(f"--{key.replace('_', '-')}")
382
+ else:
383
+ flag_parts.append(f"--{key.replace('_', '-')} {value}")
384
+
385
+ new_command = f"attune workflow run {new_name}"
386
+ if flag_parts:
387
+ new_command += " " + " ".join(flag_parts)
388
+
389
+ print()
390
+ print(f"💡 Tip: '{old_name}' is now '{new_command}'")
391
+ print(" Run 'attune config set workflow.migration.mode auto' to auto-migrate")
392
+ print(" Run 'attune config set workflow.migration.show_tips false' to hide tips")
393
+ print()
394
+
395
+
396
+ def get_canonical_workflow_name(workflow_name: str) -> str:
397
+ """Get the canonical (new) name for a workflow.
398
+
399
+ Useful for documentation and help text.
400
+ """
401
+ if workflow_name in WORKFLOW_ALIASES:
402
+ new_name, _ = WORKFLOW_ALIASES[workflow_name]
403
+ return new_name if new_name else workflow_name
404
+ return workflow_name
405
+
406
+
407
+ def list_migrations() -> list[dict[str, Any]]:
408
+ """List all workflow migrations for documentation.
409
+
410
+ Returns:
411
+ List of migration info dicts
412
+ """
413
+ migrations = []
414
+ for old_name, (new_name, kwargs) in WORKFLOW_ALIASES.items():
415
+ if new_name is None:
416
+ status = "removed"
417
+ elif kwargs.get("_deprecated"):
418
+ status = "deprecated"
419
+ else:
420
+ status = "consolidated"
421
+
422
+ migrations.append(
423
+ {
424
+ "old_name": old_name,
425
+ "new_name": new_name,
426
+ "status": status,
427
+ "kwargs": {k: v for k, v in kwargs.items() if not k.startswith("_")},
428
+ "message": kwargs.get("_message", ""),
429
+ }
430
+ )
431
+
432
+ return migrations
@@ -142,9 +142,7 @@ class WorkflowReport:
142
142
  }.get(section.style, "blue")
143
143
 
144
144
  if isinstance(section.content, str):
145
- console.print(
146
- Panel(section.content, title=section.title, border_style=border_style)
147
- )
145
+ console.print(Panel(section.content, title=section.title, border_style=border_style))
148
146
  elif isinstance(section.content, list) and all(
149
147
  isinstance(f, Finding) for f in section.content
150
148
  ):
@@ -325,10 +323,7 @@ class MetricsPanel:
325
323
  Rich Panel with formatted score
326
324
  """
327
325
  if not RICH_AVAILABLE or Panel is None:
328
- raise RuntimeError(
329
- "Rich library not available. "
330
- "Install with: pip install rich"
331
- )
326
+ raise RuntimeError("Rich library not available. " "Install with: pip install rich")
332
327
 
333
328
  style = cls.get_style(score)
334
329
  icon = cls.get_icon(score)