delimit-cli 2.3.2 → 3.0.0

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 (113) hide show
  1. package/.dockerignore +7 -0
  2. package/.github/workflows/ci.yml +22 -0
  3. package/CHANGELOG.md +33 -0
  4. package/CODE_OF_CONDUCT.md +48 -0
  5. package/CONTRIBUTING.md +67 -0
  6. package/Dockerfile +9 -0
  7. package/LICENSE +21 -0
  8. package/README.md +51 -130
  9. package/SECURITY.md +42 -0
  10. package/adapters/codex-forge.js +107 -0
  11. package/adapters/codex-jamsons.js +142 -0
  12. package/adapters/codex-security.js +94 -0
  13. package/adapters/gemini-forge.js +120 -0
  14. package/adapters/gemini-jamsons.js +152 -0
  15. package/bin/delimit-cli.js +52 -2
  16. package/bin/delimit-setup.js +258 -0
  17. package/gateway/ai/backends/__init__.py +0 -0
  18. package/gateway/ai/backends/async_utils.py +21 -0
  19. package/gateway/ai/backends/deploy_bridge.py +150 -0
  20. package/gateway/ai/backends/gateway_core.py +261 -0
  21. package/gateway/ai/backends/generate_bridge.py +38 -0
  22. package/gateway/ai/backends/governance_bridge.py +196 -0
  23. package/gateway/ai/backends/intel_bridge.py +59 -0
  24. package/gateway/ai/backends/memory_bridge.py +93 -0
  25. package/gateway/ai/backends/ops_bridge.py +137 -0
  26. package/gateway/ai/backends/os_bridge.py +82 -0
  27. package/gateway/ai/backends/repo_bridge.py +117 -0
  28. package/gateway/ai/backends/ui_bridge.py +118 -0
  29. package/gateway/ai/backends/vault_bridge.py +129 -0
  30. package/gateway/ai/server.py +1182 -0
  31. package/gateway/core/__init__.py +3 -0
  32. package/gateway/core/__pycache__/__init__.cpython-310.pyc +0 -0
  33. package/gateway/core/__pycache__/auto_baseline.cpython-310.pyc +0 -0
  34. package/gateway/core/__pycache__/ci_formatter.cpython-310.pyc +0 -0
  35. package/gateway/core/__pycache__/contract_ledger.cpython-310.pyc +0 -0
  36. package/gateway/core/__pycache__/dependency_graph.cpython-310.pyc +0 -0
  37. package/gateway/core/__pycache__/dependency_manifest.cpython-310.pyc +0 -0
  38. package/gateway/core/__pycache__/diff_engine_v2.cpython-310.pyc +0 -0
  39. package/gateway/core/__pycache__/event_backbone.cpython-310.pyc +0 -0
  40. package/gateway/core/__pycache__/event_schema.cpython-310.pyc +0 -0
  41. package/gateway/core/__pycache__/explainer.cpython-310.pyc +0 -0
  42. package/gateway/core/__pycache__/gateway.cpython-310.pyc +0 -0
  43. package/gateway/core/__pycache__/gateway_v2.cpython-310.pyc +0 -0
  44. package/gateway/core/__pycache__/gateway_v3.cpython-310.pyc +0 -0
  45. package/gateway/core/__pycache__/impact_analyzer.cpython-310.pyc +0 -0
  46. package/gateway/core/__pycache__/policy_engine.cpython-310.pyc +0 -0
  47. package/gateway/core/__pycache__/registry.cpython-310.pyc +0 -0
  48. package/gateway/core/__pycache__/registry_v2.cpython-310.pyc +0 -0
  49. package/gateway/core/__pycache__/registry_v3.cpython-310.pyc +0 -0
  50. package/gateway/core/__pycache__/semver_classifier.cpython-310.pyc +0 -0
  51. package/gateway/core/__pycache__/spec_detector.cpython-310.pyc +0 -0
  52. package/gateway/core/__pycache__/surface_bridge.cpython-310.pyc +0 -0
  53. package/gateway/core/auto_baseline.py +304 -0
  54. package/gateway/core/ci_formatter.py +283 -0
  55. package/gateway/core/complexity_analyzer.py +386 -0
  56. package/gateway/core/contract_ledger.py +345 -0
  57. package/gateway/core/dependency_graph.py +218 -0
  58. package/gateway/core/dependency_manifest.py +223 -0
  59. package/gateway/core/diff_engine_v2.py +477 -0
  60. package/gateway/core/diff_engine_v2.py.bak +426 -0
  61. package/gateway/core/event_backbone.py +268 -0
  62. package/gateway/core/event_schema.py +258 -0
  63. package/gateway/core/explainer.py +438 -0
  64. package/gateway/core/gateway.py +128 -0
  65. package/gateway/core/gateway_v2.py +154 -0
  66. package/gateway/core/gateway_v3.py +224 -0
  67. package/gateway/core/impact_analyzer.py +163 -0
  68. package/gateway/core/policies/default.yml +13 -0
  69. package/gateway/core/policies/relaxed.yml +48 -0
  70. package/gateway/core/policies/strict.yml +55 -0
  71. package/gateway/core/policy_engine.py +464 -0
  72. package/gateway/core/registry.py +52 -0
  73. package/gateway/core/registry_v2.py +132 -0
  74. package/gateway/core/registry_v3.py +134 -0
  75. package/gateway/core/semver_classifier.py +152 -0
  76. package/gateway/core/spec_detector.py +130 -0
  77. package/gateway/core/surface_bridge.py +307 -0
  78. package/gateway/core/zero_spec/__init__.py +4 -0
  79. package/gateway/core/zero_spec/__pycache__/__init__.cpython-310.pyc +0 -0
  80. package/gateway/core/zero_spec/__pycache__/detector.cpython-310.pyc +0 -0
  81. package/gateway/core/zero_spec/__pycache__/express_extractor.cpython-310.pyc +0 -0
  82. package/gateway/core/zero_spec/__pycache__/fastapi_extractor.cpython-310.pyc +0 -0
  83. package/gateway/core/zero_spec/__pycache__/nestjs_extractor.cpython-310.pyc +0 -0
  84. package/gateway/core/zero_spec/detector.py +353 -0
  85. package/gateway/core/zero_spec/express_extractor.py +483 -0
  86. package/gateway/core/zero_spec/fastapi_extractor.py +254 -0
  87. package/gateway/core/zero_spec/nestjs_extractor.py +369 -0
  88. package/gateway/tasks/__init__.py +1 -0
  89. package/gateway/tasks/__pycache__/__init__.cpython-310.pyc +0 -0
  90. package/gateway/tasks/__pycache__/check_policy.cpython-310.pyc +0 -0
  91. package/gateway/tasks/__pycache__/check_policy_v2.cpython-310.pyc +0 -0
  92. package/gateway/tasks/__pycache__/check_policy_v3.cpython-310.pyc +0 -0
  93. package/gateway/tasks/__pycache__/explain_diff.cpython-310.pyc +0 -0
  94. package/gateway/tasks/__pycache__/explain_diff_v2.cpython-310.pyc +0 -0
  95. package/gateway/tasks/__pycache__/validate_api.cpython-310.pyc +0 -0
  96. package/gateway/tasks/__pycache__/validate_api_v2.cpython-310.pyc +0 -0
  97. package/gateway/tasks/__pycache__/validate_api_v3.cpython-310.pyc +0 -0
  98. package/gateway/tasks/check_policy.py +177 -0
  99. package/gateway/tasks/check_policy_v2.py +255 -0
  100. package/gateway/tasks/check_policy_v3.py +255 -0
  101. package/gateway/tasks/explain_diff.py +305 -0
  102. package/gateway/tasks/explain_diff_v2.py +267 -0
  103. package/gateway/tasks/validate_api.py +131 -0
  104. package/gateway/tasks/validate_api_v2.py +208 -0
  105. package/gateway/tasks/validate_api_v3.py +163 -0
  106. package/package.json +3 -3
  107. package/adapters/codex-skill.js +0 -87
  108. package/adapters/cursor-extension.js +0 -190
  109. package/adapters/gemini-action.js +0 -93
  110. package/adapters/openai-function.js +0 -112
  111. package/adapters/xai-plugin.js +0 -151
  112. package/test-decision-engine.js +0 -181
  113. package/test-hook.js +0 -27
@@ -0,0 +1,132 @@
1
+ """
2
+ Fixed Task Registry with proper versioning support
3
+ V12 Core Hardening
4
+ """
5
+
6
+ from typing import Callable, Dict, Optional, List, Tuple
7
+ from functools import wraps
8
+ import logging
9
+ from packaging import version
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class TaskRegistry:
15
+ """
16
+ Registry for task handlers with proper versioning
17
+ Key format: task_name:version (e.g., "validate-api:1.0")
18
+ """
19
+
20
+ def __init__(self):
21
+ self._tasks: Dict[str, Callable] = {}
22
+ self._task_metadata: Dict[str, Dict] = {}
23
+ self._latest_versions: Dict[str, str] = {} # task_name -> latest_version
24
+
25
+ def register(self, task_name: str, version: str, **metadata):
26
+ """
27
+ Decorator to register a task handler with explicit version
28
+
29
+ Args:
30
+ task_name: Name of the task (e.g., "validate-api")
31
+ version: Version string (e.g., "1.0", "2.0")
32
+ **metadata: Additional metadata (description, etc.)
33
+ """
34
+ def decorator(func: Callable):
35
+ # Create versioned key
36
+ task_key = f"{task_name}:{version}"
37
+
38
+ @wraps(func)
39
+ def wrapper(*args, **kwargs):
40
+ return func(*args, **kwargs)
41
+
42
+ # Store handler with versioned key
43
+ self._tasks[task_key] = wrapper
44
+
45
+ # Store metadata
46
+ self._task_metadata[task_key] = {
47
+ "name": task_name,
48
+ "version": version,
49
+ "handler": func.__name__,
50
+ **metadata
51
+ }
52
+
53
+ # Update latest version tracking
54
+ if task_name not in self._latest_versions:
55
+ self._latest_versions[task_name] = version
56
+ else:
57
+ # Compare versions properly
58
+ try:
59
+ current = version.parse(self._latest_versions[task_name])
60
+ new = version.parse(version)
61
+ if new > current:
62
+ self._latest_versions[task_name] = version
63
+ except:
64
+ # Fallback to string comparison if not semantic versioning
65
+ if version > self._latest_versions[task_name]:
66
+ self._latest_versions[task_name] = version
67
+
68
+ logger.info(f"Registered task: {task_key}")
69
+ return wrapper
70
+ return decorator
71
+
72
+ def get_handler(self, task_name: str, version: Optional[str] = None) -> Optional[Callable]:
73
+ """
74
+ Get a task handler by name and optional version
75
+
76
+ Args:
77
+ task_name: Task name (e.g., "validate-api")
78
+ version: Optional version (e.g., "1.0"). If None, returns latest.
79
+
80
+ Returns:
81
+ Task handler callable or None if not found
82
+ """
83
+ if version:
84
+ # Explicit version requested
85
+ task_key = f"{task_name}:{version}"
86
+ return self._tasks.get(task_key)
87
+ else:
88
+ # No version specified, return latest
89
+ if task_name in self._latest_versions:
90
+ latest_version = self._latest_versions[task_name]
91
+ task_key = f"{task_name}:{latest_version}"
92
+ return self._tasks.get(task_key)
93
+ return None
94
+
95
+ def list_tasks(self) -> List[str]:
96
+ """List all registered task keys (with versions)"""
97
+ return sorted(self._tasks.keys())
98
+
99
+ def list_task_names(self) -> List[str]:
100
+ """List unique task names (without versions)"""
101
+ return sorted(set(self._latest_versions.keys()))
102
+
103
+ def get_task_versions(self, task_name: str) -> List[str]:
104
+ """Get all registered versions for a task"""
105
+ versions = []
106
+ for key in self._tasks.keys():
107
+ if key.startswith(f"{task_name}:"):
108
+ version = key.split(":", 1)[1]
109
+ versions.append(version)
110
+ return sorted(versions)
111
+
112
+ def has_task(self, task_name: str, version: Optional[str] = None) -> bool:
113
+ """Check if a task is registered"""
114
+ if version:
115
+ task_key = f"{task_name}:{version}"
116
+ return task_key in self._tasks
117
+ else:
118
+ return task_name in self._latest_versions
119
+
120
+ def get_metadata(self, task_name: str, version: Optional[str] = None) -> Optional[Dict]:
121
+ """Get metadata for a task"""
122
+ if version:
123
+ task_key = f"{task_name}:{version}"
124
+ elif task_name in self._latest_versions:
125
+ task_key = f"{task_name}:{self._latest_versions[task_name]}"
126
+ else:
127
+ return None
128
+ return self._task_metadata.get(task_key)
129
+
130
+
131
+ # Global registry instance
132
+ task_registry = TaskRegistry()
@@ -0,0 +1,134 @@
1
+ """
2
+ Fixed Task Registry with proper versioning support - No shadowing
3
+ V12 Core Hardening Final
4
+ """
5
+
6
+ from typing import Callable, Dict, Optional, List
7
+ from functools import wraps
8
+ import logging
9
+ from packaging import version as pkg_version
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class TaskRegistry:
15
+ """
16
+ Registry for task handlers with proper versioning
17
+ Key format: task_name:version (e.g., "validate-api:1.0")
18
+ Fixed version shadowing issue
19
+ """
20
+
21
+ def __init__(self):
22
+ self._tasks: Dict[str, Callable] = {}
23
+ self._task_metadata: Dict[str, Dict] = {}
24
+ self._latest_versions: Dict[str, str] = {} # task_name -> latest_version
25
+
26
+ def register(self, task_name: str, task_version: str, **metadata):
27
+ """
28
+ Decorator to register a task handler with explicit version
29
+
30
+ Args:
31
+ task_name: Name of the task (e.g., "validate-api")
32
+ task_version: Version string (e.g., "1.0", "2.0")
33
+ **metadata: Additional metadata (description, etc.)
34
+ """
35
+ def decorator(func: Callable):
36
+ # Create versioned key
37
+ task_key = f"{task_name}:{task_version}"
38
+
39
+ @wraps(func)
40
+ def wrapper(*args, **kwargs):
41
+ return func(*args, **kwargs)
42
+
43
+ # Store handler with versioned key
44
+ self._tasks[task_key] = wrapper
45
+
46
+ # Store metadata
47
+ self._task_metadata[task_key] = {
48
+ "name": task_name,
49
+ "version": task_version,
50
+ "handler": func.__name__,
51
+ **metadata
52
+ }
53
+
54
+ # Update latest version tracking
55
+ if task_name not in self._latest_versions:
56
+ self._latest_versions[task_name] = task_version
57
+ else:
58
+ # Compare versions properly using packaging.version
59
+ try:
60
+ current = pkg_version.parse(self._latest_versions[task_name])
61
+ new = pkg_version.parse(task_version)
62
+ if new > current:
63
+ self._latest_versions[task_name] = task_version
64
+ except Exception as e:
65
+ # Fallback to string comparison if not semantic versioning
66
+ logger.warning(f"Version comparison failed, using string comparison: {e}")
67
+ if task_version > self._latest_versions[task_name]:
68
+ self._latest_versions[task_name] = task_version
69
+
70
+ logger.info(f"Registered task: {task_key}")
71
+ return wrapper
72
+ return decorator
73
+
74
+ def get_handler(self, task_name: str, version: Optional[str] = None) -> Optional[Callable]:
75
+ """
76
+ Get a task handler by name and optional version
77
+
78
+ Args:
79
+ task_name: Task name (e.g., "validate-api")
80
+ version: Optional version (e.g., "1.0"). If None, returns latest.
81
+
82
+ Returns:
83
+ Task handler callable or None if not found
84
+ """
85
+ if version:
86
+ # Explicit version requested
87
+ task_key = f"{task_name}:{version}"
88
+ return self._tasks.get(task_key)
89
+ else:
90
+ # No version specified, return latest
91
+ if task_name in self._latest_versions:
92
+ latest_version = self._latest_versions[task_name]
93
+ task_key = f"{task_name}:{latest_version}"
94
+ return self._tasks.get(task_key)
95
+ return None
96
+
97
+ def list_tasks(self) -> List[str]:
98
+ """List all registered task keys (with versions)"""
99
+ return sorted(self._tasks.keys())
100
+
101
+ def list_task_names(self) -> List[str]:
102
+ """List unique task names (without versions)"""
103
+ return sorted(set(self._latest_versions.keys()))
104
+
105
+ def get_task_versions(self, task_name: str) -> List[str]:
106
+ """Get all registered versions for a task"""
107
+ versions = []
108
+ for key in self._tasks.keys():
109
+ if key.startswith(f"{task_name}:"):
110
+ version = key.split(":", 1)[1]
111
+ versions.append(version)
112
+ return sorted(versions)
113
+
114
+ def has_task(self, task_name: str, version: Optional[str] = None) -> bool:
115
+ """Check if a task is registered"""
116
+ if version:
117
+ task_key = f"{task_name}:{version}"
118
+ return task_key in self._tasks
119
+ else:
120
+ return task_name in self._latest_versions
121
+
122
+ def get_metadata(self, task_name: str, version: Optional[str] = None) -> Optional[Dict]:
123
+ """Get metadata for a task"""
124
+ if version:
125
+ task_key = f"{task_name}:{version}"
126
+ elif task_name in self._latest_versions:
127
+ task_key = f"{task_name}:{self._latest_versions[task_name]}"
128
+ else:
129
+ return None
130
+ return self._task_metadata.get(task_key)
131
+
132
+
133
+ # Global registry instance
134
+ task_registry = TaskRegistry()
@@ -0,0 +1,152 @@
1
+ """
2
+ Delimit Semver Classifier
3
+
4
+ Deterministic semver bump classification from diff engine output.
5
+ Takes a list of Change objects and returns the recommended version bump.
6
+
7
+ Rules:
8
+ - Any breaking change → MAJOR
9
+ - Any additive change (no breaking) → MINOR
10
+ - Non-functional changes only → PATCH
11
+ - No changes → NONE
12
+ """
13
+
14
+ from enum import Enum
15
+ from typing import Any, Dict, List
16
+
17
+ from .diff_engine_v2 import Change, ChangeType
18
+
19
+
20
+ class SemverBump(Enum):
21
+ NONE = "none"
22
+ PATCH = "patch"
23
+ MINOR = "minor"
24
+ MAJOR = "major"
25
+
26
+
27
+ # ── Change-type buckets ──────────────────────────────────────────────
28
+
29
+ BREAKING_TYPES = frozenset({
30
+ ChangeType.ENDPOINT_REMOVED,
31
+ ChangeType.METHOD_REMOVED,
32
+ ChangeType.REQUIRED_PARAM_ADDED,
33
+ ChangeType.PARAM_REMOVED,
34
+ ChangeType.RESPONSE_REMOVED,
35
+ ChangeType.REQUIRED_FIELD_ADDED,
36
+ ChangeType.FIELD_REMOVED,
37
+ ChangeType.TYPE_CHANGED,
38
+ ChangeType.FORMAT_CHANGED,
39
+ ChangeType.ENUM_VALUE_REMOVED,
40
+ })
41
+
42
+ ADDITIVE_TYPES = frozenset({
43
+ ChangeType.ENDPOINT_ADDED,
44
+ ChangeType.METHOD_ADDED,
45
+ ChangeType.OPTIONAL_PARAM_ADDED,
46
+ ChangeType.RESPONSE_ADDED,
47
+ ChangeType.OPTIONAL_FIELD_ADDED,
48
+ ChangeType.ENUM_VALUE_ADDED,
49
+ })
50
+
51
+ PATCH_TYPES = frozenset({
52
+ ChangeType.DESCRIPTION_CHANGED,
53
+ })
54
+
55
+
56
+ def classify(changes: List[Change]) -> SemverBump:
57
+ """Classify a list of changes into a semver bump level.
58
+
59
+ Deterministic: same input always produces same output.
60
+ """
61
+ if not changes:
62
+ return SemverBump.NONE
63
+
64
+ has_breaking = False
65
+ has_additive = False
66
+
67
+ for change in changes:
68
+ if change.type in BREAKING_TYPES:
69
+ has_breaking = True
70
+ break # short-circuit — can't go higher than MAJOR
71
+ if change.type in ADDITIVE_TYPES:
72
+ has_additive = True
73
+
74
+ if has_breaking:
75
+ return SemverBump.MAJOR
76
+ if has_additive:
77
+ return SemverBump.MINOR
78
+ return SemverBump.PATCH
79
+
80
+
81
+ def classify_detailed(changes: List[Change]) -> Dict[str, Any]:
82
+ """Return a detailed classification with per-category breakdowns.
83
+
84
+ Used by CLI explain and PR comment generation.
85
+ """
86
+ bump = classify(changes)
87
+
88
+ breaking = [c for c in changes if c.type in BREAKING_TYPES]
89
+ additive = [c for c in changes if c.type in ADDITIVE_TYPES]
90
+ patch = [c for c in changes if c.type in PATCH_TYPES]
91
+
92
+ return {
93
+ "bump": bump.value,
94
+ "is_breaking": bump == SemverBump.MAJOR,
95
+ "counts": {
96
+ "total": len(changes),
97
+ "breaking": len(breaking),
98
+ "additive": len(additive),
99
+ "patch": len(patch),
100
+ },
101
+ "breaking_changes": [
102
+ {"type": c.type.value, "path": c.path, "message": c.message}
103
+ for c in breaking
104
+ ],
105
+ "additive_changes": [
106
+ {"type": c.type.value, "path": c.path, "message": c.message}
107
+ for c in additive
108
+ ],
109
+ "patch_changes": [
110
+ {"type": c.type.value, "path": c.path, "message": c.message}
111
+ for c in patch
112
+ ],
113
+ }
114
+
115
+
116
+ def bump_version(current: str, bump: SemverBump) -> str:
117
+ """Apply a semver bump to a version string.
118
+
119
+ Args:
120
+ current: Version string like "1.2.3" or "v1.2.3".
121
+ bump: The bump level to apply.
122
+
123
+ Returns:
124
+ New version string (preserves 'v' prefix if present).
125
+ """
126
+ prefix = ""
127
+ ver = current
128
+ if ver.startswith("v"):
129
+ prefix = "v"
130
+ ver = ver[1:]
131
+
132
+ parts = ver.split(".")
133
+ if len(parts) != 3:
134
+ return current # can't parse — return unchanged
135
+
136
+ try:
137
+ major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
138
+ except ValueError:
139
+ return current
140
+
141
+ if bump == SemverBump.MAJOR:
142
+ major += 1
143
+ minor = 0
144
+ patch = 0
145
+ elif bump == SemverBump.MINOR:
146
+ minor += 1
147
+ patch = 0
148
+ elif bump == SemverBump.PATCH:
149
+ patch += 1
150
+ # NONE: no change
151
+
152
+ return f"{prefix}{major}.{minor}.{patch}"
@@ -0,0 +1,130 @@
1
+ """
2
+ Automatic OpenAPI specification detector for zero-config installation.
3
+ """
4
+
5
+ import os
6
+ from typing import List, Optional, Tuple
7
+ from pathlib import Path
8
+ import yaml
9
+
10
+ class SpecDetector:
11
+ """Auto-detect OpenAPI specifications in common locations."""
12
+
13
+ COMMON_SPEC_PATTERNS = [
14
+ "openapi.yaml",
15
+ "openapi.yml",
16
+ "swagger.yaml",
17
+ "swagger.yml",
18
+ "api/openapi.yaml",
19
+ "api/openapi.yml",
20
+ "api/swagger.yaml",
21
+ "api/swagger.yml",
22
+ "spec/openapi.yaml",
23
+ "spec/openapi.yml",
24
+ "docs/openapi.yaml",
25
+ "docs/api.yaml",
26
+ "api-spec.yaml",
27
+ "api-spec.yml",
28
+ "**/openapi.yaml", # Recursive search
29
+ "**/swagger.yaml",
30
+ "services/*/api/openapi.yaml", # Monorepo pattern
31
+ ]
32
+
33
+ def __init__(self, root_path: str = "."):
34
+ self.root = Path(root_path)
35
+
36
+ def detect_specs(self) -> Tuple[List[str], Optional[str]]:
37
+ """
38
+ Detect OpenAPI specifications.
39
+
40
+ Returns:
41
+ (spec_files, message): List of found specs and optional message
42
+ """
43
+ found_specs = []
44
+
45
+ # Check each common pattern
46
+ for pattern in self.COMMON_SPEC_PATTERNS:
47
+ if "**" in pattern:
48
+ # Recursive glob
49
+ for spec_file in self.root.glob(pattern):
50
+ if self._is_valid_openapi(spec_file):
51
+ found_specs.append(str(spec_file.relative_to(self.root)))
52
+ elif "*" in pattern:
53
+ # Simple glob
54
+ for spec_file in self.root.glob(pattern):
55
+ if self._is_valid_openapi(spec_file):
56
+ found_specs.append(str(spec_file.relative_to(self.root)))
57
+ else:
58
+ # Direct path
59
+ spec_file = self.root / pattern
60
+ if spec_file.exists() and self._is_valid_openapi(spec_file):
61
+ found_specs.append(pattern)
62
+
63
+ # Remove duplicates while preserving order
64
+ found_specs = list(dict.fromkeys(found_specs))
65
+
66
+ # Generate appropriate message
67
+ if len(found_specs) == 0:
68
+ message = "No OpenAPI specifications found. Please specify 'files' or create openapi.yaml"
69
+ elif len(found_specs) == 1:
70
+ message = f"Auto-detected spec: {found_specs[0]}"
71
+ else:
72
+ message = f"Multiple specs found: {', '.join(found_specs[:3])}. Please specify 'files' parameter."
73
+
74
+ return found_specs, message
75
+
76
+ def _is_valid_openapi(self, file_path: Path) -> bool:
77
+ """Check if file is a valid OpenAPI specification."""
78
+ if not file_path.is_file():
79
+ return False
80
+
81
+ try:
82
+ with open(file_path, 'r') as f:
83
+ data = yaml.safe_load(f)
84
+ # Check for OpenAPI/Swagger markers
85
+ if isinstance(data, dict):
86
+ return 'openapi' in data or 'swagger' in data
87
+ except:
88
+ return False
89
+
90
+ return False
91
+
92
+ def get_default_specs(self) -> Tuple[Optional[str], Optional[str]]:
93
+ """
94
+ Get default old_spec and new_spec for auto-detection.
95
+
96
+ Returns:
97
+ (old_spec, new_spec): Paths or None if not found
98
+ """
99
+ specs, _ = self.detect_specs()
100
+
101
+ if len(specs) == 0:
102
+ return None, None
103
+
104
+ # Use the first found spec as both old and new (baseline mode)
105
+ default_spec = specs[0]
106
+ return default_spec, default_spec
107
+
108
+
109
+ def auto_detect_specs(root_path: str = ".") -> dict:
110
+ """
111
+ Main entry point for spec auto-detection.
112
+
113
+ Returns dict with:
114
+ - detected: List of detected spec files
115
+ - old_spec: Suggested old spec path
116
+ - new_spec: Suggested new spec path
117
+ - message: User-friendly message
118
+ - requires_input: Whether user must specify paths
119
+ """
120
+ detector = SpecDetector(root_path)
121
+ specs, message = detector.detect_specs()
122
+ old_spec, new_spec = detector.get_default_specs()
123
+
124
+ return {
125
+ "detected": specs,
126
+ "old_spec": old_spec,
127
+ "new_spec": new_spec,
128
+ "message": message,
129
+ "requires_input": len(specs) != 1
130
+ }