evalstone 0.2.1__tar.gz → 0.2.2__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.
- {evalstone-0.2.1 → evalstone-0.2.2}/CHANGELOG.md +18 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/PKG-INFO +1 -1
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/__init__.py +1 -1
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/checks/comparison.py +23 -3
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/checks/judge.py +34 -4
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/providers/__init__.py +5 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_judge.py +41 -2
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_rag_and_comparison.py +32 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/.gitignore +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/LICENSE +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/README.md +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/justfile +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/pyproject.toml +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/__main__.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/_git.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/_json.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/center.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/checks/__init__.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/checks/_base.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/checks/deterministic.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/checks/human.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/checks/rag.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/checks/safety.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/checks/trajectory.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/cli.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/config.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/dataset.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/gate.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/mcp_server.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/output.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/providers/_http.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/providers/anthropic_.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/providers/gemini.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/providers/openai_compat.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/pytest_plugin.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/run.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/score.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/suite.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/src/evalstone/target.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/conftest.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_action.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_center.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_cli.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_config.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_dataset.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_deterministic.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_docs.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_human.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_judge_switching.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_mcp.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_providers.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_pytest_plugin.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_safety.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_score_and_checks.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_suite_run_gate.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_target.py +0 -0
- {evalstone-0.2.1 → evalstone-0.2.2}/tests/test_trajectory.py +0 -0
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `python/evalstone`. Semantic versioning; tags `lib-vX.Y.Z`.
|
|
4
4
|
|
|
5
|
+
## 0.2.2 — 2026-10-14
|
|
6
|
+
|
|
7
|
+
Judge replies cut off by the token cap, found by the first run against a real
|
|
8
|
+
vendor (Claude Haiku 5.5 via OpenRouter on the rag-bot demo: 4 of 10 verdicts
|
|
9
|
+
errored).
|
|
10
|
+
|
|
11
|
+
- `judge` and `comparison.pairwise` default `max_tokens` is now 1024 (was 256).
|
|
12
|
+
The cut replies needed 286–349 tokens: a verdict plus a long one-sentence reason.
|
|
13
|
+
`max_tokens` is not part of the judge version, so cached verdicts stay valid.
|
|
14
|
+
- A reply the provider stopped at the cap (`length`, `max_tokens`, `MAX_TOKENS`)
|
|
15
|
+
whose JSON is unclosed or invalid now errors with "reply truncated at
|
|
16
|
+
max_tokens=N; raise the check's max_tokens" followed by the original parse error and excerpt
|
|
17
|
+
instead of only "no JSON object in judge reply". A complete verdict at the cap is
|
|
18
|
+
accepted, and a complete but wrong one (score out of range) keeps its own error.
|
|
19
|
+
`ChatResponse.truncated` exposes the check.
|
|
20
|
+
- `comparison.pairwise`: an unparseable judge reply now counts that call's cost
|
|
21
|
+
(and latency) in the errored score, as `judge` already did.
|
|
22
|
+
|
|
5
23
|
## 0.2.1 — 2026-10-14
|
|
6
24
|
|
|
7
25
|
Attach fixes found by the docs audit.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: evalstone
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: The plug-and-play evals layer for LLM-based projects: datasets, checks, suites, runs and gates.
|
|
5
5
|
Project-URL: Homepage, https://evalstone.com
|
|
6
6
|
Project-URL: Documentation, https://evalstone.com/docs
|
|
@@ -8,7 +8,7 @@ import types
|
|
|
8
8
|
from importlib import import_module
|
|
9
9
|
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
|
-
__version__ = "0.2.
|
|
11
|
+
__version__ = "0.2.2"
|
|
12
12
|
|
|
13
13
|
# Exports resolve on first access (PEP 562) so that importing the package — which
|
|
14
14
|
# the pytest plugin does in every pytest session on the interpreter — costs
|
|
@@ -12,7 +12,7 @@ from typing import Any
|
|
|
12
12
|
|
|
13
13
|
from evalstone._json import sha256
|
|
14
14
|
from evalstone.checks._base import FunctionCheck, judge_model_for
|
|
15
|
-
from evalstone.checks.judge import _json_text, extract_json
|
|
15
|
+
from evalstone.checks.judge import DEFAULT_MAX_TOKENS, _json_text, explain_unparsed, extract_json
|
|
16
16
|
from evalstone.dataset import Sample
|
|
17
17
|
from evalstone.output import Output
|
|
18
18
|
from evalstone.providers import ChatMessage, ProviderError
|
|
@@ -46,6 +46,14 @@ def parse_winner(text: str) -> tuple[str, str]:
|
|
|
46
46
|
return winner, str(data.get("reason", "")).strip()
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
class _Unparsed(ValueError):
|
|
50
|
+
"""An unparseable judge reply, with what the call cost."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, message: str, cost_usd: float, latency_ms: float):
|
|
53
|
+
super().__init__(message)
|
|
54
|
+
self.cost_usd, self.latency_ms = cost_usd, latency_ms
|
|
55
|
+
|
|
56
|
+
|
|
49
57
|
def pairwise(
|
|
50
58
|
key: str,
|
|
51
59
|
rubric: str,
|
|
@@ -53,7 +61,7 @@ def pairwise(
|
|
|
53
61
|
baseline: Target | Mapping[str, Any],
|
|
54
62
|
model: str | None = None,
|
|
55
63
|
both_orders: bool = True,
|
|
56
|
-
max_tokens: int =
|
|
64
|
+
max_tokens: int = DEFAULT_MAX_TOKENS,
|
|
57
65
|
) -> FunctionCheck:
|
|
58
66
|
"""The candidate output beats the ``baseline`` output for the same sample.
|
|
59
67
|
|
|
@@ -78,7 +86,13 @@ def pairwise(
|
|
|
78
86
|
"max_tokens": max_tokens,
|
|
79
87
|
},
|
|
80
88
|
)
|
|
81
|
-
|
|
89
|
+
try:
|
|
90
|
+
winner, reason = parse_winner(resp.text)
|
|
91
|
+
except ValueError as exc:
|
|
92
|
+
# the failed call was still billed: carry its cost to the errored score
|
|
93
|
+
raise _Unparsed(
|
|
94
|
+
explain_unparsed(exc, resp, max_tokens), resp.cost_usd or 0.0, resp.latency_ms
|
|
95
|
+
) from None
|
|
82
96
|
if resp.cost_usd is None:
|
|
83
97
|
unpriced.append(resp.model)
|
|
84
98
|
return winner, reason, resp.cost_usd or 0.0, resp.latency_ms
|
|
@@ -127,6 +141,12 @@ def pairwise(
|
|
|
127
141
|
reasons.append(reason)
|
|
128
142
|
except ProviderError as exc:
|
|
129
143
|
return Score.errored(str(exc), cost_usd=cost, latency_ms=latency)
|
|
144
|
+
except _Unparsed as exc:
|
|
145
|
+
return Score.errored(
|
|
146
|
+
f"pairwise: {exc}",
|
|
147
|
+
cost_usd=cost + exc.cost_usd,
|
|
148
|
+
latency_ms=latency + exc.latency_ms,
|
|
149
|
+
)
|
|
130
150
|
except ValueError as exc:
|
|
131
151
|
return Score.errored(f"pairwise: {exc}", cost_usd=cost, latency_ms=latency)
|
|
132
152
|
value = sum(points) / len(points)
|
|
@@ -16,11 +16,15 @@ from evalstone._json import dumps, sha256
|
|
|
16
16
|
from evalstone.checks._base import FunctionCheck, judge_model_for
|
|
17
17
|
from evalstone.dataset import Sample
|
|
18
18
|
from evalstone.output import Output
|
|
19
|
-
from evalstone.providers import ChatMessage, ProviderError
|
|
19
|
+
from evalstone.providers import ChatMessage, ChatResponse, ProviderError
|
|
20
20
|
from evalstone.score import CheckKind, Context, Score
|
|
21
21
|
|
|
22
22
|
PROMPT_VERSION = 1
|
|
23
23
|
|
|
24
|
+
# Room for a verdict plus a long reason: 256 cut real Haiku 5.5 replies mid-reason
|
|
25
|
+
# (4 of 10 on the rag-bot demo). The cap only bounds a runaway; it is not in the version.
|
|
26
|
+
DEFAULT_MAX_TOKENS = 1024
|
|
27
|
+
|
|
24
28
|
SYSTEM_TEMPLATE = """You are a strict evaluator grading one answer produced by an AI system.
|
|
25
29
|
|
|
26
30
|
RUBRIC:
|
|
@@ -67,6 +71,22 @@ def parse_verdict(text: str, scale: int) -> tuple[int, str]:
|
|
|
67
71
|
return score, str(data.get("reason", "")).strip()
|
|
68
72
|
|
|
69
73
|
|
|
74
|
+
def explain_unparsed(exc: ValueError, resp: ChatResponse, max_tokens: int) -> str:
|
|
75
|
+
"""The parse error, prefixed with the cause when the cap cut the JSON itself.
|
|
76
|
+
|
|
77
|
+
Only an unclosed or invalid object is blamed on the cap: a complete object that is
|
|
78
|
+
wrong (score out of range, no winner) is the model's answer, not a truncation.
|
|
79
|
+
"""
|
|
80
|
+
if resp.truncated:
|
|
81
|
+
try:
|
|
82
|
+
extract_json(resp.text)
|
|
83
|
+
except ValueError:
|
|
84
|
+
return (
|
|
85
|
+
f"reply truncated at max_tokens={max_tokens}; raise the check's max_tokens ({exc})"
|
|
86
|
+
)
|
|
87
|
+
return str(exc)
|
|
88
|
+
|
|
89
|
+
|
|
70
90
|
class JudgeCheck(FunctionCheck):
|
|
71
91
|
"""A ``FunctionCheck`` whose function calls a model; exposes the judge version hash."""
|
|
72
92
|
|
|
@@ -99,7 +119,7 @@ def judge(
|
|
|
99
119
|
pass_level: int | None = None,
|
|
100
120
|
threshold: float | None = None,
|
|
101
121
|
include_reference: bool = False,
|
|
102
|
-
max_tokens: int =
|
|
122
|
+
max_tokens: int = DEFAULT_MAX_TOKENS,
|
|
103
123
|
) -> JudgeCheck:
|
|
104
124
|
"""Grade an output against ``rubric`` with a model.
|
|
105
125
|
|
|
@@ -190,7 +210,9 @@ def judge(
|
|
|
190
210
|
n, reason = parse_verdict(resp.text, scale)
|
|
191
211
|
except ValueError as exc:
|
|
192
212
|
return Score.errored(
|
|
193
|
-
f"judge: {exc
|
|
213
|
+
f"judge: {explain_unparsed(exc, resp, max_tokens)}",
|
|
214
|
+
cost_usd=resp.cost_usd or 0.0,
|
|
215
|
+
latency_ms=resp.latency_ms,
|
|
194
216
|
)
|
|
195
217
|
score = to_score(
|
|
196
218
|
n,
|
|
@@ -217,4 +239,12 @@ def _verdict_reason(score: Score) -> str:
|
|
|
217
239
|
return str((score.raw or {}).get("verdict_reason", ""))
|
|
218
240
|
|
|
219
241
|
|
|
220
|
-
__all__ = [
|
|
242
|
+
__all__ = [
|
|
243
|
+
"DEFAULT_MAX_TOKENS",
|
|
244
|
+
"PROMPT_VERSION",
|
|
245
|
+
"JudgeCheck",
|
|
246
|
+
"explain_unparsed",
|
|
247
|
+
"extract_json",
|
|
248
|
+
"judge",
|
|
249
|
+
"parse_verdict",
|
|
250
|
+
]
|
|
@@ -42,6 +42,11 @@ class ChatResponse(BaseModel):
|
|
|
42
42
|
stop_reason: str | None = None
|
|
43
43
|
raw: Any = None
|
|
44
44
|
|
|
45
|
+
@property
|
|
46
|
+
def truncated(self) -> bool:
|
|
47
|
+
"""The provider stopped at the max_tokens cap (OpenAI-compatible, Anthropic, Gemini)."""
|
|
48
|
+
return (self.stop_reason or "").lower() in {"length", "max_tokens"}
|
|
49
|
+
|
|
45
50
|
|
|
46
51
|
class ProviderError(Exception):
|
|
47
52
|
"""A provider call failed (HTTP error, refusal, malformed response)."""
|
|
@@ -16,9 +16,9 @@ class FakeProvider:
|
|
|
16
16
|
|
|
17
17
|
name = "openai"
|
|
18
18
|
|
|
19
|
-
def __init__(self, reply=None, fail=None, judge_reply=None):
|
|
19
|
+
def __init__(self, reply=None, fail=None, judge_reply=None, stop_reason=None):
|
|
20
20
|
self.reply, self.fail, self.calls = reply, fail, []
|
|
21
|
-
self.judge_reply = judge_reply
|
|
21
|
+
self.judge_reply, self.stop_reason = judge_reply, stop_reason
|
|
22
22
|
|
|
23
23
|
def complete(self, request):
|
|
24
24
|
self.calls.append(request)
|
|
@@ -41,6 +41,7 @@ class FakeProvider:
|
|
|
41
41
|
output_tokens=10,
|
|
42
42
|
cost_usd=0.001,
|
|
43
43
|
latency_ms=12.0,
|
|
44
|
+
stop_reason=self.stop_reason,
|
|
44
45
|
)
|
|
45
46
|
|
|
46
47
|
|
|
@@ -114,6 +115,44 @@ def test_judge_errors_are_never_passes():
|
|
|
114
115
|
assert no_model.error.startswith("no model given")
|
|
115
116
|
|
|
116
117
|
|
|
118
|
+
# What a real Haiku 5.5 reply looked like when max_tokens=256 cut it mid-reason.
|
|
119
|
+
CUT = '{"score": 4, "reason": "The answer directly answers the question with a concrete rule (30 da'
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.parametrize("stop", ["length", "max_tokens", "MAX_TOKENS"])
|
|
123
|
+
def test_a_reply_cut_at_the_token_cap_says_so_and_never_passes(stop):
|
|
124
|
+
chk = judge("h", "r", max_tokens=300)
|
|
125
|
+
score = chk(S, Output(value="x"), ctx_with(FakeProvider(judge_reply=CUT, stop_reason=stop)))
|
|
126
|
+
assert score.passed is False and score.cost_usd == 0.001
|
|
127
|
+
assert score.error.startswith(
|
|
128
|
+
"judge: reply truncated at max_tokens=300; raise the check's max_tokens "
|
|
129
|
+
"(no JSON object in judge reply: "
|
|
130
|
+
)
|
|
131
|
+
assert "concrete rule (30 da" in score.error # the excerpt of what came back survives
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_truncation_only_explains_a_failed_parse():
|
|
135
|
+
# A complete verdict wins even if the provider then hit the cap.
|
|
136
|
+
full = ctx_with(
|
|
137
|
+
FakeProvider(judge_reply='{"score": 5, "reason": "ok"} and', stop_reason="length")
|
|
138
|
+
)
|
|
139
|
+
assert judge("h", "r")(S, Output(value="x"), full).passed is True
|
|
140
|
+
# A complete but wrong verdict at the cap is the model's answer, not a truncation.
|
|
141
|
+
wrong = ctx_with(
|
|
142
|
+
FakeProvider(judge_reply='{"score": 7, "reason": "x"} and', stop_reason="length")
|
|
143
|
+
)
|
|
144
|
+
assert judge("h", "r")(S, Output(value="x"), wrong).error == "judge: judge score 7 outside 1..5"
|
|
145
|
+
# A normal stop keeps the parse error as it was.
|
|
146
|
+
plain = ctx_with(FakeProvider(judge_reply=CUT, stop_reason="stop"))
|
|
147
|
+
assert "no JSON object" in judge("h", "r")(S, Output(value="x"), plain).error
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_judge_default_token_budget_fits_a_long_reason():
|
|
151
|
+
prov = FakeProvider()
|
|
152
|
+
judge("h", "r")(S, Output(value="30 days"), ctx_with(prov))
|
|
153
|
+
assert prov.calls[0].max_tokens == 1024
|
|
154
|
+
|
|
155
|
+
|
|
117
156
|
def test_judge_cache_makes_the_second_identical_call_free():
|
|
118
157
|
prov = FakeProvider()
|
|
119
158
|
ctx = ctx_with(prov)
|
|
@@ -244,3 +244,35 @@ def test_config_baseline_mapping_form_and_target_form(tmp_path):
|
|
|
244
244
|
c.providers.register("openai", JudgeFake(winner='{"winner": "A"}'))
|
|
245
245
|
run = suite.run(target, ctx=c)
|
|
246
246
|
assert run.summary.errors == 0 and run.summary.results == 2
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_pairwise_reply_cut_at_the_token_cap_says_so():
|
|
250
|
+
class Cut(JudgeFake):
|
|
251
|
+
def complete(self, request):
|
|
252
|
+
self.calls.append(request)
|
|
253
|
+
return ChatResponse(
|
|
254
|
+
text='{"winner": "A", "reason": "Answer A is more compl',
|
|
255
|
+
model=request.model,
|
|
256
|
+
cost_usd=0.001,
|
|
257
|
+
stop_reason="length",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
fake = Cut()
|
|
261
|
+
score = pairwise("b", "r", baseline={"s": "x"})(S, Output(value="y"), ctx(fake))
|
|
262
|
+
assert score.passed is False
|
|
263
|
+
assert score.error.startswith("pairwise: reply truncated at max_tokens=1024")
|
|
264
|
+
assert fake.calls[0].max_tokens == 1024
|
|
265
|
+
assert score.cost_usd == pytest.approx(0.001) # the failed call was billed: it counts
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_pairwise_unparseable_second_order_keeps_every_call_cost():
|
|
269
|
+
replies = iter(['{"winner": "A", "reason": "x"}', "no verdict here"])
|
|
270
|
+
|
|
271
|
+
class Half(JudgeFake):
|
|
272
|
+
def complete(self, request):
|
|
273
|
+
self.calls.append(request)
|
|
274
|
+
return ChatResponse(text=next(replies), model=request.model, cost_usd=0.001)
|
|
275
|
+
|
|
276
|
+
score = pairwise("b", "r", baseline={"s": "x"})(S, Output(value="y"), ctx(Half()))
|
|
277
|
+
assert score.passed is False and "no JSON object" in score.error
|
|
278
|
+
assert score.cost_usd == pytest.approx(0.002)
|
|
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
|