quality-loop 0.2.0__tar.gz → 0.2.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.
Files changed (50) hide show
  1. {quality_loop-0.2.0/quality_loop.egg-info → quality_loop-0.2.1}/PKG-INFO +7 -4
  2. {quality_loop-0.2.0 → quality_loop-0.2.1}/README.md +6 -3
  3. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/__init__.py +1 -1
  4. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/drive/terminal.py +15 -2
  5. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/report.py +10 -0
  6. {quality_loop-0.2.0 → quality_loop-0.2.1/quality_loop.egg-info}/PKG-INFO +7 -4
  7. {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_drive.py +31 -0
  8. {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_loop.py +5 -3
  9. {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_operator.py +2 -2
  10. {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_scenario_report.py +18 -0
  11. {quality_loop-0.2.0 → quality_loop-0.2.1}/LICENSE +0 -0
  12. {quality_loop-0.2.0 → quality_loop-0.2.1}/pyproject.toml +0 -0
  13. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/OPERATOR.md +0 -0
  14. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/__main__.py +0 -0
  15. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/adapter.py +0 -0
  16. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/audit.py +0 -0
  17. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/cli.py +0 -0
  18. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/compare.py +0 -0
  19. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/config.py +0 -0
  20. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/drive/__init__.py +0 -0
  21. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/drive/_mac.py +0 -0
  22. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/drive/_windows.py +0 -0
  23. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/drive/keys.py +0 -0
  24. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/install.py +0 -0
  25. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/mcp.py +0 -0
  26. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/operator.py +0 -0
  27. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/pages.py +0 -0
  28. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/processes.py +0 -0
  29. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/progress.py +0 -0
  30. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/room.py +0 -0
  31. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/runner.py +0 -0
  32. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/scenario.py +0 -0
  33. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/screens/__init__.py +0 -0
  34. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/screens/_mac.py +0 -0
  35. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/screens/_windows.py +0 -0
  36. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/session.py +0 -0
  37. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/skill/SKILL.md +0 -0
  38. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/skill/references/filing.md +0 -0
  39. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/skill/references/setup.md +0 -0
  40. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/standin.py +0 -0
  41. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/transcript.py +0 -0
  42. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop.egg-info/SOURCES.txt +0 -0
  43. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop.egg-info/dependency_links.txt +0 -0
  44. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop.egg-info/entry_points.txt +0 -0
  45. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop.egg-info/requires.txt +0 -0
  46. {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop.egg-info/top_level.txt +0 -0
  47. {quality_loop-0.2.0 → quality_loop-0.2.1}/setup.cfg +0 -0
  48. {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_core.py +0 -0
  49. {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_mcp.py +0 -0
  50. {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_screens.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quality-loop
3
- Version: 0.2.0
3
+ Version: 0.2.1
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
@@ -121,9 +121,12 @@ under a policy the person sets. See [Running the loop](https://github.com/crftwr
121
121
 
122
122
  ## Status
123
123
 
124
- 0.x, early. It has been run end to end on macOS against the two examples and
125
- against Keyhac. The Windows screen and key backends are unit-tested but have
126
- not yet been run on a Windows machine. The design discussion is
124
+ 0.x, early. It has been run end to end on macOS against the two examples,
125
+ Keyhac and XeFM (including XeFM's terminal UI in a live check), and on
126
+ Windows (11, ARM64) against the toy, Keyhac and XeFM, with real page and
127
+ terminal screens. Not yet run anywhere for real: synthetic keys and
128
+ screenshots of a desktop app (`drive.DesktopApp`); a terminal app is not
129
+ driven on Windows yet. The design discussion is
127
130
  [crftwr/keyhac#205](https://github.com/crftwr/keyhac/issues/205) and this
128
131
  repository's issues.
129
132
 
@@ -90,9 +90,12 @@ under a policy the person sets. See [Running the loop](https://github.com/crftwr
90
90
 
91
91
  ## Status
92
92
 
93
- 0.x, early. It has been run end to end on macOS against the two examples and
94
- against Keyhac. The Windows screen and key backends are unit-tested but have
95
- not yet been run on a Windows machine. The design discussion is
93
+ 0.x, early. It has been run end to end on macOS against the two examples,
94
+ Keyhac and XeFM (including XeFM's terminal UI in a live check), and on
95
+ Windows (11, ARM64) against the toy, Keyhac and XeFM, with real page and
96
+ terminal screens. Not yet run anywhere for real: synthetic keys and
97
+ screenshots of a desktop app (`drive.DesktopApp`); a terminal app is not
98
+ driven on Windows yet. The design discussion is
96
99
  [crftwr/keyhac#205](https://github.com/crftwr/keyhac/issues/205) and this
97
100
  repository's issues.
98
101
 
@@ -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.0"
15
+ __version__ = "0.2.1"
@@ -31,6 +31,14 @@ class TerminalError(Exception):
31
31
  pass
32
32
 
33
33
 
34
+ #: Seconds after a lone Esc before the next key. A terminal app cannot tell
35
+ #: Esc followed quickly by a key from Alt+that key: it waits a moment (PuiKit
36
+ #: 0.1s, ncurses ESCDELAY) for the rest of a sequence. The first XeFM live
37
+ #: check sent Esc then Ctrl-T 20ms apart, and the command palette never
38
+ #: opened - it had arrived as Alt-Ctrl-T.
39
+ ESC_SETTLE = 0.3
40
+
41
+
34
42
  def _pyte():
35
43
  try:
36
44
  import pyte
@@ -68,8 +76,11 @@ class PtyApp:
68
76
  # The app's answers to queries (cursor position, device attributes)
69
77
  # go back down the pty, or a curses app that asks waits forever.
70
78
  self.screen.write_process_input = lambda data: self._write(data.encode("utf-8"))
71
- environment = dict(os.environ, **(env or {}), TERM="xterm-256color",
72
- LINES=str(rows), COLUMNS=str(cols))
79
+ # ESCDELAY: how long ncurses waits after an ESC to see whether a
80
+ # sequence follows - a second by default. Short, so a lone Esc is an
81
+ # Esc soon enough for ESC_SETTLE below to cover it.
82
+ environment = {"ESCDELAY": "25", **os.environ, **(env or {}),
83
+ "TERM": "xterm-256color", "LINES": str(rows), "COLUMNS": str(cols)}
73
84
  self.process = subprocess.Popen(
74
85
  argv, stdin=slave, stdout=slave, stderr=slave, cwd=cwd, env=environment,
75
86
  start_new_session=True, close_fds=True)
@@ -108,6 +119,8 @@ class PtyApp:
108
119
  self._write(item.text.encode("utf-8"))
109
120
  else:
110
121
  self._write(keynames.terminal_bytes(item))
122
+ if item.name == "Esc" and not item.mods:
123
+ time.sleep(ESC_SETTLE)
111
124
  time.sleep(0.02)
112
125
  self._sent = time.monotonic()
113
126
 
@@ -103,6 +103,10 @@ def parse(text: str, room: str = "") -> tuple[list[Problem], list[str]]:
103
103
  problems: list[Problem] = []
104
104
  current: Problem | None = None
105
105
  detail: list[str] = []
106
+ #: The field the indented lines below it continue. The first Windows runs
107
+ #: had operators write `- **Evidence:**` with the evidence as a nested
108
+ #: list under it, and five findings came out "no evidence" that had it.
109
+ field_key: str | None = None
106
110
 
107
111
  def close():
108
112
  if current is not None:
@@ -115,6 +119,7 @@ def parse(text: str, room: str = "") -> tuple[list[Problem], list[str]]:
115
119
  current = Problem(severity=heading["severity"].strip().lower(),
116
120
  title=heading["title"], room=room)
117
121
  detail = []
122
+ field_key = None
118
123
  elif current is not None and (field := _FIELD.match(line)) \
119
124
  and field["label"].strip().lower() in by_label:
120
125
  value = field["value"].strip()
@@ -124,7 +129,12 @@ def parse(text: str, room: str = "") -> tuple[list[Problem], list[str]]:
124
129
  if key == "kind":
125
130
  value = value.lower()
126
131
  setattr(current, key, value)
132
+ field_key = key
133
+ elif current is not None and field_key and line[:1].isspace() and line.strip():
134
+ before = getattr(current, field_key)
135
+ setattr(current, field_key, f"{before}\n{line.strip()}".strip())
127
136
  elif current is not None:
137
+ field_key = None
128
138
  detail.append(line)
129
139
  close()
130
140
  malformed = [f"{p.title}: {issue}" for p in problems for issue in p.problems()]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quality-loop
3
- Version: 0.2.0
3
+ Version: 0.2.1
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
@@ -121,9 +121,12 @@ under a policy the person sets. See [Running the loop](https://github.com/crftwr
121
121
 
122
122
  ## Status
123
123
 
124
- 0.x, early. It has been run end to end on macOS against the two examples and
125
- against Keyhac. The Windows screen and key backends are unit-tested but have
126
- not yet been run on a Windows machine. The design discussion is
124
+ 0.x, early. It has been run end to end on macOS against the two examples,
125
+ Keyhac and XeFM (including XeFM's terminal UI in a live check), and on
126
+ Windows (11, ARM64) against the toy, Keyhac and XeFM, with real page and
127
+ terminal screens. Not yet run anywhere for real: synthetic keys and
128
+ screenshots of a desktop app (`drive.DesktopApp`); a terminal app is not
129
+ driven on Windows yet. The design discussion is
127
130
  [crftwr/keyhac#205](https://github.com/crftwr/keyhac/issues/205) and this
128
131
  repository's issues.
129
132
 
@@ -190,3 +190,34 @@ def test_mac_key_codes():
190
190
  assert _mac.keycode(keys.parse("Shift-G")) == (5, _mac.FLAGS["shift"])
191
191
  assert _mac.keycode(keys.parse("G")) == (5, 0), "a letter names the key"
192
192
  assert _mac.keycode(keys.parse("F1"))[0] == 122
193
+
194
+
195
+ @posix_only
196
+ def test_a_lone_esc_is_not_glued_to_the_next_key(tmp_path):
197
+ """Esc then Ctrl-T, sent 20ms apart, reached XeFM as Alt-Ctrl-T."""
198
+ pytest.importorskip("pyte")
199
+ from quality_loop.drive import terminal
200
+ script = tmp_path / "gaps.py"
201
+ script.write_text(textwrap.dedent('''
202
+ import os, sys, time, tty
203
+ tty.setraw(0)
204
+ last, out = time.monotonic(), []
205
+ while len(out) < 2:
206
+ data = os.read(0, 16)
207
+ now = time.monotonic()
208
+ out.append((data.hex(), round(now - last, 2)))
209
+ last = now
210
+ sys.stdout.write(repr(out) + "\\r\\n")
211
+ sys.stdout.flush()
212
+ time.sleep(5)
213
+ '''))
214
+ app = terminal.PtyApp([sys.executable, str(script)], rows=5, cols=60)
215
+ try:
216
+ app.wait_quiet(patience=5)
217
+ app.send(keys.sequence(["Esc", "Ctrl-T"]))
218
+ app.wait_quiet(patience=5)
219
+ seen = eval(app.text().strip().splitlines()[-1])
220
+ finally:
221
+ app.stop()
222
+ assert [hex_ for hex_, _gap in seen] == ["1b", "14"], "two reads, not one ESC+key burst"
223
+ assert seen[1][1] >= terminal.ESC_SETTLE - 0.05
@@ -401,12 +401,14 @@ def test_an_upgrade_leaves_a_hand_edited_skill_alone(tmp_path):
401
401
 
402
402
 
403
403
  def test_the_command_found_is_relative_inside_the_checkout(tmp_path, monkeypatch):
404
- bin_dir = tmp_path / ".venv" / "bin"
404
+ # The venv's own layout: Scripts/quality-loop.exe on Windows.
405
+ bin_dir = tmp_path / ".venv" / ("Scripts" if os.name == "nt" else "bin")
405
406
  bin_dir.mkdir(parents=True)
406
- (bin_dir / "quality-loop").write_text("")
407
+ script = "quality-loop.exe" if os.name == "nt" else "quality-loop"
408
+ (bin_dir / script).write_text("")
407
409
  monkeypatch.setattr(sys, "executable", str(bin_dir / "python"))
408
410
  monkeypatch.setattr(install.shutil, "which", lambda _name: None)
409
- assert install.command_for(tmp_path) == ".venv/bin/quality-loop"
411
+ assert install.command_for(tmp_path) == (bin_dir / script).relative_to(tmp_path).as_posix()
410
412
  assert install.commands_for(tmp_path) == {
411
413
  "command": ".venv/bin/quality-loop",
412
414
  "command_windows": ".venv/Scripts/quality-loop.exe"}
@@ -41,8 +41,8 @@ def test_the_operator_has_no_shell_and_only_quality_loop_s_server(op):
41
41
  assert "Write" not in allow and "Edit" not in allow
42
42
  assert "mcp__quality-loop" in allow
43
43
  writes = [rule for rule in allow if rule.startswith(("Write(", "Edit("))]
44
- assert writes == [f"Edit(/{op.directory.resolve().as_posix()}/**)",
45
- f"Edit(/{(op.room.resolve() / 'REPORT.md').as_posix()})"]
44
+ assert writes == [f"Edit({operator.absolute(op.directory)}/**)",
45
+ f"Edit({operator.absolute(op.room / 'REPORT.md')})"]
46
46
  assert all(rule.startswith("Edit(//") for rule in writes), "absolute: //"
47
47
  assert _value(command, "--permission-mode") == "dontAsk"
48
48
 
@@ -128,6 +128,24 @@ def test_a_report_is_read_the_way_a_loop_needs_it():
128
128
  assert any("no id" in m for m in malformed) and any("no evidence" in m for m in malformed)
129
129
 
130
130
 
131
+ def test_a_field_continued_as_a_nested_list_is_kept():
132
+ problems, malformed = report.parse(
133
+ "### [medium] Defaults pasted in\n"
134
+ "- **Id:** `extend-default`\n"
135
+ "- **Kind:** product\n"
136
+ "- **Evidence:**\n"
137
+ " - 66 new lines in a 14-line file\n"
138
+ " - config.md says to copy the defaults\n"
139
+ "- **Where it would be fixed:** config.md\n"
140
+ "\n"
141
+ " An indented paragraph after a blank line is free text.\n")
142
+ assert not malformed
143
+ assert problems[0].evidence == ("- 66 new lines in a 14-line file\n"
144
+ "- config.md says to copy the defaults")
145
+ assert problems[0].where == "config.md"
146
+ assert "indented paragraph" in problems[0].detail
147
+
148
+
131
149
  def test_findings_concatenate_and_count_most_severe_first(tmp_path):
132
150
  path = report.write_findings(tmp_path, [
133
151
  ("11_f1", "F1 off", REPORT),
File without changes
File without changes