diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d3a448f..8533d9b 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,15 +5,15 @@ "email": "contact@wigtn.com" }, "metadata": { - "description": "AI-native Vibe Coding — From idea to Deploy, Zero friction. Unified plugin with 11 agents, 5 commands, 7 skills, team-based parallel build, 20 design styles, screen-spec generator.", - "version": "0.1.16" + "description": "AI-native Vibe Coding — From idea to Deploy, Zero friction. Unified plugin with 11 agents, 5 commands, 8 skills, team-based parallel build, 20 design styles, screen-spec generator.", + "version": "0.1.17" }, "plugins": [ { "name": "wigtn-plugins", - "description": "Unified AI-native Vibe Coding plugin: PRD generation, digging analysis, screen-spec (IA / User Flow / Wireframe HTML / Dev Handoff), team-based parallel implementation, quality-gated auto-commit, 20 design styles, frontend (React/Next.js), backend (NestJS/Prisma), mobile (React Native/Expo), AI integration (STT/LLM), DevOps (Docker/CI-CD). 11 agents, 5 commands, 7 skills.", + "description": "Unified AI-native Vibe Coding plugin: PRD generation, digging analysis, screen-spec (IA / User Flow / Wireframe HTML / Dev Handoff), team-based parallel implementation, quality-gated auto-commit, 20 design styles, frontend (React/Next.js), backend (NestJS/Prisma), mobile (React Native/Expo), AI integration (STT/LLM), DevOps (Docker/CI-CD). 11 agents, 5 commands, 8 skills.", "source": "./plugins/wigtn-plugins", - "version": "0.1.16", + "version": "0.1.17", "author": { "name": "WIGTN Crew", "email": "contact@wigtn.com" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 1ae704e..ed4a4e5 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,11 +1,10 @@ { "name": "wigtn-plugins", - "version": "0.1.16", - "description": "AI-native Vibe Coding — From idea to Deploy, Zero friction. Unified plugin with 11 agents, 5 commands, 7 skills, team-based parallel build, 20 design styles.", + "version": "0.1.17", + "description": "AI-native Vibe Coding — From idea to Deploy, Zero friction. Unified plugin with 11 agents, 5 commands, 8 skills, team-based parallel build, 20 design styles.", "author": { "name": "WIGTN Crew", "url": "https://github.com/wigtn" }, - "repository": "https://github.com/wigtn/wigtn-plugins" } diff --git a/CLAUDE.md b/CLAUDE.md index ee60f61..17ef1da 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ A unified Claude Code plugin enabling AI-powered Vibe Coding: idea to production with minimal friction. -**Version**: 0.1.16 +**Version**: 0.1.17 **License**: Apache-2.0 **Repository**: https://github.com/wigtn/wigtn-plugins @@ -14,11 +14,11 @@ A unified Claude Code plugin enabling AI-powered Vibe Coding: idea to production wigtn-plugins/ ├── .claude-plugin/ # Marketplace metadata ├── plugins/ -│ └── wigtn-plugins/ # Unified plugin: 11 agents, 5 commands, 7 skills, 20 design styles +│ └── wigtn-plugins/ # Unified plugin: 11 agents, 5 commands, 8 skills, 20 design styles │ ├── .claude-plugin/ # Plugin metadata │ ├── agents/ # 11 agent definitions │ ├── commands/ # 5 commands (/prd, /screen-spec, /implement, /auto-commit, /review-pr) -│ ├── skills/ # 7 skills (code-review-levels, design-system-reference, handdrawn-diagram, screen-spec, team-memory-protocol, wigtn-ppt) +│ ├── skills/ # 8 skills (code-review-levels, design-system-reference, editorial-diagram, handdrawn-diagram, knowledge-wiki, screen-spec, team-memory-protocol, wigtn-ppt) │ └── hooks/ # Hooks configuration ├── CLAUDE.md # This file ├── README.md # English docs @@ -66,7 +66,7 @@ PRD 리뷰(digging)는 4렌즈 전부 **코드베이스 grounding**만 하던 - **PASS** (커밋): critical 0 AND major 0 AND minor <5 - **Security Critical**: critical의 부분집합 → 항상 FAIL (zero-tolerance, no score-cap hack) - **정밀도**: major+ finding은 게이트 반영 전 adversarial refute 1회로 오탐 강등 -- **하드 게이트 (hook 강제)**: 게이트가 프롬프트라 컨텍스트가 차면 스킵될 수 있으므로, `hooks.json`의 PreToolUse가 `git commit`을 가로채 강제한다. 롤업 PASS 시 `/auto-commit`이 repo 루트(`git rev-parse --show-toplevel`)의 `.wigtn/gate-pass`(mtime = PASS 시각)를 기록하고, 커밋 메시지에 `Quality Score:` 신호가 있는데 30분 내 아티팩트가 없으면 hook이 커밋을 **차단(exit 2)**한다. 수동 커밋(신호 없음)·`--no-review`(신호 제거)는 무마찰. hook·auto-commit 모두 toplevel 기준이라 하위 디렉토리 cwd에서도 일치한다. +- **하드 게이트 (hook 강제)**: 게이트가 프롬프트라 컨텍스트가 차면 스킵될 수 있으므로, `hooks.json`의 PreToolUse가 `git commit`을 가로채 강제한다. 롤업 PASS 시 `/auto-commit`이 repo 루트(`git rev-parse --show-toplevel`)의 `.wigtn/gate-pass`(mtime = PASS 시각)를 기록하고, 커밋 메시지에 `Quality Gate: PASS` 신호(레거시 `Quality Score:`도 인식)가 있는데 30분 내 아티팩트가 없으면 hook이 커밋을 **차단(exit 2)**한다. 수동 커밋(신호 없음)·`--no-review`(신호 제거)는 무마찰. hook·auto-commit 모두 toplevel 기준이라 하위 디렉토리 cwd에서도 일치한다. - **객관 체크 게이트 (기본-on, PASS 날조 방어)**: gate-pass는 모델이 쓰는 아티팩트라 리뷰 날조 시 함께 써질 수 있다. Step 3.5가 빠른 검증 명령(`package.json`의 `typecheck`/`lint`, 또는 `tsconfig.json`)을 감지하면 `.wigtn/checks.sh`를 **자동 생성**하고, hook이 커밋 직전 **직접 실행**해 non-zero면 차단 — 모델이 못 꾸미는 exit code에 게이트를 바인딩("리뷰가 좋았음" → "객관 검증 통과"). 감지 실패 시 무마찰. `.wigtn/checks.sh` 편집(전체 테스트 추가)·삭제로 커스터마이즈/opt-out. ## Conventions @@ -109,6 +109,9 @@ Hooks are defined in `plugins/wigtn-plugins/hooks/hooks.json` and follow the Cla | `plugins/wigtn-plugins/skills/team-memory-protocol/` | Cross-agent shared context management | | `plugins/wigtn-plugins/skills/wigtn-ppt/` | WIGTN-brand HTML presentation generator (Light/Dark themes, principles-based, no template) | | `plugins/wigtn-plugins/skills/wigtn-ppt/references/` | `brand.md` (palette, logo, purple-dot signature), `design-guide.md` (layouts) | +| `plugins/wigtn-plugins/skills/editorial-diagram/` | Editorial diagrams as single HTML + inline SVG — 12 type routing, 4px coordinate tables, `check.py` validator, SVG/PNG export | +| `plugins/wigtn-plugins/skills/editorial-diagram/references/` | `types.md` (12 types), `geometry.md` (coordinate tables), `tokens.md` (semantic tokens + contrast) | +| `assets/` | README diagrams (architecture, pipeline) — `*.html` source + light/dark PNG, generated by the `editorial-diagram` skill. Regeneration commands are in each HTML's header comment | | `plugins/wigtn-plugins/agents/prd-reviewer.md` | PRD digging — 4 adversarial lenses + conditional external grounding (self-contained) | | `plugins/wigtn-plugins/agents/team-build-coordinator.md` | Team-based parallel build orchestration | diff --git a/README.cn.md b/README.cn.md index a2bb04d..f4025fa 100644 --- a/README.cn.md +++ b/README.cn.md @@ -6,10 +6,10 @@ **一个插件。11 个智能体。从创意到通过验证的提交。** -![Version](https://img.shields.io/badge/v0.1.16-Unified_Plugin-FF6B6B?style=for-the-badge) +![Version](https://img.shields.io/badge/v0.1.17-Unified_Plugin-FF6B6B?style=for-the-badge) ![Agents](https://img.shields.io/badge/11-Agents-5A67D8?style=for-the-badge) ![Commands](https://img.shields.io/badge/5-Commands-38B2AC?style=for-the-badge) -![Skills](https://img.shields.io/badge/6-Skills-00D4AA?style=for-the-badge) +![Skills](https://img.shields.io/badge/8-Skills-00D4AA?style=for-the-badge) ![Styles](https://img.shields.io/badge/20-Design_Styles-F59E0B?style=for-the-badge) [![GitHub Stars](https://img.shields.io/github/stars/wigtn/wigtn-plugins?style=flat-square)](https://github.com/wigtn/wigtn-plugins/stargazers) @@ -42,6 +42,16 @@ WIGTN Plugins 是一个 Claude Code 插件,由 11 个专业智能体负责需 /auto-commit → findings 汇总 + 客观检查 + 提交 ``` +

+ + + 自上而下四层:命令(5 个入口)、智能体(11 个执行单元)、技能(8 个参考资产),以及钩子 — 唯一在提示词之外强制执行的一层,在闸门产物与客观检查通过前拦截 git commit。 + +

+ +本图由插件自带的 `editorial-diagram` 技能生成 — 源文件:[`assets/architecture.html`](assets/architecture.html) + --- ## 快速开始 @@ -63,6 +73,21 @@ WIGTN Plugins 是一个 Claude Code 插件,由 11 个专业智能体负责需 ## 流水线 +

+ + + 自上而下五个阶段:/prd 产出 PRD 与计划;digging 由 prd-reviewer 以 4 个对抗视角审查并可拦截;/screen-spec 仅在 PRD 含前端页面时运行;/implement 先分流再执行 DESIGN 与 BUILD,最多 4 个团队并行;/auto-commit 汇总 findings 并运行客观检查后才提交。 + +

+ +独立步骤可以并行,存在依赖的步骤保持顺序。运行时间取决于任务、模型和已配置的检查。 + +源文件:[`assets/pipeline.html`](assets/pipeline.html) + +
+流水线详情 — 点击展开 + ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ /prd "基于 OAuth 的 SaaS 仪表盘" │ @@ -165,7 +190,7 @@ WIGTN Plugins 是一个 Claude Code 插件,由 11 个专业智能体负责需 Layer 3 — PLAN 台账 ─────────── 任务复选框 + 执行日志 ``` -独立步骤可以并行,存在依赖的步骤保持顺序。运行时间取决于任务、模型和已配置的检查。 +
--- @@ -213,13 +238,15 @@ WIGTN Plugins 是一个 Claude Code 插件,由 11 个专业智能体负责需
-技能(6 个) — 点击展开 +技能(8 个) — 点击展开 | 技能 | 提供内容 | |------|---------| | `code-review-levels` | 深度审查(Level 3:调用链、边界情况、并发)和架构审查(Level 4:SOLID、层违规、可扩展性) | | `design-system-reference` | 20 个风格指南 — 排版、色彩、组件、动效、反模式。与 design-discovery 协同进行上下文感知推荐 | +| `editorial-diagram` | 以单个 HTML + 内联 SVG 生成规整(editorial)的架构/流程/对比图。12 种类型路由、预先验算的 4px 坐标表、密度预算、WIGTN 或客户品牌令牌、`check.py` 校验(网格/溢出/对比度/无障碍)、SVG + PNG 导出 | | `handdrawn-diagram` | 通过 Mermaid `look:handDrawn` 生成手绘(草图)风格的架构/流程图,输出可提交的 SVG + PNG。在 README、GitHub、Devpost、幻灯片上渲染一致 | +| `knowledge-wiki` | 通过 4 级导出闸门将会话知识沉淀到团队 wiki。仅在 `~/.config/wigtn/knowledge-wiki.yml` 列出的路径下运行;密钥、个人信息、客户标识在导出前被拦截 | | `screen-spec` | 从 PRD 生成 5 种 UI 产物 — IA、用户流程、屏幕规格、可点击 Wireframe HTML、Dev Handoff。灰度 + 语义色 lo-fi 线框图(风格在 `/implement` 决定)。由 `/screen-spec` 调用 | | `team-memory-protocol` | 并行构建中跨智能体共享上下文(SHARED_CONTEXT)管理 | | `wigtn-ppt` | 基于品牌令牌生成 WIGTN 品牌 HTML 演示文稿(Light/Dark 主题,无需模板)。每张幻灯片带签名紫点,缺少 logo 资源时回退到 CSS/SVG 字标 | @@ -259,15 +286,17 @@ WIGTN Plugins 是一个 Claude Code 插件,由 11 个专业智能体负责需
-钩子(4 个) — 点击展开 +钩子(7 个) — 点击展开 | 钩子 | 触发器 | 功能 | |------|--------|------| -| 危险命令拦截 | `Bash` PreToolUse | 拦截 `rm -rf /`、`git push --force`、`DROP TABLE` | -| 流水线完成 | Stop | 推送前提醒审查 | -| 前端格式化 | `Write\|Edit` PostToolUse | 提醒对 `.tsx`、`.jsx`、`.css` 运行 prettier/eslint | -| 后端模式合规 | `Write\|Edit` PostToolUse | 检查 `.ts`、`.py`、`.go` 的错误处理、验证、日志 | - +| 提交质量闸门 | `Bash` PreToolUse | 运行 `hooks/gate.sh` — 每次提交都执行 `.wigtn/checks.sh`(类型检查 / lint),非零退出即拦截;当提交信息带有 `Quality Gate: PASS` 标记时,另需 30 分钟内的 `.wigtn/gate-pass` 产物 | +| 危险命令拦截 | `Bash` PreToolUse | 拦截对 `/`、`~`、`$HOME` 的递归强制删除,强制推送,硬重置,以及 `DROP DATABASE\|TABLE\|SCHEMA` | +| 闸门健康告警 | SessionStart | 当 `hooks/gate.sh` 路径无法解析时向 stderr 告警 — 此时提交闸门失效,提交会在没有客观检查的情况下通过 | +| 前端格式化 | `Write\|Edit` PostToolUse | 提醒对 `.ts`、`.tsx`、`.js`、`.jsx`、`.css`、`.scss` 运行 prettier/eslint(`.ts`/`.js` 同时触发后端提醒) | +| 后端模式合规 | `Write\|Edit` PostToolUse | 提醒核对 `.ts`、`.js`、`.py`、`.go`、`.java` 的错误处理、输入校验、日志 | +| 流水线完成 | Stop | 推送前提醒审查变更 | +| 知识沉淀 | Stop | 运行 `knowledge_wiki/accumulate.py` — 通过 4 级导出闸门沉淀会话知识(未在 `~/.config/wigtn/knowledge-wiki.yml` 配置时不启用) |
--- diff --git a/README.ko.md b/README.ko.md index c1c5bab..b1fdcfa 100644 --- a/README.ko.md +++ b/README.ko.md @@ -6,10 +6,10 @@ **하나의 플러그인. 11개 에이전트. 아이디어에서 검증된 커밋까지.** -![Version](https://img.shields.io/badge/v0.1.16-Unified_Plugin-FF6B6B?style=for-the-badge) +![Version](https://img.shields.io/badge/v0.1.17-Unified_Plugin-FF6B6B?style=for-the-badge) ![Agents](https://img.shields.io/badge/11-Agents-5A67D8?style=for-the-badge) ![Commands](https://img.shields.io/badge/5-Commands-38B2AC?style=for-the-badge) -![Skills](https://img.shields.io/badge/6-Skills-00D4AA?style=for-the-badge) +![Skills](https://img.shields.io/badge/8-Skills-00D4AA?style=for-the-badge) ![Styles](https://img.shields.io/badge/20-Design_Styles-F59E0B?style=for-the-badge) [![GitHub Stars](https://img.shields.io/github/stars/wigtn/wigtn-plugins?style=flat-square)](https://github.com/wigtn/wigtn-plugins/stargazers) @@ -42,6 +42,16 @@ WIGTN Plugins은 요구사항, 아키텍처, 구현, 리뷰를 담당하는 11 /auto-commit → findings 롤업 + 객관 검증 + 커밋 ``` +

+ + + 위에서 아래로 4개 계층: 커맨드(진입점 5개), 에이전트(실행 주체 11개), 스킬(참조 자산 8개), 그리고 훅 — 프롬프트 밖에서 강제되는 유일한 계층으로, 게이트 아티팩트와 객관 검증을 통과할 때까지 git commit을 차단한다. + +

+ +이 도식은 플러그인 자체의 `editorial-diagram` 스킬로 생성했습니다 — 소스: [`assets/architecture.html`](assets/architecture.html) + --- ## 빠른 시작 @@ -63,6 +73,21 @@ WIGTN Plugins은 요구사항, 아키텍처, 구현, 리뷰를 담당하는 11 ## 파이프라인 +

+ + + 위에서 아래로 5단계: /prd가 PRD와 계획을 만들고, digging은 prd-reviewer의 4개 적대적 렌즈로 검증해 차단할 수 있으며, /screen-spec은 PRD에 FE 페이지가 있을 때만 실행되고, /implement는 triage 후 DESIGN·BUILD를 최대 4개 팀 병렬로 수행하며, /auto-commit은 findings 롤업과 객관 검증을 통과해야 커밋한다. + +

+ +독립적인 단계는 병렬로, 의존성이 있는 단계는 순서대로 실행합니다. 실행 시간은 작업·모델·설정된 검사에 따라 달라집니다. + +소스: [`assets/pipeline.html`](assets/pipeline.html) + +
+파이프라인 상세 — 클릭하여 펼치기 + ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ /prd "OAuth 기반 SaaS 대시보드" │ @@ -165,7 +190,7 @@ WIGTN Plugins은 요구사항, 아키텍처, 구현, 리뷰를 담당하는 11 Layer 3 — PLAN 원장 ─────────── 태스크 체크박스 + 실행 로그 ``` -독립적인 단계는 병렬로, 의존성이 있는 단계는 순서대로 실행합니다. 실행 시간은 작업·모델·설정된 검사에 따라 달라집니다. +
--- @@ -213,13 +238,15 @@ WIGTN Plugins은 요구사항, 아키텍처, 구현, 리뷰를 담당하는 11
-스킬 (6개) — 클릭하여 펼치기 +스킬 (8개) — 클릭하여 펼치기 | 스킬 | 제공하는 것 | |------|-----------| | `code-review-levels` | 심층 리뷰 (Level 3: 호출 체인, 에지 케이스, 동시성) 및 아키텍처 리뷰 (Level 4: SOLID, 계층 위반, 확장성) | | `design-system-reference` | 20개 스타일 가이드 — 타이포그래피, 색상, 컴포넌트, 모션, 안티패턴. design-discovery와 연동하여 컨텍스트 기반 추천 | +| `editorial-diagram` | 정갈한(editorial) 아키텍처/흐름/비교 다이어그램을 단일 HTML + 인라인 SVG로 생성. 12개 유형 라우팅, 미리 검산된 4px 좌표표, 밀도 예산, WIGTN·클라이언트 브랜드 토큰, `check.py` 검증(그리드/오버플로/대비/접근성), SVG + PNG export | | `handdrawn-diagram` | Mermaid `look:handDrawn`로 손그림(스케치) 스타일 아키텍처/플로우 다이어그램을 커밋 가능한 SVG + PNG로 생성. README, GitHub, Devpost, 슬라이드에서 동일하게 렌더 | +| `knowledge-wiki` | 세션 지식을 4단 반출 게이트를 거쳐 팀 위키에 축적. `~/.config/wigtn/knowledge-wiki.yml`에 지정한 경로에서만 동작하며, 시크릿·개인정보·고객 식별 정보는 반출 전에 차단 | | `screen-spec` | PRD로부터 5종 UI 산출물 생성 — IA, User Flow, 화면별 명세, 클릭 가능한 Wireframe HTML, Dev Handoff. 흑백 + 의미색 lo-fi 와이어프레임(스타일은 `/implement`에서 결정). `/screen-spec`에서 호출 | | `team-memory-protocol` | 병렬 빌드 중 에이전트 간 공유 컨텍스트(SHARED_CONTEXT) 관리 | | `wigtn-ppt` | 브랜드 토큰 기반 WIGTN 브랜드 HTML 프레젠테이션 생성(Light/Dark 테마, 템플릿 불필요). 모든 슬라이드에 시그니처 퍼플 점, 로고 에셋이 없으면 CSS/SVG 워드마크 폴백 | @@ -259,15 +286,17 @@ WIGTN Plugins은 요구사항, 아키텍처, 구현, 리뷰를 담당하는 11
-훅 (4개) — 클릭하여 펼치기 - -| 훅 | 트리거 | 기능 | -|----|--------|------| -| 위험 명령 차단 | `Bash` PreToolUse | `rm -rf /`, `git push --force`, `DROP TABLE` 차단 | -| 파이프라인 완료 | Stop | 푸시 전 변경사항 검토 알림 | -| 프론트엔드 포맷팅 | `Write\|Edit` PostToolUse | `.tsx`, `.jsx`, `.css` 파일 prettier/eslint 알림 | -| 백엔드 패턴 준수 | `Write\|Edit` PostToolUse | `.ts`, `.py`, `.go` 파일 에러 핸들링, 입력 검증, 로깅 확인 | - +훅 (7개) — 클릭하여 펼치기 + +| 훅 | 트리거 | 하는 일 | +|----|--------|--------| +| 커밋 품질 게이트 | `Bash` PreToolUse | `hooks/gate.sh` 실행 — 모든 커밋에서 `.wigtn/checks.sh`(타입체크/lint)를 돌려 non-zero면 차단. 커밋 메시지에 `Quality Gate: PASS` 신호가 있으면 30분 이내의 `.wigtn/gate-pass` 아티팩트도 요구 | +| 위험 명령 차단 | `Bash` PreToolUse | `/`·`~`·`$HOME`에 대한 재귀 강제 삭제, force push, hard reset, `DROP DATABASE\|TABLE\|SCHEMA` 차단 | +| 게이트 상태 경고 | SessionStart | `hooks/gate.sh` 경로를 해석하지 못하면 stderr로 경고 — 커밋 게이트가 비활성이라 객관 체크 없이 커밋이 통과함을 알림 | +| 프론트엔드 포맷팅 | `Write\|Edit` PostToolUse | `.ts`, `.tsx`, `.js`, `.jsx`, `.css`, `.scss`에 prettier/eslint 실행 리마인드 (`.ts`/`.js`는 백엔드 리마인드도 같이 발화) | +| 백엔드 패턴 준수 | `Write\|Edit` PostToolUse | `.ts`, `.js`, `.py`, `.go`, `.java`의 에러 처리·입력 검증·로깅 확인 리마인드 | +| 파이프라인 완료 | Stop | 푸시 전 변경사항 검토 리마인드 | +| 지식 축적 | Stop | `knowledge_wiki/accumulate.py` 실행 — 세션 지식을 4단 반출 게이트로 축적 (`~/.config/wigtn/knowledge-wiki.yml` 설정이 없으면 비활성) |
--- diff --git a/README.md b/README.md index 4ddb138..14045f5 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ **One plugin. 11 agents. From idea to a verified commit.** -![Version](https://img.shields.io/badge/v0.1.16-Unified_Plugin-FF6B6B?style=for-the-badge) +![Version](https://img.shields.io/badge/v0.1.17-Unified_Plugin-FF6B6B?style=for-the-badge) ![Agents](https://img.shields.io/badge/11-Agents-5A67D8?style=for-the-badge) ![Commands](https://img.shields.io/badge/5-Commands-38B2AC?style=for-the-badge) -![Skills](https://img.shields.io/badge/6-Skills-00D4AA?style=for-the-badge) +![Skills](https://img.shields.io/badge/8-Skills-00D4AA?style=for-the-badge) ![Styles](https://img.shields.io/badge/20-Design_Styles-F59E0B?style=for-the-badge) [![GitHub Stars](https://img.shields.io/github/stars/wigtn/wigtn-plugins?style=flat-square)](https://github.com/wigtn/wigtn-plugins/stargazers) @@ -42,6 +42,16 @@ WIGTN Plugins is a Claude Code plugin with 11 specialized agents for requirement /auto-commit → Findings rollup + objective checks + commit ``` +

+ + + Four layers, top to bottom: Commands (5 entry points), Agents (11 execution units), Skills (8 reference assets), and Hooks — the only layer that enforces outside the prompt, blocking git commit until the gate artifact and objective checks pass. + +

+ +Diagram generated by this plugin's own `editorial-diagram` skill — source: [`assets/architecture.html`](assets/architecture.html) + --- ## Quick Start @@ -63,6 +73,21 @@ That's it. The plugin handles PRD generation, 4-category quality analysis, archi ## The Pipeline +

+ + + Five stages top to bottom: /prd produces the PRD and plan; digging runs prd-reviewer with four adversarial lenses and can block; /screen-spec runs only when the PRD has frontend pages; /implement triages then runs DESIGN and BUILD with up to four teams in parallel; /auto-commit rolls up findings and runs objective checks before the commit lands. + +

+ +Independent steps can run in parallel; dependent steps remain ordered. Runtime varies with the task, model, and configured checks. + +Source: [`assets/pipeline.html`](assets/pipeline.html) + +
+Full pipeline detail — click to expand + ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ /prd "SaaS dashboard with OAuth" │ @@ -165,7 +190,7 @@ Shared Memory (3 layers): Layer 3 — PLAN ledger ───────── task checkboxes + execution log ``` -Independent steps can run in parallel; dependent steps remain ordered. Runtime varies with the task, model, and configured checks. +
--- @@ -213,13 +238,15 @@ Independent steps can run in parallel; dependent steps remain ordered. Runtime v
-Skills (6) — click to expand +Skills (8) — click to expand | Skill | What it provides | |-------|-----------------| | `code-review-levels` | Deep review (Level 3: call chains, edge cases, concurrency) and architecture review (Level 4: SOLID, layer violations, scalability) | | `design-system-reference` | 20 style guides with typography, color, components, motion, and anti-patterns. Works with design-discovery for context-aware recommendations | +| `editorial-diagram` | Editorial (clean) architecture / flow / comparison diagrams as a single HTML + inline SVG. 12 type routing, precomputed 4px coordinate tables, density budget, WIGTN or client brand tokens, `check.py` validator (grid / overflow / contrast / a11y), SVG + PNG export | | `handdrawn-diagram` | Generates a hand-drawn (sketch-style) architecture or flow diagram as committable SVG + PNG via Mermaid `look:handDrawn`. Renders identically on README, GitHub, Devpost, and slides | +| `knowledge-wiki` | Accumulates session knowledge into a team wiki through a 4-stage export gate. Runs only inside paths listed in `~/.config/wigtn/knowledge-wiki.yml`; secrets, personal data, and customer identifiers are blocked before export | | `screen-spec` | Generates 5 UI artifacts from PRD — IA, User Flow, Screen Spec, clickable Wireframe HTML, Dev Handoff. Lo-fi wireframe (grayscale + semantic colors); style decisions deferred to `/implement`. Invoked by `/screen-spec` | | `team-memory-protocol` | SHARED_CONTEXT management for cross-agent coordination during parallel builds | | `wigtn-ppt` | Generates a WIGTN-brand HTML presentation (Light/Dark themes) from brand tokens — no template needed. Signature purple dot on every slide, with a CSS/SVG wordmark fallback when logo assets are absent | @@ -259,15 +286,17 @@ The `design-discovery` agent recommends the best style for your project context
-Hooks (4) — click to expand +Hooks (7) — click to expand | Hook | Trigger | What it does | |------|---------|-------------| -| Dangerous Command Blocker | `Bash` PreToolUse | Blocks `rm -rf /`, `git push --force`, `DROP TABLE` | -| Pipeline Completion | Stop | Reminds to review before pushing | -| Frontend Formatting | `Write\|Edit` PostToolUse | Reminds prettier/eslint for `.tsx`, `.jsx`, `.css` | -| Backend Pattern Compliance | `Write\|Edit` PostToolUse | Checks error handling, validation, logging for `.ts`, `.py`, `.go` | - +| Commit Quality Gate | `Bash` PreToolUse | Runs `hooks/gate.sh` — executes `.wigtn/checks.sh` (typecheck / lint) on every commit and blocks on a non-zero exit; additionally requires a fresh `.wigtn/gate-pass` artifact when the message carries a `Quality Gate: PASS` signal | +| Dangerous Command Blocker | `Bash` PreToolUse | Blocks recursive force-removal of `/`, `~` or `$HOME`, force-push, hard reset, and `DROP DATABASE\|TABLE\|SCHEMA` | +| Gate Health Warning | SessionStart | Warns on stderr when `hooks/gate.sh` cannot be resolved — the commit gate is inactive and commits would land without objective checks | +| Frontend Formatting | `Write\|Edit` PostToolUse | Reminds prettier/eslint for `.ts`, `.tsx`, `.js`, `.jsx`, `.css`, `.scss` (`.ts`/`.js` also trigger the backend reminder) | +| Backend Pattern Compliance | `Write\|Edit` PostToolUse | Reminds to verify error handling, input validation, logging for `.ts`, `.js`, `.py`, `.go`, `.java` | +| Pipeline Completion | Stop | Reminds to review changes before pushing | +| Knowledge Accumulation | Stop | Runs `knowledge_wiki/accumulate.py` — captures session knowledge through the 4-stage export gate (inactive unless configured in `~/.config/wigtn/knowledge-wiki.yml`) |
--- diff --git a/assets/architecture-dark.png b/assets/architecture-dark.png new file mode 100644 index 0000000..1f978da Binary files /dev/null and b/assets/architecture-dark.png differ diff --git a/assets/architecture-light.png b/assets/architecture-light.png new file mode 100644 index 0000000..3369148 Binary files /dev/null and b/assets/architecture-light.png differ diff --git a/assets/architecture.html b/assets/architecture.html new file mode 100644 index 0000000..9d10667 --- /dev/null +++ b/assets/architecture.html @@ -0,0 +1,166 @@ + + + + +wigtn-plugins architecture + + + + + + +
+ + wigtn-plugins architecture + Four layers, top to bottom. Commands are the five entry points. Agents are the eleven execution units for requirements, architecture, build teams and review. Skills are the eight reference assets for specs, design systems, diagrams, presentations and shared memory. Hooks sit underneath and enforce outside the prompt, blocking git commit until the gate artifact and objective checks pass. + + + + + + Plugin architecture + Entry points on top · each layer reads the one below · hooks enforce outside the prompt + + + Commands (5) + /prd → /screen-spec → /implement → /auto-commit · /review-pr + + + Agents (11) + requirements · architecture · build teams (backend / frontend / mobile / ai) · review + + + Skills (8) + screen specs · design systems (20 styles) · diagrams · presentations · shared memory + + + + Hooks + enforcement outside the prompt — blocks git commit until the gate artifact and checks pass + + generated by the editorial-diagram skill · source: assets/architecture.html + +
+ +
+ + wigtn-plugins architecture (dark) + Four layers, top to bottom. Commands are the five entry points. Agents are the eleven execution units for requirements, architecture, build teams and review. Skills are the eight reference assets for specs, design systems, diagrams, presentations and shared memory. Hooks sit underneath and enforce outside the prompt, blocking git commit until the gate artifact and objective checks pass. + + + + + + Plugin architecture + Entry points on top · each layer reads the one below · hooks enforce outside the prompt + + + Commands (5) + /prd → /screen-spec → /implement → /auto-commit · /review-pr + + + Agents (11) + requirements · architecture · build teams (backend / frontend / mobile / ai) · review + + + Skills (8) + screen specs · design systems (20 styles) · diagrams · presentations · shared memory + + + + Hooks + enforcement outside the prompt — blocks git commit until the gate artifact and checks pass + + generated by the editorial-diagram skill · source: assets/architecture.html + +
+ + + diff --git a/assets/pipeline-dark.png b/assets/pipeline-dark.png new file mode 100644 index 0000000..2526130 Binary files /dev/null and b/assets/pipeline-dark.png differ diff --git a/assets/pipeline-light.png b/assets/pipeline-light.png new file mode 100644 index 0000000..59c0fb3 Binary files /dev/null and b/assets/pipeline-light.png differ diff --git a/assets/pipeline.html b/assets/pipeline.html new file mode 100644 index 0000000..97e781f --- /dev/null +++ b/assets/pipeline.html @@ -0,0 +1,211 @@ + + + + +wigtn-plugins pipeline + + + + + + +
+ + wigtn-plugins pipeline + Five stages top to bottom. Slash prd produces the PRD and plan. Digging runs prd-reviewer with four adversarial lenses and is a gate: BLOCKED stops the pipeline, PASS continues. Slash screen-spec runs only when the PRD has frontend pages and produces IA, flow, spec and wireframe. Slash implement triages then runs DESIGN and BUILD with up to four teams in parallel. Slash auto-commit is the second gate: the findings rollup plus objective checks must pass before the commit lands. + + + + + + + + + + + + The pipeline + Left — what runs · Right — what comes out · Purple marks the two gates that can stop you + + + /prd + context scan + AskUserQuestion + PRD_*.md · PLAN_*.md + + + + digging + prd-reviewer · 4 lenses + BLOCKED → stop · PASS → go + + + /screen-spec + only when the PRD has FE pages + IA · flow · spec · wireframe + + + /implement + triage → DESIGN → BUILD + up to 4 teams in parallel + + + + /auto-commit + code-reviewer findings + rollup + checks → commit + + + + + + + generated by the editorial-diagram skill · source: assets/pipeline.html + +
+ +
+ + wigtn-plugins pipeline (dark) + Five stages top to bottom. Slash prd produces the PRD and plan. Digging runs prd-reviewer with four adversarial lenses and is a gate: BLOCKED stops the pipeline, PASS continues. Slash screen-spec runs only when the PRD has frontend pages and produces IA, flow, spec and wireframe. Slash implement triages then runs DESIGN and BUILD with up to four teams in parallel. Slash auto-commit is the second gate: the findings rollup plus objective checks must pass before the commit lands. + + + + + + + + + + + + The pipeline + Left — what runs · Right — what comes out · Purple marks the two gates that can stop you + + + /prd + context scan + AskUserQuestion + PRD_*.md · PLAN_*.md + + + + digging + prd-reviewer · 4 lenses + BLOCKED → stop · PASS → go + + + /screen-spec + only when the PRD has FE pages + IA · flow · spec · wireframe + + + /implement + triage → DESIGN → BUILD + up to 4 teams in parallel + + + + /auto-commit + code-reviewer findings + rollup + checks → commit + + + + + + + generated by the editorial-diagram skill · source: assets/pipeline.html + +
+ + + diff --git a/plugins/wigtn-plugins/.claude-plugin/plugin.json b/plugins/wigtn-plugins/.claude-plugin/plugin.json index 8c7776b..8fbd157 100644 --- a/plugins/wigtn-plugins/.claude-plugin/plugin.json +++ b/plugins/wigtn-plugins/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "wigtn-plugins", - "version": "0.1.16", - "description": "AI-native Vibe Coding — From idea to Deploy, Zero friction. 11 agents, 5 commands, 7 skills, team-based parallel build, 20 design styles, screen-spec generator (IA / User Flow / Wireframe HTML), WIGTN-brand PPT generator.", + "version": "0.1.17", + "description": "AI-native Vibe Coding — From idea to Deploy, Zero friction. 11 agents, 5 commands, 8 skills, team-based parallel build, 20 design styles, screen-spec generator (IA / User Flow / Wireframe HTML), WIGTN-brand PPT generator, editorial diagram generator.", "agents": [ "./agents/ai-agent.md", "./agents/architecture-decision.md", @@ -27,6 +27,7 @@ "./skills/design-system-reference", "./skills/screen-spec", "./skills/code-review-levels", + "./skills/editorial-diagram", "./skills/handdrawn-diagram", "./skills/wigtn-ppt", "./skills/knowledge-wiki" diff --git a/plugins/wigtn-plugins/skills/editorial-diagram/SKILL.md b/plugins/wigtn-plugins/skills/editorial-diagram/SKILL.md new file mode 100644 index 0000000..b0ae248 --- /dev/null +++ b/plugins/wigtn-plugins/skills/editorial-diagram/SKILL.md @@ -0,0 +1,176 @@ +--- +name: editorial-diagram +description: >- + 구조·흐름·비교를 설명하는 정갈한(editorial) 다이어그램을 단일 HTML + 인라인 SVG로 + 생성한다. 4px 그리드·밀도 예산·단일 악센트 규칙으로 "AI가 그린 티"를 제거하고, + WIGTN 브랜드 토큰(또는 클라이언트 토큰)을 적용한다. 발표 슬라이드·제안서·문서용. + SVG/PNG로 export 가능. 손그림 톤이 필요하면 handdrawn-diagram, 수치 차트는 + dataviz 스킬을 쓴다. Triggers on: '다이어그램', '아키텍처 다이어그램', '구조도', + '도식', '플로우 그려줘', '시스템 구성도', '스윔레인', '타임라인 그려줘', '로드맵 도식', + '2x2', '사분면', '레이어 구조', 'diagram', 'architecture diagram', 'flow diagram', + 'swimlane', 'quadrant', 'layer stack', 'editorial diagram', '슬라이드용 도식'. +allowed-tools: Read, Write, Edit, Bash, Glob, Grep +--- + +# Editorial Diagram (단일 HTML + 인라인 SVG) + +좌표를 손으로 찍는 SVG는 **규칙 없이 그리면 반드시 어긋난다.** 이 스킬은 산문 조언이 +아니라 **고정된 좌표표 + 검증 스크립트**로 그 실패를 막는다. 좌표를 창작하지 말고 +[references/geometry.md](references/geometry.md)의 표에서 **읽어 쓴다.** + +## 이 스킬을 쓰지 않는 경우 + +| 요청 | 대신 | 이유 | +|---|---|---| +| 손그림·스케치 톤, README/Devpost | `handdrawn-diagram` | Mermaid `look:handDrawn` | +| 막대·꺾은선·산점도·히트맵 등 **수치 차트** | `dataviz` | 축·스케일·팔레트 규칙이 다름 | +| 화면 레이아웃/와이어프레임 | `screen-spec` | UI 명세 도메인 | +| 목록·전후 비교표·도형 1개 | 그냥 텍스트/표 | 다이어그램이 값을 더하지 않음 | + +**판별 질문**: *"잘 쓴 문단 하나보다 이 그림이 더 알려주는가?"* 아니면 그리지 않는다. + +## 워크플로우 + +### Step 1 — 요청 계약 (4요소) + +아래 4가지가 확정되어야 그린다. 빠진 것은 **한 번에 묶어** 묻거나, 맥락에서 +명백하면 가정하고 **명시적으로 선언**한다. + +| 요소 | 예시 | 없으면 생기는 일 | +|---|---|---| +| **내용** | "프론트 → 게이트웨이 → 주문 서비스 → DB" | 무엇을 그릴지 모름 | +| **도착지** | 16:9 슬라이드 / 문서 인라인 / OG 이미지 | 캔버스·타이포 램프 결정 불가 | +| **청중** | 임원 / 혼합 / 엔지니어 | 용어 깊이·노드 수 결정 불가 | +| **강조점** | "주문 서비스 병목만 강조" | 악센트를 어디 쓸지 모름 (→ 전부 균일 = 밋밋) | + +### Step 2 — 렌더 전 선언 (필수) + +그리기 **전에** 한 단락으로 말한다. 이걸 건너뛰면 사용자는 완성된 뒤에야 +잘못된 유형이었음을 안다. + +``` +유형: 파이프라인 (4노드 가로) · 캔버스: slide-16x9 (1280×720) +강조: 주문 서비스 1개 (퍼플 점) +뺀 것: 캐시 계층, 리트라이 로직 — 임원 청중이라 3단계로 압축 +``` + +### Step 3 — 유형 선택 + +[references/types.md](references/types.md)에서 고른다. 12개 유형 각각에 +"언제 쓰나 / 언제 쓰면 안 되나 / 노드 예산 / 어느 좌표표를 쓰나"가 있다. + +### Step 4 — 토큰 로드 + +[references/tokens.md](references/tokens.md)를 Read. 기본은 **WIGTN Light**. +클라이언트 제안서면 클라이언트 토큰으로 오버라이드(같은 파일에 절차 있음). + +### Step 5 — 작성 + +1. `assets/template.html`을 복사해 시작한다. 스캐폴드에 토큰·마커·a11y·폰트가 이미 있다. +2. [references/geometry.md](references/geometry.md)의 좌표표를 **그대로 옮겨 적는다.** + 계산하지 않는다. 표에 없는 배치가 필요하면 표의 규칙(마진 64, 4의 배수)으로 파생한다. +3. 라벨은 **글자 수 예산**을 지킨다(geometry.md §라벨 예산). 초과하면 줄이거나 `` 2줄. + +### Step 6 — 검증 (건너뛰지 않는다) + +```bash +python3 "${CLAUDE_PLUGIN_ROOT}/skills/editorial-diagram/scripts/check.py" out.html +``` + +4px 위반·밀도 초과·라벨 오버플로·악센트 남용·a11y 누락·대비 미달을 잡는다. +**FAIL이면 고치고 다시 돌린다.** PASS 후 PNG를 렌더해 눈으로 한 번 더 본다. + +### 검증기의 계약 (지키면 통과, 벗어나면 FAIL) + +`check.py`는 **임의 SVG 검증기가 아니다.** 브라우저를 다시 구현하지 않으므로, +`template.html`이 만드는 *정규 형태*만 검증하고 그 밖은 조용히 통과시키지 않고 끊는다. +검증할 수 없는 것을 통과시키면 게이트가 꺼진 줄도 모르게 꺼진다. + +| 규칙 | 벗어나면 | +|---|---| +| 색은 **3/6자리 HEX 또는 선언된 `var(--토큰)`만** — 토큰뿐 아니라 **모든 도형의 `fill`/`stroke`**에 적용 (`rgb()`·`color-mix()`·색이름 금지, `none`/`transparent`는 허용) | FAIL | +| 노드는 `rect`/`polygon`/`circle`/`ellipse` + `class="node"` (`g`·`path` 노드 금지) | FAIL | +| 폰트 크기는 `px`/`rem`/`em`만 (`pt`·`%`·`calc()` 금지) | FAIL | +| 색·폰트·정렬은 **클래스 규칙**으로 (`text`/`tspan`/`g`의 표현 속성·인라인 style 금지) | FAIL | +| ``는 묶기 전용, ``은 줄바꿈 전용 — 둘 다 `class`·`style` 금지 | FAIL | +| 루트 ``에 `fill`/`stroke`/`font-*`/`text-anchor` 선언 금지 (상속으로 전부 바뀐다) | FAIL | +| `--토큰`은 **루트에서만** 선언 (테마 변형 `[data-theme=…]`은 허용) | FAIL | +| 셀렉터는 `type`/`.class`/`#id`/`[attr]`/`[attr=v]`만 (결합자·부분일치 `^=` 금지, 콤마 목록은 지원) | FAIL | +| 렌더 요소는 화이트리스트만 (`use`/`foreignObject`/`image`/**중첩 `svg`** 금지) | FAIL | +| 좌표에 단위 접미사 금지 (`x="64px"`), `rx`·`ry` ∈ {0,4,8}, 음수 폭 금지 | FAIL | +| 악센트로 초점을 **채우지** 않고, 노드 테두리에 `--line`을 쓰지 않는다 (리터럴 HEX도 검사) | FAIL | +| 캔버스는 `viewBox`로 지정 (export도 여기서 크기를 읽는다) | FAIL | + +**다크 블록을 선언하면 다크 팔레트도 함께 검증한다** — 두 테마 모두 대비를 통과해야 +한다. `--theme`으로 한쪽만 강제할 수 있다. + +`text-anchor`를 지정하지 않으면 **SVG 기본값 `start`**로 측정한다. 가운데 정렬 라벨은 +`.node-name`처럼 클래스에 `text-anchor: middle`을 명시해야 한다(템플릿에 이미 있다). + +템플릿에서 시작하면 이 계약은 자동으로 지켜진다. 어겼다는 FAIL이 나오면 문법을 +템플릿 형태로 되돌리는 게 정답이지, 검증기를 우회할 방법을 찾는 게 아니다. + +검증기 자체를 고쳤다면 회귀 스위트를 돌린다(우회 경로마다 픽스처 1개): + +```bash +python3 "${CLAUDE_PLUGIN_ROOT}/skills/editorial-diagram/scripts/selftest.py" +``` + +### Step 7 — Export (요청 시) + +```bash +bash "${CLAUDE_PLUGIN_ROOT}/skills/editorial-diagram/scripts/export.sh" out.html --scale 2 +``` + +`--svg-only` / `--png-only` / `--scale 3`. 상세는 스크립트 `--help`. + +## 하드 규칙 (위반 시 check.py FAIL) + +1. **4px 그리드** — **도형**(`rect`/`circle`/`line`/`polygon`/`path`)의 모든 좌표·폭· + 높이·간격·반지름이 4의 배수. 예외: ``의 x/y(광학 중심 정렬), `stroke-width`, + `opacity`, `font-size`, `viewBox`. 이 규칙 하나가 "어설퍼 보임"의 대부분을 없앤다. +2. **밀도 예산** — 노드 4~5개가 이상적, **9개 초과 금지**. 초과하면 그룹으로 묶거나 + 다이어그램을 쪼갠다. *가장 좋은 수정은 삭제다.* +3. **단일 악센트, 1~2개 요소** — 악센트(퍼플)는 초점 요소에만. 나머지는 중립. + 초점은 **화려한 채움이 아니라 WIGTN 시그니처 퍼플 점 + 2px 테두리**로 표시한다. +4. **1px 헤어라인, 그림자 금지, `rx` ∈ {0, 4, 8}** — `rx`도 4px 검사 대상이라 10은 통과하지 + 못한다(기본 8). 카드 그림자·글로우·그라데이션 배경 금지. +5. **3폰트 역할 고정** — 제목 Space Grotesk / 노드명 Pretendard / 기술 라벨 JetBrains Mono. + 역할을 섞지 않는다. +6. **a11y** — `role="img"` + `` + ``, `aria-labelledby`로 연결. + ID는 다이어그램마다 접두사를 달리한다(한 페이지에 여러 개 인라인될 수 있다). +7. **토큰은 `` 안 ` + + +Editorial Diagram + + + +
+ + + + 주문 처리 파이프라인 + API 게이트웨이가 요청을 받아 주문 서비스로 전달하고, 주문 서비스가 주문 DB에 기록한다. 주문 서비스가 현재 병목 지점이다. + + + + + + + + + + + + + + 주문 처리 파이프라인 + AS-IS · 임원 보고용 3단계 요약 + + + + API 게이트웨이 + envoy · p95 12ms + + + 주문 서비스 + order-svc · 3 replicas + + + + + 주문 DB + postgres · p95 780ms + + + + + + + source: 2026-08 APM p95 · 캐시/큐 계층 생략 + + + +
+ + diff --git a/plugins/wigtn-plugins/skills/editorial-diagram/references/geometry.md b/plugins/wigtn-plugins/skills/editorial-diagram/references/geometry.md new file mode 100644 index 0000000..08f2d5b --- /dev/null +++ b/plugins/wigtn-plugins/skills/editorial-diagram/references/geometry.md @@ -0,0 +1,296 @@ +# Geometry — 좌표는 창작하지 말고 읽어 쓴다 + +여기 있는 숫자는 전부 **4의 배수**로 미리 검산된 값이다. 표에서 그대로 옮겨 적으면 +`check.py`의 4px 검사를 자동으로 통과한다. 눈대중 보정이 4px 위반의 주범이다. + +--- + +## 1. 캔버스 + +**기본 캔버스는 `1280 × 720` 하나다.** SVG는 해상도 독립이므로 문서 인라인용이라고 +캔버스를 줄이지 않는다 — CSS 폭만 줄이면 된다. 캔버스를 바꾸는 건 **가로세로비가 +진짜 다를 때뿐**이다. + +| 프리셋 | viewBox | 마진 | 언제 | +|---|---|---|---| +| **slide-16x9** (기본) | `0 0 1280 720` | 64 | 슬라이드, 문서, README 전부 | +| slide-4x3 | `0 0 1024 768` | 64 | 4:3 프로젝터 강제일 때만 | +| social-og | `0 0 1200 630` | 56 | OG 이미지 | + +아래 좌표표는 전부 **slide-16x9 기준**이다. 다른 프리셋은 §7의 파생 규칙을 쓴다. + +### 1.1 slide-16x9 밴드 + +``` +x: 64 ─────────────────────── 1216 (content width W = 1152) +y: 64 제목 밴드 시작 (title baseline 96, font 28) + 124 부제/캡션 baseline (font 15, muted) + 160 ── 본문 영역 시작 ── (body H = 496) + 656 ── 본문 영역 끝 ── + 692 출처/각주 baseline (font 12, muted) +``` + +본문 영역 밖으로 도형을 내보내지 않는다. 제목이 없는 다이어그램이면 본문 영역을 +`y: 96 → 656`으로 넓혀도 된다(위쪽 64만 회수). + +--- + +## 2. Row — 가로 배치 (파이프라인 · 아키텍처 · 카드) + +**X 좌표표** (마진 64 기준, 마지막 박스 우변은 항상 1216) + +| N | 박스 폭 w | 간격 | x 위치 | +|---|---|---|---| +| 2 | 544 | 64 | `64, 672` | +| 3 | 352 | 48 | `64, 464, 864` | +| 4 | 240 | 64 | `64, 368, 672, 976` | +| 5 | 208 | 28 | `64, 300, 536, 772, 1008` | +| 6 | 152 | 48 | `64, 264, 464, 664, 864, 1064` | + +> **모든 폭이 8의 배수다.** 그래야 박스 중심(`x + w/2`)도 4의 배수가 되어, +> 중심에서 출발하는 선(Sequence 라이프라인, Tree 연결선)이 4px 검사를 통과한다. +> +> **화살표가 들어가는 파이프라인은 N ≤ 4.** N=5는 간격이 28이라 화살표가 눌린다. +> 5개 이상이면 화살표 없는 카드 나열로 쓰거나, 단계를 묶어 4개로 줄인다. + +**Y 좌표표** — 박스 높이는 **행 수에 따라 바뀐다.** 행이 적을수록 키운다. 1행짜리를 +h=96으로 그리면 16:9 캔버스에 여백만 남아 "덜 만든 그림"으로 읽힌다. + +| 행 수 | 박스 h | 세로 간격 | y 위치 | +|---|---|---|---| +| 1 | **160** | — | `328` | +| 2 | **120** | 64 | `256, 440` | +| 3 | **96** | 64 | `200, 360, 520` | + +행이 2줄 이상이면 노드 예산(≤9)에 특히 주의한다 — 3행 × 4열 = 12는 이미 초과다. + +--- + +## 3. Column — 세로 흐름 (플로차트 · 순차 단계) + +박스는 가로 중앙 정렬: **x = 464, w = 352** (또는 x = 512, w = 256 좁은 버전). + +| 단계 수 | h | 간격 | y 위치 | +|---|---|---|---| +| 3 | 88 | 48 | `228, 364, 500` | +| 4 | 88 | 48 | `160, 296, 432, 568` | +| 5 | 72 | 32 | `164, 268, 372, 476, 580` | + +6단계 이상은 세로로 안 들어간다 — 가로 Row로 바꾸거나 단계를 묶는다. + +### 척추 + 여백 주석 (spine & marginalia) + +세로 흐름은 좌우가 비어 보인다. 그 여백을 **주석 컬럼 2개**로 쓰면 단계마다 훨씬 많은 +정보를 담으면서도 노드 수는 그대로다(주석은 노드가 아니다). + +| 영역 | 범위 | 정렬 | 담는 것 | +|---|---|---|---| +| 좌측 주석 | x `64 → 440` (376) | `class="sublabel end"` at x=**440** | 무엇이 실행되나(에이전트·조건) | +| 단계 | x `464 → 816` (352) | 중앙 | 단계 이름만 | +| 우측 주석 | x `840 → 1216` (376) | `class="sublabel start"` at x=**840** | 무엇이 나오나(산출물·분기) | + +주석 baseline은 **단계 이름과 같은 값**을 쓴다(같은 줄에 정렬). 주석 폭 예산은 376 − 0 += 376px → 등폭 13px 기준 **48자**. 좌우 24px 간격은 고정. + +**분기(다이아몬드)**: 마름모는 `w = 224, h = 88`. 중앙 x = 528(폭 224 기준). +분기 후 좌/우 갈래는 x = `160` (좌), `800` (우), w = 320. +**모든 분기는 라벨을 단다** — `Yes` / `No`, `성공` / `실패`. 라벨 없는 분기는 +직선처럼 읽혀 "이럴 땐 / 저럴 땐"의 의미를 잃는다. + +--- + +## 4. Layer stack — 레이어 (기술 스택 · 거버넌스 계층) + +전폭 바: **x = 64, w = 1152**. + +| 레이어 수 | h | 간격 | y 위치 | +|---|---|---|---| +| 3 | 120 | 40 | `188, 348, 508` | +| 4 | 96 | 32 | `168, 296, 424, 552` | +| 5 | 80 | 24 | `160, 264, 368, 472, 576` | + +레이어 안에 하위 블록을 넣을 땐 좌우 패딩 24, 하위 블록 간격 16을 쓴다. +레이어 이름은 좌측 정렬(x = 88), 하위 블록은 우측 영역에 배치. + +--- + +## 5. 나머지 유형 좌표 + +### 5.1 Swimlane — 스윔레인 (부서 간 프로세스) + +- **라벨 거터**: x `64 → 240` (w 176). 레인 이름은 여기, 우측 정렬(x = 224). +- **레인 영역**: x `256 → 1216` (w 960). + +| 레인 수 | 레인 h | 간격 | 레인 y | +|---|---|---|---| +| 3 | 144 | 16 | `176, 336, 496` | +| 4 | 112 | 16 | `160, 288, 416, 544` | + +**레인 안 단계 컬럼** (레인 영역 960 기준) + +| 단계 수 | w | 간격 | x 위치 | +|---|---|---|---| +| 4 | 216 | 32 | `256, 504, 752, 1000` | +| 5 | 160 | 40 | `256, 456, 656, 856, 1056` | + +단계 박스 높이는 레인 h - 48, y는 레인 y + 24. + +### 5.2 Timeline / Roadmap — 타임라인 + +축: `y = 400`, x `64 → 1216`, stroke 1, `--line-strong`. + +| 마커 수 | x 위치 | +|---|---| +| 3 | `208, 640, 1072` | +| 4 | `208, 496, 784, 1072` | +| 5 | `208, 424, 640, 856, 1072` | +| 6 | `160, 352, 544, 736, 928, 1120` | + +마커는 r = 8 원. **라벨 baseline**: 위 `336`, 아래 `464`. +마커가 5개 이상이면 위/아래를 **번갈아** 배치해 충돌을 피한다. + +### 5.3 Quadrant — 2×2 사분면 + +- 플롯 영역: x `320 → 960` (640), y `160 → 640` (480). 셀 = 320 × 240. +- 셀 중심: `(480, 280) (800, 280) (480, 520) (800, 520)` +- 축선: 세로 x = 640, 가로 y = 400. stroke 1, `--line-strong`. +- 축 라벨: 가로축 baseline `676` (중앙 640), 세로축은 x = `284`에서 `rotate(-90)`. +- 셀 이름은 셀 중심 상단(중심 y - 32), 항목은 그 아래. + +### 5.4 Venn — 벤 다이어그램 (2~3 집합만) + +- 2원: r = 160, cy = 400, cx = `560, 720` (겹침 160) +- 3원: r = 144, 중심 `(568, 336) (712, 336) (640, 464)` +- 채움 `--paper-2` + `fill-opacity 0.6`, stroke 1 `--line-strong`. 4집합 이상 금지. + +### 5.5 Funnel / Pyramid — 퍼널·피라미드 (4단) + +| 단 | y | h | w | x | +|---|---|---|---|---| +| 1 | 176 | 104 | 1152 | 64 | +| 2 | 296 | 104 | 928 | 176 | +| 3 | 416 | 104 | 704 | 288 | +| 4 | 536 | 104 | 480 | 400 | + +3단이면 2·3·4단(y 296/416/536)을 쓰고 위를 비운다. + +### 5.6 Tree / Org — 트리·조직도 + +- 루트: x = 512, w = 256, y = 176, h = 80 +- 2단: y = 344, 3단: y = 512 (h 80 동일) +- 각 단의 x는 **§2 Row X 좌표표**를 그대로 쓴다(폭만 표대로). +- 연결선은 직각 꺾임(`L` 두 번), 사선 금지. + +### 5.7 Matrix — 비교 매트릭스 + +- 헤더 행: y = 176, h = 56. 데이터 행: y = `240, 312, 384, 456, 528` (h 64, 간격 8) +- 컬럼 x는 §2 Row X 좌표표. 첫 컬럼(행 이름)은 좌측 정렬, 나머지는 중앙. +- 셀 구분은 채움이 아니라 **1px `--line` 가로줄**로. + +--- + +## 6. 노드 내부 스펙 + +```svg + +주문 서비스 +order-svc · 3 replicas +``` + +> 노드 도형에는 반드시 `class="node"`를 준다 — `check.py`의 밀도·오버플로 검사가 +> 이 클래스로 노드를 찾는다. + +| 항목 | 값 | +|---|---| +| `rx` | **8** — `rx`도 4px 검사 대상이라 `{0, 4, 8}`만 유효(10은 FAIL) | +| 채움 / 테두리 | `--paper-2` / **`--line-strong`**, stroke-width **1** | +| 좌우 패딩 | 20 | +| 노드명 | Pretendard 500, **18px**, `--ink`, `text-anchor: middle` | +| sublabel | JetBrains Mono 400, **13px**, `--muted` | + +**baseline 표** (박스 상단 `y` 기준 오프셋) + +| 박스 h | 2행: 노드명 / sublabel | 단일행 | +|---|---|---| +| 160 | `y + 74` / `y + 98` | `y + 86` | +| 120 | `y + 54` / `y + 78` | `y + 66` | +| 96 | `y + 42` / `y + 66` | `y + 54` | +| 88 | — | `y + 50` | +| 80 | — | `y + 46` | +| 72 | — | `y + 42` | + +> ``의 `x`/`y`는 **4px 규칙 대상이 아니다.** 베이스라인은 광학 중심에 맞춰야 +> 하므로 4의 배수로 강제하면 오히려 어긋난다. 4px 규칙은 `rect`/`circle`/`line`/ +> `polygon` 등 **도형 좌표와 치수**에만 적용된다. + +### 초점(focal) 표시 — 채우지 말고 점을 찍는다 + +```svg + + +``` + +`r = 8`(4의 배수)에 중심을 노드 우상단에서 24씩 안쪽으로. 타임라인 마커 등 점 자체가 +노드인 경우도 `r = 8`로 통일한다. + +초점은 **1~2개**. 3개째부터는 초점이 아니라 배경이다. + +### 커넥터 + +```svg + + + + + + +``` + +- Row 커넥터의 y는 **박스 y + h/2** (h=96 → y+48). +- 꺾일 땐 `` 직각. 사선·베지어 금지. +- 교차가 3개 이상 = 유형 선택 오류. 스윔레인/레이어로 바꾼다. +- `stroke-width`는 4px 규칙 **대상 아님** (1, 1.5, 2 사용). + +--- + +## 7. 라벨 예산 (오버플로 방지) + +SVG ``는 넘쳐도 잘리지 않고 **박스 밖으로 삐져나온다.** 미리 글자 수를 지킨다. + +폭 추정: **한글·CJK ≈ 1.0 × font-size, 비례 Latin ≈ 0.55 × font-size, +등폭(JetBrains Mono) Latin ≈ 0.6 × font-size** — 글자당. +가용 폭 = 박스 폭 − 40 (좌우 패딩 20). + +sublabel은 **등폭**이라 같은 글자 수라도 노드명보다 넓다. 이걸 놓치면 넘친다. + +| 박스 폭 | 가용 | 노드명(18px) Latin | 노드명(18px) 한글 | sublabel(13px 등폭) | +|---|---|---|---|---| +| 1152 (전폭 레이어) | 1112 | 112자 | 61자 | **142자** | +| 544 | 504 | 50자 | 28자 | 64자 | +| 352 | 312 | 31자 | 17자 | 40자 | +| 240 | 200 | 20자 | 11자 | 25자 | +| 208 | 168 | 16자 | 9자 | 21자 | +| 152 | 112 | 11자 | 6자 | 14자 | + +초과하면 순서대로: ① 더 짧은 명사구로 줄인다 → ② `` 2줄(baseline 간격 22) +→ ③ 그래도 안 되면 노드 수를 줄여 박스를 넓힌다. **폰트를 줄여 밀어 넣지 않는다.** + +`check.py`가 이 예산으로 오버플로를 검사한다 — `class="node"`인 도형(`rect`/`polygon`/`circle`/`ellipse`) 안에 앵커가 들어간 ``를 측정하며, +`text-anchor`(start/middle/end)를 반영해 좌우 이탈을 모두 본다. **노드 밖에 있는 +여백 주석(§3)은 측정 대상이 아니므로** 표의 48자 예산을 직접 지킨다. + +--- + +## 8. 다른 캔버스 파생 규칙 + +| | slide-4x3 (1024×768) | social-og (1200×630) | +|---|---|---| +| 마진 | 64 | 56 | +| content x | 64 → 960 (W 896) | 56 → 1144 (W 1088) | +| 본문 영역 y | 160 → 704 (H 544) | 144 → 576 (H 432) | + +Row 폭 파생: `w = floor4((W − gap × (N−1)) / N)`, 남는 픽셀은 우측 마진에 흡수시킨다 +(좌측 시작 x는 마진 그대로). 결과가 4의 배수인지 **직접 검산**한 뒤 쓴다. diff --git a/plugins/wigtn-plugins/skills/editorial-diagram/references/tokens.md b/plugins/wigtn-plugins/skills/editorial-diagram/references/tokens.md new file mode 100644 index 0000000..63f28d4 --- /dev/null +++ b/plugins/wigtn-plugins/skills/editorial-diagram/references/tokens.md @@ -0,0 +1,109 @@ +# Tokens — 시맨틱 8개 + 폰트 3개 + +다이어그램은 **브랜드를 직접 참조하지 않는다.** 항상 시맨틱 토큰만 쓰고, 브랜드는 +토큰에 값을 주입한다. 그래야 같은 도식을 WIGTN 톤으로도, 클라이언트 톤으로도 렌더할 수 있다. + +## 1. 시맨틱 토큰 (이 8개가 전부) + +| 토큰 | 역할 | 쓰는 곳 | +|---|---|---| +| `--paper` | 캔버스 배경 | `` 전체 배경 | +| `--paper-2` | 노드·컨테이너 채움 | 노드 rect, 레인 줄무늬 | +| `--ink` | 1차 텍스트 | 제목, 노드명 | +| `--muted` | 2차 텍스트 | sublabel, 축 라벨, 각주 | +| `--line` | **장식용** 얇은 선 | 격자, 구분선, 점선 그룹 박스 | +| `--line-strong` | **의미 있는** 선 | 노드 테두리, 커넥터, 축 | +| `--accent` | 초점 1~2개 | 초점 노드 테두리 + 시그니처 점 | +| `--accent-deep` | 악센트 보조 | 점 그림자/헤일로 (선택) | + +### 절대 규칙 + +- **`--accent`는 텍스트 색으로 쓰지 않는다.** Light/Dark 양쪽에서 본문 크기 대비 + 4.5:1을 못 넘긴다(`--paper-2` 위에서 각각 4.07:1, 4.32:1). `--paper` 위에서는 각각 + 4.52:1 / 4.74:1로 아슬하게 통과하지만, 노드는 `--paper-2`로 채우므로 **최악값이 규칙을 + 정한다.** 악센트는 **테두리와 점**만. +- **노드 테두리는 `--line-strong`.** `--line`은 대비가 1.3:1 수준이라 노드 경계로 + 쓰면 접근성 위반이다. `--line`은 격자·구분선 같은 장식 전용. +- 채움으로 위계를 만들지 않는다. 위계는 **위치·크기·악센트 1개**로 낸다. + +## 2. WIGTN 기본값 + +`assets/template.html`에 이미 들어 있다. 값은 [wigtn-ppt/references/brand.md](../../wigtn-ppt/references/brand.md)의 실측 HEX에서 유도했다. + +### Light (기본) + +```css +svg.ed { /* :root 아니라 svg에 정의 — SVG만 떼어내도 렌더되게 */ + --paper: #FFFFFF; + --paper-2: #F4F2F8; /* brand surface-2 */ + --ink: #1E1E28; /* brand Ink */ + --muted: #5A5A6E; /* brand text-secondary */ + --line: #E6E3EE; /* brand line — 장식 전용 */ + --line-strong: #85819B; /* 유도값: 흰 배경 대비 3.74:1 */ + --accent: #9B51E0; /* brand Purple (시그니처) */ + --accent-deep: #6B2EAA; +} +``` + +### Dark + +```css +svg.ed[data-theme="dark"] { + --paper: #15151E; /* brand Ink Deep */ + --paper-2: #1E1E28; /* brand Ink */ + --ink: #F5F4FA; + --muted: #A8A6B8; + --line: #2C2C3A; + --line-strong: #6E6C84; /* 유도값: Ink Deep 대비 3.58:1 */ + --accent: #A85FEA; /* brand Purple Bright */ + --accent-deep: #6B2EAA; +} +``` + +### 검산된 대비 (WCAG) + +| 조합 | 비율 | 기준 | +|---|---|---| +| Light `--ink` on `--paper-2` | 14.9:1 | AA 본문 4.5 ✓ | +| Light `--muted` on `--paper-2` | 6.1:1 | AA 본문 4.5 ✓ | +| Light `--line-strong` on `--paper` | 3.7:1 | 비텍스트 3.0 ✓ | +| Dark `--ink` on `--paper-2` | 15.1:1 | AA 본문 4.5 ✓ | +| Dark `--muted` on `--paper-2` | 6.9:1 | AA 본문 4.5 ✓ | +| Dark `--line-strong` on `--paper` | 3.6:1 | 비텍스트 3.0 ✓ | + +`check.py`가 이 계산을 실제 토큰 값으로 다시 돌린다 — 오버라이드해도 검증된다. + +## 3. 폰트 — 역할 3개, 섞지 않는다 + +| 역할 | 폰트 | 스펙 | 쓰는 곳 | +|---|---|---|---| +| **title** | Space Grotesk 600 | 28px / `--ink` | 다이어그램 제목 | +| **node-name** | Pretendard 500 | 18px / `--ink` | 노드명, 레인명, 축 이름 | +| **sublabel** | JetBrains Mono 400 | 13px / `--muted` | 기술 라벨, 수치, 각주(12px) | + +- 한글이 들어가면 노드명은 **반드시 Pretendard** (Space Grotesk에 한글 글리프가 없다). +- 제목이 한글이면 title 역할도 Pretendard 600으로 대체한다. +- 폴백 스택은 template.html에 이미 있다. 폰트가 로드 실패해도 레이아웃이 깨지지 + 않도록 폭 예산(geometry.md §7)은 항상 지킨다. + +## 4. 클라이언트 브랜드 오버라이드 + +WIGTN이 아닌 제안서·납품물에서는 아래 절차로 토큰만 갈아끼운다. **구조·간격· +4px 규칙은 그대로 둔다.** + +1. 클라이언트 홈페이지/브랜드 가이드에서 채집: + - 배경색 → `--paper` + - 본문 텍스트색 → `--ink` + - 보조 텍스트색 → `--muted` + - 카드/섹션 배경 → `--paper-2` + - CTA·링크색 → `--accent` + - 제목 폰트 / 본문 폰트 / 코드 폰트 → title / node-name / sublabel +2. `--line`, `--line-strong`은 대개 브랜드에 없다. `--ink`를 `--paper` 쪽으로 + 섞어 만든다 — `--line` ≈ 12% ink, `--line-strong` ≈ 45% ink. 그 뒤 3:1을 검산. +3. `check.py`를 돌려 대비 FAIL이 나오면 **조정하고 그 사실을 사용자에게 말한다.** + 조용히 브랜드 색을 바꾸지 않는다. +4. **출처 영수증**을 남긴다: 어느 URL의 어느 값에서 왔고, 무엇을 대비 때문에 + 조정했는지 한 단락. 나중에 브랜드팀과 다툴 일을 없앤다. + +> 이 스킬은 클라이언트 홈페이지를 자동으로 크롤링하지 않는다. 사용자가 URL이나 +> 브랜드 가이드를 주면 그때 채집한다. diff --git a/plugins/wigtn-plugins/skills/editorial-diagram/references/types.md b/plugins/wigtn-plugins/skills/editorial-diagram/references/types.md new file mode 100644 index 0000000..4bdedf6 --- /dev/null +++ b/plugins/wigtn-plugins/skills/editorial-diagram/references/types.md @@ -0,0 +1,156 @@ +# 12개 유형 — 무엇을 언제 그리나 + +유형은 **취향이 아니라 데이터의 모양**이 정한다. 아래 라우팅 표에서 먼저 좁히고, +해당 절에서 "쓰면 안 되는 경우"를 반드시 확인한다. 유형이 틀리면 아무리 예쁘게 +그려도 읽히지 않는다. + +## 빠른 라우팅 + +| 말하려는 것 | 유형 | +|---|---| +| 시스템이 무엇으로 이루어져 있나 | **Architecture** | +| 데이터/요청이 어떤 순서로 흐르나 | **Pipeline** | +| 조건에 따라 갈라지는 로직 | **Flowchart** | +| 누가 누구에게 언제 메시지를 보내나 | **Sequence** | +| 여러 부서/역할이 한 프로세스를 나눠 하나 | **Swimlane** | +| 추상화가 쌓여 있나 | **Layers** | +| 언제 무슨 일이 있(었)나 | **Timeline** | +| 두 기준으로 항목들을 자리매김 | **Quadrant** | +| 여러 항목을 여러 기준으로 비교 | **Matrix** | +| 포함/소속 관계 | **Tree** | +| 집합이 겹치나 (2~3개) | **Venn** | +| 단계마다 줄어드나 / 순위가 있나 | **Funnel** | + +> **수치 크기 비교**(막대·꺾은선·산점도·히트맵)는 이 스킬이 아니라 `dataviz` 스킬. + +--- + +## 1. Architecture — 시스템 구성 + +**쓸 때**: 컴포넌트와 그 연결을 보여줄 때. 제안서 AS-IS/TO-BE, 시스템 개요. +**안 쓸 때**: 순서가 핵심이면 Pipeline, 실행 순서가 핵심이면 Sequence. + +- 노드 예산: **4~7** (그룹 박스는 노드로 세지 않음) +- 좌표: §2 Row (2~3행) 또는 Row + 그룹 사각형 +- 그룹은 채움 없이 `--line` 1px 점선(`stroke-dasharray="4 4"`) + 좌상단 라벨 +- 외부 시스템은 sublabel에 `external` 표기, 채움을 `--paper`로 낮춘다 + +## 2. Pipeline — 단계적 흐름 + +**쓸 때**: 분기 없는 선형 처리(수집 → 정제 → 적재 → 서빙). +**안 쓸 때**: 분기·되돌아감이 있으면 Flowchart. + +- 노드 예산: **3~4** (화살표 간격 때문에 5 이상은 화살표를 뺀다) +- 좌표: §2 Row 1행 (`y = 328`, h = 160) — 1행은 h=160이다. h=96은 3행짜리 값이다 +- 각 노드 sublabel에 처리 시간·기술 1개만. 두 줄 이상 쓰지 않는다. + +## 3. Flowchart — 조건 분기 + +**쓸 때**: "이럴 땐 A, 저럴 땐 B"가 핵심일 때. +**안 쓸 때**: 분기가 하나도 없으면 Pipeline. 분기가 4개 이상이면 Matrix나 표. + +- 노드 예산: **5~8** (다이아몬드 포함) +- 좌표: §3 Column + 분기 좌/우 (x 160 / 800) +- **모든 분기에 라벨** (`Yes`/`No`). 라벨 없는 분기는 직선처럼 읽힌다. +- 마름모는 ``, w 224 × h 88 + +## 4. Sequence — 시간축 메시지 + +**쓸 때**: 액터 간 호출 순서·왕복이 핵심일 때(인증 플로우, 결제 승인). +**안 쓸 때**: 액터가 2개 이하면 Pipeline이 더 읽기 쉽다. + +- 액터 예산: **3~5**, 메시지 **≤ 8** +- 액터 헤더는 §2 Row X 좌표표 (h = 64, y = 176), 라이프라인은 각 박스 중심 x에서 + y `240 → 640` 세로선 (`stroke-dasharray="4 4"`) +- 메시지 화살표 y: `288`부터 **48씩** 증가. 라벨은 화살표 위 baseline −8 + +## 5. Swimlane — 부서 간 프로세스 + +**쓸 때**: "누가" 하는지가 "무엇을" 하는지만큼 중요할 때. RACI, 승인 프로세스. +**안 쓸 때**: 담당이 하나면 Pipeline. + +- 레인 **3~4**, 레인당 단계 **≤ 5**, 총 노드 **≤ 9** +- 좌표: §5.1 +- 레인 배경은 홀수 레인만 `--paper-2`, 짝수는 `--paper` (얼룩 줄무늬로 구분) + +## 6. Layers — 계층 스택 + +**쓸 때**: 위가 아래에 의존하는 구조(인프라 → 플랫폼 → 앱, 거버넌스 계층). +**안 쓸 때**: 계층이 아니라 그냥 목록이면 텍스트로 쓴다. + +- 레이어 **3~5** +- 좌표: §4 +- 아래로 갈수록 `--paper-2` 채움을 유지하되 **색을 진하게 만들지 않는다**(위계는 + 위치가 이미 표현한다). 레이어 간 화살표는 넣지 않는다. + +## 7. Timeline / Roadmap — 시간축 + +**쓸 때**: 마일스톤, 분기별 로드맵, 프로젝트 페이즈. +**안 쓸 때**: 기간의 **겹침**이 핵심이면 간트형 막대가 필요하다 — Matrix 좌표에 +가로 막대를 올려 쓴다. + +- 마커 **3~6** +- 좌표: §5.2 +- 지난 구간은 `--muted`, 현재/다음은 `--ink`, 강조 마일스톤 1개만 악센트 점 + +## 8. Quadrant — 2×2 + +**쓸 때**: 두 축으로 우선순위를 정할 때(임팩트 × 노력, 리스크 × 빈도). +**안 쓸 때**: 축이 3개 이상이면 Matrix. 항목이 10개 넘으면 산점도(`dataviz`). + +- 항목 **4~8** +- 좌표: §5.3 +- 축 이름은 반드시 방향을 밝힌다: `노력 (낮음 → 높음)` +- 셀 이름(예: `Quick Wins`)은 있으면 좋고, 없으면 비워둔다. 억지로 짓지 않는다. + +## 9. Matrix — 비교 매트릭스 + +**쓸 때**: 항목 × 기준 비교(도구 선정, 권한 매트릭스). +**안 쓸 때**: 기준이 1개면 그냥 정렬된 목록. + +- 행 **≤ 5**, 열 **≤ 4** +- 좌표: §5.7 +- 셀 값은 `●` / `○` / `—` 또는 짧은 단어. 문장 금지. +- 추천 행/열 **1개**만 악센트 테두리 + 시그니처 점 + +## 10. Tree / Org — 계층 소속 + +**쓸 때**: 조직도, 모듈 소유권, 분류 체계. +**안 쓸 때**: 깊이가 4단 이상이면 들여쓴 목록이 더 읽기 쉽다. + +- 깊이 **≤ 3**, 노드 **≤ 9** +- 좌표: §5.6 +- 연결선은 직각 꺾임. 사선 금지. + +## 11. Venn — 집합 겹침 + +**쓸 때**: 역할·범위의 공통분모가 메시지일 때. +**안 쓸 때**: 집합이 4개 이상. (읽을 수 없다 — Matrix로 바꾼다.) + +- 집합 **2~3** +- 좌표: §5.4 +- 교집합 영역에 들어가는 텍스트는 **3~4단어**. 넘치면 밖으로 빼고 지시선. + +## 12. Funnel / Pyramid — 단계 감소·순위 + +**쓸 때**: 전환 퍼널, 우선순위 피라미드, 데이터 계층(브론즈/실버/골드). +**안 쓸 때**: 감소량이 실제 수치로 중요하면 막대 차트(`dataviz`). + +- 단 **3~4** +- 좌표: §5.5 +- 각 단 우측에 수치/비율 1개(모노 13px). 단 자체 폭이 이미 크기를 말하므로 + 색으로 또 강조하지 않는다. + +--- + +## 밀도 초과 시 (노드 > 9) + +순서대로 시도한다. + +1. **묶는다** — 개별 마이크로서비스 5개 → `주문 도메인 (5)` 한 노드 + sublabel +2. **뺀다** — 청중이 임원이면 캐시·큐·리트라이는 메시지에 기여하지 않는다 +3. **쪼갠다** — 개요 1장 + 상세 1장. 한 장에 다 넣지 않는다 +4. **유형을 바꾼다** — 노드가 많다는 건 대개 Architecture로 그릴 걸 + Layers/Swimlane으로 그려야 한다는 신호다 + +무엇을 뺐는지는 **Step 2 선언에 반드시 적는다.** 조용한 생략은 거짓말이 된다. diff --git a/plugins/wigtn-plugins/skills/editorial-diagram/scripts/check.py b/plugins/wigtn-plugins/skills/editorial-diagram/scripts/check.py new file mode 100755 index 0000000..33372fc --- /dev/null +++ b/plugins/wigtn-plugins/skills/editorial-diagram/scripts/check.py @@ -0,0 +1,955 @@ +#!/usr/bin/env python3 +"""editorial-diagram 출력 검증기. + + python3 check.py out.html [--theme light|dark] [--quiet] + +exit 0 = PASS/WARN, exit 1 = FAIL. + +## 이 도구의 계약 (중요) + +이건 **임의 SVG 검증기가 아니다.** `assets/template.html`이 만드는 *정규 형태*만 +검증한다. 브라우저를 다시 구현하지 않기 때문에, 지원 범위 밖 구문은 조용히 +통과시키지 않고 **"미지원 구문 → FAIL"**로 끊는다. 검증할 수 없는 것을 통과시키면 +게이트가 꺼진 줄도 모르게 꺼진다. + +정규 형태: + +- 색은 **3/6자리 HEX 또는 선언된 `var(--토큰)`만**. 토큰뿐 아니라 모든 도형의 + `fill`/`stroke`(CSS·표현 속성·인라인 style)에 적용한다. `none`/`transparent`는 허용. +- 노드는 `rect`/`polygon`/`circle`/`ellipse`에 `class="node"`. `g`·`path` 노드는 FAIL. +- 폰트 크기는 `px`/`rem`/`em`만. +- 색·폰트·정렬은 **클래스 규칙으로**. `text`/`tspan`/`g`의 표현 속성·인라인 style은 FAIL. +- `--토큰`은 **루트 한 곳에서만** 선언한다. 자손에서 재정의하면 FAIL. +- 셀렉터는 `type`/`.class`/`#id`/`[attr]`/`[attr=v]` 조합만. 결합자·부분일치는 FAIL. +- 렌더 요소는 화이트리스트만. `use`/`foreignObject`/`image`/중첩 `svg`는 FAIL. +- 캔버스는 `viewBox`로 지정한다. + +**다크 블록을 선언하면 다크 팔레트도 검증한다** — 두 테마 모두 대비를 통과해야 한다. +""" + +import argparse +import re +import sys +import xml.etree.ElementTree as ET + +GRID = 4 +MAX_NODES = 9 +IDEAL_NODES = 5 +MAX_FOCAL = 2 +PAD = 20 # 노드 좌우 안쪽 여백 +NODE_PADDING = PAD * 2 + +GEOM_ATTRS = { + "rect": ("x", "y", "width", "height", "rx", "ry"), + "circle": ("cx", "cy", "r"), + "ellipse": ("cx", "cy", "rx", "ry"), + "line": ("x1", "y1", "x2", "y2"), + "image": ("x", "y", "width", "height"), + "use": ("x", "y"), +} +POINT_SHAPES = ("polygon", "polyline") +NODE_SHAPES = ("rect", "polygon", "circle", "ellipse") +NON_RENDERED = ("defs", "marker", "symbol", "pattern", "clipPath", "mask") +CHECKED_PROPS = ("fill", "stroke", "font-size", "font-family", "text-anchor") +INHERITED_PROPS = ("fill", "font-size", "font-family", "text-anchor") + +# 정규 형태에서 나올 수 있는 요소는 이게 전부. use/foreignObject/image/중첩 svg는 +# 참조나 좌표계 때문에 정적으로 잴 수 없다 → 무시하지 않고 계약 위반으로 끊는다. +ALLOWED_ELEMENTS = {"svg", "g", "defs", "style", "title", "desc", "marker", + "rect", "circle", "ellipse", "line", "polygon", "polyline", + "path", "text", "tspan"} +ALLOWED_RX = (0, 4, 8) + +FONT_SIZE_FALLBACK = { + "title": 28.0, "subtitle": 15.0, "node-name": 18.0, "sublabel": 13.0, "foot": 12.0, +} +KNOWN_CANVASES = {(1280, 720), (1024, 768), (1200, 630)} + +# 이모지 표현형만. ✓ ✗ ★ ⚠ 같은 기호는 정상 타이포라 제외한다. +EMOJI = re.compile("[\U0001F000-\U0001FAFF⬀-⯿" + "✅❌❎❓-❕❗➕-➗➰➿]|️") +NUM_RE = r"[-+]?(?:\d*\.\d+|\d+)(?:[eE][-+]?\d+)?" +HEX_RE = re.compile(r"#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\Z") + +BAD = object() + + +# ---------------------------------------------------------------- 값 파싱 +def num(v): + if v is None: + return None + try: + return float(v) + except (TypeError, ValueError): + return BAD + + +def length_px(v): + if v is None: + return None + m = re.fullmatch(rf"({NUM_RE})\s*(px|rem|em)?", str(v).strip()) + if not m: + return BAD + return float(m.group(1)) * (16.0 if m.group(2) in ("rem", "em") else 1.0) + + +def hex_rgb(v): + if not v: + return None + m = HEX_RE.match(str(v).strip()) + if not m: + return None + h = m.group(1) + if len(h) == 3: + h = "".join(c * 2 for c in h) + return tuple(int(h[i:i + 2], 16) / 255 for i in (0, 2, 4)) + + +def srgb_lin(c): + return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 + + +def luminance(v): + rgb = hex_rgb(v) + if rgb is None: + return None + r, g, b = (srgb_lin(c) for c in rgb) + return 0.2126 * r + 0.7152 * g + 0.0722 * b + + +def contrast(fg, bg): + lf, lb = luminance(fg), luminance(bg) + if lf is None or lb is None: + return None + hi, lo = max(lf, lb), min(lf, lb) + return (hi + 0.05) / (lo + 0.05) + + +def is_wide(ch): + o = ord(ch) + return (0x1100 <= o <= 0x11FF or 0x3040 <= o <= 0x30FF or 0x3130 <= o <= 0x318F + or 0x3400 <= o <= 0x9FFF or 0xAC00 <= o <= 0xD7A3 or 0xFF00 <= o <= 0xFF60) + + +def text_width(s, font_size, mono=False): + latin = 0.6 if mono else 0.55 + return sum(font_size * (1.0 if is_wide(c) else latin) for c in s) + + +# ---------------------------------------------------------------- 리포트 +class Report: + def __init__(self): + self.items = [] + + def add(self, lv, rule, msg): + self.items.append((lv, rule, msg)) + + def fail(self, r, m): + self.add("FAIL", r, m) + + def warn(self, r, m): + self.add("WARN", r, m) + + def info(self, r, m): + self.add("INFO", r, m) + + @property + def verdict(self): + lv = {x for x, _, _ in self.items} + return "FAIL" if "FAIL" in lv else ("WARN" if "WARN" in lv else "PASS") + + +# ---------------------------------------------------------------- 문서 +class Doc: + def __init__(self, root): + self.root = root + self.ns = root.tag.split("}")[0] + "}" if "}" in root.tag else "" + self.parent = {id(root): None} + for p in root.iter(): + for c in p: + self.parent[id(c)] = p + self.hidden = set() + for name in NON_RENDERED: + for c in root.iter(self.q(name)): + self.hidden.update(id(d) for d in c.iter()) + + def q(self, n): + return f"{self.ns}{n}" + + def tag(self, el): + return el.tag.replace(self.ns, "") if self.ns else el.tag + + def rendered(self, el): + return id(el) not in self.hidden + + def iter_rendered(self, name=None): + it = self.root.iter(self.q(name)) if name else self.root.iter() + return (el for el in it if self.rendered(el)) + + +def classes(el): + return set((el.get("class") or "").split()) + + +def describe(doc, el): + keys = ("class", "id", "x", "y", "cx", "cy", "x1", "y1") + bits = [f'{k}="{el.get(k)}"' for k in keys if el.get(k) is not None] + return f"<{doc.tag(el)} {' '.join(bits)}>" if bits else f"<{doc.tag(el)}>" + + +# ---------------------------------------------------------------- CSS +SEL_TOKEN = re.compile(r"\A(?P[-\w]+)?(?P(?:\#[-\w]+|\.[-\w]+)*)\Z") +# [attr] / [attr=v] / [attr="v"] / [attr='v'] 만. ^= *= $= |= ~= 는 해석하지 않는다. +ATTR_RE = re.compile(r"""\[\s*([-\w]+)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([-\w]+))\s*)?\]""") + + +class Sel: + def __init__(self, type_, id_, cls, attrs): + self.type, self.id, self.cls, self.attrs = type_, id_, cls, attrs + self.spec = (1 if id_ else 0, len(cls) + len(attrs), 1 if type_ else 0) + + def matches(self, doc, el): + if self.type and self.type not in (doc.tag(el), "*"): + return False + if self.id and el.get("id") != self.id: + return False + if not self.cls <= classes(el): + return False + for k, v in self.attrs: + if v is None: + if el.get(k) is None: + return False + elif (el.get(k) or "") != v: + return False + return True + + +def sel_text(sel): + """진단 메시지용 — 속성까지 보여야 어느 규칙인지 식별된다.""" + parts = [sel.type or ""] + if sel.id: + parts.append(f"#{sel.id}") + parts += [f".{c}" for c in sorted(sel.cls)] + parts += [f"[{k}={v!r}]" if v is not None else f"[{k}]" for k, v in sel.attrs] + return "".join(parts) or "*" + + +def parse_selector(sel): + """해석 못 하면 None — 호출부가 FAIL로 끊는다 (잘못 해석해서 통과시키지 않는다).""" + s = re.sub(r"\s+", " ", sel).strip() + if not s or any(c in s for c in " >+~,:"): + return None + attrs = [(m.group(1), next((g for g in m.groups()[1:] if g is not None), None)) + for m in ATTR_RE.finditer(s)] + rest = ATTR_RE.sub("", s) + if "[" in rest or "]" in rest: + return None + m = SEL_TOKEN.match(rest) + if not m: + return None + ids = re.findall(r"#([-\w]+)", m.group("rest") or "") + if len(ids) > 1: + return None + cls = set(re.findall(r"\.([-\w]+)", m.group("rest") or "")) + return Sel(m.group("type"), ids[0] if ids else None, cls, attrs) + + +def strip_at_rule_blocks(css): + out, i, removed = [], 0, 0 + while True: + m = re.search(r"@[-\w]+[^;{]*\{", css[i:]) + if not m: + out.append(css[i:]) + break + start = i + m.start() + out.append(css[i:start]) + depth, j, q = 0, i + m.end() - 1, None + while j < len(css): + c = css[j] + if q: + if c == q: + q = None + elif c in "\"'": + q = c + elif c == "{": + depth += 1 + elif c == "}": + depth -= 1 + if depth == 0: + break + j += 1 + removed += 1 + i = j + 1 + return "".join(out), removed + + +def parse_style(doc, rep): + css = "\n".join("".join(s.itertext()) for s in doc.root.iter(doc.q("style"))) + css = re.sub(r"/\*.*?\*/", "", css, flags=re.S) + css, n_at = strip_at_rule_blocks(css) + if n_at: + rep.warn("css", f"at-rule 블록 {n_at}개는 검증 대상이 아니다 — " + "테마는 data-theme 속성으로 분기한다(tokens.md §2)") + rules = [] + for order, (raw, body) in enumerate(re.findall(r"([^{}]+)\{([^{}]*)\}", css)): + raw = raw.split(";")[-1] + decls, imps = {}, set() + for k, v in re.findall(r"([-\w]+)\s*:\s*([^;]+?)\s*(?:;|$)", body): + v2 = re.sub(r"\s*!\s*important\s*$", "", v) + if v2 != v: + imps.add(k) + decls[k] = v2.strip() + if not decls: + continue + watched = [k for k in decls if k.startswith("--") or k in CHECKED_PROPS] + for part in raw.split(","): # `.a, .b { … }` 는 흔한 관용구라 지원한다 + if not part.strip(): + continue + sel = parse_selector(part) + if sel is None: + if watched: + rep.fail("css", f"해석할 수 없는 셀렉터 '{part.strip()}'가 " + f"{', '.join(sorted(watched))}를 선언한다 — " + "type/.class/#id/[attr=v] 조합만 지원한다") + continue + rules.append((sel, decls, imps, order)) + return rules + + +def declared(doc, rules, el, prop): + """CSS 캐스케이드: (!important, 특정성, 문서순서) 최대.""" + best, best_key = None, None + for sel, decls, imps, order in rules: + if prop not in decls or not sel.matches(doc, el): + continue + key = (prop in imps, sel.spec, order) + if best_key is None or key > best_key: + best, best_key = decls[prop], key + return best + + +def inline_prop(el, prop): + m = re.search(rf"(? keys[k]: + toks[k], keys[k] = v, key + return toks + + +def declared_themes(rules): + """스타일시트가 선언한 테마. 다크 블록이 있으면 다크도 검증해야 한다.""" + themes = {"light"} + for sel, _, _, _ in rules: + for k, v in sel.attrs: + if k == "data-theme" and v: + themes.add(v) + return themes + + +def resolve_font(doc, rules, el): + raw = inline_prop(el, "font-size") or declared(doc, rules, el, "font-size") + size = length_px(raw) if raw is not None else None + if size is None: + size = next((FONT_SIZE_FALLBACK[c] for c in sorted(classes(el)) + if c in FONT_SIZE_FALLBACK), 16.0) + fam = (inline_prop(el, "font-family") or declared(doc, rules, el, "font-family") or "") + return size, "mono" in fam.lower() + + +def resolve_anchor(doc, rules, el): + # SVG 기본값은 start다. middle로 가정하면 좌측 배치 라벨에 없는 오버플로를 만들고, + # 중앙 배치 라벨의 실제 이탈을 놓친다. + v = (inline_prop(el, "text-anchor") or declared(doc, rules, el, "text-anchor") or "start") + v = v.strip() + return v if v in ("start", "middle", "end") else None + + +# ---------------------------------------------------------------- 계약 +def token_values(doc, rules, name): + """선언된 모든 테마에서의 토큰 값 집합 (정규화된 RGB). 리터럴 우회 탐지용.""" + out, original = set(), doc.root.get("data-theme") + for th in sorted(declared_themes(rules)): + doc.root.set("data-theme", th) + v = tokens_for(doc, rules).get(f"--{name}") # 토큰 키는 `--name` 형태다 + rgb = hex_rgb(v) if v else None + if rgb: + out.add(rgb) + if original is None: + doc.root.attrib.pop("data-theme", None) + else: + doc.root.set("data-theme", original) + return out + + +def uses(value, names, values): + """`var(--name)` 이거나, 토큰과 같은 색을 손으로 적었거나. + + 이름은 **경계까지** 맞춘다 — 접두사로 비교하면 `var(--line-strong)`이 + `--line` 규칙에 걸린다(정상 사용을 차단하는 오탐). + """ + v = value or "" + for nm in names: + if re.search(rf"var\(\s*--{re.escape(nm)}\s*[,)]", v): + return True + rgb = hex_rgb(value) + return rgb is not None and rgb in values + + +def check_contract(doc, rules, rep): + accents = token_values(doc, rules, "accent") + lines = token_values(doc, rules, "line") + for el in doc.root.iter(): + t = doc.tag(el) + if t == "svg" and el is not doc.root: + rep.fail("contract", "중첩 — 새 뷰포트를 만들어 내용을 자르거나 스케일한다. " + "정적으로 측정할 수 없다") + continue + if t not in ALLOWED_ELEMENTS: + rep.fail("contract", f"<{t}> — 정규 형태에 없는 요소다. use/foreignObject/image/" + "중첩 svg는 정적으로 측정할 수 없어 검사를 무력화한다") + for el in doc.iter_rendered(): + t = doc.tag(el) + if "node" in classes(el) and t not in NODE_SHAPES: + rep.fail("contract", f'{describe(doc, el)} — class="node"는 ' + f"{'/'.join(NODE_SHAPES)}에만 붙인다(받은 것: {t})") + if t in ("text", "tspan", "g"): + for prop in INHERITED_PROPS: + if el.get(prop) is not None: + rep.fail("contract", f'{describe(doc, el)} {prop}="{el.get(prop)}" — ' + f"{t}의 {prop}은 표현 속성이 아니라 클래스 규칙으로 준다") + if prop != "font-size" and inline_prop(el, prop): + rep.fail("contract", f"{describe(doc, el)} style에 {prop} — " + "클래스 규칙으로 준다") + if t in ("g", "tspan") and (classes(el) or el.get("style")): + what = "는 묶기 전용" if t == "g" else "은 줄바꿈 전용" + rep.fail("contract", f"{describe(doc, el)} — {what}이다. class·style을 주면 " + "상속으로 색·폰트가 바뀌는데 그걸 계산하지 않는다" + "(스타일은 부모 에 준다)") + if t == "rect": + for a in ("rx", "ry"): + v = num(el.get(a)) + if v not in (None, BAD) and int(v) not in ALLOWED_RX: + rep.fail("contract", f'{describe(doc, el)} {a}="{v:g}" — ' + f"{ALLOWED_RX} 중 하나여야 한다") + if "focal" in classes(el) and doc.tag(el) != "circle": + fill = el.get("fill") or declared(doc, rules, el, "fill") or "" + if uses(fill, ("accent", "accent-deep"), accents): + rep.fail("contract", f"{describe(doc, el)} — 초점은 악센트로 **채우지** 않는다. " + "2px 테두리 + 시그니처 점으로 표시한다(SKILL.md 하드 규칙 3)") + if "node" in classes(el): + stroke = el.get("stroke") or declared(doc, rules, el, "stroke") or "" + if uses(stroke, ("line",), lines): + rep.fail("contract", f"{describe(doc, el)} — 노드 테두리에 --line은 대비 1.3:1이라 " + "쓸 수 없다. --line-strong을 쓴다(tokens.md §1)") + for el in doc.iter_rendered("text"): + if resolve_font(doc, rules, el)[0] is BAD: + rep.fail("contract", f"{describe(doc, el)} font-size를 px/rem/em으로 읽을 수 없다") + if resolve_anchor(doc, rules, el) is None: + rep.fail("contract", f"{describe(doc, el)} text-anchor를 해석할 수 없다 " + "(start/middle/end만 지원)") + # 루트를 겨냥한 규칙이 상속 속성을 선언하면 와 같은 문제가 생긴다 + original_r = doc.root.get("data-theme") + for th in sorted(declared_themes(rules)): + doc.root.set("data-theme", th) + for sel, decls, _, _ in rules: + watched = sorted(k for k in decls if k in INHERITED_PROPS) + if watched and sel.matches(doc, doc.root): + rep.fail("contract", f"셀렉터 '{sel_text(sel)}'가 루트에 " + f"{', '.join(watched)}를 선언한다 — 상속으로 모든 자손의 " + "색·폰트가 바뀌는데 그걸 계산하지 않는다. 클래스에 준다") + if original_r is None: + doc.root.attrib.pop("data-theme", None) + else: + doc.root.set("data-theme", original_r) + + # --토큰은 루트에서만. 단 테마 변형(`[data-theme=…]`)은 정규 형태이므로, + # 선언된 테마 중 하나로라도 루트에 걸리면 합법이다. + original = doc.root.get("data-theme") + themes = sorted(declared_themes(rules)) + for sel, decls, _, _ in rules: + if not any(k.startswith("--") for k in decls): + continue + ok = False + for th in themes: + doc.root.set("data-theme", th) + if sel.matches(doc, doc.root): + ok = True + break + if not ok: + rep.fail("contract", f"셀렉터 '{sel_text(sel)}'가 --토큰을 재정의한다 — " + "토큰은 루트에서만 선언한다" + "(CSS 변수는 상속되어 대비 검증을 우회한다)") + if original is None: + doc.root.attrib.pop("data-theme", None) + else: + doc.root.set("data-theme", original) + + +COLOR_PROPS = ("fill", "stroke") +COLOR_KEYWORDS = {"none", "transparent", "inherit", "currentcolor"} +VAR_RE = re.compile(r"var\(\s*(--[-\w]+)\s*(?:,[^()]*)?\)\Z") + + +def color_ok(value, token_names): + """3/6자리 HEX, 선언된 var(--토큰), none/transparent 만 허용한다. + + 토큰만 검사하면 `.node{fill:rgb(0 0 0)}` 처럼 도형 색을 직접 쓴 경우가 + 통째로 빠진다 — 계약이 "HEX만"이라고 선언해 놓고 검증은 안 하는 상태가 된다. + """ + v = (value or "").strip() + if not v or v.lower() in COLOR_KEYWORDS: + return True + m = VAR_RE.match(v) + if m: + return m.group(1) in token_names + return hex_rgb(v) is not None + + +def check_colors(doc, rules, rep): + names = {k for _, decls, _, _ in rules for k in decls if k.startswith("--")} + for sel, decls, _, _ in rules: + for prop in COLOR_PROPS: + if prop in decls and not color_ok(decls[prop], names): + rep.fail("contract", f"셀렉터 '{sel_text(sel)}'의 {prop}: " + f"{decls[prop].strip()!r} — 3/6자리 HEX 또는 " + "선언된 var(--토큰)만 지원한다" + "(rgb()·color-mix()·색이름은 정적으로 검증할 수 없다)") + for el in doc.root.iter(): + for prop in COLOR_PROPS: + v = el.get(prop) + if v and not color_ok(v, names): + rep.fail("contract", f"{describe(doc, el)} {prop}=\"{v}\" — " + "3/6자리 HEX 또는 선언된 var(--토큰)만 지원한다") + inl = inline_prop(el, "fill") or inline_prop(el, "stroke") + if inl and not color_ok(inl, names): + rep.fail("contract", f"{describe(doc, el)} style의 색 {inl!r} — " + "3/6자리 HEX 또는 선언된 var(--토큰)만 지원한다") + + +def check_tokens_are_hex(tokens, theme, rep): + for k, v in sorted(tokens.items()): + if hex_rgb(v) is None: + rep.fail("contract", f"[{theme}] 토큰 {k}={v.strip()!r} — 3/6자리 HEX만 지원한다") + + +# ---------------------------------------------------------------- 기하 +PATH_PARAMS = {"m": 2, "l": 2, "t": 2, "h": 1, "v": 1, "c": 6, "s": 4, "q": 4, "a": 7, "z": 0} +ARC_COORD_IDX = {5, 6} + + +def path_numbers(d): + toks = re.findall(rf"[A-Za-z]|{NUM_RE}", d or "") + coords, cmd, idx = [], None, 0 + for t in toks: + if t.isalpha(): + cmd, idx = t.lower(), 0 + continue + if cmd is None: + continue + n = PATH_PARAMS.get(cmd, 0) + if n == 0: + continue + if cmd != "a" or (idx % n) in ARC_COORD_IDX: + coords.append((float(t), t)) + idx += 1 + return coords + + +def points_of(el): + """polygon/polyline 좌표. 숫자가 아닌 토큰이 섞이면 BAD.""" + raw = (el.get("points") or "").strip() + if not raw: + return None + toks = [t for t in re.split(r"[\s,]+", raw) if t] + vals = [] + for t in toks: + v = num(t) + if v is BAD or v is None: + return BAD + vals.append(v) + if len(vals) < 4 or len(vals) % 2: + return BAD + return vals + + +def bbox(doc, el): + t = doc.tag(el) + g = {k: num(el.get(k)) for k in ("x", "y", "width", "height", "cx", "cy", "r", "rx", "ry")} + if any(v is BAD for v in g.values()): + return None + if t == "rect" and None not in (g["x"], g["y"], g["width"], g["height"]): + if g["width"] <= 0 or g["height"] <= 0: + return None + return (g["x"], g["y"], g["width"], g["height"]) + if t == "circle" and None not in (g["cx"], g["cy"], g["r"]): + return (g["cx"] - g["r"], g["cy"] - g["r"], 2 * g["r"], 2 * g["r"]) + if t == "ellipse" and None not in (g["cx"], g["cy"], g["rx"], g["ry"]): + return (g["cx"] - g["rx"], g["cy"] - g["ry"], 2 * g["rx"], 2 * g["ry"]) + if t in POINT_SHAPES: + vals = points_of(el) + if vals not in (None, BAD): + xs, ys = vals[0::2], vals[1::2] + return (min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys)) + return None + + +# ---------------------------------------------------------------- 검사 +def check_grid(doc, rep): + bad = 0 + for el in doc.iter_rendered(): + t = doc.tag(el) + for attr in GEOM_ATTRS.get(t, ()): + v = num(el.get(attr)) + if v is BAD: + bad += 1 + rep.fail("4px-grid", f'{describe(doc, el)} {attr}="{el.get(attr)}" — 숫자가 아니다') + elif v is not None and v % GRID != 0: + bad += 1 + rep.fail("4px-grid", f'{describe(doc, el)} {attr}="{v:g}" — 4의 배수 아님') + if t in POINT_SHAPES: + vals = points_of(el) + if vals is BAD: + bad += 1 + rep.fail("4px-grid", f"{describe(doc, el)} points를 좌표쌍으로 읽을 수 없다 " + "(숫자가 아니거나 개수가 홀수)") + elif vals: + for v in vals: + if v % GRID != 0: + bad += 1 + rep.fail("4px-grid", f"{describe(doc, el)} points {v:g} — 4의 배수 아님") + elif t == "path": + for v, raw in path_numbers(el.get("d")): + if v % GRID != 0: + bad += 1 + rep.fail("4px-grid", f"{describe(doc, el)} d 좌표 {raw} — 4의 배수 아님") + if not bad: + rep.info("4px-grid", "모든 도형 좌표가 4의 배수") + + +def check_transform(doc, rep): + hit = [el for el in doc.iter_rendered() if el.get("transform")] + if hit: + rep.warn("transform", f"transform이 {len(hit)}곳 — 검사는 변환 전 좌표만 본다 " + f"({describe(doc, hit[0])})") + + +def check_density(doc, rep): + nodes = [el for el in doc.iter_rendered() + if doc.tag(el) in NODE_SHAPES and "node" in classes(el)] + n = len(nodes) + if n == 0: + rep.fail("density", 'class="node"인 노드가 없다 — 밀도·오버플로 검사가 통째로 꺼진다') + elif n > MAX_NODES: + rep.fail("density", f"노드 {n}개 — 최대 {MAX_NODES}. 묶거나 빼거나 쪼갠다") + elif n > IDEAL_NODES: + rep.warn("density", f"노드 {n}개 — 이상적 범위는 4~{IDEAL_NODES}") + else: + rep.info("density", f"노드 {n}개") + return nodes + + +def check_focus_count(doc, rep): + """개수는 테마와 무관하다 — 테마 루프 안에서 세면 findings가 테마 수만큼 중복된다.""" + focal = [el for el in doc.iter_rendered() if "focal" in classes(el)] + dots = [el for el in doc.iter_rendered() if "dot" in classes(el)] + if len(focal) > MAX_FOCAL: + rep.fail("accent", f"focal {len(focal)}개 — 최대 {MAX_FOCAL}") + if len(dots) > MAX_FOCAL: + rep.fail("accent", f"시그니처 점 {len(dots)}개 — 최대 {MAX_FOCAL}") + if not focal and not dots: + rep.warn("accent", "초점 표시가 없다 — 강조점이 정말 없는 다이어그램인지 확인") + + +def check_accent(doc, rules, theme, accents, rep): + for el in doc.iter_rendered("text"): + fill = declared(doc, rules, el, "fill") or "" + if uses(fill, ("accent", "accent-deep"), accents): + rep.fail("accent", f"[{theme}] {describe(doc, el)} — 악센트를 텍스트 색으로 사용" + "(대비 미달). 테두리·점에만 쓴다") + + +def check_a11y(doc, rep): + svg = doc.root + if svg.get("role") != "img": + rep.fail("a11y", '에 role="img"가 없다') + labelled = (svg.get("aria-labelledby") or "").split() + kids = list(svg) + titles = [e for e in kids if doc.tag(e) == "title"] + descs = [e for e in kids if doc.tag(e) == "desc"] + if not titles: + rep.fail("a11y", "이 의 직계 자식으로 없다") + elif not (titles[0].text or "").strip(): + rep.fail("a11y", "이 비어 있다") + if not descs: + rep.fail("a11y", "가 의 직계 자식으로 없다") + elif not ("".join(descs[0].itertext())).strip(): + rep.fail("a11y", "가 비어 있다 — 그림을 못 보는 사람에게 내용을 전달할 수단") + ids = {e.get("id") for e in titles + descs if e.get("id")} + if not labelled: + rep.fail("a11y", "aria-labelledby가 없다") + else: + missing = [i for i in labelled if i not in ids] + if missing: + rep.fail("a11y", f"aria-labelledby가 존재하지 않는 id를 가리킨다: {', '.join(missing)}") + + +def anchor_of(doc, el): + x, y = num(el.get("x")), num(el.get("y")) + for sp in el.iter(doc.q("tspan")): + if x in (None, BAD): + x = num(sp.get("x")) + if y in (None, BAD): + y = num(sp.get("y")) + if x in (None, BAD) or y in (None, BAD): + return None + return x, y + + +def text_lines(doc, el): + spans = [s for s in el.iter(doc.q("tspan"))] + if spans: + return [t for t in ("".join(s.itertext()).strip() for s in spans) if t] + s = "".join(el.itertext()).strip() + return [s] if s else [] + + +def span_of(anchor, x, w): + if anchor == "start": + return x, x + w + if anchor == "end": + return x - w, x + return x - w / 2, x + w / 2 + + +def check_overflow(doc, rules, nodes, rep): + boxes = [] + for nd in nodes: + bb = bbox(doc, nd) + if bb is None: + rep.fail("label-overflow", f"{describe(doc, nd)} — 노드 크기를 계산할 수 없다. " + "오버플로 검사 불가") + else: + boxes.append((nd, bb)) + hit = 0 + for t in doc.iter_rendered("text"): + if not text_lines(doc, t): + continue + pt = anchor_of(doc, t) + if pt is None: + rep.fail("label-overflow", f"{describe(doc, t)} — 기준점(x·y)을 찾을 수 없다") + continue + tx, ty = pt + cands = [(bw * bh, nd, (bx, by, bw, bh)) for nd, (bx, by, bw, bh) in boxes + if bx <= tx <= bx + bw and by <= ty <= by + bh] + if not cands: + continue + _, nd, (bx, _, bw, _) = min(cands, key=lambda c: c[0]) + size, mono = resolve_font(doc, rules, t) + anchor = resolve_anchor(doc, rules, t) + if size is BAD or anchor is None: + continue + if bw < 2 * PAD: + rep.fail("label-overflow", f"{describe(doc, nd)} 폭 {int(bw)} — 좌우 패딩 " + f"{2 * PAD}보다 좁아 라벨이 들어갈 수 없다") + continue + left_lim, right_lim = bx + PAD, bx + bw - PAD + for line in text_lines(doc, t): + w = text_width(line, size, mono) + lo, hi = span_of(anchor, tx, w) + over = max(left_lim - lo, hi - right_lim) + if over > 0: + hit += 1 + rep.fail("label-overflow", + f'"{line}" ({size:g}px{", mono" if mono else ""}, anchor={anchor}) ' + f"→ [{lo:.0f}, {hi:.0f}] 이 박스 안쪽 [{left_lim:.0f}, {right_lim:.0f}]를 " + f"{int(over)}px 넘침. 줄이거나 2줄로") + if not hit: + rep.info("label-overflow", "측정된 모든 라벨이 박스 안쪽 예산 안") + + +CONTRAST_PAIRS = [ + ("--ink", "--paper-2", 4.5, "본문 텍스트"), + ("--muted", "--paper-2", 4.5, "보조 텍스트"), + ("--ink", "--paper", 4.5, "제목 텍스트"), + ("--line-strong", "--paper", 3.0, "노드 테두리·커넥터"), +] +REQUIRED_TOKENS = sorted({t for a, b, _, _ in CONTRAST_PAIRS for t in (a, b)}) + + +def check_contrast(tokens, theme, rep): + if not tokens: + rep.fail("contrast", f"[{theme}] 시맨틱 토큰을 찾지 못했다 — 토큰은 루트를 겨냥한 " + "셀렉터로 정의해야 검증된다(tokens.md §1)") + return + for fg, bg, need, label in CONTRAST_PAIRS: + if fg not in tokens or bg not in tokens: + rep.fail("contrast", f"[{theme}] 토큰 {fg} 또는 {bg}가 없다 — 필요: " + f"{', '.join(REQUIRED_TOKENS)}") + continue + r = contrast(tokens[fg], tokens[bg]) + if r is None: + continue + if r < need: + rep.fail("contrast", f"[{theme}] {label}: {fg} on {bg} = {r:.2f}:1 < {need}:1") + else: + rep.info("contrast", f"[{theme}] {label}: {r:.2f}:1 ≥ {need}:1") + + +def check_antipatterns(doc, rep): + raw = ET.tostring(doc.root, encoding="unicode") + if "linearGradient" in raw or "radialGradient" in raw: + rep.warn("anti-pattern", "그라데이션 사용 — 배경 그라데이션 금지") + if "drop-shadow" in raw or "feDropShadow" in raw or "feGaussianBlur" in raw: + rep.fail("anti-pattern", "그림자/블러 사용 — 1px 헤어라인만 쓴다") + for t in doc.iter_rendered("text"): + s = "".join(t.itertext()) + if EMOJI.search(s): + rep.fail("anti-pattern", f'이모지 라벨 "{s.strip()}" — PNG 렌더 시 tofu 위험') + if not classes(t): + rep.warn("font-role", f"{describe(doc, t)} class 없음") + + +def check_viewbox(doc, rep): + vb = [p for p in re.split(r"[\s,]+", (doc.root.get("viewBox") or "").strip()) if p] + if len(vb) != 4: + rep.fail("canvas", "viewBox가 없거나 값이 4개가 아니다") + return + try: + w, h = float(vb[2]), float(vb[3]) + except ValueError: + rep.fail("canvas", f"viewBox 값이 숫자가 아니다: {' '.join(vb)}") + return + if (w, h) not in KNOWN_CANVASES: + rep.warn("canvas", f"비표준 캔버스 {w:g}×{h:g} — geometry.md 좌표표가 맞지 않을 수 있다") + + +# ---------------------------------------------------------------- 추출 +def _tag_end(s, start): + q = None + for i in range(start, len(s)): + c = s[i] + if q: + if c == q: + q = None + elif c in "\"'": + q = c + elif c == ">": + return i + return None + + +def extract_svgs(src): + src = re.sub(r"", "", src, flags=re.S) + out, i = [], 0 + open_re = re.compile(r"/])") + any_re = re.compile(r"/])|") + while True: + m = open_re.search(src, i) + if not m: + return out + j = _tag_end(src, m.start()) + if j is None: + return out + if src[m.start():j + 1].rstrip().endswith("/>"): + i = j + 1 + continue + depth, k = 1, j + 1 + while depth > 0: + m2 = any_re.search(src, k) + if not m2: + return out + if m2.group(0).startswith(""): + depth += 1 + k = jj + 1 + out.append(src[m.start():k]) + i = k + + +def check_svg(svg_text, theme_override, rep): + try: + root = ET.fromstring(svg_text) + except ET.ParseError as e: + rep.fail("parse", f"SVG를 XML로 파싱할 수 없다: {e}") + return + doc = Doc(root) + rules = parse_style(doc, rep) + + themes = sorted({theme_override} if theme_override else declared_themes(rules)) + rep.info("theme", f"검증할 테마: {', '.join(themes)}" + + ("" if doc.ns else " (xmlns 없는 인라인 SVG)")) + + check_contract(doc, rules, rep) + check_colors(doc, rules, rep) + check_viewbox(doc, rep) + check_grid(doc, rep) + check_transform(doc, rep) + nodes = check_density(doc, rep) + check_focus_count(doc, rep) + check_a11y(doc, rep) + check_overflow(doc, rules, nodes, rep) + check_antipatterns(doc, rep) + + # 테마별 검증 — 루트 속성을 실제로 바꿔 캐스케이드가 다크 규칙을 켜게 한다. + original = root.get("data-theme") + for th in themes: + root.set("data-theme", th) + tokens = tokens_for(doc, rules) + check_tokens_are_hex(tokens, th, rep) + check_contrast(tokens, th, rep) + check_accent(doc, rules, th, token_values(doc, rules, "accent"), rep) + if original is None: + root.attrib.pop("data-theme", None) + else: + root.set("data-theme", original) + + +def main(): + ap = argparse.ArgumentParser(description="editorial-diagram 출력 검증") + ap.add_argument("path") + ap.add_argument("--theme", choices=("light", "dark")) + ap.add_argument("--quiet", action="store_true") + a = ap.parse_args() + try: + src = open(a.path, encoding="utf-8").read() + except OSError as e: + print(f"FAIL 파일을 읽을 수 없다: {e}") + return 1 + svgs = extract_svgs(src) + if not svgs: + print("FAIL 요소를 찾지 못했다") + return 1 + worst = "PASS" + for i, s in enumerate(svgs, 1): + rep = Report() + check_svg(s, a.theme, rep) + print(f"\n── SVG #{i} ── {rep.verdict}") + for lv, rule, msg in rep.items: + if a.quiet and lv == "INFO": + continue + print(f" {lv:<4} [{rule}] {msg}") + if rep.verdict == "FAIL" or (rep.verdict == "WARN" and worst == "PASS"): + worst = rep.verdict + print(f"\n결과: {worst} ({len(svgs)}개 SVG)") + return 1 if worst == "FAIL" else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugins/wigtn-plugins/skills/editorial-diagram/scripts/export.sh b/plugins/wigtn-plugins/skills/editorial-diagram/scripts/export.sh new file mode 100755 index 0000000..a399164 --- /dev/null +++ b/plugins/wigtn-plugins/skills/editorial-diagram/scripts/export.sh @@ -0,0 +1,258 @@ +#!/usr/bin/env bash +# editorial-diagram export — HTML 안의 인라인 SVG를 독립 .svg / .png 로 뽑는다. +# +# ./export.sh diagram.html # .svg + .png(@2x) +# ./export.sh diagram.html --scale 3 # PNG 3배 +# ./export.sh diagram.html --svg-only +# ./export.sh diagram.html --png-only +# +# PNG는 시스템 Chrome/Edge headless를 쓴다(설치 불필요). 없으면 SVG만 나오고 안내한다. +# 실패는 조용히 넘어가지 않는다 — 크기를 못 구하거나 PNG가 잘리면 non-zero로 끝난다. +set -euo pipefail + +SRC=""; SCALE=2; DO_SVG=1; DO_PNG=1 +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) sed -n '2,11p' "$0"; exit 0 ;; + --scale) + [ $# -ge 2 ] || { echo "--scale 에 값이 없다" >&2; exit 1; } + SCALE="$2"; shift 2 ;; + --svg-only) DO_PNG=0; shift ;; + --png-only) DO_SVG=0; shift ;; + -*) echo "알 수 없는 옵션: $1" >&2; exit 1 ;; + *) SRC="$1"; shift ;; + esac +done +[ -n "$SRC" ] || { echo "사용법: export.sh [--scale N] [--svg-only|--png-only]" >&2; exit 1; } +[ -f "$SRC" ] || { echo "파일 없음: $SRC" >&2; exit 1; } +case "$SCALE" in + ''|*[!0-9]*) echo "--scale 은 양의 정수여야 한다 (받은 값: '$SCALE')" >&2; exit 1 ;; + 0) echo "--scale 은 1 이상이어야 한다" >&2; exit 1 ;; +esac + +# .svg를 입력으로 받으면 출력이 원본을 덮어쓴다 — 접미사를 붙여 막는다. +BASE="${SRC%.*}" +case "$SRC" in *.svg) BASE="${BASE}-export" ;; esac + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +# --- SVG 추출 + 크기 결정 ----------------------------------------------------- +# 크기는 **루트 태그 안에서만** 읽는다. 문서 전체에서 첫 viewBox를 찾으면 +# 를 집어 8×8 PNG를 만들고도 성공으로 보고한다. +python3 - "$SRC" "$TMP" <<'PY' +import re, sys, pathlib + +NUM = r"[-+]?(?:\d*\.\d+|\d+)(?:[eE][-+]?\d+)?" +src = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8") +out = pathlib.Path(sys.argv[2]) +src = re.sub(r"", "", src, flags=re.S) + + +def tag_end(s, start): + q = None + for i in range(start, len(s)): + c = s[i] + if q: + if c == q: + q = None + elif c in "\"'": + q = c + elif c == ">": + return i + return None + + +def extract(s): + out, i = [], 0 + open_re = re.compile(r"/])") + any_re = re.compile(r"/])|") + while True: + m = open_re.search(s, i) + if not m: + return out + j = tag_end(s, m.start()) + if j is None: + return out + if s[m.start():j + 1].rstrip().endswith("/>"): + i = j + 1 + continue + depth, k = 1, j + 1 + while depth > 0: + m2 = any_re.search(s, k) + if not m2: + return out + if m2.group(0).startswith(""): + depth += 1 + k = jj + 1 + out.append(s[m.start():k]) + i = k + + +def attr(open_tag, name): + # \b는 stroke-width의 width에도 걸린다 → 하이픈 앞 경계를 명시한다 + m = re.search(rf'(?를 찾지 못했다") + +for i, s in enumerate(svgs, 1): + j = tag_end(s, 0) + open_tag = s[:j + 1] + w, h = size_of(open_tag, i) + if "xmlns=" not in open_tag: + s = s.replace("\n' + standalone, encoding="utf-8") + (out / f"{i}.html").write_text( + '' + '' + standalone, + encoding="utf-8") + (out / f"{i}.size").write_text(f"{w} {h}\n", encoding="utf-8") +(out / "count").write_text(str(len(svgs)), encoding="utf-8") +PY + +COUNT="$(cat "$TMP/count")" + +find_chrome() { + for c in \ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ + "/Applications/Chromium.app/Contents/MacOS/Chromium" \ + "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" \ + "$(command -v google-chrome || true)" \ + "$(command -v chromium || true)" \ + "$(command -v chromium-browser || true)"; do + [ -n "$c" ] && [ -x "$c" ] && { echo "$c"; return 0; } + done + return 1 +} + +# PNG가 끝까지 쓰였는지 — IEND 청크로 확인한다. 파일 크기가 잠깐 멈춘 것만으로는 +# "다 썼다"를 알 수 없어서, 잘린 PNG를 성공으로 보고하는 일이 생긴다. +png_complete() { + python3 - "$1" <<'PY' +import sys, pathlib +p = pathlib.Path(sys.argv[1]) +try: + d = p.read_bytes() +except OSError: + sys.exit(1) +# IEND 청크는 길이(4) + "IEND" + CRC(4) = 12바이트다. 8바이트만 보면 절대 일치하지 않는다. +sys.exit(0 if d[:8] == b"\x89PNG\r\n\x1a\n" and d[-12:] == b"\x00\x00\x00\x00IEND\xaeB`\x82" else 1) +PY +} + +# Chrome headless는 원격 폰트(@import)를 기다리느라 스크린샷을 쓰고도 종료하지 않는 +# 경우가 있다(오프라인·사내망). 완성된 PNG가 보이면 바로 끊는다. macOS엔 timeout(1)이 없다. +shoot() { + local chrome="$1" src="$2" out="$3" w="$4" h="$5" + rm -f "$out" + "$chrome" --headless=new --disable-gpu --no-sandbox --no-first-run \ + --disable-extensions --hide-scrollbars --virtual-time-budget=3000 \ + --run-all-compositor-stages-before-draw \ + --force-device-scale-factor="$SCALE" --window-size="$w,$h" \ + --user-data-dir="$TMP/chrome" --screenshot="$out" "file://$src" \ + >/dev/null 2>&1 & + local pid=$! n=0 done=1 prev=-1 cur=0 + while [ "$n" -lt 100 ]; do # 상한 ~30초 + sleep 0.3; n=$((n + 1)) + if [ -f "$out" ]; then + cur=$(wc -c < "$out" | tr -d " ") + # 크기가 멈춘 뒤에만 IEND를 확인한다 (매 폴링마다 python3를 띄우면 상한이 늘어난다) + if [ "$cur" -gt 0 ] && [ "$cur" = "$prev" ] && png_complete "$out"; then + done=0; kill "$pid" 2>/dev/null || true; break + fi + prev="$cur" + fi + kill -0 "$pid" 2>/dev/null || { done=0; break; } # 스스로 종료함 + done + if [ "$n" -ge 100 ]; then + kill "$pid" 2>/dev/null || true # 상한 도달 → 반드시 끊는다 + fi + { wait "$pid"; } >/dev/null 2>&1 || true + return "$done" +} + +rc=0 +for i in $(seq 1 "$COUNT"); do + suffix="" + if [ "$COUNT" -gt 1 ]; then suffix="-$i"; fi + + if [ "$DO_SVG" -eq 1 ]; then + cp "$TMP/$i.svg" "${BASE}${suffix}.svg" + echo "SVG ${BASE}${suffix}.svg" + fi + + if [ "$DO_PNG" -eq 1 ]; then + if CHROME="$(find_chrome)"; then + read -r W H < "$TMP/$i.size" || true + shoot "$CHROME" "$TMP/$i.html" "${BASE}${suffix}.png" "$W" "$H" || true + if [ -f "${BASE}${suffix}.png" ] && png_complete "${BASE}${suffix}.png"; then + echo "PNG ${BASE}${suffix}.png (@${SCALE}x = $((W * SCALE))×$((H * SCALE)))" + else + if [ "$DO_SVG" -eq 1 ]; then + echo "PNG 실패 — 완결된 PNG가 만들어지지 않았다 (SVG #$i). SVG는 정상 생성됨." >&2 + else + echo "PNG 실패 — 완결된 PNG가 만들어지지 않았다 (SVG #$i)." >&2 + fi + rm -f "${BASE}${suffix}.png" + rc=1 + fi + else + echo "PNG 건너뜀 — Chrome/Chromium/Edge를 찾지 못했다. SVG를 브라우저에서 열어 저장하거나 Chrome을 설치한다." >&2 + rc=1 + fi + fi +done + +cat <<'NOTE' + +주의 +- 독립 SVG는 폰트를 @import로 불러온다 → 브라우저에선 정상, Illustrator/Figma에선 + 폰트가 없으면 폴백된다. 벡터 편집이 목적이면 대상 머신에 Pretendard / + Space Grotesk / JetBrains Mono를 설치한다. +- PNG를 슬라이드에 넣을 땐 @2x 이상. 인쇄물은 @3x. +NOTE + +exit "$rc" diff --git a/plugins/wigtn-plugins/skills/editorial-diagram/scripts/selftest.py b/plugins/wigtn-plugins/skills/editorial-diagram/scripts/selftest.py new file mode 100644 index 0000000..1c20ebb --- /dev/null +++ b/plugins/wigtn-plugins/skills/editorial-diagram/scripts/selftest.py @@ -0,0 +1,460 @@ +#!/usr/bin/env python3 +"""check.py 회귀 테스트 — 우회 경로마다 픽스처 1개 + 변이 테스트. + + python3 selftest.py # 케이스만 + python3 selftest.py --mutate # 변이 테스트까지 (느림, 각 수정을 되돌려 확인) + +**케이스는 판정만이 아니라 어느 rule이 발화했는지까지 단언한다.** 판정만 보면 +"기능이 동작함"과 "기능이 완전히 망가짐"이 같은 결과로 보인다 — 실제로 그래서 +이전 스위트의 3케이스가 회귀를 못 잡았다. + +변이 테스트: 수정 지점을 되돌렸을 때 해당 케이스가 뒤집히지 않으면 그 테스트는 +아무것도 보증하지 않는다는 뜻이므로 실패로 처리한다. +""" +import argparse +import pathlib +import re +import shutil +import subprocess +import sys +import tempfile + +HERE = pathlib.Path(__file__).resolve().parent +CHECK = HERE / "check.py" + +GOOD = ("--paper:#FFFFFF;--paper-2:#F4F2F8;--ink:#1E1E28;--muted:#5A5A6E;" + "--line:#E6E3EE;--line-strong:#85819B;--accent:#9B51E0;--accent-deep:#6B2EAA;") +LOW_MUTED = GOOD.replace("--muted:#5A5A6E", "--muted:#8F8DA0") # 2.91:1 — AA 미달 + +BASE_RULES = ( + ".node-name{font-family:Pretendard;font-size:18px;fill:var(--ink)}" + ".sublabel{font-family:'JetBrains Mono',monospace;font-size:13px;fill:var(--muted)}" + ".node{fill:var(--paper-2);stroke:var(--line-strong)}" + ".dot{fill:var(--accent)}" +) +NODE = ('' + '') +LABEL = '주문 서비스' +LONG = "가나다라마바사아자차카타파하가나다라마바사아자차카타파하" # 28자 → 504px + + +def doc(body, *, tokens=GOOD, root="ed", xmlns=True, theme=None, + rules=BASE_RULES, extra_css="", root_sel=None): + ns = ' xmlns="http://www.w3.org/2000/svg"' if xmlns else "" + th = f' data-theme="{theme}"' if theme else "" + sel = root_sel or f"svg.{root}" + css = f"{sel}{{{tokens}}}{rules}{extra_css}" if tokens is not None else rules + extra_css + return (f'' + 'TD' + f"{body}") + + +# (이름, 기대판정, 기대 FAIL rule(None이면 무관), 문서) +CASES = [ + ("baseline", "PASS", None, doc(NODE + LABEL)), + + ("단위접미사_좌표", "FAIL", "4px-grid", + doc('' + LABEL)), + + ("xmlns없어도_오탐없음", "PASS", None, doc(NODE + LABEL, xmlns=False)), + + ("xmlns없어도_이모지_검출", "FAIL", "anti-pattern", + doc(NODE + 'DB \U0001F680', xmlns=False)), + + ("tspan_앵커_합성", "FAIL", ("label-overflow", "넘침"), + doc(NODE + f'{LONG}')), + + ("text_y_tspan_x_분리", "FAIL", ("label-overflow", "넘침"), + doc(NODE + f'{LONG}')), + + ("마름모_오버플로", "FAIL", "label-overflow", + doc('' + '' + f'{LONG}')), + + ("g노드는_계약위반", "FAIL", "contract", + doc('' + LABEL)), + + ("path노드는_계약위반", "FAIL", "contract", + doc('' + LABEL)), + + ("노드없으면_실패", "FAIL", "density", doc(LABEL)), + + ("points_홀수개", "FAIL", "label-overflow", + doc('' + LABEL)), + + ("media가_라이트토큰_덮기", "FAIL", "contrast", + doc(NODE + LABEL, tokens=None, + rules=f"svg.ed{{{LOW_MUTED}}}@media (prefers-color-scheme:dark)" + f"{{svg.ed{{--muted:#3A3A48}}}}" + BASE_RULES)), + + ("media_문자열속_중괄호", "FAIL", "contrast", + doc(NODE + LABEL, tokens=None, + rules=f'svg.ed{{{LOW_MUTED}}}@media screen{{i::after{{content:"}}"}}' + f"svg.ed{{--muted:#3A3A48}}}}" + BASE_RULES)), + + # 다크 블록은 선언만으로도 검증돼야 한다 (라이트는 정상, 다크만 저대비) + ("다크팔레트도_검증한다", "FAIL", ("contrast", "[dark]"), + doc(NODE + LABEL, tokens=None, + rules=f"svg.ed{{{GOOD}}}svg.ed[data-theme=\"dark\"]{{--muted:#24242E;" + "--paper-2:#1E1E28;--paper:#15151E;--ink:#F5F4FA;--line-strong:#6E6C84}}" + + BASE_RULES)), + + ("따옴표없는_data-theme도_인식", "FAIL", ("contrast", "[dark]"), + doc(NODE + LABEL, tokens=None, + rules=f"svg.ed{{{GOOD}}}svg.ed[data-theme=dark]{{--muted:#24242E;" + "--paper-2:#1E1E28;--paper:#15151E;--ink:#F5F4FA;--line-strong:#6E6C84}}" + + BASE_RULES)), + + ("부분일치_속성셀렉터는_계약위반", "FAIL", "css", + doc(NODE + LABEL, extra_css='text[data-x^=y]{fill:var(--accent)}')), + + # 중요 규칙을 BASE_RULES보다 앞에 둔다 — 뒤에 두면 문서순서만으로도 이겨서 + # "!important를 실제로 존중하는가"를 구별하지 못한다 + ("important가_특정성을_이긴다", "FAIL", "accent", + doc(NODE + 'DB', + rules="text{fill:var(--accent)!important}" + BASE_RULES)), + + ("use는_계약위반", "FAIL", "contract", + doc('' + + NODE + LABEL + '')), + + ("foreignObject는_계약위반", "FAIL", "contract", + doc(NODE + LABEL + '')), + + ("g에_class주면_계약위반", "FAIL", "contract", + doc('' + NODE + LABEL + "")), + + ("자손이_토큰_재정의하면_계약위반", "FAIL", "contract", + doc(NODE + LABEL, extra_css=".dim{--muted:#8F8DA0}")), + + ("인라인style_fill은_계약위반", "FAIL", "contract", + doc(NODE + 'DB')), + + ("tspan_fill속성은_계약위반", "FAIL", "contract", + doc(NODE + '' + 'DB')), + + ("tspan_class는_계약위반", "FAIL", "contract", + doc(NODE + '' + '가나다', extra_css=".big{font-size:40px}")), + + ("루트svg에_fill선언은_계약위반", "FAIL", "contract", + doc(NODE + 'DB', + extra_css="svg{fill:var(--accent)}.plain{font-family:Pretendard;font-size:18px}")), + + ("중첩svg는_계약위반", "FAIL", "contract", + doc(NODE + LABEL + '' + '')), + + ("앵커기본값은_start", "FAIL", ("label-overflow", "anchor=start"), + doc('' + '' + # .foot에는 text-anchor 선언이 없다 → SVG 기본값 start → 오른쪽으로 이탈 + f'{LONG[:20]}')), + + # 색 표현 계약은 토큰뿐 아니라 **모든 도형의 fill/stroke**에 적용된다 + ("도형fill_rgb는_계약위반", "FAIL", "contract", + doc(NODE + LABEL, extra_css=".node{fill:rgb(0 0 0)}")), + + ("도형fill_colormix는_계약위반", "FAIL", "contract", + doc(NODE + LABEL, extra_css=".node{fill:color-mix(in srgb,#000 5%,#fff)}")), + + ("도형stroke_색이름은_계약위반", "FAIL", "contract", + doc(NODE + LABEL, extra_css=".node{stroke:rebeccapurple}")), + + ("표현속성_색_rgb는_계약위반", "FAIL", "contract", + doc('' + '' + LABEL)), + + ("선언안된_토큰참조는_계약위반", "FAIL", "contract", + doc(NODE + LABEL, extra_css=".node{fill:var(--nope)}")), + + ("fill_none은_허용", "PASS", None, + doc(NODE + LABEL + '', + extra_css=".link{stroke:var(--line-strong);fill:none}")), + + ("리터럴HEX로_악센트채우기_금지", "FAIL", "contract", + doc(NODE + LABEL, extra_css=".focal{fill:#9B51E0}")), + + ("리터럴HEX_line테두리_금지", "FAIL", "contract", + doc(NODE + LABEL, extra_css=".node{stroke:#E6E3EE}")), + + ("결합자셀렉터가_stroke만_선언해도_계약위반", "FAIL", "css", + doc(NODE + LABEL, extra_css="svg .node{stroke:var(--line)}")), + + ("ry도_4의배수집합만", "FAIL", "contract", + doc('' + '' + LABEL)), + + ("음수폭_노드는_검사불가", "FAIL", "label-overflow", + doc('' + '' + LABEL)), + + ("패딩보다_좁은노드", "FAIL", ("label-overflow", "좁아"), + doc('' + '' + 'A')), + + ("콤마셀렉터는_지원한다", "FAIL", "accent", + doc(NODE + 'DB', + extra_css=".zzz,.hot{fill:var(--accent)}")), + + ("end앵커_왼쪽이탈", "FAIL", ("label-overflow", "anchor=end"), + doc(NODE + '가나다라마바사아자차카타', + extra_css=".end{text-anchor:end}")), + + ("a11y_role누락", "FAIL", "a11y", + doc(NODE + LABEL).replace(' role="img"', "", 1)), + + ("a11y_desc비어있음", "FAIL", "a11y", + doc(NODE + LABEL).replace("D", '', 1)), + + ("viewBox_누락", "FAIL", "canvas", + doc(NODE + LABEL).replace(' viewBox="0 0 1280 720"', "", 1)), + + ("노드_10개면_초과", "FAIL", "density", + doc("".join(f'' + for i in range(10)) + NODE + LABEL)), + + # 박스 [64,416], anchor=start, x=240 → 오른쪽 한계는 PAD=20이면 396, PAD=0이면 416. + # 9자 × 18px = 162px → [240,402]: PAD=20에서만 넘친다. 경계값을 실제로 고정한다. + ("패딩경계_20px_지킨다", "FAIL", ("label-overflow", "넘침"), + doc('' + '' + f'{LONG[:9]}')), + + ("rx는_4의배수집합만", "FAIL", "contract", + doc('' + '' + LABEL)), + + ("초점을_악센트로_채우면_계약위반", "FAIL", "contract", + doc(NODE + LABEL, extra_css=".focal{fill:var(--accent)}")), + + ("노드테두리에_line은_계약위반", "FAIL", "contract", + doc(NODE + LABEL, extra_css=".node{stroke:var(--line)}")), + + ("루트클래스_변경해도_검증", "FAIL", "contrast", + doc(NODE + LABEL, root="diag", tokens=LOW_MUTED, root_sel="svg.diag")), + + ("rgb_퍼센트는_계약위반", "FAIL", "contract", + doc(NODE + LABEL, tokens=GOOD.replace("--muted:#5A5A6E", "--muted:rgb(60% 58% 59%)"))), + + ("color_mix는_계약위반", "FAIL", "contract", + doc(NODE + LABEL, + tokens=GOOD.replace("--paper-2:#F4F2F8", "--paper-2:color-mix(in srgb,#000 5%,#fff)"))), + + ("토큰없으면_실패", "FAIL", "contrast", doc(NODE + LABEL, tokens=None)), + + ("CSS악센트_텍스트색", "FAIL", "accent", + doc(NODE + 'DB', + extra_css=".hot{fill:var(--accent)}")), + + # .foot에는 경쟁하는 fill 규칙이 없다 — 그래야 type 셀렉터가 실제로 적용된다 + # (.node-name{fill:var(--ink)}은 특정성 0,1,0으로 text{...} 0,0,1을 이긴다) + ("type셀렉터_악센트", "FAIL", "accent", + doc(NODE + LABEL + 'src', + extra_css="text{fill:var(--accent)}")), + + ("id셀렉터_악센트", "FAIL", "accent", + doc(NODE + 'DB', + extra_css="#lbl{fill:var(--accent)}")), + + ("특정성_높은쪽이_이긴다", "FAIL", "accent", + doc(NODE + 'DB', + extra_css=".node-name.hot{fill:var(--accent)}.node-name{fill:var(--ink)}")), + + ("text_fill속성은_계약위반", "FAIL", "contract", + doc(NODE + 'DB')), + + ("pt폰트는_계약위반", "FAIL", "contract", + doc(NODE + f'{LONG[:15]}', + extra_css=".node-name{font-size:24pt}")), + + ("인라인style_폰트_반영", "FAIL", "label-overflow", + doc(NODE + f'' + f"{LONG[:15]}")), + + ("결합자셀렉터는_계약위반", "FAIL", "css", + doc(NODE + LABEL, extra_css="svg .node-name{fill:var(--accent)}")), + + ("defs템플릿은_카운트제외", "PASS", None, + doc('' + '' + '' + '' + '' + NODE + LABEL)), + + ("arc플래그_오탐없음", "PASS", None, + doc(NODE + LABEL + '')), + + ("체크표시는_이모지_아님", "PASS", None, + doc(NODE + '완료 ✓ 실패 ✗')), + + ("자기닫힘_아이콘svg_건너뜀", "PASS", None, + '' + + doc(NODE + LABEL).split("")[1]), +] + +# 변이: (설명, check.py 원문, 치환문, 뒤집혀야 하는 케이스 이름들) +MUTATIONS = [ + ("단위접미사 fail-closed 제거", " except (TypeError, ValueError):\n return BAD", + " except (TypeError, ValueError):\n return None", ["단위접미사_좌표"]), + ("defs 제외 제거", + 'NON_RENDERED = ("defs", "marker", "symbol", "pattern", "clipPath", "mask")', + "NON_RENDERED = ()", ["defs템플릿은_카운트제외"]), + ("at-rule 제거 무력화", " out, i, removed = [], 0, 0", + " return css, 0\n out, i, removed = [], 0, 0", + ["media가_라이트토큰_덮기", "media_문자열속_중괄호"]), + ("HEX 전용 계약 해제", " m = HEX_RE.match(str(v).strip())\n if not m:\n return None", + " m = HEX_RE.match(str(v).strip())\n if not m:\n return (0.5, 0.5, 0.5)", + ["rgb_퍼센트는_계약위반", "color_mix는_계약위반"]), + ("tspan 앵커 합성 제거", + " for sp in el.iter(doc.q(\"tspan\")):\n if x in (None, BAD):\n" + " x = num(sp.get(\"x\"))\n if y in (None, BAD):\n" + " y = num(sp.get(\"y\"))", + " pass", ["tspan_앵커_합성", "text_y_tspan_x_분리"]), + ("arc 플래그 제외 해제", "ARC_COORD_IDX = {5, 6}", "ARC_COORD_IDX = {0,1,2,3,4,5,6}", + ["arc플래그_오탐없음"]), + ("특정성 무시(문서순서만)", " key = (prop in imps, sel.spec, order)", + " key = (0, 0, order)", ["특정성_높은쪽이_이긴다", "important가_특정성을_이긴다"]), + ("a11y 검사 무력화", "def check_a11y(doc, rep):\n svg = doc.root", + "def check_a11y(doc, rep):\n return\n svg = doc.root", + ["a11y_role누락", "a11y_desc비어있음"]), + ("viewBox 검사 무력화", + "def check_viewbox(doc, rep):\n vb =", "def check_viewbox(doc, rep):\n return\n vb =", + ["viewBox_누락"]), + ("밀도 상한 해제", "MAX_NODES = 9", "MAX_NODES = 99999", ["노드_10개면_초과"]), + ("패딩 예산 제거", "PAD = 20 ", "PAD = 0 ", ["패딩경계_20px_지킨다"]), + # (구 "다크 테마 발견 무력화" 변이는 제거했다 — declared_themes를 끄면 토큰 재정의 + # 검사가 먼저 contract로 FAIL해서, 대비가 검증된다는 명제를 증명하지 못하고 뒤집힌 것처럼 + # 보였다. 대신 "다크 대비 평가를 라이트로 고정" 변이가 그 명제를 직접 겨냥한다.) + # (속성 "값" 비교 변이는 넣지 않는다 — 자기일관된 다크 팔레트에서는 값 비교를 + # 무시해도 다크가 전역 적용될 뿐 판정이 바뀌지 않아, 구별 불가능한 변이가 된다. + # 속성 파싱 자체는 위 두 케이스와 "다크 테마 발견 무력화" 변이가 덮는다.) + ("요소 화이트리스트 해제", " if t not in ALLOWED_ELEMENTS:", + " if False:", ["use는_계약위반", "foreignObject는_계약위반"]), + ("tspan/g class 금지 해제", ' if t in ("g", "tspan") and (classes(el) or el.get("style")):', + " if False:", ["g에_class주면_계약위반", "tspan_class는_계약위반"]), + ("루트 상속 속성 금지 해제", " watched = sorted(k for k in decls if k in INHERITED_PROPS)", + " watched = []", ["루트svg에_fill선언은_계약위반"]), + ("중첩 svg 허용", ' if t == "svg" and el is not doc.root:', + " if False:", ["중첩svg는_계약위반"]), + ("rx/ry 집합 해제", ' for a in ("rx", "ry"):', " for a in ():", + ["rx는_4의배수집합만", "ry도_4의배수집합만"]), + ("색 전역 검증 무력화", "def check_colors(doc, rules, rep):\n names =", + "def check_colors(doc, rules, rep):\n return\n names =", + ["도형fill_rgb는_계약위반", "도형fill_colormix는_계약위반", + "도형stroke_색이름은_계약위반", "표현속성_색_rgb는_계약위반", + "선언안된_토큰참조는_계약위반"]), + ("미선언 토큰 참조 허용", " return m.group(1) in token_names", " return True", + ["선언안된_토큰참조는_계약위반"]), + ("리터럴 값 비교 해제", " rgb = hex_rgb(value)\n return rgb is not None and rgb in values", + " return False", ["리터럴HEX로_악센트채우기_금지", "리터럴HEX_line테두리_금지"]), + ("stroke 감시 해제", + 'CHECKED_PROPS = ("fill", "stroke", "font-size", "font-family", "text-anchor")', + 'CHECKED_PROPS = ("fill", "font-size", "font-family", "text-anchor")', + ["결합자셀렉터가_stroke만_선언해도_계약위반"]), + ("음수 크기 허용", ' if g["width"] <= 0 or g["height"] <= 0:\n return None', + " pass", ["음수폭_노드는_검사불가"]), + ("좁은 노드 분기 제거", " if bw < 2 * PAD:", " if False:", ["패딩보다_좁은노드"]), + ("콤마 분해 제거", ' for part in raw.split(","):', " for part in [raw]:", + ["콤마셀렉터는_지원한다"]), + ("앵커 기본값 middle로 되돌림", '"text-anchor") or "start")', '"text-anchor") or "middle")', + ["앵커기본값은_start"]), + ("다크 대비 평가를 라이트로 고정", " for th in themes:\n root.set", + ' for th in ["light"]:\n root.set', + ["다크팔레트도_검증한다", "따옴표없는_data-theme도_인식"]), + ("text-anchor 해석 무시", ' v = (inline_prop(el, "text-anchor") or declared', + ' v = ("start" if False else "middle") or (inline_prop(el, "text-anchor") or declared', + ["end앵커_왼쪽이탈"]), +] + + +def run(text): + with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False, encoding="utf-8") as f: + f.write(text) + path = f.name + try: + out = subprocess.run([sys.executable, str(CHECK), path, "--quiet"], + capture_output=True, text=True).stdout + verdict = ("FAIL" if "결과: FAIL" in out else + "WARN" if "결과: WARN" in out else + "PASS" if "결과: PASS" in out else "?") + fails = set(re.findall(r"FAIL \[([-\w]+)\]", out)) + return verdict, fails, out + finally: + pathlib.Path(path).unlink(missing_ok=True) + + +def run_cases(verbose=True): + bad = [] + for name, want, rule, text in CASES: + got, fails, out = run(text) + ok = (got == want) or (want == "PASS" and got == "WARN") + if ok and rule: + want_rule, want_msg = rule if isinstance(rule, tuple) else (rule, None) + ok = want_rule in fails + if ok and want_msg: + ok = any(want_msg in ln for ln in out.splitlines() + if f"FAIL [{want_rule}]" in ln) + if verbose: + extra = f" rules={sorted(fails)}" if not ok else "" + label = rule[0] if isinstance(rule, tuple) else rule + print(f" {'ok ' if ok else 'FAIL'} {name:<30} 기대={want}" + f"{'/' + label if label else '':<18} 실제={got}{extra}") + if not ok: + for line in out.strip().splitlines()[1:5]: + print(f" {line}") + if not ok: + bad.append(name) + return bad + + +def run_mutations(): + """수정을 되돌렸을 때 케이스가 뒤집히지 않으면 그 테스트는 무의미하다.""" + src = CHECK.read_text(encoding="utf-8") + bak = CHECK.with_suffix(".py.bak") + shutil.copy(CHECK, bak) + bad = [] + try: + for desc, old, new, targets in MUTATIONS: + if old not in src: + print(f" FAIL 변이 '{desc}' — 대상 코드를 찾지 못했다(테스트가 낡음)") + bad.append(desc) + continue + CHECK.write_text(src.replace(old, new, 1), encoding="utf-8") + broke = run_cases(verbose=False) + missed = [t for t in targets if t not in broke] + mark = "ok " if not missed else "FAIL" + print(f" {mark} 변이: {desc:<28} 뒤집힘={len(targets) - len(missed)}/{len(targets)}") + if missed: + print(f" 뒤집히지 않은 케이스(무의미): {', '.join(missed)}") + bad.append(desc) + finally: + shutil.move(str(bak), str(CHECK)) + return bad + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--mutate", action="store_true", help="변이 테스트까지 실행") + a = ap.parse_args() + + print("케이스") + bad = run_cases() + print(f"\n {len(CASES) - len(bad)}/{len(CASES)} 통과") + + mbad = [] + if a.mutate: + print("\n변이 테스트 (수정을 되돌리면 뒤집혀야 한다)") + mbad = run_mutations() + print(f"\n {len(MUTATIONS) - len(mbad)}/{len(MUTATIONS)} 유효") + + return 1 if bad or mbad else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugins/wigtn-plugins/skills/wigtn-ppt/SKILL.md b/plugins/wigtn-plugins/skills/wigtn-ppt/SKILL.md index 024b45e..b36fe08 100644 --- a/plugins/wigtn-plugins/skills/wigtn-ppt/SKILL.md +++ b/plugins/wigtn-plugins/skills/wigtn-ppt/SKILL.md @@ -60,7 +60,15 @@ WIGTN 브랜드 정체성을 담은 **단일 HTML 프레젠테이션**을 만드 ### Phase 2: 다이어그램 (필요 시) -흐름도·아키텍처가 필요하면 같은 플러그인의 **`handdrawn-diagram` 스킬**을 호출해 committable 다이어그램(SVG+PNG)을 만들고 슬라이드에 삽입한다. 손그림 톤이 부담되면 단순 CSS/SVG 도형으로 직접 그린다. +흐름도·아키텍처·비교 도식이 필요하면 **`editorial-diagram` 스킬을 호출한다(기본 경로).** +브랜드 발표물의 톤에 맞는 정갈한 인라인 SVG를 4px 그리드·밀도 예산·WIGTN 토큰으로 만들고, +`check.py`로 검증한 뒤 슬라이드에 삽입한다. 삽입은 둘 중 하나: + +- **인라인 SVG 직접 붙여넣기** (권장) — 슬라이드와 같은 테마 토큰을 쓰므로 Light/Dark가 자동으로 맞는다 +- `export.sh`로 뽑은 PNG를 ``로 (외부 배포·PPTX 변환 대상일 때) + +**도식을 직접 손으로 그리지 않는다.** 가이드 없는 CSS/SVG 도형이 "AI가 만든 티"의 주범이다. +손그림 톤이 어울리는 자리(해커톤·캐주얼 공유)에서만 `handdrawn-diagram`을 쓴다. ### Phase 3: HTML 생성 @@ -106,12 +114,14 @@ WIGTN 로고는 **팀 전용 에셋**이라 공개 플러그인에 커밋하지 - **[references/brand.md](references/brand.md)** — WIGTN 브랜드 시스템: Light/Dark 팔레트, 로고 변형·배치, 퍼플 점 규칙, 타이포, CSS 워드마크 폴백 - **[references/design-guide.md](references/design-guide.md)** — 슬라이드 타입별 레이아웃 원칙(표지/목차/섹션/본문/마무리), viewport 규칙, 애니메이션, anti-pattern - **`design-system-reference` 스킬** — 레이아웃 밀도·타이포 위계 아이디어 재활용(Swiss Minimal, Editorial, Minimal Corporate가 WIGTN 톤에 가장 근접). 단, 색·점·로고는 **항상 WIGTN 토큰으로 덮어쓴다**. -- **`handdrawn-diagram` 스킬** — 다이어그램이 필요한 슬라이드 +- **`editorial-diagram` 스킬** — 다이어그램 슬라이드의 **기본 경로**. 12개 유형 라우팅, + 4px 좌표표, 밀도 예산, WIGTN 토큰, `check.py` 검증, SVG/PNG export +- **`handdrawn-diagram` 스킬** — 손그림 톤이 어울리는 자리에서만 ## 체크리스트 - [ ] Phase 0: AskUserQuestion으로 테마(Light/Dark) + 콘텐츠 1회 수집 - [ ] Phase 1: 슬라이드 맵 + 밀도 제한 -- [ ] Phase 2: 다이어그램 필요 슬라이드 식별 (handdrawn-diagram) +- [ ] Phase 2: 다이어그램 필요 슬라이드 식별 → editorial-diagram 호출 (손그림 톤일 때만 handdrawn-diagram) - [ ] Phase 3: brand.md + design-guide.md Read → 단일 HTML 생성 (viewport, 퍼플 점, 로고) - [ ] Phase 4: 스크린샷 검증 → 전달 diff --git a/plugins/wigtn-plugins/skills/wigtn-ppt/references/design-guide.md b/plugins/wigtn-plugins/skills/wigtn-ppt/references/design-guide.md index aca3325..90a26d4 100644 --- a/plugins/wigtn-plugins/skills/wigtn-ppt/references/design-guide.md +++ b/plugins/wigtn-plugins/skills/wigtn-ppt/references/design-guide.md @@ -61,7 +61,7 @@ | Big Number / KPI | 지표 강조 | 초대형 숫자 + 라벨, 숫자 끝 퍼플 점 | | Quote | 인용/메시지 | 대형 인용문, 좌측 퍼플 세로바 | | Image + Text | 이미지 설명 | 좌 41% 텍스트 / 우 이미지(또는 반대) | -| Diagram | 흐름/구조 | handdrawn-diagram 또는 CSS/SVG 도형 | +| Diagram | 흐름/구조 | **editorial-diagram** 스킬로 생성해 인라인 SVG 삽입 (직접 그리지 않는다) | | Timeline | 단계/로드맵 | 가로/세로 라인 + 노드(노드를 퍼플 점으로) | 공통: Slide Title 좌상단(`clamp(1.4rem,2.6vw,2rem)` Bold), 우하단 작은 마크 로고 + 페이지 번호.