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.
Files changed (89) hide show
  1. {wup-0.2.78/wup.egg-info → wup-0.2.79}/PKG-INFO +6 -6
  2. {wup-0.2.78 → wup-0.2.79}/README.md +5 -5
  3. {wup-0.2.78 → wup-0.2.79}/pyproject.toml +1 -1
  4. {wup-0.2.78 → wup-0.2.79}/wup/__init__.py +1 -1
  5. {wup-0.2.78 → wup-0.2.79}/wup/aql.py +88 -50
  6. {wup-0.2.78 → wup-0.2.79/wup.egg-info}/PKG-INFO +6 -6
  7. {wup-0.2.78 → wup-0.2.79}/LICENSE +0 -0
  8. {wup-0.2.78 → wup-0.2.79}/setup.cfg +0 -0
  9. {wup-0.2.78 → wup-0.2.79}/tests/test_aql.py +0 -0
  10. {wup-0.2.78 → wup-0.2.79}/tests/test_assistant.py +0 -0
  11. {wup-0.2.78 → wup-0.2.79}/tests/test_auto_detection.py +0 -0
  12. {wup-0.2.78 → wup-0.2.79}/tests/test_bootstrap.py +0 -0
  13. {wup-0.2.78 → wup-0.2.79}/tests/test_cli_bridge.py +0 -0
  14. {wup-0.2.78 → wup-0.2.79}/tests/test_cli_filtering.py +0 -0
  15. {wup-0.2.78 → wup-0.2.79}/tests/test_control.py +0 -0
  16. {wup-0.2.78 → wup-0.2.79}/tests/test_discovery_adapters.py +0 -0
  17. {wup-0.2.78 → wup-0.2.79}/tests/test_e2e.py +0 -0
  18. {wup-0.2.78 → wup-0.2.79}/tests/test_endpoints_init_cli.py +0 -0
  19. {wup-0.2.78 → wup-0.2.79}/tests/test_genericity.py +0 -0
  20. {wup-0.2.78 → wup-0.2.79}/tests/test_health_summary_passed.py +0 -0
  21. {wup-0.2.78 → wup-0.2.79}/tests/test_monitoring_manifest.py +0 -0
  22. {wup-0.2.78 → wup-0.2.79}/tests/test_multi_project.py +0 -0
  23. {wup-0.2.78 → wup-0.2.79}/tests/test_oql.py +0 -0
  24. {wup-0.2.78 → wup-0.2.79}/tests/test_planfile_reporter_dedupe.py +0 -0
  25. {wup-0.2.78 → wup-0.2.79}/tests/test_probe_mutex.py +0 -0
  26. {wup-0.2.78 → wup-0.2.79}/tests/test_service_inference.py +0 -0
  27. {wup-0.2.78 → wup-0.2.79}/tests/test_status_data.py +0 -0
  28. {wup-0.2.78 → wup-0.2.79}/tests/test_sync.py +0 -0
  29. {wup-0.2.78 → wup-0.2.79}/tests/test_testql_monitor.py +0 -0
  30. {wup-0.2.78 → wup-0.2.79}/tests/test_testql_watcher.py +0 -0
  31. {wup-0.2.78 → wup-0.2.79}/tests/test_visual_diff_periodic_skip.py +0 -0
  32. {wup-0.2.78 → wup-0.2.79}/tests/test_visual_diff_progress.py +0 -0
  33. {wup-0.2.78 → wup-0.2.79}/tests/test_watch_exclude.py +0 -0
  34. {wup-0.2.78 → wup-0.2.79}/tests/test_watch_no_paths_exit.py +0 -0
  35. {wup-0.2.78 → wup-0.2.79}/tests/test_web_client.py +0 -0
  36. {wup-0.2.78 → wup-0.2.79}/tests/test_wup.py +0 -0
  37. {wup-0.2.78 → wup-0.2.79}/tests/test_wup_generate.py +0 -0
  38. {wup-0.2.78 → wup-0.2.79}/wup/_ast_detector.py +0 -0
  39. {wup-0.2.78 → wup-0.2.79}/wup/_base_detector.py +0 -0
  40. {wup-0.2.78 → wup-0.2.79}/wup/_hash_detector.py +0 -0
  41. {wup-0.2.78 → wup-0.2.79}/wup/_yaml_detector.py +0 -0
  42. {wup-0.2.78 → wup-0.2.79}/wup/anomaly_detector.py +0 -0
  43. {wup-0.2.78 → wup-0.2.79}/wup/anomaly_models.py +0 -0
  44. {wup-0.2.78 → wup-0.2.79}/wup/assistant.py +0 -0
  45. {wup-0.2.78 → wup-0.2.79}/wup/assistant_discovery.py +0 -0
  46. {wup-0.2.78 → wup-0.2.79}/wup/assistant_validator.py +0 -0
  47. {wup-0.2.78 → wup-0.2.79}/wup/bootstrap.py +0 -0
  48. {wup-0.2.78 → wup-0.2.79}/wup/bus.py +0 -0
  49. {wup-0.2.78 → wup-0.2.79}/wup/cli.py +0 -0
  50. {wup-0.2.78 → wup-0.2.79}/wup/cli_bridge.py +0 -0
  51. {wup-0.2.78 → wup-0.2.79}/wup/cli_config_generator.py +0 -0
  52. {wup-0.2.78 → wup-0.2.79}/wup/cli_scanner.py +0 -0
  53. {wup-0.2.78 → wup-0.2.79}/wup/config.py +0 -0
  54. {wup-0.2.78 → wup-0.2.79}/wup/control.py +0 -0
  55. {wup-0.2.78 → wup-0.2.79}/wup/core.py +0 -0
  56. {wup-0.2.78 → wup-0.2.79}/wup/dependency_mapper.py +0 -0
  57. {wup-0.2.78 → wup-0.2.79}/wup/discovery.py +0 -0
  58. {wup-0.2.78 → wup-0.2.79}/wup/endpoints.py +0 -0
  59. {wup-0.2.78 → wup-0.2.79}/wup/event_store.py +0 -0
  60. {wup-0.2.78 → wup-0.2.79}/wup/file_watcher/events/file_events.py +0 -0
  61. {wup-0.2.78 → wup-0.2.79}/wup/generate.py +0 -0
  62. {wup-0.2.78 → wup-0.2.79}/wup/init_cli.py +0 -0
  63. {wup-0.2.78 → wup-0.2.79}/wup/models/__init__.py +0 -0
  64. {wup-0.2.78 → wup-0.2.79}/wup/models/config.py +0 -0
  65. {wup-0.2.78 → wup-0.2.79}/wup/models/target.py +0 -0
  66. {wup-0.2.78 → wup-0.2.79}/wup/monitoring_manifest.py +0 -0
  67. {wup-0.2.78 → wup-0.2.79}/wup/multi.py +0 -0
  68. {wup-0.2.78 → wup-0.2.79}/wup/oql.py +0 -0
  69. {wup-0.2.78 → wup-0.2.79}/wup/paths.py +0 -0
  70. {wup-0.2.78 → wup-0.2.79}/wup/planfile_reporter.py +0 -0
  71. {wup-0.2.78 → wup-0.2.79}/wup/status_data.py +0 -0
  72. {wup-0.2.78 → wup-0.2.79}/wup/sync.py +0 -0
  73. {wup-0.2.78 → wup-0.2.79}/wup/testing/events/health_events.py +0 -0
  74. {wup-0.2.78 → wup-0.2.79}/wup/testing/events/test_results.py +0 -0
  75. {wup-0.2.78 → wup-0.2.79}/wup/testing/handlers/event_handlers.py +0 -0
  76. {wup-0.2.78 → wup-0.2.79}/wup/testing/handlers/health_handlers.py +0 -0
  77. {wup-0.2.78 → wup-0.2.79}/wup/testing/queries/health_queries.py +0 -0
  78. {wup-0.2.78 → wup-0.2.79}/wup/testql_cli_generator.py +0 -0
  79. {wup-0.2.78 → wup-0.2.79}/wup/testql_discovery.py +0 -0
  80. {wup-0.2.78 → wup-0.2.79}/wup/testql_monitor.py +0 -0
  81. {wup-0.2.78 → wup-0.2.79}/wup/testql_watcher.py +0 -0
  82. {wup-0.2.78 → wup-0.2.79}/wup/validate.py +0 -0
  83. {wup-0.2.78 → wup-0.2.79}/wup/visual_diff.py +0 -0
  84. {wup-0.2.78 → wup-0.2.79}/wup/web_client.py +0 -0
  85. {wup-0.2.78 → wup-0.2.79}/wup.egg-info/SOURCES.txt +0 -0
  86. {wup-0.2.78 → wup-0.2.79}/wup.egg-info/dependency_links.txt +0 -0
  87. {wup-0.2.78 → wup-0.2.79}/wup.egg-info/entry_points.txt +0 -0
  88. {wup-0.2.78 → wup-0.2.79}/wup.egg-info/requires.txt +0 -0
  89. {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.78
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.78-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
51
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.12-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-54.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
50
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.79-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
51
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.70-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-54.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
52
52
 
53
- - 🤖 **LLM usage:** $7.1247 (108 commits)
54
- - 👤 **Human dev:** ~$5451 (54.5h @ $100/h, 30min dedup)
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
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.78-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
60
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.79-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.78-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-$7.12-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-54.5h-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.79-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-$7.70-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-54.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.1247 (108 commits)
10
- - 👤 **Human dev:** ~$5451 (54.5h @ $100/h, 30min dedup)
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
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.78-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.79-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.78"
7
+ version = "0.2.79"
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.10"
@@ -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.78"
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 _resolve_path(data: Any, path: str) -> Any:
69
- """Resolve a dotted/indexed path (e.g. ``.a.b[0]``) or return ``_MISSING``."""
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 _MISSING
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
- if isinstance(token, int):
89
- if isinstance(current, (list, tuple)) and 0 <= token < len(current):
90
- current = current[token]
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
- if tokens:
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
- if tokens and tokens[0].lower() in _TYPE_NAMES:
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
- if len(tokens) >= 2 and tokens[0] in _COMPARATORS and tokens[1].lstrip("-").isdigit():
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
- raise AQLError(f"operator {keyword!r} requires a value")
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 _passes(rule: AQLRule, value: Any) -> bool:
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
- data = self._load(file_path, parsed.selector)
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
- shown = "(missing)" if value is _MISSING else repr(value)[:60]
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.78
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.78-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
51
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.12-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-54.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
50
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.79-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
51
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.70-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-54.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
52
52
 
53
- - 🤖 **LLM usage:** $7.1247 (108 commits)
54
- - 👤 **Human dev:** ~$5451 (54.5h @ $100/h, 30min dedup)
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
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.78-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
60
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.79-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
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