design-playbook 0.21.0 → 0.21.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.
- package/README.md +11 -0
- package/codex/AGENTS.md +1 -0
- package/commands/run-review.md +1 -1
- package/commands/ui-review.md +1 -1
- package/examples/dogfood/run/craft-guard.md +9 -0
- package/examples/export-entry/run/craft-guard.md +9 -0
- package/examples/export-pointfix-upgrade/run/craft-guard.md +9 -0
- package/examples/export-upgrade/run/craft-guard.md +9 -0
- package/lib/cli.js +46 -0
- package/mcp/evidence/README.md +2 -2
- package/mcp/evidence/capture_runtime.py +9 -9
- package/mcp/evidence/disclosure.py +4 -2
- package/mcp/evidence/handoff.py +20 -3
- package/mcp/evidence/static_handoff_page.html +1 -1
- package/mcp/evidence/test_handoff.py +65 -1
- package/mcp/preview/control.css +204 -35
- package/mcp/preview/control.html +28 -1
- package/mcp/preview/control.js +367 -24
- package/mcp/preview/control.py +92 -1
- package/mcp/preview/control.review.js +37 -7
- package/mcp/preview/i18n.py +27 -6
- package/mcp/preview/integrity.py +27 -19
- package/mcp/preview/pin_bridge.py +129 -4
- package/mcp/preview/review_session.py +51 -4
- package/mcp/preview/test_browser_control.py +450 -0
- package/mcp/preview/test_i18n_labels.py +20 -2
- package/mcp/preview/test_integrity.py +29 -0
- package/mcp/preview/test_transaction.py +103 -9
- package/mcp/preview/transaction.py +71 -4
- package/mcp/run_console/actions.py +31 -4
- package/mcp/run_console/http_server.py +10 -0
- package/mcp/run_console/projection.py +35 -5
- package/mcp/run_console/snapshot_builder.py +8 -1
- package/mcp/run_console/test_actions.py +52 -9
- package/mcp/run_console/test_parity.py +84 -0
- package/mcp/run_console/test_snapshot_builder.py +118 -0
- package/package.json +4 -1
- package/scripts/adapter_matrix.py +199 -0
- package/scripts/adapter_templates/codex-agents.md +86 -0
- package/scripts/adapter_templates/cursor-mcp-note.mdc +27 -0
- package/scripts/adapter_templates/windsurf-mcp-guide.md +45 -0
- package/scripts/generate_adapter.py +787 -0
- package/scripts/rules_registry.py +6 -3
- package/skills/craft-guard/SKILL.md +1 -1
- package/skills/craft-guard/references/craft.md +20 -18
- package/skills/design-playbook/SKILL.md +8 -7
- package/skills/design-playbook/references/rules.md +211 -0
- package/skills/reference-intake/SKILL.md +1 -1
- package/skills/ui-evaluator/SKILL.md +6 -4
- package/skills/ui-evaluator/references/rubric.md +22 -22
- package/skills/ui-picker/SKILL.md +3 -1
- package/skills/ui-picker/references/components.md +13 -13
- package/skills/ui-picker/references/design.md +14 -14
- package/skills/ui-picker/references/domain.md +14 -14
- package/skills/ui-picker/references/template.md +16 -16
- package/skills/ux-spec/SKILL.md +3 -3
- package/skills/ux-spec/references/spec-template.md +29 -29
package/README.md
CHANGED
|
@@ -101,6 +101,17 @@ pi install npm:pi-mcp-adapter
|
|
|
101
101
|
|
|
102
102
|
`<pkg>` is the installed package root — `~/.pi/agent/npm/node_modules/design-playbook` for a user install, `.pi/npm/node_modules/design-playbook` for a project install. Evidence also needs `pip install playwright && playwright install chromium`.
|
|
103
103
|
|
|
104
|
+
## Install on other agents
|
|
105
|
+
|
|
106
|
+
Generate adapter files for Cursor, Gemini CLI, OpenCode, Windsurf, GitHub Copilot, or any of 22 Tier-3 agents:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npx design-playbook init <agent> # e.g. cursor, gemini-cli, windsurf
|
|
110
|
+
npx design-playbook --list # all 29 agents, shows which have renderers
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
See the root [README](../../README.md#install-on-other-agents) for the tier table and capability notes.
|
|
114
|
+
|
|
104
115
|
## Stack with other skills
|
|
105
116
|
|
|
106
117
|
| Package | Use for |
|
package/codex/AGENTS.md
CHANGED
package/commands/run-review.md
CHANGED
|
@@ -20,7 +20,7 @@ Scan user-side `.scratch/<run>/` (not monorepo `dogfood/*` globs). **Include** o
|
|
|
20
20
|
Ledger row shape: **ui-evaluator** step 2 (do not restate).
|
|
21
21
|
|
|
22
22
|
```
|
|
23
|
-
|
|
23
|
+
Prohibited:
|
|
24
24
|
- no new run ledger
|
|
25
25
|
- no prose lessons / narrative "learning"
|
|
26
26
|
- no auto-writeback to baseline
|
package/commands/ui-review.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Dual-track UI review emitting the six-block point-back report (ledger / findings / positives / coverage / limitations / verdict)
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
Run skill **ui-evaluator** (pull craft-guard checks when AI slop/motion/loading is in scope). Output issue/source/fix/severity; blocking first.
|
|
5
|
+
Run skill **ui-evaluator** (pull craft-guard checks when AI slop/motion/loading is in scope). Output issue/source/fix/severity/track; blocking first.
|
|
6
6
|
|
|
7
7
|
Scope:
|
|
8
8
|
$ARGUMENTS
|
|
@@ -17,3 +17,12 @@ Registry: `skills/design-playbook/references/rules.md`, full catalog (P3 run:
|
|
|
17
17
|
| I18N-01@1 | not-applicable | 单语控制台(zh-CN),无 i18n 声明(无 i18n.* 契约字段,L1 未声明多语言用户群) | - | - | - | 单语声明成立 | - |
|
|
18
18
|
| PERF-01@1 | applicable | - | clear | 长运行有持续进度感(feed 条目级进度逐 tick 更新;全局暂停 busy 即时反馈) | evidence/L6.2-pause-trace.json 反馈序列 | 反馈相称性未承诺耗时阈值(契约无阈值声明) | - |
|
|
19
19
|
| SEC-01@1 | not-applicable | 声明范围无敏感操作新增(敏感模拟参数默认脱敏沿用;全局暂停非敏感操作) | - | - | - | 无敏感面可查 | - |
|
|
20
|
+
| COPY-01@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档(2026-08-14);主动语态与动作命名一致性审查所需的全流程文案清单未采集 | - | - | - | - | - |
|
|
21
|
+
| COPY-02@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;用户侧命名审查所需的界面名词与实现命名对照未采集 | - | - | - | - | - |
|
|
22
|
+
| COPY-03@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;错误信息语气审查所需的错误态文案样本未采集 | - | - | - | - | - |
|
|
23
|
+
| A11Y-02@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;可见键盘焦点判定所需的聚焦态截图与键盘走查未采集(a11y 树无法证明视觉属性) | - | - | - | - | - |
|
|
24
|
+
| CRAFT-09@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;归档缺审计面样式源——filled-ui.md 为声明性产物索引不含样式源,candidates/preview 为一次性原型资产而非填充面源码 | - | - | - | - | - |
|
|
25
|
+
| CRAFT-10@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;归档缺审计面源标记与结构装置的视觉捕捉——evidence 仅交互轨迹与 a11y 树 JSON,无填充面标记源归档 | - | - | - | - | - |
|
|
26
|
+
| DECIDE-01@1 | applicable | - | clear | 选中方向为全局 run console 构成重组(candidates/console-region.html 草图 + preview round 1/2 用户确认),非未审视的默认外观收敛 | DD-0001 理由可回溯 l1.scenes(切页续读)与 PERF-01 比较轴及经用户确认的布局段突破;DD-0002 理由可回溯 l6.c4(跨视图状态闭环)与运行中心第一步方向——均引用 brief 具体事实 | 常规方向经比较矩阵沿 brief 轴证成,非未审视默认;无基线默认方向身份声明 | - |
|
|
27
|
+
|
|
28
|
+
注:2026-08-28 注册批(COPY-01/02/03、A11Y-02、CRAFT-09/10、DECIDE-01)晚于本 run 存档;按三态谓词补记——blocked 行在理由列点名缺失的证据面;DECIDE-01 依归档内可读的决策报告求值为 applicable。
|
|
@@ -17,3 +17,12 @@ Registry: `skills/design-playbook/references/rules.md`, full catalog (P2 run). S
|
|
|
17
17
|
| I18N-01@1 | not-applicable | 单语控制台,无 i18n 声明(无 i18n.* 契约字段,L1 未声明多语言用户群) | - | - | - | 单语声明成立 | - |
|
|
18
18
|
| PERF-01@1 | blocked | 性能感知需运行时度量,本 run provider 缺度量面(measurement 层不可采) | - | 导出等待仅观察到 busy 态 | 度量面缺席,无法判定反馈与耗时的相称性 | 无法在不承诺阈值的情况下检查例外 | 补采运行时度量后重评;缺口的证据语义见 point-back 覆盖声明 |
|
|
19
19
|
| SEC-01@1 | not-applicable | 声明范围无敏感操作新增(导出非敏感数据;隐藏敏感列由 column_scope 假设排除) | - | - | - | 无敏感面可查 | - |
|
|
20
|
+
| COPY-01@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档(2026-08-14);主动语态与动作命名一致性审查所需的全流程文案清单未采集 | - | - | - | - | - |
|
|
21
|
+
| COPY-02@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;用户侧命名审查所需的界面名词与实现命名对照未采集 | - | - | - | - | - |
|
|
22
|
+
| COPY-03@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;错误信息语气审查所需的错误态文案样本未采集 | - | - | - | - | - |
|
|
23
|
+
| A11Y-02@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;可见键盘焦点判定所需的聚焦态截图与键盘走查未采集(a11y 树无法证明视觉属性) | - | - | - | - | - |
|
|
24
|
+
| CRAFT-09@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;归档缺审计面样式源——filled-ui.md 为静态替身描述,无填充面样式源文件归档 | - | - | - | - | - |
|
|
25
|
+
| CRAFT-10@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;归档缺审计面源标记——filled-ui.md 为静态替身描述;既有截图仅覆盖错误态,无结构装置的视觉捕捉 | - | - | - | - | - |
|
|
26
|
+
| DECIDE-01@1 | applicable | - | clear | 选中方向为按周命名模式(compare 档轻量比较),不落入 2026-08 记录的自默认外观样貌;DD-0001 为 record 档,不在本规则范围 | DD-0002 理由可回溯 l1.target_user 比较轴(周频归档场景下检索是主任务)——引用 brief 具体事实而非通用措辞 | 常规方向经比较矩阵沿 brief 轴证成,非未审视默认;无基线默认方向身份声明 | - |
|
|
27
|
+
|
|
28
|
+
注:2026-08-28 注册批(COPY-01/02/03、A11Y-02、CRAFT-09/10、DECIDE-01)晚于本 run 存档;按三态谓词补记——blocked 行在理由列点名缺失的证据面;DECIDE-01 依归档内可读的决策报告求值为 applicable。
|
|
@@ -17,3 +17,12 @@ Registry: `skills/design-playbook/references/rules.md`. Seven-column rows; after
|
|
|
17
17
|
| I18N-01@1 | not-applicable | 单语控制台,无 i18n 声明(无 i18n.* 契约字段,L1 未声明多语言用户群) | - | - | - | 单语声明成立 | - |
|
|
18
18
|
| PERF-01@1 | blocked | 性能感知需运行时度量,本 run provider 缺度量面(measurement 层不可采) | - | 导出等待仅观察到 busy 态 | 度量面缺席,无法判定反馈与耗时的相称性 | 无法在不承诺阈值的情况下检查例外 | 补采运行时度量后重评;缺口的证据语义见 point-back 覆盖声明 |
|
|
19
19
|
| SEC-01@1 | not-applicable | 声明范围无敏感操作新增(导出非敏感数据;隐藏敏感列由 column_scope 假设排除) | - | - | - | 无敏感面可查 | - |
|
|
20
|
+
| COPY-01@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档(2026-08-14);主动语态与动作命名一致性审查所需的全流程文案清单未采集 | - | - | - | - | - |
|
|
21
|
+
| COPY-02@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;用户侧命名审查所需的界面名词与实现命名对照未采集 | - | - | - | - | - |
|
|
22
|
+
| COPY-03@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;错误信息语气审查所需的错误态文案样本未采集 | - | - | - | - | - |
|
|
23
|
+
| A11Y-02@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;可见键盘焦点判定所需的聚焦态截图与键盘走查未采集(a11y 树无法证明视觉属性) | - | - | - | - | - |
|
|
24
|
+
| CRAFT-09@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;选择器优先级冲突审查所需的样式源走查未执行 | - | - | - | - | - |
|
|
25
|
+
| CRAFT-10@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;结构装置与内容属性对应关系的走查未执行 | - | - | - | - | - |
|
|
26
|
+
| DECIDE-01@1 | not-applicable | 决策报告仅含 record 档 DD-0101,无 compare/explore 档方向决策条目 | - | - | - | - | - |
|
|
27
|
+
|
|
28
|
+
注:2026-08-28 注册批(COPY-01/02/03、A11Y-02、CRAFT-09/10、DECIDE-01)晚于本 run 存档;按三态谓词补记,blocked 行在理由列记缺失证据面。
|
|
@@ -17,3 +17,12 @@ Registry: `skills/design-playbook/references/rules.md`, full catalog (P3 run:
|
|
|
17
17
|
| I18N-01@1 | not-applicable | 单语控制台,无 i18n 声明(无 i18n.* 契约字段,L1 未声明多语言用户群) | - | - | - | 单语声明成立 | - |
|
|
18
18
|
| PERF-01@1 | applicable | - | clear | 长导出有持续进度感(30s 窗口 5 次采样,条目级进度持续更新) | evidence/L6.1-status-trace.json 进度采样序列 | 反馈相称性未承诺耗时阈值(契约无阈值声明) | - |
|
|
19
19
|
| SEC-01@1 | not-applicable | 声明范围无敏感操作新增(导出非敏感数据;隐藏敏感列由 column_scope 假设排除) | - | - | - | 无敏感面可查 | - |
|
|
20
|
+
| COPY-01@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档(2026-08-14);主动语态与动作命名一致性审查所需的全流程文案清单未采集 | - | - | - | - | - |
|
|
21
|
+
| COPY-02@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;用户侧命名审查所需的界面名词与实现命名对照未采集 | - | - | - | - | - |
|
|
22
|
+
| COPY-03@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;错误信息语气审查所需的错误态文案样本未采集 | - | - | - | - | - |
|
|
23
|
+
| A11Y-02@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;可见键盘焦点判定所需的聚焦态截图与键盘走查未采集(a11y 树无法证明视觉属性) | - | - | - | - | - |
|
|
24
|
+
| CRAFT-09@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;归档缺审计面样式源——无填充产物源文件归档,candidates/preview 为一次性原型资产而非填充面源码 | - | - | - | - | - |
|
|
25
|
+
| CRAFT-10@1 | blocked | 条目 2026-08-28 注册,晚于本 run 存档;归档缺审计面源标记与结构装置的视觉捕捉——evidence 仅交互轨迹 JSON,无填充产物源文件归档 | - | - | - | - | - |
|
|
26
|
+
| DECIDE-01@1 | applicable | - | clear | 选中方向为启用既有 status region 收纳导出任务(candidates/B.html 草图 + preview round 1/2 用户确认),非未审视的默认外观收敛 | DD-0003 理由可回溯 l1.scenes(导出中切页全局可查)与 PERF-01 比较轴;DD-0004 理由可回溯 l6.c2(跨视图状态闭环)与基线 status region 惯例声明——均引用 brief 具体事实 | 常规方向经比较矩阵沿 brief 轴证成,非未审视默认;基线声明的是 status region 惯例而非默认外观身份 | - |
|
|
27
|
+
|
|
28
|
+
注:2026-08-28 注册批(COPY-01/02/03、A11Y-02、CRAFT-09/10、DECIDE-01)晚于本 run 存档;按三态谓词补记——blocked 行在理由列点名缺失的证据面;DECIDE-01 依归档内可读的决策报告求值为 applicable。
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
/**
|
|
4
|
+
* design-playbook CLI shim (ADR-0042).
|
|
5
|
+
*
|
|
6
|
+
* Locates python3/python and delegates to
|
|
7
|
+
* packages/design-playbook/scripts/generate_adapter.py with passthrough args.
|
|
8
|
+
*
|
|
9
|
+
* npx design-playbook --list
|
|
10
|
+
* npx design-playbook init <agent>
|
|
11
|
+
* npx design-playbook <agent> --dry-run
|
|
12
|
+
*/
|
|
13
|
+
const { execFileSync } = require('node:child_process')
|
|
14
|
+
const path = require('node:path')
|
|
15
|
+
|
|
16
|
+
const SCRIPT = path.join(__dirname, '..', 'scripts', 'generate_adapter.py')
|
|
17
|
+
|
|
18
|
+
function findPython() {
|
|
19
|
+
for (const bin of ['python3', 'python']) {
|
|
20
|
+
try {
|
|
21
|
+
const out = execFileSync(bin, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
|
|
22
|
+
if (/python\s+3\./i.test(out)) return bin
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const python = findPython()
|
|
29
|
+
if (!python) {
|
|
30
|
+
process.stderr.write(
|
|
31
|
+
'error: design-playbook requires Python 3 (python3 or python).\n' +
|
|
32
|
+
'Install Python 3 from https://python.org and re-run.\n'
|
|
33
|
+
)
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Strip leading "init" if invoked as `design-playbook init <agent>` so both
|
|
38
|
+
// forms are equivalent: `design-playbook init codex` == `design-playbook codex`
|
|
39
|
+
const rawArgs = process.argv.slice(2)
|
|
40
|
+
const args = rawArgs[0] === 'init' ? rawArgs.slice(1) : rawArgs
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
execFileSync(python, [SCRIPT, ...args], { stdio: 'inherit' })
|
|
44
|
+
} catch (err) {
|
|
45
|
+
process.exit(err.status ?? 1)
|
|
46
|
+
}
|
package/mcp/evidence/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Runtime for the optional **observe\*** step. Writes capture artifacts only — *
|
|
|
8
8
|
| --- | --- |
|
|
9
9
|
| Unset | Artifact paths resolve under the **MCP process cwd** |
|
|
10
10
|
| `"."` (default in package `.mcp.json`) | Same — relative to process cwd, **not** the chat workspace root |
|
|
11
|
-
| Absolute path | Preferred for cross-repo dogfood: set to the run root (e.g.
|
|
11
|
+
| Absolute path | Preferred for cross-repo dogfood: set to the run root (e.g. `/path/to/host-app/.scratch/playbook-smoke/<run>`) so `evidence/L6.*.png` lands next to `manifest.jsonl` |
|
|
12
12
|
|
|
13
13
|
Relative values are resolved with `Path(value).resolve()` at process start semantics (cwd-relative). If captures appear under the plugin monorepo instead of the host run, check cwd and this env — the tool also returns **`written_path`** (absolute) so mis-roots are obvious without a filesystem search.
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ Example (host run):
|
|
|
16
16
|
|
|
17
17
|
```json
|
|
18
18
|
"env": {
|
|
19
|
-
"DESIGN_PLAYBOOK_RUN_ROOT": "
|
|
19
|
+
"DESIGN_PLAYBOOK_RUN_ROOT": "/path/to/app/.scratch/my-run"
|
|
20
20
|
}
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -662,7 +662,7 @@ def matrix_viewport(name: str) -> dict[str, Any]:
|
|
|
662
662
|
def capture_delivery_matrix(
|
|
663
663
|
*,
|
|
664
664
|
url: str,
|
|
665
|
-
out_dir: Path
|
|
665
|
+
out_dir: Path,
|
|
666
666
|
freeze: dict[str, Any] | None = None,
|
|
667
667
|
browser_adapter: BrowserAdapter | None = None,
|
|
668
668
|
) -> dict[str, dict[str, Any]]:
|
|
@@ -674,19 +674,19 @@ def capture_delivery_matrix(
|
|
|
674
674
|
can assemble the ``disclosure-review.json`` matrix and the ``/export-zip``
|
|
675
675
|
package from one pass.
|
|
676
676
|
|
|
677
|
-
``out_dir``
|
|
678
|
-
|
|
679
|
-
the
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
adapters
|
|
677
|
+
``out_dir`` is required and carries no default: handoff artifacts live
|
|
678
|
+
under the run tree (``<run_root>/evidence/static-handoff/``, ADR-0034 §5),
|
|
679
|
+
never the process CWD, so the caller must pass the run-tree destination
|
|
680
|
+
explicitly. ``browser_adapter`` uses the same ``BrowserAdapter`` seam as
|
|
681
|
+
``execute_capture_plan`` (real Playwright by default, injected fake in
|
|
682
|
+
tests). Production adapters should expose ``capture_and_probe`` so the
|
|
683
|
+
snapshot and metrics share one page; adapters without that seam remain
|
|
684
|
+
explicitly ``unmeasured``.
|
|
683
685
|
"""
|
|
684
686
|
freeze = freeze or {"enabled": True, "waitFonts": True, "networkIdle": False}
|
|
685
687
|
if browser_adapter is None:
|
|
686
688
|
browser_adapter = PlaywrightBrowserAdapter()
|
|
687
689
|
|
|
688
|
-
if out_dir is None:
|
|
689
|
-
out_dir = Path("output/playwright/static-handoff")
|
|
690
690
|
out_dir = Path(out_dir)
|
|
691
691
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
692
692
|
results: dict[str, dict[str, Any]] = {}
|
|
@@ -19,8 +19,10 @@ Two seams keep the contract builder pure and testable without a browser:
|
|
|
19
19
|
It only normalizes caller-supplied facts into the §4.2 shape.
|
|
20
20
|
|
|
21
21
|
``build_handoff_zip()`` packages the disclosure credential plus any caller-
|
|
22
|
-
supplied snapshot artifacts into a single ZIP
|
|
23
|
-
|
|
22
|
+
supplied snapshot artifacts into a single ZIP. The Evidence-side builder
|
|
23
|
+
(``handoff.py``) writes it to disk as ``static-handoff.zip`` next to the
|
|
24
|
+
delivery page under ``<run_root>/evidence/static-handoff/`` (ADR-0034); no
|
|
25
|
+
HTTP delivery endpoint exists. It never reads outside the caller-provided
|
|
24
26
|
file list.
|
|
25
27
|
"""
|
|
26
28
|
|
package/mcp/evidence/handoff.py
CHANGED
|
@@ -59,6 +59,7 @@ class StaticHandoffResult:
|
|
|
59
59
|
json_path: Path
|
|
60
60
|
zip_path: Path
|
|
61
61
|
index_html: Path
|
|
62
|
+
deliverable_html: Path
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
def _iso_now() -> str:
|
|
@@ -691,7 +692,8 @@ def build_static_handoff(
|
|
|
691
692
|
output (``filled-ui.html``) - the page the five-viewport matrix and the
|
|
692
693
|
layout probe actually target (ADR-0034 §4). Everything is written under
|
|
693
694
|
``<run_root>/evidence/static-handoff/``: snapshots, the disclosure JSON,
|
|
694
|
-
the ZIP package,
|
|
695
|
+
the ZIP package, a same-directory ``deliverable.html`` copy (the page's
|
|
696
|
+
relative link target, spec A5), and a self-contained index page.
|
|
695
697
|
"""
|
|
696
698
|
run_root = Path(run_root)
|
|
697
699
|
deliverable = Path(deliverable)
|
|
@@ -707,6 +709,14 @@ def build_static_handoff(
|
|
|
707
709
|
if gate_runner is None:
|
|
708
710
|
gate_runner = _run_gate_validation
|
|
709
711
|
|
|
712
|
+
# Read the deliverable source up front and fail fast: its bytes are the
|
|
713
|
+
# run identity (hash), the ZIP's prototype member, and the on-disk copy
|
|
714
|
+
# the delivery page links relatively (#107). A missing or undecodable
|
|
715
|
+
# source aborts before any capture launches or artifact is written, so a
|
|
716
|
+
# delivery page can never exist without its link target.
|
|
717
|
+
deliverable_bytes = deliverable.read_bytes()
|
|
718
|
+
deliverable_text = deliverable_bytes.decode("utf-8")
|
|
719
|
+
|
|
710
720
|
# Sample conditional-gate preconditions BEFORE writing anything: this
|
|
711
721
|
# builder's own output lives under evidence/, and a precondition sampled
|
|
712
722
|
# afterwards would be one this run manufactured for itself.
|
|
@@ -769,7 +779,6 @@ def build_static_handoff(
|
|
|
769
779
|
else:
|
|
770
780
|
verdict = "Pending"
|
|
771
781
|
|
|
772
|
-
deliverable_bytes = deliverable.read_bytes()
|
|
773
782
|
run_id = (
|
|
774
783
|
f"static-handoff-{round_n}-{hashlib.sha256(deliverable_bytes).hexdigest()[:12]}"
|
|
775
784
|
)
|
|
@@ -808,6 +817,13 @@ def build_static_handoff(
|
|
|
808
817
|
json_path = out_dir / "disclosure-review.json"
|
|
809
818
|
json_path.write_text(disclosure_json(payload), encoding="utf-8")
|
|
810
819
|
|
|
820
|
+
# The delivery page links "deliverable.html" as a same-directory relative
|
|
821
|
+
# anchor (spec A5: disk artifacts, same-directory relative links); the copy
|
|
822
|
+
# must exist beside index.html, byte-identical to the ZIP member, or the
|
|
823
|
+
# delivery surface ships a dead link (#107).
|
|
824
|
+
deliverable_copy = out_dir / "deliverable.html"
|
|
825
|
+
deliverable_copy.write_bytes(deliverable_bytes)
|
|
826
|
+
|
|
811
827
|
artifacts: dict[str, str] = {}
|
|
812
828
|
if snap_dir.is_dir():
|
|
813
829
|
for vp in VIEWPORT_ORDER:
|
|
@@ -820,7 +836,7 @@ def build_static_handoff(
|
|
|
820
836
|
artifact_files=artifacts,
|
|
821
837
|
# spec §4.1: the handoff ships "snapshots and prototype code". PNGs
|
|
822
838
|
# alone do not let the recipient rebuild the reviewed page.
|
|
823
|
-
text_members={"deliverable.html":
|
|
839
|
+
text_members={"deliverable.html": deliverable_text},
|
|
824
840
|
zip_target=str(zip_path),
|
|
825
841
|
)
|
|
826
842
|
|
|
@@ -835,4 +851,5 @@ def build_static_handoff(
|
|
|
835
851
|
json_path=json_path,
|
|
836
852
|
zip_path=zip_path,
|
|
837
853
|
index_html=index_html,
|
|
854
|
+
deliverable_html=deliverable_copy,
|
|
838
855
|
)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*{box-sizing:border-box}
|
|
13
13
|
body{margin:0;background:var(--bg);color:var(--ink);
|
|
14
14
|
font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",
|
|
15
|
-
"Hiragino Sans GB","Microsoft YaHei",sans-serif;}
|
|
15
|
+
"Hiragino Sans GB","Microsoft YaHei","Noto Sans SC",sans-serif;}
|
|
16
16
|
.wrap{max-width:960px;margin:0 auto;padding:32px 20px 64px;}
|
|
17
17
|
header{border-bottom:2px solid var(--primary);padding-bottom:16px;margin-bottom:24px;}
|
|
18
18
|
h1{margin:0;font-size:22px;letter-spacing:-.01em;}
|
|
@@ -195,8 +195,16 @@ class BuildStaticHandoffTests(unittest.TestCase):
|
|
|
195
195
|
result = self._build(tmp, run_root)
|
|
196
196
|
base = run_root / "evidence" / "static-handoff"
|
|
197
197
|
self.assertEqual(result.out_dir, base)
|
|
198
|
-
for path in (
|
|
198
|
+
for path in (
|
|
199
|
+
result.json_path,
|
|
200
|
+
result.zip_path,
|
|
201
|
+
result.index_html,
|
|
202
|
+
result.deliverable_html,
|
|
203
|
+
):
|
|
199
204
|
self.assertTrue(path.is_file(), path)
|
|
205
|
+
# #107: the page's relative "deliverable.html" anchor must resolve
|
|
206
|
+
# in the same directory as index.html (spec A5).
|
|
207
|
+
self.assertEqual(result.deliverable_html, base / "deliverable.html")
|
|
200
208
|
self.assertTrue((base / "snapshots" / "viewport-1280x900.png").is_file())
|
|
201
209
|
# nothing outside the run tree
|
|
202
210
|
self.assertFalse((tmp / "output").exists())
|
|
@@ -390,6 +398,30 @@ class BuildStaticHandoffTests(unittest.TestCase):
|
|
|
390
398
|
result = self._build(tmp, run_root)
|
|
391
399
|
self.assertEqual(result.payload["profile"], "unknown")
|
|
392
400
|
|
|
401
|
+
def test_missing_deliverable_fails_before_any_artifact_is_written(self) -> None:
|
|
402
|
+
"""#107 coherence: a delivery page must never exist without its link
|
|
403
|
+
target. A missing Stage 7 source aborts the build up front, so nothing
|
|
404
|
+
lands under evidence/static-handoff/ - no page, no dead link."""
|
|
405
|
+
import tempfile
|
|
406
|
+
|
|
407
|
+
with tempfile.TemporaryDirectory() as tmp_s:
|
|
408
|
+
tmp = Path(tmp_s)
|
|
409
|
+
run_root = _make_run(tmp)
|
|
410
|
+
missing = tmp / "filled-ui.html" # never written
|
|
411
|
+
with self.assertRaises(OSError):
|
|
412
|
+
handoff.build_static_handoff(
|
|
413
|
+
run_root,
|
|
414
|
+
missing,
|
|
415
|
+
round_n=1,
|
|
416
|
+
summary="s",
|
|
417
|
+
capture_runner=_fake_capture_runner,
|
|
418
|
+
gate_runner=_passing_gate_runner,
|
|
419
|
+
)
|
|
420
|
+
self.assertFalse(
|
|
421
|
+
(run_root / "evidence" / "static-handoff").exists(),
|
|
422
|
+
"missing source must fail before any artifact is written",
|
|
423
|
+
)
|
|
424
|
+
|
|
393
425
|
|
|
394
426
|
class HandoffPageTests(unittest.TestCase):
|
|
395
427
|
def test_page_template_is_own_content_with_no_cdn(self) -> None:
|
|
@@ -430,6 +462,38 @@ class HandoffPageTests(unittest.TestCase):
|
|
|
430
462
|
# No unsanitized injection from run-controlled text.
|
|
431
463
|
self.assertNotIn("<script>", html[start:end])
|
|
432
464
|
|
|
465
|
+
def test_page_deliverable_link_target_exists_beside_the_page(self) -> None:
|
|
466
|
+
"""#107: "Everything below sits next to it on disk" must be true.
|
|
467
|
+
|
|
468
|
+
The page links ``deliverable.html`` as a same-directory relative
|
|
469
|
+
anchor (spec A5), so the builder must write that copy beside
|
|
470
|
+
index.html, byte-identical to the ZIP member and the Stage 7 source.
|
|
471
|
+
"""
|
|
472
|
+
import tempfile
|
|
473
|
+
|
|
474
|
+
with tempfile.TemporaryDirectory() as tmp_s:
|
|
475
|
+
tmp = Path(tmp_s)
|
|
476
|
+
run_root = _make_run(tmp)
|
|
477
|
+
deliverable = tmp / "filled-ui.html"
|
|
478
|
+
deliverable.write_text(DELIVERABLE_HTML, encoding="utf-8")
|
|
479
|
+
result = handoff.build_static_handoff(
|
|
480
|
+
run_root, deliverable, round_n=1, summary="s",
|
|
481
|
+
capture_runner=_fake_capture_runner,
|
|
482
|
+
gate_runner=_passing_gate_runner,
|
|
483
|
+
)
|
|
484
|
+
html = result.index_html.read_text(encoding="utf-8")
|
|
485
|
+
self.assertIn('href="deliverable.html"', html)
|
|
486
|
+
on_disk = result.index_html.parent / "deliverable.html"
|
|
487
|
+
self.assertTrue(
|
|
488
|
+
on_disk.is_file(),
|
|
489
|
+
"the page's relative deliverable.html link must not dangle",
|
|
490
|
+
)
|
|
491
|
+
self.assertEqual(result.deliverable_html, on_disk)
|
|
492
|
+
with zipfile.ZipFile(result.zip_path) as zf:
|
|
493
|
+
member = zf.read("deliverable.html")
|
|
494
|
+
self.assertEqual(on_disk.read_bytes(), member)
|
|
495
|
+
self.assertEqual(on_disk.read_bytes(), deliverable.read_bytes())
|
|
496
|
+
|
|
433
497
|
|
|
434
498
|
class GateNormalizationTests(unittest.TestCase):
|
|
435
499
|
"""Ported from the review-session surface; the logic now lives in handoff."""
|