bugbee 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.
bugbee/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """Top level package for BugBee.
2
+
3
+ Provides version information and exposes the public API.
4
+ """
5
+
6
+ from importlib.metadata import version, PackageNotFoundError
7
+
8
+ try:
9
+ __version__ = version(__name__)
10
+ except PackageNotFoundError: # pragma: no cover – during development the package may not be installed yet
11
+ __version__ = "0.1.0"
12
+
13
+ __all__ = ["__version__"]
@@ -0,0 +1,76 @@
1
+ """Cache handling for BugBee.
2
+
3
+ Provides a thin wrapper around a JSON file (default ``bugbee.json``) used to
4
+ store LLM responses keyed by a deterministic error hash. The API mirrors the
5
+ original script's ``check_cache`` and ``save_to_cache`` functions.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+ from typing import Any, Dict, Optional
13
+
14
+ from bugbee.config.settings import settings
15
+
16
+ __all__ = ["Cache", "cache"]
17
+
18
+
19
+ class Cache:
20
+ """Simple JSON‑file cache.
21
+
22
+ The cache file is created on first use and persisted between runs. Loading
23
+ and writing are performed lazily to avoid disk I/O during CLI start‑up.
24
+ """
25
+
26
+ def __init__(self, path: Optional[Path] = None) -> None:
27
+ self.path = path or settings.cache_file
28
+ self._data: Dict[str, Any] = {}
29
+ self._loaded = False
30
+
31
+ def _load(self) -> None:
32
+ if self._loaded:
33
+ return
34
+ if self.path.is_file():
35
+ try:
36
+ self._data = json.load(self.path.open())
37
+ except json.JSONDecodeError:
38
+ self._data = {}
39
+ else:
40
+ self._data = {}
41
+ self._loaded = True
42
+
43
+ def _save(self) -> None:
44
+ self.path.parent.mkdir(parents=True, exist_ok=True)
45
+ self.path.write_text(json.dumps(self._data, indent=2))
46
+
47
+ def get(self, key: str) -> Optional[Any]:
48
+ self._load()
49
+ return self._data.get(key)
50
+
51
+ def set(self, key: str, value: Any) -> None:
52
+ self._load()
53
+ self._data[key] = value
54
+ self._save()
55
+
56
+ def clear(self) -> None:
57
+ self._data.clear()
58
+ self._loaded = True
59
+ if self.path.is_file():
60
+ self.path.unlink()
61
+
62
+ # Export a lazy accessor for the cache. The cache object is instantiated
63
+ # only when first needed, avoiding file I/O during CLI start‑up.
64
+
65
+ def get_cache() -> Cache:
66
+ """Return a shared ``Cache`` instance, creating it on first call.
67
+
68
+ This mirrors the previous ``cache`` singleton but defers the actual object
69
+ construction (and the associated disk read) until the cache is accessed.
70
+ """
71
+ global _cache_instance
72
+ try:
73
+ return _cache_instance
74
+ except NameError:
75
+ _cache_instance = Cache()
76
+ return _cache_instance
bugbee/cli/__init__.py ADDED
File without changes
bugbee/cli/main.py ADDED
@@ -0,0 +1,258 @@
1
+ """CLI entry point for the BugBee package.
2
+
3
+ The original monolithic implementation lived in ``cli-wrapper/src/cli_wrapper/main.py``.
4
+ This version keeps the ``run`` command as a clean pipeline built on top of:
5
+
6
+ * ``bugbee.parser.error_parser`` – stack‑trace extraction, sanitisation, hashing
7
+ * ``bugbee.cache`` – JSON cache wrapper
8
+ * ``bugbee.retrieval`` – ChromaDB document retrieval
9
+ * ``bugbee.llm`` – LLM provider abstraction
10
+ * ``bugbee.patcher`` – safe patch application with backup
11
+ * ``bugbee.validator`` – post‑patch validation runner
12
+ * ``bugbee.metrics`` – metrics collection and JSONL persistence
13
+ * ``bugbee.ui`` – ALL logging and terminal presentation (spinners, colored
14
+ status lines, panels, tables, confirmation prompts) lives here. This file
15
+ contains no direct ``log.*`` or ``typer.echo`` calls — every user-facing
16
+ message is routed through the ``ui`` singleton so the pipeline below reads
17
+ as pure business logic.
18
+
19
+ The command line mimics the historic behaviour while being fully testable,
20
+ type‑safe, and pleasant to actually use.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import re
26
+ import sys
27
+ from pathlib import Path
28
+ from typing import List, Tuple
29
+
30
+ import typer
31
+
32
+ from bugbee import __version__
33
+ from bugbee.config.settings import settings
34
+ from bugbee.parser.error_parser import (
35
+ extract_location,
36
+ sanitize_error,
37
+ error_hash,
38
+ )
39
+ from bugbee.cache import get_cache
40
+ from bugbee.lazy import lazy_import
41
+ from bugbee.ui import ui
42
+ from bugbee.utils.logging import enable_console_logging
43
+ # Heavy imports are lazy‑loaded inside the command handlers.
44
+ # from bugbee.retrieval import get_related_docs
45
+ # from bugbee.llm import default_provider
46
+ # from bugbee.metrics import MetricsCollector
47
+ # from bugbee.patcher import apply_patch
48
+ # from bugbee.validator import run_validation
49
+
50
+ app = typer.Typer(
51
+ help="BugBee – AI‑powered CLI debugger and auto‑repair tool",
52
+ rich_markup_mode="rich",
53
+ )
54
+
55
+
56
+ @app.callback()
57
+ def main(
58
+ verbose: bool = typer.Option(
59
+ False,
60
+ "--verbose",
61
+ "-v",
62
+ help="Also print raw, timestamped log lines to the console (in addition to the normal UI).",
63
+ ),
64
+ ) -> None:
65
+ """BugBee – AI‑powered CLI debugger and auto‑repair tool."""
66
+ if verbose:
67
+ enable_console_logging()
68
+
69
+
70
+ def _run_subprocess(command: List[str]) -> Tuple[int, str, str]:
71
+ """Execute *command* and return ``(returncode, stdout, stderr)``.
72
+
73
+ The function deliberately forwards stdout directly to the console (as the
74
+ original tool did) and captures stderr for analysis.
75
+ """
76
+ import subprocess
77
+
78
+ ui.running_command(command)
79
+ proc = subprocess.Popen(
80
+ command,
81
+ stdout=subprocess.PIPE,
82
+ stderr=subprocess.PIPE,
83
+ text=True,
84
+ )
85
+ out, err = proc.communicate()
86
+ # Echo stdout to the console to preserve original behaviour.
87
+ if out:
88
+ sys.stdout.write(out)
89
+ return proc.returncode, out, err
90
+
91
+
92
+ def _parse_llm_output(output: str) -> Tuple[str, str, str]:
93
+ """Extract ``error_line`` and ``fix`` from the LLM ``output``.
94
+
95
+ The original script expected XML‑like tags <ERROR_LINE> and <FIX>. We keep
96
+ that contract for backward compatibility.
97
+ """
98
+ error_line_match = re.search(r"<ERROR_LINE>(.*?)</ERROR_LINE>", output, re.DOTALL)
99
+ fix_match = re.search(r"<FIX>(.*?)</FIX>", output, re.DOTALL)
100
+ error_line = error_line_match.group(1).strip() if error_line_match else ""
101
+ fix = fix_match.group(1).strip() if fix_match else ""
102
+ # The full content without the tags is also useful for reporting.
103
+ content = output.strip()
104
+ return content, error_line, fix
105
+
106
+
107
+ @app.command()
108
+ def run(
109
+ command: List[str] = typer.Argument(..., help="Command to execute and debug"),
110
+ yes: bool = typer.Option(False, "--yes", "-y", help="Auto‑accept fix without prompting"),
111
+ ) -> None:
112
+ """Execute *command*, analyse failures and optionally apply an auto‑fix."""
113
+ # Lazy imports of heavy modules – they are only loaded when this command runs.
114
+ MetricsCollector = lazy_import('bugbee.metrics').MetricsCollector
115
+ get_related_docs = lazy_import('bugbee.retrieval').get_related_docs
116
+ default_provider = lazy_import('bugbee.llm').default_provider
117
+ apply_patch = lazy_import('bugbee.patcher').apply_patch
118
+ run_validation = lazy_import('bugbee.validator').run_validation
119
+
120
+ metrics = MetricsCollector()
121
+ metrics.update("command", " ".join(command))
122
+
123
+ returncode, _, stderr = _run_subprocess(command)
124
+ if returncode == 0:
125
+ ui.command_succeeded()
126
+ metrics.write()
127
+ raise typer.Exit(code=0)
128
+
129
+ ui.command_failed()
130
+
131
+ # 1️⃣ Extract location info (file + line) – may be ``None``.
132
+ file_path, line_number = extract_location(stderr, command)
133
+ if file_path and line_number:
134
+ ui.location_found(file_path, line_number)
135
+ else:
136
+ ui.location_not_found()
137
+
138
+ # 2️⃣ Sanitize error and compute hash for caching.
139
+ cleaned = sanitize_error(stderr)
140
+ err_hash = error_hash(cleaned)
141
+ cache_obj = get_cache()
142
+ cached = cache_obj.get(err_hash)
143
+ if cached:
144
+ ui.cache_hit()
145
+ llm_content, error_line, fix = cached
146
+ else:
147
+ ui.cache_miss()
148
+ # Retrieve relevant docs.
149
+ with ui.spinner("Retrieving relevant documentation…"):
150
+ docs, top_score, avg_score = get_related_docs(stderr)
151
+ metrics.update("retrieval.top_score", top_score)
152
+ metrics.update("retrieval.avg_score", avg_score)
153
+ ui.retrieval_stats(top_score, avg_score)
154
+
155
+ # Very simple prompt composition.
156
+ prompt = (
157
+ f"Error message:\n{stderr}\n\n"
158
+ f"Relevant documentation:\n{docs}\n\n"
159
+ "Provide an XML-like response containing <ERROR_LINE> and <FIX>."
160
+ )
161
+ with ui.spinner("Consulting the model for a fix…"):
162
+ llm_resp = default_provider().generate(prompt)
163
+
164
+ llm_content, error_line, fix = _parse_llm_output(llm_resp)
165
+ cache_obj.set(err_hash, (llm_content, error_line, fix))
166
+
167
+ # Show the LLM analysis to the user.
168
+ ui.show_analysis(llm_content)
169
+
170
+ # Auto‑fix handling.
171
+ performed = False
172
+ accept = ui.confirm_fix(yes)
173
+ if accept and file_path:
174
+ ui.applying_patch(file_path, line_number)
175
+ try:
176
+ success = apply_patch(file_path, line_number, error_line, fix + "\n", create_backup=True)
177
+ if success:
178
+ performed = True
179
+ metrics.update("autofix.patch_applied", True)
180
+ metrics.update("autofix.files_modified", 1)
181
+ ui.patch_applied()
182
+ # Run validation to ensure fix works.
183
+ with ui.spinner("Validating the fix…"):
184
+ validation_ok = run_validation(command, cwd=Path.cwd())
185
+ if validation_ok:
186
+ metrics.update("validation.passed", True)
187
+ ui.validation_passed()
188
+ else:
189
+ ui.validation_failed()
190
+ else:
191
+ ui.patch_not_applied()
192
+ except Exception as exc:
193
+ ui.patch_error(exc)
194
+ else:
195
+ ui.auto_fix_declined()
196
+
197
+ # Record final metrics.
198
+ metrics.update("diagnosis.error_line_found", bool(error_line))
199
+ metrics.update("diagnosis.fix_generated", bool(fix))
200
+ metrics.write()
201
+
202
+ ui.summary(fix_applied=performed, returncode=returncode)
203
+
204
+ # Exit with the original return code if no successful fix was applied.
205
+ if performed:
206
+ raise typer.Exit(code=0)
207
+ else:
208
+ raise typer.Exit(code=returncode)
209
+
210
+
211
+ @app.command()
212
+ def analyze(
213
+ command: List[str] = typer.Argument(..., help="Command to analyze without applying a fix"),
214
+ ) -> None:
215
+ """Run *command* and display LLM analysis but do not prompt for auto‑fix."""
216
+ # Reuse ``run`` logic but force ``yes=False`` and skip patch step.
217
+ run(command=command, yes=False)
218
+
219
+
220
+ @app.command()
221
+ def version() -> None:
222
+ """Print the installed BugBee version."""
223
+ ui.version(__version__)
224
+
225
+
226
+ @app.command()
227
+ def cache_clear() -> None:
228
+ """Remove the local cache file (``bugbee.json``)."""
229
+ get_cache().clear()
230
+ ui.cache_cleared()
231
+
232
+
233
+ @app.command()
234
+ def config_show() -> None:
235
+ """Display the effective configuration (environment variables, .env, defaults)."""
236
+ ui.config_table(settings.dict())
237
+
238
+
239
+ @app.command()
240
+ def doctor() -> None:
241
+ """Run a quick health‑check of required dependencies and connectivity."""
242
+ ui.doctor_start()
243
+ missing = []
244
+ try:
245
+ import chromadb # noqa: F401
246
+ except Exception:
247
+ missing.append("chromadb")
248
+ try:
249
+ import langchain # noqa: F401
250
+ except Exception:
251
+ missing.append("langchain")
252
+ ui.doctor_result(missing)
253
+ if missing:
254
+ raise typer.Exit(code=1)
255
+
256
+
257
+ if __name__ == "__main__":
258
+ app()
@@ -0,0 +1,116 @@
1
+ """Configuration handling for BugBee.
2
+
3
+ BugBee loads configuration from:
4
+
5
+ 1. Environment variables
6
+ 2. .env file
7
+ 3. Optional YAML configuration
8
+
9
+ Environment variables always take precedence.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+ from typing import Any, Mapping
16
+
17
+ import yaml
18
+ from pydantic import Field
19
+ from pydantic_settings import BaseSettings, SettingsConfigDict
20
+ from platformdirs import user_cache_dir, user_data_dir
21
+
22
+
23
+ class Settings(BaseSettings):
24
+ """Global configuration for BugBee."""
25
+
26
+ # ------------------------------------------------------------------
27
+ # Pydantic Settings Configuration
28
+ # ------------------------------------------------------------------
29
+
30
+ model_config = SettingsConfigDict(
31
+ env_file=".env",
32
+ env_file_encoding="utf-8",
33
+ case_sensitive=False,
34
+ extra="ignore", # Ignore unknown environment variables
35
+ )
36
+
37
+ # ------------------------------------------------------------------
38
+ # General
39
+ # ------------------------------------------------------------------
40
+
41
+ log_level: str = "INFO"
42
+
43
+ temperature: float = 0.1
44
+
45
+ retrieval_k: int = 3
46
+
47
+ # ------------------------------------------------------------------
48
+ # Paths
49
+ # ------------------------------------------------------------------
50
+
51
+ cache_file: Path = Field(
52
+ default_factory=lambda: Path(user_cache_dir("bugbee")) / "cache.json"
53
+ )
54
+
55
+ chromadb_path: Path = Field(
56
+ default_factory=lambda: Path(user_data_dir("bugbee")) / "chromadb"
57
+ )
58
+
59
+ # ------------------------------------------------------------------
60
+ # LLM Configuration
61
+ # ------------------------------------------------------------------
62
+
63
+ llm_endpoint: str = (
64
+ "https://api-inference.huggingface.co/models/deepseek-ai/DeepSeek-R1"
65
+ )
66
+
67
+ # ------------------------------------------------------------------
68
+ # API Keys
69
+ # ------------------------------------------------------------------
70
+
71
+ openai_api_key: str | None = Field(
72
+ default=None,
73
+ alias="OPENAI_API_KEY",
74
+ )
75
+
76
+ anthropic_api_key: str | None = Field(
77
+ default=None,
78
+ alias="ANTHROPIC_API_KEY",
79
+ )
80
+
81
+ google_api_key: str | None = Field(
82
+ default=None,
83
+ alias="GOOGLE_API_KEY",
84
+ )
85
+
86
+ openrouter_api_key: str | None = Field(
87
+ default=None,
88
+ alias="OPENROUTER_API_KEY",
89
+ )
90
+
91
+ huggingface_api_key: str | None = Field(
92
+ default=None,
93
+ validation_alias="HF_TOKEN",
94
+ )
95
+
96
+
97
+ # ------------------------------------------------------------------
98
+ # Optional YAML Loader
99
+ # ------------------------------------------------------------------
100
+
101
+ @classmethod
102
+ def from_yaml(cls, yaml_path: Path) -> "Settings":
103
+ """Load configuration from a YAML file."""
104
+
105
+ if not yaml_path.exists():
106
+ return cls()
107
+
108
+ data: Mapping[str, Any] = yaml.safe_load(
109
+ yaml_path.read_text(encoding="utf-8")
110
+ ) or {}
111
+
112
+ return cls(**data)
113
+
114
+
115
+ # Singleton instance used across the project.
116
+ settings = Settings()
bugbee/lazy.py ADDED
@@ -0,0 +1,42 @@
1
+ """Utility helpers for lazy importing heavy dependencies.
2
+
3
+ This module provides a simple ``lazy_import`` function that defers the actual
4
+ ``import`` until the first attribute access. It is used throughout the codebase
5
+ to keep the CLI start‑up path lightweight.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import sys
12
+ import types
13
+ from typing import Any
14
+
15
+
16
+ def lazy_import(module_name: str) -> types.ModuleType:
17
+ """Return a proxy module that imports ``module_name`` on first attribute use.
18
+
19
+ The returned object behaves like the real module but delays the import until
20
+ an attribute or ``__getattr__`` is accessed. Subsequent accesses reuse the
21
+ already-loaded module.
22
+ """
23
+
24
+ class _LazyModule(types.ModuleType):
25
+ __dict__: dict[str, Any]
26
+ _module: types.ModuleType | None = None
27
+
28
+ def _load(self) -> types.ModuleType:
29
+ if self._module is None:
30
+ self._module = importlib.import_module(module_name)
31
+ # Insert the loaded module into ``sys.modules`` so other imports
32
+ # resolve to the same object.
33
+ sys.modules[module_name] = self._module
34
+ return self._module
35
+
36
+ def __getattr__(self, name: str) -> Any: # pragma: no cover – exercised at runtime
37
+ return getattr(self._load(), name)
38
+
39
+ def __dir__(self) -> list[str]: # pragma: no cover
40
+ return dir(self._load())
41
+
42
+ return _LazyModule(module_name)
bugbee/legacy.py ADDED
@@ -0,0 +1,31 @@
1
+ """Legacy shim for backward compatibility.
2
+
3
+ The original project exposed a ``main`` function in ``cli-wrapper/src/cli_wrapper/main.py``.
4
+ We import that function and expose it here so existing ``import bugbee.legacy``
5
+ or ``python -m bugbee.legacy`` continues to work.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ # Ensure the original source directory is on ``sys.path``.
12
+ _PROJECT_ROOT = Path(__file__).resolve().parents[2] # points to repository root
13
+ _SRC_DIR = _PROJECT_ROOT / "cli-wrapper" / "src" / "cli_wrapper"
14
+ if str(_SRC_DIR) not in sys.path:
15
+ sys.path.insert(0, str(_SRC_DIR))
16
+
17
+ # Import the historic ``main`` function.
18
+ try:
19
+ from main import main as _original_main # type: ignore
20
+ except Exception as exc: # pragma: no cover – during early development the module may not be importable yet
21
+ raise ImportError("Unable to import legacy main function") from exc
22
+
23
+
24
+ def main() -> None:
25
+ """Entry point that forwards to the original implementation.
26
+
27
+ This wrapper exists so that external scripts that called the old ``main``
28
+ continue to operate. All behaviour (including interactive prompts) is
29
+ unchanged.
30
+ """
31
+ _original_main()
bugbee/llm/__init__.py ADDED
@@ -0,0 +1,59 @@
1
+ """
2
+ LLM provider for BugBee.
3
+
4
+ Currently BugBee uses DeepSeek-R1 through the Hugging Face Inference API.
5
+
6
+ Future versions can add OpenAI, Anthropic, Gemini, Ollama, etc.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from functools import lru_cache
12
+
13
+ from bugbee.config.settings import settings
14
+ from bugbee.lazy import lazy_import
15
+
16
+ # Lazy imports (only loaded when the provider is created)
17
+ ChatHuggingFace = lazy_import("langchain_huggingface").ChatHuggingFace
18
+ HuggingFaceEndpoint = lazy_import("langchain_huggingface").HuggingFaceEndpoint
19
+ HumanMessage = lazy_import("langchain_core.messages").HumanMessage
20
+
21
+
22
+ class DeepSeekProvider:
23
+ """DeepSeek provider backed by Hugging Face Inference API."""
24
+
25
+ def __init__(self) -> None:
26
+
27
+ self.endpoint = HuggingFaceEndpoint(
28
+ repo_id="deepseek-ai/DeepSeek-R1",
29
+ task="text-generation",
30
+ huggingfacehub_api_token=settings.huggingface_api_key,
31
+ temperature=settings.temperature,
32
+ )
33
+
34
+ self.model = ChatHuggingFace(
35
+ llm=self.endpoint,
36
+ )
37
+
38
+ def generate(self, prompt: str) -> str:
39
+ """Generate a response from DeepSeek."""
40
+
41
+ response = self.model.invoke(
42
+ [
43
+ HumanMessage(
44
+ content=prompt
45
+ )
46
+ ]
47
+ )
48
+
49
+ return response.content
50
+
51
+
52
+ @lru_cache(maxsize=1)
53
+ def default_provider() -> DeepSeekProvider:
54
+ """
55
+ Singleton provider.
56
+
57
+ Prevents recreating the model every request.
58
+ """
59
+ return DeepSeekProvider()
@@ -0,0 +1,58 @@
1
+ """Metrics collection for BugBee.
2
+
3
+ The original script accumulated a dictionary ``metrics_obj`` and appended JSON
4
+ lines to ``metrics.jsonl``. ``MetricsCollector`` provides a thin wrapper around
5
+ that behaviour, exposing methods to update sections and write a record. All
6
+ timestamps are ISO‑8601 strings.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import time
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ from typing import Any, Dict
16
+
17
+ from bugbee.config.settings import settings
18
+
19
+ __all__ = ["MetricsCollector"]
20
+
21
+
22
+ class MetricsCollector:
23
+ """Collect and persist metrics for a single run.
24
+
25
+ Usage example::
26
+
27
+ collector = MetricsCollector()
28
+ collector.update("command", "python myscript.py")
29
+ collector.update("retrieval", {"top_score": 0.9})
30
+ collector.write()
31
+ """
32
+
33
+ def __init__(self, file_path: Path | None = None) -> None:
34
+ self.file_path = file_path or Path.cwd() / "metrics.jsonl"
35
+ self.metrics: Dict[str, Any] = {
36
+ "timestamp": datetime.utcnow().isoformat(),
37
+ "command": "",
38
+ "exceptionType": "NA",
39
+ "retrieval": {"top_score": 0.0, "avg_score": 0.0, "latency_ms": 0.0},
40
+ "llm": {"input_tokens": 0, "output_tokens": 0, "latency_ms": 0.0},
41
+ "diagnosis": {"error_line_found": False, "fix_generated": False},
42
+ "autofix": {"patch_applied": False, "files_modified": 0},
43
+ "validation": {"passed": False},
44
+ }
45
+
46
+ def update(self, key: str, value: Any) -> None:
47
+ """Set *key* to *value* – creates nested dicts as needed."""
48
+ parts = key.split(".")
49
+ target = self.metrics
50
+ for part in parts[:-1]:
51
+ target = target.setdefault(part, {})
52
+ target[parts[-1]] = value
53
+
54
+ def write(self) -> None:
55
+ """Append the current metrics as a JSON line to ``metrics.jsonl``."""
56
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
57
+ with self.file_path.open("a", encoding="utf-8") as f:
58
+ f.write(json.dumps(self.metrics) + "\n")
File without changes