yuho 5.0.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.
Files changed (91) hide show
  1. yuho/__init__.py +16 -0
  2. yuho/ast/__init__.py +196 -0
  3. yuho/ast/builder.py +926 -0
  4. yuho/ast/constant_folder.py +280 -0
  5. yuho/ast/dead_code.py +199 -0
  6. yuho/ast/exhaustiveness.py +503 -0
  7. yuho/ast/nodes.py +907 -0
  8. yuho/ast/overlap.py +291 -0
  9. yuho/ast/reachability.py +293 -0
  10. yuho/ast/scope_analysis.py +490 -0
  11. yuho/ast/transformer.py +490 -0
  12. yuho/ast/type_check.py +471 -0
  13. yuho/ast/type_inference.py +425 -0
  14. yuho/ast/visitor.py +239 -0
  15. yuho/cli/__init__.py +14 -0
  16. yuho/cli/commands/__init__.py +1 -0
  17. yuho/cli/commands/api.py +431 -0
  18. yuho/cli/commands/ast_viz.py +334 -0
  19. yuho/cli/commands/check.py +218 -0
  20. yuho/cli/commands/config.py +311 -0
  21. yuho/cli/commands/contribute.py +122 -0
  22. yuho/cli/commands/diff.py +487 -0
  23. yuho/cli/commands/explain.py +240 -0
  24. yuho/cli/commands/fmt.py +253 -0
  25. yuho/cli/commands/generate.py +316 -0
  26. yuho/cli/commands/graph.py +410 -0
  27. yuho/cli/commands/init.py +120 -0
  28. yuho/cli/commands/library.py +656 -0
  29. yuho/cli/commands/lint.py +503 -0
  30. yuho/cli/commands/lsp.py +36 -0
  31. yuho/cli/commands/preview.py +377 -0
  32. yuho/cli/commands/repl.py +444 -0
  33. yuho/cli/commands/serve.py +44 -0
  34. yuho/cli/commands/test.py +528 -0
  35. yuho/cli/commands/transpile.py +121 -0
  36. yuho/cli/commands/wizard.py +370 -0
  37. yuho/cli/completions.py +182 -0
  38. yuho/cli/error_formatter.py +193 -0
  39. yuho/cli/main.py +1064 -0
  40. yuho/config/__init__.py +46 -0
  41. yuho/config/loader.py +235 -0
  42. yuho/config/mask.py +194 -0
  43. yuho/config/schema.py +147 -0
  44. yuho/library/__init__.py +84 -0
  45. yuho/library/index.py +328 -0
  46. yuho/library/install.py +699 -0
  47. yuho/library/lockfile.py +330 -0
  48. yuho/library/package.py +421 -0
  49. yuho/library/resolver.py +791 -0
  50. yuho/library/signature.py +335 -0
  51. yuho/llm/__init__.py +45 -0
  52. yuho/llm/config.py +75 -0
  53. yuho/llm/factory.py +123 -0
  54. yuho/llm/prompts.py +146 -0
  55. yuho/llm/providers.py +383 -0
  56. yuho/llm/utils.py +470 -0
  57. yuho/lsp/__init__.py +14 -0
  58. yuho/lsp/code_action_handler.py +518 -0
  59. yuho/lsp/completion_handler.py +85 -0
  60. yuho/lsp/diagnostics.py +100 -0
  61. yuho/lsp/hover_handler.py +130 -0
  62. yuho/lsp/server.py +1425 -0
  63. yuho/mcp/__init__.py +10 -0
  64. yuho/mcp/server.py +1452 -0
  65. yuho/parser/__init__.py +8 -0
  66. yuho/parser/source_location.py +108 -0
  67. yuho/parser/wrapper.py +311 -0
  68. yuho/testing/__init__.py +48 -0
  69. yuho/testing/coverage.py +274 -0
  70. yuho/testing/fixtures.py +263 -0
  71. yuho/transpile/__init__.py +52 -0
  72. yuho/transpile/alloy_transpiler.py +546 -0
  73. yuho/transpile/base.py +100 -0
  74. yuho/transpile/blocks_transpiler.py +338 -0
  75. yuho/transpile/english_transpiler.py +470 -0
  76. yuho/transpile/graphql_transpiler.py +404 -0
  77. yuho/transpile/json_transpiler.py +217 -0
  78. yuho/transpile/jsonld_transpiler.py +250 -0
  79. yuho/transpile/latex_preamble.py +161 -0
  80. yuho/transpile/latex_transpiler.py +406 -0
  81. yuho/transpile/latex_utils.py +206 -0
  82. yuho/transpile/mermaid_transpiler.py +357 -0
  83. yuho/transpile/registry.py +275 -0
  84. yuho/verify/__init__.py +43 -0
  85. yuho/verify/alloy.py +352 -0
  86. yuho/verify/combined.py +218 -0
  87. yuho/verify/z3_solver.py +1155 -0
  88. yuho-5.0.0.dist-info/METADATA +186 -0
  89. yuho-5.0.0.dist-info/RECORD +91 -0
  90. yuho-5.0.0.dist-info/WHEEL +4 -0
  91. yuho-5.0.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,791 @@
1
+ """
2
+ Dependency resolution with version constraint solving for Yuho packages.
3
+
4
+ Implements a SAT-based resolver for package dependencies with support for
5
+ semver version constraints like ^1.0.0, ~1.2.0, >=1.0.0, etc.
6
+ """
7
+
8
+ from typing import List, Dict, Optional, Tuple, Set
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ import re
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ # Regex pattern for strict semver validation (based on semver.org spec)
18
+ SEMVER_PATTERN = re.compile(
19
+ r"^v?" # Optional 'v' prefix
20
+ r"(?P<major>0|[1-9]\d*)"
21
+ r"\.(?P<minor>0|[1-9]\d*)"
22
+ r"\.(?P<patch>0|[1-9]\d*)"
23
+ r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
24
+ r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
25
+ r"(?:\+(?P<build>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
26
+ )
27
+
28
+
29
+ class SemverValidationError(Exception):
30
+ """Raised when a version string is not valid semver."""
31
+ pass
32
+
33
+
34
+ @dataclass
35
+ class SemverValidation:
36
+ """Result of semver validation."""
37
+ valid: bool
38
+ errors: List[str] = field(default_factory=list)
39
+ warnings: List[str] = field(default_factory=list)
40
+
41
+
42
+ def validate_semver(version_str: str, strict: bool = True) -> SemverValidation:
43
+ """
44
+ Validate a version string against semver specification.
45
+
46
+ Args:
47
+ version_str: Version string to validate
48
+ strict: If True, requires full semver compliance. If False, allows
49
+ partial versions like "1.0" or "1".
50
+
51
+ Returns:
52
+ SemverValidation result with validity and any errors/warnings
53
+ """
54
+ errors = []
55
+ warnings = []
56
+
57
+ if not version_str:
58
+ return SemverValidation(False, ["Version string is empty"])
59
+
60
+ version_str = version_str.strip()
61
+
62
+ # Check for strict semver compliance
63
+ match = SEMVER_PATTERN.match(version_str)
64
+
65
+ if match:
66
+ # Valid semver
67
+ if version_str.startswith("v"):
68
+ warnings.append("Version has 'v' prefix (valid but not recommended)")
69
+ return SemverValidation(True, [], warnings)
70
+
71
+ if strict:
72
+ errors.append(f"'{version_str}' is not valid semver format (expected X.Y.Z)")
73
+
74
+ # Provide helpful hints
75
+ parts = version_str.lstrip("v").split(".")
76
+ if len(parts) < 3:
77
+ errors.append(f"Missing components: semver requires MAJOR.MINOR.PATCH")
78
+ elif len(parts) > 3 and "-" not in version_str and "+" not in version_str:
79
+ errors.append(f"Too many version components")
80
+
81
+ # Check for invalid characters
82
+ if re.search(r"[^0-9a-zA-Z.\-+]", version_str.lstrip("v")):
83
+ errors.append("Contains invalid characters")
84
+
85
+ return SemverValidation(False, errors)
86
+
87
+ # Non-strict mode: allow partial versions
88
+ partial_pattern = re.compile(r"^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?")
89
+ if partial_pattern.match(version_str):
90
+ warnings.append(f"'{version_str}' is a partial version (not strict semver)")
91
+ return SemverValidation(True, [], warnings)
92
+
93
+ return SemverValidation(False, [f"'{version_str}' cannot be parsed as a version"])
94
+
95
+
96
+ class ConstraintOp(Enum):
97
+ """Version constraint operators."""
98
+ EQ = "=" # Exact version
99
+ GTE = ">=" # Greater than or equal
100
+ LTE = "<=" # Less than or equal
101
+ GT = ">" # Greater than
102
+ LT = "<" # Less than
103
+ CARET = "^" # Compatible with (major version locked)
104
+ TILDE = "~" # Approximately (minor version locked)
105
+ ANY = "*" # Any version
106
+
107
+
108
+ @dataclass
109
+ class Version:
110
+ """Semantic version representation with full semver 2.0 support."""
111
+ major: int
112
+ minor: int
113
+ patch: int
114
+ prerelease: str = ""
115
+ build: str = "" # Build metadata (ignored in comparisons per semver spec)
116
+
117
+ @classmethod
118
+ def parse(cls, version_str: str, validate: bool = False) -> "Version":
119
+ """
120
+ Parse version string like '1.2.3' or 'v1.2.3-alpha+build.123'.
121
+
122
+ Args:
123
+ version_str: Version string to parse
124
+ validate: If True, raises SemverValidationError for invalid versions
125
+
126
+ Returns:
127
+ Parsed Version object
128
+
129
+ Raises:
130
+ SemverValidationError: If validate=True and version is invalid
131
+ """
132
+ if validate:
133
+ result = validate_semver(version_str, strict=True)
134
+ if not result.valid:
135
+ raise SemverValidationError("; ".join(result.errors))
136
+
137
+ v = version_str.strip().lstrip("v")
138
+ prerelease = ""
139
+ build = ""
140
+
141
+ # Extract build metadata (after +)
142
+ if "+" in v:
143
+ v, build = v.split("+", 1)
144
+
145
+ # Extract prerelease (after -)
146
+ if "-" in v:
147
+ v, prerelease = v.split("-", 1)
148
+
149
+ parts = v.split(".")
150
+ major = int(parts[0]) if len(parts) > 0 and parts[0].isdigit() else 0
151
+ minor = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 0
152
+ patch = int(parts[2]) if len(parts) > 2 and parts[2].isdigit() else 0
153
+
154
+ return cls(major, minor, patch, prerelease, build)
155
+
156
+ @classmethod
157
+ def is_valid(cls, version_str: str, strict: bool = True) -> bool:
158
+ """
159
+ Check if a version string is valid semver.
160
+
161
+ Args:
162
+ version_str: Version string to check
163
+ strict: If True, requires full semver compliance
164
+
165
+ Returns:
166
+ True if valid semver
167
+ """
168
+ return validate_semver(version_str, strict).valid
169
+
170
+ def __str__(self) -> str:
171
+ base = f"{self.major}.{self.minor}.{self.patch}"
172
+ if self.prerelease:
173
+ base = f"{base}-{self.prerelease}"
174
+ if self.build:
175
+ base = f"{base}+{self.build}"
176
+ return base
177
+
178
+ def to_tuple(self) -> Tuple[int, int, int, str]:
179
+ """Return version as comparable tuple (excludes build metadata)."""
180
+ return (self.major, self.minor, self.patch, self.prerelease)
181
+
182
+ def _compare_prerelease(self, other: "Version") -> int:
183
+ """
184
+ Compare prerelease versions per semver spec.
185
+
186
+ Returns:
187
+ -1 if self < other, 0 if equal, 1 if self > other
188
+ """
189
+ # No prerelease > prerelease (1.0.0 > 1.0.0-alpha)
190
+ if not self.prerelease and other.prerelease:
191
+ return 1
192
+ if self.prerelease and not other.prerelease:
193
+ return -1
194
+ if self.prerelease == other.prerelease:
195
+ return 0
196
+
197
+ # Compare prerelease identifiers
198
+ self_parts = self.prerelease.split(".")
199
+ other_parts = other.prerelease.split(".")
200
+
201
+ for s, o in zip(self_parts, other_parts):
202
+ # Numeric identifiers < alphanumeric
203
+ s_num = s.isdigit()
204
+ o_num = o.isdigit()
205
+
206
+ if s_num and o_num:
207
+ if int(s) < int(o):
208
+ return -1
209
+ if int(s) > int(o):
210
+ return 1
211
+ elif s_num:
212
+ return -1 # numeric < alphanumeric
213
+ elif o_num:
214
+ return 1
215
+ else:
216
+ if s < o:
217
+ return -1
218
+ if s > o:
219
+ return 1
220
+
221
+ # Longer prerelease > shorter (1.0.0-alpha.1 > 1.0.0-alpha)
222
+ if len(self_parts) < len(other_parts):
223
+ return -1
224
+ if len(self_parts) > len(other_parts):
225
+ return 1
226
+ return 0
227
+
228
+ def __lt__(self, other: "Version") -> bool:
229
+ if (self.major, self.minor, self.patch) != (other.major, other.minor, other.patch):
230
+ return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
231
+ return self._compare_prerelease(other) < 0
232
+
233
+ def __le__(self, other: "Version") -> bool:
234
+ return self == other or self < other
235
+
236
+ def __gt__(self, other: "Version") -> bool:
237
+ return other < self
238
+
239
+ def __ge__(self, other: "Version") -> bool:
240
+ return self == other or self > other
241
+
242
+ def __eq__(self, other: object) -> bool:
243
+ if not isinstance(other, Version):
244
+ return False
245
+ # Build metadata is ignored in equality per semver spec
246
+ return (self.major, self.minor, self.patch, self.prerelease) == \
247
+ (other.major, other.minor, other.patch, other.prerelease)
248
+
249
+ def __hash__(self) -> int:
250
+ return hash((self.major, self.minor, self.patch, self.prerelease))
251
+
252
+ def is_compatible_with(self, other: "Version") -> bool:
253
+ """
254
+ Check if this version is backwards-compatible with another version.
255
+
256
+ Per semver rules:
257
+ - Same major version (>0) = compatible
258
+ - Major version 0 = unstable, only same minor is compatible
259
+
260
+ Args:
261
+ other: Version to check compatibility with
262
+
263
+ Returns:
264
+ True if self is backwards-compatible with other
265
+ """
266
+ if self.major == 0 and other.major == 0:
267
+ # 0.x.y versions: only same minor is compatible
268
+ return self.minor == other.minor
269
+ return self.major == other.major
270
+
271
+ def is_breaking_change_from(self, other: "Version") -> bool:
272
+ """
273
+ Check if upgrading from other to self is a breaking change.
274
+
275
+ Args:
276
+ other: Previous version
277
+
278
+ Returns:
279
+ True if this version introduces breaking changes
280
+ """
281
+ if self <= other:
282
+ return False # Downgrade or same version
283
+ if self.major == 0 and other.major == 0:
284
+ # 0.x.y: minor bumps can be breaking
285
+ return self.minor > other.minor
286
+ return self.major > other.major
287
+
288
+ def next_major(self) -> "Version":
289
+ """Return next major version."""
290
+ return Version(self.major + 1, 0, 0)
291
+
292
+ def next_minor(self) -> "Version":
293
+ """Return next minor version."""
294
+ return Version(self.major, self.minor + 1, 0)
295
+
296
+ def next_patch(self) -> "Version":
297
+ """Return next patch version."""
298
+ return Version(self.major, self.minor, self.patch + 1)
299
+
300
+
301
+ @dataclass
302
+ class VersionConstraint:
303
+ """A version constraint like '^1.0.0' or '>=1.2.0'."""
304
+ op: ConstraintOp
305
+ version: Optional[Version]
306
+
307
+ @classmethod
308
+ def parse(cls, constraint_str: str) -> "VersionConstraint":
309
+ """Parse constraint string."""
310
+ constraint_str = constraint_str.strip()
311
+
312
+ if constraint_str == "*" or not constraint_str:
313
+ return cls(ConstraintOp.ANY, None)
314
+
315
+ # Check for operators
316
+ for op_str, op in [
317
+ (">=", ConstraintOp.GTE),
318
+ ("<=", ConstraintOp.LTE),
319
+ (">", ConstraintOp.GT),
320
+ ("<", ConstraintOp.LT),
321
+ ("^", ConstraintOp.CARET),
322
+ ("~", ConstraintOp.TILDE),
323
+ ("=", ConstraintOp.EQ),
324
+ ]:
325
+ if constraint_str.startswith(op_str):
326
+ version = Version.parse(constraint_str[len(op_str):])
327
+ return cls(op, version)
328
+
329
+ # Default to exact match
330
+ return cls(ConstraintOp.EQ, Version.parse(constraint_str))
331
+
332
+ def satisfies(self, version: Version) -> bool:
333
+ """Check if a version satisfies this constraint."""
334
+ if self.op == ConstraintOp.ANY:
335
+ return True
336
+
337
+ if self.version is None:
338
+ return True
339
+
340
+ v = self.version
341
+
342
+ if self.op == ConstraintOp.EQ:
343
+ return version == v
344
+ elif self.op == ConstraintOp.GTE:
345
+ return version >= v
346
+ elif self.op == ConstraintOp.LTE:
347
+ return version <= v
348
+ elif self.op == ConstraintOp.GT:
349
+ return version > v
350
+ elif self.op == ConstraintOp.LT:
351
+ return version < v
352
+ elif self.op == ConstraintOp.CARET:
353
+ # ^1.2.3 means >=1.2.3 <2.0.0 (major locked)
354
+ if version < v:
355
+ return False
356
+ if v.major == 0:
357
+ # ^0.x.y means >=0.x.y <0.(x+1).0
358
+ return version.major == 0 and version.minor == v.minor
359
+ return version.major == v.major
360
+ elif self.op == ConstraintOp.TILDE:
361
+ # ~1.2.3 means >=1.2.3 <1.3.0 (minor locked)
362
+ if version < v:
363
+ return False
364
+ return version.major == v.major and version.minor == v.minor
365
+
366
+ return False
367
+
368
+ def __str__(self) -> str:
369
+ if self.op == ConstraintOp.ANY:
370
+ return "*"
371
+
372
+ op_str = {
373
+ ConstraintOp.EQ: "=",
374
+ ConstraintOp.GTE: ">=",
375
+ ConstraintOp.LTE: "<=",
376
+ ConstraintOp.GT: ">",
377
+ ConstraintOp.LT: "<",
378
+ ConstraintOp.CARET: "^",
379
+ ConstraintOp.TILDE: "~",
380
+ }[self.op]
381
+
382
+ return f"{op_str}{self.version}"
383
+
384
+
385
+ @dataclass
386
+ class Dependency:
387
+ """A package dependency with version constraint."""
388
+ package: str # Section number
389
+ constraint: VersionConstraint
390
+
391
+ @classmethod
392
+ def parse(cls, dep_str: str) -> "Dependency":
393
+ """Parse dependency string like 'S403@^1.0.0' or 'S403>=1.0.0'."""
394
+ # Check for @ separator
395
+ if "@" in dep_str:
396
+ package, version = dep_str.split("@", 1)
397
+ return cls(package.strip(), VersionConstraint.parse(version))
398
+
399
+ # Check for version operators
400
+ for op in [">=", "<=", ">", "<", "^", "~", "="]:
401
+ if op in dep_str:
402
+ idx = dep_str.index(op)
403
+ package = dep_str[:idx].strip()
404
+ constraint = VersionConstraint.parse(dep_str[idx:])
405
+ return cls(package, constraint)
406
+
407
+ # No version constraint - any version
408
+ return cls(dep_str.strip(), VersionConstraint(ConstraintOp.ANY, None))
409
+
410
+ def __str__(self) -> str:
411
+ return f"{self.package}@{self.constraint}"
412
+
413
+
414
+ @dataclass
415
+ class PackageInfo:
416
+ """Information about a package for resolution."""
417
+ section_number: str
418
+ version: Version
419
+ dependencies: List[Dependency] = field(default_factory=list)
420
+
421
+
422
+ @dataclass
423
+ class Resolution:
424
+ """Result of dependency resolution."""
425
+ success: bool
426
+ packages: Dict[str, Version] # section_number -> resolved version
427
+ errors: List[str] = field(default_factory=list)
428
+ install_order: List[str] = field(default_factory=list)
429
+
430
+
431
+ class DependencyResolver:
432
+ """
433
+ Resolves package dependencies with version constraints.
434
+
435
+ Uses a backtracking algorithm to find a consistent set of
436
+ package versions that satisfy all constraints.
437
+ """
438
+
439
+ def __init__(self):
440
+ """Initialize resolver."""
441
+ self._available: Dict[str, List[PackageInfo]] = {} # package -> versions
442
+ self._constraints: Dict[str, List[VersionConstraint]] = {} # package -> constraints
443
+
444
+ def add_available_package(self, package: PackageInfo) -> None:
445
+ """Add an available package version."""
446
+ if package.section_number not in self._available:
447
+ self._available[package.section_number] = []
448
+ self._available[package.section_number].append(package)
449
+ # Keep sorted by version (newest first)
450
+ self._available[package.section_number].sort(
451
+ key=lambda p: p.version, reverse=True
452
+ )
453
+
454
+ def load_from_index(self, index) -> None:
455
+ """Load available packages from library index."""
456
+ for entry in index.list_all():
457
+ pkg = PackageInfo(
458
+ section_number=entry.section_number,
459
+ version=Version.parse(entry.version),
460
+ dependencies=[], # Would need to load from package
461
+ )
462
+ self.add_available_package(pkg)
463
+
464
+ def resolve(
465
+ self,
466
+ root_dependencies: List[Dependency],
467
+ installed: Optional[Dict[str, Version]] = None,
468
+ ) -> Resolution:
469
+ """
470
+ Resolve dependencies starting from root dependencies.
471
+
472
+ Args:
473
+ root_dependencies: Initial dependencies to resolve
474
+ installed: Already installed packages (locked versions)
475
+
476
+ Returns:
477
+ Resolution result
478
+ """
479
+ installed = installed or {}
480
+ self._constraints = {}
481
+
482
+ # Add root constraints
483
+ for dep in root_dependencies:
484
+ self._add_constraint(dep.package, dep.constraint)
485
+
486
+ # Try to resolve
487
+ resolved: Dict[str, Version] = dict(installed)
488
+ to_resolve = [dep.package for dep in root_dependencies]
489
+ errors = []
490
+
491
+ while to_resolve:
492
+ package = to_resolve.pop(0)
493
+
494
+ if package in resolved:
495
+ # Already resolved, check constraint
496
+ if not self._check_constraints(package, resolved[package]):
497
+ errors.append(
498
+ f"Conflict: {package}@{resolved[package]} violates constraints"
499
+ )
500
+ continue
501
+
502
+ # Find best matching version
503
+ version = self._find_best_version(package)
504
+
505
+ if version is None:
506
+ errors.append(f"No version of {package} satisfies constraints")
507
+ continue
508
+
509
+ resolved[package] = version
510
+
511
+ # Add transitive dependencies
512
+ pkg_info = self._get_package(package, version)
513
+ if pkg_info:
514
+ for dep in pkg_info.dependencies:
515
+ self._add_constraint(dep.package, dep.constraint)
516
+ if dep.package not in resolved:
517
+ to_resolve.append(dep.package)
518
+
519
+ if errors:
520
+ return Resolution(
521
+ success=False,
522
+ packages={},
523
+ errors=errors,
524
+ )
525
+
526
+ # Compute install order (topological sort)
527
+ install_order = self._topological_sort(resolved)
528
+
529
+ return Resolution(
530
+ success=True,
531
+ packages=resolved,
532
+ install_order=install_order,
533
+ )
534
+
535
+ def _add_constraint(self, package: str, constraint: VersionConstraint) -> None:
536
+ """Add a constraint for a package."""
537
+ if package not in self._constraints:
538
+ self._constraints[package] = []
539
+ self._constraints[package].append(constraint)
540
+
541
+ def _check_constraints(self, package: str, version: Version) -> bool:
542
+ """Check if version satisfies all constraints for package."""
543
+ if package not in self._constraints:
544
+ return True
545
+ return all(c.satisfies(version) for c in self._constraints[package])
546
+
547
+ def _find_best_version(self, package: str) -> Optional[Version]:
548
+ """Find the best (newest) version satisfying constraints."""
549
+ if package not in self._available:
550
+ return None
551
+
552
+ for pkg_info in self._available[package]:
553
+ if self._check_constraints(package, pkg_info.version):
554
+ return pkg_info.version
555
+
556
+ return None
557
+
558
+ def _get_package(self, package: str, version: Version) -> Optional[PackageInfo]:
559
+ """Get package info for a specific version."""
560
+ if package not in self._available:
561
+ return None
562
+
563
+ for pkg_info in self._available[package]:
564
+ if pkg_info.version == version:
565
+ return pkg_info
566
+
567
+ return None
568
+
569
+ def _topological_sort(self, packages: Dict[str, Version]) -> List[str]:
570
+ """Sort packages by dependency order (dependencies first)."""
571
+ # Build dependency graph
572
+ graph: Dict[str, Set[str]] = {pkg: set() for pkg in packages}
573
+
574
+ for pkg, version in packages.items():
575
+ pkg_info = self._get_package(pkg, version)
576
+ if pkg_info:
577
+ for dep in pkg_info.dependencies:
578
+ if dep.package in packages:
579
+ graph[pkg].add(dep.package)
580
+
581
+ # Kahn's algorithm
582
+ in_degree = {pkg: 0 for pkg in packages}
583
+ for pkg, deps in graph.items():
584
+ for dep in deps:
585
+ in_degree[pkg] += 1
586
+
587
+ queue = [pkg for pkg, degree in in_degree.items() if degree == 0]
588
+ result = []
589
+
590
+ while queue:
591
+ pkg = queue.pop(0)
592
+ result.append(pkg)
593
+
594
+ for other, deps in graph.items():
595
+ if pkg in deps:
596
+ in_degree[other] -= 1
597
+ if in_degree[other] == 0:
598
+ queue.append(other)
599
+
600
+ return result
601
+
602
+
603
+ @dataclass
604
+ class CompatibilityResult:
605
+ """Result of compatibility check between package versions."""
606
+ compatible: bool
607
+ breaking_changes: List[str] = field(default_factory=list)
608
+ warnings: List[str] = field(default_factory=list)
609
+ upgrade_type: str = "" # "major", "minor", "patch", "prerelease", "downgrade"
610
+
611
+
612
+ class CompatibilityChecker:
613
+ """
614
+ Checks version compatibility between packages.
615
+
616
+ Enforces semver compatibility rules and detects breaking changes.
617
+ """
618
+
619
+ @staticmethod
620
+ def check_upgrade(
621
+ from_version: Version,
622
+ to_version: Version,
623
+ allow_major: bool = False,
624
+ ) -> CompatibilityResult:
625
+ """
626
+ Check if upgrading between versions is compatible.
627
+
628
+ Args:
629
+ from_version: Current version
630
+ to_version: Target version
631
+ allow_major: If True, allow major version upgrades
632
+
633
+ Returns:
634
+ CompatibilityResult with compatibility status
635
+ """
636
+ breaking = []
637
+ warnings = []
638
+
639
+ # Determine upgrade type
640
+ if to_version < from_version:
641
+ upgrade_type = "downgrade"
642
+ warnings.append(f"Downgrade from {from_version} to {to_version}")
643
+ elif to_version.major > from_version.major:
644
+ upgrade_type = "major"
645
+ elif to_version.minor > from_version.minor:
646
+ upgrade_type = "minor"
647
+ elif to_version.patch > from_version.patch:
648
+ upgrade_type = "patch"
649
+ elif to_version.prerelease != from_version.prerelease:
650
+ upgrade_type = "prerelease"
651
+ else:
652
+ upgrade_type = "none"
653
+
654
+ # Check for breaking changes
655
+ is_breaking = to_version.is_breaking_change_from(from_version)
656
+ if is_breaking:
657
+ if from_version.major == 0:
658
+ breaking.append(
659
+ f"Minor version bump in 0.x series ({from_version} -> {to_version}) "
660
+ "may contain breaking changes"
661
+ )
662
+ else:
663
+ breaking.append(
664
+ f"Major version bump ({from_version} -> {to_version}) "
665
+ "indicates breaking API changes"
666
+ )
667
+
668
+ compatible = not is_breaking or allow_major
669
+
670
+ return CompatibilityResult(
671
+ compatible=compatible,
672
+ breaking_changes=breaking,
673
+ warnings=warnings,
674
+ upgrade_type=upgrade_type,
675
+ )
676
+
677
+ @staticmethod
678
+ def check_constraint_compatibility(
679
+ constraint1: VersionConstraint,
680
+ constraint2: VersionConstraint,
681
+ ) -> Tuple[bool, Optional[VersionConstraint]]:
682
+ """
683
+ Check if two version constraints can be satisfied simultaneously.
684
+
685
+ Args:
686
+ constraint1: First constraint
687
+ constraint2: Second constraint
688
+
689
+ Returns:
690
+ Tuple of (compatible, merged_constraint or None if incompatible)
691
+ """
692
+ # ANY matches everything
693
+ if constraint1.op == ConstraintOp.ANY:
694
+ return (True, constraint2)
695
+ if constraint2.op == ConstraintOp.ANY:
696
+ return (True, constraint1)
697
+
698
+ # For exact matches, check if they're the same
699
+ if constraint1.op == ConstraintOp.EQ and constraint2.op == ConstraintOp.EQ:
700
+ if constraint1.version == constraint2.version:
701
+ return (True, constraint1)
702
+ return (False, None)
703
+
704
+ # For one exact and one range, check if exact satisfies range
705
+ if constraint1.op == ConstraintOp.EQ:
706
+ if constraint2.satisfies(constraint1.version):
707
+ return (True, constraint1)
708
+ return (False, None)
709
+ if constraint2.op == ConstraintOp.EQ:
710
+ if constraint1.satisfies(constraint2.version):
711
+ return (True, constraint2)
712
+ return (False, None)
713
+
714
+ # For two ranges, find intersection
715
+ # This is a simplified check - returns the more restrictive constraint
716
+ # A full implementation would compute the actual intersection
717
+ v1, v2 = constraint1.version, constraint2.version
718
+
719
+ # Check if there's any overlap by testing boundary versions
720
+ test_versions = [v1, v2]
721
+ if v1:
722
+ test_versions.extend([v1.next_patch(), v1.next_minor(), v1.next_major()])
723
+ if v2:
724
+ test_versions.extend([v2.next_patch(), v2.next_minor(), v2.next_major()])
725
+
726
+ for v in test_versions:
727
+ if v and constraint1.satisfies(v) and constraint2.satisfies(v):
728
+ # There's at least one satisfying version
729
+ # Return the constraint with higher minimum
730
+ if constraint1.version and constraint2.version:
731
+ if constraint1.version >= constraint2.version:
732
+ return (True, constraint1)
733
+ return (True, constraint2)
734
+ return (True, constraint1)
735
+
736
+ return (False, None)
737
+
738
+ @staticmethod
739
+ def find_compatible_versions(
740
+ available: List[Version],
741
+ constraints: List[VersionConstraint],
742
+ ) -> List[Version]:
743
+ """
744
+ Find all versions that satisfy all constraints.
745
+
746
+ Args:
747
+ available: List of available versions
748
+ constraints: List of constraints to satisfy
749
+
750
+ Returns:
751
+ List of compatible versions (sorted newest first)
752
+ """
753
+ compatible = []
754
+ for v in available:
755
+ if all(c.satisfies(v) for c in constraints):
756
+ compatible.append(v)
757
+ return sorted(compatible, reverse=True)
758
+
759
+
760
+ def resolve_dependencies(
761
+ dependencies: List[str],
762
+ installed: Optional[Dict[str, str]] = None,
763
+ ) -> Resolution:
764
+ """
765
+ Convenience function to resolve dependencies.
766
+
767
+ Args:
768
+ dependencies: List of dependency strings like 'S403@^1.0.0'
769
+ installed: Dict of installed packages (section -> version string)
770
+
771
+ Returns:
772
+ Resolution result
773
+ """
774
+ from yuho.library.index import LibraryIndex
775
+
776
+ resolver = DependencyResolver()
777
+
778
+ # Load available packages from index
779
+ index = LibraryIndex()
780
+ resolver.load_from_index(index)
781
+
782
+ # Parse dependencies
783
+ parsed_deps = [Dependency.parse(d) for d in dependencies]
784
+
785
+ # Parse installed versions
786
+ parsed_installed = {}
787
+ if installed:
788
+ for pkg, ver in installed.items():
789
+ parsed_installed[pkg] = Version.parse(ver)
790
+
791
+ return resolver.resolve(parsed_deps, parsed_installed)