nabla-cli 0.9.0__tar.gz → 0.9.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.
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/PKG-INFO +1 -1
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/__init__.py +1 -1
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/cli.py +17 -2
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/puller.py +148 -119
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/pusher.py +8 -1
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla_cli.egg-info/PKG-INFO +1 -1
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/pyproject.toml +1 -1
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/tests/test_cli_ux.py +3 -1
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/tests/test_pull.py +79 -76
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/tests/test_push.py +3 -1
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/README.md +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/__main__.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/branding.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/config.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/contract.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/induce.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/profile.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/prompt_recon.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/recorder.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/samplegen.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/scanner.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/schema_resolver.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/schemas/site_profile.schema.json +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/term.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla/ui.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla_cli.egg-info/SOURCES.txt +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla_cli.egg-info/dependency_links.txt +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla_cli.egg-info/entry_points.txt +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla_cli.egg-info/requires.txt +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/nabla_cli.egg-info/top_level.txt +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/setup.cfg +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/tests/test_config.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/tests/test_e2e_profile.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/tests/test_induce.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/tests/test_prompt_recon.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/tests/test_recorder.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/tests/test_samplegen.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/tests/test_scanner.py +0 -0
- {nabla_cli-0.9.0 → nabla_cli-0.9.2}/tests/test_schema_resolver.py +0 -0
|
@@ -14,7 +14,7 @@ from rich.console import Console
|
|
|
14
14
|
from . import __version__
|
|
15
15
|
from .branding import print_banner
|
|
16
16
|
from .config import PROVIDERS, apply_to_env, save_config
|
|
17
|
-
from .contract import Example, Verifiability, is_supported
|
|
17
|
+
from .contract import Example, Verifiability, compute_site_id, is_supported
|
|
18
18
|
from .profile import build_profile, find_site
|
|
19
19
|
from .recorder import RecordSource, ZeroTrafficError, record
|
|
20
20
|
from .scanner import scan
|
|
@@ -947,6 +947,11 @@ def _build_one(args, site, final_rail: bool = False, mode: str = "profile") -> i
|
|
|
947
947
|
|
|
948
948
|
if mode == "jeremy":
|
|
949
949
|
seed = {
|
|
950
|
+
# Stable site identity (template+schema hash) — the thread that
|
|
951
|
+
# lets `pull` later match a fine-tuned deployment back to this
|
|
952
|
+
# exact call site. Same value as the profile's site_id.
|
|
953
|
+
"site_id": compute_site_id(template.template, schema),
|
|
954
|
+
"symbol": site.symbol,
|
|
950
955
|
"goal": _compose_seed_goal(goal),
|
|
951
956
|
"system_prompt": template.system_prompt or "",
|
|
952
957
|
"examples": [
|
|
@@ -1083,6 +1088,12 @@ def _cmd_pull(args) -> int:
|
|
|
1083
1088
|
for site in supported:
|
|
1084
1089
|
replaces = extract_sampling(site, args.repo).get("model") or "unknown"
|
|
1085
1090
|
deployments.append((site, mock_deployment(site.symbol, replaces)))
|
|
1091
|
+
# Simulation richness: a lone site still demos a multi-row pull.
|
|
1092
|
+
filler = None
|
|
1093
|
+
if len(deployments) == 1 and args.site is None:
|
|
1094
|
+
from .puller import filler_deployment
|
|
1095
|
+
filler = filler_deployment()
|
|
1096
|
+
deployments.append(filler)
|
|
1086
1097
|
|
|
1087
1098
|
rail(_out)
|
|
1088
1099
|
table = Table(box=rich_box.SIMPLE_HEAD, header_style="bold", pad_edge=False)
|
|
@@ -1098,10 +1109,14 @@ def _cmd_pull(args) -> int:
|
|
|
1098
1109
|
)
|
|
1099
1110
|
_out.print(table)
|
|
1100
1111
|
|
|
1112
|
+
from .puller import filler_diff
|
|
1113
|
+
|
|
1101
1114
|
for site, dep in deployments:
|
|
1102
1115
|
rail(_out)
|
|
1103
1116
|
rail(_out, f"{site.symbol} · {site.file}", style="bold")
|
|
1104
|
-
|
|
1117
|
+
is_filler = filler is not None and site is filler[0]
|
|
1118
|
+
diff = filler_diff(dep) if is_filler else build_diff(args.repo, site, dep)
|
|
1119
|
+
for style, line in diff:
|
|
1105
1120
|
color = {"add": "green", "del": "red", "hunk": "cyan",
|
|
1106
1121
|
"head": "bold", "ctx": ""}[style]
|
|
1107
1122
|
rail(_out, line, style=color)
|
|
@@ -1,119 +1,148 @@
|
|
|
1
|
-
"""`nabla pull` — UX simulation of the loop-closing command (cli-plan §7).
|
|
2
|
-
|
|
3
|
-
The deployments API doesn't exist yet, so this module renders the full
|
|
4
|
-
intended experience — deployment table, per-site diffs, apply + verify,
|
|
5
|
-
before/after report — as a faithful simulation that NEVER writes anything:
|
|
6
|
-
no file edits, no .env changes, no network calls. Diffs are built from the
|
|
7
|
-
target repo's real source lines so the preview is true to life; deployment
|
|
8
|
-
rows are fabricated deterministically from the site symbol.
|
|
9
|
-
|
|
10
|
-
When the real backend lands, the fabrication points (_mock_deployments,
|
|
11
|
-
the apply/verify steps) get replaced by API calls and real edits; the
|
|
12
|
-
rendering stays.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import os
|
|
18
|
-
import time
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
from .config import load_config
|
|
22
|
-
|
|
23
|
-
DEFAULT_ENDPOINT = "http://127.0.0.1:8000/deployments"
|
|
24
|
-
_BASE_URL = "https://api.freesolo.ai/v1"
|
|
25
|
-
_KEY_ENV = "NABLA_MODEL_API_KEY"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def resolve_endpoint(flag_value: str | None = None) -> str:
|
|
29
|
-
if flag_value:
|
|
30
|
-
return flag_value
|
|
31
|
-
env = os.environ.get("NABLA_PULL_ENDPOINT")
|
|
32
|
-
if env:
|
|
33
|
-
return env
|
|
34
|
-
stored = load_config().get("pull_endpoint")
|
|
35
|
-
if stored:
|
|
36
|
-
return stored
|
|
37
|
-
return DEFAULT_ENDPOINT
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def mock_deployment(symbol: str, replaces: str) -> dict:
|
|
41
|
-
"""A deterministic, plausible deployment row for one site."""
|
|
42
|
-
slug = symbol.replace("_", "-")
|
|
43
|
-
seed = sum(ord(c) for c in symbol)
|
|
44
|
-
return {
|
|
45
|
-
"symbol": symbol,
|
|
46
|
-
"status": "ready",
|
|
47
|
-
"model": f"freesolo/qwen3.5-0.8b-ft-{slug}",
|
|
48
|
-
"base_url": _BASE_URL,
|
|
49
|
-
"api_key_env": _KEY_ENV,
|
|
50
|
-
"replaces": replaces or "unknown",
|
|
51
|
-
"pass_rate": round(0.88 + (seed % 10) / 100, 2),
|
|
52
|
-
"schema_valid_rate": 1.0,
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def build_diff(repo: str, site, deployment: dict) -> list[tuple[str, str]]:
|
|
57
|
-
"""(style, line) pairs for a unified diff of the intended rewrite,
|
|
58
|
-
grounded in the real source. Read-only."""
|
|
59
|
-
path = Path(repo) / site.file
|
|
60
|
-
try:
|
|
61
|
-
lines = path.read_text(encoding="utf-8").splitlines()
|
|
62
|
-
except OSError:
|
|
63
|
-
return []
|
|
64
|
-
|
|
65
|
-
env_suffix = site.symbol.upper()
|
|
66
|
-
out: list[tuple[str, str]] = [
|
|
67
|
-
("head", f"--- a/{site.file}"),
|
|
68
|
-
("head", f"+++ b/{site.file}"),
|
|
69
|
-
]
|
|
70
|
-
|
|
71
|
-
# Hunk 1: client construction, if a literal OpenAI(...) line exists.
|
|
72
|
-
client_idx = next((i for i, l in enumerate(lines)
|
|
73
|
-
if "OpenAI(" in l and "import" not in l), None)
|
|
74
|
-
if client_idx is not None:
|
|
75
|
-
old = lines[client_idx]
|
|
76
|
-
indent = old[: len(old) - len(old.lstrip())]
|
|
77
|
-
assign = old.split("OpenAI(", 1)[0]
|
|
78
|
-
out += [
|
|
79
|
-
("hunk", f"@@ -{client_idx + 1},1 +{client_idx + 1},2 @@"),
|
|
80
|
-
("del", f"-{old}"),
|
|
81
|
-
("add", f"+{assign}OpenAI("),
|
|
82
|
-
("add", f"+{indent} base_url=os.environ.get(\"NABLA_BASE_URL\", "
|
|
83
|
-
f"\"{deployment['base_url']}\"),"),
|
|
84
|
-
("add", f"+{indent} api_key=os.environ[\"{deployment['api_key_env']}\"],"),
|
|
85
|
-
("add", f"+{indent})"),
|
|
86
|
-
]
|
|
87
|
-
|
|
88
|
-
# Hunk 2: the model kwarg inside the call span.
|
|
89
|
-
start, end = site.span
|
|
90
|
-
model_idx = next((i for i in range(start - 1, min(end, len(lines)))
|
|
91
|
-
if "model=" in lines[i] or "model =" in lines[i]), None)
|
|
92
|
-
if model_idx is not None:
|
|
93
|
-
old = lines[model_idx]
|
|
94
|
-
indent = old[: len(old) - len(old.lstrip())]
|
|
95
|
-
trailing = "," if old.rstrip().endswith(",") else ""
|
|
96
|
-
context = lines[model_idx - 1] if model_idx else ""
|
|
97
|
-
out += [
|
|
98
|
-
("hunk", f"@@ -{model_idx},2 +{model_idx},2 @@"),
|
|
99
|
-
("ctx", f" {context}"),
|
|
100
|
-
("del", f"-{old}"),
|
|
101
|
-
("add", f"+{indent}model=os.environ.get(\"NABLA_MODEL_{env_suffix}\", "
|
|
102
|
-
f"\"{deployment['model']}\"){trailing}"),
|
|
103
|
-
]
|
|
104
|
-
|
|
105
|
-
# Hunk 3: credentials, appended to the repo's .env.
|
|
106
|
-
out += [
|
|
107
|
-
("head", "--- a/.env"),
|
|
108
|
-
("head", "+++ b/.env"),
|
|
109
|
-
("add", f"+{deployment['api_key_env']}=fsk-................ (provisioned by backend)"),
|
|
110
|
-
("add", f"+NABLA_BASE_URL={deployment['base_url']}"),
|
|
111
|
-
("add", f"+NABLA_MODEL_{env_suffix}={deployment['model']}"),
|
|
112
|
-
]
|
|
113
|
-
return out
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def pace(seconds: float, interactive: bool) -> None:
|
|
117
|
-
"""Small dramatic pause, interactive terminals only."""
|
|
118
|
-
if interactive:
|
|
119
|
-
time.sleep(seconds)
|
|
1
|
+
"""`nabla pull` — UX simulation of the loop-closing command (cli-plan §7).
|
|
2
|
+
|
|
3
|
+
The deployments API doesn't exist yet, so this module renders the full
|
|
4
|
+
intended experience — deployment table, per-site diffs, apply + verify,
|
|
5
|
+
before/after report — as a faithful simulation that NEVER writes anything:
|
|
6
|
+
no file edits, no .env changes, no network calls. Diffs are built from the
|
|
7
|
+
target repo's real source lines so the preview is true to life; deployment
|
|
8
|
+
rows are fabricated deterministically from the site symbol.
|
|
9
|
+
|
|
10
|
+
When the real backend lands, the fabrication points (_mock_deployments,
|
|
11
|
+
the apply/verify steps) get replaced by API calls and real edits; the
|
|
12
|
+
rendering stays.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from .config import load_config
|
|
22
|
+
|
|
23
|
+
DEFAULT_ENDPOINT = "http://127.0.0.1:8000/deployments"
|
|
24
|
+
_BASE_URL = "https://api.freesolo.ai/v1"
|
|
25
|
+
_KEY_ENV = "NABLA_MODEL_API_KEY"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_endpoint(flag_value: str | None = None) -> str:
|
|
29
|
+
if flag_value:
|
|
30
|
+
return flag_value
|
|
31
|
+
env = os.environ.get("NABLA_PULL_ENDPOINT")
|
|
32
|
+
if env:
|
|
33
|
+
return env
|
|
34
|
+
stored = load_config().get("pull_endpoint")
|
|
35
|
+
if stored:
|
|
36
|
+
return stored
|
|
37
|
+
return DEFAULT_ENDPOINT
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def mock_deployment(symbol: str, replaces: str) -> dict:
|
|
41
|
+
"""A deterministic, plausible deployment row for one site."""
|
|
42
|
+
slug = symbol.replace("_", "-")
|
|
43
|
+
seed = sum(ord(c) for c in symbol)
|
|
44
|
+
return {
|
|
45
|
+
"symbol": symbol,
|
|
46
|
+
"status": "ready",
|
|
47
|
+
"model": f"freesolo/qwen3.5-0.8b-ft-{slug}",
|
|
48
|
+
"base_url": _BASE_URL,
|
|
49
|
+
"api_key_env": _KEY_ENV,
|
|
50
|
+
"replaces": replaces or "unknown",
|
|
51
|
+
"pass_rate": round(0.88 + (seed % 10) / 100, 2),
|
|
52
|
+
"schema_valid_rate": 1.0,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_diff(repo: str, site, deployment: dict) -> list[tuple[str, str]]:
|
|
57
|
+
"""(style, line) pairs for a unified diff of the intended rewrite,
|
|
58
|
+
grounded in the real source. Read-only."""
|
|
59
|
+
path = Path(repo) / site.file
|
|
60
|
+
try:
|
|
61
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
62
|
+
except OSError:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
env_suffix = site.symbol.upper()
|
|
66
|
+
out: list[tuple[str, str]] = [
|
|
67
|
+
("head", f"--- a/{site.file}"),
|
|
68
|
+
("head", f"+++ b/{site.file}"),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
# Hunk 1: client construction, if a literal OpenAI(...) line exists.
|
|
72
|
+
client_idx = next((i for i, l in enumerate(lines)
|
|
73
|
+
if "OpenAI(" in l and "import" not in l), None)
|
|
74
|
+
if client_idx is not None:
|
|
75
|
+
old = lines[client_idx]
|
|
76
|
+
indent = old[: len(old) - len(old.lstrip())]
|
|
77
|
+
assign = old.split("OpenAI(", 1)[0]
|
|
78
|
+
out += [
|
|
79
|
+
("hunk", f"@@ -{client_idx + 1},1 +{client_idx + 1},2 @@"),
|
|
80
|
+
("del", f"-{old}"),
|
|
81
|
+
("add", f"+{assign}OpenAI("),
|
|
82
|
+
("add", f"+{indent} base_url=os.environ.get(\"NABLA_BASE_URL\", "
|
|
83
|
+
f"\"{deployment['base_url']}\"),"),
|
|
84
|
+
("add", f"+{indent} api_key=os.environ[\"{deployment['api_key_env']}\"],"),
|
|
85
|
+
("add", f"+{indent})"),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
# Hunk 2: the model kwarg inside the call span.
|
|
89
|
+
start, end = site.span
|
|
90
|
+
model_idx = next((i for i in range(start - 1, min(end, len(lines)))
|
|
91
|
+
if "model=" in lines[i] or "model =" in lines[i]), None)
|
|
92
|
+
if model_idx is not None:
|
|
93
|
+
old = lines[model_idx]
|
|
94
|
+
indent = old[: len(old) - len(old.lstrip())]
|
|
95
|
+
trailing = "," if old.rstrip().endswith(",") else ""
|
|
96
|
+
context = lines[model_idx - 1] if model_idx else ""
|
|
97
|
+
out += [
|
|
98
|
+
("hunk", f"@@ -{model_idx},2 +{model_idx},2 @@"),
|
|
99
|
+
("ctx", f" {context}"),
|
|
100
|
+
("del", f"-{old}"),
|
|
101
|
+
("add", f"+{indent}model=os.environ.get(\"NABLA_MODEL_{env_suffix}\", "
|
|
102
|
+
f"\"{deployment['model']}\"){trailing}"),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
# Hunk 3: credentials, appended to the repo's .env.
|
|
106
|
+
out += [
|
|
107
|
+
("head", "--- a/.env"),
|
|
108
|
+
("head", "+++ b/.env"),
|
|
109
|
+
("add", f"+{deployment['api_key_env']}=fsk-................ (provisioned by backend)"),
|
|
110
|
+
("add", f"+NABLA_BASE_URL={deployment['base_url']}"),
|
|
111
|
+
("add", f"+NABLA_MODEL_{env_suffix}={deployment['model']}"),
|
|
112
|
+
]
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def pace(seconds: float, interactive: bool) -> None:
|
|
117
|
+
"""Small dramatic pause, interactive terminals only."""
|
|
118
|
+
if interactive:
|
|
119
|
+
time.sleep(seconds)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class FillerSite:
|
|
123
|
+
"""Stand-in site so single-site repos still demo a multi-row pull."""
|
|
124
|
+
|
|
125
|
+
symbol = "extract_invoice"
|
|
126
|
+
file = "app/enrichment.py"
|
|
127
|
+
span = (41, 58)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def filler_deployment() -> tuple[FillerSite, dict]:
|
|
131
|
+
site = FillerSite()
|
|
132
|
+
dep = mock_deployment(site.symbol, "gpt-4o")
|
|
133
|
+
return site, dep
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def filler_diff(dep: dict) -> list[tuple[str, str]]:
|
|
137
|
+
"""Canned diff for the filler site (its file doesn't exist on disk)."""
|
|
138
|
+
return [
|
|
139
|
+
("head", f"--- a/{FillerSite.file}"),
|
|
140
|
+
("head", f"+++ b/{FillerSite.file}"),
|
|
141
|
+
("hunk", "@@ -44,2 +44,2 @@"),
|
|
142
|
+
("ctx", " response = client.chat.completions.create("),
|
|
143
|
+
("del", '- model="gpt-4o",'),
|
|
144
|
+
("add", f'+ model=os.environ.get("NABLA_MODEL_EXTRACT_INVOICE", "{dep["model"]}"),'),
|
|
145
|
+
("head", "--- a/.env"),
|
|
146
|
+
("head", "+++ b/.env"),
|
|
147
|
+
("add", f"+NABLA_MODEL_EXTRACT_INVOICE={dep['model']}"),
|
|
148
|
+
]
|
|
@@ -50,9 +50,15 @@ def resolve_endpoint(flag_value: str | None = None) -> str:
|
|
|
50
50
|
|
|
51
51
|
def to_run_task(kind: str, artifact: dict) -> dict:
|
|
52
52
|
"""Translate a build/jeremy artifact into the dispatcher's
|
|
53
|
-
RunTaskRequest shape. Pure rearrangement — nothing invented.
|
|
53
|
+
RunTaskRequest shape. Pure rearrangement — nothing invented.
|
|
54
|
+
|
|
55
|
+
site_id (the profile/seed's stable template+schema hash) rides along so
|
|
56
|
+
the backend can key the eventual fine-tuned deployment to the exact
|
|
57
|
+
call site — `nabla pull` joins on it later. Never file:line, never the
|
|
58
|
+
bare symbol: both are ambiguous across drift and duplicates."""
|
|
54
59
|
if kind == "jeremy":
|
|
55
60
|
return {
|
|
61
|
+
"site_id": artifact.get("site_id"),
|
|
56
62
|
"goal": artifact.get("goal", ""),
|
|
57
63
|
"examples": artifact.get("examples", []),
|
|
58
64
|
"student_system_prompt": artifact.get("system_prompt", "") or "",
|
|
@@ -65,6 +71,7 @@ def to_run_task(kind: str, artifact: dict) -> dict:
|
|
|
65
71
|
+ [str(c).rstrip(".") + "." for c in goal_block.get("constraints", [])]
|
|
66
72
|
).strip()
|
|
67
73
|
return {
|
|
74
|
+
"site_id": artifact.get("site_id"),
|
|
68
75
|
"goal": goal_text,
|
|
69
76
|
"examples": [
|
|
70
77
|
{"problem": e.get("prompt", ""), "solution": e.get("completion", "")}
|
|
@@ -547,7 +547,9 @@ def test_jeremy_single_site_writes_seed_spec(tmp_path, monkeypatch, capsys):
|
|
|
547
547
|
|
|
548
548
|
assert rc == 0
|
|
549
549
|
seed = json.loads((tmp_path / "extract_receipt.jeremy.json").read_text(encoding="utf-8"))
|
|
550
|
-
assert set(seed) == {"goal", "system_prompt", "examples"}
|
|
550
|
+
assert set(seed) == {"site_id", "symbol", "goal", "system_prompt", "examples"}
|
|
551
|
+
assert len(seed["site_id"]) == 64 # matches the profile's stable hash
|
|
552
|
+
assert seed["symbol"] == "extract_receipt"
|
|
551
553
|
assert seed["goal"].startswith("Solve a grade-school word problem")
|
|
552
554
|
assert "final answer is a bare number" in seed["goal"]
|
|
553
555
|
assert "Extract the receipt fields" in seed["system_prompt"]
|
|
@@ -1,76 +1,79 @@
|
|
|
1
|
-
"""Tests for the nabla pull simulation — its one hard guarantee: it never
|
|
2
|
-
writes anything."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import json
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
|
|
10
|
-
from nabla.cli import main
|
|
11
|
-
|
|
12
|
-
from test_cli_ux import _SINGLE_SITE_SRC
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@pytest.fixture(autouse=True)
|
|
16
|
-
def _clean_env(monkeypatch, tmp_path):
|
|
17
|
-
monkeypatch.setattr("nabla.config.config_path", lambda: tmp_path / "no-config.json")
|
|
18
|
-
monkeypatch.delenv("NABLA_PULL_ENDPOINT", raising=False)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _snapshot(root):
|
|
22
|
-
return {p: p.read_bytes() for p in root.rglob("*") if p.is_file()}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_pull_dry_run_prints_diff_and_writes_nothing(tmp_path, monkeypatch, capsys):
|
|
26
|
-
repo = tmp_path / "repo"
|
|
27
|
-
repo.mkdir()
|
|
28
|
-
(repo / "single_call_site.py").write_text(_SINGLE_SITE_SRC, encoding="utf-8")
|
|
29
|
-
monkeypatch.chdir(tmp_path)
|
|
30
|
-
before = _snapshot(tmp_path)
|
|
31
|
-
|
|
32
|
-
rc = main(["pull", str(repo)])
|
|
33
|
-
|
|
34
|
-
assert rc == 0
|
|
35
|
-
assert _snapshot(tmp_path) == before # nothing created or modified
|
|
36
|
-
out = capsys.readouterr().out
|
|
37
|
-
assert "freesolo/qwen3.5-0.8b-ft-extract-receipt" in out
|
|
38
|
-
assert "gpt-4o-mini" in out # REPLACES column from real sampling
|
|
39
|
-
assert "--- a/single_call_site.py" in out
|
|
40
|
-
assert "+++ b/.env" in out
|
|
41
|
-
assert "dry run" in out
|
|
42
|
-
assert "simulation — nothing was written" in out
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
assert
|
|
61
|
-
assert
|
|
62
|
-
|
|
63
|
-
assert "
|
|
64
|
-
assert "
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
1
|
+
"""Tests for the nabla pull simulation — its one hard guarantee: it never
|
|
2
|
+
writes anything."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from nabla.cli import main
|
|
11
|
+
|
|
12
|
+
from test_cli_ux import _SINGLE_SITE_SRC
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(autouse=True)
|
|
16
|
+
def _clean_env(monkeypatch, tmp_path):
|
|
17
|
+
monkeypatch.setattr("nabla.config.config_path", lambda: tmp_path / "no-config.json")
|
|
18
|
+
monkeypatch.delenv("NABLA_PULL_ENDPOINT", raising=False)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _snapshot(root):
|
|
22
|
+
return {p: p.read_bytes() for p in root.rglob("*") if p.is_file()}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_pull_dry_run_prints_diff_and_writes_nothing(tmp_path, monkeypatch, capsys):
|
|
26
|
+
repo = tmp_path / "repo"
|
|
27
|
+
repo.mkdir()
|
|
28
|
+
(repo / "single_call_site.py").write_text(_SINGLE_SITE_SRC, encoding="utf-8")
|
|
29
|
+
monkeypatch.chdir(tmp_path)
|
|
30
|
+
before = _snapshot(tmp_path)
|
|
31
|
+
|
|
32
|
+
rc = main(["pull", str(repo)])
|
|
33
|
+
|
|
34
|
+
assert rc == 0
|
|
35
|
+
assert _snapshot(tmp_path) == before # nothing created or modified
|
|
36
|
+
out = capsys.readouterr().out
|
|
37
|
+
assert "freesolo/qwen3.5-0.8b-ft-extract-receipt" in out
|
|
38
|
+
assert "gpt-4o-mini" in out # REPLACES column from real sampling
|
|
39
|
+
assert "--- a/single_call_site.py" in out
|
|
40
|
+
assert "+++ b/.env" in out
|
|
41
|
+
assert "dry run" in out
|
|
42
|
+
assert "simulation — nothing was written" in out
|
|
43
|
+
# single-site repos are padded with a canned second deployment
|
|
44
|
+
assert "extract_invoice" in out
|
|
45
|
+
assert "freesolo/qwen3.5-0.8b-ft-extract-invoice" in out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_pull_apply_simulates_and_writes_nothing(tmp_path, monkeypatch, capsys):
|
|
49
|
+
repo = tmp_path / "repo"
|
|
50
|
+
repo.mkdir()
|
|
51
|
+
(repo / "single_call_site.py").write_text(_SINGLE_SITE_SRC, encoding="utf-8")
|
|
52
|
+
(tmp_path / "extract_receipt.profile.json").write_text(json.dumps(
|
|
53
|
+
{"baseline": {"cost_per_call_usd": 0.011, "p50_latency_ms": 2000,
|
|
54
|
+
"calls_observed": 5}}), encoding="utf-8")
|
|
55
|
+
monkeypatch.chdir(tmp_path)
|
|
56
|
+
before = _snapshot(tmp_path)
|
|
57
|
+
|
|
58
|
+
rc = main(["pull", str(repo), "--apply"])
|
|
59
|
+
|
|
60
|
+
assert rc == 0
|
|
61
|
+
assert _snapshot(tmp_path) == before
|
|
62
|
+
out = capsys.readouterr().out
|
|
63
|
+
assert "2 edits -> single_call_site.py" in out
|
|
64
|
+
assert "completions valid" in out
|
|
65
|
+
assert "$0.011000 -> $0.001000" in out
|
|
66
|
+
assert "rollback: unset NABLA_MODEL_EXTRACT_RECEIPT" in out
|
|
67
|
+
assert "simulation — nothing was written" in out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_pull_unknown_site_exits_2(tmp_path, monkeypatch, capsys):
|
|
71
|
+
repo = tmp_path / "repo"
|
|
72
|
+
repo.mkdir()
|
|
73
|
+
(repo / "single_call_site.py").write_text(_SINGLE_SITE_SRC, encoding="utf-8")
|
|
74
|
+
monkeypatch.chdir(tmp_path)
|
|
75
|
+
|
|
76
|
+
rc = main(["pull", str(repo), "--site", "nope"])
|
|
77
|
+
|
|
78
|
+
assert rc == 2
|
|
79
|
+
assert "no supported site" in capsys.readouterr().err
|
|
@@ -70,6 +70,7 @@ def test_push_artifact_translates_jeremy_to_run_task():
|
|
|
70
70
|
ok, detail = push_artifact(backend.url, "s", "jeremy", artifact)
|
|
71
71
|
assert ok and "200" in detail
|
|
72
72
|
assert backend.received == [{
|
|
73
|
+
"site_id": None,
|
|
73
74
|
"goal": "Solve problems.",
|
|
74
75
|
"examples": [{"problem": "p", "solution": "s"}],
|
|
75
76
|
"student_system_prompt": "You are a tutor.",
|
|
@@ -114,7 +115,8 @@ def test_cmd_push_sweeps_site_artifacts(tmp_path, monkeypatch, capsys):
|
|
|
114
115
|
|
|
115
116
|
assert rc == 0
|
|
116
117
|
assert len(backend.received) == 2
|
|
117
|
-
assert all(set(r) == {"
|
|
118
|
+
assert all(set(r) == {"site_id", "goal", "examples",
|
|
119
|
+
"student_system_prompt", "student_goal"}
|
|
118
120
|
for r in backend.received)
|
|
119
121
|
assert "pushed 2 artifact(s)" in capsys.readouterr().out
|
|
120
122
|
|
|
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
|