wup 0.2.27__tar.gz → 0.2.28__tar.gz

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 (36) hide show
  1. {wup-0.2.27/wup.egg-info → wup-0.2.28}/PKG-INFO +6 -6
  2. {wup-0.2.27 → wup-0.2.28}/README.md +5 -5
  3. {wup-0.2.27 → wup-0.2.28}/pyproject.toml +1 -1
  4. {wup-0.2.27 → wup-0.2.28}/tests/test_testql_monitor.py +7 -0
  5. {wup-0.2.27 → wup-0.2.28}/tests/test_testql_watcher.py +78 -1
  6. {wup-0.2.27 → wup-0.2.28}/wup/__init__.py +1 -1
  7. {wup-0.2.27 → wup-0.2.28}/wup/testql_monitor.py +29 -1
  8. {wup-0.2.27 → wup-0.2.28}/wup/testql_watcher.py +56 -4
  9. {wup-0.2.27 → wup-0.2.28/wup.egg-info}/PKG-INFO +6 -6
  10. {wup-0.2.27 → wup-0.2.28}/LICENSE +0 -0
  11. {wup-0.2.27 → wup-0.2.28}/setup.cfg +0 -0
  12. {wup-0.2.27 → wup-0.2.28}/tests/test_e2e.py +0 -0
  13. {wup-0.2.27 → wup-0.2.28}/tests/test_monitoring_manifest.py +0 -0
  14. {wup-0.2.27 → wup-0.2.28}/tests/test_web_client.py +0 -0
  15. {wup-0.2.27 → wup-0.2.28}/tests/test_wup.py +0 -0
  16. {wup-0.2.27 → wup-0.2.28}/wup/_ast_detector.py +0 -0
  17. {wup-0.2.27 → wup-0.2.28}/wup/_hash_detector.py +0 -0
  18. {wup-0.2.27 → wup-0.2.28}/wup/_yaml_detector.py +0 -0
  19. {wup-0.2.27 → wup-0.2.28}/wup/anomaly_detector.py +0 -0
  20. {wup-0.2.27 → wup-0.2.28}/wup/anomaly_models.py +0 -0
  21. {wup-0.2.27 → wup-0.2.28}/wup/assistant.py +0 -0
  22. {wup-0.2.27 → wup-0.2.28}/wup/cli.py +0 -0
  23. {wup-0.2.27 → wup-0.2.28}/wup/config.py +0 -0
  24. {wup-0.2.27 → wup-0.2.28}/wup/core.py +0 -0
  25. {wup-0.2.27 → wup-0.2.28}/wup/dependency_mapper.py +0 -0
  26. {wup-0.2.27 → wup-0.2.28}/wup/models/__init__.py +0 -0
  27. {wup-0.2.27 → wup-0.2.28}/wup/models/config.py +0 -0
  28. {wup-0.2.27 → wup-0.2.28}/wup/monitoring_manifest.py +0 -0
  29. {wup-0.2.27 → wup-0.2.28}/wup/testql_discovery.py +0 -0
  30. {wup-0.2.27 → wup-0.2.28}/wup/visual_diff.py +0 -0
  31. {wup-0.2.27 → wup-0.2.28}/wup/web_client.py +0 -0
  32. {wup-0.2.27 → wup-0.2.28}/wup.egg-info/SOURCES.txt +0 -0
  33. {wup-0.2.27 → wup-0.2.28}/wup.egg-info/dependency_links.txt +0 -0
  34. {wup-0.2.27 → wup-0.2.28}/wup.egg-info/entry_points.txt +0 -0
  35. {wup-0.2.27 → wup-0.2.28}/wup.egg-info/requires.txt +0 -0
  36. {wup-0.2.27 → wup-0.2.28}/wup.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.27
3
+ Version: 0.2.28
4
4
  Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -29,17 +29,17 @@ Dynamic: license-file
29
29
 
30
30
  ## AI Cost Tracking
31
31
 
32
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.27-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
33
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.13-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-16.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.28-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
33
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.27-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-16.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
34
34
 
35
- - 🤖 **LLM usage:** $2.1268 (37 commits)
35
+ - 🤖 **LLM usage:** $2.2653 (38 commits)
36
36
  - 👤 **Human dev:** ~$1632 (16.3h @ $100/h, 30min dedup)
37
37
 
38
- Generated on 2026-05-16 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
38
+ Generated on 2026-05-17 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
39
39
 
40
40
  ---
41
41
 
42
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.27-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.28-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
43
43
 
44
44
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
45
45
 
@@ -3,17 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.27-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.13-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-16.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.28-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.27-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-16.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $2.1268 (37 commits)
9
+ - 🤖 **LLM usage:** $2.2653 (38 commits)
10
10
  - 👤 **Human dev:** ~$1632 (16.3h @ $100/h, 30min dedup)
11
11
 
12
- Generated on 2026-05-16 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
12
+ Generated on 2026-05-17 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.27-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.28-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
19
19
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wup"
7
- version = "0.2.27"
7
+ version = "0.2.28"
8
8
  description = "WUP (What's Up) - Intelligent file watcher for regression testing in large projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -34,6 +34,13 @@ API[1]{method, endpoint, expected_status}:
34
34
  assert is_monitoring_probe(probes[0])
35
35
 
36
36
 
37
+ def test_firmware_plugin_health_on_8202_not_live_probe():
38
+ probe = ProbeTarget(url="http://localhost:8202/api/v1/plugins/modbus-io/health")
39
+ assert not is_monitoring_probe(probe)
40
+ direct = ProbeTarget(url="http://localhost:8202/health")
41
+ assert is_monitoring_probe(direct)
42
+
43
+
37
44
  def test_connect_api_paths_on_8100_are_not_monitoring_probes():
38
45
  probe = ProbeTarget(url="http://localhost:8100/api/id/health")
39
46
  assert not is_monitoring_probe(probe)
@@ -6,7 +6,14 @@ from pathlib import Path
6
6
  from subprocess import CompletedProcess
7
7
 
8
8
  from wup.testql_watcher import TestQLWatcher
9
- from wup.models.config import WupConfig, ProjectConfig, ServiceConfig, TestQLConfig, VisualDiffConfig
9
+ from wup.models.config import (
10
+ ProjectConfig,
11
+ ServiceConfig,
12
+ TestQLConfig,
13
+ VisualDiffConfig,
14
+ WatchConfig,
15
+ WupConfig,
16
+ )
10
17
 
11
18
 
12
19
  def test_process_changed_file_creates_track_on_failure():
@@ -203,6 +210,76 @@ def test_service_health_transitions_are_persisted():
203
210
  assert "up" in statuses
204
211
 
205
212
 
213
+ def test_normalize_fleet_health_entry_down_to_degraded():
214
+ with tempfile.TemporaryDirectory() as tmpdir:
215
+ root = Path(tmpdir)
216
+ health_path = root / ".wup" / "service-health.json"
217
+ health_path.parent.mkdir(parents=True)
218
+ health_path.write_text(
219
+ json.dumps(
220
+ {
221
+ "demo": {
222
+ "status": "down",
223
+ "stage": "health_scenario",
224
+ "message": "partial",
225
+ }
226
+ }
227
+ ),
228
+ encoding="utf-8",
229
+ )
230
+ cfg = WupConfig(
231
+ project=ProjectConfig(name="demo"),
232
+ services=[ServiceConfig(name="frontend", paths=["frontend/**"])],
233
+ watch=WatchConfig(),
234
+ testql=TestQLConfig(health_scenario_strict=False),
235
+ )
236
+ TestQLWatcher(
237
+ project_root=str(root),
238
+ deps_file=str(root / "deps.json"),
239
+ scenarios_dir="testql-scenarios",
240
+ track_dir=".wup/tracks",
241
+ config=cfg,
242
+ )
243
+ state = json.loads(health_path.read_text(encoding="utf-8"))
244
+ assert state["demo"]["status"] == "degraded"
245
+
246
+
247
+ def test_fleet_health_scenario_non_strict_records_degraded_not_down():
248
+ with tempfile.TemporaryDirectory() as tmpdir:
249
+ root = Path(tmpdir)
250
+ scenario_dir = root / "testql-scenarios"
251
+ scenario_dir.mkdir(parents=True, exist_ok=True)
252
+ (scenario_dir / "fleet.testql.toon.yaml").write_text("name: fleet\n", encoding="utf-8")
253
+
254
+ cfg = WupConfig(
255
+ project=ProjectConfig(name="demo"),
256
+ services=[ServiceConfig(name="frontend", paths=["frontend/**"])],
257
+ watch=WatchConfig(),
258
+ testql=TestQLConfig(
259
+ scenario_dir="testql-scenarios",
260
+ health_scenario="fleet.testql.toon.yaml",
261
+ health_scenario_strict=False,
262
+ ),
263
+ )
264
+ watcher = TestQLWatcher(
265
+ project_root=str(root),
266
+ deps_file=str(root / "deps.json"),
267
+ scenarios_dir="testql-scenarios",
268
+ track_dir=".wup/tracks",
269
+ config=cfg,
270
+ )
271
+ watcher._run_testql = lambda args, timeout: CompletedProcess( # type: ignore[method-assign]
272
+ args=args,
273
+ returncode=1,
274
+ stdout='{"passed": 1, "failed": 1, "errors": ["L1: bad"]}',
275
+ stderr="",
276
+ )
277
+ assert asyncio.run(watcher._run_fleet_health_scenario()) is True
278
+ state = json.loads((root / ".wup" / "service-health.json").read_text(encoding="utf-8"))
279
+ assert state["demo"]["status"] == "degraded"
280
+ assert state["demo"]["stage"] == "health_scenario"
281
+
282
+
206
283
  def test_visual_differ_disabled_by_default():
207
284
  """visual_differ exists but is disabled (no-op) when visual_diff.enabled=False."""
208
285
  with tempfile.TemporaryDirectory() as tmpdir:
@@ -7,7 +7,7 @@ WUP monitors file changes and runs intelligent regression tests using a 3-layer
7
7
  3. Detail Layer: Full tests with blame reports (only on failure)
8
8
  """
9
9
 
10
- __version__ = "0.2.27"
10
+ __version__ = "0.2.28"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -125,10 +125,23 @@ def _connect_module_api_on_frontend_proxy(probe: ProbeTarget) -> bool:
125
125
  return any(path.startswith(prefix) for prefix in _CONNECT_API_PREFIXES)
126
126
 
127
127
 
128
+ def _firmware_plugin_probe_without_runtime(probe: ProbeTarget) -> bool:
129
+ """Plugin health on :8202 requires loaded plugins — skip for bare simulator live probes."""
130
+ if not probe.url.startswith("http"):
131
+ return False
132
+ parsed = urlparse(probe.url)
133
+ if parsed.port != 8202:
134
+ return False
135
+ path = (parsed.path or "").lower()
136
+ return "/api/v1/plugins/" in path and path.endswith("/health")
137
+
138
+
128
139
  def is_monitoring_probe(probe: ProbeTarget) -> bool:
129
140
  """True when this endpoint should be used for live service health checks."""
130
141
  if _connect_module_api_on_frontend_proxy(probe):
131
142
  return False
143
+ if _firmware_plugin_probe_without_runtime(probe):
144
+ return False
132
145
  if probe.url.startswith("http"):
133
146
  path = urlparse(probe.url).path or probe.url
134
147
  else:
@@ -334,6 +347,20 @@ class TestQLMonitor:
334
347
 
335
348
  return [p for p in merged if p.url.startswith("http://") or p.url.startswith("https://")]
336
349
 
350
+ @staticmethod
351
+ def _sort_probes_for_live(probes: Sequence[ProbeTarget]) -> List[ProbeTarget]:
352
+ """Prefer wup.yaml endpoints before scenario discovery for pass/fail."""
353
+
354
+ def rank(probe: ProbeTarget) -> Tuple[int, str]:
355
+ source = probe.source or ""
356
+ if source.startswith("wup.yaml:endpoints_by_service"):
357
+ return (0, probe.url)
358
+ if source.startswith("wup.yaml:explicit_endpoints"):
359
+ return (1, probe.url)
360
+ return (2, probe.url)
361
+
362
+ return sorted(probes, key=rank)
363
+
337
364
  def run_probes(
338
365
  self,
339
366
  service: str,
@@ -347,7 +374,8 @@ class TestQLMonitor:
347
374
  return True, ""
348
375
 
349
376
  failed: List[str] = []
350
- for probe in list(probes)[:max_count]:
377
+ ordered = self._sort_probes_for_live(probes)
378
+ for probe in ordered[:max_count]:
351
379
  ok, detail = probe.probe(timeout_s=timeout_s)
352
380
  if ok:
353
381
  continue
@@ -101,6 +101,27 @@ class TestQLWatcher(WupWatcher):
101
101
  self.visual_differ = VisualDiffer(project_root, config.visual_diff) if config and config.visual_diff else None
102
102
  self.web_client = WebClient(config.web) if config and getattr(config, "web", None) else WebClient()
103
103
  self._probe_thread = None
104
+ self._normalize_fleet_health_entry()
105
+
106
+ def _normalize_fleet_health_entry(self) -> None:
107
+ """Upgrade stale fleet ``down`` to ``degraded`` when health_scenario is non-strict."""
108
+ if not self.config:
109
+ return
110
+ strict = bool(getattr(self.config.testql, "health_scenario_strict", False))
111
+ if strict:
112
+ return
113
+ fleet = self.config.project.name
114
+ entry = self.service_health.get(fleet)
115
+ if not isinstance(entry, dict):
116
+ return
117
+ if entry.get("stage") != "health_scenario":
118
+ return
119
+ if str(entry.get("status", "")).lower() != "down":
120
+ return
121
+ entry = dict(entry)
122
+ entry["status"] = "degraded"
123
+ self.service_health[fleet] = entry
124
+ self._save_service_health()
104
125
 
105
126
  def _load_service_health(self) -> Dict[str, Dict]:
106
127
  if not self.health_state_path.exists():
@@ -472,6 +493,36 @@ class TestQLWatcher(WupWatcher):
472
493
  )
473
494
  return False
474
495
 
496
+ @staticmethod
497
+ def _summarize_health_scenario_failure(result: subprocess.CompletedProcess) -> str:
498
+ """Extract a short human summary from TestQL --output json (avoid trailing '}')."""
499
+ blob = "\n".join(part for part in (result.stdout or "", result.stderr or "") if part).strip()
500
+ if not blob:
501
+ return "health_scenario failed"
502
+
503
+ start = blob.rfind("{")
504
+ if start >= 0:
505
+ try:
506
+ data = json.loads(blob[start:])
507
+ except json.JSONDecodeError:
508
+ data = None
509
+ if isinstance(data, dict):
510
+ passed = data.get("passed")
511
+ failed = data.get("failed")
512
+ if isinstance(passed, int) and isinstance(failed, int):
513
+ total = passed + failed
514
+ errors = data.get("errors") or []
515
+ hint = f" — {errors[0]}" if errors else ""
516
+ return f"{passed}/{total} passed, {failed} failed{hint}"
517
+
518
+ for line in reversed(blob.splitlines()):
519
+ stripped = line.strip()
520
+ if not stripped or stripped in {"}", "{"}:
521
+ continue
522
+ if "passed" in stripped.lower() or "failed" in stripped.lower() or "❌" in stripped:
523
+ return stripped
524
+ return "health_scenario failed"
525
+
475
526
  async def _run_fleet_health_scenario(self) -> bool:
476
527
  """Optional full TestQL run (not dry-run) for fleet-wide health scenarios."""
477
528
  scenario_name = (self.config.testql.health_scenario or "").strip()
@@ -502,17 +553,18 @@ class TestQLWatcher(WupWatcher):
502
553
  )
503
554
  return True
504
555
 
505
- reason = result.stderr.strip() or result.stdout.strip() or "health_scenario failed"
506
- summary = reason.splitlines()[-1] if reason else "health_scenario failed"
556
+ summary = self._summarize_health_scenario_failure(result)
507
557
  track_path = self._write_track(service=fleet, stage="health_scenario", scenario=scenario_path, result=result)
558
+ strict = bool(getattr(self.config.testql, "health_scenario_strict", False))
559
+ # Non-strict: informational only — must not mark fleet "down" (koru reads service-health.json).
560
+ fleet_status = "down" if strict else "degraded"
508
561
  self._record_health_transition(
509
562
  service=fleet,
510
- status="down",
563
+ status=fleet_status,
511
564
  stage="health_scenario",
512
565
  message=summary[:500],
513
566
  track_file=str(track_path),
514
567
  )
515
- strict = bool(getattr(self.config.testql, "health_scenario_strict", False))
516
568
  if strict:
517
569
  self.console.print(f"[red]✗ Fleet health scenario failed: {summary}[/red]")
518
570
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.27
3
+ Version: 0.2.28
4
4
  Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -29,17 +29,17 @@ Dynamic: license-file
29
29
 
30
30
  ## AI Cost Tracking
31
31
 
32
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.27-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
33
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.13-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-16.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.28-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
33
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.27-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-16.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
34
34
 
35
- - 🤖 **LLM usage:** $2.1268 (37 commits)
35
+ - 🤖 **LLM usage:** $2.2653 (38 commits)
36
36
  - 👤 **Human dev:** ~$1632 (16.3h @ $100/h, 30min dedup)
37
37
 
38
- Generated on 2026-05-16 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
38
+ Generated on 2026-05-17 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
39
39
 
40
40
  ---
41
41
 
42
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.27-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.28-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
43
43
 
44
44
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
45
45
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes