nabla-cli 0.6.4__tar.gz → 0.7.1__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.
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/PKG-INFO +19 -1
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/README.md +18 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/__init__.py +1 -1
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/cli.py +76 -23
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla_cli.egg-info/PKG-INFO +19 -1
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/pyproject.toml +1 -1
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_cli_ux.py +68 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/__main__.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/branding.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/config.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/contract.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/induce.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/profile.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/prompt_recon.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/recorder.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/samplegen.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/scanner.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/schema_resolver.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/schemas/site_profile.schema.json +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/term.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla/ui.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla_cli.egg-info/SOURCES.txt +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla_cli.egg-info/dependency_links.txt +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla_cli.egg-info/entry_points.txt +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla_cli.egg-info/requires.txt +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/nabla_cli.egg-info/top_level.txt +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/setup.cfg +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_config.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_e2e_profile.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_induce.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_prompt_recon.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_recorder.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_samplegen.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_scanner.py +0 -0
- {nabla_cli-0.6.4 → nabla_cli-0.7.1}/tests/test_schema_resolver.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nabla-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Call-site harvester: scan OpenAI call sites, record real traffic, emit site profiles
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -162,6 +162,24 @@ returns a goal description, implicit constraints, and difficulty
|
|
|
162
162
|
dimensions with cited evidence. That's the goal line printed above, and
|
|
163
163
|
it's what lands in the profile's `goal` block.
|
|
164
164
|
|
|
165
|
+
### `nabla jeremy`
|
|
166
|
+
|
|
167
|
+
An alternative to `build` with the same inputs and site handling: instead
|
|
168
|
+
of the full site profile, it emits `<site>.jeremy.json` — a seed spec for
|
|
169
|
+
a problem generator:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"goal": "<build's inferred goal description and constraints, joined into one string>",
|
|
174
|
+
"system_prompt": "<the site's own system prompt>",
|
|
175
|
+
"examples": [ {"problem": "...", "solution": "..."}, ... ]
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Same pipeline as `build` (captured pairs in, one goal-inference call) —
|
|
180
|
+
only the output shape differs; nothing is added that `build`'s own
|
|
181
|
+
output doesn't contain.
|
|
182
|
+
|
|
165
183
|
## Providers
|
|
166
184
|
|
|
167
185
|
`nabla init` supports three providers. Pick whichever you have a key
|
|
@@ -148,6 +148,24 @@ returns a goal description, implicit constraints, and difficulty
|
|
|
148
148
|
dimensions with cited evidence. That's the goal line printed above, and
|
|
149
149
|
it's what lands in the profile's `goal` block.
|
|
150
150
|
|
|
151
|
+
### `nabla jeremy`
|
|
152
|
+
|
|
153
|
+
An alternative to `build` with the same inputs and site handling: instead
|
|
154
|
+
of the full site profile, it emits `<site>.jeremy.json` — a seed spec for
|
|
155
|
+
a problem generator:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"goal": "<build's inferred goal description and constraints, joined into one string>",
|
|
160
|
+
"system_prompt": "<the site's own system prompt>",
|
|
161
|
+
"examples": [ {"problem": "...", "solution": "..."}, ... ]
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Same pipeline as `build` (captured pairs in, one goal-inference call) —
|
|
166
|
+
only the output shape differs; nothing is added that `build`'s own
|
|
167
|
+
output doesn't contain.
|
|
168
|
+
|
|
151
169
|
## Providers
|
|
152
170
|
|
|
153
171
|
`nabla init` supports three providers. Pick whichever you have a key
|
|
@@ -132,6 +132,30 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
132
132
|
p_profile.add_argument("--out", default=None,
|
|
133
133
|
help="output path (default: <site>.profile.json)")
|
|
134
134
|
|
|
135
|
+
p_jeremy = sub.add_parser(
|
|
136
|
+
"jeremy",
|
|
137
|
+
help="assemble a seed spec (goal + system_prompt + problem/solution examples)",
|
|
138
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
139
|
+
epilog=(
|
|
140
|
+
"an alternative to `build`: same pipeline and inputs, but emits\n"
|
|
141
|
+
"<site>.jeremy.json — build's goal rearranged into one string, the\n"
|
|
142
|
+
"site's system prompt as its own field, and the captured pairs as\n"
|
|
143
|
+
"{problem, solution} reference examples.\n\n"
|
|
144
|
+
"examples:\n"
|
|
145
|
+
" nabla jeremy auto: pick site, find <site>.captured.jsonl\n"
|
|
146
|
+
" nabla jeremy path/to/repo --site extract_invoice --captured captured.jsonl\n"
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
p_jeremy.add_argument("repo", nargs="?", default=".", help="path to target repo (default: current directory)")
|
|
150
|
+
p_jeremy.add_argument(
|
|
151
|
+
"--site",
|
|
152
|
+
help="site symbol (auto-picked when omitted and the repo has exactly one supported call site)",
|
|
153
|
+
)
|
|
154
|
+
p_jeremy.add_argument("--captured", "--examples", dest="examples",
|
|
155
|
+
help="captured JSONL from `nabla capture` (default: <site>.captured.jsonl)")
|
|
156
|
+
p_jeremy.add_argument("--out", default=None,
|
|
157
|
+
help="output path (default: <site>.jeremy.json)")
|
|
158
|
+
|
|
135
159
|
return parser
|
|
136
160
|
|
|
137
161
|
|
|
@@ -193,6 +217,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
193
217
|
"record": _cmd_record, # legacy alias
|
|
194
218
|
"build": _cmd_profile,
|
|
195
219
|
"profile": _cmd_profile, # legacy alias
|
|
220
|
+
"jeremy": lambda a: _cmd_profile(a, mode="jeremy"),
|
|
196
221
|
}
|
|
197
222
|
try:
|
|
198
223
|
return commands[args.command](args)
|
|
@@ -778,21 +803,23 @@ def _find_captured_file(site) -> Path | None:
|
|
|
778
803
|
)
|
|
779
804
|
|
|
780
805
|
|
|
781
|
-
def _cmd_profile(args) -> int:
|
|
806
|
+
def _cmd_profile(args, mode: str = "profile") -> int:
|
|
782
807
|
from .ui import rail, rail_end, rail_start
|
|
783
808
|
|
|
784
|
-
|
|
809
|
+
command = "jeremy" if mode == "jeremy" else "build"
|
|
810
|
+
noun = "seed spec(s)" if mode == "jeremy" else "profile(s)"
|
|
811
|
+
rail_start(_out, f"nabla {command}")
|
|
785
812
|
sites = scan(args.repo)
|
|
786
813
|
|
|
787
|
-
# Multi-site: no --site/--captured, several supported sites — build
|
|
788
|
-
#
|
|
814
|
+
# Multi-site: no --site/--captured, several supported sites — build the
|
|
815
|
+
# artifact for every site that has captured pairs on disk.
|
|
789
816
|
if args.site is None and args.examples is None:
|
|
790
817
|
supported = [s for s in sites
|
|
791
818
|
if is_supported(resolve_schema(s, args.repo)[1], s.flags)]
|
|
792
819
|
if len(supported) > 1:
|
|
793
820
|
with_captures = [s for s in supported if _find_captured_file(s) is not None]
|
|
794
821
|
if not with_captures:
|
|
795
|
-
print(f"nabla
|
|
822
|
+
print(f"nabla {command}: no captured pairs found for any of the "
|
|
796
823
|
f"{len(supported)} supported sites. Run first: nabla capture {args.repo}",
|
|
797
824
|
file=sys.stderr)
|
|
798
825
|
return 2
|
|
@@ -801,11 +828,11 @@ def _cmd_profile(args) -> int:
|
|
|
801
828
|
for i, site in enumerate(with_captures):
|
|
802
829
|
if i:
|
|
803
830
|
rail(_out)
|
|
804
|
-
rc = _build_one(args, site) or rc
|
|
831
|
+
rc = _build_one(args, site, mode=mode) or rc
|
|
805
832
|
if skipped:
|
|
806
833
|
rail(_out, f"skipped (no captured pairs): {', '.join(skipped)}", style="yellow")
|
|
807
834
|
rail(_out)
|
|
808
|
-
rail_end(_out, f"done — built {len(with_captures)}
|
|
835
|
+
rail_end(_out, f"done — built {len(with_captures)} {noun}", style="green")
|
|
809
836
|
return rc
|
|
810
837
|
|
|
811
838
|
site, err = _resolve_site(args, sites)
|
|
@@ -813,16 +840,26 @@ def _cmd_profile(args) -> int:
|
|
|
813
840
|
return err
|
|
814
841
|
if args.site:
|
|
815
842
|
rail(_out, f"site {site.symbol} · {site.file}")
|
|
816
|
-
rc = _build_one(args, site, final_rail=True)
|
|
843
|
+
rc = _build_one(args, site, final_rail=True, mode=mode)
|
|
817
844
|
return rc
|
|
818
845
|
|
|
819
846
|
|
|
820
|
-
def
|
|
847
|
+
def _compose_seed_goal(goal) -> str:
|
|
848
|
+
"""The seed spec's goal string: build's induced goal, rearranged into
|
|
849
|
+
prose — description followed by the constraints. Nothing is added that
|
|
850
|
+
build's own output doesn't contain."""
|
|
851
|
+
parts = [goal.description.rstrip(".") + "."]
|
|
852
|
+
parts += [c.rstrip(".") + "." for c in goal.constraints]
|
|
853
|
+
return " ".join(parts)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _build_one(args, site, final_rail: bool = False, mode: str = "profile") -> int:
|
|
821
857
|
from .induce import induce
|
|
822
858
|
from .prompt_recon import reconstruct
|
|
823
859
|
from .schema_resolver import resolve_schema as _resolve
|
|
824
860
|
from .ui import rail, rail_end, rail_ok
|
|
825
861
|
|
|
862
|
+
command = "jeremy" if mode == "jeremy" else "build"
|
|
826
863
|
examples_path = args.examples
|
|
827
864
|
if examples_path is None:
|
|
828
865
|
found = _find_captured_file(site) or (
|
|
@@ -831,7 +868,7 @@ def _build_one(args, site, final_rail: bool = False) -> int:
|
|
|
831
868
|
capture_cmd = f"nabla capture {args.repo}"
|
|
832
869
|
if args.site:
|
|
833
870
|
capture_cmd += f" --site {site.symbol}"
|
|
834
|
-
print(f"nabla
|
|
871
|
+
print(f"nabla {command}: no captured pairs found ({site.symbol}.captured.jsonl). "
|
|
835
872
|
f"Run first: {capture_cmd}", file=sys.stderr)
|
|
836
873
|
return 2
|
|
837
874
|
examples_path = str(found)
|
|
@@ -857,21 +894,37 @@ def _build_one(args, site, final_rail: bool = False) -> int:
|
|
|
857
894
|
else:
|
|
858
895
|
goal = induce(template, schema, examples)
|
|
859
896
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
897
|
+
if mode == "jeremy":
|
|
898
|
+
seed = {
|
|
899
|
+
"goal": _compose_seed_goal(goal),
|
|
900
|
+
"system_prompt": template.system_prompt or "",
|
|
901
|
+
"examples": [
|
|
902
|
+
{"problem": e.prompt, "solution": e.completion} for e in examples
|
|
903
|
+
],
|
|
904
|
+
}
|
|
905
|
+
out = (args.out if final_rail else None) or f"{site.symbol}.jeremy.json"
|
|
906
|
+
Path(out).write_text(json.dumps(seed, indent=2), encoding="utf-8")
|
|
907
|
+
rail(_out)
|
|
908
|
+
rail_ok(_out, f"wrote {out}")
|
|
909
|
+
rail(_out, f"site {site.symbol}")
|
|
910
|
+
rail(_out, f"examples {len(examples)} ({recorded_from})")
|
|
911
|
+
rail(_out, f"goal {seed['goal'][:90]}")
|
|
912
|
+
else:
|
|
913
|
+
profile = build_profile(site, args.repo, examples, goal, recorded_from=recorded_from)
|
|
914
|
+
out = (args.out if final_rail else None) or f"{site.symbol}.profile.json"
|
|
915
|
+
Path(out).write_text(json.dumps(profile, indent=2), encoding="utf-8")
|
|
916
|
+
supported = profile["classification"]["supported"]
|
|
917
|
+
rail(_out)
|
|
918
|
+
rail_ok(_out, f"wrote {out}")
|
|
919
|
+
rail(_out, f"site {site.symbol} · {profile['site_id'][:16]}…")
|
|
920
|
+
rail(_out, f"supported {supported}")
|
|
921
|
+
rail(_out, f"examples {len(examples)} ({recorded_from})")
|
|
922
|
+
rail(_out, f"goal {goal.description[:90]}")
|
|
923
|
+
dims = ", ".join(d["name"] for d in goal.difficulty_dimensions[:4])
|
|
924
|
+
rail(_out, f"difficulty {dims}")
|
|
872
925
|
if len(examples) < 20:
|
|
873
926
|
rail(_out, f"only {len(examples)} examples (< 20) — enough for a look, but a "
|
|
874
|
-
"handoff-quality
|
|
927
|
+
"handoff-quality artifact wants 20+ real-traffic pairs", style="yellow")
|
|
875
928
|
if final_rail:
|
|
876
929
|
rail(_out)
|
|
877
930
|
rail_end(_out, f"done — {out} is the handoff artifact", style="green")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nabla-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Call-site harvester: scan OpenAI call sites, record real traffic, emit site profiles
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -162,6 +162,24 @@ returns a goal description, implicit constraints, and difficulty
|
|
|
162
162
|
dimensions with cited evidence. That's the goal line printed above, and
|
|
163
163
|
it's what lands in the profile's `goal` block.
|
|
164
164
|
|
|
165
|
+
### `nabla jeremy`
|
|
166
|
+
|
|
167
|
+
An alternative to `build` with the same inputs and site handling: instead
|
|
168
|
+
of the full site profile, it emits `<site>.jeremy.json` — a seed spec for
|
|
169
|
+
a problem generator:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"goal": "<build's inferred goal description and constraints, joined into one string>",
|
|
174
|
+
"system_prompt": "<the site's own system prompt>",
|
|
175
|
+
"examples": [ {"problem": "...", "solution": "..."}, ... ]
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Same pipeline as `build` (captured pairs in, one goal-inference call) —
|
|
180
|
+
only the output shape differs; nothing is added that `build`'s own
|
|
181
|
+
output doesn't contain.
|
|
182
|
+
|
|
165
183
|
## Providers
|
|
166
184
|
|
|
167
185
|
`nabla init` supports three providers. Pick whichever you have a key
|
|
@@ -517,3 +517,71 @@ def test_scan_skips_build_and_dist_dirs(tmp_path, monkeypatch, capsys):
|
|
|
517
517
|
out = capsys.readouterr().out
|
|
518
518
|
assert "1 call site found" in out
|
|
519
519
|
assert "copy_of_site" not in out
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
# ---------------------------------------------------------------------------
|
|
523
|
+
# nabla jeremy — seed-spec sibling of build
|
|
524
|
+
# ---------------------------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
def _fake_goal(*a, **k):
|
|
527
|
+
from nabla.contract import GoalBlock
|
|
528
|
+
|
|
529
|
+
return GoalBlock(
|
|
530
|
+
description="Solve a grade-school word problem step by step.",
|
|
531
|
+
constraints=["final answer is a bare number", "no invented constants"],
|
|
532
|
+
difficulty_dimensions=[{"name": "d", "levels": ["a", "b"], "evidence": "e"}],
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def test_jeremy_single_site_writes_seed_spec(tmp_path, monkeypatch, capsys):
|
|
537
|
+
repo = _make_single_site_repo(tmp_path)
|
|
538
|
+
monkeypatch.chdir(tmp_path)
|
|
539
|
+
pair = {"input_slots": {"ocr_text": "r"}, "prompt": "A farmer has 6 crates...",
|
|
540
|
+
"completion": "The answer is 57.",
|
|
541
|
+
"tokens": {"in": 5, "out": 5}, "latency_ms": 10, "_source_kind": "samples"}
|
|
542
|
+
(tmp_path / "extract_receipt.captured.jsonl").write_text(
|
|
543
|
+
json.dumps(pair) + "\n", encoding="utf-8")
|
|
544
|
+
monkeypatch.setattr("nabla.induce.induce", _fake_goal)
|
|
545
|
+
|
|
546
|
+
rc = main(["jeremy", str(repo)])
|
|
547
|
+
|
|
548
|
+
assert rc == 0
|
|
549
|
+
seed = json.loads((tmp_path / "extract_receipt.jeremy.json").read_text(encoding="utf-8"))
|
|
550
|
+
assert set(seed) == {"goal", "system_prompt", "examples"}
|
|
551
|
+
assert seed["goal"].startswith("Solve a grade-school word problem")
|
|
552
|
+
assert "final answer is a bare number" in seed["goal"]
|
|
553
|
+
assert "Extract the receipt fields" in seed["system_prompt"]
|
|
554
|
+
assert seed["examples"] == [
|
|
555
|
+
{"problem": "A farmer has 6 crates...", "solution": "The answer is 57."}
|
|
556
|
+
]
|
|
557
|
+
assert "extract_receipt.jeremy.json" in capsys.readouterr().out
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def test_jeremy_multi_site(tmp_path, monkeypatch, capsys):
|
|
561
|
+
repo = _make_two_site_repo(tmp_path)
|
|
562
|
+
monkeypatch.chdir(tmp_path)
|
|
563
|
+
pair = {"input_slots": {}, "prompt": "p", "completion": "s",
|
|
564
|
+
"tokens": {"in": 1, "out": 1}, "latency_ms": 1, "_source_kind": "samples"}
|
|
565
|
+
for symbol in ("extract_receipt", "extract_menu"):
|
|
566
|
+
(tmp_path / f"{symbol}.captured.jsonl").write_text(
|
|
567
|
+
json.dumps(pair) + "\n", encoding="utf-8")
|
|
568
|
+
monkeypatch.setattr("nabla.induce.induce", _fake_goal)
|
|
569
|
+
|
|
570
|
+
rc = main(["jeremy", str(repo)])
|
|
571
|
+
|
|
572
|
+
assert rc == 0
|
|
573
|
+
assert (tmp_path / "extract_receipt.jeremy.json").exists()
|
|
574
|
+
assert (tmp_path / "extract_menu.jeremy.json").exists()
|
|
575
|
+
assert "built 2 seed spec(s)" in capsys.readouterr().out
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def test_jeremy_without_captures_points_at_capture(tmp_path, monkeypatch, capsys):
|
|
579
|
+
repo = _make_single_site_repo(tmp_path)
|
|
580
|
+
monkeypatch.chdir(tmp_path)
|
|
581
|
+
|
|
582
|
+
rc = main(["jeremy", str(repo)])
|
|
583
|
+
|
|
584
|
+
assert rc == 2
|
|
585
|
+
err = capsys.readouterr().err
|
|
586
|
+
assert "nabla jeremy" in err
|
|
587
|
+
assert "nabla capture" in err
|
|
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
|