codetour-cli 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.
- codetour_cli/__init__.py +9 -0
- codetour_cli/cli.py +2087 -0
- codetour_cli/config.py +144 -0
- codetour_cli/description_validator.py +197 -0
- codetour_cli/detection.py +91 -0
- codetour_cli/discovery.py +226 -0
- codetour_cli/lint.py +334 -0
- codetour_cli/logging/__init__.py +6 -0
- codetour_cli/logging/setup.py +63 -0
- codetour_cli/migration/__init__.py +6 -0
- codetour_cli/migration/direct.py +385 -0
- codetour_cli/migration/git_diff.py +418 -0
- codetour_cli/migration/stepwise.py +400 -0
- codetour_cli/review_parser.py +348 -0
- codetour_cli/schema_models.py +203 -0
- codetour_cli/tour/__init__.py +6 -0
- codetour_cli/tour/schema.py +255 -0
- codetour_cli/tour/updater.py +428 -0
- codetour_cli-0.1.0.dist-info/METADATA +47 -0
- codetour_cli-0.1.0.dist-info/RECORD +24 -0
- codetour_cli-0.1.0.dist-info/WHEEL +5 -0
- codetour_cli-0.1.0.dist-info/entry_points.txt +2 -0
- codetour_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- codetour_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Step-by-step migration mode: A → B → C (sequential).
|
|
3
|
+
|
|
4
|
+
Migrates CodeTour steps by replaying commits sequentially, applying
|
|
5
|
+
each commit's changes one at a time. More accurate than direct mode
|
|
6
|
+
for complex refactorings, but slower for long commit histories.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Optional, List, Tuple
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import git
|
|
14
|
+
import structlog
|
|
15
|
+
|
|
16
|
+
from codetour_cli.migration.direct import DirectMigrator, MigrationResult
|
|
17
|
+
from codetour_cli.migration.git_diff import GitDiffParser
|
|
18
|
+
from codetour_cli.tour.schema import TourStep
|
|
19
|
+
|
|
20
|
+
logger = structlog.get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class StepwiseResult:
|
|
25
|
+
"""
|
|
26
|
+
Result of step-by-step migration with full trace.
|
|
27
|
+
|
|
28
|
+
Contains the final migration result plus intermediate steps
|
|
29
|
+
and comparison with direct mode.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# Final result
|
|
33
|
+
final_result: MigrationResult
|
|
34
|
+
|
|
35
|
+
# Intermediate migrations (one per commit)
|
|
36
|
+
intermediate_results: List[MigrationResult] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
# Direct mode comparison (for data collection)
|
|
39
|
+
direct_result: Optional[MigrationResult] = None
|
|
40
|
+
methods_disagree: bool = False
|
|
41
|
+
|
|
42
|
+
# Commit path taken
|
|
43
|
+
commit_path: List[str] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def num_steps(self) -> int:
|
|
47
|
+
"""Number of intermediate steps taken."""
|
|
48
|
+
return len(self.intermediate_results)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def confidence_history(self) -> List[float]:
|
|
52
|
+
"""Confidence at each step."""
|
|
53
|
+
return [r.confidence for r in self.intermediate_results]
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def min_confidence(self) -> float:
|
|
57
|
+
"""Minimum confidence across all steps."""
|
|
58
|
+
if not self.intermediate_results:
|
|
59
|
+
return 1.0
|
|
60
|
+
return min(self.confidence_history)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class StepwiseMigrator:
|
|
64
|
+
"""
|
|
65
|
+
Step-by-step migration engine: A → B → C.
|
|
66
|
+
|
|
67
|
+
Migrates tour steps by replaying commits sequentially, tracking
|
|
68
|
+
line movements through each intermediate state.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
repo: git.Repo,
|
|
74
|
+
source_commit, # str or git.Commit
|
|
75
|
+
target_commit, # str or git.Commit
|
|
76
|
+
diff_algorithm: str = "histogram",
|
|
77
|
+
compute_direct_comparison: bool = True
|
|
78
|
+
):
|
|
79
|
+
"""
|
|
80
|
+
Initialize the step-by-step migrator.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
repo: GitPython repository object
|
|
84
|
+
source_commit: Source commit SHA or Commit object
|
|
85
|
+
target_commit: Target commit SHA or Commit object
|
|
86
|
+
diff_algorithm: Git diff algorithm ('myers', 'patience', 'histogram')
|
|
87
|
+
compute_direct_comparison: Also compute direct mode for comparison
|
|
88
|
+
"""
|
|
89
|
+
self.repo = repo
|
|
90
|
+
|
|
91
|
+
# Convert Commit objects to strings
|
|
92
|
+
self.source_commit = (source_commit.hexsha if hasattr(source_commit, 'hexsha')
|
|
93
|
+
else source_commit)
|
|
94
|
+
self.target_commit = (target_commit.hexsha if hasattr(target_commit, 'hexsha')
|
|
95
|
+
else target_commit)
|
|
96
|
+
|
|
97
|
+
self.diff_algorithm = diff_algorithm
|
|
98
|
+
self.compute_direct = compute_direct_comparison
|
|
99
|
+
|
|
100
|
+
self.log = logger.bind(
|
|
101
|
+
source=self.source_commit[:8],
|
|
102
|
+
target=self.target_commit[:8],
|
|
103
|
+
mode="stepwise"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _get_commit_path(self) -> List[str]:
|
|
107
|
+
"""
|
|
108
|
+
Get the sequential path of commits from source to target.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of commit SHAs from source to target (inclusive)
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
# Use git rev-list to get commit path
|
|
115
|
+
# This gives us A, B1, B2, ..., C in order
|
|
116
|
+
commits = list(self.repo.iter_commits(
|
|
117
|
+
f"{self.source_commit}..{self.target_commit}"
|
|
118
|
+
))
|
|
119
|
+
|
|
120
|
+
# Reverse to get chronological order (oldest to newest)
|
|
121
|
+
commits.reverse()
|
|
122
|
+
|
|
123
|
+
# Convert to SHAs and include source
|
|
124
|
+
commit_path = [self.source_commit] + [c.hexsha for c in commits]
|
|
125
|
+
|
|
126
|
+
self.log.info(
|
|
127
|
+
"computed_commit_path",
|
|
128
|
+
num_commits=len(commit_path),
|
|
129
|
+
path_length=len(commit_path) - 1 # Number of steps
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return commit_path
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
self.log.error(
|
|
136
|
+
"failed_to_get_commit_path",
|
|
137
|
+
error=str(e)
|
|
138
|
+
)
|
|
139
|
+
# Fall back to direct path
|
|
140
|
+
return [self.source_commit, self.target_commit]
|
|
141
|
+
|
|
142
|
+
def migrate_step(
|
|
143
|
+
self,
|
|
144
|
+
file_path: str,
|
|
145
|
+
line_number: int,
|
|
146
|
+
step_id: Optional[int] = None,
|
|
147
|
+
step: Optional[TourStep] = None
|
|
148
|
+
) -> StepwiseResult:
|
|
149
|
+
"""
|
|
150
|
+
Migrate a single tour step through sequential commits.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
file_path: File path in source commit
|
|
154
|
+
line_number: Line number in source commit (1-indexed)
|
|
155
|
+
step_id: Optional step ID for logging
|
|
156
|
+
step: Optional TourStep for pattern validation
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
StepwiseResult with final location and full trace
|
|
160
|
+
"""
|
|
161
|
+
log = self.log.bind(
|
|
162
|
+
file=file_path,
|
|
163
|
+
line=line_number,
|
|
164
|
+
step_id=step_id
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
log.info("starting_stepwise_migration")
|
|
168
|
+
|
|
169
|
+
# Get commit path
|
|
170
|
+
commit_path = self._get_commit_path()
|
|
171
|
+
|
|
172
|
+
if len(commit_path) < 2:
|
|
173
|
+
log.warning("commit_path_too_short", path_length=len(commit_path))
|
|
174
|
+
# Fall back to single direct migration
|
|
175
|
+
migrator = DirectMigrator(
|
|
176
|
+
self.repo,
|
|
177
|
+
self.source_commit,
|
|
178
|
+
self.target_commit,
|
|
179
|
+
self.diff_algorithm
|
|
180
|
+
)
|
|
181
|
+
final_result = migrator.migrate_step(file_path, line_number, step_id, step)
|
|
182
|
+
return StepwiseResult(
|
|
183
|
+
final_result=final_result,
|
|
184
|
+
commit_path=commit_path
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Track current location as we migrate through commits
|
|
188
|
+
current_file = file_path
|
|
189
|
+
current_line = line_number
|
|
190
|
+
intermediate_results = []
|
|
191
|
+
ever_renamed = False # Track if file was renamed at any point
|
|
192
|
+
|
|
193
|
+
# Migrate through each consecutive pair of commits
|
|
194
|
+
for i in range(len(commit_path) - 1):
|
|
195
|
+
commit_a = commit_path[i]
|
|
196
|
+
commit_b = commit_path[i + 1]
|
|
197
|
+
|
|
198
|
+
log.debug(
|
|
199
|
+
"migrating_step",
|
|
200
|
+
step=i + 1,
|
|
201
|
+
total_steps=len(commit_path) - 1,
|
|
202
|
+
from_commit=commit_a[:8],
|
|
203
|
+
to_commit=commit_b[:8],
|
|
204
|
+
current_location=f"{current_file}:{current_line}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Migrate this single step
|
|
208
|
+
migrator = DirectMigrator(
|
|
209
|
+
self.repo,
|
|
210
|
+
commit_a,
|
|
211
|
+
commit_b,
|
|
212
|
+
self.diff_algorithm
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
result = migrator.migrate_step(
|
|
216
|
+
current_file,
|
|
217
|
+
current_line,
|
|
218
|
+
step_id=step_id,
|
|
219
|
+
step=step
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
intermediate_results.append(result)
|
|
223
|
+
|
|
224
|
+
# Track if file was renamed at any point
|
|
225
|
+
if result.file_renamed:
|
|
226
|
+
ever_renamed = True
|
|
227
|
+
|
|
228
|
+
# Check if migration failed (file deleted, etc.)
|
|
229
|
+
if not result.success:
|
|
230
|
+
log.warning(
|
|
231
|
+
"migration_failed_at_intermediate_step",
|
|
232
|
+
step=i + 1,
|
|
233
|
+
reason=result.method,
|
|
234
|
+
commit=commit_b[:8]
|
|
235
|
+
)
|
|
236
|
+
# Can't continue - return current state
|
|
237
|
+
return StepwiseResult(
|
|
238
|
+
final_result=result,
|
|
239
|
+
intermediate_results=intermediate_results,
|
|
240
|
+
commit_path=commit_path[:i + 2] # Up to failure point
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Update current location for next iteration
|
|
244
|
+
current_file = result.new_file
|
|
245
|
+
current_line = result.new_line
|
|
246
|
+
|
|
247
|
+
log.debug(
|
|
248
|
+
"step_migrated",
|
|
249
|
+
step=i + 1,
|
|
250
|
+
new_location=f"{current_file}:{current_line}",
|
|
251
|
+
confidence=result.confidence,
|
|
252
|
+
method=result.method
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Final result is the last intermediate result
|
|
256
|
+
final_result = intermediate_results[-1]
|
|
257
|
+
|
|
258
|
+
# Compute aggregate confidence (minimum across all steps)
|
|
259
|
+
min_confidence = min(r.confidence for r in intermediate_results)
|
|
260
|
+
avg_confidence = sum(r.confidence for r in intermediate_results) / len(intermediate_results)
|
|
261
|
+
|
|
262
|
+
# Adjust final confidence based on journey
|
|
263
|
+
# Use average but flag if any step had low confidence
|
|
264
|
+
final_result.confidence = avg_confidence
|
|
265
|
+
if min_confidence < 0.7:
|
|
266
|
+
final_result.needs_review = True
|
|
267
|
+
final_result.explanation += f" (min confidence {min_confidence:.2f})"
|
|
268
|
+
|
|
269
|
+
# Set file_renamed flag if file was renamed at any point in the journey
|
|
270
|
+
if ever_renamed:
|
|
271
|
+
final_result.file_renamed = True
|
|
272
|
+
|
|
273
|
+
log.info(
|
|
274
|
+
"stepwise_migration_complete",
|
|
275
|
+
final_location=f"{final_result.new_file}:{final_result.new_line}",
|
|
276
|
+
num_steps=len(intermediate_results),
|
|
277
|
+
min_confidence=min_confidence,
|
|
278
|
+
avg_confidence=avg_confidence
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Optionally compute direct mode for comparison
|
|
282
|
+
direct_result = None
|
|
283
|
+
methods_disagree = False
|
|
284
|
+
|
|
285
|
+
if self.compute_direct:
|
|
286
|
+
direct_migrator = DirectMigrator(
|
|
287
|
+
self.repo,
|
|
288
|
+
self.source_commit,
|
|
289
|
+
self.target_commit,
|
|
290
|
+
self.diff_algorithm
|
|
291
|
+
)
|
|
292
|
+
direct_result = direct_migrator.migrate_step(
|
|
293
|
+
file_path,
|
|
294
|
+
line_number,
|
|
295
|
+
step_id,
|
|
296
|
+
step
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Check if methods disagree
|
|
300
|
+
if (final_result.new_file != direct_result.new_file or
|
|
301
|
+
final_result.new_line != direct_result.new_line):
|
|
302
|
+
methods_disagree = True
|
|
303
|
+
|
|
304
|
+
log.warning(
|
|
305
|
+
"methods_disagree",
|
|
306
|
+
stepwise_result=f"{final_result.new_file}:{final_result.new_line}",
|
|
307
|
+
direct_result=f"{direct_result.new_file}:{direct_result.new_line}",
|
|
308
|
+
stepwise_confidence=final_result.confidence,
|
|
309
|
+
direct_confidence=direct_result.confidence
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Lower confidence when methods disagree (per ADR-0001)
|
|
313
|
+
final_result.confidence *= 0.9
|
|
314
|
+
final_result.explanation += " (methods disagree)"
|
|
315
|
+
final_result.needs_review = True
|
|
316
|
+
|
|
317
|
+
return StepwiseResult(
|
|
318
|
+
final_result=final_result,
|
|
319
|
+
intermediate_results=intermediate_results,
|
|
320
|
+
direct_result=direct_result,
|
|
321
|
+
methods_disagree=methods_disagree,
|
|
322
|
+
commit_path=commit_path
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def migrate_steps(
|
|
326
|
+
self,
|
|
327
|
+
steps: List[Tuple[str, int, int]]
|
|
328
|
+
) -> List[StepwiseResult]:
|
|
329
|
+
"""
|
|
330
|
+
Migrate multiple tour steps through sequential commits.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
steps: List of (file_path, line_number, step_id) tuples
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
List of StepwiseResult objects
|
|
337
|
+
"""
|
|
338
|
+
self.log.info("migrating_steps_stepwise", num_steps=len(steps))
|
|
339
|
+
|
|
340
|
+
results = []
|
|
341
|
+
for file_path, line_number, step_id in steps:
|
|
342
|
+
result = self.migrate_step(file_path, line_number, step_id)
|
|
343
|
+
results.append(result)
|
|
344
|
+
|
|
345
|
+
# Log summary
|
|
346
|
+
successful = sum(1 for r in results if r.final_result.success)
|
|
347
|
+
disagreements = sum(1 for r in results if r.methods_disagree)
|
|
348
|
+
|
|
349
|
+
self.log.info(
|
|
350
|
+
"stepwise_migration_summary",
|
|
351
|
+
total=len(results),
|
|
352
|
+
successful=successful,
|
|
353
|
+
methods_disagree=disagreements
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return results
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def should_use_stepwise(
|
|
360
|
+
repo: git.Repo,
|
|
361
|
+
source_commit: str,
|
|
362
|
+
target_commit: str,
|
|
363
|
+
threshold: int = 10
|
|
364
|
+
) -> bool:
|
|
365
|
+
"""
|
|
366
|
+
Determine if step-by-step mode should be used.
|
|
367
|
+
|
|
368
|
+
Per ADR-0001, use stepwise if:
|
|
369
|
+
- Number of commits is small (< threshold, default 10)
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
repo: GitPython repository
|
|
373
|
+
source_commit: Source commit SHA
|
|
374
|
+
target_commit: Target commit SHA
|
|
375
|
+
threshold: Commit count threshold (default 10)
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
True if stepwise should be used, False for direct mode
|
|
379
|
+
"""
|
|
380
|
+
try:
|
|
381
|
+
# Count commits between source and target
|
|
382
|
+
commits = list(repo.iter_commits(f"{source_commit}..{target_commit}"))
|
|
383
|
+
num_commits = len(commits)
|
|
384
|
+
|
|
385
|
+
logger.info(
|
|
386
|
+
"evaluating_migration_mode",
|
|
387
|
+
num_commits=num_commits,
|
|
388
|
+
threshold=threshold,
|
|
389
|
+
recommendation="stepwise" if num_commits < threshold else "direct"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return num_commits < threshold
|
|
393
|
+
|
|
394
|
+
except Exception as e:
|
|
395
|
+
logger.error(
|
|
396
|
+
"failed_to_count_commits",
|
|
397
|
+
error=str(e)
|
|
398
|
+
)
|
|
399
|
+
# Default to direct mode on error
|
|
400
|
+
return False
|