wup 0.2.78__tar.gz → 0.2.79__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.
- {wup-0.2.78/wup.egg-info → wup-0.2.79}/PKG-INFO +6 -6
- {wup-0.2.78 → wup-0.2.79}/README.md +5 -5
- {wup-0.2.78 → wup-0.2.79}/pyproject.toml +1 -1
- {wup-0.2.78 → wup-0.2.79}/wup/__init__.py +1 -1
- {wup-0.2.78 → wup-0.2.79}/wup/aql.py +88 -50
- {wup-0.2.78 → wup-0.2.79/wup.egg-info}/PKG-INFO +6 -6
- {wup-0.2.78 → wup-0.2.79}/LICENSE +0 -0
- {wup-0.2.78 → wup-0.2.79}/setup.cfg +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_aql.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_assistant.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_auto_detection.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_bootstrap.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_cli_bridge.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_cli_filtering.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_control.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_discovery_adapters.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_e2e.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_endpoints_init_cli.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_genericity.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_health_summary_passed.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_multi_project.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_oql.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_planfile_reporter_dedupe.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_probe_mutex.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_service_inference.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_status_data.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_sync.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_testql_monitor.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_testql_watcher.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_visual_diff_periodic_skip.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_visual_diff_progress.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_watch_exclude.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_watch_no_paths_exit.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_web_client.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_wup.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/tests/test_wup_generate.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/_ast_detector.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/_base_detector.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/_hash_detector.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/_yaml_detector.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/anomaly_detector.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/anomaly_models.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/assistant.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/assistant_discovery.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/assistant_validator.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/bootstrap.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/bus.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/cli.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/cli_bridge.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/cli_config_generator.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/cli_scanner.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/config.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/control.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/core.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/dependency_mapper.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/discovery.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/endpoints.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/event_store.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/file_watcher/events/file_events.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/generate.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/init_cli.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/models/__init__.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/models/config.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/models/target.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/monitoring_manifest.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/multi.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/oql.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/paths.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/planfile_reporter.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/status_data.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/sync.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/testing/events/health_events.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/testing/events/test_results.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/testing/handlers/event_handlers.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/testing/handlers/health_handlers.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/testing/queries/health_queries.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/testql_cli_generator.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/testql_discovery.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/testql_monitor.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/testql_watcher.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/validate.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/visual_diff.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup/web_client.py +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup.egg-info/SOURCES.txt +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.78 → wup-0.2.79}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.78 → wup-0.2.79}/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.
|
|
3
|
+
Version: 0.2.79
|
|
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
|
|
@@ -47,17 +47,17 @@ Dynamic: license-file
|
|
|
47
47
|
|
|
48
48
|
## AI Cost Tracking
|
|
49
49
|
|
|
50
|
-
    
|
|
51
|
+
  
|
|
52
52
|
|
|
53
|
-
- 🤖 **LLM usage:** $7.
|
|
54
|
-
- 👤 **Human dev:** ~$
|
|
53
|
+
- 🤖 **LLM usage:** $7.6978 (109 commits)
|
|
54
|
+
- 👤 **Human dev:** ~$5461 (54.6h @ $100/h, 30min dedup)
|
|
55
55
|
|
|
56
56
|
Generated on 2026-07-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
57
57
|
|
|
58
58
|
---
|
|
59
59
|
|
|
60
|
-
    
|
|
61
61
|
|
|
62
62
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
63
63
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $7.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $7.6978 (109 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$5461 (54.6h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-07-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
19
19
|
|
|
@@ -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.
|
|
10
|
+
__version__ = "0.2.79"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -65,36 +65,43 @@ class AQLRule:
|
|
|
65
65
|
|
|
66
66
|
# --- path resolution ------------------------------------------------------
|
|
67
67
|
|
|
68
|
-
def
|
|
69
|
-
"""
|
|
70
|
-
if path in ("", "."):
|
|
71
|
-
return data
|
|
72
|
-
# Split ".a.b[0].c" into ['a','b',0,'c'].
|
|
68
|
+
def _path_tokens(path: str) -> List[Any] | None:
|
|
69
|
+
"""Split ``.a.b[0].c`` into ``['a', 'b', 0, 'c']``."""
|
|
73
70
|
tokens: List[Any] = []
|
|
74
71
|
for part in path.lstrip(".").split("."):
|
|
75
72
|
if not part:
|
|
76
73
|
continue
|
|
77
74
|
match = re.match(r"^([^\[\]]*)((?:\[\d+\])*)$", part)
|
|
78
75
|
if not match:
|
|
79
|
-
return
|
|
76
|
+
return None
|
|
80
77
|
key, indices = match.group(1), match.group(2)
|
|
81
78
|
if key:
|
|
82
79
|
tokens.append(key)
|
|
83
80
|
for idx in re.findall(r"\[(\d+)\]", indices):
|
|
84
81
|
tokens.append(int(idx))
|
|
82
|
+
return tokens
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _path_child(current: Any, token: Any) -> Any:
|
|
86
|
+
if isinstance(token, int) and isinstance(current, (list, tuple)) and 0 <= token < len(current):
|
|
87
|
+
return current[token]
|
|
88
|
+
if isinstance(token, str) and isinstance(current, dict) and token in current:
|
|
89
|
+
return current[token]
|
|
90
|
+
return _MISSING
|
|
85
91
|
|
|
92
|
+
|
|
93
|
+
def _resolve_path(data: Any, path: str) -> Any:
|
|
94
|
+
"""Resolve a dotted/indexed path (e.g. ``.a.b[0]``) or return ``_MISSING``."""
|
|
95
|
+
if path in ("", "."):
|
|
96
|
+
return data
|
|
97
|
+
tokens = _path_tokens(path)
|
|
98
|
+
if tokens is None:
|
|
99
|
+
return _MISSING
|
|
86
100
|
current = data
|
|
87
101
|
for token in tokens:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
else:
|
|
92
|
-
return _MISSING
|
|
93
|
-
else:
|
|
94
|
-
if isinstance(current, dict) and token in current:
|
|
95
|
-
current = current[token]
|
|
96
|
-
else:
|
|
97
|
-
return _MISSING
|
|
102
|
+
current = _path_child(current, token)
|
|
103
|
+
if current is _MISSING:
|
|
104
|
+
return _MISSING
|
|
98
105
|
return current
|
|
99
106
|
|
|
100
107
|
|
|
@@ -141,26 +148,40 @@ def _rule_selector(tokens: List[str]) -> Tuple[str, str]:
|
|
|
141
148
|
return selector, tokens.pop(0)
|
|
142
149
|
|
|
143
150
|
|
|
151
|
+
def _predicate_value(tokens: List[str], message: str) -> str:
|
|
152
|
+
if tokens:
|
|
153
|
+
return tokens.pop(0)
|
|
154
|
+
raise AQLError(message)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _length_rule(selector: str, path: str, tokens: List[str], severity: str, raw: str) -> AQLRule:
|
|
158
|
+
if len(tokens) < 2 or tokens[0] not in _COMPARATORS:
|
|
159
|
+
raise AQLError("'length' requires an operator and number, e.g. length > 0")
|
|
160
|
+
operator, number = tokens[0], tokens[1]
|
|
161
|
+
if not number.lstrip("-").isdigit():
|
|
162
|
+
raise AQLError(f"'length' expects an integer, got {number!r}")
|
|
163
|
+
return AQLRule(selector, path, "length", number, length_op=operator, severity=severity, raw=raw)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _type_rule(selector: str, path: str, tokens: List[str], severity: str, raw: str) -> AQLRule:
|
|
167
|
+
value = _predicate_value(tokens, f"'type' requires one of {', '.join(sorted(_TYPE_NAMES))}").lower()
|
|
168
|
+
if value not in _TYPE_NAMES:
|
|
169
|
+
raise AQLError(f"'type' requires one of {', '.join(sorted(_TYPE_NAMES))}")
|
|
170
|
+
return AQLRule(selector, path, "type", value, severity=severity, raw=raw)
|
|
171
|
+
|
|
172
|
+
|
|
144
173
|
def _predicate_rule(selector: str, path: str, keyword: str, tokens: List[str], severity: str, raw: str) -> AQLRule:
|
|
145
174
|
lower = keyword.lower()
|
|
146
175
|
if lower in ("exists", "missing"):
|
|
147
176
|
return AQLRule(selector, path, lower, severity=severity, raw=raw)
|
|
148
177
|
if lower == "matches":
|
|
149
|
-
|
|
150
|
-
return AQLRule(selector, path, "matches", tokens.pop(0), severity=severity, raw=raw)
|
|
151
|
-
raise AQLError("'matches' requires a regex")
|
|
178
|
+
return AQLRule(selector, path, "matches", _predicate_value(tokens, "'matches' requires a regex"), severity=severity, raw=raw)
|
|
152
179
|
if lower == "type":
|
|
153
|
-
|
|
154
|
-
return AQLRule(selector, path, "type", tokens.pop(0).lower(), severity=severity, raw=raw)
|
|
155
|
-
raise AQLError(f"'type' requires one of {', '.join(sorted(_TYPE_NAMES))}")
|
|
180
|
+
return _type_rule(selector, path, tokens, severity, raw)
|
|
156
181
|
if lower == "length":
|
|
157
|
-
|
|
158
|
-
return AQLRule(selector, path, "length", tokens[1], length_op=tokens[0], severity=severity, raw=raw)
|
|
159
|
-
raise AQLError("'length' requires an operator and number, e.g. length > 0")
|
|
160
|
-
if keyword in _COMPARATORS and tokens:
|
|
161
|
-
return AQLRule(selector, path, keyword, tokens.pop(0), severity=severity, raw=raw)
|
|
182
|
+
return _length_rule(selector, path, tokens, severity, raw)
|
|
162
183
|
if keyword in _COMPARATORS:
|
|
163
|
-
|
|
184
|
+
return AQLRule(selector, path, keyword, _predicate_value(tokens, f"operator {keyword!r} requires a value"), severity=severity, raw=raw)
|
|
164
185
|
raise AQLError(f"unknown predicate {keyword!r}")
|
|
165
186
|
|
|
166
187
|
|
|
@@ -222,26 +243,40 @@ def _type_name(value: Any) -> str:
|
|
|
222
243
|
return "unknown"
|
|
223
244
|
|
|
224
245
|
|
|
225
|
-
def
|
|
226
|
-
present = value is not _MISSING
|
|
227
|
-
if rule.op == "exists":
|
|
228
|
-
return present
|
|
229
|
-
if rule.op == "missing":
|
|
230
|
-
return not present
|
|
231
|
-
if not present:
|
|
232
|
-
return False # every other predicate needs the value to exist
|
|
233
|
-
|
|
246
|
+
def _matches_value(rule: AQLRule, value: Any) -> bool:
|
|
234
247
|
if rule.op == "matches":
|
|
235
248
|
return re.search(rule.value, str(value)) is not None
|
|
236
249
|
if rule.op == "type":
|
|
237
250
|
wanted = "bool" if rule.value in ("bool", "boolean") else rule.value
|
|
238
251
|
return _type_name(value) == wanted
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _collection_passes(rule: AQLRule, value: Any) -> bool | None:
|
|
239
256
|
if rule.op == "length":
|
|
240
257
|
length = _length_of(value)
|
|
241
258
|
return length is not None and _compare(length, rule.length_op, rule.value)
|
|
242
259
|
if rule.op in ("~", "!~"):
|
|
243
260
|
contains = rule.value.lower() in str(value).lower()
|
|
244
261
|
return contains if rule.op == "~" else not contains
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _passes(rule: AQLRule, value: Any) -> bool:
|
|
266
|
+
present = value is not _MISSING
|
|
267
|
+
if rule.op == "exists":
|
|
268
|
+
return present
|
|
269
|
+
if rule.op == "missing":
|
|
270
|
+
return not present
|
|
271
|
+
if not present:
|
|
272
|
+
return False # every other predicate needs the value to exist
|
|
273
|
+
|
|
274
|
+
if rule.op in ("matches", "type"):
|
|
275
|
+
return _matches_value(rule, value)
|
|
276
|
+
if result := _collection_passes(rule, value):
|
|
277
|
+
return result
|
|
278
|
+
if rule.op in ("length", "~", "!~"):
|
|
279
|
+
return False
|
|
245
280
|
return _compare(value, rule.op, rule.value)
|
|
246
281
|
|
|
247
282
|
|
|
@@ -261,6 +296,20 @@ class AQLEngine:
|
|
|
261
296
|
except json.JSONDecodeError:
|
|
262
297
|
return yaml.safe_load(text)
|
|
263
298
|
|
|
299
|
+
def _value_for_rule(self, file_path: Path, rule: AQLRule) -> Any:
|
|
300
|
+
data = self._load(file_path, rule.selector)
|
|
301
|
+
value = data if rule.selector == "text" else _resolve_path(data, rule.path)
|
|
302
|
+
return sorted(value.keys()) if rule.selector == "keys" and isinstance(value, dict) else value
|
|
303
|
+
|
|
304
|
+
@staticmethod
|
|
305
|
+
def _violation(file_path: Path, rule: AQLRule, value: Any) -> AnomalyResult:
|
|
306
|
+
shown = "(missing)" if value is _MISSING else repr(value)[:60]
|
|
307
|
+
return AnomalyResult(
|
|
308
|
+
detector="aql", file_path=str(file_path), anomaly_type="assertion", severity=rule.severity,
|
|
309
|
+
message=f"assertion failed: {rule.raw.strip()} (actual: {shown})",
|
|
310
|
+
details={"rule": rule.raw.strip(), "actual": None if value is _MISSING else value},
|
|
311
|
+
)
|
|
312
|
+
|
|
264
313
|
def check_file(self, file_path: Path | str, rules: List[str | AQLRule]) -> List[AnomalyResult]:
|
|
265
314
|
"""Evaluate rules against one file; return a violation per failing rule."""
|
|
266
315
|
file_path = Path(file_path)
|
|
@@ -271,24 +320,13 @@ class AQLEngine:
|
|
|
271
320
|
for rule in rules:
|
|
272
321
|
parsed = rule if isinstance(rule, AQLRule) else parse_rule(rule)
|
|
273
322
|
try:
|
|
274
|
-
|
|
275
|
-
value = data if parsed.selector == "text" else _resolve_path(data, parsed.path)
|
|
276
|
-
if parsed.selector == "keys":
|
|
277
|
-
value = sorted(value.keys()) if isinstance(value, dict) else value
|
|
323
|
+
value = self._value_for_rule(file_path, parsed)
|
|
278
324
|
except (OSError, yaml.YAMLError) as exc:
|
|
279
325
|
violations.append(AnomalyResult("aql", str(file_path), "error", "high", f"could not read: {exc}"))
|
|
280
326
|
continue
|
|
281
327
|
|
|
282
328
|
if not _passes(parsed, value):
|
|
283
|
-
|
|
284
|
-
violations.append(AnomalyResult(
|
|
285
|
-
detector="aql",
|
|
286
|
-
file_path=str(file_path),
|
|
287
|
-
anomaly_type="assertion",
|
|
288
|
-
severity=parsed.severity,
|
|
289
|
-
message=f"assertion failed: {parsed.raw.strip()} (actual: {shown})",
|
|
290
|
-
details={"rule": parsed.raw.strip(), "actual": None if value is _MISSING else value},
|
|
291
|
-
))
|
|
329
|
+
violations.append(self._violation(file_path, parsed, value))
|
|
292
330
|
return violations
|
|
293
331
|
|
|
294
332
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.79
|
|
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
|
|
@@ -47,17 +47,17 @@ Dynamic: license-file
|
|
|
47
47
|
|
|
48
48
|
## AI Cost Tracking
|
|
49
49
|
|
|
50
|
-
    
|
|
51
|
+
  
|
|
52
52
|
|
|
53
|
-
- 🤖 **LLM usage:** $7.
|
|
54
|
-
- 👤 **Human dev:** ~$
|
|
53
|
+
- 🤖 **LLM usage:** $7.6978 (109 commits)
|
|
54
|
+
- 👤 **Human dev:** ~$5461 (54.6h @ $100/h, 30min dedup)
|
|
55
55
|
|
|
56
56
|
Generated on 2026-07-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
57
57
|
|
|
58
58
|
---
|
|
59
59
|
|
|
60
|
-
    
|
|
61
61
|
|
|
62
62
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
63
63
|
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|