diff --git a/skills/product-prototype/README.md b/skills/product-prototype/README.md new file mode 100644 index 0000000..ac0d894 --- /dev/null +++ b/skills/product-prototype/README.md @@ -0,0 +1,139 @@ +# product-prototype + +可复用的离线 Product Prototype Studio Skill:用文件型需求、动线、布局和评审数据驱动 React + +TypeScript + React Flow 原型。 + +## 安装 + +```bash +pouch add product-prototype # 当前项目 +pouch add product-prototype -g # 全局 +``` + +Skill 安装后,从 Skill 目录运行初始化脚本。使用仓库 checkout 时命令如下: + +```bash +python3 skills/product-prototype/scripts/init.py ./prototype-studio +``` + +全局安装后可使用 `~/.pouch/skills/product-prototype/scripts/init.py`。初始化脚本是安全的 +补齐操作:目标中已有文件会被保留,并在输出中标记为 `skip`。 + +## 使用前准备 + +- Node.js 20+ 与 npm 10+(模板声明 npm,不依赖 pnpm/yarn)。 +- 一份产品目标、核心场景、页面/状态和关键交互说明。 +- 运行时不需要后端、账号、真实设备或网络资源;依赖安装由目标项目自行决定。 + +模板是可运行的离线 Studio,不是只读的文件占位:它包含 React Flow 画布、共享组件/Action +registry、flow 驱动的 NavigationRuntime、layout 保存/恢复、review 批注导出和确定性设备管理 +mock。四类源数据仍是唯一事实来源;浏览器产物和构建目录不回写源数据。 + +## 初始化后的目录 + +```text +prototype-studio/ +├── prototype/ # 源数据:requirements/flow/layout/review +├── src/ +│ ├── App.tsx # React Flow Studio 与 runtime preview +│ ├── contracts/ # 四类源数据 TypeScript 契约 +│ ├── data/ # 稳定 ID 数据解析入口 +│ ├── layout/ # layout.json round-trip 与显式导出/恢复 +│ ├── registry/ # 页面/Action 注册表唯一入口 +│ ├── runtime/ # navigation runtime 与结构化条件 +│ ├── studio/ # React Flow 画布说明 +│ └── review/ # review.json 校验与导出 +├── scripts/ # 命令、离线契约测试与增量回归 +├── public/static/ # 只放本地、可版本化静态资源 +├── package.json +└── package-lock.json +``` + +数据职责、ID 规则和边界见 [`references/data-contract.md`](references/data-contract.md)。注册表和 +导航约束见 [`references/registry.md`](references/registry.md),命令/报告见 +[`references/commands.md`](references/commands.md),增量修改见 +[`references/incremental.md`](references/incremental.md),截图和报告见 +[`references/artifacts.md`](references/artifacts.md)。 + +## 使用示例 + +```text +请用 product-prototype 初始化一个设备管理原型,先保留 requirements、flow、layout、review +四类源数据和稳定 ID;生成离线模板和设备管理 mock,不连接真实设备,并按实际环境记录浏览器验证结果。 +``` + +初始化后: + +```bash +cd prototype-studio +npm install # 安装 package-lock 锁定的依赖 +npm run init # 幂等补齐当前项目;已有文件只报告 skip +npm run dev # 启动 Vite(加载/依赖错误原样可见) +npm run validate -- --data-dir prototype +npm run snapshot # 启动 Vite + Chrome,写入 .prototype-artifacts/snapshots +npm run build # TypeScript noEmit + Vite production build +npm run test:incremental # 离线 flow/layout/review 新增、修改、删除边界检查 +npm test # 离线合约 + mock + Playwright 关键交互与三视口 smoke +``` + +`snapshot` 使用实际 Vite 页面和可发现的 Chrome,生成真实 PNG 及一一对应 manifest;浏览器 +缺失、页面加载失败或产物不可写都会非零退出。Playwright 报告写入 +`.prototype-artifacts/playwright-report`。测试只使用确定性本地 mock,不连接真实设备/后端。Playwright +使用一个 worker 显式覆盖 desktop/tablet/mobile;浏览器缺失、服务端口冲突、页面加载或断言失败 +都必须记录为失败,不应跳过。 + +## 四类数据与审核 + +- `requirements.md` 描述目标、场景和验收上下文,供作者、Agent 和评审阅读。 +- `flow.json` 声明 page/dialog/state 节点及 Action/条件边,是导航唯一事实来源;组件只能发出 + registry 中的 Action ID。 +- `layout.json` 独立保存 React Flow 坐标和 viewport。Studio 只在作者点击 `Save layout.json` + 时导出,`Restore layout.json` 会先校验稳定 ID,不接受悬空节点。 +- `review.json` 保存审核状态和绑定到 node/edge/page ID 的批注;导出前会再次拒绝悬空目标。 + +设备管理示例覆盖列表、新建、配置、连接中、成功、失败重试、详情和空列表,数据来自 +`src/mocks/device-management.ts`,不访问真实设备、后端、凭据或 localStorage。 + +## 增量修改与删除边界 + +新增页面或 Action 时只添加对应节点/边、layout 坐标、相关 review 和实现;修改文案/组件时只 +更新对应稳定 ID 及其相关截图/批注;删除前先检查并报告入边、出边、layout 坐标和 review 目标, +删除后不得留下悬空引用。数组排序或重新渲染不能造成无关 ID 重编号,冲突必须显式失败。 + +模板提供可离线复核的 fixture 检查: + +```bash +cd prototype-studio +npm run test:incremental +``` + +它同时比较 flow、layout、review 的 add/change/delete 结果,并断言 `page.keep`、无关坐标和 +`comment.keep` 等未受影响记录保持不变;该检查是边界回归,不替代真实浏览器回归。 + +## 如何判断完成 + +至少应完成:`init` 幂等、`validate` 通过合法 fixture 且拒绝非法 fixture、增量检查通过、 +`build` 通过,以及按实际环境记录 `snapshot`/Playwright 结果。真实截图必须是非空 PNG 且与 +manifest 一一对应;受限宿主环境不能被写成浏览器测试通过。 + +## 故障排查 + +- `init` 报目标不可写:换到可写的空目录,或修正目标目录权限;脚本不会提权或覆盖文件。 +- `check_structure.py` 或 `check_docs.py` 报错:确认从仓库根目录运行,或用 `--skill-dir` 指向完整 + Skill 目录;前者检查结构/链接,后者检查实现能力文档和过时占位措辞。 +- npm 依赖安装失败:记录 Node/npm 版本和完整错误;不要把网络安装失败改写成模板验证通过。 +- `BROWSER_MISSING`:安装 Chrome 或设置 `PROTOTYPE_CHROME_PATH=/绝对路径/google-chrome`,再重跑 + `npm run snapshot` / `npm test`;不会跳过浏览器测试。 +- `SNAPSHOT_FAILED`:检查 `.prototype-artifacts` 父目录可写、Vite 端口 4173 可用和页面 + `data-testid`/flow 稳定 ID;Vite 使用 strict port,失败不会发布新的 generated manifest。 +- `DEV_SERVER_TIMEOUT`、`DEV_SERVER_EXITED` 或 Playwright 残留:先停止占用 4173 的旧进程,再 + 重跑;snapshot/test 会在退出时关闭浏览器和 Vite,清理失败时应记录宿主环境状态。 +- 画布没有业务节点:检查 `prototype/flow.json`、`layout.json` 和 registry 是否被 validate + 接受;模板不从 localStorage 或后端补数据。 + +## 安全与回滚 + +模板默认不读取凭据、不访问网络、不连接真实设备,也不执行条件字符串。增量修改前保留 Git +提交或复制源数据作为回滚点;回滚应恢复同一批 requirements/flow/layout/review 与相关实现, +不要只回滚截图或构建产物。若模板不适用,移除或回退 `skills/product-prototype/` 即可;不要 +修改 Agent 指令、其它 skill 或真实业务后端配置。 diff --git a/skills/product-prototype/SKILL.md b/skills/product-prototype/SKILL.md new file mode 100644 index 0000000..a0bfd1e --- /dev/null +++ b/skills/product-prototype/SKILL.md @@ -0,0 +1,73 @@ +--- +name: product-prototype +description: >- + 将产品需求整理为可校验的文件型 prototype 数据,并复制或维护 React + TypeScript + React Flow + Prototype Studio。用户提出做高保真原型、页面动线、原型画布、React Flow 原型、交互预览、 + 原型截图/评审、requirements.md/flow.json/layout.json/review.json,或要求初始化可复用 + Prototype Studio 模板时使用。不要用于真实后端、真实设备连接、生产 UI 编辑器或 Figma 双向同步。 +--- + +# Product Prototype + +把需求、页面关系、布局和评审结论放进可版本化的 prototype 数据,再由本地 Prototype Studio +读取这些数据。Skill 的规范和模板位于本目录;项目初始化只复制模板和项目源数据,不在 Agent +目录写入普通 skill 文件。 + +## 何时使用 + +- 从产品需求建立 React + TypeScript + React Flow 原型项目。 +- 需要页面、弹窗、状态节点和带 Action/条件的页面动线。 +- 需要统一的 `requirements.md`、`flow.json`、`layout.json`、`review.json` 文件契约。 +- 需要离线、确定性 mock 的原型预览、截图和评审记录。 + +## 不适用 + +- 真实设备、真实后端 API、账号权限、云端同步或线上部署。 +- Figma 导入导出、多人协作或浏览器内生产级拖拽编辑器。 +- 只有一张插画/营销页,或仅需要低保真 Draw.io 流程图(使用 `draw-prototype-flow`)。 + +## 当前能力与执行边界 + +1. 开始前读取 `references/data-contract.md`,并根据需要读取 `registry.md`、`commands.md`、 + `incremental.md` 和 `artifacts.md`。 +2. 使用 `scripts/init.py` 从 `assets/studio-template/` 复制项目;默认只创建缺失文件,绝不覆盖 + 用户已有的 requirements、flow、layout、review 或实现文件。模板内的 `npm run init` 提供相同的 + 幂等补齐能力。 +3. `flow.json` 是导航唯一事实来源。页面组件只能调用注册表中的 Action ID;不得在组件里维护 + 第二套页面跳转映射。 +4. 条件只能使用结构化白名单规则;禁止 `eval`、`Function` 或执行任意字符串表达式。 +5. 当前模板已实现 React Flow Studio、共享 registry、NavigationRuntime、四类源数据 validator、 + 确定性设备 mock,以及 `init`/`dev`/`validate`/`snapshot`/`build`/`test` 命令。命令失败时 + 使用稳定错误码和非零退出码,不把缺浏览器、页面失败或宿主环境问题伪装成通过。 +6. `snapshot` 使用真实 Vite 页面和可发现的 Chrome 生成 PNG 与 manifest;`test` 运行离线合约、 + mock 检查和 Playwright 交互/三视口检查。测试产物只写入 `.prototype-artifacts/`。 +7. 增量变更按稳定 ID 维护 flow、layout、review 和相关实现/截图;新增、修改、删除都必须同步 + 引用并做离线 diff,不能重编号或漂移无关节点。删除前要报告受影响的边、布局和批注。 + +## 输入与输出 + +- 输入:需求目标、用户场景、页面/状态清单、关键交互、验收上下文和可选的现有项目路径。 +- 源数据:项目目录中的 `prototype/requirements.md`、`flow.json`、`layout.json`、`review.json`。 +- 输出:可复制的 npm 项目、可追踪的稳定 ID、离线文档、可运行的 Studio/runtime 预览和可复核 + 的截图/测试产物。 +- 生成物:截图、报告和测试输出只能写入项目约定的 artifacts 目录;它们不是源数据。 + +## 路由 + +- **初始化**:读取 `references/commands.md`,运行 `python3 scripts/init.py `;先检查目标, + 只补齐缺失文件,并报告 skipped/created。 +- **检查**:运行 `python3 scripts/check_structure.py` 与 `python3 scripts/check_docs.py`,再按模板项目 + README 的命令检查;需要浏览器的命令必须记录浏览器、页面和产物结果,不得伪造截图。 +- **增量回归**:在模板根目录运行 `npm run test:incremental`,检查 flow/layout/review 的新增、 + 修改、删除 diff,并确认无关稳定 ID、坐标和批注保留。 +- **实现/扩展**:先读对应 reference;所有页面与 Action 仍通过共享 registry 和 flow 驱动,禁止 + 在业务组件内新增第二套导航映射。真实后端、设备或外部服务需求先停下说明。 + +## 完成标准 + +- `SKILL.md` 的 frontmatter 名称与目录一致,README 和 references 链接可读。 +- 模板有锁定的 npm 依赖声明、React/TypeScript/React Flow 入口、本地样式和四类源数据;Studio + 可查看节点/边、预览 runtime、保存/恢复 layout 并导出 review。 +- `init` 重复运行不会覆盖已有源数据;静态检查失败时返回非零码并指出文件。 +- `validate`、离线合约、增量检查和 TypeScript/Vite build 可复核;浏览器截图与 Playwright 结果 + 必须按实际环境报告,未运行或因宿主环境失败的项目只能记录为 test gap。 diff --git a/skills/product-prototype/assets/studio-template/.gitignore b/skills/product-prototype/assets/studio-template/.gitignore new file mode 100644 index 0000000..8060b94 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +test-results/ +.prototype-artifacts/ diff --git a/skills/product-prototype/assets/studio-template/fixtures/README.md b/skills/product-prototype/assets/studio-template/fixtures/README.md new file mode 100644 index 0000000..475eff8 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/README.md @@ -0,0 +1,11 @@ +# Validator and incremental fixtures + +Each directory is a complete `prototype/`-shaped data set. Run +`npm run validate -- --data-dir fixtures/valid` for the passing fixture; the +`invalid-*` fixtures must fail with the named stable ID and error code. + +`incremental/` contains the original stable-ID layout base/patch/expected files plus a flow/layout/review +add-change-delete fixture. Run `npm run test:incremental` from the template root to verify that only +declared IDs change, deleted IDs disappear, new IDs are present, and unrelated layout/review records stay +unchanged. The patch's `expected` coordinate makes concurrent edits fail closed rather than overwriting a +changed coordinate. diff --git a/skills/product-prototype/assets/studio-template/fixtures/incremental/base-flow.json b/skills/product-prototype/assets/studio-template/fixtures/incremental/base-flow.json new file mode 100644 index 0000000..1186d25 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/incremental/base-flow.json @@ -0,0 +1,14 @@ +{ + "flowVersion": 1, + "startNodeId": "page.keep", + "nodes": [ + { "id": "page.keep", "kind": "page", "component": "StudioPlaceholder", "title": "Keep" }, + { "id": "page.change", "kind": "page", "component": "StudioPlaceholder", "title": "Before" }, + { "id": "page.delete", "kind": "page", "component": "StudioPlaceholder", "title": "Remove" } + ], + "edges": [ + { "id": "edge.keep", "source": "page.keep", "target": "page.change", "actionId": "action.studio.noop", "label": "Keep" }, + { "id": "edge.change", "source": "page.change", "target": "page.delete", "actionId": "action.studio.noop", "label": "Before" }, + { "id": "edge.delete", "source": "page.delete", "target": "page.keep", "actionId": "action.studio.noop", "label": "Remove" } + ] +} diff --git a/skills/product-prototype/assets/studio-template/fixtures/incremental/base-layout-scope.json b/skills/product-prototype/assets/studio-template/fixtures/incremental/base-layout-scope.json new file mode 100644 index 0000000..47ef697 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/incremental/base-layout-scope.json @@ -0,0 +1,9 @@ +{ + "layoutVersion": 1, + "viewport": { "x": 0, "y": 0, "zoom": 1 }, + "nodes": { + "page.keep": { "x": 100, "y": 200 }, + "page.change": { "x": 300, "y": 400 }, + "page.delete": { "x": 500, "y": 600 } + } +} diff --git a/skills/product-prototype/assets/studio-template/fixtures/incremental/base-layout.json b/skills/product-prototype/assets/studio-template/fixtures/incremental/base-layout.json new file mode 100644 index 0000000..caf17fc --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/incremental/base-layout.json @@ -0,0 +1 @@ +{"layoutVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":{"page.start":{"x":10,"y":20},"page.keep":{"x":300,"y":400}}} diff --git a/skills/product-prototype/assets/studio-template/fixtures/incremental/base-review.json b/skills/product-prototype/assets/studio-template/fixtures/incremental/base-review.json new file mode 100644 index 0000000..c20de92 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/incremental/base-review.json @@ -0,0 +1,9 @@ +{ + "reviewVersion": 1, + "status": "draft", + "comments": [ + { "id": "comment.keep", "body": "Unrelated comment", "target": { "kind": "node", "id": "page.keep" } }, + { "id": "comment.change", "body": "Before", "target": { "kind": "node", "id": "page.change" } }, + { "id": "comment.delete", "body": "Remove", "target": { "kind": "node", "id": "page.delete" } } + ] +} diff --git a/skills/product-prototype/assets/studio-template/fixtures/incremental/expected-flow.json b/skills/product-prototype/assets/studio-template/fixtures/incremental/expected-flow.json new file mode 100644 index 0000000..b27d83e --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/incremental/expected-flow.json @@ -0,0 +1,14 @@ +{ + "flowVersion": 1, + "startNodeId": "page.keep", + "nodes": [ + { "id": "page.keep", "kind": "page", "component": "StudioPlaceholder", "title": "Keep" }, + { "id": "page.change", "kind": "page", "component": "StudioPlaceholder", "title": "After" }, + { "id": "page.add", "kind": "state", "component": "StudioPlaceholder", "title": "Added" } + ], + "edges": [ + { "id": "edge.keep", "source": "page.keep", "target": "page.change", "actionId": "action.studio.noop", "label": "Keep" }, + { "id": "edge.change", "source": "page.change", "target": "page.add", "actionId": "action.studio.noop", "label": "After" }, + { "id": "edge.add", "source": "page.add", "target": "page.keep", "actionId": "action.studio.noop", "label": "Added" } + ] +} diff --git a/skills/product-prototype/assets/studio-template/fixtures/incremental/expected-layout-scope.json b/skills/product-prototype/assets/studio-template/fixtures/incremental/expected-layout-scope.json new file mode 100644 index 0000000..6d8f57f --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/incremental/expected-layout-scope.json @@ -0,0 +1,9 @@ +{ + "layoutVersion": 1, + "viewport": { "x": 0, "y": 0, "zoom": 1 }, + "nodes": { + "page.keep": { "x": 100, "y": 200 }, + "page.change": { "x": 325, "y": 435 }, + "page.add": { "x": 80, "y": 90 } + } +} diff --git a/skills/product-prototype/assets/studio-template/fixtures/incremental/expected-layout.json b/skills/product-prototype/assets/studio-template/fixtures/incremental/expected-layout.json new file mode 100644 index 0000000..e17aaa7 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/incremental/expected-layout.json @@ -0,0 +1 @@ +{"layoutVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":{"page.start":{"x":15,"y":25},"page.keep":{"x":300,"y":400},"page.new":{"x":800,"y":900}}} diff --git a/skills/product-prototype/assets/studio-template/fixtures/incremental/expected-review.json b/skills/product-prototype/assets/studio-template/fixtures/incremental/expected-review.json new file mode 100644 index 0000000..bea8180 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/incremental/expected-review.json @@ -0,0 +1,9 @@ +{ + "reviewVersion": 1, + "status": "draft", + "comments": [ + { "id": "comment.keep", "body": "Unrelated comment", "target": { "kind": "node", "id": "page.keep" } }, + { "id": "comment.change", "body": "After", "target": { "kind": "node", "id": "page.change" } }, + { "id": "comment.add", "body": "Added", "target": { "kind": "node", "id": "page.add" } } + ] +} diff --git a/skills/product-prototype/assets/studio-template/fixtures/incremental/patch-data.json b/skills/product-prototype/assets/studio-template/fixtures/incremental/patch-data.json new file mode 100644 index 0000000..6f83f0b --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/incremental/patch-data.json @@ -0,0 +1,24 @@ +{ + "nodes": { + "update": [{ "id": "page.change", "patch": { "title": "After" } }], + "removeIds": ["page.delete"], + "add": [{ "id": "page.add", "kind": "state", "component": "StudioPlaceholder", "title": "Added" }] + }, + "edges": { + "update": [{ "id": "edge.change", "patch": { "target": "page.add", "label": "After" } }], + "removeIds": ["edge.delete"], + "add": [{ "id": "edge.add", "source": "page.add", "target": "page.keep", "actionId": "action.studio.noop", "label": "Added" }] + }, + "comments": { + "update": [{ "id": "comment.change", "patch": { "body": "After" } }], + "removeIds": ["comment.delete"], + "add": [{ "id": "comment.add", "body": "Added", "target": { "kind": "node", "id": "page.add" } }] + }, + "layout": { + "nodes": { + "page.change": { "x": 325, "y": 435, "expected": { "x": 300, "y": 400 } }, + "page.add": { "x": 80, "y": 90 } + }, + "removeNodeIds": ["page.delete"] + } +} diff --git a/skills/product-prototype/assets/studio-template/fixtures/incremental/patch-layout.json b/skills/product-prototype/assets/studio-template/fixtures/incremental/patch-layout.json new file mode 100644 index 0000000..eb0a376 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/incremental/patch-layout.json @@ -0,0 +1 @@ +{"layoutVersion":1,"nodes":{"page.start":{"x":15,"y":25,"expected":{"x":10,"y":20}},"page.new":{"x":800,"y":900}},"removeNodeIds":["page.deleted"]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-condition/flow.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-condition/flow.json new file mode 100644 index 0000000..c8c666e --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-condition/flow.json @@ -0,0 +1 @@ +{"flowVersion":1,"startNodeId":"page.start","nodes":[{"id":"page.start","kind":"page","component":"StudioPlaceholder","title":"A"},{"id":"page.end","kind":"page","component":"StudioPlaceholder","title":"B"}],"edges":[{"id":"edge.bad-condition","source":"page.start","target":"page.end","actionId":"action.studio.noop","when":"device.status === 'connected'"}]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-condition/layout.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-condition/layout.json new file mode 100644 index 0000000..d9e5fae --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-condition/layout.json @@ -0,0 +1 @@ +{"layoutVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":{}} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-condition/requirements.md b/skills/product-prototype/assets/studio-template/fixtures/invalid-condition/requirements.md new file mode 100644 index 0000000..d90782b --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-condition/requirements.md @@ -0,0 +1,7 @@ +# Prototype requirements +## Goal +conditions +## Scenario +test +## Acceptance context +test diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-condition/review.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-condition/review.json new file mode 100644 index 0000000..d1176c1 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-condition/review.json @@ -0,0 +1 @@ +{"reviewVersion":1,"status":"draft","comments":[]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-dangling-flow/flow.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-dangling-flow/flow.json new file mode 100644 index 0000000..ce46647 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-dangling-flow/flow.json @@ -0,0 +1 @@ +{"flowVersion":1,"startNodeId":"page.start","nodes":[{"id":"page.start","kind":"page","component":"StudioPlaceholder","title":"A"}],"edges":[{"id":"edge.missing","source":"page.start","target":"page.missing","actionId":"action.studio.noop"}]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-dangling-flow/layout.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-dangling-flow/layout.json new file mode 100644 index 0000000..d9e5fae --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-dangling-flow/layout.json @@ -0,0 +1 @@ +{"layoutVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":{}} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-dangling-flow/requirements.md b/skills/product-prototype/assets/studio-template/fixtures/invalid-dangling-flow/requirements.md new file mode 100644 index 0000000..3691047 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-dangling-flow/requirements.md @@ -0,0 +1,7 @@ +# Prototype requirements +## Goal +references +## Scenario +test +## Acceptance context +test diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-dangling-flow/review.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-dangling-flow/review.json new file mode 100644 index 0000000..d1176c1 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-dangling-flow/review.json @@ -0,0 +1 @@ +{"reviewVersion":1,"status":"draft","comments":[]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-duplicate-id/flow.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-duplicate-id/flow.json new file mode 100644 index 0000000..c941ba0 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-duplicate-id/flow.json @@ -0,0 +1 @@ +{"flowVersion":1,"startNodeId":"page.start","nodes":[{"id":"page.start","kind":"page","component":"StudioPlaceholder","title":"A"},{"id":"page.start","kind":"page","component":"StudioPlaceholder","title":"B"}],"edges":[]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-duplicate-id/layout.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-duplicate-id/layout.json new file mode 100644 index 0000000..d9e5fae --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-duplicate-id/layout.json @@ -0,0 +1 @@ +{"layoutVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":{}} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-duplicate-id/requirements.md b/skills/product-prototype/assets/studio-template/fixtures/invalid-duplicate-id/requirements.md new file mode 100644 index 0000000..d6a91fe --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-duplicate-id/requirements.md @@ -0,0 +1,7 @@ +# Prototype requirements +## Goal +duplicate IDs +## Scenario +test +## Acceptance context +test diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-duplicate-id/review.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-duplicate-id/review.json new file mode 100644 index 0000000..d1176c1 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-duplicate-id/review.json @@ -0,0 +1 @@ +{"reviewVersion":1,"status":"draft","comments":[]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-layout-reference/flow.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-layout-reference/flow.json new file mode 100644 index 0000000..9dba0b5 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-layout-reference/flow.json @@ -0,0 +1 @@ +{"flowVersion":1,"startNodeId":"page.start","nodes":[{"id":"page.start","kind":"page","component":"StudioPlaceholder","title":"A"}],"edges":[]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-layout-reference/layout.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-layout-reference/layout.json new file mode 100644 index 0000000..dd1c3d4 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-layout-reference/layout.json @@ -0,0 +1 @@ +{"layoutVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":{"page.missing":{"x":1,"y":2}}} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-layout-reference/requirements.md b/skills/product-prototype/assets/studio-template/fixtures/invalid-layout-reference/requirements.md new file mode 100644 index 0000000..c50ed30 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-layout-reference/requirements.md @@ -0,0 +1,7 @@ +# Prototype requirements +## Goal +layout refs +## Scenario +test +## Acceptance context +test diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-layout-reference/review.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-layout-reference/review.json new file mode 100644 index 0000000..d1176c1 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-layout-reference/review.json @@ -0,0 +1 @@ +{"reviewVersion":1,"status":"draft","comments":[]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-review-reference/flow.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-review-reference/flow.json new file mode 100644 index 0000000..9dba0b5 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-review-reference/flow.json @@ -0,0 +1 @@ +{"flowVersion":1,"startNodeId":"page.start","nodes":[{"id":"page.start","kind":"page","component":"StudioPlaceholder","title":"A"}],"edges":[]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-review-reference/layout.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-review-reference/layout.json new file mode 100644 index 0000000..d9e5fae --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-review-reference/layout.json @@ -0,0 +1 @@ +{"layoutVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":{}} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-review-reference/requirements.md b/skills/product-prototype/assets/studio-template/fixtures/invalid-review-reference/requirements.md new file mode 100644 index 0000000..ad729f1 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-review-reference/requirements.md @@ -0,0 +1,7 @@ +# Prototype requirements +## Goal +review refs +## Scenario +test +## Acceptance context +test diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-review-reference/review.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-review-reference/review.json new file mode 100644 index 0000000..4f0331d --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-review-reference/review.json @@ -0,0 +1 @@ +{"reviewVersion":1,"status":"draft","comments":[{"id":"comment.missing","body":"Missing target","target":{"kind":"node","id":"page.missing"}}]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-action/flow.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-action/flow.json new file mode 100644 index 0000000..9ef175a --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-action/flow.json @@ -0,0 +1 @@ +{"flowVersion":1,"startNodeId":"page.start","nodes":[{"id":"page.start","kind":"page","component":"StudioPlaceholder","title":"A"},{"id":"page.end","kind":"page","component":"StudioPlaceholder","title":"B"}],"edges":[{"id":"edge.unknown-action","source":"page.start","target":"page.end","actionId":"action.missing"}]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-action/layout.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-action/layout.json new file mode 100644 index 0000000..d9e5fae --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-action/layout.json @@ -0,0 +1 @@ +{"layoutVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":{}} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-action/requirements.md b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-action/requirements.md new file mode 100644 index 0000000..bb7fdad --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-action/requirements.md @@ -0,0 +1,7 @@ +# Prototype requirements +## Goal +registry +## Scenario +test +## Acceptance context +test diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-action/review.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-action/review.json new file mode 100644 index 0000000..d1176c1 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-action/review.json @@ -0,0 +1 @@ +{"reviewVersion":1,"status":"draft","comments":[]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-component/flow.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-component/flow.json new file mode 100644 index 0000000..463465f --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-component/flow.json @@ -0,0 +1 @@ +{"flowVersion":1,"startNodeId":"page.start","nodes":[{"id":"page.start","kind":"page","component":"ComponentMissing","title":"A"}],"edges":[]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-component/layout.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-component/layout.json new file mode 100644 index 0000000..d9e5fae --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-component/layout.json @@ -0,0 +1 @@ +{"layoutVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":{}} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-component/requirements.md b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-component/requirements.md new file mode 100644 index 0000000..bb7fdad --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-component/requirements.md @@ -0,0 +1,7 @@ +# Prototype requirements +## Goal +registry +## Scenario +test +## Acceptance context +test diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-component/review.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-component/review.json new file mode 100644 index 0000000..d1176c1 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-unknown-component/review.json @@ -0,0 +1 @@ +{"reviewVersion":1,"status":"draft","comments":[]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-unreachable/flow.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-unreachable/flow.json new file mode 100644 index 0000000..85227f0 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-unreachable/flow.json @@ -0,0 +1 @@ +{"flowVersion":1,"startNodeId":"page.start","nodes":[{"id":"page.start","kind":"page","component":"StudioPlaceholder","title":"A"},{"id":"page.orphan","kind":"page","component":"StudioPlaceholder","title":"Orphan"}],"edges":[]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-unreachable/layout.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-unreachable/layout.json new file mode 100644 index 0000000..d9e5fae --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-unreachable/layout.json @@ -0,0 +1 @@ +{"layoutVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":{}} diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-unreachable/requirements.md b/skills/product-prototype/assets/studio-template/fixtures/invalid-unreachable/requirements.md new file mode 100644 index 0000000..a40b66a --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-unreachable/requirements.md @@ -0,0 +1,7 @@ +# Prototype requirements +## Goal +reachability +## Scenario +test +## Acceptance context +test diff --git a/skills/product-prototype/assets/studio-template/fixtures/invalid-unreachable/review.json b/skills/product-prototype/assets/studio-template/fixtures/invalid-unreachable/review.json new file mode 100644 index 0000000..d1176c1 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/invalid-unreachable/review.json @@ -0,0 +1 @@ +{"reviewVersion":1,"status":"draft","comments":[]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/valid/flow.json b/skills/product-prototype/assets/studio-template/fixtures/valid/flow.json new file mode 100644 index 0000000..8ae3b95 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/valid/flow.json @@ -0,0 +1 @@ +{"flowVersion":1,"startNodeId":"page.studio-placeholder","nodes":[{"id":"page.studio-placeholder","kind":"page","component":"StudioPlaceholder","title":"Studio"}],"edges":[]} diff --git a/skills/product-prototype/assets/studio-template/fixtures/valid/layout.json b/skills/product-prototype/assets/studio-template/fixtures/valid/layout.json new file mode 100644 index 0000000..b763d2c --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/valid/layout.json @@ -0,0 +1 @@ +{"layoutVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":{"page.studio-placeholder":{"x":80,"y":80}}} diff --git a/skills/product-prototype/assets/studio-template/fixtures/valid/requirements.md b/skills/product-prototype/assets/studio-template/fixtures/valid/requirements.md new file mode 100644 index 0000000..463721d --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/valid/requirements.md @@ -0,0 +1,13 @@ +# Prototype requirements + +## Goal + +Keep a versioned prototype graph. + +## Scenario + +An author validates source data offline. + +## Acceptance context + +Validation is deterministic and reports stable IDs. diff --git a/skills/product-prototype/assets/studio-template/fixtures/valid/review.json b/skills/product-prototype/assets/studio-template/fixtures/valid/review.json new file mode 100644 index 0000000..d1176c1 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/fixtures/valid/review.json @@ -0,0 +1 @@ +{"reviewVersion":1,"status":"draft","comments":[]} diff --git a/skills/product-prototype/assets/studio-template/index.html b/skills/product-prototype/assets/studio-template/index.html new file mode 100644 index 0000000..8249db9 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/index.html @@ -0,0 +1,13 @@ + + + + + + + Prototype Studio + + +
+ + + diff --git a/skills/product-prototype/assets/studio-template/package-lock.json b/skills/product-prototype/assets/studio-template/package-lock.json new file mode 100644 index 0000000..106a30e --- /dev/null +++ b/skills/product-prototype/assets/studio-template/package-lock.json @@ -0,0 +1,2094 @@ +{ + "name": "prototype-studio-template", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "prototype-studio-template", + "version": "0.1.0", + "dependencies": { + "@xyflow/react": "12.3.1", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@playwright/test": "1.55.0", + "@types/node": "22.10.2", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", + "@vitejs/plugin-react": "4.3.3", + "typescript": "5.6.3", + "vite": "5.4.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@playwright/test": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", + "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.7.tgz", + "integrity": "sha512-GFF2tHk0NqYVObrK0+/ps/LEXsV/v2q6Lyiy7mZr+Ej24fpQScvqK2ziz9NHeEQYnKWKym38Ml7dZbPtC9tuAw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.7.tgz", + "integrity": "sha512-BOymcXJFYYXqluDidj6sLelNjXruC3u1BqVxLn2OPCXH9xaPSOTH+MS4uychsGI1NU7tJoiv9KvqijDHFe+hhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.7.tgz", + "integrity": "sha512-jgVpGweIbls2H5jfBB70tnX52abMd5pJr3xmtoDILx6JdmGl+Un+tMb9R+MbHPVB46tTBpY8emtn7M71SnQhEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.7.tgz", + "integrity": "sha512-AVMpt94mrXH7WjyWKmd9nT4RC5yM0AN3Lt6A89xVIsuLDi3dDX6dxMytLbW36FlppY9JsoqUoS3LUUpUrYxiMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.7.tgz", + "integrity": "sha512-Nje29c/MyajxRNFGU05oyeTDS6SWdMg/wWMSlZSBTBju5BKz2NVc0kFiFSQGzRELNiB+CBk/ZvAVsWsENgjGag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.7.tgz", + "integrity": "sha512-xxgN5/kMzCNz/NYJ4t49B0/WS6InNlWv6xvgwUQqNnWM9WJMMwMUGWrAxdNEgYWdHL0sMnuh8I87StGrIMTA+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.7.tgz", + "integrity": "sha512-5U/9gVn/X657I8+NHoHO9Wnfyd0lEXwqbslobRoB9TC8SwYzOitcIr6TLE7531Q/NWs3L4mb+YOGzlf3vHIeYw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.7.tgz", + "integrity": "sha512-hcDS0a/Y8ggM3dcOfzH7UpSaogCViWTtNDSl0EXpWIj8g8/r86ITqptjil2hayTXS0Umzb+Yt3mjgpQVFVf2+A==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.7.tgz", + "integrity": "sha512-awbAr4N7Qd/yjZAQBEEPzgcPveYG2Gi5L6gq1/Bp+3SMRjn5F68Wv1zKsntzvvoioBR6yKvhLtWqu11am1TsxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.7.tgz", + "integrity": "sha512-Ut7xQJfeLhzu2yN3rHzVvL9ccyPJvN4aCdP1V7hJ5lGxX/eCzgB9SiMk/Md3J+rWWZgvoYsDW4BUFggJr32pBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.7.tgz", + "integrity": "sha512-trqoni/vHkuM5oo5iQhoKPh6fjjD7E80pMgCXWgGxU8vSRPUlwvG8GeYvEEcB6eJAxNa99WH9iVHPHelgtKiHA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.7.tgz", + "integrity": "sha512-gvlUy0XFIwtUWiQmhc+4GT2Knm3KosinpUVhr7CtTFA78gFDnE5NWwMPzHrJsYpKkh2LlY7yvBIboN1FAV6Xeg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.7.tgz", + "integrity": "sha512-8yS/kSXTJ/47Z62JW+C2AJ3vd/8zZw016vFjtNpZwmZlwJumLua4s1i78dkYuSSqQ+0aIMxJcBp/4uIwp2ehjA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.7.tgz", + "integrity": "sha512-mxVSKvvty0hOP1AFo/l4Kpv3i3HK0zx4rYUrirZDToYlWhS3xzmIkq3vUJ7rtrM2cPyXveDyDT6M1FfXZ9hTRA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.7.tgz", + "integrity": "sha512-lEse1zra1CagM+uHH2bl28GInDsZZ45NJzHei2OMaXQFzZWKBO1hFo07rS0xB6ayV4WU360UrSmPDSyCMpYsUQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.7.tgz", + "integrity": "sha512-d/cqCoiG9WMZ5VuJZ5Qdlv9g97QyoGwnmvHccvGhKSacTy7xFrAS+jMcJSBdMNOw1oacSkl6p7K6O/DRYTKL3g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.7.tgz", + "integrity": "sha512-kVEA//CeDOZZP1XCH2JBgqXb9+ALlwuA/4civaazMVdgzaSQC9WM38w65zG/LLk0lA7F9Od19CSBZW7J9mCbNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.7.tgz", + "integrity": "sha512-14oLfDsOSr0TSntT/34jRg/V8B2XW/Jomh9zX9CjIM9gwwNlI1OCymS/GVn0DV32h67eRf7U0GGem0rvfXgNwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.7.tgz", + "integrity": "sha512-dFVtPfjZqAnUPpPY4Bfg+87yo1IBW9HiY8iCFtg49Pn46MfZTebSHZh8PL6SOsgyt9LQP8Nj/sE6jm3aWpySmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.7.tgz", + "integrity": "sha512-Dm9CZ2SM3u2NgvBj4awW/gGqkOwzH8f6MSYIJE6+bKcKLI+T0+LhpD6vtetTfrIIx+rLXDf5RJqBP6v7V5O+wg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.7.tgz", + "integrity": "sha512-oaxsvS2mL6+78741CT2HCORBvy1USg3Wqpt5qvZwy3aMXRMl4zxyDo8maeFRWJkF3dp0Wbi71fKH5Gr9YI3vYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.7.tgz", + "integrity": "sha512-UKZz1Y4mxUqkWh60uH7B1aZn6UcAimPk4ipRktpb3wfZq7JrzZYyAXTwiafkb3T+kd5P3Sf+vzcRdsDks4WtjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.7.tgz", + "integrity": "sha512-WWkSuC1J+mZJnsDh0mRaCAFSWfQdl6y3fDjZeWElI6zHo8CcOSG5lY3yurKJjEq+FrX0WD60sFrVVpbHqzMmrw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.7.tgz", + "integrity": "sha512-u9bp2yXCI3/BUjUxY6uWknAY32pfvvDCPysyUTySotx0dj3ydlFHrRQYfn/b/kZgnfjyEwySMtI7Ga054uiysQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.7.tgz", + "integrity": "sha512-EhX6N3XPMEnVU5cRfNgzPOKcSucsbu3nNNWuoAaEuKIqmddBKHN5b9d4rPe65T1I+FaOT176JKiZNsUBp3rqbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz", + "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz", + "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.3.tgz", + "integrity": "sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/@xyflow/react": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.3.1.tgz", + "integrity": "sha512-PurYFxwzJa0U6RRX9k4VbNRU+vQd6mRKFR8Uk1dF81diCKZDj495y6AupqsjMHtkO66tGHV0LdenLpIHvnOEFw==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.43", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.43.tgz", + "integrity": "sha512-1zHgad1cWr1mKm2xbFaarK0Jg8WRgaQ8ubSBIo/pRdq3fEgCuqgNkL9NSAP6Rvm8zi3+Lu4JPUMN+EEx5QgX9A==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.29.tgz", + "integrity": "sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001816", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001816.tgz", + "integrity": "sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.458", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.458.tgz", + "integrity": "sha512-/hIH2gRSmxrf4/X4Nw/t/Wc8PVS22l8/3HnWjWTWsTOM5dsnMUPZF/+u+yr3LqweNuTBJE1rq6qAQzKkZVwMfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.59", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.59.tgz", + "integrity": "sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/playwright": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.64.7", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.7.tgz", + "integrity": "sha512-aCiJONoZp0lNGJ6sYqspLkh6UXaWEey/7Ko2TZyXQ4kgzAhDUxAgbt/oulXKmD+zFHXw1k+RF6ZZWOrjp7DZnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.7", + "@rollup/rollup-android-arm64": "4.64.7", + "@rollup/rollup-darwin-arm64": "4.64.7", + "@rollup/rollup-darwin-x64": "4.64.7", + "@rollup/rollup-freebsd-arm64": "4.64.7", + "@rollup/rollup-freebsd-x64": "4.64.7", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.7", + "@rollup/rollup-linux-arm-musleabihf": "4.64.7", + "@rollup/rollup-linux-arm64-gnu": "4.64.7", + "@rollup/rollup-linux-arm64-musl": "4.64.7", + "@rollup/rollup-linux-loong64-gnu": "4.64.7", + "@rollup/rollup-linux-loong64-musl": "4.64.7", + "@rollup/rollup-linux-ppc64-gnu": "4.64.7", + "@rollup/rollup-linux-ppc64-musl": "4.64.7", + "@rollup/rollup-linux-riscv64-gnu": "4.64.7", + "@rollup/rollup-linux-riscv64-musl": "4.64.7", + "@rollup/rollup-linux-s390x-gnu": "4.64.7", + "@rollup/rollup-linux-x64-gnu": "4.64.7", + "@rollup/rollup-linux-x64-musl": "4.64.7", + "@rollup/rollup-openbsd-x64": "4.64.7", + "@rollup/rollup-openharmony-arm64": "4.64.7", + "@rollup/rollup-win32-arm64-msvc": "4.64.7", + "@rollup/rollup-win32-ia32-msvc": "4.64.7", + "@rollup/rollup-win32-x64-gnu": "4.64.7", + "@rollup/rollup-win32-x64-msvc": "4.64.7", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", + "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/skills/product-prototype/assets/studio-template/package.json b/skills/product-prototype/assets/studio-template/package.json new file mode 100644 index 0000000..1ab7b6b --- /dev/null +++ b/skills/product-prototype/assets/studio-template/package.json @@ -0,0 +1,31 @@ +{ + "name": "prototype-studio-template", + "version": "0.1.0", + "private": true, + "packageManager": "npm@10", + "description": "Offline React + TypeScript + React Flow Prototype Studio skeleton", + "scripts": { + "init": "node scripts/init.mjs", + "dev": "node scripts/dev.mjs", + "validate": "node scripts/validate.mjs", + "snapshot": "node scripts/snapshot.mjs", + "build": "tsc --noEmit && vite build", + "test:incremental": "node scripts/incremental-check.mjs", + "test": "node scripts/test.mjs", + "test:e2e": "playwright test" + }, + "dependencies": { + "@xyflow/react": "12.3.1", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@playwright/test": "1.55.0", + "@types/node": "22.10.2", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", + "@vitejs/plugin-react": "4.3.3", + "typescript": "5.6.3", + "vite": "5.4.10" + } +} diff --git a/skills/product-prototype/assets/studio-template/playwright.config.ts b/skills/product-prototype/assets/studio-template/playwright.config.ts new file mode 100644 index 0000000..8e2bba3 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/playwright.config.ts @@ -0,0 +1,23 @@ +import { defineConfig, devices } from "@playwright/test"; +import { execFileSync } from "node:child_process"; + +function findChrome(): string | undefined { + if (process.env.PROTOTYPE_CHROME_PATH) return process.env.PROTOTYPE_CHROME_PATH; + try { return execFileSync("which", ["google-chrome"], { encoding: "utf8" }).trim(); } catch { return undefined; } +} +const chrome = findChrome(); + +export default defineConfig({ + testDir: "./tests/e2e", + timeout: 30_000, + fullyParallel: false, + workers: 1, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 2 : 0, + reporter: [["line"], ["html", { outputFolder: ".prototype-artifacts/playwright-report", open: "never" }]], + use: { baseURL: ["http", "://", "127.0.0.1:4173"].join(""), ...(chrome ? { launchOptions: { executablePath: chrome, args: ["--disable-gpu"] } } : { channel: "chrome" }), headless: true, trace: "retain-on-failure", video: "off", screenshot: "only-on-failure" }, + webServer: { command: "node scripts/test-server.mjs", url: ["http", "://", "127.0.0.1:4173"].join(""), reuseExistingServer: false, timeout: 30_000 }, + // Keep one browser worker: the viewport matrix is exercised explicitly in the smoke test, + // avoiding three concurrent Chrome instances on constrained CI runners. + projects: [{ name: "desktop", use: { ...devices["Desktop Chrome"], viewport: { width: 1440, height: 900 } } }], +}); diff --git a/skills/product-prototype/assets/studio-template/prototype/flow.json b/skills/product-prototype/assets/studio-template/prototype/flow.json new file mode 100644 index 0000000..6e39494 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/prototype/flow.json @@ -0,0 +1,38 @@ +{ + "flowVersion": 1, + "startNodeId": "page.studio-placeholder", + "nodes": [ + { "id": "page.studio-placeholder", "kind": "page", "component": "StudioPlaceholder", "title": "Studio overview", "description": "A page node with actions from the shared flow.", "thumbnail": "/static/studio-overview.svg" }, + { "id": "dialog.studio-help", "kind": "dialog", "component": "StudioPlaceholder", "title": "Help dialog", "description": "A dialog node rendered from the same registry.", "thumbnail": "/static/studio-help.svg" }, + { "id": "state.studio-empty", "kind": "state", "component": "StudioPlaceholder", "title": "Empty state", "description": "A state node used to demonstrate conditional navigation.", "thumbnail": "/static/studio-empty.svg" }, + { "id": "page.device-list", "kind": "page", "component": "DeviceList", "title": "设备列表", "description": "设备管理高保真示例:查看设备、创建设备或预览空列表。" }, + { "id": "page.device-create", "kind": "page", "component": "DeviceCreate", "title": "新建设备", "description": "填写设备基础信息。" }, + { "id": "page.device-config", "kind": "page", "component": "DeviceConfig", "title": "配置连接参数", "description": "填写并保存设备连接参数。" }, + { "id": "state.device-connecting", "kind": "state", "component": "DeviceConnecting", "title": "正在连接设备", "description": "运行确定性连接 mock。" }, + { "id": "state.device-connected", "kind": "state", "component": "DeviceConnected", "title": "设备连接成功", "description": "连接成功后查看设备详情。" }, + { "id": "state.device-connection-failed", "kind": "state", "component": "DeviceConnectionFailed", "title": "连接失败", "description": "显示失败原因并支持重试。" }, + { "id": "state.device-empty", "kind": "state", "component": "DeviceEmpty", "title": "设备列表为空", "description": "无设备时的确定性空状态。" }, + { "id": "page.device-detail", "kind": "page", "component": "DeviceDetail", "title": "设备详情", "description": "查看连接成功设备的详细信息。" } + ], + "edges": [ + { "id": "edge.studio.open-help", "source": "page.studio-placeholder", "target": "dialog.studio-help", "actionId": "action.studio.open-help", "label": "Open help" }, + { "id": "edge.studio.show-empty", "source": "page.studio-placeholder", "target": "state.studio-empty", "actionId": "action.studio.show-empty", "label": "Show empty", "when": { "op": "truthy", "field": "showEmpty" } }, + { "id": "edge.studio.close-help", "source": "dialog.studio-help", "target": "page.studio-placeholder", "actionId": "action.studio.close-help", "label": "Close" }, + { "id": "edge.studio.back", "source": "state.studio-empty", "target": "page.studio-placeholder", "actionId": "action.studio.back", "label": "Back" }, + { "id": "edge.studio.open-device-management", "source": "page.studio-placeholder", "target": "page.device-list", "actionId": "device.open-management", "label": "打开设备管理" }, + { "id": "edge.device.create", "source": "page.device-list", "target": "page.device-create", "actionId": "device.create", "label": "新建设备" }, + { "id": "edge.device.show-empty", "source": "page.device-list", "target": "state.device-empty", "actionId": "device.show-empty", "label": "预览空列表", "when": { "op": "eq", "field": "deviceScenario", "value": "empty" } }, + { "id": "edge.device.open-detail", "source": "page.device-list", "target": "page.device-detail", "actionId": "device.open-detail", "label": "查看详情", "when": { "op": "truthy", "field": "deviceExists" } }, + { "id": "edge.device.create-to-list", "source": "page.device-create", "target": "page.device-list", "actionId": "device.back-to-list", "label": "取消" }, + { "id": "edge.device.create-to-config", "source": "page.device-create", "target": "page.device-config", "actionId": "device.continue-to-config", "label": "继续配置", "when": { "op": "truthy", "field": "formValid" } }, + { "id": "edge.device.config-to-create", "source": "page.device-config", "target": "page.device-create", "actionId": "device.back-to-create", "label": "返回上一步" }, + { "id": "edge.device.config-to-connecting", "source": "page.device-config", "target": "state.device-connecting", "actionId": "device.save-config", "label": "保存并连接", "when": { "op": "truthy", "field": "formValid" } }, + { "id": "edge.device.connecting-success", "source": "state.device-connecting", "target": "state.device-connected", "actionId": "device.resolve-connection", "label": "连接成功", "when": { "op": "eq", "field": "connectionOutcome", "value": "success" } }, + { "id": "edge.device.connecting-failure", "source": "state.device-connecting", "target": "state.device-connection-failed", "actionId": "device.resolve-connection", "label": "连接失败", "when": { "op": "eq", "field": "connectionOutcome", "value": "failure" } }, + { "id": "edge.device.connected-detail", "source": "state.device-connected", "target": "page.device-detail", "actionId": "device.open-detail", "label": "查看详情", "when": { "op": "truthy", "field": "deviceExists" } }, + { "id": "edge.device.failed-retry", "source": "state.device-connection-failed", "target": "state.device-connecting", "actionId": "device.retry-connection", "label": "重试连接", "when": { "op": "truthy", "field": "canRetry" } }, + { "id": "edge.device.failed-list", "source": "state.device-connection-failed", "target": "page.device-list", "actionId": "device.back-to-list", "label": "返回列表" }, + { "id": "edge.device.empty-create", "source": "state.device-empty", "target": "page.device-create", "actionId": "device.create", "label": "新建设备" }, + { "id": "edge.device.detail-list", "source": "page.device-detail", "target": "page.device-list", "actionId": "device.back-to-list", "label": "返回列表" } + ] +} diff --git a/skills/product-prototype/assets/studio-template/prototype/layout.json b/skills/product-prototype/assets/studio-template/prototype/layout.json new file mode 100644 index 0000000..b66d8a1 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/prototype/layout.json @@ -0,0 +1,17 @@ +{ + "layoutVersion": 1, + "viewport": { "x": 0, "y": 0, "zoom": 1 }, + "nodes": { + "page.studio-placeholder": { "x": 80, "y": 80 }, + "dialog.studio-help": { "x": 420, "y": 40 }, + "state.studio-empty": { "x": 420, "y": 260 }, + "page.device-list": { "x": 760, "y": 80 }, + "page.device-create": { "x": 1080, "y": 80 }, + "page.device-config": { "x": 1400, "y": 80 }, + "state.device-connecting": { "x": 1720, "y": 80 }, + "state.device-connected": { "x": 2040, "y": 80 }, + "state.device-connection-failed": { "x": 2040, "y": 320 }, + "state.device-empty": { "x": 1080, "y": 420 }, + "page.device-detail": { "x": 2360, "y": 80 } + } +} diff --git a/skills/product-prototype/assets/studio-template/prototype/requirements.md b/skills/product-prototype/assets/studio-template/prototype/requirements.md new file mode 100644 index 0000000..4c446a5 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/prototype/requirements.md @@ -0,0 +1,15 @@ +# Prototype requirements + +## Goal + +用本地 Prototype Studio 维护页面关系、布局和评审记录。 + +## Scenario + +作者从一份需求初始化 Studio,使用共享 registry 和 flow Action 查看页面动线、runtime 预览、 +设备管理 mock、layout 保存和 review 批注导出。 + +## Acceptance context + +验收包括四类源数据和稳定 ID、schema/引用/可达性校验、增量边界、幂等初始化、构建,以及按 +实际环境记录的 snapshot/Playwright 结果;不连接真实后端或设备。 diff --git a/skills/product-prototype/assets/studio-template/prototype/review.json b/skills/product-prototype/assets/studio-template/prototype/review.json new file mode 100644 index 0000000..d3ebd93 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/prototype/review.json @@ -0,0 +1,5 @@ +{ + "reviewVersion": 1, + "status": "draft", + "comments": [] +} diff --git a/skills/product-prototype/assets/studio-template/public/static/README.md b/skills/product-prototype/assets/studio-template/public/static/README.md new file mode 100644 index 0000000..daee37c --- /dev/null +++ b/skills/product-prototype/assets/studio-template/public/static/README.md @@ -0,0 +1,4 @@ +# Offline static assets + +只放项目自有、可版本化的本地静态资源。模板不下载或引用远程 CSS、字体、图片、图标 CDN 或 +运行时服务;空目录由此说明文件保留。 diff --git a/skills/product-prototype/assets/studio-template/public/static/studio-empty.svg b/skills/product-prototype/assets/studio-template/public/static/studio-empty.svg new file mode 100644 index 0000000..cb455cd --- /dev/null +++ b/skills/product-prototype/assets/studio-template/public/static/studio-empty.svg @@ -0,0 +1 @@ + diff --git a/skills/product-prototype/assets/studio-template/public/static/studio-help.svg b/skills/product-prototype/assets/studio-template/public/static/studio-help.svg new file mode 100644 index 0000000..f44554d --- /dev/null +++ b/skills/product-prototype/assets/studio-template/public/static/studio-help.svg @@ -0,0 +1 @@ + diff --git a/skills/product-prototype/assets/studio-template/public/static/studio-overview.svg b/skills/product-prototype/assets/studio-template/public/static/studio-overview.svg new file mode 100644 index 0000000..0aa548e --- /dev/null +++ b/skills/product-prototype/assets/studio-template/public/static/studio-overview.svg @@ -0,0 +1 @@ + diff --git a/skills/product-prototype/assets/studio-template/scripts/dev.mjs b/skills/product-prototype/assets/studio-template/scripts/dev.mjs new file mode 100644 index 0000000..9f92f71 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/scripts/dev.mjs @@ -0,0 +1,13 @@ +#!/usr/bin/env node +import { spawn } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const vite = path.join(root, "node_modules", "vite", "bin", "vite.js"); +if (!fs.existsSync(vite)) { console.error(`DEV_DEPENDENCY_MISSING: Vite is not installed at ${vite}`); process.exit(2); } +const child = spawn(process.execPath, [vite, ...process.argv.slice(2)], { cwd: root, stdio: "inherit", env: { ...process.env, BROWSER: "none" } }); +for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => child.kill(signal)); +child.on("error", (error) => { console.error(`DEV_START_FAILED: ${error.message}`); process.exitCode = 2; }); +child.on("exit", (code, signal) => { process.exitCode = code ?? (signal ? 2 : 0); }); diff --git a/skills/product-prototype/assets/studio-template/scripts/incremental-check.mjs b/skills/product-prototype/assets/studio-template/scripts/incremental-check.mjs new file mode 100644 index 0000000..cea5d66 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/scripts/incremental-check.mjs @@ -0,0 +1,100 @@ +#!/usr/bin/env node +/** + * Offline regression for the stable-ID incremental data boundary. + * + * This is deliberately a fixture check, not a second product merge engine: + * it proves the documented add/change/delete diff contract without changing + * the Studio/runtime implementation. + */ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const fixtureRoot = path.join(root, "fixtures", "incremental"); + +function read(name) { + return JSON.parse(fs.readFileSync(path.join(fixtureRoot, name), "utf8")); +} + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +function equal(left, right) { + return JSON.stringify(left) === JSON.stringify(right); +} + +function indexById(items) { + return new Map(items.map((item) => [item.id, item])); +} + +function applyCollection(baseItems, patch) { + const items = clone(baseItems); + const remove = new Set(patch.removeIds ?? []); + const updates = new Map((patch.update ?? []).map((item) => [item.id, item.patch])); + const added = new Set((patch.add ?? []).map((item) => item.id)); + const result = items + .filter((item) => !remove.has(item.id)) + .map((item) => (updates.has(item.id) ? { ...item, ...updates.get(item.id) } : item)); + result.push(...clone(patch.add ?? [])); + return { result, remove, updates, added }; +} + +function applyLayout(base, patch) { + const result = clone(base); + for (const id of patch.removeNodeIds ?? []) delete result.nodes[id]; + for (const [id, position] of Object.entries(patch.nodes ?? {})) { + const { expected: _expected, ...next } = position; + result.nodes[id] = next; + } + return result; +} + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +const baseFlow = read("base-flow.json"); +const patch = read("patch-data.json"); +const expectedFlow = read("expected-flow.json"); +const baseLayout = read("base-layout-scope.json"); +const expectedLayout = read("expected-layout-scope.json"); +const baseReview = read("base-review.json"); +const expectedReview = read("expected-review.json"); + +const flowNodes = applyCollection(baseFlow.nodes, patch.nodes); +const flowEdges = applyCollection(baseFlow.edges, patch.edges); +const reviewComments = applyCollection(baseReview.comments, patch.comments); +const actualFlow = { ...baseFlow, nodes: flowNodes.result, edges: flowEdges.result }; +const actualReview = { ...baseReview, comments: reviewComments.result }; +const actualLayout = applyLayout(baseLayout, patch.layout); + +assert(equal(actualFlow, expectedFlow), "flow add/change/delete diff does not match expected fixture"); +assert(equal(actualLayout, expectedLayout), "layout add/change/delete diff does not match expected fixture"); +assert(equal(actualReview, expectedReview), "review add/change/delete diff does not match expected fixture"); + +for (const [name, baseItems, actualItems, patchInfo] of [ + ["flow node", baseFlow.nodes, actualFlow.nodes, flowNodes], + ["flow edge", baseFlow.edges, actualFlow.edges, flowEdges], + ["review comment", baseReview.comments, actualReview.comments, reviewComments], +]) { + const actualById = indexById(actualItems); + for (const item of baseItems) { + if (patchInfo.remove.has(item.id) || patchInfo.updates.has(item.id)) continue; + assert(equal(actualById.get(item.id), item), `${name} ${item.id} changed outside the patch`); + } + for (const id of patchInfo.remove) assert(!actualById.has(id), `${name} ${id} was not removed`); + for (const id of patchInfo.added) assert(actualById.has(id), `${name} ${id} was not added`); +} + +for (const id of patch.layout.removeNodeIds ?? []) assert(!(id in actualLayout.nodes), `layout ${id} was not removed`); +for (const id of ["page.keep", "page.change"]) { + if (!(id in (patch.layout.nodes ?? {}))) assert(equal(actualLayout.nodes[id], baseLayout.nodes[id]), `layout ${id} changed outside the patch`); +} +for (const id of ["comment.keep", "comment.change"]) { + const comment = actualReview.comments.find((item) => item.id === id); + assert(Boolean(comment), `review ${id} disappeared unexpectedly`); +} + +console.log("incremental scope check: PASS (flow/layout/review add-change-delete; unrelated stable IDs preserved)"); diff --git a/skills/product-prototype/assets/studio-template/scripts/init.mjs b/skills/product-prototype/assets/studio-template/scripts/init.mjs new file mode 100644 index 0000000..cb04c94 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/scripts/init.mjs @@ -0,0 +1,36 @@ +#!/usr/bin/env node +/** Idempotently copy this template, never overwriting an existing file. */ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const excluded = new Set(["node_modules", "dist", "test-results", ".prototype-artifacts"]); +const args = process.argv.slice(2); +if (args.includes("--help") || args.includes("-h")) { console.log("Usage: npm run init -- [TARGET_DIR]"); process.exit(0); } +if (args.length > 1 || args.some((arg) => arg.startsWith("-"))) fail("INIT_ARGUMENT", "expected zero or one target directory"); +const target = path.resolve(args[0] ?? "."); +if (args.length && (target === root || root.startsWith(`${target}${path.sep}`))) fail("INIT_TARGET", "target must not be the template directory or its parent"); +if (fs.existsSync(target) && !fs.statSync(target).isDirectory()) fail("INIT_TARGET", `target is not a directory: ${target}`); +try { fs.mkdirSync(target, { recursive: true }); } catch (error) { fail("INIT_WRITE", `cannot create target ${target}: ${error.message}`); } +const created = []; +const skipped = []; +copyTree(root, target, root); +console.log(`Prototype Studio init: ${target}`); +console.log(`created: ${created.length}`); +for (const item of created) console.log(` create ${item}`); +console.log(`skipped: ${skipped.length}`); +for (const item of skipped) console.log(` skip ${item}`); + +function copyTree(source, destination, sourceRoot) { + for (const entry of fs.readdirSync(source, { withFileTypes: true })) { + if (excluded.has(entry.name) || entry.name === ".git") continue; + const from = path.join(source, entry.name); const to = path.join(destination, entry.name); + const relative = path.relative(sourceRoot, from) || entry.name; + if (entry.isDirectory()) { fs.mkdirSync(to, { recursive: true }); copyTree(from, to, sourceRoot); continue; } + if (fs.existsSync(to)) { skipped.push(relative); continue; } + try { fs.mkdirSync(path.dirname(to), { recursive: true }); fs.copyFileSync(from, to); created.push(relative); } + catch (error) { fail("INIT_WRITE", `cannot copy ${relative}: ${error.message}`); } + } +} +function fail(code, message) { console.error(`${code}: ${message}`); process.exit(2); } diff --git a/skills/product-prototype/assets/studio-template/scripts/snapshot.mjs b/skills/product-prototype/assets/studio-template/scripts/snapshot.mjs new file mode 100644 index 0000000..fc9430d --- /dev/null +++ b/skills/product-prototype/assets/studio-template/scripts/snapshot.mjs @@ -0,0 +1,68 @@ +#!/usr/bin/env node +import fs from "node:fs/promises"; +import { execFileSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { chromium } from "@playwright/test"; +import { startViteServer, stopViteServer } from "./vite-server.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const viewports = [{ name: "desktop", width: 1440, height: 900 }, { name: "tablet", width: 1024, height: 900 }, { name: "mobile", width: 390, height: 844 }]; +const pages = ["list", "create", "config", "connecting", "connected", "failed", "detail", "empty"]; +const outputRoot = path.resolve(process.cwd(), process.env.PROTOTYPE_ARTIFACTS ?? ".prototype-artifacts"); +const outputDir = path.join(outputRoot, "snapshots"); const manifestPath = path.join(outputDir, "device-management.manifest.json"); const staging = `${outputDir}.staging-${process.pid}`; +let server; let browser; +let shuttingDown = false; +async function cleanup() { + const currentBrowser = browser; browser = undefined; + const currentServer = server; server = undefined; + await currentBrowser?.close(); + await stopViteServer(currentServer?.child); +} +for (const [signal, code] of [["SIGINT", 130], ["SIGTERM", 143]]) process.once(signal, () => { + if (shuttingDown) return; + shuttingDown = true; + void cleanup().finally(() => process.exit(code)); +}); +try { + await fs.rm(staging, { recursive: true, force: true }); await fs.mkdir(staging, { recursive: true }); + server = await startViteServer(root); + try { browser = await chromium.launch({ executablePath: findChrome(), headless: true, args: ["--disable-gpu"] }); } catch (error) { fail("BROWSER_MISSING", `Chrome/Playwright browser unavailable: ${error.message}`); } + const screenshots = []; + for (const viewport of viewports) for (const scene of pages) { + const page = await browser.newPage({ viewport: { width: viewport.width, height: viewport.height } }); + try { + await driveToScene(page, server.url, scene); + const testId = await page.locator("[data-testid^='device-page-']").getAttribute("data-testid"); + if (!testId) fail("PAGE_NOT_REGISTERED", `scene ${scene} did not render a registered device page`); + const file = `device-management.${scene}.${viewport.name}.png`; const filePath = path.join(staging, file); + await page.screenshot({ path: filePath, fullPage: true, animations: "disabled" }); const stat = await fs.stat(filePath); + if (!stat.size) fail("SCREENSHOT_EMPTY", `screenshot is empty: ${file}`); + screenshots.push({ scene: "device-management", scenario: scene === "failed" ? "retry-success" : scene === "empty" ? "empty" : "normal", page: scene, nodeId: testId.replace("device-page-", ""), viewport, path: `snapshots/${file}`, bytes: stat.size }); + } finally { await page.close(); } + } + if (screenshots.length !== viewports.length * pages.length) fail("MANIFEST_INCOMPLETE", "not every page/viewport pair produced a screenshot"); + const manifest = { version: 1, status: "generated", generatedBy: "npm run snapshot", baseUrl: server.url, screenshots }; + await fs.mkdir(outputDir, { recursive: true }); await fs.rm(outputDir, { recursive: true, force: true }); await fs.rename(staging, outputDir); await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + console.log(`snapshot: OK (${screenshots.length} actual screenshots)`); console.log(`manifest: ${manifestPath}`); +} catch (error) { console.error(`${error.code ?? "SNAPSHOT_FAILED"}: ${error.message}`); await fs.rm(staging, { recursive: true, force: true }); process.exitCode = 1; } +finally { if (!shuttingDown) await cleanup(); } + +async function driveToScene(page, url, scene) { + await page.goto(url, { waitUntil: "networkidle", timeout: 20_000 }); await page.locator("[data-testid='flow-node-page.studio-placeholder']").waitFor({ state: "attached", timeout: 10_000 }); + if (scene === "failed") await page.getByTestId("device-scenario").selectOption("retry-success", { force: true }); + await page.getByTestId("dispatch-device.open-management").click(); await page.getByTestId("device-page-list").waitFor(); + if (scene === "list") return; if (scene === "empty") { await page.getByTestId("device-scenario").selectOption("empty", { force: true }); return; } + await page.getByTestId("device-action-device.create").click(); await page.getByTestId("device-page-create").waitFor(); if (scene === "create") return; + if (scene === "config") { await page.getByTestId("device-name").fill("截图设备"); await page.getByTestId("device-host").fill("10.0.0.8"); await page.getByTestId("device-action-device.continue-to-config").click(); await page.getByTestId("device-page-config").waitFor(); return; } + await page.getByTestId("device-name").fill("截图设备"); await page.getByTestId("device-host").fill("10.0.0.8"); await page.getByTestId("device-action-device.continue-to-config").click(); await page.getByTestId("device-page-config").waitFor(); + if (scene === "connecting") { await page.getByTestId("device-action-device.save-config").click(); await page.getByTestId("device-page-connecting").waitFor(); return; } + if (scene === "failed") { await page.getByTestId("device-action-device.save-config").click(); await page.getByTestId("device-action-device.resolve-connection").click(); await page.getByTestId("device-page-failed").waitFor(); return; } + await page.getByTestId("device-action-device.save-config").click(); await page.getByTestId("device-action-device.resolve-connection").click(); await page.getByTestId("device-page-connected").waitFor(); if (scene === "connected") return; + await page.getByTestId("device-action-device.open-detail").click(); await page.getByTestId("device-page-detail").waitFor(); +} +function fail(code, message) { const error = new Error(message); error.code = code; throw error; } +function findChrome() { + if (process.env.PROTOTYPE_CHROME_PATH) return process.env.PROTOTYPE_CHROME_PATH; + try { return execFileSync("which", ["google-chrome"], { encoding: "utf8" }).trim(); } catch { return undefined; } +} diff --git a/skills/product-prototype/assets/studio-template/scripts/test-server.mjs b/skills/product-prototype/assets/studio-template/scripts/test-server.mjs new file mode 100644 index 0000000..a2fed0d --- /dev/null +++ b/skills/product-prototype/assets/studio-template/scripts/test-server.mjs @@ -0,0 +1,27 @@ +#!/usr/bin/env node +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { startViteServer, stopViteServer } from "./vite-server.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +let server; +let shuttingDown = false; + +async function shutdown(code) { + if (shuttingDown) return; + shuttingDown = true; + await stopViteServer(server?.child); + process.exit(code); +} + +process.once("SIGINT", () => void shutdown(130)); +process.once("SIGTERM", () => void shutdown(143)); + +try { + server = await startViteServer(root); + await new Promise(() => {}); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + await stopViteServer(server?.child); + process.exit(1); +} diff --git a/skills/product-prototype/assets/studio-template/scripts/test.mjs b/skills/product-prototype/assets/studio-template/scripts/test.mjs new file mode 100644 index 0000000..a874a8a --- /dev/null +++ b/skills/product-prototype/assets/studio-template/scripts/test.mjs @@ -0,0 +1,79 @@ +#!/usr/bin/env node +/** Offline validator, Studio/runtime, device mock, and browser contract checks. */ +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const validator = path.join(root, "scripts", "validate.mjs"); +const fixtures = path.join(root, "fixtures"); +const run = (name) => spawnSync(process.execPath, [validator, "--data-dir", path.join(fixtures, name), "--json"], { encoding: "utf8" }); +const failures = []; +const valid = run("valid"); +if (valid.status !== 0 || !JSON.parse(valid.stdout).valid) failures.push("valid fixture should pass"); +for (const [name, code, id] of [ + ["invalid-duplicate-id", "DUPLICATE_ID", "page.start"], + ["invalid-dangling-flow", "DANGLING_REFERENCE", "edge.missing"], + ["invalid-condition", "INVALID_CONDITION", "edge.bad-condition"], + ["invalid-unreachable", "UNREACHABLE_NODE", "page.orphan"], + ["invalid-unknown-action", "UNKNOWN_ACTION", "action.missing"], + ["invalid-unknown-component", "UNKNOWN_COMPONENT", "page.start"], + ["invalid-layout-reference", "DANGLING_REFERENCE", "page.missing"], + ["invalid-review-reference", "DANGLING_REFERENCE", "page.missing"], +]) { + const result = run(name); + let report; + try { report = JSON.parse(result.stdout); } catch (_error) { report = { errors: [] }; } + if (result.status === 0 || !report.errors.some((error) => error.code === code && error.id === id && error.file)) failures.push(`${name} should report ${code} for ${id} with file`); +} + +const base = JSON.parse(fs.readFileSync(path.join(fixtures, "incremental", "base-layout.json"), "utf8")); +const patch = JSON.parse(fs.readFileSync(path.join(fixtures, "incremental", "patch-layout.json"), "utf8")); +const expected = JSON.parse(fs.readFileSync(path.join(fixtures, "incremental", "expected-layout.json"), "utf8")); +const merged = { ...base, nodes: { ...base.nodes } }; +for (const id of patch.removeNodeIds ?? []) delete merged.nodes[id]; +for (const [id, position] of Object.entries(patch.nodes ?? {})) { + const current = merged.nodes[id]; + if (position.expected && current && (position.expected.x !== current.x || position.expected.y !== current.y)) failures.push(`layout conflict for ${id} was not detected`); + const { expected: _expected, ...next } = position; + merged.nodes[id] = next; +} +if (JSON.stringify(merged) !== JSON.stringify(expected)) failures.push("incremental layout merge changed an unrelated stable node"); + +const prototypeFlow = JSON.parse(fs.readFileSync(path.join(root, "prototype", "flow.json"), "utf8")); +const registry = JSON.parse(fs.readFileSync(path.join(root, "src", "registry", "registry.json"), "utf8")); +const kinds = new Set(prototypeFlow.nodes.map((node) => node.kind)); +for (const kind of ["page", "dialog", "state"]) if (!kinds.has(kind)) failures.push(`T3 prototype must include ${kind} node`); +for (const edge of prototypeFlow.edges) { + if (!registry.actions[edge.actionId]) failures.push(`T3 edge ${edge.id} has no registry Action`); + if (edge.label === undefined) failures.push(`T3 edge ${edge.id} should expose a label`); +} +const appSource = fs.readFileSync(path.join(root, "src", "App.tsx"), "utf8"); +for (const marker of [" node.id)); +for (const id of ["page.device-list", "page.device-create", "page.device-config", "state.device-connecting", "state.device-connected", "state.device-connection-failed", "state.device-empty", "page.device-detail"]) if (!deviceNodeIds.has(id)) failures.push(`T4 device flow node missing: ${id}`); +if (new Set(deviceFlow.nodes.map((node) => node.id)).size !== deviceFlow.nodes.length) failures.push("T4 device flow contains duplicate node IDs"); +if (new Set(deviceFlow.edges.map((edge) => edge.id)).size !== deviceFlow.edges.length) failures.push("T4 device flow contains duplicate edge IDs"); +const deviceComponents = new Set(["DeviceList", "DeviceCreate", "DeviceConfig", "DeviceConnecting", "DeviceConnected", "DeviceConnectionFailed", "DeviceEmpty", "DeviceDetail"]); +for (const node of deviceFlow.nodes.filter((item) => deviceNodeIds.has(item.id) && item.id.startsWith("page.device") || item.id.startsWith("state.device"))) if (!deviceComponents.has(node.component)) failures.push(`T4 node ${node.id} has no device component`); +const mockSource = fs.readFileSync(path.join(root, "src", "mocks", "device-management.ts"), "utf8"); +for (const forbidden of ["Math.random", "fetch(", "localStorage"]) if (mockSource.includes(forbidden)) failures.push(`T4 mock must not use ${forbidden}`); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "snapshots", "device-management.manifest.json"), "utf8")); +if (manifest.status !== "pending-t5-browser-generation" || manifest.screenshots.length !== 0) failures.push("source screenshot manifest must defer real rendering without fake images"); +const deviceMockTest = spawnSync(process.execPath, ["--experimental-strip-types", path.join(root, "tests", "device-management.test.ts")], { encoding: "utf8" }); +if (deviceMockTest.status !== 0) failures.push(`T4 deterministic mock unit test failed: ${deviceMockTest.stderr || deviceMockTest.stdout}`); +if (failures.length) { console.error(failures.map((failure) => `TEST-FAIL ${failure}`).join("\n")); process.exit(1); } +console.log("offline validator contract checks passed (8 invalid fixtures, valid fixture, incremental layout)"); +console.log("Studio/runtime contract checks passed (node kinds, registry edges, controls, fail-closed runtime)"); +console.log("device flow and deterministic mock checks passed"); +const playwrightBin = path.join(root, "node_modules", "@playwright", "test", "cli.js"); +if (!fs.existsSync(playwrightBin)) { console.error(`TEST_DEPENDENCY_MISSING: Playwright is not installed at ${playwrightBin}`); process.exit(2); } +const e2e = spawnSync(process.execPath, [playwrightBin, "test"], { cwd: root, encoding: "utf8", stdio: "inherit", env: { ...process.env, PROTOTYPE_ARTIFACTS: process.env.PROTOTYPE_ARTIFACTS ?? ".prototype-artifacts" } }); +if (e2e.error) { console.error(`TEST_ENVIRONMENT: unable to start Playwright: ${e2e.error.message}`); process.exit(2); } +if (e2e.status !== 0) { console.error(`TEST_PLAYWRIGHT_FAILED: Playwright exited with code ${e2e.status ?? 1}`); process.exit(e2e.status ?? 1); } +console.log("Playwright desktop/tablet/mobile interaction tests passed"); diff --git a/skills/product-prototype/assets/studio-template/scripts/validate.mjs b/skills/product-prototype/assets/studio-template/scripts/validate.mjs new file mode 100644 index 0000000..7a73e2a --- /dev/null +++ b/skills/product-prototype/assets/studio-template/scripts/validate.mjs @@ -0,0 +1,153 @@ +#!/usr/bin/env node +/** Offline validator for the four prototype source files. */ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import registry from "../src/registry/registry.json" with { type: "json" }; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const args = process.argv.slice(2); +let dataDir = "prototype"; +let jsonOutput = false; +let quiet = false; +for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === "--data-dir") dataDir = args[++index] ?? ""; + else if (arg === "--json") jsonOutput = true; + else if (arg === "--quiet") quiet = true; + else if (arg === "--help" || arg === "-h") { console.log("Usage: node scripts/validate.mjs [--data-dir DIR] [--json] [--quiet]"); process.exit(0); } + else { console.error(`validate: unknown argument ${arg}`); process.exit(2); } +} +const root = path.resolve(process.cwd(), dataDir); +const errors = []; +const push = (file, id, code, message) => errors.push({ file, id, code, message }); +const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value); +const isPrimitive = (value) => value === null || ["string", "number", "boolean"].includes(typeof value); +const idPattern = /^[A-Za-z][A-Za-z0-9._:-]*$/; +const fieldPattern = /^[A-Za-z][A-Za-z0-9_.-]*$/; +const reservedFields = new Set(["__proto__", "prototype", "constructor"]); +const nodeKinds = new Set(["page", "dialog", "state"]); +const reviewStatuses = new Set(["draft", "in-review", "approved", "changes-requested"]); +const targetKinds = new Set(["node", "edge", "page"]); +const conditionOps = new Set(["eq", "neq", "in", "notIn", "exists", "truthy", "falsy", "and", "or", "not"]); + +function readJson(fileName) { + const file = path.join(root, fileName); + try { return JSON.parse(fs.readFileSync(file, "utf8")); } + catch (cause) { + const code = cause?.code === "ENOENT" ? "FILE_READ" : "JSON_PARSE"; + push(fileName, fileName.replace(/\.json$/, ""), code, cause?.code === "ENOENT" ? `file does not exist: ${file}` : `invalid JSON: ${cause.message}`); + return null; + } +} + +function validateRequirements() { + const file = "requirements.md"; + let value; + try { value = fs.readFileSync(path.join(root, file), "utf8"); } + catch (_cause) { push(file, "requirements", "FILE_READ", `file does not exist: ${path.join(root, file)}`); return; } + if (!value.trim()) { push(file, "requirements", "SCHEMA_INVALID", "requirements.md must be non-empty markdown"); return; } + for (const section of ["Goal", "Scenario", "Acceptance context"]) if (!new RegExp(`^##\\s+${section}\\s*$`, "mi").test(value)) push(file, `requirements.${section.toLowerCase().replaceAll(" ", "-")}`, "SCHEMA_INVALID", `missing required ## ${section} section`); +} + +function validateCondition(value, file, id) { + if (!isObject(value) || typeof value.op !== "string" || !conditionOps.has(value.op)) { push(file, id, "INVALID_CONDITION", "condition must use a whitelisted structured operator"); return; } + const op = value.op; + if (["eq", "neq", "in", "notIn", "exists", "truthy", "falsy"].includes(op) && (typeof value.field !== "string" || !fieldPattern.test(value.field) || reservedFields.has(value.field))) push(file, id, "INVALID_CONDITION", "condition field must be a safe dotted path"); + if (["eq", "neq"].includes(op) && !isPrimitive(value.value)) push(file, id, "INVALID_CONDITION", `${op} value must be a primitive`); + if (["in", "notIn"].includes(op) && (!Array.isArray(value.value) || !value.value.every(isPrimitive))) push(file, id, "INVALID_CONDITION", `${op} value must be an array of primitives`); + if (["and", "or"].includes(op)) { + if (!Array.isArray(value.conditions) || value.conditions.length === 0) push(file, id, "INVALID_CONDITION", `${op} requires a non-empty conditions array`); + else value.conditions.forEach((child, index) => validateCondition(child, file, `${id}.conditions[${index}]`)); + } + if (op === "not") { + if (!("condition" in value)) push(file, id, "INVALID_CONDITION", "not requires a nested condition"); + else validateCondition(value.condition, file, `${id}.condition`); + } + const allowed = op === "not" ? ["op", "condition"] : ["and", "or"].includes(op) ? ["op", "conditions"] : ["exists", "truthy", "falsy"].includes(op) ? ["op", "field"] : ["op", "field", "value"]; + Object.keys(value).filter((key) => !allowed.includes(key)).forEach((key) => push(file, id, "INVALID_CONDITION", `unknown condition property ${key}`)); +} + +function duplicateId(ids, file, kind) { + const seen = new Set(); + for (const [index, id] of ids.entries()) { + const stableId = typeof id === "string" ? id : `${kind}[${index}]`; + if (typeof id !== "string" || !idPattern.test(id)) push(file, stableId, "SCHEMA_INVALID", `${kind} contains an invalid stable ID`); + else if (seen.has(id)) push(file, id, "DUPLICATE_ID", `duplicate ${kind} ID ${id}`); + else seen.add(id); + } +} + +function validateFlow(flow) { + const file = "flow.json"; + if (!isObject(flow) || flow.flowVersion !== 1 || typeof flow.startNodeId !== "string" || !Array.isArray(flow.nodes) || !Array.isArray(flow.edges)) { push(file, "flow", "SCHEMA_INVALID", "flow requires flowVersion 1, startNodeId, nodes and edges"); return { nodes: new Map(), edges: new Map() }; } + duplicateId(flow.nodes.map((node) => node?.id), file, "node"); + duplicateId(flow.edges.map((edge) => edge?.id), file, "edge"); + const nodes = new Map(); + flow.nodes.forEach((node, index) => { + const id = typeof node?.id === "string" ? node.id : `nodes[${index}]`; + if (!isObject(node) || !idPattern.test(node.id ?? "") || !nodeKinds.has(node.kind) || typeof node.component !== "string" || typeof node.title !== "string") { push(file, id, "SCHEMA_INVALID", "node requires id, kind, component and title"); return; } + if (nodes.has(id)) return; + nodes.set(id, node); + if (!registry.components[node.component]) push(file, id, "UNKNOWN_COMPONENT", `component ${node.component} is not registered`); + else if (!registry.components[node.component].kinds.includes(node.kind)) push(file, id, "UNKNOWN_COMPONENT", `component ${node.component} cannot render node kind ${node.kind}`); + }); + if (!nodes.has(flow.startNodeId)) push(file, flow.startNodeId, "INVALID_START", "startNodeId does not reference a node"); + const edges = new Map(); + flow.edges.forEach((edge, index) => { + const id = typeof edge?.id === "string" ? edge.id : `edges[${index}]`; + if (!isObject(edge) || !idPattern.test(edge.id ?? "") || typeof edge.source !== "string" || typeof edge.target !== "string" || typeof edge.actionId !== "string") { push(file, id, "SCHEMA_INVALID", "edge requires id, source, target and actionId"); return; } + if (edges.has(id)) return; + edges.set(id, edge); + if (!nodes.has(edge.source)) push(file, id, "DANGLING_REFERENCE", `edge source ${edge.source} does not exist`); + if (!nodes.has(edge.target)) push(file, id, "DANGLING_REFERENCE", `edge target ${edge.target} does not exist`); + const action = registry.actions[edge.actionId]; + if (!action) push(file, edge.actionId, "UNKNOWN_ACTION", `action ${edge.actionId} is not registered`); + else if (nodes.has(edge.source) && !action.sourceKinds.includes(nodes.get(edge.source).kind)) push(file, edge.actionId, "UNKNOWN_ACTION", `action ${edge.actionId} is not allowed from ${nodes.get(edge.source).kind}`); + if ("when" in edge) validateCondition(edge.when, file, id); + }); + if (nodes.has(flow.startNodeId)) { + const reachable = new Set([flow.startNodeId]); + let changed = true; + while (changed) { changed = false; for (const edge of edges.values()) if (reachable.has(edge.source) && nodes.has(edge.target) && !reachable.has(edge.target)) { reachable.add(edge.target); changed = true; } } + for (const id of nodes.keys()) if (!reachable.has(id)) push(file, id, "UNREACHABLE_NODE", `node ${id} is not reachable from ${flow.startNodeId}`); + } + return { nodes, edges }; +} + +function validateLayout(layout, nodes) { + const file = "layout.json"; + if (!isObject(layout) || layout.layoutVersion !== 1 || !isObject(layout.viewport) || !isObject(layout.nodes) || typeof layout.viewport.x !== "number" || !Number.isFinite(layout.viewport.x) || typeof layout.viewport.y !== "number" || !Number.isFinite(layout.viewport.y) || typeof layout.viewport.zoom !== "number" || !Number.isFinite(layout.viewport.zoom) || layout.viewport.zoom <= 0) { push(file, "layout", "SCHEMA_INVALID", "layout requires version 1, viewport and nodes"); return; } + for (const [id, position] of Object.entries(layout.nodes)) { + if (!nodes.has(id)) push(file, id, "DANGLING_REFERENCE", `layout node ${id} does not exist in flow`); + if (!isObject(position) || typeof position.x !== "number" || !Number.isFinite(position.x) || typeof position.y !== "number" || !Number.isFinite(position.y)) push(file, id, "SCHEMA_INVALID", `layout node ${id} requires finite x/y coordinates`); + } +} + +function validateReview(review, nodes, edges) { + const file = "review.json"; + if (!isObject(review) || review.reviewVersion !== 1 || !reviewStatuses.has(review.status) || !Array.isArray(review.comments)) { push(file, "review", "SCHEMA_INVALID", "review requires version 1, status and comments"); return; } + const comments = new Set(); + review.comments.forEach((comment, index) => { + const id = typeof comment?.id === "string" ? comment.id : `comments[${index}]`; + if (!isObject(comment) || !idPattern.test(comment.id ?? "") || typeof comment.body !== "string" || !isObject(comment.target) || !targetKinds.has(comment.target.kind) || typeof comment.target.id !== "string") { push(file, id, "SCHEMA_INVALID", "comment requires id, body and a node/edge/page target"); return; } + if (comments.has(id)) push(file, id, "DUPLICATE_ID", `duplicate comment ID ${id}`); else comments.add(id); + const exists = comment.target.kind === "edge" ? edges.has(comment.target.id) : nodes.has(comment.target.id); + if (!exists) push(file, comment.target.id, "DANGLING_REFERENCE", `review target ${comment.target.id} does not exist in flow`); + if (comment.target.kind === "page" && nodes.has(comment.target.id) && nodes.get(comment.target.id).kind !== "page") push(file, comment.target.id, "DANGLING_REFERENCE", `review page target ${comment.target.id} is not a page node`); + }); +} + +validateRequirements(); +const flow = readJson("flow.json"); +const layout = readJson("layout.json"); +const review = readJson("review.json"); +const graph = flow ? validateFlow(flow) : { nodes: new Map(), edges: new Map() }; +if (layout) validateLayout(layout, graph.nodes); +if (review) validateReview(review, graph.nodes, graph.edges); +if (jsonOutput) console.log(JSON.stringify({ valid: errors.length === 0, errors }, null, 2)); +else if (!quiet) { + if (!errors.length) console.log(`validate: OK (${root})`); + else for (const item of errors) console.error(`${item.file} :: ${item.id} :: ${item.code} :: ${item.message}`); +} +process.exitCode = errors.length ? 1 : 0; diff --git a/skills/product-prototype/assets/studio-template/scripts/vite-server.mjs b/skills/product-prototype/assets/studio-template/scripts/vite-server.mjs new file mode 100644 index 0000000..332173e --- /dev/null +++ b/skills/product-prototype/assets/studio-template/scripts/vite-server.mjs @@ -0,0 +1,43 @@ +import { spawn } from "node:child_process"; +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; + +export async function startViteServer(root, { host = "127.0.0.1", port = 4173 } = {}) { + const viteBin = path.join(root, "node_modules", "vite", "bin", "vite.js"); + if (!fs.existsSync(viteBin)) { + const error = new Error(`DEV_DEPENDENCY_MISSING: Vite is not installed at ${viteBin}`); + error.code = "DEV_DEPENDENCY_MISSING"; + throw error; + } + const child = spawn(process.execPath, [viteBin, "--host", host, "--port", String(port), "--strictPort"], { cwd: root, env: { ...process.env, BROWSER: "none" }, stdio: ["ignore", "pipe", "pipe"] }); + let output = ""; + child.stdout.on("data", (chunk) => { output += chunk.toString(); }); + child.stderr.on("data", (chunk) => { output += chunk.toString(); }); + const url = [`http`, `://`, host, `:`, port].join(""); + try { await waitForHttp(url, child); } + catch (error) { await stopViteServer(child); error.message = `${error.message}\n${output.trim()}`.trim(); throw error; } + return { child, url, output: () => output }; +} +export function stopViteServer(server) { + if (!server || server.exitCode !== null) return Promise.resolve(); + return new Promise((resolve) => { + let settled = false; + const finish = () => { if (settled) return; settled = true; clearTimeout(forceKill); resolve(); }; + const forceKill = setTimeout(() => { if (server.exitCode === null) server.kill("SIGKILL"); finish(); }, 5_000); + server.once("exit", finish); + try { if (!server.kill("SIGTERM")) finish(); } catch { finish(); } + }); +} +function waitForHttp(url, child) { + return new Promise((resolve, reject) => { + const deadline = Date.now() + 20_000; + const retry = () => { if (Date.now() >= deadline) { const error = new Error(`DEV_SERVER_TIMEOUT: Vite did not serve ${url}`); error.code = "DEV_SERVER_TIMEOUT"; reject(error); } else setTimeout(poll, 100); }; + const poll = () => { + if (child.exitCode !== null) { const error = new Error(`DEV_SERVER_EXITED: Vite exited with code ${child.exitCode}`); error.code = "DEV_SERVER_EXITED"; reject(error); return; } + const request = http.get(url, (response) => { response.resume(); if ((response.statusCode ?? 500) < 500) resolve(); else retry(); }); + request.on("error", retry); request.setTimeout(1_000, () => { request.destroy(); retry(); }); + }; + poll(); + }); +} diff --git a/skills/product-prototype/assets/studio-template/snapshots/README.md b/skills/product-prototype/assets/studio-template/snapshots/README.md new file mode 100644 index 0000000..aff7787 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/snapshots/README.md @@ -0,0 +1,6 @@ +# Snapshot handoff + +`device-management.manifest.json` is the source declaration for the device-management screenshot targets. +`npm run snapshot` generates actual rendered screenshots for each declared page and viewport, then writes +output paths, browser environment and `status: generated` to `.prototype-artifacts/snapshots/`. No image +placeholder is valid. diff --git a/skills/product-prototype/assets/studio-template/snapshots/device-management.manifest.json b/skills/product-prototype/assets/studio-template/snapshots/device-management.manifest.json new file mode 100644 index 0000000..bf528b2 --- /dev/null +++ b/skills/product-prototype/assets/studio-template/snapshots/device-management.manifest.json @@ -0,0 +1,8 @@ +{ + "schemaVersion": 1, + "feature": "device-management", + "status": "pending-t5-browser-generation", + "source": "../../examples/device-management/screenshot-manifest.json", + "screenshots": [], + "note": "T4 declares real-render targets only. T5 owns browser generation; no PNG is claimed here." +} diff --git a/skills/product-prototype/assets/studio-template/src/App.tsx b/skills/product-prototype/assets/studio-template/src/App.tsx new file mode 100644 index 0000000..e1739dd --- /dev/null +++ b/skills/product-prototype/assets/studio-template/src/App.tsx @@ -0,0 +1,115 @@ +import { useCallback, useMemo, useRef, useState } from "react"; +import { Background, Controls, Handle, MiniMap, MarkerType, Position, ReactFlow, ReactFlowProvider, applyNodeChanges, type Edge, type Node, type NodeProps, type OnSelectionChangeParams, type Viewport } from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; +import flowSource from "../prototype/flow.json"; +import layoutSource from "../prototype/layout.json"; +import reviewSource from "../prototype/review.json"; +import requirementsSource from "../prototype/requirements.md?raw"; +import type { FlowDocument, FlowEdge, FlowNode, Condition } from "./contracts/flow"; +import type { LayoutDocument } from "./contracts/layout"; +import type { ReviewDocument, ReviewTarget } from "./contracts/review"; +import { captureLayout, downloadText, layoutNodes, parseLayout, serializeLayout } from "./layout"; +import { addReviewComment, exportReview, reviewTargetLabel, updateReviewStatus } from "./review"; +import { createNavigationRuntime, type DispatchResult } from "./runtime"; +import { DeviceManagementPreview, isDevicePreview } from "./examples/device-management/DeviceManagementPreview"; +import { createDeviceMock, reduceDeviceMock, type DeviceDraft, type DeviceScenario } from "./mocks/device-management"; + +const flow = flowSource as FlowDocument; +const initialLayout = layoutSource as LayoutDocument; +const initialReview = reviewSource as ReviewDocument; +const kindLabel: Record = { page: "PAGE", dialog: "DIALOG", state: "STATE" }; +const kindColor: Record = { page: "#74c0fc", dialog: "#f783ac", state: "#63e6be" }; + +function formatCondition(condition: Condition): string { + switch (condition.op) { + case "and": return condition.conditions.map(formatCondition).join(" AND "); + case "or": return condition.conditions.map(formatCondition).join(" OR "); + case "not": return `NOT (${formatCondition(condition.condition)})`; + case "exists": return `${condition.field} exists`; + case "truthy": return `${condition.field} is truthy`; + case "falsy": return `${condition.field} is falsy`; + case "in": + case "notIn": return `${condition.field} ${condition.op} [${condition.value.map(String).join(", ")}]`; + default: return `${condition.field} ${condition.op} ${String(condition.value)}`; + } +} + +function PrototypeNode({ data, selected }: NodeProps>) { + return
+ +
{kindLabel[data.kind]}
+
{data.title}
{data.id} + {data.thumbnail ? {`${data.title} :
No thumbnail asset
} +
{data.component}
+
; +} +const nodeTypes = { prototype: PrototypeNode }; +function selectedTarget(nodeId: string | null, edgeId: string | null): ReviewTarget | null { return edgeId ? { kind: "edge", id: edgeId } : nodeId ? { kind: "node", id: nodeId } : null; } + +function StudioApp() { + const runtime = useMemo(() => createNavigationRuntime(flow), []); + const [nodes, setNodes] = useState[]>(() => layoutNodes(flow, initialLayout)); + const [viewport, setViewport] = useState(initialLayout.viewport); + const [selectedNodeId, setSelectedNodeId] = useState(flow.startNodeId); + const [selectedEdgeId, setSelectedEdgeId] = useState(null); + const [currentNodeId, setCurrentNodeId] = useState(flow.startNodeId); + const [runtimeMessage, setRuntimeMessage] = useState("Ready — choose an Action from the preview."); + const [runtimeError, setRuntimeError] = useState(false); + const [showEmpty, setShowEmpty] = useState(false); + const [deviceScenario, setDeviceScenario] = useState("normal"); + const [deviceMock, setDeviceMock] = useState(() => createDeviceMock("normal")); + const [deviceDraft, setDeviceDraft] = useState(() => createDeviceMock("normal").draft); + const [deviceFormErrors, setDeviceFormErrors] = useState([]); + const [review, setReview] = useState(initialReview); + const [commentBody, setCommentBody] = useState(""); + const [reviewStatus, setReviewStatus] = useState(initialReview.status); + const layoutInput = useRef(null); + const currentNode = flow.nodes.find((node) => node.id === currentNodeId); + const selectedNode = flow.nodes.find((node) => node.id === selectedNodeId); + const selectedEdge = flow.edges.find((edge) => edge.id === selectedEdgeId); + const outgoing = currentNode ? runtime.outgoing(currentNode.id) : []; + const edges = useMemo[]>(() => flow.edges.map((edge: FlowEdge) => ({ + id: edge.id, source: edge.source, target: edge.target, type: "smoothstep", + markerEnd: { type: MarkerType.ArrowClosed, color: "#86a8ca" }, + label: `${edge.actionId}${edge.when ? ` · when ${formatCondition(edge.when)}` : ""}`, + data: edge, labelStyle: { fill: "#c5ddf7", fontSize: 10, fontWeight: 650 }, + labelBgStyle: { fill: "#17263b", fillOpacity: 0.96, stroke: "#365274" }, labelBgPadding: [6, 3] as [number, number], + })), []); + const onSelectionChange = useCallback(({ nodes: selectedNodes, edges: selectedEdges }: OnSelectionChangeParams) => { setSelectedNodeId(selectedNodes[0]?.id ?? null); setSelectedEdgeId(selectedEdges[0]?.id ?? null); }, []); + const dispatchAction = (actionId: string, input: Partial = {}) => { + let context: Record = { showEmpty, deviceScenario, deviceExists: deviceMock.devices.length > 0 }; + if (actionId.startsWith("device.")) { + const mockResult = reduceDeviceMock(deviceMock, actionId, input); + setDeviceMock(mockResult.state); + setDeviceDraft(mockResult.state.draft); + setDeviceFormErrors(mockResult.errors); + if (!mockResult.ok) { setRuntimeError(true); setRuntimeMessage(`FORM_REQUIRED: ${mockResult.errors.join(";")}`); return; } + context = { ...context, ...mockResult.context, deviceExists: mockResult.state.devices.length > 0 }; + } + const result: DispatchResult = runtime.dispatch(currentNodeId, actionId, context); + if (result.ok) { setCurrentNodeId(result.nodeId); setSelectedNodeId(result.nodeId); setSelectedEdgeId(result.edgeId); setRuntimeError(false); setRuntimeMessage(`Navigated via ${result.actionId} → ${result.nodeId}`); } + else { setRuntimeError(true); setRuntimeMessage(`${result.error.code}: ${result.error.message}`); } + }; + const dispatch = (edge: FlowEdge) => dispatchAction(edge.actionId); + const changeDeviceScenario = (scenario: DeviceScenario) => { const nextMock = createDeviceMock(scenario); setDeviceScenario(scenario); setDeviceMock(nextMock); setDeviceDraft(nextMock.draft); setDeviceFormErrors([]); setRuntimeError(false); setRuntimeMessage(`Device mock scenario: ${scenario}`); }; + const updateDeviceDraft = (patch: Partial) => { setDeviceDraft((current) => ({ ...current, ...patch })); setDeviceFormErrors([]); }; + const saveLayout = () => { downloadText("layout.json", serializeLayout(captureLayout(nodes, viewport))); setRuntimeError(false); setRuntimeMessage("layout.json exported from current node coordinates and viewport."); }; + const restoreLayout = async (file: File) => { try { const next = parseLayout(await file.text(), flow); setNodes(layoutNodes(flow, next)); setViewport(next.viewport); setRuntimeError(false); setRuntimeMessage("layout.json restored; coordinates are keyed by stable node ID."); } catch (error) { setRuntimeError(true); setRuntimeMessage(error instanceof Error ? error.message : "LAYOUT_INVALID: unable to restore layout.json"); } }; + const addComment = () => { + const target = selectedTarget(selectedNodeId, selectedEdgeId); if (!target || !commentBody.trim()) return; + try { setReview((current) => addReviewComment(current, flow, { id: `comment.${current.comments.length + 1}`, body: commentBody.trim(), target, status: "open", author: "Studio" })); setCommentBody(""); setRuntimeError(false); setRuntimeMessage(`Review comment attached to ${reviewTargetLabel(target)}.`); } + catch (error) { setRuntimeError(true); setRuntimeMessage(error instanceof Error ? error.message : "DANGLING_TARGET: review target rejected"); } + }; + const exportReviewFile = () => { try { const next = updateReviewStatus(review, reviewStatus); setReview(next); downloadText("review.json", exportReview(next, flow)); setRuntimeError(false); setRuntimeMessage("review.json exported with stable node/edge targets."); } catch (error) { setRuntimeError(true); setRuntimeMessage(error instanceof Error ? error.message : "DANGLING_TARGET: review export rejected"); } }; + const previewNode = selectedNode ?? currentNode; + return
+

PRODUCT PROTOTYPE / T3

Prototype Studio

offline · flow-driven { const file = event.target.files?.[0]; if (file) void restoreLayout(file); event.target.value = ""; }} />
+
+