From b225be076b1f4a1a11b21fb71411575c789e315b Mon Sep 17 00:00:00 2001 From: "Terry.Mao" Date: Fri, 19 Jun 2026 12:17:01 +0800 Subject: [PATCH 1/6] feat(pr-walkthrough): add PR walkthrough skill Refs #272 --- .agents/skills/pr-walkthrough/SKILL.md | 250 ++++++++ .../scripts/d3_canvas_runtime.py | 534 ++++++++++++++++++ .../scripts/validate_d3_canvas.py | 240 ++++++++ .gitignore | 2 + 4 files changed, 1026 insertions(+) create mode 100644 .agents/skills/pr-walkthrough/SKILL.md create mode 100644 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py create mode 100644 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py diff --git a/.agents/skills/pr-walkthrough/SKILL.md b/.agents/skills/pr-walkthrough/SKILL.md new file mode 100644 index 00000000..e9d5f399 --- /dev/null +++ b/.agents/skills/pr-walkthrough/SKILL.md @@ -0,0 +1,250 @@ +--- +name: pr-walkthrough +description: Generate a local static interactive D3 walkthrough of a pull request. Use when the user wants a zoomable PR map, graph/canvas PR orientation, or alternate visualization of PR system components, data flow, code dependencies, and user actions. +--- + +# pr-walkthrough + +为当前分支或 GitHub PR 生成一个本地静态 HTML 讲解页,帮助 reviewer 快速理解 PR 涉及的系统、数据流、代码依赖和用户路径。此技能不是代码评审技能,不生成新的 review finding、approval 或 request-changes 结论。 + +## 输出 + +生成文件到: + +```text +.aicodingflow/pr-walkthrough/index.html +``` + +站点必须可以直接用 `file://` 打开,不要求 dev server、打包器、安装依赖或构建步骤。优先生成单个自包含 HTML 文件,内联 CSS、JavaScript 和图数据;如果必须拆分资源,只使用相对路径,并避免用 `fetch()` 读取本地数据。 + +D3 使用固定版本的官方 CDN: + +```text +https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js +``` + +优先使用本技能自带脚本生成和验证页面: + +```bash +python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --template --data graph.json > .aicodingflow/pr-walkthrough/index.html +python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html .aicodingflow/pr-walkthrough/index.html --require-browser +``` + +## 视觉风格 + +不要套用任何公司视觉规范、专属字体、专属色或外部视觉规范技能。使用脚本内置的中性文档/工具界面样式即可。可以为四个视图使用不同的功能色,但颜色只表达信息类型,不表达品牌。 + +推荐视图颜色: + +- System overview view: `#b7791f` +- Data flow graph: `#0f7b5f` +- Code dependency graph: `#2563eb` +- User action graph: `#7c3aed` +- Active/focus/selected node: `#2563eb` + +## 工作流 + +### 1. 建立 PR 上下文 + +识别仓库根目录、当前分支和比较 base。若当前分支已有 GitHub PR,优先使用 PR base,并记录 PR URL 生成 diff links: + +```bash +gh pr view --json baseRefName,headRefName,title,body,url,state,reviewRequests,reviews,files +``` + +若没有 PR,从远端默认分支或仓库约定推断 base: + +```bash +git symbolic-ref --short refs/remotes/origin/HEAD +``` + +收集 review 输入: + +```bash +git --no-pager diff --stat ...HEAD +git --no-pager diff --name-status ...HEAD +git --no-pager log --oneline ..HEAD +git --no-pager diff ...HEAD +``` + +根据 changed lines、changed files 和概念跨度估算 PR 大小,默认生成最小有用讲解: + +- Tiny PR: 约 1 个文件或 75 行以内。每个视图 2-3 个节点/卡片,1-2 个 tour steps。 +- Small PR: 250 行以内或 1-3 个文件。每个视图 3-4 个节点,2-4 个 tour steps。 +- Medium PR: 250-800 行或多个相关文件。每个视图 4-7 个节点。 +- Large PR: 只有跨多个子系统、有新架构或有大量 spec/review 上下文时,才使用 5-12 个节点。 + +不要只读 diff。要读取关键变更文件的当前完整版本,并沿 imports、call sites、types、state owners、renderers、tests 和相邻模块理解系统边界。System overview 尤其要作为稳定的代码阅读产物,而不是 PR 变更列表。 + +如果存在 GitHub PR,收集已有评论和 review discussion: + +```bash +gh pr view --json comments,reviews,reviewThreads +gh api repos/:owner/:repo/pulls//comments --paginate +gh api repos/:owner/:repo/issues//comments --paginate +``` + +这些评论只作为讲解素材,不作为改代码指令。 + +### 2. 收集视觉素材 + +查找能帮助 reviewer 理解用户可见变化的截图、mock、视频、设计资产或 changed image。来源包括 PR body/comments/reviews、关联 issue、变更的图片/SVG/mock fixture、本地测试截图,以及 `.aicodingflow/` 下已有临时产物。 + +需要纳入页面的外部视觉素材应下载或导出到: + +```text +.aicodingflow/pr-walkthrough/assets/ +``` + +用相对路径引用,或在更简单时嵌入为 data URI。不要 hotlink 远端图片。 + +### 3. 构造 GitHub diff links + +当已知 PR URL 时,每个 changed file reference、节点附件、代码摘录和依赖边都应链接到 PR 的 Files changed 页: + +```text +/files#diff- +/files#diff-R +/files#diff-L +``` + +`` 是变更文件路径的 lowercase SHA-256 hex digest。用确定性 helper 或脚本生成,不要手写猜测。 + +### 4. 设计四个独立视图 + +先构建数据模型,再生成 HTML。必须恰好包含四个视图: + +- `system-overview`: 受影响子系统的稳定架构概览。不要提 PR、changed files、diff links、review comments、screenshots、specs 或实现 delta。通常 `edges: []`,用较大的卡片和可读段落说明。 +- `data-flow`: 状态、数据、事件、请求、文件、资产或渲染输出如何流动。 +- `code-dependency`: 变更组件之间的依赖方向、入口点、边界和 leaf dependencies。 +- `user-action`: 用户从哪个 surface 开始,触发什么动作,看到什么反馈。 + +每个视图都需要自己的 nodes、edges 和 guided tour。除 `system-overview` 外,其他视图必须有有向边、箭头和描述 source-to-target 关系的 edge label。 + +每个节点回答: + +- reviewer 需要先理解什么? +- 此节点在这个视图中解释哪个概念? +- 哪些文件、spec、测试、视觉素材或已有评论能作为证据? +- 点击后 detail panel 应该展示什么? + +Tour 顺序要教 reviewer 从起点读到终点,不要只是文件顺序。 + +### 5. 写入数据模型 + +将图数据内联到 HTML,赋值给 `window.PR_WALKTHROUGH_D3_DATA` 或写入 `id="pr-walkthrough-data"` 的 JSON script。不要用 `fetch()` 加载本地 JSON。 + +数据形状: + +```json +{ + "meta": { + "title": "PR title", + "prUrl": "https://github.com/owner/repo/pull/123", + "baseRef": "main", + "headRef": "feature-branch", + "summary": "What the PR is trying to accomplish." + }, + "graphs": [ + { + "id": "system-overview", + "label": "System overview", + "color": "#b7791f", + "summary": "Concise component overview for the affected subsystem.", + "nodes": [], + "edges": [], + "tour": [] + } + ] +} +``` + +坐标建议: + +- 起点放在左侧或上方。 +- Tour 路径尽量从左到右或从上到下。 +- 相关节点靠近,低层依赖放在调用者右侧或下方。 +- 小 PR 的图应紧凑到无需大量平移即可读懂。 + +### 6. 生成静态页面 + +可先生成样例数据,修改为真实 PR 数据,再渲染: + +```bash +mkdir -p .aicodingflow/pr-walkthrough +python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --sample-data > .aicodingflow/pr-walkthrough/graph.json +python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --template --data .aicodingflow/pr-walkthrough/graph.json > .aicodingflow/pr-walkthrough/index.html +``` + +必备交互: + +- 单个 D3 SVG canvas,支持 zoom、pan、fit-to-view 和 reset zoom。 +- 四个 view toggles: `System overview`, `Data flow graph`, `Code dependency graph`, `User action graph`。 +- Tour controls: `Previous tour step`, `Next tour step`, `Restart tour` 和 step indicator。 +- Search input 可搜索 active graph 的 node titles、file paths 和 comments。 +- 点击节点更新 detail panel,并在可能时同步到对应 tour step。 +- 键盘支持:Right Arrow/`n`、Left Arrow/`p`、`1`-`4`、`+`/`=`、`-`、`0`、`f`、`/`、`Escape`。 +- 稳定的 `data-graph-id`、`data-node-id`、`data-edge-id`、`data-tour-index` 属性,方便自动化截图和验证。 + +### 7. 验证 + +完成前必须运行: + +```bash +python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html .aicodingflow/pr-walkthrough/index.html --require-browser +``` + +验证至少确认: + +- D3 使用固定版本 URL,未使用 `latest`。 +- 页面不用 `fetch()` 读取本地数据。 +- 图数据恰好包含 `system-overview`、`data-flow`、`code-dependency`、`user-action`。 +- 必备控件存在。 +- 每个视图都有节点和 tour。 +- 非 overview 图都有有向边和箭头。 +- System overview 是 PR-agnostic 的架构概览,不带 PR 附件。 +- PR-changed specs 和已有 PR review comments 被纳入或明确报告为不存在/不可用。 +- 视觉素材是本地相对路径或 data URI。 + +如果浏览器环境不可用,报告 canvas rendering 未验证,不要说 walkthrough 已完全 ready。 + +### 8. 可选发布到 GitHub Pages + +默认只保留本地产物,不发布公网,也不提交 `.aicodingflow/pr-walkthrough/`。只有用户明确要求公开 URL 时才发布。发布前必须确认 PR 内容、截图、评论和代码上下文可以公开。 + +推荐使用 `gh-pages` 分支作为 Pages 来源,并把生成站点复制到临时 worktree,避免把生成物混入当前开发分支: + +```bash +repo="$(gh repo view --json nameWithOwner --jq .nameWithOwner)" +sha="$(git rev-parse --short HEAD)" +site_dir="/tmp/aicodingflow-pr-walkthrough-pages-$sha" +git fetch origin gh-pages || true +git worktree add "$site_dir" gh-pages +mkdir -p "$site_dir/pr-walkthrough/$sha" +cp -R .aicodingflow/pr-walkthrough/. "$site_dir/pr-walkthrough/$sha/" +cd "$site_dir" +git add "pr-walkthrough/$sha" +git commit -m "docs: publish PR walkthrough $sha" +git push origin gh-pages +``` + +如果仓库尚未启用 GitHub Pages,先让用户在仓库设置里选择 `gh-pages` 分支作为 Pages source,或在有权限时使用 GitHub API/CLI 配置 Pages。不要在未征得用户同意时更改仓库 Pages 设置。 + +发布后的 URL 通常为: + +```text +https://.github.io//pr-walkthrough// +``` + +## 最终回复 + +报告: + +- 生成的 walkthrough 路径。 +- `file://` URL。 +- 使用的 base branch、PR title 或 branch name。 +- 用于 diff links 的 GitHub PR URL。 +- 是否找到并纳入 PR review comments。 +- D3 canvas validation 是否通过。 +- 若发布,报告 GitHub Pages URL。 +- 重要 caveats、缺失 specs 或无法完成的验证。 diff --git a/.agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py b/.agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py new file mode 100644 index 00000000..89325550 --- /dev/null +++ b/.agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py @@ -0,0 +1,534 @@ +#!/usr/bin/env python3 +"""Reusable D3 multi-graph tour helpers for the pr-walkthrough skill.""" + +from __future__ import annotations + +import argparse +import html +import json +from pathlib import Path +from textwrap import dedent + +D3_VERSION = "7.9.0" +D3_CDN_URL = f"https://cdn.jsdelivr.net/npm/d3@{D3_VERSION}/dist/d3.min.js" + + +def d3_canvas_css() -> str: + return dedent( + """ + :root { + --walkthrough-bg: #f6f7f9; + --walkthrough-panel: #ffffff; + --walkthrough-panel-2: #eef1f5; + --walkthrough-border: #d7dce2; + --walkthrough-text: #17202a; + --walkthrough-muted: #52606d; + --walkthrough-dim: #7b8794; + --walkthrough-accent: #2563eb; + --walkthrough-green: #0f7b5f; + --walkthrough-blue: #2563eb; + --walkthrough-purple: #7c3aed; + --walkthrough-yellow: #b7791f; + --walkthrough-font-sans: Inter, ui-sans-serif, system-ui, sans-serif; + --walkthrough-font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', ui-monospace, monospace; + } + * { box-sizing: border-box; } + body { margin: 0; min-height: 100vh; background: var(--walkthrough-bg); color: var(--walkthrough-text); font-family: var(--walkthrough-font-sans); } + a { color: var(--walkthrough-text); text-decoration-color: var(--walkthrough-accent); text-underline-offset: 3px; } + button, input { font: inherit; } + .d3-walkthrough-shell { min-height: 100vh; display: grid; grid-template-rows: auto 1fr; } + .d3-walkthrough-header { display: grid; gap: 10px; padding: 28px 32px 20px; border-bottom: 1px solid var(--walkthrough-border); background: #ffffff; } + .d3-kicker { color: var(--walkthrough-accent); font-family: var(--walkthrough-font-mono); font-size: 12px; letter-spacing: 0; text-transform: uppercase; } + .d3-walkthrough-header h1 { margin: 0; max-width: 1080px; font-size: 42px; line-height: 1.05; letter-spacing: 0; } + .d3-meta-row { display: flex; flex-wrap: wrap; gap: 8px; color: var(--walkthrough-muted); font-family: var(--walkthrough-font-mono); font-size: 12px; } + .d3-summary { max-width: 920px; margin: 0; color: var(--walkthrough-muted); font-size: 17px; line-height: 1.45; } + .d3-canvas-layout { min-height: 0; display: grid; grid-template-columns: 310px minmax(560px, 1fr) 390px; gap: 0; } + .d3-control-panel, .d3-detail-panel { min-height: 0; overflow: auto; background: var(--walkthrough-panel); border-right: 1px solid var(--walkthrough-border); padding: 18px; } + .d3-detail-panel { border-right: 0; border-left: 1px solid var(--walkthrough-border); } + .d3-panel-title { margin: 0 0 12px; font-size: 12px; color: var(--walkthrough-muted); font-family: var(--walkthrough-font-mono); letter-spacing: 0; text-transform: uppercase; } + .d3-control-stack { display: grid; gap: 10px; margin-bottom: 18px; } + .d3-control-button, .d3-graph-toggle { border: 1px solid var(--walkthrough-border); background: var(--walkthrough-panel-2); color: var(--walkthrough-text); border-radius: 10px; padding: 10px 12px; cursor: pointer; text-align: left; } + .d3-control-button:hover, .d3-graph-toggle:hover, .d3-control-button:focus, .d3-graph-toggle:focus { border-color: var(--walkthrough-accent); outline: none; } + .d3-graph-toggle[aria-pressed="true"] { border-color: var(--graph-color, var(--walkthrough-accent)); box-shadow: inset 3px 0 0 var(--graph-color, var(--walkthrough-accent)); } + .d3-tour-card { border: 1px solid var(--walkthrough-border); background: #ffffff; border-radius: 8px; padding: 12px; margin-bottom: 14px; } + .d3-tour-step-label { color: var(--walkthrough-accent); font-family: var(--walkthrough-font-mono); font-size: 11px; letter-spacing: 0; text-transform: uppercase; } + .d3-tour-title { margin: 6px 0; font-size: 18px; line-height: 1.15; } + .d3-tour-body { color: var(--walkthrough-muted); line-height: 1.4; margin: 0; } + .d3-search { width: 100%; border: 1px solid var(--walkthrough-border); background: #ffffff; color: var(--walkthrough-text); border-radius: 8px; padding: 10px 12px; } + .d3-search:focus { border-color: var(--walkthrough-accent); outline: none; } + .d3-help { color: var(--walkthrough-dim); font-family: var(--walkthrough-font-mono); font-size: 11px; line-height: 1.5; } + .d3-canvas-stage { min-height: 0; position: relative; overflow: hidden; background: #f8fafc; } + #pr-walkthrough-canvas { width: 100%; height: 100%; min-height: 700px; display: block; } + .d3-canvas-error { position: absolute; inset: 18px; display: none; place-items: center; border: 1px solid var(--walkthrough-border); background: #fffffff2; color: var(--walkthrough-text); padding: 24px; z-index: 2; } + body.d3-canvas-error .d3-canvas-error { display: grid; } + .d3-graph-title { fill: #17202a; opacity: 0.42; font-family: var(--walkthrough-font-mono); font-size: 13px; letter-spacing: 0; text-transform: uppercase; } + .d3-edge path { fill: none; stroke: var(--edge-color, #868584); stroke-width: 2; stroke-opacity: 0.68; } + .d3-edge-arrow path { fill: var(--edge-color, #868584); } + .d3-edge text { fill: #3e4c59; font-family: var(--walkthrough-font-mono); font-size: 11px; paint-order: stroke; stroke: #f8fafc; stroke-width: 4px; stroke-linejoin: round; } + .d3-node { cursor: pointer; } + .d3-node rect { fill: #ffffff; stroke: var(--node-color, var(--walkthrough-accent)); stroke-width: 2; filter: drop-shadow(0 10px 24px #0000001f); } + .d3-node.is-selected rect { stroke: var(--walkthrough-accent); stroke-width: 4; } + .d3-node.is-tour-node rect { stroke: var(--walkthrough-accent); stroke-width: 4; filter: drop-shadow(0 0 16px #2563eb66); } + .d3-node.is-dimmed, .d3-edge.is-dimmed { opacity: 0.18; } + .d3-node-title { fill: #17202a; font-family: var(--walkthrough-font-sans); font-size: 15px; font-weight: 700; pointer-events: none; } + .d3-node-kind { fill: #52606d; font-family: var(--walkthrough-font-mono); font-size: 10px; letter-spacing: 0; text-transform: uppercase; pointer-events: none; } + .d3-node-summary { fill: #3e4c59; font-family: var(--walkthrough-font-sans); font-size: 12px; pointer-events: none; } + .d3-detail-title { margin: 0 0 6px; font-size: 24px; line-height: 1.1; } + .d3-detail-kind { color: var(--walkthrough-accent); font-family: var(--walkthrough-font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; } + .d3-detail-summary { color: var(--walkthrough-muted); line-height: 1.45; } + .d3-detail-section { margin-top: 18px; } + .d3-detail-section h3 { margin: 0 0 8px; color: var(--walkthrough-muted); font-family: var(--walkthrough-font-mono); font-size: 12px; letter-spacing: 0; text-transform: uppercase; } + .d3-detail-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; } + .d3-detail-list li { border: 1px solid var(--walkthrough-border); background: #ffffff; border-radius: 8px; padding: 10px; color: var(--walkthrough-muted); line-height: 1.35; } + .d3-file-link { display: block; overflow-wrap: anywhere; color: var(--walkthrough-text); font-family: var(--walkthrough-font-mono); font-size: 12px; } + .d3-comment-author { display: block; color: var(--walkthrough-accent); font-family: var(--walkthrough-font-mono); font-size: 11px; margin-bottom: 4px; } + .d3-empty { color: var(--walkthrough-dim); } + @media (max-width: 1180px) { .d3-canvas-layout { grid-template-columns: 1fr; grid-template-rows: auto minmax(680px, 1fr) auto; } .d3-control-panel, .d3-detail-panel { border: 0; border-bottom: 1px solid var(--walkthrough-border); max-height: 360px; } } + """ + ).strip() + + +def d3_canvas_runtime_script() -> str: + return dedent( + f""" + + """ + ).strip() + + +def graph_controls_markup(data: dict) -> str: + buttons = "\n".join( + f'' + for graph in data.get("graphs", []) + ) + return dedent( + f""" + + """ + ).strip() + + +def html_template(data: dict) -> str: + meta = data.get("meta") or {} + title = str(meta.get("title") or "PR Walkthrough") + summary = str(meta.get("summary") or "Interactive PR walkthrough graphs.") + pr_url = str(meta.get("prUrl") or "") + base = str(meta.get("baseRef") or "") + head = str(meta.get("headRef") or "") + data_json = json.dumps(data, ensure_ascii=False).replace(" + + + + + {html.escape(title)} + + + +
+
+
AICodingFlow PR walkthrough
+

{html.escape(title)}

+
{html.escape(base)} ← {html.escape(head)}{f'Open PR' if pr_url else ''}
+

{html.escape(summary)}

+
+
+ {graph_controls_markup(data)} +
+ + +
+ +
+ + + {d3_canvas_runtime_script()} +
+ + + """ + ).strip() + + +def sample_data() -> dict: + return { + "meta": {"title": "Sample PR D3 walkthrough", "prUrl": "", "baseRef": "master", "headRef": "feature", "summary": "Replace this sample graph with PR-specific guided graph tours."}, + "graphs": [ + { + "id": "system-overview", "label": "System overview", "color": "#b7791f", "summary": "Major touched components.", + "nodes": [ + {"id": "surface", "title": "User-facing surface", "kind": "overview card", "x": -220, "y": -80, "width": 360, "height": 220, "summaryLines": 7, "summary": "Use a full paragraph here to define the surface, what code owns it, and why a reviewer needs that concept before reading the PR. Keep this scoped to orientation, not implementation deltas.", "details": ["Explain the stable component."], "files": [], "comments": [], "links": []}, + {"id": "component", "title": "State or action owner", "kind": "overview card", "x": 220, "y": -80, "width": 360, "height": 220, "summaryLines": 7, "summary": "Use another full paragraph for the next essential concept. If a concept is not needed to understand the review surface, leave it out of the system overview.", "details": ["Explain what this component owns."], "files": [], "comments": [], "links": []}, + ], + "edges": [], + "tour": [{"nodeId": "surface", "title": "Start with the surface", "body": "The system overview starts with the smallest useful orientation concept."}, {"nodeId": "component", "title": "Name the owner", "body": "Then identify the state or action owner a reviewer needs to know."}], + }, + { + "id": "data-flow", "label": "Data flow graph", "color": "#0f7b5f", "summary": "How state moves.", + "nodes": [ + {"id": "intent", "title": "Intent", "kind": "input", "x": -260, "y": -80, "summary": "Spec intent enters the system.", "details": ["Start with the PR intent."], "files": [], "comments": [], "links": []}, + {"id": "state", "title": "State", "kind": "owner", "x": 80, "y": 20, "summary": "State owner carries the change.", "details": ["Explain the data owner."], "files": [], "comments": [], "links": []}, + ], + "edges": [{"source": "intent", "target": "state", "label": "flows into"}], + "tour": [{"nodeId": "intent", "title": "Start with intent", "body": "The data-flow graph begins with the product intent."}, {"nodeId": "state", "title": "Follow state", "body": "Then inspect where state is owned."}], + }, + { + "id": "code-dependency", "label": "Code dependency graph", "color": "#2563eb", "summary": "How code depends.", + "nodes": [ + {"id": "entry", "title": "Entry point", "kind": "entry", "x": -220, "y": -60, "summary": "Changed entry point.", "details": ["Start at the high-level code seam."], "files": [], "comments": [], "links": []}, + {"id": "leaf", "title": "Leaf dependency", "kind": "leaf", "x": 160, "y": 70, "summary": "Lower-level dependency.", "details": ["Inspect the dependency."], "files": [], "comments": [], "links": []}, + ], + "edges": [{"source": "entry", "target": "leaf", "label": "depends on"}], + "tour": [{"nodeId": "entry", "title": "Start at entry", "body": "Begin with the high-level code seam."}, {"nodeId": "leaf", "title": "Drill down", "body": "Then move to the leaf dependency."}], + }, + { + "id": "user-action", "label": "User action graph", "color": "#7c3aed", "summary": "How the user moves.", + "nodes": [ + {"id": "surface", "title": "Surface", "kind": "user", "x": -240, "y": -70, "summary": "Where the user starts.", "details": ["Explain the user-facing surface."], "files": [], "comments": [], "links": []}, + {"id": "feedback", "title": "Feedback", "kind": "result", "x": 160, "y": 70, "summary": "What the user sees.", "details": ["Explain the visible result."], "files": [], "comments": [], "links": []}, + ], + "edges": [{"source": "surface", "target": "feedback", "label": "user sees"}], + "tour": [{"nodeId": "surface", "title": "Start at surface", "body": "Begin where the user acts."}, {"nodeId": "feedback", "title": "End at feedback", "body": "End with what the user sees."}], + }, + ], + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="Emit reusable D3 PR walkthrough graph tour snippets.") + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--css", action="store_true", help="Print pr-walkthrough D3 graph CSS.") + group.add_argument("--runtime", action="store_true", help="Print pinned-CDN D3 runtime and graph renderer.") + group.add_argument("--template", action="store_true", help="Print a complete HTML template from graph JSON.") + group.add_argument("--sample-data", action="store_true", help="Print sample graph JSON.") + parser.add_argument("--data", type=Path, help="Graph JSON file for --template. If omitted, sample data is used.") + args = parser.parse_args() + if args.css: + print(d3_canvas_css()) + elif args.runtime: + print(d3_canvas_runtime_script()) + elif args.sample_data: + print(json.dumps(sample_data(), indent=2)) + elif args.template: + data = json.loads(args.data.read_text()) if args.data else sample_data() + print(html_template(data)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py b/.agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py new file mode 100644 index 00000000..78e1fcff --- /dev/null +++ b/.agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +"""Validate a generated pr-walkthrough multi-graph tour HTML canvas.""" + +from __future__ import annotations + +import argparse +import html +import json +import re +from html.parser import HTMLParser +from pathlib import Path + +REQUIRED_GRAPHS = {"system-overview", "data-flow", "code-dependency", "user-action"} +DIRECTED_EDGE_GRAPHS = {"data-flow", "code-dependency", "user-action"} +REQUIRED_CONTROLS = ( + "Fit to view", + "Reset zoom", + "System overview", + "Data flow graph", + "Code dependency graph", + "User action graph", + "Previous tour step", + "Next tour step", + "Restart tour", +) + + +class DataScriptExtractor(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.capture = False + self.parts: list[str] = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + if tag == "script" and dict(attrs).get("id") == "pr-walkthrough-data": + self.capture = True + self.parts = [] + + def handle_endtag(self, tag: str) -> None: + if tag == "script" and self.capture: + self.capture = False + + def handle_data(self, data: str) -> None: + if self.capture: + self.parts.append(data) + + +def extract_graph_data(html_text: str) -> dict: + parser = DataScriptExtractor() + parser.feed(html_text) + if parser.parts: + raw = "".join(parser.parts) + try: + return json.loads(raw) + except json.JSONDecodeError: + return json.loads(html.unescape(raw)) + match = re.search(r"window\.PR_WALKTHROUGH_D3_DATA\s*=\s*(\{.*?\});", html_text, re.S) + if not match: + raise ValueError("Missing inline D3 graph data") + return json.loads(match.group(1)) + + +def static_validate(html_text: str, data: dict) -> list[str]: + errors: list[str] = [] + lower = html_text.lower() + if "d3@7.9.0/dist/d3.min.js" not in html_text: + errors.append("HTML does not reference the pinned D3 7.9.0 CDN URL") + if "d3@latest" in lower or "/d3/latest" in lower: + errors.append("HTML uses an unpinned D3 `latest` runtime") + if "fetch(" in html_text: + errors.append("HTML uses fetch(); inline graph data is required for file:// usage") + if 'id="pr-walkthrough-canvas"' not in html_text: + errors.append("Missing #pr-walkthrough-canvas SVG") + if "marker-end" not in html_text or "d3-arrowhead" not in html_text: + errors.append("HTML does not include visible directed edge arrowhead rendering") + for label in REQUIRED_CONTROLS: + if label not in html_text: + errors.append(f"Missing required control label: {label}") + + graphs = data.get("graphs", []) + graph_ids = {graph.get("id") for graph in graphs} + missing = REQUIRED_GRAPHS - graph_ids + if missing: + errors.append(f"Missing required graphs: {', '.join(sorted(missing))}") + extra = graph_ids - REQUIRED_GRAPHS + if extra: + errors.append(f"Graph data includes unexpected graph ids: {', '.join(sorted(str(item) for item in extra))}") + if graph_ids != REQUIRED_GRAPHS: + errors.append("Graph data must include exactly system-overview, data-flow, code-dependency, and user-action") + + for graph in graphs: + graph_id = graph.get("id") + nodes = graph.get("nodes", []) + edges = graph.get("edges", []) + tour = graph.get("tour", []) + if not graph.get("label"): + errors.append(f"Graph {graph_id} missing label") + if not nodes: + errors.append(f"Graph {graph_id} has no nodes") + if graph_id in DIRECTED_EDGE_GRAPHS and not edges: + errors.append(f"Graph {graph_id} has no edges") + if not tour: + errors.append(f"Graph {graph_id} has no guided tour") + node_ids = {node.get("id") for node in nodes} + for node in nodes: + if not node.get("id"): + errors.append(f"Graph {graph_id} has node missing id") + if not node.get("title"): + errors.append(f"Graph {graph_id} node {node.get('id')} missing title") + if not node.get("summary") and not node.get("details"): + errors.append(f"Graph {graph_id} node {node.get('id')} missing explanatory text") + for edge in edges: + if not edge.get("label"): + errors.append(f"Graph {graph_id} edge {edge.get('id') or edge.get('source')} missing directional label") + if edge.get("source") not in node_ids: + errors.append(f"Graph {graph_id} edge references unknown source: {edge.get('source')}") + if edge.get("target") not in node_ids: + errors.append(f"Graph {graph_id} edge references unknown target: {edge.get('target')}") + for index, step in enumerate(tour): + if step.get("nodeId") not in node_ids: + errors.append(f"Graph {graph_id} tour step {index + 1} references unknown node: {step.get('nodeId')}") + if not step.get("title") or not step.get("body"): + errors.append(f"Graph {graph_id} tour step {index + 1} missing title/body") + return errors + + +def browser_validate(html_path: Path, timeout_ms: int) -> tuple[bool, str]: + try: + from playwright.sync_api import sync_playwright + except Exception as exc: + return False, f"Playwright is unavailable: {exc}" + + url = html_path.resolve().as_uri() + with sync_playwright() as playwright: + browser = None + launch_errors: list[str] = [] + for label, kwargs in (("bundled Chromium", {}), ("system Chrome", {"channel": "chrome"}), ("system Chromium", {"channel": "chromium"})): + try: + browser = playwright.chromium.launch(**kwargs) + break + except Exception as exc: + launch_errors.append(f"{label}: {exc}") + if browser is None: + return False, "Unable to launch a Playwright browser. " + " | ".join(launch_errors) + try: + page = browser.new_page(viewport={"width": 1440, "height": 960}) + page.goto(url, wait_until="domcontentloaded", timeout=timeout_ms) + page.wait_for_function(""" + () => document.body.classList.contains('d3-canvas-ready') || + document.body.classList.contains('d3-canvas-error') + """, timeout=timeout_ms) + initial = page.evaluate(""" + () => ({ + ready: document.body.classList.contains('d3-canvas-ready'), + error: document.body.classList.contains('d3-canvas-error'), + nodes: document.querySelectorAll('.d3-node').length, + edges: document.querySelectorAll('.d3-edge').length, + arrows: document.querySelectorAll('.d3-edge path[marker-end]').length, + detailHasContent: Boolean(document.querySelector('#pr-walkthrough-details')?.textContent?.trim()), + tourText: document.querySelector('.d3-tour-step-label')?.textContent || '', + controls: Array.from(document.querySelectorAll('button')).map((button) => button.textContent.trim()), + }) + """) + graph_results = [] + for graph_id in ["system-overview", "data-flow", "code-dependency", "user-action"]: + page.click(f'[data-graph-id="{graph_id}"]') + page.wait_for_timeout(150) + before = page.text_content('.d3-tour-step-label') or '' + page.click('[data-d3-action="tour-next"]') + page.wait_for_timeout(120) + after = page.text_content('.d3-tour-step-label') or '' + graph_results.append(page.evaluate(""" + (args) => ({ + graphId: args.graphId, + before: args.before, + after: args.after, + nodes: document.querySelectorAll('.d3-node').length, + edges: document.querySelectorAll('.d3-edge').length, + arrows: document.querySelectorAll('.d3-edge path[marker-end]').length, + selected: document.querySelectorAll('.d3-node.is-tour-node').length, + pressed: document.querySelector(`[data-graph-id="${args.graphId}"]`)?.getAttribute('aria-pressed'), + }) + """, {"graphId": graph_id, "before": before, "after": after}) ) + except Exception as exc: + return False, f"browser validation failed while loading or inspecting the page: {exc}" + finally: + browser.close() + + if initial["error"] or not initial["ready"]: + return False, "D3 canvas reported an error state" + if initial["nodes"] == 0: + return False, "Initial graph did not render nodes" + if not initial["detailHasContent"]: + return False, "Detail panel did not render content" + missing_controls = [label for label in REQUIRED_CONTROLS if label not in initial["controls"]] + if missing_controls: + return False, f"Missing browser-visible controls: {', '.join(missing_controls)}" + for result in graph_results: + if result["nodes"] == 0: + return False, f"Graph {result['graphId']} did not render nodes" + if result["graphId"] in DIRECTED_EDGE_GRAPHS and result["edges"] == 0: + return False, f"Graph {result['graphId']} did not render directed edges" + if result["graphId"] in DIRECTED_EDGE_GRAPHS and result["arrows"] != result["edges"]: + return False, f"Graph {result['graphId']} did not render an arrowhead for every edge" + if result["selected"] == 0: + return False, f"Graph {result['graphId']} did not mark a tour node" + if result["pressed"] != "true": + return False, f"Graph {result['graphId']} toggle did not become active" + return True, "browser rendered all 4 graphs, directed arrows, and tour controls successfully" + + +def main() -> int: + parser = argparse.ArgumentParser(description="Validate a pr-walkthrough generated HTML file.") + parser.add_argument("--html", required=True, type=Path, help="Path to .aicodingflow/pr-walkthrough/index.html") + parser.add_argument("--require-browser", action="store_true", help="Fail if browser validation cannot be performed.") + parser.add_argument("--timeout-ms", type=int, default=15000, help="Browser validation timeout.") + args = parser.parse_args() + + html_text = args.html.read_text() + data = extract_graph_data(html_text) + errors = static_validate(html_text, data) + if errors: + for error in errors: + print(f"FAIL - {error}") + return 1 + graph_count = len(data.get("graphs", [])) + node_count = sum(len(graph.get("nodes", [])) for graph in data.get("graphs", [])) + edge_count = sum(len(graph.get("edges", [])) for graph in data.get("graphs", [])) + print(f"Static validation passed: {graph_count} graph(s), {node_count} node(s), {edge_count} edge(s).") + ok, message = browser_validate(args.html, args.timeout_ms) + if ok: + print(f"PASS - {message}") + return 0 + prefix = "FAIL" if args.require_browser else "WARN" + print(f"{prefix} - {message}") + return 1 if args.require_browser else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.gitignore b/.gitignore index 4ad53430..f7a46960 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ __pycache__/ .coverage htmlcov/ +.aicodingflow/ + /.worktrees/ From baf2a915bc33adf89131d25554dfa53ff19aa7ca Mon Sep 17 00:00:00 2001 From: "Terry.Mao" Date: Fri, 19 Jun 2026 21:25:55 +0800 Subject: [PATCH 2/6] docs(pr-walkthrough): use temp output paths --- .agents/skills/pr-walkthrough/SKILL.md | 46 ++++++++++++++++---------- .gitignore | 2 -- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/.agents/skills/pr-walkthrough/SKILL.md b/.agents/skills/pr-walkthrough/SKILL.md index e9d5f399..cbb2f387 100644 --- a/.agents/skills/pr-walkthrough/SKILL.md +++ b/.agents/skills/pr-walkthrough/SKILL.md @@ -9,12 +9,19 @@ description: Generate a local static interactive D3 walkthrough of a pull reques ## 输出 -生成文件到: +生成文件到临时目录下的统一 slug 目录。默认本地产物根目录为 `${TMPDIR:-/tmp}/pr-walkthrough`;如果用户明确指定其他目录,可改用指定目录。下文用 `` 表示该目录。 + +优先使用 PR number;无 PR number 时使用当前分支名: ```text -.aicodingflow/pr-walkthrough/index.html +/pr-walkthrough-pr--/graph.json +/pr-walkthrough-pr--/index.html +/pr-walkthrough-branch--/graph.json +/pr-walkthrough-branch--/index.html ``` +`` 是生成 walkthrough 时的 head commit 短 SHA。`` 使用小写字母、数字和 `-`,把 `/`、空格和其他分隔符归一为 `-`。本地目录名和 GitHub Pages 路径必须使用同一个 slug。PR 更新后再次调用会因 short SHA 变化生成新目录;只有用户明确要求更新同一路径时才覆盖旧目录。不要把生成产物写入仓库目录,除非用户明确要求。 + 站点必须可以直接用 `file://` 打开,不要求 dev server、打包器、安装依赖或构建步骤。优先生成单个自包含 HTML 文件,内联 CSS、JavaScript 和图数据;如果必须拆分资源,只使用相对路径,并避免用 `fetch()` 读取本地数据。 D3 使用固定版本的官方 CDN: @@ -26,8 +33,8 @@ https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js 优先使用本技能自带脚本生成和验证页面: ```bash -python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --template --data graph.json > .aicodingflow/pr-walkthrough/index.html -python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html .aicodingflow/pr-walkthrough/index.html --require-browser +python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --template --data //graph.json > //index.html +python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html //index.html --require-browser ``` ## 视觉风格 @@ -88,12 +95,12 @@ gh api repos/:owner/:repo/issues//comments --paginate ### 2. 收集视觉素材 -查找能帮助 reviewer 理解用户可见变化的截图、mock、视频、设计资产或 changed image。来源包括 PR body/comments/reviews、关联 issue、变更的图片/SVG/mock fixture、本地测试截图,以及 `.aicodingflow/` 下已有临时产物。 +查找能帮助 reviewer 理解用户可见变化的截图、mock、视频、设计资产或 changed image。来源包括 PR body/comments/reviews、关联 issue、变更的图片/SVG/mock fixture、本地测试截图,以及 `/` 下已有临时产物。 需要纳入页面的外部视觉素材应下载或导出到: ```text -.aicodingflow/pr-walkthrough/assets/ +//assets/ ``` 用相对路径引用,或在更简单时嵌入为 data URI。不要 hotlink 远端图片。 @@ -171,9 +178,12 @@ Tour 顺序要教 reviewer 从起点读到终点,不要只是文件顺序。 可先生成样例数据,修改为真实 PR 数据,再渲染: ```bash -mkdir -p .aicodingflow/pr-walkthrough -python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --sample-data > .aicodingflow/pr-walkthrough/graph.json -python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --template --data .aicodingflow/pr-walkthrough/graph.json > .aicodingflow/pr-walkthrough/index.html +sha="$(git rev-parse --short HEAD)" +artifact_root="${PR_WALKTHROUGH_ARTIFACT_ROOT:-${TMPDIR:-/tmp}/pr-walkthrough}" +slug="pr-walkthrough-pr--$sha" +mkdir -p "$artifact_root/$slug" +python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --sample-data > "$artifact_root/$slug/graph.json" +python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --template --data "$artifact_root/$slug/graph.json" > "$artifact_root/$slug/index.html" ``` 必备交互: @@ -191,7 +201,7 @@ python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --template -- 完成前必须运行: ```bash -python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html .aicodingflow/pr-walkthrough/index.html --require-browser +python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html //index.html --require-browser ``` 验证至少确认: @@ -210,21 +220,23 @@ python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html .aico ### 8. 可选发布到 GitHub Pages -默认只保留本地产物,不发布公网,也不提交 `.aicodingflow/pr-walkthrough/`。只有用户明确要求公开 URL 时才发布。发布前必须确认 PR 内容、截图、评论和代码上下文可以公开。 +默认只保留临时目录里的本地产物,不发布公网,也不提交生成 HTML。只有用户明确要求公开 URL 时才发布。发布前必须确认 PR 内容、截图、评论和代码上下文可以公开。 推荐使用 `gh-pages` 分支作为 Pages 来源,并把生成站点复制到临时 worktree,避免把生成物混入当前开发分支: ```bash repo="$(gh repo view --json nameWithOwner --jq .nameWithOwner)" sha="$(git rev-parse --short HEAD)" -site_dir="/tmp/aicodingflow-pr-walkthrough-pages-$sha" +artifact_root="${PR_WALKTHROUGH_ARTIFACT_ROOT:-${TMPDIR:-/tmp}/pr-walkthrough}" +slug="pr-walkthrough-pr--$sha" +site_dir="/tmp/aicodingflow-pr-walkthrough-pages-$slug" git fetch origin gh-pages || true git worktree add "$site_dir" gh-pages -mkdir -p "$site_dir/pr-walkthrough/$sha" -cp -R .aicodingflow/pr-walkthrough/. "$site_dir/pr-walkthrough/$sha/" +mkdir -p "$site_dir/pr-walkthrough/$slug" +cp -R "$artifact_root/$slug/." "$site_dir/pr-walkthrough/$slug/" cd "$site_dir" -git add "pr-walkthrough/$sha" -git commit -m "docs: publish PR walkthrough $sha" +git add "pr-walkthrough/$slug" +git commit -m "docs: publish PR walkthrough $slug" git push origin gh-pages ``` @@ -233,7 +245,7 @@ git push origin gh-pages 发布后的 URL 通常为: ```text -https://.github.io//pr-walkthrough// +https://.github.io//pr-walkthrough// ``` ## 最终回复 diff --git a/.gitignore b/.gitignore index f7a46960..4ad53430 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,4 @@ __pycache__/ .coverage htmlcov/ -.aicodingflow/ - /.worktrees/ From 1b1c4ceb3c3017577fbd8553e3be1ab0b4be4f06 Mon Sep 17 00:00:00 2001 From: "Terry.Mao" Date: Fri, 19 Jun 2026 21:31:26 +0800 Subject: [PATCH 3/6] docs(pr-walkthrough): normalize temp artifact path --- .agents/skills/pr-walkthrough/SKILL.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.agents/skills/pr-walkthrough/SKILL.md b/.agents/skills/pr-walkthrough/SKILL.md index cbb2f387..1807cc25 100644 --- a/.agents/skills/pr-walkthrough/SKILL.md +++ b/.agents/skills/pr-walkthrough/SKILL.md @@ -9,7 +9,7 @@ description: Generate a local static interactive D3 walkthrough of a pull reques ## 输出 -生成文件到临时目录下的统一 slug 目录。默认本地产物根目录为 `${TMPDIR:-/tmp}/pr-walkthrough`;如果用户明确指定其他目录,可改用指定目录。下文用 `` 表示该目录。 +生成文件到临时目录下的统一 slug 目录。默认本地产物根目录为 `${TMPDIR%/}/pr-walkthrough`(`TMPDIR` 为空时使用 `/tmp/pr-walkthrough`);如果用户明确指定其他目录,可改用指定目录。下文用 `` 表示该目录。 优先使用 PR number;无 PR number 时使用当前分支名: @@ -179,7 +179,9 @@ Tour 顺序要教 reviewer 从起点读到终点,不要只是文件顺序。 ```bash sha="$(git rev-parse --short HEAD)" -artifact_root="${PR_WALKTHROUGH_ARTIFACT_ROOT:-${TMPDIR:-/tmp}/pr-walkthrough}" +tmp_root="${TMPDIR:-/tmp}" +tmp_root="${tmp_root%/}" +artifact_root="${PR_WALKTHROUGH_ARTIFACT_ROOT:-$tmp_root/pr-walkthrough}" slug="pr-walkthrough-pr--$sha" mkdir -p "$artifact_root/$slug" python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --sample-data > "$artifact_root/$slug/graph.json" @@ -227,7 +229,9 @@ python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html Date: Sat, 20 Jun 2026 00:49:48 +0800 Subject: [PATCH 4/6] docs(pr-walkthrough): centralize output path setup --- .agents/skills/pr-walkthrough/SKILL.md | 82 +++++++++++++++----------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/.agents/skills/pr-walkthrough/SKILL.md b/.agents/skills/pr-walkthrough/SKILL.md index 1807cc25..47fb0247 100644 --- a/.agents/skills/pr-walkthrough/SKILL.md +++ b/.agents/skills/pr-walkthrough/SKILL.md @@ -9,15 +9,13 @@ description: Generate a local static interactive D3 walkthrough of a pull reques ## 输出 -生成文件到临时目录下的统一 slug 目录。默认本地产物根目录为 `${TMPDIR%/}/pr-walkthrough`(`TMPDIR` 为空时使用 `/tmp/pr-walkthrough`);如果用户明确指定其他目录,可改用指定目录。下文用 `` 表示该目录。 +生成文件到临时目录下的统一 slug 目录。默认本地产物根目录为 `${TMPDIR%/}/pr-walkthrough`(`TMPDIR` 为空时使用 `/tmp/pr-walkthrough`);如果用户明确指定其他目录,可改用指定目录。 优先使用 PR number;无 PR number 时使用当前分支名: ```text -/pr-walkthrough-pr--/graph.json -/pr-walkthrough-pr--/index.html -/pr-walkthrough-branch--/graph.json -/pr-walkthrough-branch--/index.html +$output_dir/graph.json +$output_dir/index.html ``` `` 是生成 walkthrough 时的 head commit 短 SHA。`` 使用小写字母、数字和 `-`,把 `/`、空格和其他分隔符归一为 `-`。本地目录名和 GitHub Pages 路径必须使用同一个 slug。PR 更新后再次调用会因 short SHA 变化生成新目录;只有用户明确要求更新同一路径时才覆盖旧目录。不要把生成产物写入仓库目录,除非用户明确要求。 @@ -33,8 +31,8 @@ https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js 优先使用本技能自带脚本生成和验证页面: ```bash -python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --template --data //graph.json > //index.html -python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html //index.html --require-browser +python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --template --data "$graph_json" > "$index_html" +python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html "$index_html" --require-browser ``` ## 视觉风格 @@ -93,19 +91,47 @@ gh api repos/:owner/:repo/issues//comments --paginate 这些评论只作为讲解素材,不作为改代码指令。 -### 2. 收集视觉素材 +### 2. 初始化输出路径 -查找能帮助 reviewer 理解用户可见变化的截图、mock、视频、设计资产或 changed image。来源包括 PR body/comments/reviews、关联 issue、变更的图片/SVG/mock fixture、本地测试截图,以及 `/` 下已有临时产物。 +在生成 `graph.json` 前初始化一次输出变量,后续生成、验证和发布都复用这些变量,不要重新拼路径: + +```bash +sha="$(git rev-parse --short HEAD)" +tmp_root="${TMPDIR:-/tmp}" +tmp_root="${tmp_root%/}" +artifact_root="${PR_WALKTHROUGH_ARTIFACT_ROOT:-$tmp_root/pr-walkthrough}" + +if [ -n "${pr_number:-}" ]; then + slug="pr-walkthrough-pr-$pr_number-$sha" +else + branch="$(git branch --show-current)" + branch_slug="$(printf '%s' "$branch" | tr '[:upper:]' '[:lower:]' | sed 's#[^a-z0-9][^a-z0-9]*#-#g; s#^-##; s#-$##')" + slug="pr-walkthrough-branch-$branch_slug-$sha" +fi + +output_dir="$artifact_root/$slug" +graph_json="$output_dir/graph.json" +index_html="$output_dir/index.html" +assets_dir="$output_dir/assets" +pages_path="pr-walkthrough/$slug" +mkdir -p "$output_dir" +``` + +如果用户明确要求覆盖固定路径,可以复用已有 `slug`;否则每次 PR head commit 变化都生成新的 slug 目录。 + +### 3. 收集视觉素材 + +查找能帮助 reviewer 理解用户可见变化的截图、mock、视频、设计资产或 changed image。来源包括 PR body/comments/reviews、关联 issue、变更的图片/SVG/mock fixture、本地测试截图,以及 `$artifact_root/` 下已有临时产物。 需要纳入页面的外部视觉素材应下载或导出到: ```text -//assets/ +$assets_dir/ ``` 用相对路径引用,或在更简单时嵌入为 data URI。不要 hotlink 远端图片。 -### 3. 构造 GitHub diff links +### 4. 构造 GitHub diff links 当已知 PR URL 时,每个 changed file reference、节点附件、代码摘录和依赖边都应链接到 PR 的 Files changed 页: @@ -117,7 +143,7 @@ gh api repos/:owner/:repo/issues//comments --paginate `` 是变更文件路径的 lowercase SHA-256 hex digest。用确定性 helper 或脚本生成,不要手写猜测。 -### 4. 设计四个独立视图 +### 5. 设计四个独立视图 先构建数据模型,再生成 HTML。必须恰好包含四个视图: @@ -137,7 +163,7 @@ gh api repos/:owner/:repo/issues//comments --paginate Tour 顺序要教 reviewer 从起点读到终点,不要只是文件顺序。 -### 5. 写入数据模型 +### 6. 写入数据模型 将图数据内联到 HTML,赋值给 `window.PR_WALKTHROUGH_D3_DATA` 或写入 `id="pr-walkthrough-data"` 的 JSON script。不要用 `fetch()` 加载本地 JSON。 @@ -173,19 +199,13 @@ Tour 顺序要教 reviewer 从起点读到终点,不要只是文件顺序。 - 相关节点靠近,低层依赖放在调用者右侧或下方。 - 小 PR 的图应紧凑到无需大量平移即可读懂。 -### 6. 生成静态页面 +### 7. 生成静态页面 可先生成样例数据,修改为真实 PR 数据,再渲染: ```bash -sha="$(git rev-parse --short HEAD)" -tmp_root="${TMPDIR:-/tmp}" -tmp_root="${tmp_root%/}" -artifact_root="${PR_WALKTHROUGH_ARTIFACT_ROOT:-$tmp_root/pr-walkthrough}" -slug="pr-walkthrough-pr--$sha" -mkdir -p "$artifact_root/$slug" -python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --sample-data > "$artifact_root/$slug/graph.json" -python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --template --data "$artifact_root/$slug/graph.json" > "$artifact_root/$slug/index.html" +python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --sample-data > "$graph_json" +python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --template --data "$graph_json" > "$index_html" ``` 必备交互: @@ -198,12 +218,12 @@ python3 .agents/skills/pr-walkthrough/scripts/d3_canvas_runtime.py --template -- - 键盘支持:Right Arrow/`n`、Left Arrow/`p`、`1`-`4`、`+`/`=`、`-`、`0`、`f`、`/`、`Escape`。 - 稳定的 `data-graph-id`、`data-node-id`、`data-edge-id`、`data-tour-index` 属性,方便自动化截图和验证。 -### 7. 验证 +### 8. 验证 完成前必须运行: ```bash -python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html //index.html --require-browser +python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html "$index_html" --require-browser ``` 验证至少确认: @@ -220,26 +240,20 @@ python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html Date: Fri, 19 Jun 2026 17:03:35 +0000 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=E5=88=9D=E5=A7=8B=E5=8C=96=20pr=20w?= =?UTF-8?q?alkthrough=20=E7=9A=84=20PR=20=E7=BC=96=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/pr-walkthrough/SKILL.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.agents/skills/pr-walkthrough/SKILL.md b/.agents/skills/pr-walkthrough/SKILL.md index 47fb0247..03595744 100644 --- a/.agents/skills/pr-walkthrough/SKILL.md +++ b/.agents/skills/pr-walkthrough/SKILL.md @@ -51,10 +51,19 @@ python3 .agents/skills/pr-walkthrough/scripts/validate_d3_canvas.py --html "$ind ### 1. 建立 PR 上下文 -识别仓库根目录、当前分支和比较 base。若当前分支已有 GitHub PR,优先使用 PR base,并记录 PR URL 生成 diff links: +识别仓库根目录、当前分支和比较 base。若用户 Prompt 明确给出 PR number、`#` 或 PR URL,先把该编号记录为 `pr_number`。否则从当前分支的 GitHub PR 获取 `pr_number`,并优先使用 PR base、记录 PR URL 生成 diff links: ```bash -gh pr view --json baseRefName,headRefName,title,body,url,state,reviewRequests,reviews,files +pr_number="${pr_number:-}" # 用户 Prompt 已明确给出 PR 编号时,先由执行者设置这个变量。 +if [ -z "$pr_number" ]; then + pr_number="$(gh pr view --json number --jq '.number // empty' 2>/dev/null || true)" +fi + +if [ -n "$pr_number" ]; then + gh pr view "$pr_number" --json number,baseRefName,headRefName,title,body,url,state,reviewRequests,reviews,files +else + gh pr view --json number,baseRefName,headRefName,title,body,url,state,reviewRequests,reviews,files +fi ``` 若没有 PR,从远端默认分支或仓库约定推断 base: From 01ecaf7e406a6762dd71417ece522b7f84faeda5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:10:11 +0000 Subject: [PATCH 6/6] fix: align pr walkthrough temp path docs --- .agents/skills/pr-walkthrough/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/pr-walkthrough/SKILL.md b/.agents/skills/pr-walkthrough/SKILL.md index 03595744..d2228230 100644 --- a/.agents/skills/pr-walkthrough/SKILL.md +++ b/.agents/skills/pr-walkthrough/SKILL.md @@ -9,7 +9,7 @@ description: Generate a local static interactive D3 walkthrough of a pull reques ## 输出 -生成文件到临时目录下的统一 slug 目录。默认本地产物根目录为 `${TMPDIR%/}/pr-walkthrough`(`TMPDIR` 为空时使用 `/tmp/pr-walkthrough`);如果用户明确指定其他目录,可改用指定目录。 +生成文件到临时目录下的统一 slug 目录。默认本地产物根目录为 `${TMPDIR:-/tmp}/pr-walkthrough`(并去掉末尾 `/`);如果用户明确指定其他目录,可改用指定目录。 优先使用 PR number;无 PR number 时使用当前分支名: