design-playbook 0.9.0 → 0.9.2

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.
@@ -142,9 +142,34 @@ def _captured(
142
142
  }
143
143
 
144
144
 
145
+ _RUN_MARKERS = ("plan.md", "point-back.md")
146
+ _warned_run_root = False
147
+
148
+
145
149
  def _run_root() -> Path:
146
150
  configured = os.environ.get(RUN_ROOT_ENV)
147
- return Path(configured).resolve() if configured else Path.cwd().resolve()
151
+ if not configured or configured == ".":
152
+ # cwd-relative default silently mis-roots multi-run workspaces (the
153
+ # root .mcp.json ships DESIGN_PLAYBOOK_RUN_ROOT="."). Warn only when
154
+ # cwd does not look like a run dir (no run marker file) — the shipped
155
+ # default resolving to a real run dir is correct usage, not a
156
+ # misconfig — and only once per process to avoid per-capture spam.
157
+ root = Path.cwd().resolve()
158
+ global _warned_run_root
159
+ if not _warned_run_root and not any(
160
+ (root / marker).is_file() for marker in _RUN_MARKERS
161
+ ):
162
+ _warned_run_root = True
163
+ _log(
164
+ "WARNING: DESIGN_PLAYBOOK_RUN_ROOT is unset or '.' "
165
+ f"(cwd-relative) and {root} has no run marker "
166
+ f"({' / '.join(_RUN_MARKERS)}); artifacts resolve under "
167
+ f"{root}/evidence/. Set DESIGN_PLAYBOOK_RUN_ROOT to the run "
168
+ "root when the host workspace is not the intended run "
169
+ "directory."
170
+ )
171
+ return root
172
+ return Path(configured).resolve()
148
173
 
149
174
 
150
175
  def _resolve_artifact_path(artifact_path: str) -> Path:
@@ -18,6 +18,7 @@ whose names did not match any keyword. See review item M3.
18
18
  from __future__ import annotations
19
19
 
20
20
  import json
21
+ import os
21
22
  import subprocess
22
23
  import sys
23
24
  import tempfile
@@ -50,6 +51,7 @@ def _run_stdio(
50
51
  *,
51
52
  cwd: Path | None = None,
52
53
  no_site: bool = False,
54
+ env: dict[str, str] | None = None,
53
55
  ) -> subprocess.CompletedProcess[str]:
54
56
  wire_input = "".join(
55
57
  json.dumps(request, ensure_ascii=False) + "\n" for request in requests
@@ -67,6 +69,7 @@ def _run_stdio(
67
69
  timeout=timeout,
68
70
  check=False,
69
71
  cwd=cwd,
72
+ env=env,
70
73
  )
71
74
 
72
75
 
@@ -204,6 +207,57 @@ class EvidencePurePathTests(unittest.TestCase):
204
207
  self.assertEqual(payload["observed_state"], "unknown")
205
208
  self.assertFalse(outside.exists())
206
209
 
210
+ def test_run_root_warning_only_without_run_marker_and_once(self) -> None:
211
+ """Default RUN_ROOT ('.'/unset) must not warn when cwd is a run dir.
212
+
213
+ Shipped defaults (root .mcp.json RUN_ROOT="." / external installs
214
+ unset) are correct usage when cwd is the run dir — warning there is a
215
+ 100% false positive. Warn only when cwd lacks a run marker
216
+ (plan.md / point-back.md), and only once per process.
217
+ """
218
+ def _call(request_id: int) -> dict:
219
+ return {
220
+ "jsonrpc": "2.0",
221
+ "id": request_id,
222
+ "method": "tools/call",
223
+ "params": {
224
+ "name": "execute_capture_plan",
225
+ "arguments": {
226
+ "url": "about:blank",
227
+ "type": "screenshot",
228
+ "state": "ok",
229
+ "actions": [],
230
+ # Rejected before Playwright import, but after
231
+ # _run_root() — cheap way to reach the warning path.
232
+ "artifact_path": "spec.md",
233
+ },
234
+ },
235
+ }
236
+
237
+ requests = [
238
+ {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}},
239
+ _call(2),
240
+ _call(3),
241
+ ]
242
+ env = {k: v for k, v in os.environ.items() if k != "DESIGN_PLAYBOOK_RUN_ROOT"}
243
+
244
+ with tempfile.TemporaryDirectory() as tmp:
245
+ bare = Path(tmp)
246
+ completed = _run_stdio(requests, timeout=15, cwd=bare, env=env)
247
+ self.assertEqual(completed.returncode, 0, completed.stderr)
248
+ self.assertEqual(
249
+ completed.stderr.count("DESIGN_PLAYBOOK_RUN_ROOT is unset or '.'"),
250
+ 1,
251
+ completed.stderr,
252
+ )
253
+
254
+ with tempfile.TemporaryDirectory() as tmp:
255
+ run_dir = Path(tmp)
256
+ (run_dir / "plan.md").write_text("# plan", encoding="utf-8")
257
+ completed = _run_stdio(requests, timeout=15, cwd=run_dir, env=env)
258
+ self.assertEqual(completed.returncode, 0, completed.stderr)
259
+ self.assertNotIn("DESIGN_PLAYBOOK_RUN_ROOT is unset", completed.stderr)
260
+
207
261
  def test_provider_rejects_non_evidence_subtree_paths(self) -> None:
208
262
  """G6 containment: artifact_path must already live under evidence/."""
209
263
  with tempfile.TemporaryDirectory() as tmp:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "design-playbook",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Design I/O for coding agents: controllable UI generation via declarations (spec/domain/craft/design/components/template) and contracts (skill/evaluator). Use for product UI—console, dashboard, agent-ops, CJK-first apps.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -159,7 +159,7 @@ After craft, probe MCP `tools/list` for **`execute_capture_plan`**.
159
159
  - **Absent** → skip; `ui-evaluator` ledger `observed` stays free-text (current behavior). G6 not triggered.
160
160
  - **Present** → for each L6 criterion whose proof is a runtime state, run the evidence loop in this orchestrator (not inside any skill):
161
161
  1. **Derive** a capture plan from L6 `Given -> When -> Then` (in memory, not on disk): `Given`/`When` → `state` + `actions`; `Then` → required proof (already in the ledger `required` field). Do not add or remove verification intent; L6 wins on conflict.
162
- 2. **Execute**: call `execute_capture_plan({url, type, state, actions, artifact_path})`. The provider returns `{artifact, observed_state, result, error, written_path}` and never sees the criterion. Prefer `written_path` (absolute) when locating the file; if it points outside `.scratch/<run>/`, fix `DESIGN_PLAYBOOK_RUN_ROOT` / cwd before binding. `artifact_path` must start with `evidence/` (e.g., `evidence/empty-state.png`, not `empty-state.png`) — the provider resolves it under `<run_root>/evidence/` and refuses absolute paths, `..` segments, or anything that escapes that subtree (`mcp/evidence/server.py` `_resolve_artifact_path`); a bare filename is rejected because it would land outside the evidence subtree.
162
+ 2. **Execute**: call `execute_capture_plan({url, type, state, actions, artifact_path})`. The provider returns `{artifact, observed_state, result, error, written_path}` and never sees the criterion. Prefer `written_path` (absolute) when locating the file; if it points outside `.scratch/<run>/`, fix `DESIGN_PLAYBOOK_RUN_ROOT` / cwd before binding. `artifact_path` must start with `evidence/` (e.g., `evidence/empty-state.png`, not `empty-state.png`) — the provider resolves it under `<run_root>/evidence/` and refuses absolute paths, `..` segments, or anything that escapes that subtree (`mcp/evidence/server.py` `_resolve_artifact_path`); a bare filename is rejected because it would land outside the evidence subtree. **Async-init timing**: when the page has an async init (skeleton/loading before `body[data-state]` reaches the target state), include a `wait_for_state` action for that state before the capture action. A capture that lands mid-init records the loading state honestly (`observed_state: loading`), which proves the wrong criterion (dogfood 2026-08-01 settings run).
163
163
  3. **Bind** (orchestrator owns the manifest; provider never writes it). After **each** successful or failed capture, **immediately append** one line to `.scratch/<run>/evidence/manifest.jsonl` — do not batch-rewrite the file at the end. Rules:
164
164
  - **`observed_state` / `result` / `error`**: copy the provider return **verbatim**. If the provider returns `unknown`, write `unknown` — never overwrite with the requested `state` (request intent lives only under `capture.state`).
165
165
  - **Embedded capture snapshot**: store the full call parameters used (`url` including query string, `type`, `state`, `actions`, `artifact_path`). Omit nothing that would be needed to re-run the capture.