design-playbook 0.21.2 → 0.22.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.
- package/README.md +15 -4
- package/codex/AGENTS.md +11 -13
- package/commands/doctor.md +2 -0
- package/commands/run-handoff.md +27 -0
- package/commands/run-status.md +4 -2
- package/lib/index.js +7 -6
- package/mcp/evidence/handoff.py +93 -26
- package/mcp/evidence/handoff_i18n.py +103 -0
- package/mcp/evidence/static_handoff_page.html +25 -22
- package/mcp/evidence/test_handoff.py +48 -3
- package/mcp/evidence/test_handoff_i18n.py +115 -0
- package/mcp/preview/control.css +17 -14
- package/mcp/preview/control.html +58 -55
- package/mcp/preview/control.js +37 -17
- package/mcp/preview/control.py +25 -143
- package/mcp/preview/control.review.js +91 -31
- package/mcp/preview/i18n.py +32 -12
- package/mcp/preview/review_session.py +3 -3
- package/mcp/preview/test_browser_control.py +6 -3
- package/mcp/preview/test_i18n_labels.py +3 -0
- package/mcp/preview/test_preview_usability.py +247 -0
- package/mcp/run_console/app.css +73 -1
- package/mcp/run_console/app.html +1 -0
- package/mcp/run_console/app.js +526 -8
- package/mcp/run_console/repair_packet.py +379 -0
- package/mcp/run_console/test_read_only_trial.py +3 -1
- package/mcp/run_console/test_repair_packet.py +550 -0
- package/mcp/run_console/test_ui_browser.py +17 -8
- package/mcp/ui_locale.py +20 -0
- package/package.json +1 -1
- package/scripts/adapter_templates/codex-agents.md +10 -12
- package/scripts/capability_receipt.py +291 -0
- package/scripts/run_continuation.py +462 -0
- package/scripts/run_facts.py +35 -15
- package/scripts/run_handoff.py +257 -0
- package/scripts/run_profile.py +3 -3
- package/scripts/run_status.py +17 -2
- package/scripts/status_projection.py +21 -7
- package/skills/design-playbook/SKILL.md +19 -4
package/README.md
CHANGED
|
@@ -56,6 +56,9 @@ After install, skills and commands are **namespaced** by the plugin name:
|
|
|
56
56
|
| `/design-playbook:ux-spec` | Spec-only command |
|
|
57
57
|
| `/design-playbook:ui-review` | Review command |
|
|
58
58
|
| `/design-playbook:run-review` | Cross-run review command |
|
|
59
|
+
| `/design-playbook:run-status` | Run phase, blocker, and resume/continuation narration |
|
|
60
|
+
| `/design-playbook:run-handoff` | Static delivery package for one reviewed run |
|
|
61
|
+
| `/design-playbook:doctor` | Install health diagnosis |
|
|
59
62
|
|
|
60
63
|
Bare `/design-io` is **not** the installed name — always use the `design-playbook:` prefix.
|
|
61
64
|
|
|
@@ -73,7 +76,7 @@ pi has no plugin namespace — skills are `/skill:<name>`, commands are bare `/<
|
|
|
73
76
|
| --- | --- |
|
|
74
77
|
| `/skill:design-playbook` | Orchestrator skill (model-invoked) |
|
|
75
78
|
| `/skill:ux-spec` … `/skill:ui-evaluator` | Same eight skills as above |
|
|
76
|
-
| `/design-io` · `/ux-spec` · `/ui-review` · `/run-review` | Pipeline / spec-only / review / cross-run commands |
|
|
79
|
+
| `/design-io` · `/ux-spec` · `/ui-review` · `/run-review` · `/run-status` · `/run-handoff` · `/doctor` | Pipeline / spec-only / review / cross-run / status / handoff / health commands |
|
|
77
80
|
|
|
78
81
|
pi ships no built-in MCP, so `preview*` and `observe*` skip by default (ADR-0009 absent→skip; the pipeline still runs spec → picker → fill → craft → accept). To enable both gates, install an MCP adapter and register the bundled servers in your project `.mcp.json`:
|
|
79
82
|
|
|
@@ -128,7 +131,7 @@ See the root [README](../../README.md#install-on-other-agents) for the tier tabl
|
|
|
128
131
|
.mcp.json ← bundled MCP servers, launched via ${CLAUDE_PLUGIN_ROOT} (ADR-0009)
|
|
129
132
|
mcp/{preview,evidence}/← MCP adapter runtimes (preview_prototype / execute_capture_plan)
|
|
130
133
|
skills/<name>/SKILL.md ← model-invoked skills
|
|
131
|
-
commands/<name>.md ← slash commands (design-io, ux-spec, ui-review, run-review)
|
|
134
|
+
commands/<name>.md ← slash commands (design-io, ux-spec, ui-review, run-review, run-status, run-handoff, doctor)
|
|
132
135
|
codex/AGENTS.md ← Codex bridge notes
|
|
133
136
|
examples/ ← self-authored onboarding samples
|
|
134
137
|
LICENSE · NOTICE ← authored-only scope
|
|
@@ -154,7 +157,15 @@ python <pkg>/scripts/run_status.py .scratch/<run> --json # machine-readable
|
|
|
154
157
|
python <pkg>/scripts/run_status.py --list # newest runs under .scratch/
|
|
155
158
|
```
|
|
156
159
|
|
|
157
|
-
The status command reuses the packaged validator’s G5 confirm rules. It is part of the installed package — not monorepo-only tooling.
|
|
160
|
+
The status command reuses the packaged validator’s G5 confirm rules. It is part of the installed package — not monorepo-only tooling. For an eligible run it also reports an explicit `open-console` continuation command for the local Run Console (it never starts a server itself), with the blocking reason and a safe fallback when the run or Console prerequisites are ineligible. The Console’s current claim is **local, experimental, and trial-gated** — `run-status --json` reports the same capability receipt (`publicClaim: experimental`) — and no authorized external trial or public release is claimed until the separately authorized read-only trial gate passes (ADR-0043).
|
|
161
|
+
|
|
162
|
+
### Static handoff
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
python <pkg>/scripts/run_handoff.py <run> [--fill <declared-path>] [--round N] [--json]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Builds the existing static delivery package for one explicit run from its declared `fill:` path. Multiple declarations need an explicit `--fill`; a missing declaration fails with repair guidance instead of guessing. `verdict` / `authority` / `confirmationSource` are reported verbatim — a `Pending` handoff stays `Pending` and never becomes acceptance.
|
|
158
169
|
|
|
159
170
|
### Doctor
|
|
160
171
|
|
|
@@ -163,7 +174,7 @@ python <pkg>/scripts/doctor.py
|
|
|
163
174
|
python <pkg>/scripts/doctor.py --json
|
|
164
175
|
```
|
|
165
176
|
|
|
166
|
-
One packaged diagnosis for interpreter, package surface, optional Playwright, and run-root configuration. Distinguishes `ok` / `degraded` / `broken` with repair actions.
|
|
177
|
+
One packaged diagnosis for interpreter, package surface, optional Playwright, and run-root configuration. Distinguishes `ok` / `degraded` / `broken` with repair actions. Those three states describe **install and runtime health** of what is present locally — they are not a public capability-maturity verdict; maturity vocabulary (`stable` / `experimental` / `blocked-by-gate` / `not-shipped`) stays with the `run-status` capability receipt, and doctor reads existing facts rather than adding a new health or capability-state authority.
|
|
167
178
|
|
|
168
179
|
**Bundled MCP (v0.3+):** Preview (`mcp/preview/`) and Evidence (`mcp/evidence/`) runtimes ship inside this package and are registered by `.mcp.json` (`${CLAUDE_PLUGIN_ROOT}`). Sibling monorepo dirs remain compatibility launchers/docs. The orchestrator still **probes** MCP `tools/list` and skips `preview*` / `observe*` when tools are absent. Evidence provider writes artifacts only — never the manifest. **`DESIGN_PLAYBOOK_RUN_ROOT`:** default `"."` in `.mcp.json` is the **MCP process cwd**, not the chat workspace — for a host-app dogfood, set an **absolute** path to `.scratch/<run>/` (see [`mcp/evidence/README.md`](mcp/evidence/README.md)). Capture responses include `written_path` (absolute) so mis-rooted writes are visible without a filesystem search.
|
|
169
180
|
|
package/codex/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- generated-by design-playbook v0.
|
|
1
|
+
<!-- generated-by design-playbook v0.22.0 -->
|
|
2
2
|
# design-playbook for Codex
|
|
3
3
|
|
|
4
4
|
## Install (path of record)
|
|
@@ -58,25 +58,23 @@ args = ["<abs>/packages/design-playbook/mcp/evidence/server.py"]
|
|
|
58
58
|
Verify: `codex mcp list` should list both. `preview*` needs a system Edge/Chrome (the
|
|
59
59
|
adapter spawns it via `--app=`); `observe*` needs Playwright + Chromium.
|
|
60
60
|
|
|
61
|
-
> **`preview*`
|
|
62
|
-
>
|
|
63
|
-
>
|
|
61
|
+
> **`preview*` skips when `preview_prototype` is absent; G5 is not triggered.**
|
|
62
|
+
> Follow the orchestrator's skip narration to report the reason and enable path.
|
|
63
|
+
> This is designed skip behaviour, not a crash. Confirm the tool is registered
|
|
64
64
|
> (`codex mcp list`) before treating it as a preview failure. Codex end-to-end preview
|
|
65
65
|
> smoke is not yet validated (v0.4.4 deferred the codex E2E smoke; only evidence/G6 was
|
|
66
66
|
> server-level smoked).
|
|
67
67
|
|
|
68
68
|
## Load order
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
Read `skills/design-playbook/SKILL.md` first. Its **Run profile** and **Steps**
|
|
71
|
+
sections are the sole authority for stage order, conditional entry, and completion
|
|
72
|
+
criteria. Consume the `run_profile.py route` result as directed there; this bridge
|
|
73
|
+
does not define a second route or a host-specific sequence.
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
Conditional entry `reference-intake?` (screenshot/URL/design/product analogy, ADR-0011) runs **before** `ux-spec?` when the router returns `requires_reference_contract` — fixed orchestrator order, not reorderable. Run `native-craft` only for an explicit native-desktop/native-feel target. Web and mobile Web skip `native-craft`; if the platform is unclear, ask before choosing the order. The orchestrator owns the decision gate, render-surface seam handoff, and fail-closed behavior.
|
|
78
|
-
|
|
79
|
-
Mirror the orchestrator's skip narration (SKILL.md Steps preamble): when a step is skipped, output one line — step name + reason + how to enable, with the gate label when one applies, e.g. `-> preview*: adapter absent, skipped (G5 not triggered; enable via packages/design-playbook/mcp/preview/ or host MCP)`.
|
|
75
|
+
For optional adapters, follow the orchestrator's **Steps** preamble and
|
|
76
|
+
`skills/design-playbook/references/load-map.md` pointers. Use its one-line skip
|
|
77
|
+
narration when a stage does not run, including the reason and enable path.
|
|
80
78
|
|
|
81
79
|
Audit preferences (ADR-0033) apply identically on Codex. Follow `skills/design-playbook/SKILL.md` § *Audit preferences* as sole authority; this bridge adds no host-specific preference rules.
|
|
82
80
|
|
package/commands/doctor.md
CHANGED
|
@@ -14,3 +14,5 @@ python <plugin>/scripts/doctor.py --repo-root <target-repo>
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
Reports `ok` / `degraded` / `broken`. Failed checks include a concrete repair. Optional adapters (Playwright, run-root env) degrade rather than hard-fail the install. Audit-preference state shows effective stage values, sources, asked status, and corrupt layers for target repository.
|
|
17
|
+
|
|
18
|
+
`ok` / `degraded` / `broken` describe install and runtime health of the local surface only — they are not a public capability-maturity verdict. Maturity vocabulary (`stable` / `experimental` / `blocked-by-gate` / `not-shipped`) stays with the `run-status` capability receipt; doctor reads existing facts and adds no new health or capability-state authority.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Build a static run handoff from an explicit run and its declared Fill
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# run-handoff
|
|
6
|
+
|
|
7
|
+
Build the existing static delivery package for one explicit Design I/O run. Not a pipeline obligation and not acceptance.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
python <plugin>/scripts/run_handoff.py <run> [--fill <declared-path>] [--round N] [--summary <text>] [--lang zh-CN|en] [--json]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- `<run>` is required. Do not discover a run from `.scratch/`.
|
|
16
|
+
- Read `fill:` declarations from that run's `plan.md` (unfenced column-0 lines). One declared Fill is used automatically; multiple require `--fill` with one of those declared paths; none or a missing/ineligible declaration fails with repair guidance.
|
|
17
|
+
- Do not scan the project for HTML, and do not use preview or reference assets as the reviewed Fill.
|
|
18
|
+
- The existing Evidence builder writes `evidence/static-handoff/` (delivery page, Fill copy, archive, disclosure, snapshots). Report `verdict`, `authority`, and `confirmationSource` verbatim, including `Pending`, `blocked`, `not-applicable`, and `unsubstantiated`.
|
|
19
|
+
- Do not write acceptance or convert a non-Pass state into Pass.
|
|
20
|
+
- Page language: explicit `--lang`, then `DPB_PREVIEW_LANG`, then `LANG`, otherwise Chinese. Unknown environment locales use the same Chinese default as Preview. Language affects UI labels only; machine fields, original diagnostics, and user-authored content remain verbatim.
|
|
21
|
+
|
|
22
|
+
## Done when
|
|
23
|
+
|
|
24
|
+
The user asked for a handoff; `index.html` exists under the selected run tree; and `verdict` / `authority` / `confirmationSource` were reported verbatim.
|
|
25
|
+
|
|
26
|
+
Scope:
|
|
27
|
+
$ARGUMENTS
|
package/commands/run-status.md
CHANGED
|
@@ -13,9 +13,11 @@ python <plugin>/scripts/run_status.py [.scratch/<run>] [--json] [--list] [--scra
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
- Omit the run path to pick the newest directory under `--scratch` (default `.scratch/`).
|
|
16
|
-
- `--json` emits machine-readable stages + `next` action.
|
|
16
|
+
- `--json` emits machine-readable stages + `next` action, plus the continuation block: selected run, current phase, blocker, run integrity, and the Console capability receipt (`implementation` / `validation` / `availability` / `publicClaim`).
|
|
17
17
|
- `--list` only lists discovered runs.
|
|
18
18
|
|
|
19
|
+
For an eligible run the continuation names an explicit `open-console` command for the local, experimental, trial-gated Run Console — no authorized external trial or public release is claimed until the separately authorized read-only trial gate passes (ADR-0043). `run-status` never starts a server, daemon, or background process itself; an ineligible run reports the blocking reason and a safe fallback instead.
|
|
20
|
+
|
|
19
21
|
## Done when
|
|
20
22
|
|
|
21
|
-
The command names completed stage markers, any active blocker (preview floor, baseline gate, recirculate verdict), and the single next valid resume action. It reuses `validate_run` judgments for G5 confirm validity rather than inventing a second state machine.
|
|
23
|
+
The command names completed stage markers, any active blocker (preview floor, baseline gate, recirculate verdict), and the single next valid resume action. It reuses `validate_run` judgments for G5 confirm validity rather than inventing a second state machine. Stale, partial, hash-mismatched, malformed, or inconsistent runs stay visible as those states and are never replaced by an older successful snapshot.
|
package/lib/index.js
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* `skills/` directory. The plugin locates the directory via `__dirname`,
|
|
9
9
|
* so no `!!js` expression and no cwd-dependent resolution is involved.
|
|
10
10
|
*
|
|
11
|
-
* 2.
|
|
12
|
-
* `run-review`, `run-status`, `ui-review`, `ux-spec` —
|
|
13
|
-
* matching `commands/<name>.md` prompt, substitute
|
|
14
|
-
* raw trailing input, and inject it as a
|
|
15
|
-
* `agent.followup()`.
|
|
11
|
+
* 2. Slash commands (ctx.commands) — `design-io`, `doctor`,
|
|
12
|
+
* `run-handoff`, `run-review`, `run-status`, `ui-review`, `ux-spec` —
|
|
13
|
+
* that load the matching `commands/<name>.md` prompt, substitute
|
|
14
|
+
* `$ARGUMENTS` with the raw trailing input, and inject it as a
|
|
15
|
+
* user-role follow-up turn via `agent.followup()`.
|
|
16
16
|
*
|
|
17
17
|
* The Cordis `!!js` evaluation scope provides no `require` (only Node globals
|
|
18
18
|
* plus ctx-provided values like dshHomePath/loader), so pointing a
|
|
@@ -81,7 +81,7 @@ exports.parseSkillFile = parseSkillFile
|
|
|
81
81
|
exports.createUserMessageFromPrompt = createUserMessageFromPrompt
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
84
|
+
* Slash commands this plugin registers. Each maps to a
|
|
85
85
|
* `commands/<name>.md` prompt file; the file's frontmatter `description`
|
|
86
86
|
* becomes the command's discovery metadata, and the body (with `$ARGUMENTS`
|
|
87
87
|
* substituted) is injected as a user follow-up turn.
|
|
@@ -89,6 +89,7 @@ exports.createUserMessageFromPrompt = createUserMessageFromPrompt
|
|
|
89
89
|
const COMMAND_NAMES = [
|
|
90
90
|
'design-io',
|
|
91
91
|
'doctor',
|
|
92
|
+
'run-handoff',
|
|
92
93
|
'run-review',
|
|
93
94
|
'run-status',
|
|
94
95
|
'ui-review',
|
package/mcp/evidence/handoff.py
CHANGED
|
@@ -10,6 +10,12 @@ ADR-0034 fixed three boundaries the first Stage 9 implementation crossed:
|
|
|
10
10
|
``confirm-round-*.json`` that ``transaction.py`` persists (ADR-0013), never
|
|
11
11
|
re-derived. The ADR-0008 floor lives inside that record; re-deriving it here
|
|
12
12
|
would be a second confirmation authority, which CONTEXT.md forbids.
|
|
13
|
+
- **Verdict authority** - ``point-back.md`` owns the verdict (continuation
|
|
14
|
+
pack decision 4): a delivery Pass additionally requires the point-back's
|
|
15
|
+
own canonical Pass verdict, read through the single verdict-syntax parser
|
|
16
|
+
(ADR-0025) shared with G3 and run status. A confirmed round with resolved
|
|
17
|
+
gates can never convert a Recirculate/blocked - or missing/ambiguous -
|
|
18
|
+
owner verdict into Pass (decision 25).
|
|
13
19
|
- **Capture target** - the five-viewport matrix and the layout probe run
|
|
14
20
|
against the deliverable itself, not against any review chrome.
|
|
15
21
|
|
|
@@ -33,6 +39,8 @@ from dataclasses import dataclass
|
|
|
33
39
|
from pathlib import Path
|
|
34
40
|
from typing import Any, Callable
|
|
35
41
|
|
|
42
|
+
from design_playbook.mcp.ui_locale import resolve_ui_locale
|
|
43
|
+
from design_playbook.mcp.evidence.handoff_i18n import STRINGS
|
|
36
44
|
from design_playbook.mcp.evidence.disclosure import (
|
|
37
45
|
VIEWPORT_ORDER,
|
|
38
46
|
ViewportMetrics,
|
|
@@ -118,6 +126,25 @@ def _confirmation_from_record(
|
|
|
118
126
|
return True, ""
|
|
119
127
|
|
|
120
128
|
|
|
129
|
+
def _point_back_verdict(run_root: Path) -> str | None:
|
|
130
|
+
"""The canonical point-back verdict, or ``None`` when there is not
|
|
131
|
+
exactly one.
|
|
132
|
+
|
|
133
|
+
point-back.md is the finding/verdict authority (continuation pack
|
|
134
|
+
decision 4). Reading goes through the single verdict-syntax parser
|
|
135
|
+
(ADR-0025) that G3 and run status already share - never a second
|
|
136
|
+
parser here. ``None`` (missing, malformed, ambiguous, or repeated
|
|
137
|
+
verdict text) is never a Pass.
|
|
138
|
+
"""
|
|
139
|
+
from design_playbook.scripts.verdict_syntax import parse_verdict
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
text = (run_root / "point-back.md").read_text(encoding="utf-8")
|
|
143
|
+
except (OSError, UnicodeError):
|
|
144
|
+
return None
|
|
145
|
+
return parse_verdict(text).canonical
|
|
146
|
+
|
|
147
|
+
|
|
121
148
|
def _run_gate_validation(run_root: Path) -> dict[str, Any]:
|
|
122
149
|
"""Run the canonical validator over one run directory."""
|
|
123
150
|
spec = run_root / "spec.md"
|
|
@@ -587,7 +614,7 @@ def _decision_records_from_run(
|
|
|
587
614
|
|
|
588
615
|
|
|
589
616
|
def _render_index_html(
|
|
590
|
-
payload: dict[str, Any], matrix: Any, snap_dir: Path
|
|
617
|
+
payload: dict[str, Any], matrix: Any, snap_dir: Path, locale: str
|
|
591
618
|
) -> str:
|
|
592
619
|
"""Render the delivery page with the payload inlined.
|
|
593
620
|
|
|
@@ -605,13 +632,22 @@ def _render_index_html(
|
|
|
605
632
|
.replace('"', """)
|
|
606
633
|
)
|
|
607
634
|
|
|
635
|
+
strings = STRINGS[locale]
|
|
636
|
+
|
|
637
|
+
def _display(value: Any) -> str:
|
|
638
|
+
if value is None:
|
|
639
|
+
return "—"
|
|
640
|
+
if isinstance(value, bool):
|
|
641
|
+
return strings["yes" if value else "no"]
|
|
642
|
+
return strings.get(str(value), str(value))
|
|
643
|
+
|
|
608
644
|
statuses = payload.get("gateStatuses")
|
|
609
645
|
rows = ""
|
|
610
646
|
if isinstance(statuses, list):
|
|
611
647
|
for index, state in enumerate(statuses[:8], start=1):
|
|
612
648
|
rows += (
|
|
613
649
|
f'<tr><td>G{index}</td><td class="st st-{_esc(state)}">'
|
|
614
|
-
f"{_esc(state)}</td></tr>"
|
|
650
|
+
f"{_esc(_display(state))}</td></tr>"
|
|
615
651
|
)
|
|
616
652
|
viewports = payload.get("viewports")
|
|
617
653
|
vp_rows = ""
|
|
@@ -626,9 +662,9 @@ def _render_index_html(
|
|
|
626
662
|
f"<td>{_esc(vp.get('name'))}</td>"
|
|
627
663
|
f"<td>{_esc(metrics.get('sw'))}</td>"
|
|
628
664
|
f"<td>{_esc(metrics.get('innerH'))}</td>"
|
|
629
|
-
f"<td>{_esc(metrics.get('hOverflow'))}</td>"
|
|
630
|
-
f"<td>{_esc(disclosure.get('inFold'))}</td>"
|
|
631
|
-
f"<td>{_esc(metrics.get('measurementStatus'))}</td>"
|
|
665
|
+
f"<td>{_esc(_display(metrics.get('hOverflow')))}</td>"
|
|
666
|
+
f"<td>{_esc(_display(disclosure.get('inFold')))}</td>"
|
|
667
|
+
f"<td>{_esc(_display(metrics.get('measurementStatus')))}</td>"
|
|
632
668
|
"</tr>"
|
|
633
669
|
)
|
|
634
670
|
decisions = payload.get("decisions")
|
|
@@ -639,7 +675,7 @@ def _render_index_html(
|
|
|
639
675
|
decision_items += (
|
|
640
676
|
f"<li><code>{_esc(item.get('id'))}</code> "
|
|
641
677
|
f"{_esc(item.get('title'))} "
|
|
642
|
-
f"<small>({_esc(item.get('authority'))})</small></li>"
|
|
678
|
+
f"<small>({_esc(_display(item.get('authority')))})</small></li>"
|
|
643
679
|
)
|
|
644
680
|
payload_json = json.dumps(payload, ensure_ascii=False, indent=2)
|
|
645
681
|
# A literal "</script>" inside the payload would close the block early;
|
|
@@ -652,27 +688,44 @@ def _render_index_html(
|
|
|
652
688
|
matrix.get(vp), vp, snap_dir
|
|
653
689
|
):
|
|
654
690
|
snap_figures += (
|
|
655
|
-
f'<figure><img src="snapshots/viewport-{vp}.png"
|
|
691
|
+
f'<figure><img src="snapshots/viewport-{vp}.png" '
|
|
692
|
+
f'alt="{_esc(strings["snapshot"].format(viewport=vp))}" '
|
|
656
693
|
f'loading="lazy"/><figcaption>viewport-{vp}.png</figcaption></figure>'
|
|
657
694
|
)
|
|
658
695
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
.
|
|
675
|
-
|
|
696
|
+
note_key = "confirmed_note" if payload.get("authority") == "confirmed-user" else "pending_note"
|
|
697
|
+
note = _esc(strings[note_key])
|
|
698
|
+
if payload.get("confirmationNote"):
|
|
699
|
+
# Runtime diagnostics remain verbatim, distinct from localized UI copy.
|
|
700
|
+
note += (
|
|
701
|
+
f'<details><summary>{_esc(strings["diagnostic"])}</summary>'
|
|
702
|
+
f'<pre>{_esc(payload["confirmationNote"])}</pre></details>'
|
|
703
|
+
)
|
|
704
|
+
values = {key.upper(): _esc(text) for key, text in strings.items()}
|
|
705
|
+
values.update({
|
|
706
|
+
"LANG": locale,
|
|
707
|
+
"VERDICT_LABEL": strings["verdict"],
|
|
708
|
+
"AUTHORITY_LABEL": strings["authority"],
|
|
709
|
+
"PROFILE_LABEL": strings["profile"],
|
|
710
|
+
"GATES_PASSED_LABEL": strings["gates_passed"],
|
|
711
|
+
"VERDICT": _esc(_display(payload.get("verdict"))),
|
|
712
|
+
"VERDICT_LOWERCASE": _esc(str(payload.get("verdict")).lower()),
|
|
713
|
+
"RUN_ID": _esc(payload.get("runId")),
|
|
714
|
+
"AUTHORITY": _esc(_display(payload.get("authority"))),
|
|
715
|
+
"PROFILE": _esc(_display(payload.get("profile"))),
|
|
716
|
+
"TIMESTAMP": _esc(payload.get("timestamp")),
|
|
717
|
+
"GATES_PASSED": _esc(payload.get("gatesPassed")),
|
|
718
|
+
"CAPTURE_STATUS": _esc(_display(payload.get("captureStatus"))),
|
|
719
|
+
"CONFIRMATION_NOTE": note,
|
|
720
|
+
"GATE_ROWS": rows,
|
|
721
|
+
"VIEWPORT_ROWS": vp_rows,
|
|
722
|
+
"DECISION_ITEMS": decision_items,
|
|
723
|
+
"SNAPSHOT_FIGURES": snap_figures,
|
|
724
|
+
"PAYLOAD_JSON": payload_json,
|
|
725
|
+
"PAYLOAD_TEXT": _esc(json.dumps(payload, ensure_ascii=False, indent=2)),
|
|
726
|
+
})
|
|
727
|
+
# One pass: user-authored text resembling a template marker stays literal.
|
|
728
|
+
return re.sub(r"%([A-Z_]+)%", lambda match: values[match[1]], _page_template())
|
|
676
729
|
|
|
677
730
|
|
|
678
731
|
def build_static_handoff(
|
|
@@ -684,6 +737,7 @@ def build_static_handoff(
|
|
|
684
737
|
capture_runner: Callable[..., Any] | None = None,
|
|
685
738
|
gate_runner: Callable[..., Any] | None = None,
|
|
686
739
|
out_dir: Path | None = None,
|
|
740
|
+
locale: str | None = None,
|
|
687
741
|
) -> StaticHandoffResult:
|
|
688
742
|
"""Build the Stage 9 static handoff from durable run artifacts.
|
|
689
743
|
|
|
@@ -695,6 +749,7 @@ def build_static_handoff(
|
|
|
695
749
|
the ZIP package, a same-directory ``deliverable.html`` copy (the page's
|
|
696
750
|
relative link target, spec A5), and a self-contained index page.
|
|
697
751
|
"""
|
|
752
|
+
locale = resolve_ui_locale(locale)
|
|
698
753
|
run_root = Path(run_root)
|
|
699
754
|
deliverable = Path(deliverable)
|
|
700
755
|
out_dir = Path(out_dir) if out_dir is not None else run_root / "evidence" / "static-handoff"
|
|
@@ -772,7 +827,19 @@ def build_static_handoff(
|
|
|
772
827
|
confirmed, confirmation_reason = _confirmation_from_record(run_root, round_n)
|
|
773
828
|
|
|
774
829
|
capture_complete = capture_status == "captured"
|
|
775
|
-
|
|
830
|
+
# The point-back owner verdict gates a delivery Pass (decision 25): a
|
|
831
|
+
# confirmed round with resolved gates still cannot convert a
|
|
832
|
+
# Recirculate/blocked owner verdict - or a missing/ambiguous one - into
|
|
833
|
+
# Pass. Pending/Recirculate behaviour and gatesPassed semantics are
|
|
834
|
+
# unchanged.
|
|
835
|
+
owner_pass = _point_back_verdict(run_root) == "pass"
|
|
836
|
+
if (
|
|
837
|
+
confirmed
|
|
838
|
+
and owner_pass
|
|
839
|
+
and capture_complete
|
|
840
|
+
and gate.get("available")
|
|
841
|
+
and gates_resolved
|
|
842
|
+
):
|
|
776
843
|
verdict = "Pass"
|
|
777
844
|
elif confirmed:
|
|
778
845
|
verdict = "Recirculate"
|
|
@@ -842,7 +909,7 @@ def build_static_handoff(
|
|
|
842
909
|
|
|
843
910
|
index_html = out_dir / "index.html"
|
|
844
911
|
index_html.write_text(
|
|
845
|
-
_render_index_html(payload, matrix, snap_dir), encoding="utf-8"
|
|
912
|
+
_render_index_html(payload, matrix, snap_dir, locale), encoding="utf-8"
|
|
846
913
|
)
|
|
847
914
|
|
|
848
915
|
return StaticHandoffResult(
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Static delivery UI copy; protocol fields and author text are never translated."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from design_playbook.mcp.ui_locale import EN, ZH
|
|
5
|
+
|
|
6
|
+
STRINGS: dict[str, dict[str, str]] = {
|
|
7
|
+
ZH: {
|
|
8
|
+
"title": "静态交付",
|
|
9
|
+
"credential": "交付凭据",
|
|
10
|
+
"run": "运行",
|
|
11
|
+
"generated": "生成时间",
|
|
12
|
+
"verdict": "验收结论",
|
|
13
|
+
"authority": "决策来源",
|
|
14
|
+
"profile": "运行档位",
|
|
15
|
+
"gates_passed": "已通过的门禁",
|
|
16
|
+
"capture": "证据采集",
|
|
17
|
+
"gate_matrix": "门禁矩阵",
|
|
18
|
+
"gate": "门禁",
|
|
19
|
+
"status": "状态",
|
|
20
|
+
"viewports": "视口披露",
|
|
21
|
+
"viewport": "视口",
|
|
22
|
+
"width": "页面宽度(px)",
|
|
23
|
+
"height": "视口高度(px)",
|
|
24
|
+
"overflow": "横向溢出",
|
|
25
|
+
"in_fold": "首屏内可见",
|
|
26
|
+
"measurement": "测量状态",
|
|
27
|
+
"decisions": "决策记录",
|
|
28
|
+
"artifacts": "交付文件",
|
|
29
|
+
"artifacts_note": "此页是交付摘要。下列文件均保存在同一目录,可离线打开。",
|
|
30
|
+
"download": "下载 ZIP(凭据、截图与代码)",
|
|
31
|
+
"disclosure_link": "查看披露 JSON",
|
|
32
|
+
"deliverable_link": "打开交付页面",
|
|
33
|
+
"payload": "完整披露数据(机器可读,字段保留原文)",
|
|
34
|
+
"snapshot": "{viewport} 视口截图",
|
|
35
|
+
"confirmed_note": "用户确认已记录;最终结论同时取决于验收结果与证据。",
|
|
36
|
+
"pending_note": "尚无本轮有效的用户确认,不能据此认定交付通过。",
|
|
37
|
+
"diagnostic": "确认详情(原始诊断)",
|
|
38
|
+
"Pass": "通过",
|
|
39
|
+
"Recirculate": "需返工",
|
|
40
|
+
"Pending": "待确认",
|
|
41
|
+
"pass": "通过",
|
|
42
|
+
"fail": "未通过",
|
|
43
|
+
"pending": "待检查",
|
|
44
|
+
"not-applicable": "不适用",
|
|
45
|
+
"captured": "已采集",
|
|
46
|
+
"blocked": "受阻",
|
|
47
|
+
"measured": "已测量",
|
|
48
|
+
"unmeasured": "未测量",
|
|
49
|
+
"confirmed-user": "用户已确认",
|
|
50
|
+
"pending-user": "等待用户确认",
|
|
51
|
+
"unknown": "未知",
|
|
52
|
+
"yes": "是",
|
|
53
|
+
"no": "否",
|
|
54
|
+
},
|
|
55
|
+
EN: {
|
|
56
|
+
"title": "Static Handoff",
|
|
57
|
+
"credential": "Delivery credential",
|
|
58
|
+
"run": "run",
|
|
59
|
+
"generated": "generated",
|
|
60
|
+
"verdict": "Verdict",
|
|
61
|
+
"authority": "Decision authority",
|
|
62
|
+
"profile": "Run profile",
|
|
63
|
+
"gates_passed": "Gates passed",
|
|
64
|
+
"capture": "Capture",
|
|
65
|
+
"gate_matrix": "Gate matrix",
|
|
66
|
+
"gate": "Gate",
|
|
67
|
+
"status": "Status",
|
|
68
|
+
"viewports": "Viewport disclosure",
|
|
69
|
+
"viewport": "Viewport",
|
|
70
|
+
"width": "Page width (px)",
|
|
71
|
+
"height": "Viewport height (px)",
|
|
72
|
+
"overflow": "Horizontal overflow",
|
|
73
|
+
"in_fold": "Visible above fold",
|
|
74
|
+
"measurement": "Measurement",
|
|
75
|
+
"decisions": "Decisions",
|
|
76
|
+
"artifacts": "Artifacts",
|
|
77
|
+
"artifacts_note": "This is the delivery summary. The files below are in the same directory and work offline.",
|
|
78
|
+
"download": "Download ZIP (credential + snapshots + code)",
|
|
79
|
+
"disclosure_link": "View disclosure JSON",
|
|
80
|
+
"deliverable_link": "Open deliverable",
|
|
81
|
+
"payload": "Full disclosure payload (machine-readable, original fields)",
|
|
82
|
+
"snapshot": "{viewport} viewport snapshot",
|
|
83
|
+
"confirmed_note": "User confirmation is recorded; the final verdict also depends on acceptance results and evidence.",
|
|
84
|
+
"pending_note": "No valid user confirmation exists for this round. This does not establish a delivery Pass.",
|
|
85
|
+
"diagnostic": "Confirmation details (raw diagnostic)",
|
|
86
|
+
"Pass": "Pass",
|
|
87
|
+
"Recirculate": "Recirculate",
|
|
88
|
+
"Pending": "Pending",
|
|
89
|
+
"pass": "Pass",
|
|
90
|
+
"fail": "Fail",
|
|
91
|
+
"pending": "Pending",
|
|
92
|
+
"not-applicable": "Not applicable",
|
|
93
|
+
"captured": "Captured",
|
|
94
|
+
"blocked": "Blocked",
|
|
95
|
+
"measured": "Measured",
|
|
96
|
+
"unmeasured": "Unmeasured",
|
|
97
|
+
"confirmed-user": "Confirmed by user",
|
|
98
|
+
"pending-user": "Awaiting user confirmation",
|
|
99
|
+
"unknown": "Unknown",
|
|
100
|
+
"yes": "Yes",
|
|
101
|
+
"no": "No",
|
|
102
|
+
},
|
|
103
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="%LANG%">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8"/>
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
6
|
-
<title
|
|
6
|
+
<title>%TITLE% - Design Playbook</title>
|
|
7
7
|
<style>
|
|
8
8
|
:root{
|
|
9
9
|
--primary:#2563EB; --bg:#F8FAFC; --surface:#FFFFFF; --border:#E2E8F0;
|
|
@@ -62,52 +62,55 @@
|
|
|
62
62
|
<body>
|
|
63
63
|
<div class="wrap">
|
|
64
64
|
<header>
|
|
65
|
-
<h1
|
|
66
|
-
<div class="sub"
|
|
65
|
+
<h1>%TITLE%</h1>
|
|
66
|
+
<div class="sub">%CREDENTIAL% · %RUN% <code>%RUN_ID%</code> · %GENERATED% %TIMESTAMP%</div>
|
|
67
67
|
</header>
|
|
68
68
|
|
|
69
69
|
<div class="grid">
|
|
70
|
-
<div class="card"><div class="k"
|
|
70
|
+
<div class="card"><div class="k">%VERDICT_LABEL%</div>
|
|
71
71
|
<div class="v %VERDICT_LOWERCASE%">%VERDICT%</div></div>
|
|
72
|
-
<div class="card"><div class="k"
|
|
72
|
+
<div class="card"><div class="k">%AUTHORITY_LABEL%</div>
|
|
73
73
|
<div class="v">%AUTHORITY%</div></div>
|
|
74
|
-
<div class="card"><div class="k"
|
|
74
|
+
<div class="card"><div class="k">%PROFILE_LABEL%</div>
|
|
75
75
|
<div class="v">%PROFILE%</div></div>
|
|
76
|
-
<div class="card"><div class="k"
|
|
76
|
+
<div class="card"><div class="k">%GATES_PASSED_LABEL%</div>
|
|
77
77
|
<div class="v">%GATES_PASSED% / 8</div></div>
|
|
78
|
-
<div class="card"><div class="k"
|
|
78
|
+
<div class="card"><div class="k">%CAPTURE%</div>
|
|
79
79
|
<div class="v">%CAPTURE_STATUS%</div></div>
|
|
80
80
|
</div>
|
|
81
81
|
<div class="note">%CONFIRMATION_NOTE%</div>
|
|
82
82
|
|
|
83
|
-
<h2
|
|
83
|
+
<h2>%GATE_MATRIX%</h2>
|
|
84
84
|
<table>
|
|
85
|
-
<thead><tr><th
|
|
85
|
+
<thead><tr><th>%GATE%</th><th>%STATUS%</th></tr></thead>
|
|
86
86
|
<tbody>%GATE_ROWS%</tbody>
|
|
87
87
|
</table>
|
|
88
88
|
|
|
89
|
-
<h2
|
|
89
|
+
<h2>%VIEWPORTS%</h2>
|
|
90
|
+
<div style="overflow-x:auto">
|
|
90
91
|
<table>
|
|
91
|
-
<thead><tr><th
|
|
92
|
-
<th
|
|
92
|
+
<thead><tr><th>%VIEWPORT%</th><th>%WIDTH%</th><th>%HEIGHT%</th><th>%OVERFLOW%</th>
|
|
93
|
+
<th>%IN_FOLD%</th><th>%MEASUREMENT%</th></tr></thead>
|
|
93
94
|
<tbody>%VIEWPORT_ROWS%</tbody>
|
|
94
95
|
</table>
|
|
96
|
+
</div>
|
|
95
97
|
|
|
96
|
-
<h2
|
|
98
|
+
<h2>%DECISIONS%</h2>
|
|
97
99
|
<ul class="decisions">%DECISION_ITEMS%</ul>
|
|
98
100
|
|
|
99
|
-
<h2
|
|
100
|
-
<p
|
|
101
|
+
<h2>%ARTIFACTS%</h2>
|
|
102
|
+
<p>%ARTIFACTS_NOTE%</p>
|
|
101
103
|
<div class="dl">
|
|
102
|
-
<a href="static-handoff.zip"
|
|
103
|
-
<a class="secondary" href="disclosure-review.json"
|
|
104
|
-
<a class="secondary" href="deliverable.html"
|
|
104
|
+
<a href="static-handoff.zip">%DOWNLOAD%</a>
|
|
105
|
+
<a class="secondary" href="disclosure-review.json">%DISCLOSURE_LINK%</a>
|
|
106
|
+
<a class="secondary" href="deliverable.html">%DELIVERABLE_LINK%</a>
|
|
105
107
|
</div>
|
|
106
108
|
<div class="snaps">%SNAPSHOT_FIGURES%</div>
|
|
107
109
|
|
|
108
110
|
<details>
|
|
109
|
-
<summary
|
|
110
|
-
<pre
|
|
111
|
+
<summary>%PAYLOAD%</summary>
|
|
112
|
+
<pre>%PAYLOAD_TEXT%</pre>
|
|
113
|
+
<script type="application/json">%PAYLOAD_JSON%</script>
|
|
111
114
|
</details>
|
|
112
115
|
</div>
|
|
113
116
|
</body>
|