ai-dev-cli-tools 0.5.0a1__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 (60) hide show
  1. ai_dev_cli_tools-0.5.0a1.dist-info/METADATA +240 -0
  2. ai_dev_cli_tools-0.5.0a1.dist-info/RECORD +60 -0
  3. ai_dev_cli_tools-0.5.0a1.dist-info/WHEEL +4 -0
  4. ai_dev_cli_tools-0.5.0a1.dist-info/entry_points.txt +2 -0
  5. ai_dev_cli_tools-0.5.0a1.dist-info/licenses/LICENSE +21 -0
  6. ai_dev_tools/__init__.py +3 -0
  7. ai_dev_tools/cache/__init__.py +11 -0
  8. ai_dev_tools/cache/graph.py +136 -0
  9. ai_dev_tools/cache/repository.py +169 -0
  10. ai_dev_tools/cache/validation.py +154 -0
  11. ai_dev_tools/cli.py +387 -0
  12. ai_dev_tools/completion.py +72 -0
  13. ai_dev_tools/config.py +223 -0
  14. ai_dev_tools/context/__init__.py +5 -0
  15. ai_dev_tools/context/builder.py +506 -0
  16. ai_dev_tools/context/incremental.py +107 -0
  17. ai_dev_tools/context/models.py +59 -0
  18. ai_dev_tools/context/profiles.py +49 -0
  19. ai_dev_tools/context/selection.py +270 -0
  20. ai_dev_tools/context/symbols.py +178 -0
  21. ai_dev_tools/detectors/__init__.py +1 -0
  22. ai_dev_tools/detectors/environment.py +125 -0
  23. ai_dev_tools/detectors/project.py +189 -0
  24. ai_dev_tools/detectors/repository_map.py +129 -0
  25. ai_dev_tools/detectors/runtime.py +190 -0
  26. ai_dev_tools/detectors/workspaces.py +228 -0
  27. ai_dev_tools/git/__init__.py +1 -0
  28. ai_dev_tools/git/inspect.py +219 -0
  29. ai_dev_tools/models/__init__.py +1 -0
  30. ai_dev_tools/models/report.py +95 -0
  31. ai_dev_tools/models/workspace.py +48 -0
  32. ai_dev_tools/parsers/__init__.py +1 -0
  33. ai_dev_tools/parsers/logs.py +372 -0
  34. ai_dev_tools/parsers/registry.py +60 -0
  35. ai_dev_tools/reporters/__init__.py +1 -0
  36. ai_dev_tools/reporters/progressive.py +161 -0
  37. ai_dev_tools/reporters/writer.py +74 -0
  38. ai_dev_tools/runners/__init__.py +1 -0
  39. ai_dev_tools/runners/baseline.py +190 -0
  40. ai_dev_tools/runners/bootstrap.py +191 -0
  41. ai_dev_tools/runners/bootstrap_models.py +64 -0
  42. ai_dev_tools/runners/bootstrap_strategies.py +444 -0
  43. ai_dev_tools/runners/cache.py +23 -0
  44. ai_dev_tools/runners/check.py +509 -0
  45. ai_dev_tools/runners/check_checkpoint.py +50 -0
  46. ai_dev_tools/runners/check_models.py +51 -0
  47. ai_dev_tools/runners/check_scheduler.py +94 -0
  48. ai_dev_tools/runners/check_selection.py +267 -0
  49. ai_dev_tools/runners/diagnostics.py +96 -0
  50. ai_dev_tools/runners/feedback.py +193 -0
  51. ai_dev_tools/runners/finish.py +105 -0
  52. ai_dev_tools/runners/focused.py +37 -0
  53. ai_dev_tools/runners/index.py +44 -0
  54. ai_dev_tools/runtime/__init__.py +3 -0
  55. ai_dev_tools/runtime/runner.py +380 -0
  56. ai_dev_tools/runtime/supervisor.py +145 -0
  57. ai_dev_tools/security/__init__.py +1 -0
  58. ai_dev_tools/security/secrets.py +58 -0
  59. ai_dev_tools/utils/__init__.py +1 -0
  60. ai_dev_tools/utils/subprocess.py +74 -0
@@ -0,0 +1,189 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import tomllib
6
+ from pathlib import Path
7
+
8
+ from ai_dev_tools.config import load_settings
9
+ from ai_dev_tools.detectors.runtime import detect_runtime_requirements
10
+ from ai_dev_tools.detectors.workspaces import detect_workspaces
11
+ from ai_dev_tools.models.report import Report
12
+ from ai_dev_tools.reporters.writer import write_json, write_markdown
13
+
14
+ SIGNALS = {
15
+ "pyproject.toml": "python",
16
+ "requirements.txt": "python",
17
+ "Pipfile": "python",
18
+ "package.json": "javascript",
19
+ "pnpm-lock.yaml": "javascript",
20
+ "yarn.lock": "javascript",
21
+ "pom.xml": "java",
22
+ "build.gradle": "java",
23
+ "build.gradle.kts": "java",
24
+ "Cargo.toml": "rust",
25
+ "composer.json": "php",
26
+ "Dockerfile": "docker",
27
+ "compose.yaml": "docker",
28
+ "docker-compose.yml": "docker",
29
+ "Makefile": "make",
30
+ }
31
+ FRAMEWORK_HINTS = {
32
+ "django": "Django",
33
+ "flask": "Flask",
34
+ "fastapi": "FastAPI",
35
+ "react": "React",
36
+ "next": "Next.js",
37
+ "vue": "Vue",
38
+ "svelte": "Svelte",
39
+ "express": "Express",
40
+ "spring-boot": "Spring Boot",
41
+ "laravel": "Laravel",
42
+ }
43
+
44
+
45
+ def scan_project(project_root: Path) -> Report:
46
+ settings = load_settings(project_root)
47
+ report = Report(command="scan", project_root=settings.project_root)
48
+ files = {path.name: path for path in settings.project_root.iterdir() if path.is_file()}
49
+ scripts = _detect_scripts(settings.project_root)
50
+ dependencies = _dependency_names(settings.project_root)
51
+ workspaces = detect_workspaces(settings.project_root)
52
+ runtime_requirements = detect_runtime_requirements(settings.project_root)
53
+ report.summary = {
54
+ "project_name": settings.project_name or settings.project_root.name,
55
+ "languages": sorted({language for name, language in SIGNALS.items() if name in files}),
56
+ "frameworks": sorted(
57
+ {label for dep, label in FRAMEWORK_HINTS.items() if dep in dependencies}
58
+ ),
59
+ "package_managers": _detect_package_managers(files),
60
+ "entrypoints": _entrypoints(settings.project_root, scripts),
61
+ "tests": _matching_scripts(scripts, ("test", "pytest", "phpunit")),
62
+ "lint": _matching_scripts(scripts, ("lint", "ruff", "eslint", "checkstyle", "clippy")),
63
+ "formatter": _matching_scripts(
64
+ scripts, ("format", "fmt", "black", "prettier", "php-cs-fixer")
65
+ ),
66
+ "type_checker": _matching_scripts(scripts, ("type", "mypy", "tsc", "phpstan")),
67
+ "run": _matching_scripts(scripts, ("start", "dev", "serve", "run")),
68
+ "ci": sorted(
69
+ str(path.relative_to(settings.project_root))
70
+ for path in (settings.project_root / ".github" / "workflows").glob("*.y*ml")
71
+ ),
72
+ "docker": any(
73
+ name in files for name in ("Dockerfile", "compose.yaml", "docker-compose.yml")
74
+ ),
75
+ "env_example_variables": _env_examples(settings.project_root),
76
+ "config_files": sorted(name for name in files if name in SIGNALS or name.startswith(".")),
77
+ "config_warnings": settings.warnings,
78
+ "runtime_requirements": [requirement.to_dict() for requirement in runtime_requirements],
79
+ "workspaces": [workspace.to_dict() for workspace in workspaces],
80
+ "workspace_count": len(workspaces),
81
+ }
82
+ report.finish()
83
+ write_markdown(report, settings.reports_directory / "project-scan.md")
84
+ write_json(report, settings.reports_directory / "project-scan.json")
85
+ return report
86
+
87
+
88
+ def _detect_package_managers(files: dict[str, Path]) -> list[str]:
89
+ pairs = [
90
+ ("pyproject.toml", "pip/pyproject"),
91
+ ("requirements.txt", "pip"),
92
+ ("package.json", "npm"),
93
+ ("pnpm-lock.yaml", "pnpm"),
94
+ ("yarn.lock", "yarn"),
95
+ ("pom.xml", "maven"),
96
+ ("Cargo.toml", "cargo"),
97
+ ("composer.json", "composer"),
98
+ ]
99
+ managers = [manager for filename, manager in pairs if filename in files]
100
+ if "build.gradle" in files or "build.gradle.kts" in files:
101
+ managers.append("gradle")
102
+ return managers
103
+
104
+
105
+ def _detect_scripts(root: Path) -> dict[str, str]:
106
+ scripts: dict[str, str] = {}
107
+ package_json = root / "package.json"
108
+ if package_json.exists():
109
+ try:
110
+ package = json.loads(package_json.read_text(encoding="utf-8"))
111
+ scripts.update({f"npm:{k}": str(v) for k, v in package.get("scripts", {}).items()})
112
+ except json.JSONDecodeError:
113
+ pass
114
+ pyproject = root / "pyproject.toml"
115
+ if pyproject.exists():
116
+ try:
117
+ data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
118
+ scripts.update(
119
+ {
120
+ f"python:{k}": str(v)
121
+ for k, v in data.get("project", {}).get("scripts", {}).items()
122
+ }
123
+ )
124
+ except tomllib.TOMLDecodeError:
125
+ pass
126
+ makefile = root / "Makefile"
127
+ if makefile.exists():
128
+ for line in makefile.read_text(encoding="utf-8", errors="replace").splitlines():
129
+ match = re.match(r"^([A-Za-z0-9_.-]+):", line)
130
+ if match:
131
+ scripts[f"make:{match.group(1)}"] = f"make {match.group(1)}"
132
+ return scripts
133
+
134
+
135
+ def _dependency_names(root: Path) -> set[str]:
136
+ names: set[str] = set()
137
+ package_json = root / "package.json"
138
+ if package_json.exists():
139
+ try:
140
+ package = json.loads(package_json.read_text(encoding="utf-8"))
141
+ names.update(package.get("dependencies", {}))
142
+ names.update(package.get("devDependencies", {}))
143
+ except json.JSONDecodeError:
144
+ pass
145
+ pyproject = root / "pyproject.toml"
146
+ if pyproject.exists():
147
+ try:
148
+ data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
149
+ for dep in data.get("project", {}).get("dependencies", []):
150
+ names.add(str(dep).split("[", 1)[0].split("=", 1)[0].lower())
151
+ for deps in data.get("project", {}).get("optional-dependencies", {}).values():
152
+ for dep in deps:
153
+ names.add(str(dep).split("[", 1)[0].split("=", 1)[0].lower())
154
+ except tomllib.TOMLDecodeError:
155
+ pass
156
+ return names
157
+
158
+
159
+ def _env_examples(root: Path) -> list[str]:
160
+ variables: set[str] = set()
161
+ for path in root.glob(".env*"):
162
+ if path.name == ".env":
163
+ continue
164
+ for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
165
+ if "=" in line and not line.lstrip().startswith("#"):
166
+ variables.add(line.split("=", 1)[0].strip())
167
+ return sorted(variables)
168
+
169
+
170
+ def _matching_scripts(scripts: dict[str, str], needles: tuple[str, ...]) -> dict[str, str]:
171
+ return {
172
+ key: value
173
+ for key, value in scripts.items()
174
+ if any(n in f"{key} {value}".lower() for n in needles)
175
+ }
176
+
177
+
178
+ def _entrypoints(root: Path, scripts: dict[str, str]) -> list[str]:
179
+ found = [
180
+ name
181
+ for name in ("main.py", "app.py", "src/main.py", "index.js", "src/index.ts", "src/main.rs")
182
+ if (root / name).exists()
183
+ ]
184
+ found.extend(
185
+ f"script:{name}"
186
+ for name in scripts
187
+ if any(token in name for token in ("start", "dev", "run"))
188
+ )
189
+ return sorted(found)
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ import fnmatch
4
+ from pathlib import Path
5
+
6
+ from ai_dev_tools.config import load_settings
7
+ from ai_dev_tools.models.report import Report
8
+ from ai_dev_tools.reporters.writer import write_json, write_markdown
9
+
10
+ IMPORTANT_NAMES = {
11
+ "pyproject.toml",
12
+ "package.json",
13
+ "Cargo.toml",
14
+ "composer.json",
15
+ "pom.xml",
16
+ "build.gradle",
17
+ "Dockerfile",
18
+ "compose.yaml",
19
+ "docker-compose.yml",
20
+ "Makefile",
21
+ "README.md",
22
+ "LICENSE",
23
+ }
24
+ BINARY_EXTENSIONS = {
25
+ ".png",
26
+ ".jpg",
27
+ ".jpeg",
28
+ ".gif",
29
+ ".ico",
30
+ ".pdf",
31
+ ".zip",
32
+ ".exe",
33
+ ".dll",
34
+ ".so",
35
+ ".dylib",
36
+ }
37
+ GENERATED_PATTERNS = ("*.min.js", "*.lock", "package-lock.json", "coverage.xml")
38
+
39
+
40
+ def map_repository(project_root: Path, max_files: int = 500, max_depth: int = 6) -> Report:
41
+ settings = load_settings(project_root)
42
+ ignores = settings.ignore_paths | _gitignore_patterns(settings.project_root)
43
+ files: list[Path] = []
44
+ dirs: set[str] = set()
45
+ generated: list[str] = []
46
+ for path in settings.project_root.rglob("*"):
47
+ rel = path.relative_to(settings.project_root)
48
+ if len(rel.parts) > max_depth or _ignored(rel, ignores) or _is_binary(path):
49
+ continue
50
+ if path.is_dir():
51
+ dirs.add(str(rel))
52
+ continue
53
+ files.append(path)
54
+ if any(fnmatch.fnmatch(path.name, pattern) for pattern in GENERATED_PATTERNS):
55
+ generated.append(str(rel))
56
+ important = [
57
+ str(p.relative_to(settings.project_root))
58
+ for p in files
59
+ if p.name in IMPORTANT_NAMES or ".github/workflows" in p.as_posix()
60
+ ]
61
+ tests = [
62
+ str(p.relative_to(settings.project_root))
63
+ for p in files
64
+ if "test" in p.name.lower() or "tests" in p.parts
65
+ ]
66
+ docs = [
67
+ str(p.relative_to(settings.project_root))
68
+ for p in files
69
+ if p.suffix.lower() in {".md", ".rst"} or "docs" in p.parts
70
+ ]
71
+ report = Report(command="map", project_root=settings.project_root)
72
+ truncated = len(files) > max_files
73
+ report.summary = {
74
+ "directories": sorted(dirs)[:max_files],
75
+ "important_files": sorted(important)[:max_files],
76
+ "tests": sorted(tests)[:max_files],
77
+ "ci_workflows": sorted(
78
+ p for p in important if p.replace("\\", "/").startswith(".github/workflows/")
79
+ ),
80
+ "documentation": sorted(docs)[:max_files],
81
+ "generated_or_lock_files": sorted(generated)[:max_files],
82
+ "omitted_patterns": sorted(ignores),
83
+ "file_count_scanned": len(files),
84
+ "max_files": max_files,
85
+ "max_depth": max_depth,
86
+ "truncated": truncated,
87
+ "large_files": _large_files(settings.project_root, files),
88
+ }
89
+ report.finish()
90
+ write_markdown(report, settings.reports_directory / "repository-map.md")
91
+ write_json(report, settings.reports_directory / "repository-map.json")
92
+ return report
93
+
94
+
95
+ def _gitignore_patterns(root: Path) -> set[str]:
96
+ path = root / ".gitignore"
97
+ if not path.exists():
98
+ return set()
99
+ return {
100
+ line.strip().rstrip("/")
101
+ for line in path.read_text(encoding="utf-8", errors="replace").splitlines()
102
+ if line.strip() and not line.startswith("#")
103
+ }
104
+
105
+
106
+ def _ignored(rel: Path, patterns: set[str]) -> bool:
107
+ parts = set(rel.parts)
108
+ text = rel.as_posix()
109
+ normalized_patterns = {pattern.replace("\\", "/") for pattern in patterns}
110
+ return any(
111
+ pattern in parts
112
+ or text == pattern
113
+ or text.startswith(f"{pattern}/")
114
+ or fnmatch.fnmatch(text, pattern)
115
+ or fnmatch.fnmatch(rel.name, pattern)
116
+ for pattern in normalized_patterns
117
+ )
118
+
119
+
120
+ def _is_binary(path: Path) -> bool:
121
+ return path.is_file() and path.suffix.lower() in BINARY_EXTENSIONS
122
+
123
+
124
+ def _large_files(root: Path, files: list[Path]) -> list[str]:
125
+ return [
126
+ str(path.relative_to(root))
127
+ for path in files
128
+ if path.exists() and path.is_file() and path.stat().st_size > 1_000_000
129
+ ][:50]
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import tomllib
6
+ from pathlib import Path
7
+
8
+ from ai_dev_tools.models.workspace import RuntimeRequirement
9
+
10
+ _VERSION = re.compile(r"\d+(?:\.\d+){0,3}")
11
+
12
+
13
+ def detect_runtime_requirements(root: Path) -> list[RuntimeRequirement]:
14
+ requirements: list[RuntimeRequirement] = []
15
+ pyproject = _toml(root / "pyproject.toml")
16
+ project = pyproject.get("project", {})
17
+ if isinstance(project, dict) and isinstance(project.get("requires-python"), str):
18
+ requirements.append(
19
+ RuntimeRequirement("python", project["requires-python"], "pyproject.toml")
20
+ )
21
+ python_version = _first_line(root / ".python-version")
22
+ if python_version:
23
+ requirements.append(RuntimeRequirement("python", python_version, ".python-version"))
24
+
25
+ package = _json(root / "package.json")
26
+ engines = package.get("engines", {})
27
+ if isinstance(engines, dict) and isinstance(engines.get("node"), str):
28
+ requirements.append(RuntimeRequirement("node", engines["node"], "package.json"))
29
+ nvmrc = _first_line(root / ".nvmrc")
30
+ if nvmrc:
31
+ requirements.append(RuntimeRequirement("node", nvmrc, ".nvmrc"))
32
+
33
+ rust_toolchain = _toml(root / "rust-toolchain.toml")
34
+ toolchain = rust_toolchain.get("toolchain", {})
35
+ if isinstance(toolchain, dict) and isinstance(toolchain.get("channel"), str):
36
+ requirements.append(RuntimeRequirement("rust", toolchain["channel"], "rust-toolchain.toml"))
37
+ elif channel := _first_line(root / "rust-toolchain"):
38
+ requirements.append(RuntimeRequirement("rust", channel, "rust-toolchain"))
39
+
40
+ composer = _json(root / "composer.json")
41
+ composer_require = composer.get("require", {})
42
+ if isinstance(composer_require, dict) and isinstance(composer_require.get("php"), str):
43
+ requirements.append(RuntimeRequirement("php", composer_require["php"], "composer.json"))
44
+
45
+ pom_text = _text(root / "pom.xml")
46
+ java_constraint = _first_match(
47
+ pom_text,
48
+ (
49
+ r"<maven\.compiler\.release>\s*([^<]+)",
50
+ r"<maven\.compiler\.source>\s*([^<]+)",
51
+ r"<java\.version>\s*([^<]+)",
52
+ ),
53
+ )
54
+ if java_constraint:
55
+ requirements.append(RuntimeRequirement("java", java_constraint, "pom.xml"))
56
+
57
+ gradle_text = _text(root / "build.gradle") + "\n" + _text(root / "build.gradle.kts")
58
+ gradle_java = _first_match(
59
+ gradle_text,
60
+ (
61
+ r"(?:sourceCompatibility|languageVersion)\s*[=:]?\s*(?:JavaVersion\.VERSION_)?([0-9_\.]+)",
62
+ ),
63
+ )
64
+ if gradle_java:
65
+ requirements.append(
66
+ RuntimeRequirement("java", gradle_java.replace("_", "."), "Gradle configuration")
67
+ )
68
+ return _deduplicate(requirements)
69
+
70
+
71
+ def evaluate_requirement(
72
+ requirement: RuntimeRequirement, detected_version: str | None
73
+ ) -> dict[str, str | None]:
74
+ if detected_version is None:
75
+ return {
76
+ **requirement.to_dict(),
77
+ "detected": None,
78
+ "status": "missing",
79
+ }
80
+ detected = _version_tuple(detected_version)
81
+ if detected is None:
82
+ return {
83
+ **requirement.to_dict(),
84
+ "detected": detected_version,
85
+ "status": "unknown",
86
+ }
87
+ compatible = _matches_constraint(detected, requirement.constraint)
88
+ return {
89
+ **requirement.to_dict(),
90
+ "detected": detected_version,
91
+ "status": "compatible" if compatible else "incompatible",
92
+ }
93
+
94
+
95
+ def _matches_constraint(version: tuple[int, ...], constraint: str) -> bool:
96
+ normalized = constraint.strip().lower().removeprefix("v")
97
+ if normalized in {"stable", "nightly", "beta", "*", "latest"}:
98
+ return True
99
+ alternatives = [item.strip() for item in normalized.split("||")]
100
+ return any(_matches_all(version, alternative) for alternative in alternatives)
101
+
102
+
103
+ def _matches_all(version: tuple[int, ...], constraint: str) -> bool:
104
+ parts = [item for item in re.split(r"\s*,\s*|\s+", constraint) if item]
105
+ if not parts:
106
+ return True
107
+ for part in parts:
108
+ match = re.match(r"(>=|<=|==|=|>|<|\^|~)?\s*v?(\d+(?:\.\d+){0,3})", part)
109
+ if match is None:
110
+ continue
111
+ operator = match.group(1) or "=="
112
+ expected = _version_tuple(match.group(2))
113
+ if expected is None:
114
+ continue
115
+ left, right = _padded(version, expected)
116
+ if operator in {"=", "=="} and left[: len(expected)] != right[: len(expected)]:
117
+ return False
118
+ if operator == ">=" and left < right:
119
+ return False
120
+ if operator == "<=" and left > right:
121
+ return False
122
+ if operator == ">" and left <= right:
123
+ return False
124
+ if operator == "<" and left >= right:
125
+ return False
126
+ if operator == "^":
127
+ upper = (right[0] + 1, *([0] * (len(right) - 1)))
128
+ if left < right or left >= upper:
129
+ return False
130
+ if operator == "~":
131
+ upper = (right[0], (right[1] if len(right) > 1 else 0) + 1, 0, 0)
132
+ upper = upper[: len(right)]
133
+ if left < right or left >= upper:
134
+ return False
135
+ return True
136
+
137
+
138
+ def _padded(
139
+ left: tuple[int, ...], right: tuple[int, ...]
140
+ ) -> tuple[tuple[int, ...], tuple[int, ...]]:
141
+ size = max(len(left), len(right), 3)
142
+ return left + (0,) * (size - len(left)), right + (0,) * (size - len(right))
143
+
144
+
145
+ def _version_tuple(value: str) -> tuple[int, ...] | None:
146
+ match = _VERSION.search(value)
147
+ return tuple(int(item) for item in match.group(0).split(".")) if match else None
148
+
149
+
150
+ def _deduplicate(items: list[RuntimeRequirement]) -> list[RuntimeRequirement]:
151
+ found: dict[tuple[str, str, str], RuntimeRequirement] = {}
152
+ for item in items:
153
+ found[(item.runtime, item.constraint, item.source)] = item
154
+ return list(found.values())
155
+
156
+
157
+ def _first_match(text: str, patterns: tuple[str, ...]) -> str | None:
158
+ for pattern in patterns:
159
+ if match := re.search(pattern, text):
160
+ return match.group(1).strip()
161
+ return None
162
+
163
+
164
+ def _first_line(path: Path) -> str | None:
165
+ text = _text(path).strip()
166
+ return text.splitlines()[0].strip() if text else None
167
+
168
+
169
+ def _text(path: Path) -> str:
170
+ try:
171
+ return path.read_text(encoding="utf-8", errors="replace")
172
+ except OSError:
173
+ return ""
174
+
175
+
176
+ def _toml(path: Path) -> dict[str, object]:
177
+ try:
178
+ with path.open("rb") as handle:
179
+ value = tomllib.load(handle)
180
+ return value if isinstance(value, dict) else {}
181
+ except (OSError, tomllib.TOMLDecodeError):
182
+ return {}
183
+
184
+
185
+ def _json(path: Path) -> dict[str, object]:
186
+ try:
187
+ value = json.loads(path.read_text(encoding="utf-8"))
188
+ return value if isinstance(value, dict) else {}
189
+ except (OSError, json.JSONDecodeError):
190
+ return {}