diff --git a/.github/scripts/validate_repository.py b/.github/scripts/validate_repository.py index bc0b6d6..fb8e9d6 100755 --- a/.github/scripts/validate_repository.py +++ b/.github/scripts/validate_repository.py @@ -16,6 +16,7 @@ "screen-spec", "acceptance-verifier", "design-direction", + "visual-craft", "verified-delivery", "release-readiness", "handdrawn-diagram", diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99f67fe..ed44a23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,11 @@ jobs: with: fetch-depth: 0 + - name: Set up Node for rendered-detector fixtures + uses: actions/setup-node@v4 + with: + node-version: "22" + - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 85468f2..7f7c0ea 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -21,6 +21,11 @@ jobs: - name: Check out repository uses: actions/checkout@v4 + - name: Set up Node for rendered-detector fixtures + uses: actions/setup-node@v4 + with: + node-version: "22" + - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/README.md b/README.md index f75e50f..78b829f 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ **PRD·저장 가능한 작업 계획·요구사항 검증·Git 작업을 위한 선택형 스킬 모음.** -![Version](https://img.shields.io/badge/version-0.5.6-6C5CE7?style=for-the-badge) -![Skills](https://img.shields.io/badge/core_skills-9-00B894?style=for-the-badge) +![Version](https://img.shields.io/badge/version-0.5.7-6C5CE7?style=for-the-badge) +![Skills](https://img.shields.io/badge/core_skills-10-00B894?style=for-the-badge) ![Platform](https://img.shields.io/badge/platform-Codex-111827?style=for-the-badge) ![License](https://img.shields.io/badge/license-Apache--2.0-0984E3?style=for-the-badge) @@ -58,6 +58,8 @@ Codex 앱에서는 Plugins를 열어 WIGTN 마켓플레이스를 선택한 뒤 ```text "OAuth 로그인 기능 PRD 뽑아줘" "이 PRD로 화면정의서 만들어줘" +"프론트 UI 만들어줘" +"이번 실험을 테크리포트 문서로 작성해줘" "요구사항이 실제 구현에 반영됐는지 검증해줘" "변경사항 검증하고 커밋해줘" ``` @@ -96,11 +98,13 @@ acceptance-verifier ── 요구사항 ↔ 코드·테스트 증거 매핑 release-readiness ──── 안전한 커밋·푸시·PR ``` +UI·문서 제작에는 `visual-craft`가 구성과 문구를 보조합니다. 파일 생성·렌더링은 해당 형식의 워크플로를 따릅니다. 오타 수정이나 백엔드 작업에는 적용하지 않습니다. + 문서나 발표가 필요할 때는 `handdrawn-diagram`, `wigtn-presentation`을 독립적으로 사용할 수 있습니다. --- -## 포함된 9개 Core 스킬 +## 포함된 10개 Core 스킬 | 스킬 | 하는 일 | 자연어 호출 예시 | 호출 정책 | |---|---|---|---| @@ -108,6 +112,7 @@ release-readiness ──── 안전한 커밋·푸시·PR | `screen-spec` | 요청한 IA, User Flow, 화면 명세, self-contained lo-fi HTML 와이어프레임 또는 개발 핸드오프 | “화면정의서 만들어줘” | 자동 | | `work-planner` | 요구사항을 저장·재개 가능한 dependency-aware WorkGraph로 변환 | “이 계획을 WorkGraph로 저장해줘” | 자동·저장형 전용 | | `acceptance-verifier` | PRD 요구사항과 코드·실행 테스트 증거를 연결해 충족 여부 판정 | “요구사항 반영됐는지 검증해줘” | 자동 | +| `visual-craft` | UI·문서 제작 시 정보 구조, 자연스러운 문구, 줄바꿈과 과도한 장식 검수 | “프론트 UI 만들어줘”, “문서 만들어줘” | 자동·제작 및 실질적 수정 | | `design-direction` | 기존 디자인 시스템을 먼저 읽고 신규 UI 방향 제안 | “이 UI 디자인 방향 잡아줘” | 자동·제한적 | | `verified-delivery` | 구현에 비례적인 requirement→code→executed-check 근거 추가 | `$verified-delivery로 구현해줘` | **명시 호출 전용** | | `release-readiness` | 변경 범위를 보존하며 검증, 커밋, 푸시, PR 수행 | “커밋해줘”, “PR 올려줘” | 자동·제한적 | @@ -120,6 +125,20 @@ release-readiness ──── 안전한 커밋·푸시·PR --- +## v0.5.7: Visual Craft와 자동 검사 + +`visual-craft`를 추가해 Core 스킬이 9개에서 10개로 늘었습니다. UI·문서 제작과 실질적인 수정에 적용하며, 기존 9개 스킬의 호출 정책은 유지합니다. + +- 프론트 UI와 문서·보고서·발표자료 제작 요청에서 자동 선택을 허용합니다. `$wigtn-plugins-with-codex:visual-craft`로 명시할 수도 있습니다. +- 공통 기준만 먼저 읽고, UI 배치와 문서 구성에 필요한 참조를 선택해 읽습니다. 기존 디자인 시스템·브랜드·출처·요구사항을 보존합니다. +- 실제 산출물의 줄바꿈·과밀 구성·장식·가독성을 검수합니다. 모든 작업에 별도 디자인 계획이나 도구 설치를 요구하지 않습니다. +- HTML·Markdown 소스 검사와 브라우저의 읽기 전용 배치 검사를 제공합니다. 확실한 오류와 문맥 검토 항목을 나누며, 검사 통과를 디자인 품질 점수로 취급하지 않습니다. +- 자동 선택 허용과 정적 검증은 실제 모델의 호출 성공률이나 디자인 품질 개선을 보장하지 않습니다. + +[변경 내용과 검증 범위](docs/RELEASE-v0.5.7-KO.md) · [English release notes](docs/RELEASE-v0.5.7.md) · [설계 근거](docs/VISUAL-CRAFT.md) + +--- + ## v0.5.6: 요구사항 원문 보존과 단일 문서 범위 수정 - 요구사항 importer가 `unit_price`, `ROUND_HALF_UP`, 곱셈 기호와 본문 내부 공백을 보존합니다. 서로 다른 본문을 같게 정규화해 이전 검증을 재사용하던 문제를 수정했습니다. diff --git a/docs/RELEASE-v0.5.7-KO.md b/docs/RELEASE-v0.5.7-KO.md new file mode 100644 index 0000000..dbec8a0 --- /dev/null +++ b/docs/RELEASE-v0.5.7-KO.md @@ -0,0 +1,42 @@ +# v0.5.7: Visual Craft와 읽기 전용 자동 검사 + +UI·문서·보고서·발표자료를 제작하거나 실질적으로 다듬을 때 구성, 정보 밀도, +문구, 줄바꿈과 과도한 장식을 검토하는 `visual-craft`를 추가했습니다. +Core 스킬은 9개에서 10개로 늘며, 기존 스킬과 호출 정책은 유지합니다. + +## 적용 방식 + +공통 기준을 읽고 요청한 산출물에 맞는 UI 또는 문서 참조를 선택합니다. +기존 브랜드·인용문·수치·요구사항을 보존하며, 파일 생성과 렌더링은 해당 +형식의 전문 워크플로를 따릅니다. 일반 대화·백엔드·오타 수정에는 개입하지 않습니다. +`$wigtn-plugins-with-codex:visual-craft`로 명시하거나 자연어 제작 요청에서 +Codex가 선택하도록 사용할 수 있습니다. + +## 자동 검사 + +- HTML·Markdown 검사: 중복 ID, 문서 언어, 내부 링크 대상, 입력 라벨, + 제목 단계, 강제 줄바꿈과 반복 기호를 검사합니다. Python 표준 라이브러리만 + 사용하며 파일 수정·스크립트 실행·외부 요청을 하지 않습니다. +- 브라우저 검사: 허용된 브라우저의 현재 화면에서 페이지 넘침, 잘린 글자, + 작은 글자와 작은 클릭 영역을 측정합니다. 별도 브라우저 실행기나 훅은 추가하지 않습니다. +- 확실한 소스 오류와 문맥 검토 항목을 구분합니다. 의도적인 말줄임, + 동적 라우트, 인용문과 브랜드를 경고 제거 목적으로 바꾸지 않습니다. + +## 검증 범위 + +릴리스 검증은 `bash scripts/validate.sh`로 실행합니다. 자동 검사 회귀 테스트 +17개와 그중 Node 기반 테스트의 합성 DOM 측정 조건 10개를 포함합니다. +CI에 Node를 명시해 측정 조건 테스트를 실행하도록 했습니다. + +로컬 HTML 시안 3개는 소스 검사상 검출 항목이 없었습니다. 실제 브라우저 +통합 검증과 디자인 품질 비교는 아직 완료하지 않았습니다. 자동 선택 설정과 +키워드 예시는 실제 호출 성공률을 입증하지 않으며, 모델 성능·속도·토큰 개선을 +주장하지 않습니다. 자동 검사는 시각 검수나 전체 접근성 검사를 대체하지 않습니다. + +과거 full8/full9 실험의 스킬 구성은 유지합니다. Knowledge Wiki는 공동 버전 +정책에 따라 manifest 버전만 변경합니다. + +임시 개인용 `visual-craft`를 설치했다면 배포본과 비교해 로컬 수정 여부를 +확인한 뒤 백업하고 중복 등록을 정리하세요. 업데이트 후 새 Codex 작업을 시작하세요. + +[설계 근거와 출처](VISUAL-CRAFT.md) diff --git a/docs/RELEASE-v0.5.7.md b/docs/RELEASE-v0.5.7.md new file mode 100644 index 0000000..3d1cf28 --- /dev/null +++ b/docs/RELEASE-v0.5.7.md @@ -0,0 +1,47 @@ +# v0.5.7: Visual Craft and read-only artifact checks + +UI and document creation can now use `visual-craft` to review hierarchy, density, +natural copy, wrapping and decorative clutter. The Core catalog grows from nine +to ten skills. Existing skills and their invocation policies remain available. + +## Artifact-specific guidance + +The skill supports frontend interfaces and authored documents, reports and slides. +It loads UI or document references according to the requested artifact, respects +existing brands and format-specific workflows, and preserves source quotations, +data and requirements. It excludes ordinary chat, backend work and typo-only edits. +Invoke it explicitly with `$wigtn-plugins-with-codex:visual-craft`, or let Codex +select it for a matching creation or substantial refinement request. + +## Automatic checks + +- The Python standard-library checker reads explicit HTML/Markdown files and + reports duplicate IDs, language declarations, fragment targets, source-level + labels, heading structure and contextual copy patterns. It does not execute + embedded scripts, fetch links or edit files. +- A read-only browser expression measures viewport overflow, clipped text, small + text and small targets within the current viewport. It uses an already-authorized + browser; no browser launcher, hook or third-party package is installed. +- Confirmed source defects and contextual review findings are separate. Intentional + truncation, dynamic hash routes, quotations and brand marks must not be rewritten + simply to clear a warning. Findings are not aesthetic scores. + +## Validation and limitations + +- The release runs `bash scripts/validate.sh`, including 17 detector regression + tests and ten synthetic DOM geometry assertions inside the Node fixture test. + CI explicitly provisions Node so the geometry fixture can run. +- Source checks of three local HTML samples found no findings. Real-browser + integration and visual quality comparison remain unverified. Geometry fixtures + do not replace browser testing, keyboard checks or a full accessibility audit. +- Automatic selection metadata and lexical fixtures do not establish model routing + accuracy. No GPT-6 quality, speed or token-saving improvement is claimed. +- Historical full8/full9 ablation catalogs retain their original skill membership; + they do not represent the new ten-skill package. +- Knowledge Wiki receives only the matching version under the shared-version policy. + +If you installed the temporary personal `visual-craft` copy, compare it with the +packaged skill and archive it after the plugin update, preserving any local edits. +Start a new Codex task to pick up the updated plugin. + +[Design rationale and research sources](VISUAL-CRAFT.md) diff --git a/docs/VISUAL-CRAFT.md b/docs/VISUAL-CRAFT.md new file mode 100644 index 0000000..c4aeaff --- /dev/null +++ b/docs/VISUAL-CRAFT.md @@ -0,0 +1,101 @@ +# Visual Craft: scope and validation + +Release: v0.5.7. No model-quality or token-saving claim. + +## Design choice + +One implicitly discoverable skill supports frontend UI and authored documents. +The entrypoint carries common decisions; UI and document guidance load by artifact. +No third-party runtime dependency, hook, renderer, style menu or mandatory planning file is added. +Read-only checks use Python and the already-authorized browser workflow. +Existing format and product skills retain responsibility for their own outputs. +The new skill does not force WIGTN branding on unrelated documents. + +The default excludes typo-only edits, ordinary chat and backend work. Direction-only +requests still belong to design-direction; PRD and screen content contracts belong +to their existing skills. A relevant craft pass can accompany those artifacts without +starting a second workflow. Skill selection remains a model decision, not a keyword hook. + +## Research provenance + +Reviewed upstream main on 2026-09-22. These are independently written instructions +informed by the following design approaches, not vendored upstream skill files. +Upstream links may change after this review. + +- [Anthropic frontend-design](https://github.com/anthropics/skills/blob/main/skills/frontend-design/SKILL.md): + brief-specific design, content-led hierarchy, meaningful structural markers and plain copy. +- [Taste-skill](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md): + concrete examples of decorative dots, broken headings and detached explanatory text. + Do not import its global font bans, motion defaults, punctuation replacement in quotes, + or forced asymmetric layouts. +- [Impeccable](https://github.com/pbakaus/impeccable): + focused simplification, typography, layout and copy review with bounded visual checks. + Do not import its binary launcher, installer, hooks or whole command suite. + +Korean writing, source fidelity, publication formats and cooperation with existing +WIGTN contracts are local additions. An em dash in a quotation, a status dot and the +WIGTN logo period are intentionally different from decorative new copy. + +## Validation and practical limits + +Run `bash scripts/validate.sh` for packaging, resource links, explicit/implicit policy +and repository regressions. The trigger fixtures are lexical examples only: changing +their mapping does not establish actual Codex routing. No automatic visual-quality +score is introduced. Historical full8/full9 experiment catalogs retain their old +skill membership; neither represents the ten-skill package introduced in v0.5.7. + +Before claiming a quality improvement, compare generated artifacts with and without +the skill using the same brief, assets, model and settings. Inspect rendered outputs, +preserved content and task completion; include latency and input usage rather than +rewarding deletion alone. Useful behavioral cases include: + +| Request | Observable outcome to inspect | +|---|---| +| Korean product landing page | Natural heading wraps at narrow/wide widths; functional CTA; no invented proof | +| Dense operations dashboard | Required columns and actions remain usable; no marketing hero added | +| Technical report from supplied measurements | Exact data, sources and limitations survive; no fabricated improvement | +| Word report with a long table | Valid requested file; readable repeated headers and page breaks | +| Generic slides | Requested deck format, no accidental WIGTN brand | +| WIGTN deck | Approved brand and logo dot preserved; readable slides | +| Existing legal text and quotations | Source text unchanged despite punctuation preferences | +| Small CSS or typo fix | Only requested change, no redesign or new documents | + +These cases define a follow-up behavioral evaluation, not completed model runs. + +## Checks executed on 2026-09-22 + +- Full `bash scripts/validate.sh`: PASS, including ten core skill manifests, + all resource links, 65 lexical routing examples and existing repository contracts. +- `git diff --check`: PASS. +- Personal installation of this skill only: validated with `quick_validate.py`. +- Real Codex app-server `skills/list` with forceReload: one enabled user-scope + `visual-craft` entry, no related discovery errors. No model generation was run. +- The entrypoint is 55 lines; the UI and document references are selected by task. + File length is an implementation fact, not a measured token saving. + +Before v0.5.7, local testing used a personal copy with the unqualified +`$visual-craft` default prompt. After installing v0.5.7, archive that copy only after +checking for local edits and confirming the packaged skill is enabled, so duplicate +discovery does not remain. The plugin uses `$wigtn-plugins-with-codex:visual-craft`. + +## Automatic detectors added on 2026-09-23 + +The skill now routes artifact completion through `references/automatic-checks.md`. +`scripts/audit-source.py` checks explicit HTML/Markdown files and emits bounded JSON +findings with rule, severity and source line. `scripts/audit-rendered.js` is a +read-only expression for an existing authorized browser evaluation API. It reports +viewport overflow and contextual clipping, small text and small target findings. +Neither script installs a hook, fetches resources or modifies the artifact. + +Source checks deliberately do not claim browser visibility, full Markdown parsing, +JSX support or document-format validation. Rendered checks cover only the current +viewport, excluding iframe contents and shadow roots. Contrast, keyboard behavior, +copy correctness and aesthetic quality are not certified. Browser permission blocks +remain blocks; the detector is not an alternate browser launcher. + +Validation: 17 regression tests passed, including a Node VM fixture with ten DOM +geometry assertions. Tests cover positive findings, protected quotations/code, +native labels, inert templates, decoded fragment targets, bounded reporting, +unsupported inputs and read-only operation. Geometry fixtures do not constitute a +real-browser integration test. The three existing smoke-test HTML files returned +no source findings; this is not a rendered or interaction pass. diff --git a/plugins/wigtn-knowledge-wiki/.codex-plugin/plugin.json b/plugins/wigtn-knowledge-wiki/.codex-plugin/plugin.json index cae730c..74c7e80 100644 --- a/plugins/wigtn-knowledge-wiki/.codex-plugin/plugin.json +++ b/plugins/wigtn-knowledge-wiki/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "wigtn-knowledge-wiki", - "version": "0.5.6", + "version": "0.5.7", "description": "Opt-in, gated Codex session knowledge capture for an explicitly authorized personal wiki namespace.", "author": { "name": "WIGTN" diff --git a/plugins/wigtn-plugins-with-codex/.codex-plugin/plugin.json b/plugins/wigtn-plugins-with-codex/.codex-plugin/plugin.json index 15b40cc..de39337 100644 --- a/plugins/wigtn-plugins-with-codex/.codex-plugin/plugin.json +++ b/plugins/wigtn-plugins-with-codex/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "wigtn-plugins-with-codex", - "version": "0.5.6", - "description": "Selective product contracts for Codex: compact PRDs, saved WorkGraphs, scoped screen artifacts, acceptance evidence, explicit verified delivery, and safe release actions.", + "version": "0.5.7", + "description": "Selective product contracts and visual craft for Codex: PRDs, saved WorkGraphs, screen artifacts, UI and document composition, acceptance evidence, and safe release actions.", "author": { "name": "WIGTN" }, @@ -9,7 +9,7 @@ "interface": { "displayName": "WIGTN Plugin for Codex", "shortDescription": "Product contracts, evidence, and safe delivery.", - "longDescription": "Adds compact product contracts, saved WorkGraph planning, scoped screen artifacts, evidence integrity, and release authority boundaries while leaving ordinary Codex coding unconstrained. Verified delivery remains explicit-only.", + "longDescription": "Adds compact product contracts, saved WorkGraph planning, scoped screen artifacts, UI and document craft, evidence integrity, and release authority boundaries. Visual craft applies to artifact creation and substantial refinement; ordinary fixes stay lightweight. Verified delivery remains explicit-only.", "developerName": "WIGTN", "category": "Productivity", "capabilities": [], diff --git a/plugins/wigtn-plugins-with-codex/skills/visual-craft/SKILL.md b/plugins/wigtn-plugins-with-codex/skills/visual-craft/SKILL.md new file mode 100644 index 0000000..0b27084 --- /dev/null +++ b/plugins/wigtn-plugins-with-codex/skills/visual-craft/SKILL.md @@ -0,0 +1,59 @@ +--- +name: visual-craft +description: Create or substantially refine frontend UI and authored documents (문서, reports, slides) with clear hierarchy, natural copy and restrained styling. Use for UI creation, document drafting or AI-looking design cleanup; not backend work, typo-only edits or ordinary chat. +--- + +# Visual Craft + +Make the requested interface or document feel specific to its content and audience. +Preserve the user's format, language, factual meaning and existing design system. +This is a craft layer, not a prerequisite planning pipeline. + +## Apply only what the artifact needs + +- Use the brief, real content and representative existing work to choose hierarchy + and density. If these already settle the direction, proceed without a style menu. +- For frontend implementation or visual refinement, read [UI](references/ui.md). + For authored documents, reports, slides or substantial prose, read + [documents](references/documents.md). Read both only for a document with a custom + web interface. Do not turn a plain document into a website or slide deck. +- Available document, PDF and presentation skills own file creation and rendering; + product-spec and screen-spec own their requested content contracts. Apply craft + within those workflows, without repeating their planning or verification. + An existing design-direction decision is input, not a reason to restart design. +- Use this independently when those skills are absent. Change only the requested + artifact; a local adjustment does not authorize a site-wide redesign. + +## Shared decisions + +- Give each section a clear purpose. Separate unrelated decisions; combine repeated + explanations. Dense tables and reference material may need high information + density. Do not delete requirements or evidence to make a layout airy. +- Establish hierarchy through grouping, type and spacing before adding containers. + Repeated cards, nested panels and tiny labels should earn their place. +- Choose alignment for the reading order and content relationships. Neither left + alignment, centering nor asymmetry is a universal style requirement. +- Avoid decorative status dots, numbering, symbol strips and em-dash slogans in + newly authored copy. Preserve meaningful punctuation, genuine state indicators, + ordered steps, source quotations and brand marks, including the WIGTN logo dot. + An explicit user style or established brand takes precedence over these defaults. +- Let sentences wrap naturally. Do not force line breaks or isolate one italic or + colored word merely to make a heading look designed. A deliberate title treatment + is valid when requested and checked at the actual output sizes. +- Write concrete copy in the audience's language. Keep familiar technical terms + when they are clearer than a forced translation. Do not invent metrics, customer + quotes, awards, screenshots or product capabilities as decoration. +- Do not replace a generic look with another preset. Fonts, palettes, radii and + motion are choices tied to this artifact, not global bans or mandatory effects. + +## Finish proportionately + +For a created or substantially edited artifact, follow the +[automatic checks](references/automatic-checks.md): scan explicit HTML/Markdown +files and inspect rendered geometry when an authorized browser is available. +Treat contextual findings as review prompts, not mandatory style changes. +Inspect the rendered artifact using the relevant reference and available tools. +Fix observed reading, overflow, hierarchy and interaction defects together, then +recheck affected views. Stop when the requested output works; do not add a style +score, redesign rounds or new deliverables unless requested. Distinguish rendered +checks from source-only review and report material unchecked areas honestly. diff --git a/plugins/wigtn-plugins-with-codex/skills/visual-craft/agents/openai.yaml b/plugins/wigtn-plugins-with-codex/skills/visual-craft/agents/openai.yaml new file mode 100644 index 0000000..14f50be --- /dev/null +++ b/plugins/wigtn-plugins-with-codex/skills/visual-craft/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "WIGTN Visual Craft" + short_description: "Clear UI and documents without template clutter" + default_prompt: "Use $wigtn-plugins-with-codex:visual-craft to create or refine this UI or document with clear hierarchy and natural copy." +policy: + allow_implicit_invocation: true diff --git a/plugins/wigtn-plugins-with-codex/skills/visual-craft/references/automatic-checks.md b/plugins/wigtn-plugins-with-codex/skills/visual-craft/references/automatic-checks.md new file mode 100644 index 0000000..24ef3ff --- /dev/null +++ b/plugins/wigtn-plugins-with-codex/skills/visual-craft/references/automatic-checks.md @@ -0,0 +1,64 @@ +# Automatic craft checks + +Run after creating or substantially editing the requested artifact, before delivery. +Inspect findings, fix confirmed defects, and rerun affected checks. Do not run a +repository-wide scan, install hooks, or generate additional deliverables by default. + +## Source checks + +For HTML or Markdown, run with explicit file paths: + +```sh +python3 /scripts/audit-source.py [] +``` + +Resolve `` to this skill's installed location. The +[source checker](../scripts/audit-source.py) uses only Python's standard library, +reads files without modifying them, and prints JSON with rule, severity and line. +It does not follow external links or execute embedded scripts. + +- `error`: duplicate IDs and missing document language. +- `review`: unresolved same-document fragments, missing source-level labels/alt, + skipped heading levels, hard-broken + headings, repeated em dashes/separators and forced Markdown line breaks. +- Exit 0: no definite source errors (review findings may remain); 1: source errors; + 2: input or unsupported-format errors. Check the JSON, not just the exit code. +- Quotations, code, SVG, math and explicitly hidden HTML are excluded from copy + heuristics. Markdown fences, quote lines, frontmatter and common code forms are + excluded. This lightweight parser does not implement all Markdown dialects. + +Fragment targets may be created at runtime or used by a hash router; verify them +in the running UI before treating them as broken links. +Do not rewrite preserved quotations or brand marks just to clear a finding. +Keep intentional cases with a short reason; there is no required zero-warning score. +JSX/TSX, PDF, DOCX and PPTX are deliberately unsupported by the source checker. +Use the relevant renderer/browser or existing format-specific validators instead. + +## Rendered UI checks + +After opening the authorized preview using the available browser workflow, read +[audit-rendered.js](../scripts/audit-rendered.js) and pass its function expression +to the supported read-only page evaluation API. For Browser's documented runtime: + +```js +const checkSource = await fs.readFile('/absolute/skill-directory/scripts/audit-rendered.js', 'utf8'); +const result = await tab.playwright.evaluate(checkSource); +nodeRepl.write(result); +``` + +Use the environment's approved file reader and browser API; `fs` above is the Node +file module, not a page global. The function only reads DOM geometry and computed +styles. It does not navigate, mutate DOM, fetch resources or launch a browser. +Never use it to bypass a blocked URL or unavailable browser permission. + +Check wide and narrow layouts and relevant opened panels through the normal browser +workflow. The detector examines the current viewport, so scroll to other meaningful +sections before checking them. Nested scrollable tables are allowed; full-page +horizontal overflow is reported. Clipped text, small text and small targets are +review findings because truncation, metadata and inline links may be intentional. + +Read the measured findings alongside the screenshot and actual task. Keyboard +behavior, contrast, long Korean heading wraps, meaningful information density and +summary/list consistency still require appropriate checks. If browser inspection +is blocked, return source findings and explicitly mark rendered checks unavailable. +No findings means only that these detectors found nothing, not visual QA passed. diff --git a/plugins/wigtn-plugins-with-codex/skills/visual-craft/references/documents.md b/plugins/wigtn-plugins-with-codex/skills/visual-craft/references/documents.md new file mode 100644 index 0000000..adc3559 --- /dev/null +++ b/plugins/wigtn-plugins-with-codex/skills/visual-craft/references/documents.md @@ -0,0 +1,48 @@ +# Document craft + +Use for writing or substantially refining documents, reports and slides. Follow +the requested format and existing publication or company template. Plain Markdown +is a valid final artifact; do not add a cover, executive summary or slide deck by default. + +## Narrative and evidence + +- Organize around the reader's question and the material available. For a technical + report, make the question, method, result, limitations and resulting decision easy + to find. These are content needs, not mandatory headings or a fixed file bundle. +- Keep connected reasoning in paragraphs. Use lists for genuinely parallel items, + ordered steps for sequences and tables for comparisons. Avoid turning every + sentence into its own paragraph or every paragraph into a titled section. +- Keep the evidence beside the claim. Preserve sources, units, denominators, + conditions, uncertainty and caveats. Do not shorten away a limitation or turn a + proposal into a measured result for a cleaner page. +- Use direct, natural Korean when requested. Keep terms such as API, UI, frontend + or benchmark when familiar to the audience; explain unfamiliar terms once. + Match the user's register without mechanically repeating polite endings, + grand conclusions, rhetorical contrasts or promotional adjectives. +- In new copy, prefer sentence structure over em-dash interruptions and decorative + separators. Never silently rewrite quotations, code, equations, identifiers, + citations or legal wording to satisfy a visual preference. + +## Layout by format + +- Markdown: use meaningful heading levels and readable paragraphs; avoid manual + line breaks, blank-line padding and HTML styling merely to control appearance. +- Paginated documents: keep a heading with its following content, make table headers + usable across pages, and inspect breaks around figures, captions and source notes. + Remove redundant prose or adjust layout before shrinking body text. +- Slides: give each slide a clear audience-facing message. Split genuinely separate + messages when scope permits; preserve material detail in readable supporting + slides or notes as appropriate. Do not add slides solely to satisfy a word quota. +- Charts and tables: preserve exact values, scales and units. Choose the display + for the comparison, not for decorative variety. Dense appendices and data tables + are legitimate when needed. + +## Verify the delivered artifact + +Review the final text for fidelity and repeated filler. For visual formats, use the +host workflow to inspect rendered pages or slides for clipping, font substitution, +Korean line breaks, stranded headings, unreadable footnotes and table overflow. +For web documents, inspect narrow and wide layouts. For plain Markdown, check +structure, links and reading flow; do not force an export solely for this skill. +Report only checks actually performed. Follow any stricter rendering requirements +of the format-specific workflow without duplicating its checks. diff --git a/plugins/wigtn-plugins-with-codex/skills/visual-craft/references/ui.md b/plugins/wigtn-plugins-with-codex/skills/visual-craft/references/ui.md new file mode 100644 index 0000000..08775d6 --- /dev/null +++ b/plugins/wigtn-plugins-with-codex/skills/visual-craft/references/ui.md @@ -0,0 +1,47 @@ +# UI craft + +Use for building or refining an interface. Existing tokens, components and brand +references are the starting point. A request to improve a screen does not require +new fonts, dependencies, motion libraries or a replacement design system. + +## Composition + +- Establish the primary task and reading order. Keep supporting detail close to + what it explains; defer optional detail only when it remains easy to find. +- A landing page needs a convincing message and relevant proof. A dashboard needs + comparison, state and efficient action. Do not impose marketing-page whitespace + or a hero section on an operational tool. +- Review overloaded sections for competing headings, badges, statistics and CTAs. + Group by purpose instead of reducing every font or moving essential detail into + hidden tabs. Use a table for comparison, a list for scanning and a card when the + content is a distinct object or action. +- Avoid repeating a giant left headline with a tiny detached paragraph on the + right. Put the explanation in the same reading path or align a real second + column. Symmetric and centered layouts remain valid when they fit the content. +- Decoration must not simulate evidence: a real component preview or supplied + screenshot is useful; a made-up dashboard with success figures is not proof. + +## Type, copy and effects + +- Use real strings early, including Korean and long labels. Adjust content width, + wrapping and hierarchy before shrinking type. Do not encode desktop composition + with repeated hard breaks or non-breaking spaces. +- For Korean prose, judge phrase breaks with the actual font. Apply keep-all only + where it improves reading, with a suitable fallback for long URLs or tokens. + Do not apply a blanket wrapping rule to code, tables and every component. +- Prefer labels that name the action or content. Skip repeated eyebrow labels, + decorative counters, dot-separated slogans and automatic arrows on every CTA. + Keep arrows that explain navigation and dots that communicate real state. +- Use motion to explain change or satisfy the brief. Avoid attaching the same + entrance effect to every section; retain reduced-motion behavior. + +## Verify the implemented surface + +- View the actual page at wide and narrow sizes appropriate to the product and + at any intermediate width where the composition changes. Check reading order, + awkward heading wraps, tiny final lines, clipping and accidental horizontal scroll. +- Exercise the interactions touched by the change, including keyboard focus and + relevant loading, empty, error or expanded states. Keep contrast and semantics + intact; a screenshot alone cannot verify these behaviors. +- Fix observable defects and confirm the affected views. If rendering is unavailable, + identify the unverified visual aspects; do not equate a source scan with visual QA. diff --git a/plugins/wigtn-plugins-with-codex/skills/visual-craft/scripts/audit-rendered.js b/plugins/wigtn-plugins-with-codex/skills/visual-craft/scripts/audit-rendered.js new file mode 100644 index 0000000..5b9a559 --- /dev/null +++ b/plugins/wigtn-plugins-with-codex/skills/visual-craft/scripts/audit-rendered.js @@ -0,0 +1,62 @@ +/* Read-only expression for an authorized browser evaluate call. No navigation, + mutation, fetch, dependency or automatic execution. */ +() => { + const findings = []; + let total = 0; + const add = (rule, severity, target, message, measured) => { + total += 1; + if (findings.length < 100) findings.push({ rule, severity, target, message, measured }); + }; + const root = document.documentElement; + const width = root.clientWidth; + if (width > 0 && root.scrollWidth > width + 2) { + add('page-horizontal-overflow', 'error', 'document', + 'The page is wider than its viewport; inspect unintended horizontal scrolling.', + { viewportWidth: width, scrollWidth: root.scrollWidth }); + } + const nodes = [...document.querySelectorAll('body *')]; + let inspected = 0; + for (const [index, element] of nodes.entries()) { + if (element.closest('script,style,template,svg,[hidden],[aria-hidden="true"]')) continue; + const rect = element.getBoundingClientRect(); + const css = getComputedStyle(element); + if (!element.getClientRects().length || rect.width <= 0 || rect.height <= 0 || + css.visibility === 'hidden' || css.visibility === 'collapse' || css.display === 'none') continue; + // Check the current viewport. Scroll then rerun to inspect additional areas. + if (rect.bottom <= 0 || rect.right <= 0 || rect.top >= innerHeight || rect.left >= innerWidth) continue; + inspected += 1; + const target = `${element.tagName.toLowerCase()}${element.id ? '#' + element.id : ''} [body index ${index}]`; + const directText = [...element.childNodes].some(n => n.nodeType === 3 && n.textContent.trim()); + const overflowX = ['hidden', 'clip'].includes(css.overflowX); + const overflowY = ['hidden', 'clip'].includes(css.overflowY); + if (directText && !element.matches('input,textarea,select') && + ((overflowX && element.scrollWidth > element.clientWidth + 2) || + (overflowY && element.scrollHeight > element.clientHeight + 2))) { + add('clipped-text', 'review', target, + 'Text extends beyond a clipped box. Check whether truncation is intentional and the full value is accessible.', + { width: element.clientWidth, scrollWidth: element.scrollWidth, + height: element.clientHeight, scrollHeight: element.scrollHeight }); + } + const size = parseFloat(css.fontSize); + if (directText && Number.isFinite(size) && size < 12) { + add('small-text', 'review', target, 'Text is smaller than 12 CSS px; check its role and readability.', { fontSize: size }); + } + if (element.matches('button,input:not([type="hidden"]),select,textarea,[role="button"],a[href]') && + !element.disabled && css.pointerEvents !== 'none' && + (rect.width < 24 || rect.height < 24)) { + add('small-target', 'review', target, + 'Target has a dimension below 24 CSS px; inspect spacing, label association and inline-link exceptions.', + { width: rect.width, height: rect.height }); + } + } + return { + schema_version: 1, mode: 'rendered-viewport', rendered: true, + status: findings.some(f => f.severity === 'error') ? 'issues' : total ? 'review' : 'no-findings', + viewport: { width: innerWidth, height: innerHeight }, inspected, + checks: ['page-horizontal-overflow', 'clipped-text', 'small-text', 'small-target'], + findings, omitted: Math.max(0, total - findings.length), + limitations: ['Current viewport only; closed panels, iframe contents and shadow roots are not inspected.', + 'No contrast, accessible-name, keyboard or state-consistency certification.', + 'Thresholds are review heuristics, not a WCAG audit or aesthetic score.'] + }; +} diff --git a/plugins/wigtn-plugins-with-codex/skills/visual-craft/scripts/audit-source.py b/plugins/wigtn-plugins-with-codex/skills/visual-craft/scripts/audit-source.py new file mode 100644 index 0000000..f593dbd --- /dev/null +++ b/plugins/wigtn-plugins-with-codex/skills/visual-craft/scripts/audit-source.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +"""Read-only HTML/Markdown craft checks. No rendering, network or dependencies.""" +from __future__ import annotations + +import argparse +from collections import Counter +from html.parser import HTMLParser +import json +from pathlib import Path +import re +import sys +from urllib.parse import unquote, urlsplit + +VOID = {'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', + 'meta', 'param', 'source', 'track', 'wbr'} +PROTECTED = {'script', 'style', 'template', 'noscript', 'pre', 'code', + 'blockquote', 'q', 'math', 'svg'} +CHECKS = ['duplicate-id', 'missing-local-fragment', 'missing-lang', + 'missing-alt', 'heading-level-skip', 'heading-hard-break', + 'unnamed-control', 'repeated-em-dash', 'separator-strip', + 'markdown-hard-breaks'] + + +class Node: + def __init__(self, tag, attrs, line, parent=None): + self.tag, self.attrs, self.line, self.parent = tag, dict(attrs), line, parent + self.children = [] + + def text(self): + return ''.join(c.text() if isinstance(c, Node) else c for c in self.children) + + def excluded(self): + node = self + while node: + if (node.tag in PROTECTED or 'hidden' in node.attrs + or node.attrs.get('aria-hidden', '').lower() == 'true'): + return True + node = node.parent + return False + + def ancestors(self): + node = self.parent + while node: + yield node + node = node.parent + + def descendants(self): + pending = [c for c in self.children if isinstance(c, Node)] + while pending: + child = pending.pop() + yield child + pending.extend(c for c in child.children if isinstance(c, Node)) + + +class Tree(HTMLParser): + def __init__(self, source): + super().__init__(convert_charrefs=True) + self.root = Node('root', {}, 1) + self.stack = [self.root] + self.nodes = [] + self.feed(source) + + def handle_starttag(self, tag, attrs): + node = Node(tag, attrs, self.getpos()[0], self.stack[-1]) + self.stack[-1].children.append(node) + self.nodes.append(node) + if tag not in VOID: + self.stack.append(node) + + def handle_startendtag(self, tag, attrs): + self.handle_starttag(tag, attrs) + if tag not in VOID: + self.handle_endtag(tag) + + def handle_endtag(self, tag): + for i in range(len(self.stack) - 1, 0, -1): + if self.stack[i].tag == tag: + del self.stack[i:] + break + + def handle_data(self, data): + self.stack[-1].children.append(data) + + +def finding(rule, severity, line, message): + return dict(rule=rule, severity=severity, line=line, message=message) + + +def copy_findings(text, line): + results = [] + if text.count('—') >= 2: + results.append(finding('repeated-em-dash', 'review', line, + 'Repeated em dashes in one text block; review intent, do not rewrite quotations.')) + if text.count('·') >= 3 or text.count('•') >= 3: + results.append(finding('separator-strip', 'review', line, + 'Repeated separators in one text block; check whether a list or plain sentence reads better.')) + return results + + +def html_findings(source): + tree = Tree(source) + nodes = tree.nodes + results = [] + ids = {} + for n in nodes: + if any(a.tag == 'template' for a in n.ancestors()): + continue + identifier = n.attrs.get('id') + if identifier: + if identifier in ids: + results.append(finding('duplicate-id', 'error', n.line, + 'Duplicate HTML id; anchors and labels may resolve to the wrong element.')) + ids[identifier] = n + has_base = any(n.tag == 'base' and n.attrs.get('href') for n in nodes) + for n in nodes: + if n.tag == 'html' and not n.attrs.get('lang', '').strip(): + results.append(finding('missing-lang', 'error', n.line, + 'Full HTML document has no language declaration.')) + if n.tag == 'a' and not has_base and not any(a.tag == 'template' for a in n.ancestors()): + href = n.attrs.get('href', '') + if href.startswith('#'): + fragment = unquote(urlsplit(href).fragment) + # Empty #, the reserved top fragment and text directives are valid. + if fragment and fragment.lower() != 'top' and ':~:text=' not in fragment and fragment not in ids: + results.append(finding('missing-local-fragment', 'review', n.line, + 'No matching id in source; check runtime-generated targets or hash routing.')) + if n.excluded(): + continue + if n.tag == 'img' and 'alt' not in n.attrs and n.attrs.get('role') not in {'presentation', 'none'}: + results.append(finding('missing-alt', 'review', n.line, + 'Image has no alt attribute; describe it or mark it decorative.')) + if n.tag == 'br' and any(re.fullmatch('h[1-6]', a.tag) for a in n.ancestors()): + results.append(finding('heading-hard-break', 'review', n.line, + 'Heading contains a hard break; verify the intended wrap at narrow widths.')) + if n.tag in {'button', 'input', 'select', 'textarea'}: + kind = n.attrs.get('type', '').lower() + if kind == 'hidden': + continue + name = n.attrs.get('aria-label', '').strip() + labelled = n.attrs.get('aria-labelledby', '').split() + name = name or ' '.join(ids[x].text().strip() for x in labelled if x in ids) + if n.tag == 'button': + name = name or n.text().strip() + # Icon accessible names need a browser check, not a source-level verdict. + name = name or any(c.tag in {'svg', 'img'} and (c.attrs.get('alt') or c.attrs.get('aria-label') or c.text().strip()) + for c in n.descendants()) + if n.tag == 'input' and kind in {'submit', 'reset'}: + name = True # Native default label exists even without value. + if n.tag == 'input' and kind == 'button': + name = name or n.attrs.get('value') + if n.tag == 'input' and kind == 'image': + name = name or n.attrs.get('alt') + if n.tag != 'button': + name = name or any(a.tag == 'label' and a.text().strip() for a in n.ancestors()) + name = name or any(a.tag == 'label' and a.attrs.get('for') == n.attrs.get('id') + and a.attrs.get('for') and a.text().strip() for a in nodes) + if not name: + results.append(finding('unnamed-control', 'review', n.line, + 'No source-level control label found; check the rendered accessible name.')) + # Leaf blocks avoid counting nested list/paragraph text several times. + if n.tag in {'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'figcaption'}: + protected_child = any(c.excluded() for c in n.descendants()) + nested_block = any(c.tag in {'p', 'li'} for c in n.descendants()) + if not protected_child and not nested_block: + results.extend(copy_findings(n.text(), n.line)) + previous = 0 + for n in nodes: + if re.fullmatch('h[1-6]', n.tag) and not n.excluded(): + level = int(n.tag[1]) + if previous and level > previous + 1: + results.append(finding('heading-level-skip', 'review', n.line, + f'Heading level jumps from h{previous} to h{level}.')) + previous = level + return results + + +def markdown_findings(source): + results, paragraph = [], [] + fence, frontmatter, raw_protected, previous = None, False, None, 0 + + def flush(): + if paragraph: + results.extend(copy_findings(' '.join(x[1] for x in paragraph), paragraph[0][0])) + count = sum(bool(re.search(r'( {2,}|\\)$', text)) for _, text in paragraph) + if count >= 3: + results.append(finding('markdown-hard-breaks', 'review', paragraph[0][0], + 'Three or more forced line breaks in one paragraph; review reading flow.')) + paragraph.clear() + + for number, line in enumerate(source.splitlines(), 1): + if number == 1 and line.strip() == '---': + frontmatter = True + continue + if frontmatter: + if line.strip() in {'---', '...'}: + frontmatter = False + continue + marker = re.match(r'^ {0,3}(`{3,}|~{3,})(.*)$', line) + if fence: + if marker and marker[1][0] == fence[0] and len(marker[1]) >= fence[1] and not marker[2].strip(): + fence = None + continue + if marker: + flush() + fence = (marker[1][0], len(marker[1])) + continue + # Raw HTML quotations/code and indented Markdown code are protected too. + if raw_protected: + if re.search(rf'', line, re.I): + raw_protected = None + continue + raw = re.search(r'<(blockquote|pre|script|style)\b', line, re.I) + if raw: + flush() + if not re.search(rf'', line, re.I): + raw_protected = raw[1] + continue + if not line.strip() or re.match(r'^\s*>|^( {4}|\t)', line): + flush() + continue + cleaned = re.sub(r'(`+).*?\1', '', line) + heading = re.match(r'^ {0,3}(#{1,6})\s+(.+)', cleaned) + if heading: + flush() + level = len(heading[1]) + if previous and level > previous + 1: + results.append(finding('heading-level-skip', 'review', number, + f'Heading level jumps from {previous} to {level}.')) + previous = level + results.extend(copy_findings(heading[2], number)) + if re.search(r'', heading[2], re.I): + results.append(finding('heading-hard-break', 'review', number, + 'Heading contains an HTML hard break.')) + elif '|' not in cleaned and not re.match(r'^\s*(?:[-+*]|\d+[.)])\s', cleaned): + paragraph.append((number, cleaned)) + else: + flush() + flush() + return results + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('files', nargs='+', type=Path, help='Explicit HTML or Markdown files; no recursive scan') + args = parser.parse_args() + reports = [] + input_errors = [] + for path in args.files: + suffix = path.suffix.lower() + if suffix not in {'.html', '.htm', '.md', '.markdown'}: + input_errors.append({'file': str(path), 'error': 'Unsupported format; use rendered inspection for JSX, PDF, DOCX or PPTX.'}) + continue + try: + source = path.read_text(encoding='utf-8-sig') + findings = html_findings(source) if suffix in {'.html', '.htm'} else markdown_findings(source) + except (OSError, UnicodeError, ValueError) as exc: + input_errors.append({'file': str(path), 'error': str(exc)}) + continue + counts = dict(Counter(f['severity'] for f in findings)) + reports.append({'file': str(path), 'counts': counts, 'findings': findings[:100], + 'omitted': max(0, len(findings)-100)}) + has_errors = any(r['counts'].get('error') for r in reports) + print(json.dumps({'schema_version': 1, 'mode': 'source', 'rendered': False, + 'status': 'input-error' if input_errors else 'issues' if has_errors else 'review' if any(r['findings'] for r in reports) else 'no-findings', + 'checks': CHECKS, 'files': reports, 'input_errors': input_errors, + 'limitations': ['Source heuristics only; not browser visibility or full Markdown parsing.', + 'No layout, contrast, interaction or overall accessibility certification.', + 'Review findings are contextual, not defects or quality scores.']}, + ensure_ascii=False, indent=2)) + return 2 if input_errors else 1 if has_errors else 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/build-ablation-marketplace.py b/scripts/build-ablation-marketplace.py index 4a2706e..77ee647 100755 --- a/scripts/build-ablation-marketplace.py +++ b/scripts/build-ablation-marketplace.py @@ -20,6 +20,8 @@ "verified-delivery", "release-readiness", ) +HISTORICAL_FULL9 = frozenset((*CORE, "screen-spec", "design-direction", + "handdrawn-diagram", "wigtn-presentation", "work-planner")) PLACEBO_NAMES = ( "control-catalog", "control-index", @@ -114,12 +116,12 @@ def build(destination: Path, variant: str) -> None: shutil.rmtree(skills) skills.mkdir() create_placebo(skills) - elif variant == "full8": - shutil.rmtree(skills / "work-planner") - # Historical evaluation shape: the current core nine minus work-planner. - elif variant == "full9": - # Current core-nine catalog without the separately installed Wiki plugin. - pass + elif variant in {"full8", "full9"}: + # Preserve the historical catalog even as new production skills are added. + keep = HISTORICAL_FULL9 - ({"work-planner"} if variant == "full8" else set()) + for path in skills.iterdir(): + if path.is_dir() and path.name not in keep: + shutil.rmtree(path) else: raise ValueError(f"unsupported variant: {variant}") diff --git a/scripts/check-ablation-builder.py b/scripts/check-ablation-builder.py index 043789b..3c01a5c 100644 --- a/scripts/check-ablation-builder.py +++ b/scripts/check-ablation-builder.py @@ -44,6 +44,15 @@ def main() -> int: ) records[variant] = record + historical = { + "product-spec", "acceptance-verifier", "verified-delivery", + "release-readiness", "screen-spec", "design-direction", + "handdrawn-diagram", "wigtn-presentation", "work-planner", + } + for variant, expected in (("full9", historical), ("full8", historical - {"work-planner"})): + if set(records[variant]["skills"]) != expected: + raise AssertionError(f"{variant}: historical inventory changed") + core_chars = records["core4"]["description_characters"] placebo_chars = records["placebo4"]["description_characters"] if core_chars != placebo_chars: diff --git a/scripts/check-triggers.py b/scripts/check-triggers.py index ac06344..9a74f13 100755 --- a/scripts/check-triggers.py +++ b/scripts/check-triggers.py @@ -17,6 +17,12 @@ ), "acceptance-verifier": ("요구사항을 만족", "요구사항 반영", "acceptance criteria", "fr별"), "design-direction": ("ui 디자인 방향", "visual direction"), + "visual-craft": ( + "프론트 ui 만들어", "랜딩 페이지 만들어", "문서 만들어", + "테크리포트 작성", "보고서 작성", "문서 레이아웃", + "ui의 ai 티", "일반 회사 발표자료 만들어", "create a frontend ui", + "draft a report", "create a document", "refine this document layout", + ), "handdrawn-diagram": ( "손그림", "스케치 아키텍처", diff --git a/scripts/check-visual-craft-audit.py b/scripts/check-visual-craft-audit.py new file mode 100644 index 0000000..0181666 --- /dev/null +++ b/scripts/check-visual-craft-audit.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""Detector regression tests. DOM fixtures are not real-browser visual QA.""" +import hashlib +import importlib.util +import json +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SKILL = ROOT/'plugins/wigtn-plugins-with-codex/skills/visual-craft' +if not SKILL.exists(): + SKILL = Path(__file__).parent # Staging execution before installation. +SOURCE = SKILL/'scripts/audit-source.py' +spec = importlib.util.spec_from_file_location('craft_audit', SOURCE) +audit = importlib.util.module_from_spec(spec) +spec.loader.exec_module(audit) + + +def rules(findings): + return {f['rule'] for f in findings} + + +class SourceTests(unittest.TestCase): + def test_html_defects_and_source_locations(self): + findings = audit.html_findings('\n

Title

\n

Body

\nGo') + self.assertEqual(rules(findings), {'missing-lang', 'duplicate-id', 'missing-local-fragment'}) + self.assertEqual(next(f['line'] for f in findings if f['rule'] == 'duplicate-id'), 3) + self.assertEqual(next(f['severity'] for f in findings if f['rule'] == 'missing-local-fragment'), 'review') + + def test_fragment_decoding_and_reserved_targets(self): + findings = audit.html_findings('

제목

이동상단toptext다른 문서') + self.assertNotIn('missing-local-fragment', rules(findings)) + + def test_base_url_does_not_make_external_fragment_a_local_error(self): + self.assertEqual(audit.html_findings('Go'), []) + + def test_preserved_copy_is_not_flagged(self): + source = '

WIGTN.

원문 — 인용 — 그대로 · 유지 · 필요 · 함

원문 — 인용 — 유지

코드 — 코드 — 코드
브랜드 — 원본 — 보존' + self.assertEqual(audit.html_findings(source), []) + + def test_review_patterns_are_not_hard_errors(self): + findings = audit.html_findings('

좋은
제목

건너뛴 제목

문장 — 문장 — 문장

가 · 나 · 다 · 라

') + self.assertEqual(rules(findings), {'heading-hard-break', 'heading-level-skip', 'repeated-em-dash', 'separator-strip'}) + self.assertTrue(all(f['severity'] == 'review' for f in findings)) + + def test_native_and_associated_control_names(self): + source = '설명' + self.assertEqual(audit.html_findings(source), []) + + def test_unlabelled_controls_and_images(self): + findings = audit.html_findings('') + self.assertEqual(rules(findings), {'unnamed-control', 'missing-alt'}) + self.assertEqual(len(findings), 3) + + def test_submit_button_has_no_native_default_name(self): + findings = audit.html_findings('') + self.assertEqual(len(findings), 1) + self.assertEqual(findings[0]['rule'], 'unnamed-control') + + def test_inert_template_does_not_collide_with_live_ids(self): + self.assertEqual(audit.html_findings('
'), []) + + def test_markdown_protected_content(self): + source = '---\ntitle: a — b — c\n---\n# 제목\n> 인용 — 그대로 — 유지\n\n```text\na — b — c\n```\n\n code — code — code\n\n`a — b — c`\n\n
\na — b — c\n
\n' + self.assertEqual(audit.markdown_findings(source), []) + + def test_fence_only_closes_with_matching_marker(self): + source = '````\n```\na — b — c\n````\n\n일반 — 문장 — 검토\n' + findings = audit.markdown_findings(source) + self.assertEqual(len(findings), 1) + self.assertEqual(findings[0]['line'], 6) + + def test_markdown_heading_break_and_paragraph_breaks(self): + source = '# 시작\n\n### 제목
줄\n\n하나 \n둘 \n셋 \n넷\n' + self.assertEqual(rules(audit.markdown_findings(source)), {'heading-level-skip', 'heading-hard-break', 'markdown-hard-breaks'}) + + def test_dense_tables_and_ordered_lists_are_not_decoration(self): + self.assertEqual(audit.markdown_findings('# 결과\n\n| 열 | 값 |\n|---|---|\n| x | 20 |\n\n1. 설치\n2. 실행\n'), []) + + def test_cli_does_not_mutate_or_execute_input(self): + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp)/'test.html' + marker = Path(tmp)/'marker' + path.write_text(f'

제목

문장 — 문장 — 문장

') + before = hashlib.sha256(path.read_bytes()).hexdigest() + result = subprocess.run([sys.executable, str(SOURCE), str(path)], capture_output=True, text=True) + self.assertEqual(result.returncode, 0) + self.assertEqual(json.loads(result.stdout)['status'], 'review') + self.assertFalse(marker.exists()) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), before) + + def test_cli_error_codes_and_unsupported_input(self): + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp)/'test.html' + path.write_text('

a

b

') + result = subprocess.run([sys.executable, str(SOURCE), str(path)], capture_output=True, text=True) + self.assertEqual(result.returncode, 1) + result = subprocess.run([sys.executable, str(SOURCE), str(Path(tmp)/'missing.md'), 'component.tsx'], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertEqual(len(json.loads(result.stdout)['input_errors']), 2) + + def test_report_is_bounded_without_hiding_total(self): + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp)/'test.html' + path.write_text(''*120) + result = subprocess.run([sys.executable, str(SOURCE), str(path)], capture_output=True, text=True) + report = json.loads(result.stdout)['files'][0] + self.assertEqual(len(report['findings']), 100) + self.assertEqual(report['counts']['review'], 120) + self.assertEqual(report['omitted'], 20) + + +class GeometryFixtureTests(unittest.TestCase): + @unittest.skipUnless(shutil.which('node'), 'Node unavailable; DOM geometry fixture not run') + def test_readonly_detector_geometry(self): + runner = r''' +const fs = require('node:fs'); const vm = require('node:vm'); const assert = require('node:assert/strict'); +const source = fs.readFileSync(process.argv[1], 'utf8'); +function node(p = {}) { + const style = Object.assign({visibility:'visible',display:'block',overflowX:'visible',overflowY:'visible',fontSize:'16px',pointerEvents:'auto'}, p.style); + const rect = Object.assign({width:100,height:44,top:0,left:0,bottom:44,right:100}, p.rect); + return {tagName:'P',id:'',disabled:false,style,childNodes:[{nodeType:3,textContent:'Text'}], + clientWidth:100,scrollWidth:100,clientHeight:44,scrollHeight:44, + closest:()=>null,getClientRects:()=>[rect],getBoundingClientRect:()=>rect, + matches:(selector)=>selector.startsWith('button') && !!p.interactive, + ...p, style, getBoundingClientRect:()=>rect}; +} +function run(nodes, scrollWidth=390) { + const context = {document:{documentElement:{clientWidth:390,scrollWidth},querySelectorAll:()=>nodes}, + getComputedStyle:n=>n.style,innerWidth:390,innerHeight:844}; + return vm.runInNewContext('('+source+')()', context); +} +assert.equal(run([]).status,'no-findings'); +assert.equal(run([],500).findings[0].rule,'page-horizontal-overflow'); +assert.equal(run([node({style:{overflowX:'auto'},scrollWidth:730})]).findings.length,0); +assert.equal(run([node({style:{overflowX:'hidden'},scrollWidth:300})]).findings[0].rule,'clipped-text'); +assert.equal(run([node({style:{fontSize:'11px'}})]).findings[0].severity,'review'); +assert.equal(run([node({interactive:true,rect:{height:20}})]).findings[0].rule,'small-target'); +assert.equal(run([node({closest:()=>({})})]).inspected,0); +assert.equal(run([node({rect:{top:900,bottom:944},style:{fontSize:'8px'}})]).inspected,0); +assert.equal(run([node({style:{display:'none'}})]).inspected,0); +const bounded=run(Array.from({length:120},()=>node({style:{fontSize:'8px'}}))); +assert.equal(bounded.findings.length,100);assert.equal(bounded.omitted,20); +console.log('10 synthetic DOM geometry assertions passed; no real browser was run.'); +''' + result = subprocess.run(['node', '-e', runner, str(SKILL/'scripts/audit-rendered.js')], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + print(result.stdout.strip()) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/scripts/run-static-contracts.sh b/scripts/run-static-contracts.sh index e748c20..0a06b27 100755 --- a/scripts/run-static-contracts.sh +++ b/scripts/run-static-contracts.sh @@ -6,6 +6,7 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" env PYTHONDONTWRITEBYTECODE=1 python3 "$repo_root/scripts/check-triggers.py" env PYTHONDONTWRITEBYTECODE=1 python3 "$repo_root/scripts/check-plugin-resources.py" env PYTHONDONTWRITEBYTECODE=1 python3 "$repo_root/scripts/check-visual-contracts.py" +env PYTHONDONTWRITEBYTECODE=1 python3 "$repo_root/scripts/check-visual-craft-audit.py" env PYTHONDONTWRITEBYTECODE=1 python3 "$repo_root/scripts/check-evidence-contract.py" env PYTHONDONTWRITEBYTECODE=1 python3 "$repo_root/scripts/check-evidence-inspector.py" env PYTHONDONTWRITEBYTECODE=1 python3 "$repo_root/scripts/check-project-context.py" diff --git a/scripts/validate.sh b/scripts/validate.sh index f73ec86..40ad012 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -19,7 +19,7 @@ if test -f "$plugin_validator" && test -f "$skill_validator"; then python3 "$skill_validator" "$skill" skill_count=$((skill_count + 1)) done - test "$skill_count" -eq 9 || { echo "Expected 9 core skills, found $skill_count"; exit 1; } + test "$skill_count" -eq 10 || { echo "Expected 10 core skills, found $skill_count"; exit 1; } python3 "$skill_validator" "$wiki_plugin/skills/knowledge-wiki" else echo "Codex system validators unavailable; repository contract validation used." diff --git a/tests/trigger-cases.tsv b/tests/trigger-cases.tsv index 976dca5..e469ec8 100644 --- a/tests/trigger-cases.tsv +++ b/tests/trigger-cases.tsv @@ -38,7 +38,7 @@ none 이 에러 고쳐줘 none README 오타 수정해줘 none 이 기능 구현해줘 none 이 기능 작업 완료해줘 -none 일반 회사 발표자료 만들어줘 +visual-craft 일반 회사 발표자료 만들어줘 none Devpost 제출용 깔끔한 시스템 다이어그램 만들어줘 none WIGTN 로고를 웹사이트 푸터에 넣어줘 none 코드 리팩터링해줘 @@ -48,3 +48,19 @@ none 오늘 할 일 체크리스트를 간단히 만들어줘 none 이 기능 구현 계획을 대화로만 정리해줘 none 이 기능을 구현하고 검증까지 해줘 none 세션 내용을 짧게 요약해줘 +visual-craft 프론트 UI 만들어줘 +visual-craft 우리 제품 랜딩 페이지 만들어줘 +visual-craft 이 내용으로 문서 만들어줘 +visual-craft 이번 실험 테크리포트 작성해줘 +visual-craft 첨부 자료를 바탕으로 보고서 작성해줘 +visual-craft 문서 레이아웃 다듬어줘 +visual-craft 이 UI의 AI 티 좀 줄여줘 +visual-craft Create a frontend UI for this product +visual-craft Draft a report from these experiment results +visual-craft Create a document explaining our onboarding process +visual-craft Refine this document layout without changing the facts +none 문서 다운로드 API의 인증 오류 고쳐줘 +none 이 문서에서 오타 하나만 수정해줘 +none UI 테스트의 mock 응답만 수정해줘 +none 이 보고서의 합계만 계산해줘 +none React의 상태 관리가 뭔지 설명해줘