quality-loop 0.2.2__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.
Files changed (50) hide show
  1. {quality_loop-0.2.2/quality_loop.egg-info → quality_loop-0.3.0}/PKG-INFO +1 -1
  2. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/__init__.py +1 -1
  3. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/adapter.py +16 -0
  4. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/runner.py +7 -3
  5. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/screens/__init__.py +35 -5
  6. {quality_loop-0.2.2 → quality_loop-0.3.0/quality_loop.egg-info}/PKG-INFO +1 -1
  7. {quality_loop-0.2.2 → quality_loop-0.3.0}/tests/test_screens.py +43 -0
  8. {quality_loop-0.2.2 → quality_loop-0.3.0}/LICENSE +0 -0
  9. {quality_loop-0.2.2 → quality_loop-0.3.0}/README.md +0 -0
  10. {quality_loop-0.2.2 → quality_loop-0.3.0}/pyproject.toml +0 -0
  11. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/OPERATOR.md +0 -0
  12. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/__main__.py +0 -0
  13. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/audit.py +0 -0
  14. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/cli.py +0 -0
  15. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/compare.py +0 -0
  16. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/config.py +0 -0
  17. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/drive/__init__.py +0 -0
  18. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/drive/_mac.py +0 -0
  19. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/drive/_windows.py +0 -0
  20. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/drive/keys.py +0 -0
  21. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/drive/terminal.py +0 -0
  22. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/install.py +0 -0
  23. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/mcp.py +0 -0
  24. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/operator.py +0 -0
  25. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/pages.py +0 -0
  26. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/processes.py +0 -0
  27. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/progress.py +0 -0
  28. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/report.py +0 -0
  29. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/room.py +0 -0
  30. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/scenario.py +0 -0
  31. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/screens/_mac.py +0 -0
  32. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/screens/_windows.py +0 -0
  33. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/session.py +0 -0
  34. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/skill/SKILL.md +0 -0
  35. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/skill/references/filing.md +0 -0
  36. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/skill/references/setup.md +0 -0
  37. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/standin.py +0 -0
  38. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop/transcript.py +0 -0
  39. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop.egg-info/SOURCES.txt +0 -0
  40. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop.egg-info/dependency_links.txt +0 -0
  41. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop.egg-info/entry_points.txt +0 -0
  42. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop.egg-info/requires.txt +0 -0
  43. {quality_loop-0.2.2 → quality_loop-0.3.0}/quality_loop.egg-info/top_level.txt +0 -0
  44. {quality_loop-0.2.2 → quality_loop-0.3.0}/setup.cfg +0 -0
  45. {quality_loop-0.2.2 → quality_loop-0.3.0}/tests/test_core.py +0 -0
  46. {quality_loop-0.2.2 → quality_loop-0.3.0}/tests/test_drive.py +0 -0
  47. {quality_loop-0.2.2 → quality_loop-0.3.0}/tests/test_loop.py +0 -0
  48. {quality_loop-0.2.2 → quality_loop-0.3.0}/tests/test_mcp.py +0 -0
  49. {quality_loop-0.2.2 → quality_loop-0.3.0}/tests/test_operator.py +0 -0
  50. {quality_loop-0.2.2 → quality_loop-0.3.0}/tests/test_scenario_report.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quality-loop
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Measure what a product teaches - its skill, tools or manual - with isolated agent runs of real tasks, reported by an operator agent
5
5
  Author: craftware
6
6
  License-Expression: MIT
@@ -12,4 +12,4 @@ room is handed, putting the user's machine back - is code here, not an
12
12
  agent's discretion.
13
13
  """
14
14
 
15
- __version__ = "0.2.2"
15
+ __version__ = "0.3.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
@@ -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(sys.platform, say=say)
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) if directory.exists() else []
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) -> pathlib.Path:
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 backend():
128
- """This OS's implementation."""
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quality-loop
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Measure what a product teaches - its skill, tools or manual - with isolated agent runs of real tasks, reported by an operator agent
5
5
  Author: craftware
6
6
  License-Expression: MIT
@@ -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