design-playbook 0.12.0 → 0.13.0

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 (43) hide show
  1. package/design_playbook.py +20 -0
  2. package/mcp/evidence/capture_contract.py +268 -0
  3. package/mcp/evidence/capture_runtime.py +505 -0
  4. package/mcp/evidence/containment.py +164 -0
  5. package/mcp/evidence/ledger_syntax.py +131 -0
  6. package/mcp/evidence/server.py +16 -570
  7. package/mcp/evidence/test_capture_contract.py +355 -0
  8. package/mcp/evidence/test_containment.py +581 -0
  9. package/mcp/evidence/test_ledger_syntax.py +252 -0
  10. package/mcp/evidence/test_server_stdio.py +254 -426
  11. package/mcp/preview/compatibility.py +158 -0
  12. package/mcp/preview/control.js +7 -9
  13. package/mcp/preview/control.py +3 -7
  14. package/mcp/preview/integrity.py +362 -0
  15. package/mcp/preview/{browser.py → review_session.py} +61 -15
  16. package/mcp/preview/server.py +14 -9
  17. package/mcp/preview/test_browser_control.py +194 -177
  18. package/mcp/preview/test_integrity.py +208 -0
  19. package/mcp/preview/test_server_stdio.py +10 -449
  20. package/mcp/preview/test_transaction.py +23 -18
  21. package/mcp/preview/test_versions.py +77 -24
  22. package/mcp/preview/test_versions_freeze.py +176 -0
  23. package/mcp/preview/transaction.py +32 -56
  24. package/mcp/preview/util.py +1 -21
  25. package/mcp/preview/versions.py +78 -132
  26. package/mcp/test_transport.py +6 -2
  27. package/package.json +2 -1
  28. package/scripts/__init__.py +0 -0
  29. package/scripts/g1_spec.py +81 -0
  30. package/scripts/g2_g4_pointback.py +310 -0
  31. package/scripts/g5_preview.py +265 -0
  32. package/scripts/g6_evidence.py +252 -0
  33. package/scripts/g6_records.py +56 -0
  34. package/scripts/g6_warnings.py +104 -0
  35. package/scripts/g7_contract_drift.py +2 -2
  36. package/scripts/run_facts.py +212 -0
  37. package/scripts/run_status.py +127 -147
  38. package/scripts/stages.py +90 -0
  39. package/scripts/test_verdict_syntax.py +239 -0
  40. package/scripts/validate_run.py +67 -978
  41. package/scripts/verdict_syntax.py +96 -0
  42. package/mcp/preview/test_anchor_v2.py +0 -69
  43. package/scripts/_preview_integrity.py +0 -288
@@ -0,0 +1,20 @@
1
+ """Namespace alias: absolute ``design_playbook.*`` imports inside this plugin.
2
+
3
+ The plugin directory is ``design-playbook`` (hyphen), so Python's import
4
+ system cannot load it under the identifier ``design_playbook`` — FileFinder
5
+ matches directory names literally. This module aliases the package root onto
6
+ ``design_playbook.__path__`` so that, once the one bootstrap (ADR-0022) puts
7
+ the package root on ``sys.path``, absolute imports such as
8
+ ``design_playbook.mcp.preview.integrity`` and
9
+ ``design_playbook.scripts.stages`` resolve to the real ``mcp/`` and
10
+ ``scripts/`` trees below.
11
+
12
+ The alias also keeps the import seam namespaced: a bare ``mcp.*`` import
13
+ would collide with the PyPI ``mcp`` SDK when the host has it installed.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from pathlib import Path
18
+
19
+ __path__ = [str(Path(__file__).resolve().parent)]
20
+ __all__: list[str] = []
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env python3
2
+ """Capture contract v1 rules, owned by the bundled Evidence runtime.
3
+
4
+ ADR-0018 enforcement site 1: this module is the single owner of the v1
5
+ contract surface — the write-side parse/normalize authority, the read-side
6
+ full-shape snapshot validator, and the contract-fields JSON Schema fragment
7
+ the provider tool schema composes. The provider (server.py) keeps only
8
+ Runtime Object fields, path/overwrite boundaries, and Playwright I/O; G6
9
+ (scripts/validate_run.py) validates bound manifest request snapshots through
10
+ ``validate_capture_snapshot`` instead of hand-written partial checks.
11
+
12
+ Named ``capture_contract.py`` to avoid collision with
13
+ ``scripts/contract_v1.py`` (the persistent contract, ADR-0017).
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from dataclasses import dataclass
19
+ from typing import Any
20
+
21
+ CAPTURE_SCHEMA_VERSION = 1
22
+ COLOR_SCHEMES = frozenset({"light", "dark", "no-preference"})
23
+ MIN_VIEWPORT_DPR = 0.1
24
+ RECAPTURE_HINT = "recapture with capture contract schemaVersion=1"
25
+ FREEZE_DEFAULTS = {
26
+ "enabled": True,
27
+ "waitFonts": True,
28
+ "networkIdle": False,
29
+ }
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class CaptureFact:
34
+ """One host-neutral capture-contract violation (empty facts = valid)."""
35
+
36
+ code: str
37
+ detail: str
38
+ expected: str = ""
39
+ actual: str = ""
40
+
41
+
42
+ def _is_schema_version(value: object) -> bool:
43
+ return type(value) is int and value == CAPTURE_SCHEMA_VERSION
44
+
45
+
46
+ def _bad_viewport(viewport: dict[str, Any]) -> str | None:
47
+ """First malformed viewport field, or None when the shape is valid.
48
+
49
+ Mirrors the parser's field rules exactly so the read side and write side
50
+ cannot disagree on what a valid viewport is.
51
+ """
52
+ width = viewport.get("width")
53
+ height = viewport.get("height")
54
+ dpr = viewport.get("devicePixelRatio")
55
+ scheme = viewport.get("colorScheme")
56
+ if type(width) is not int or width < 1:
57
+ return "viewport.width must be a positive integer"
58
+ if type(height) is not int or height < 1:
59
+ return "viewport.height must be a positive integer"
60
+ if (
61
+ type(dpr) not in (int, float)
62
+ or not math.isfinite(dpr)
63
+ or dpr < MIN_VIEWPORT_DPR
64
+ ):
65
+ return (
66
+ "viewport.devicePixelRatio must be a number greater than or equal "
67
+ f"to {MIN_VIEWPORT_DPR}"
68
+ )
69
+ if not isinstance(scheme, str) or scheme not in COLOR_SCHEMES:
70
+ return (
71
+ f"viewport.colorScheme must be one of {sorted(COLOR_SCHEMES)}; "
72
+ f"got {scheme!r}"
73
+ )
74
+ return None
75
+
76
+
77
+ def _bad_freeze(freeze: dict[str, Any]) -> str | None:
78
+ """First malformed freeze field, or None when the shape is valid."""
79
+ for key in FREEZE_DEFAULTS:
80
+ if not isinstance(freeze.get(key), bool):
81
+ return f"freeze.{key} must be a boolean"
82
+ return None
83
+
84
+
85
+ def parse_capture_contract(args: dict[str, Any]) -> dict[str, Any]:
86
+ """Validate capture contract v1 fields and return a normalized request.
87
+
88
+ Write authority (ADR-0018): raises ValueError with a recapture instruction
89
+ for missing/unknown versions or an incomplete viewport. Pure — no browser
90
+ side effects. The normalized output is what the provider echoes into the
91
+ manifest request snapshot, so real snapshots always carry freeze defaults.
92
+ """
93
+ if "schemaVersion" not in args:
94
+ raise ValueError(
95
+ f"capture contract schemaVersion is required; {RECAPTURE_HINT}"
96
+ )
97
+ version = args.get("schemaVersion")
98
+ if not _is_schema_version(version):
99
+ raise ValueError(
100
+ f"unsupported capture schemaVersion {version!r}; {RECAPTURE_HINT}"
101
+ )
102
+ viewport = args.get("viewport")
103
+ if not isinstance(viewport, dict):
104
+ raise ValueError(
105
+ f"viewport object is required for schemaVersion=1; {RECAPTURE_HINT}"
106
+ )
107
+ bad_viewport = _bad_viewport(viewport)
108
+ if bad_viewport is not None:
109
+ raise ValueError(bad_viewport)
110
+ width = viewport["width"]
111
+ height = viewport["height"]
112
+ dpr = viewport["devicePixelRatio"]
113
+ scheme = viewport["colorScheme"]
114
+
115
+ freeze_raw = args.get("freeze")
116
+ if freeze_raw is None:
117
+ freeze_raw = {}
118
+ if not isinstance(freeze_raw, dict):
119
+ raise ValueError("freeze must be an object when provided")
120
+ freeze = {
121
+ key: freeze_raw.get(key, FREEZE_DEFAULTS[key])
122
+ for key in FREEZE_DEFAULTS
123
+ }
124
+ bad_freeze = _bad_freeze(freeze)
125
+ if bad_freeze is not None:
126
+ raise ValueError(bad_freeze)
127
+
128
+ return {
129
+ "schemaVersion": CAPTURE_SCHEMA_VERSION,
130
+ "viewport": {
131
+ "width": width,
132
+ "height": height,
133
+ "devicePixelRatio": float(dpr),
134
+ "colorScheme": scheme,
135
+ },
136
+ "freeze": freeze,
137
+ }
138
+
139
+
140
+ def validate_capture_snapshot(snapshot: object) -> list[CaptureFact]:
141
+ """Read authority: full-shape validation of a bound manifest snapshot.
142
+
143
+ Host-neutral — never raises, returns facts (empty list = valid). Strict on
144
+ the v1 full shape: schemaVersion=1, a complete typed viewport, and a
145
+ complete boolean freeze. The parser normalizes defaults at capture time;
146
+ the read side requires the recorded snapshot to be self-contained so the
147
+ manifest alone can reproduce the capture (ADR-0018). Malformed viewport
148
+ shape or missing freeze therefore fail closed (sanctioned correction; was
149
+ lax in the old hand-written G6 checks). Unknown extra keys are tolerated
150
+ (host-neutral forward compatibility).
151
+ """
152
+ if not isinstance(snapshot, dict):
153
+ return [CaptureFact(
154
+ "missing_schema_version",
155
+ "no request snapshot on the bound entry",
156
+ expected="schemaVersion=1 with viewport and freeze",
157
+ actual=("None" if snapshot is None else type(snapshot).__name__),
158
+ )]
159
+ version = snapshot.get("schemaVersion")
160
+ if not _is_schema_version(version):
161
+ return [CaptureFact(
162
+ ("missing_schema_version" if version is None
163
+ else "unsupported_schema_version"),
164
+ ("missing schemaVersion" if version is None
165
+ else f"unsupported schemaVersion {version!r}"),
166
+ expected="schemaVersion=1",
167
+ actual=repr(version),
168
+ )]
169
+ viewport = snapshot.get("viewport")
170
+ if not isinstance(viewport, dict):
171
+ return [CaptureFact(
172
+ "missing_viewport",
173
+ "viewport object is required for schemaVersion=1",
174
+ expected="viewport width/height/devicePixelRatio/colorScheme",
175
+ actual=type(viewport).__name__ if viewport is not None else "missing",
176
+ )]
177
+ bad_viewport = _bad_viewport(viewport)
178
+ if bad_viewport is not None:
179
+ return [CaptureFact(
180
+ "bad_viewport_shape",
181
+ bad_viewport,
182
+ expected="viewport width/height/devicePixelRatio/colorScheme",
183
+ actual=bad_viewport,
184
+ )]
185
+ freeze = snapshot.get("freeze")
186
+ if not isinstance(freeze, dict):
187
+ return [CaptureFact(
188
+ "missing_freeze",
189
+ "freeze snapshot is required on the bound entry",
190
+ expected="freeze enabled/waitFonts/networkIdle booleans",
191
+ actual=type(freeze).__name__ if freeze is not None else "missing",
192
+ )]
193
+ bad_freeze = _bad_freeze(freeze)
194
+ if bad_freeze is not None:
195
+ return [CaptureFact(
196
+ "bad_freeze_shape",
197
+ bad_freeze,
198
+ expected="freeze enabled/waitFonts/networkIdle booleans",
199
+ actual=bad_freeze,
200
+ )]
201
+ return []
202
+
203
+
204
+ def capture_contract_schema_fragment() -> dict[str, Any]:
205
+ """JSON Schema fragment for the contract fields (schemaVersion/viewport/freeze).
206
+
207
+ The provider composes this into its tool schema alongside its Runtime
208
+ Object fields. const/enum/required/default all come from the same module
209
+ constants the parser uses, so the schema and the parser cannot drift.
210
+ """
211
+ return {
212
+ "properties": {
213
+ "schemaVersion": {
214
+ "type": "integer",
215
+ "description": "Capture contract version. Only 1 is supported.",
216
+ "const": CAPTURE_SCHEMA_VERSION,
217
+ },
218
+ "viewport": {
219
+ "type": "object",
220
+ "description": (
221
+ "Required capture viewport. Provider does not invent "
222
+ "desktop defaults."
223
+ ),
224
+ "properties": {
225
+ "width": {"type": "integer", "minimum": 1},
226
+ "height": {"type": "integer", "minimum": 1},
227
+ "devicePixelRatio": {
228
+ "type": "number",
229
+ "minimum": MIN_VIEWPORT_DPR,
230
+ },
231
+ "colorScheme": {
232
+ "type": "string",
233
+ "enum": sorted(COLOR_SCHEMES),
234
+ },
235
+ },
236
+ "required": [
237
+ "width",
238
+ "height",
239
+ "devicePixelRatio",
240
+ "colorScheme",
241
+ ],
242
+ "additionalProperties": False,
243
+ },
244
+ "freeze": {
245
+ "type": "object",
246
+ "description": (
247
+ "Deterministic freeze controls. Defaults: "
248
+ "enabled=true, waitFonts=true, networkIdle=false."
249
+ ),
250
+ "properties": {
251
+ "enabled": {
252
+ "type": "boolean",
253
+ "default": FREEZE_DEFAULTS["enabled"],
254
+ },
255
+ "waitFonts": {
256
+ "type": "boolean",
257
+ "default": FREEZE_DEFAULTS["waitFonts"],
258
+ },
259
+ "networkIdle": {
260
+ "type": "boolean",
261
+ "default": FREEZE_DEFAULTS["networkIdle"],
262
+ },
263
+ },
264
+ "additionalProperties": False,
265
+ },
266
+ },
267
+ "required": ["schemaVersion", "viewport"],
268
+ }