quality-loop 0.2.1__tar.gz → 0.3.0__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.
- {quality_loop-0.2.1/quality_loop.egg-info → quality_loop-0.3.0}/PKG-INFO +1 -1
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/__init__.py +1 -1
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/adapter.py +16 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/report.py +6 -1
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/runner.py +7 -3
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/screens/__init__.py +35 -5
- {quality_loop-0.2.1 → quality_loop-0.3.0/quality_loop.egg-info}/PKG-INFO +1 -1
- {quality_loop-0.2.1 → quality_loop-0.3.0}/tests/test_scenario_report.py +8 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/tests/test_screens.py +43 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/LICENSE +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/README.md +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/pyproject.toml +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/OPERATOR.md +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/__main__.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/audit.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/cli.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/compare.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/config.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/drive/__init__.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/drive/_mac.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/drive/_windows.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/drive/keys.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/drive/terminal.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/install.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/mcp.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/operator.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/pages.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/processes.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/progress.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/room.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/scenario.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/screens/_mac.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/screens/_windows.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/session.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/skill/SKILL.md +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/skill/references/filing.md +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/skill/references/setup.md +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/standin.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop/transcript.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop.egg-info/SOURCES.txt +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop.egg-info/dependency_links.txt +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop.egg-info/entry_points.txt +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop.egg-info/requires.txt +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/quality_loop.egg-info/top_level.txt +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/setup.cfg +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/tests/test_core.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/tests/test_drive.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/tests/test_loop.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/tests/test_mcp.py +0 -0
- {quality_loop-0.2.1 → quality_loop-0.3.0}/tests/test_operator.py +0 -0
|
@@ -103,6 +103,22 @@ class Adapter:
|
|
|
103
103
|
"""
|
|
104
104
|
return [(case, "") for case in cases]
|
|
105
105
|
|
|
106
|
+
def screen_backend(self) -> dict | None:
|
|
107
|
+
"""Where the declared screens are put up, when not on this desktop.
|
|
108
|
+
|
|
109
|
+
``{"path": file, "settings": {...}}``: a Python file with
|
|
110
|
+
``open_page``, ``open_terminal`` and ``close`` as in
|
|
111
|
+
`quality_loop.screens`, and what its ``configure(settings)`` is given.
|
|
112
|
+
Asked once per room, after the product is up, so settings may name
|
|
113
|
+
where it runs this time. None, the default, is this OS's own backend.
|
|
114
|
+
"""
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def platform(self) -> str:
|
|
118
|
+
"""The OS the room's app runs on, as ``sys.platform`` names it - what
|
|
119
|
+
the operator is told. Not this machine's when the product runs in a VM."""
|
|
120
|
+
return sys.platform
|
|
121
|
+
|
|
106
122
|
def alive(self) -> bool:
|
|
107
123
|
"""Whether the product is still answering, asked on the heartbeat."""
|
|
108
124
|
return True
|
|
@@ -125,7 +125,12 @@ def parse(text: str, room: str = "") -> tuple[list[Problem], list[str]]:
|
|
|
125
125
|
value = field["value"].strip()
|
|
126
126
|
key = by_label[field["label"].strip().lower()]
|
|
127
127
|
if key == "id":
|
|
128
|
-
|
|
128
|
+
# The id is the first code span, or else the first word. The
|
|
129
|
+
# first Keyhac run with a known list had operators write
|
|
130
|
+
# "`dialog-gone` (known)", and the note became part of the id,
|
|
131
|
+
# so every known problem was counted as new.
|
|
132
|
+
code = re.match(r"`([^`]+)`", value)
|
|
133
|
+
value = code[1].strip() if code else (value.split() or [""])[0]
|
|
129
134
|
if key == "kind":
|
|
130
135
|
value = value.lower()
|
|
131
136
|
setattr(current, key, value)
|
|
@@ -385,22 +385,24 @@ def _one(adapter, case, variant, here: pathlib.Path, bundle, model, operator_mod
|
|
|
385
385
|
served = pages.Pages(adapter.fixtures) if adapter.fixtures else contextlib.nullcontext()
|
|
386
386
|
own_product = adapter.product_for(built) or contextlib.nullcontext()
|
|
387
387
|
op = None
|
|
388
|
+
backend = None
|
|
388
389
|
try:
|
|
389
390
|
with own_product, stand_in, served as site:
|
|
391
|
+
backend = adapter.screen_backend()
|
|
390
392
|
manifest = screens.write_manifest(
|
|
391
393
|
private / "manifest.json", adapter.screens,
|
|
392
394
|
fixtures=adapter.fixtures or adapter.checkout,
|
|
393
395
|
origin=site.origin if site else "", python=adapter.python,
|
|
394
396
|
state=directory, evidence=adapter.evidence,
|
|
395
397
|
apps=drive.manifest_entries(adapter.apps) if live else None,
|
|
396
|
-
record=evidence / "live")
|
|
398
|
+
record=evidence / "live", backend=backend)
|
|
397
399
|
op = operator.Operator(
|
|
398
400
|
room=built, directory=directory, scenario=case.path,
|
|
399
401
|
checkout=adapter.checkout,
|
|
400
402
|
evidence_help=adapter.evidence_help, manifest=manifest,
|
|
401
403
|
python=adapter.python, model=operator_model, known=known)
|
|
402
404
|
say(" operator: phase 1 - the screen")
|
|
403
|
-
op.phase_one(
|
|
405
|
+
op.phase_one(adapter.platform(), say=say)
|
|
404
406
|
_checkpoint()
|
|
405
407
|
status.phase(label, "room")
|
|
406
408
|
say(" room: running")
|
|
@@ -465,7 +467,9 @@ def _one(adapter, case, variant, here: pathlib.Path, bundle, model, operator_mod
|
|
|
465
467
|
finally:
|
|
466
468
|
if op is not None:
|
|
467
469
|
status.cost(label, op.cost)
|
|
468
|
-
closed = screens.close_all(directory
|
|
470
|
+
closed = (screens.close_all(directory, screens.load_backend(backend)
|
|
471
|
+
if backend else None)
|
|
472
|
+
if directory.exists() else [])
|
|
469
473
|
if closed:
|
|
470
474
|
say(f" closed: {', '.join(closed)} (left open by the operator)")
|
|
471
475
|
shutil.rmtree(private, ignore_errors=True)
|
|
@@ -78,13 +78,16 @@ class Terminal:
|
|
|
78
78
|
def write_manifest(path: pathlib.Path, screens: dict, *, fixtures: pathlib.Path,
|
|
79
79
|
origin: str, python: str, state: pathlib.Path,
|
|
80
80
|
evidence: dict | None = None, apps: dict | None = None,
|
|
81
|
-
record: pathlib.Path | None = None
|
|
81
|
+
record: pathlib.Path | None = None,
|
|
82
|
+
backend: dict | None = None) -> pathlib.Path:
|
|
82
83
|
"""The declarations, made absolute, for the operator's tools to read.
|
|
83
84
|
|
|
84
85
|
``evidence`` is ``{name: {"argv": [...], "help": "..."}}``: the product's
|
|
85
86
|
evidence commands, run by the operator's MCP server and nothing else.
|
|
86
87
|
``apps`` (from `quality_loop.drive.manifest_entries`) are the apps its
|
|
87
88
|
live check may drive, and ``record`` where those steps are written.
|
|
89
|
+
``backend`` is the adapter's own screen backend (`Adapter.screen_backend`),
|
|
90
|
+
used in place of this OS's.
|
|
88
91
|
"""
|
|
89
92
|
entries = {}
|
|
90
93
|
for name, screen in screens.items():
|
|
@@ -98,6 +101,8 @@ def write_manifest(path: pathlib.Path, screens: dict, *, fixtures: pathlib.Path,
|
|
|
98
101
|
"evidence": evidence or {}}
|
|
99
102
|
if apps:
|
|
100
103
|
manifest.update(apps=apps, record=str(record))
|
|
104
|
+
if backend:
|
|
105
|
+
manifest["backend"] = backend
|
|
101
106
|
path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
|
102
107
|
return path
|
|
103
108
|
|
|
@@ -124,8 +129,33 @@ def _record(manifest: dict, opened: list[dict]) -> None:
|
|
|
124
129
|
path.write_text(json.dumps(opened, indent=2), encoding="utf-8")
|
|
125
130
|
|
|
126
131
|
|
|
127
|
-
def
|
|
128
|
-
"""
|
|
132
|
+
def load_backend(spec: dict):
|
|
133
|
+
"""An adapter's own screen backend, from ``{"path": file, "settings": {...}}``.
|
|
134
|
+
|
|
135
|
+
The file is imported by path - it lives in the product's checkout, not in
|
|
136
|
+
an installed package - and, if it has ``configure(settings)``, that is
|
|
137
|
+
called before it is used. It answers what this OS's module answers:
|
|
138
|
+
``open_page(url, title, patience)``, ``open_terminal(command, title,
|
|
139
|
+
patience)`` and ``close(kind, handle)``. It is how a product puts its
|
|
140
|
+
screens where its app runs - in a VM, say - rather than on this desktop.
|
|
141
|
+
"""
|
|
142
|
+
import importlib.util
|
|
143
|
+
path = pathlib.Path(spec["path"])
|
|
144
|
+
module_spec = importlib.util.spec_from_file_location(
|
|
145
|
+
f"quality_loop_screen_backend_{path.stem}", path)
|
|
146
|
+
if module_spec is None or module_spec.loader is None:
|
|
147
|
+
raise ScreenError(f"cannot load the screen backend {path}")
|
|
148
|
+
module = importlib.util.module_from_spec(module_spec)
|
|
149
|
+
module_spec.loader.exec_module(module)
|
|
150
|
+
if hasattr(module, "configure"):
|
|
151
|
+
module.configure(spec.get("settings", {}))
|
|
152
|
+
return module
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def backend(manifest: dict | None = None):
|
|
156
|
+
"""The manifest's own backend if the adapter named one, else this OS's."""
|
|
157
|
+
if manifest and manifest.get("backend"):
|
|
158
|
+
return load_backend(manifest["backend"])
|
|
129
159
|
if sys.platform == "darwin":
|
|
130
160
|
from quality_loop.screens import _mac as module
|
|
131
161
|
elif os.name == "nt":
|
|
@@ -148,7 +178,7 @@ def open_screen(name: str, patience: float = 15.0, os_backend=None) -> dict:
|
|
|
148
178
|
raise ScreenError(f"no screen {name!r}; the adapter declares: "
|
|
149
179
|
+ ", ".join(sorted(screens)))
|
|
150
180
|
entry = screens[name]
|
|
151
|
-
os_backend = os_backend or backend()
|
|
181
|
+
os_backend = os_backend or backend(manifest)
|
|
152
182
|
if entry["kind"] == "page":
|
|
153
183
|
handle = os_backend.open_page(entry["url"], entry["title"], patience)
|
|
154
184
|
elif entry["kind"] == "terminal":
|
|
@@ -174,7 +204,7 @@ def close_screen(screen_id: str, os_backend=None) -> dict:
|
|
|
174
204
|
if record is None:
|
|
175
205
|
raise ScreenError(f"{screen_id!r} was not opened here; opened: "
|
|
176
206
|
+ (", ".join(r["id"] for r in opened) or "nothing"))
|
|
177
|
-
(os_backend or backend()).close(record["kind"], record["handle"])
|
|
207
|
+
(os_backend or backend(manifest)).close(record["kind"], record["handle"])
|
|
178
208
|
_record(manifest, [r for r in opened if r["id"] != screen_id])
|
|
179
209
|
return record
|
|
180
210
|
|
|
@@ -146,6 +146,14 @@ def test_a_field_continued_as_a_nested_list_is_kept():
|
|
|
146
146
|
assert "indented paragraph" in problems[0].detail
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
@pytest.mark.parametrize("written", ["`dialog-gone` (known)", "dialog-gone (known)",
|
|
150
|
+
"`dialog-gone`", "dialog-gone"])
|
|
151
|
+
def test_a_note_after_the_id_is_not_part_of_it(written):
|
|
152
|
+
problems, malformed = report.parse(
|
|
153
|
+
f"### [medium] Dialog\n- **Id:** {written}\n- **Evidence:** calls\n")
|
|
154
|
+
assert not malformed and problems[0].id == "dialog-gone"
|
|
155
|
+
|
|
156
|
+
|
|
149
157
|
def test_findings_concatenate_and_count_most_severe_first(tmp_path):
|
|
150
158
|
path = report.write_findings(tmp_path, [
|
|
151
159
|
("11_f1", "F1 off", REPORT),
|
|
@@ -165,3 +165,46 @@ def test_live_a_terminal_opens_with_its_title_and_closes(tmp_path, monkeypatch):
|
|
|
165
165
|
monkeypatch.setenv(screens.MANIFEST, str(path))
|
|
166
166
|
record = screens.open_screen("hello")
|
|
167
167
|
screens.close_screen(record["id"])
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
BACKEND = '''
|
|
171
|
+
calls = []
|
|
172
|
+
settings = {}
|
|
173
|
+
|
|
174
|
+
def configure(given):
|
|
175
|
+
settings.update(given)
|
|
176
|
+
|
|
177
|
+
def open_page(url, title, patience):
|
|
178
|
+
calls.append(("page", url, title, settings.get("where")))
|
|
179
|
+
return {"vm": settings.get("where")}
|
|
180
|
+
|
|
181
|
+
def open_terminal(command, title, patience):
|
|
182
|
+
calls.append(("terminal", command, title))
|
|
183
|
+
return 7
|
|
184
|
+
|
|
185
|
+
def close(kind, handle):
|
|
186
|
+
calls.append(("close", kind, handle))
|
|
187
|
+
'''
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_an_adapter_s_own_backend_opens_and_closes_the_screens(tmp_path, monkeypatch):
|
|
191
|
+
path = tmp_path / "guest_screens.py"
|
|
192
|
+
path.write_text(BACKEND, encoding="utf-8")
|
|
193
|
+
spec = {"path": str(path), "settings": {"where": "a-vm"}}
|
|
194
|
+
manifest = screens.write_manifest(
|
|
195
|
+
tmp_path / "manifest.json", {"approval-queue": Page("dialog.html", title="Queue")},
|
|
196
|
+
fixtures=tmp_path, origin="http://127.0.0.1:5000", python="/venv/python",
|
|
197
|
+
state=tmp_path / "operator", backend=spec)
|
|
198
|
+
monkeypatch.setenv(screens.MANIFEST, str(manifest))
|
|
199
|
+
record = screens.open_screen("approval-queue")
|
|
200
|
+
assert record["handle"] == {"vm": "a-vm"}, "configured before it was used"
|
|
201
|
+
# The runner's sweep has no manifest: it is handed the same backend.
|
|
202
|
+
backend = screens.load_backend(spec)
|
|
203
|
+
assert screens.close_all(tmp_path / "operator", os_backend=backend) == ["approval-queue-1"]
|
|
204
|
+
assert backend.calls == [("close", "page", {"vm": "a-vm"})]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_without_a_backend_of_its_own_it_is_this_os_s():
|
|
208
|
+
if sys.platform not in ("darwin", "win32"):
|
|
209
|
+
pytest.skip("no screen backend for this OS")
|
|
210
|
+
assert screens.backend({"screens": {}}) is screens.backend()
|
|
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
|