From e96b41c55b5e3a2d5e5f9afd6abbf2a7ce2c380a Mon Sep 17 00:00:00 2001 From: Yuan <20144414+baskduf@users.noreply.github.com> Date: Sat, 20 Jun 2026 02:07:23 +0900 Subject: [PATCH] refactor: focus Artic on init start workflow --- .agents/plugins/marketplace.json | 2 +- .claude-plugin/marketplace.json | 2 +- CHANGELOG.md | 9 + HANDOFF.md | 68 -- README.ja.md | 23 +- README.ko.md | 23 +- README.md | 23 +- README.zh-CN.md | 23 +- README.zh-TW.md | 23 +- examples/prompts.md | 8 - .../claude-artic/.claude-plugin/plugin.json | 2 +- plugins/claude-artic/skills/artic/SKILL.md | 81 +- .../skills/artic/scripts/artic_init.py | 32 +- .../artic/scripts/artic_init_session.py | 163 +-- .../skills/artic/scripts/artic_show.py | 833 ------------- .../skills/artic/scripts/artic_start.py | 134 +-- .../skills/artic/scripts/risk_readiness.py | 335 ------ .../artic/scripts/scaffold_artic_files.py | 4 +- .../artic/scripts/validate_artic_outputs.py | 52 - .../skills/artic/templates/brief.schema.json | 21 - .../artic/templates/strategy.schema.json | 21 - plugins/codex-artic/.codex-plugin/plugin.json | 2 +- plugins/codex-artic/skills/artic/SKILL.md | 81 +- .../skills/artic/scripts/artic_init.py | 32 +- .../artic/scripts/artic_init_session.py | 163 +-- .../skills/artic/scripts/artic_show.py | 833 ------------- .../skills/artic/scripts/artic_start.py | 134 +-- .../skills/artic/scripts/risk_readiness.py | 335 ------ .../artic/scripts/scaffold_artic_files.py | 4 +- .../artic/scripts/validate_artic_outputs.py | 52 - .../skills/artic/templates/brief.schema.json | 21 - .../artic/templates/strategy.schema.json | 21 - pyproject.toml | 2 +- skills/artic/SKILL.md | 81 +- skills/artic/scripts/artic_init.py | 32 +- skills/artic/scripts/artic_init_session.py | 163 +-- skills/artic/scripts/artic_show.py | 833 ------------- skills/artic/scripts/artic_start.py | 134 +-- skills/artic/scripts/risk_readiness.py | 335 ------ skills/artic/scripts/scaffold_artic_files.py | 4 +- .../artic/scripts/validate_artic_outputs.py | 52 - skills/artic/templates/brief.schema.json | 21 - skills/artic/templates/strategy.schema.json | 21 - tests/test_artic_package.py | 1045 +---------------- 44 files changed, 161 insertions(+), 6122 deletions(-) delete mode 100644 HANDOFF.md delete mode 100644 plugins/claude-artic/skills/artic/scripts/artic_show.py delete mode 100644 plugins/claude-artic/skills/artic/scripts/risk_readiness.py delete mode 100644 plugins/codex-artic/skills/artic/scripts/artic_show.py delete mode 100644 plugins/codex-artic/skills/artic/scripts/risk_readiness.py delete mode 100644 skills/artic/scripts/artic_show.py delete mode 100644 skills/artic/scripts/risk_readiness.py diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 7eda4f6..16380bf 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -4,7 +4,7 @@ "displayName": "Artic" }, "description": "Reference-driven AI-native design docs for homepage generation.", - "version": "0.4.1", + "version": "0.5.0", "author": "baskduf", "plugins": [ { diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 35a71fa..5d22bfe 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -2,7 +2,7 @@ "name": "artic", "display_name": "Artic", "description": "Reference-driven AI-native design docs for homepage generation.", - "version": "0.4.1", + "version": "0.5.0", "author": "baskduf", "plugins": [ { diff --git a/CHANGELOG.md b/CHANGELOG.md index 27dd69e..59b590d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.5.0 - 2026-06-20 + +### Removed +- Removed the public `@artic show` and `@artic review` workflow surface to keep Artic focused on `@artic init` → `@artic start`. +- Removed show preview bundle generation, risk-readiness gating, production-readiness status fields, and related defensive output sections from the skill, scripts, schemas, validators, docs, and mirrored Claude/Codex packages. + +### Changed +- Reframed reference safety as constructive design-principle extraction for original project-specific direction. + ## 0.4.1 - 2026-06-20 ### Fixed diff --git a/HANDOFF.md b/HANDOFF.md deleted file mode 100644 index 33df2f8..0000000 --- a/HANDOFF.md +++ /dev/null @@ -1,68 +0,0 @@ -# Artic Work Handoff / Scope Lock - -Date: 2026-06-19 -Owner: Hermes / current agent -Repo: `/Users/wb/Desktop/artic` -Branch observed: `release/v0.2.0` - -## Active ownership - -This agent owns the proposed `artic show` work. Other agents should not edit the files listed in the reserved scope below unless WB explicitly reassigns or clears this handoff. - -## Proposed feature - -Add a safe `show` command that renders an Artic-generated design into a preview page using the existing outputs from `artic start`. - -Intended default behavior: -- Read `DESIGN.md`, `docs/homepage-design-prompt.md`, `.artic/brief.json`, and `.artic/references.json` from a target project root. -- Generate a non-invasive static preview, likely `.artic/show/index.html`. -- Do not modify the user's actual app/source files by default. -- If project code application is added later, it must require explicit opt-in such as `--apply` or a separate `artic apply` command. - -## Reserved edit scope - -Canonical Artic skill: -- `skills/artic/SKILL.md` -- `skills/artic/scripts/artic_show.py` if created -- `skills/artic/templates/` only for show/preview-specific templates if needed -- `skills/artic/scripts/artic_start.py` only if minimal integration/shared helpers are truly needed -- `skills/artic/scripts/validate_artic_outputs.py` only if show output validation is added - -Plugin sync copies: -- `plugins/claude-artic/skills/artic/SKILL.md` -- `plugins/claude-artic/skills/artic/scripts/artic_show.py` if created -- `plugins/claude-artic/skills/artic/templates/` only matching canonical show/preview changes -- `plugins/codex-artic/skills/artic/SKILL.md` -- `plugins/codex-artic/skills/artic/scripts/artic_show.py` if created -- `plugins/codex-artic/skills/artic/templates/` only matching canonical show/preview changes - -Tests/docs: -- `tests/test_artic_package.py` or a new targeted test file for `artic_show` -- `README.md`, `README.ko.md`, `README.ja.md`, `README.zh-CN.md`, `README.zh-TW.md` only if command docs are added - -## Out of scope / do not touch - -Other agents should avoid these while this handoff is active: -- Catalog expansion work in `skills/artic/references/source-catalog.json` -- Existing init/start lifecycle behavior unless required by show tests -- Locale/multilingual workflow changes unrelated to show -- Release artifacts under `dist/` -- Unrelated package/version/release changes -- Any user's downstream website source files such as `app/page.tsx`, `src/App.tsx`, `pages/index.tsx`, etc. - -## Expected verification when implemented - -Run at minimum: -- `python3 -m pytest -q` - -Feature-specific tests should verify: -- `show` fails cleanly when required design inputs are missing. -- `show` creates a preview under `.artic/show/` when design inputs exist. -- default `show` does not modify app/source files. -- canonical skill changes are synced into Claude and Codex plugin copies. - -## Release condition - -This handoff can be cleared when either: -1. WB explicitly cancels/reassigns the `artic show` work, or -2. the implementation is complete, verified, and summarized back to WB. diff --git a/README.ja.md b/README.ja.md index 103e5e7..f9f853b 100644 --- a/README.ja.md +++ b/README.ja.md @@ -21,11 +21,9 @@ Artic は、実装前にホームページのデザイン方向を契約とし ```text @artic init # 意図と参照を収集 @artic start # strategy artifacts を作成し DESIGN.md 文書をコンパイル -@artic show # provenance を記録した asset-first preview bundle をレンダリング -@artic review # 実装を文書に照らして確認 ``` -エージェントは内部で、ユーザーへのヒアリング、brief の検索 facet 化、専門/OSS デザイン参照の検索、`.artic/strategy.json` 作成、`DESIGN.md` と補助文書のコンパイル、出力検証を処理します。スクリプトは validator/compiler/renderer helper であり、design judgment や strategy authorship を置き換えません。 +エージェントは内部で、ユーザーへのヒアリング、brief の検索 facet 化、専門/OSS デザイン参照の検索、`.artic/strategy.json` 作成、`DESIGN.md` と補助文書のコンパイル、出力検証を処理します。スクリプトは validator/compiler helper であり、design judgment や strategy authorship を置き換えません。 > Artic は参照サイトを **クローンしません**。専門/OSS デザインシステムから再利用可能な原則を抽出し、project-specific な strategy と AI-native docs に実装を結び付けます。 @@ -45,7 +43,6 @@ marketplace パッケージをインストールします: ```text /claude-artic:artic init /claude-artic:artic start -/claude-artic:artic show ``` 自然文でも依頼できます: @@ -65,7 +62,7 @@ codex plugin marketplace add baskduf/artic 安定したインストールが必要な場合はリリースタグに固定します: ```bash -codex plugin marketplace add baskduf/artic@v0.4.1 +codex plugin marketplace add baskduf/artic@v0.5.0 ``` プラグインブラウザから `codex-artic` をインストールします: @@ -85,7 +82,6 @@ Codex に依頼します: ```text @artic init @artic start -@artic show ``` ### Local checkout fallback @@ -104,12 +100,11 @@ python3 skills/artic/scripts/synthesize_reference_notes.py --query "ai product d python3 skills/artic/scripts/synthesize_reference_notes.py --query "ai product developer saas" --limit 3 --live-fetch --cache-dir /tmp/artic-cache --fixtures-dir /tmp/no-fixtures --output /tmp/artic-smoke/docs/live-reference-synthesis.md python3 skills/artic/scripts/scaffold_artic_files.py --root /tmp/artic-smoke python3 skills/artic/scripts/validate_artic_outputs.py --root /tmp/artic-smoke -python3 skills/artic/scripts/artic_show.py --root . python3 skills/artic/scripts/artic_version.py --root . python3 skills/artic/scripts/artic_update.py --root . ``` -Helper scripts は deterministic helper です。`validate_artic_outputs.py` は contract を検証し、`artic_start.py` は agent-authored strategy を文書へコンパイルし、`artic_show.py` は preview をレンダリングします。design judgment の出所はスクリプトではなく、public `@artic start` agent workflow が作成する `.artic/strategy.json` です。 +Helper scripts は deterministic helper です。`validate_artic_outputs.py` は contract を検証し、`artic_start.py` は agent-authored strategy を文書へコンパイルします。design judgment の出所はスクリプトではなく、public `@artic start` agent workflow が作成する `.artic/strategy.json` です。 ## What Changes In The Agent @@ -121,8 +116,6 @@ Artic が呼び出されると、エージェントは次を行います: 4. 色の役割、タイプ階層、余白リズム、コンポーネント、モーション、アクセシビリティなど、再利用可能なルールを抽出します。 5. ユーザーのプロジェクト目標に基づいて、レファレンス間の衝突を解決します。 6. `@artic start` で public agent workflow が `.artic/strategy.json` を作成し、`docs/artic-strategy.md` を保存してから compiler を実行し、`DESIGN.md` と補助文書を生成します。 -7. `@artic show` で strategy artifacts に基づく asset-first visual draft bundle を `.artic/show/` にレンダリングし、アプリのソースファイルは変更しません。 -8. `@artic review` で実装を `.artic/strategy.json`、`docs/artic-strategy.md`、`DESIGN.md` と比較し、生成されたデザイン文書を検証します。 ## When To Use It @@ -146,8 +139,6 @@ Artic が呼び出されると、エージェントは次を行います: | デザインヒアリング開始 | `@artic init` | | 高速ヒアリング実行 | `@artic init quick` | | strategy 作成と文書コンパイル | `@artic start` | -| strategy artifacts から asset-first preview bundle をレンダリング | `@artic show` | -| strategy + `DESIGN.md` に対して実装をレビュー | `@artic review the homepage against DESIGN.md` | | インストール済み/最新バージョン確認 | `@artic version` | | 安全な更新コマンド表示 | `@artic update` | @@ -172,14 +163,6 @@ DESIGN.md docs/design-rules.md docs/design-qa-checklist.md docs/homepage-design-prompt.md -.artic/show/index.html -.artic/show/styles.css -.artic/show/tokens.json -.artic/show/assets/manifest.json # asset provenance; 未検証 asset は preview-only で production-cleared ではありません -.artic/show/report.json -.artic/show/critique.md -.artic/show/selected.json -.artic/show/iterations//... # @artic show が生成。アプリファイルは変更しません ``` ## Repository Layout diff --git a/README.ko.md b/README.ko.md index 75d4836..df2e523 100644 --- a/README.ko.md +++ b/README.ko.md @@ -21,11 +21,9 @@ Artic은 구현 전에 홈페이지 디자인 방향을 계약으로 고정하 ```text @artic init # 의도와 레퍼런스 수집 @artic start # strategy artifacts를 작성한 뒤 DESIGN.md 문서 컴파일 -@artic show # 출처가 기록된 asset-first preview bundle 렌더링 -@artic review # 구현 결과를 문서 기준으로 검토 ``` -에이전트는 내부에서 사용자 인터뷰, brief 검색 facet 정규화, 전문/오픈소스 디자인 레퍼런스 검색, `.artic/strategy.json` 작성, `DESIGN.md`와 보조 문서 컴파일, 출력 검증을 처리합니다. 스크립트는 validator/compiler/renderer helper이며, 디자인 판단이나 strategy authorship을 대체하지 않습니다. +에이전트는 내부에서 사용자 인터뷰, brief 검색 facet 정규화, 전문/오픈소스 디자인 레퍼런스 검색, `.artic/strategy.json` 작성, `DESIGN.md`와 보조 문서 컴파일, 출력 검증을 처리합니다. 스크립트는 validator/compiler helper이며, 디자인 판단이나 strategy authorship을 대체하지 않습니다. > Artic은 레퍼런스 사이트를 **복제하지 않습니다**. 전문/OSS 디자인 시스템에서 재사용 가능한 원칙을 추출하고, 프로젝트 맞춤 strategy와 AI-native 문서에 구현을 묶습니다. @@ -45,7 +43,6 @@ marketplace 패키지 설치: ```text /claude-artic:artic init /claude-artic:artic start -/claude-artic:artic show ``` 자연어로도 요청할 수 있습니다: @@ -65,7 +62,7 @@ codex plugin marketplace add baskduf/artic 안정적인 설치가 필요하면 릴리즈 태그로 고정: ```bash -codex plugin marketplace add baskduf/artic@v0.4.1 +codex plugin marketplace add baskduf/artic@v0.5.0 ``` 플러그인 브라우저에서 `codex-artic` 설치: @@ -85,7 +82,6 @@ Codex에 요청: ```text @artic init @artic start -@artic show ``` ### Local checkout fallback @@ -104,12 +100,11 @@ python3 skills/artic/scripts/synthesize_reference_notes.py --query "ai product d python3 skills/artic/scripts/synthesize_reference_notes.py --query "ai product developer saas" --limit 3 --live-fetch --cache-dir /tmp/artic-cache --fixtures-dir /tmp/no-fixtures --output /tmp/artic-smoke/docs/live-reference-synthesis.md python3 skills/artic/scripts/scaffold_artic_files.py --root /tmp/artic-smoke python3 skills/artic/scripts/validate_artic_outputs.py --root /tmp/artic-smoke -python3 skills/artic/scripts/artic_show.py --root . python3 skills/artic/scripts/artic_version.py --root . python3 skills/artic/scripts/artic_update.py --root . ``` -Helper scripts는 deterministic helper입니다. `validate_artic_outputs.py`는 contract를 검증하고, `artic_start.py`는 agent-authored strategy를 문서로 컴파일하며, `artic_show.py`는 preview를 렌더링합니다. 디자인 판단의 출처는 스크립트가 아니라 public `@artic start` agent workflow가 작성한 `.artic/strategy.json`입니다. +Helper scripts는 deterministic helper입니다. `validate_artic_outputs.py`는 contract를 검증하고, `artic_start.py`는 agent-authored strategy를 문서로 컴파일합니다. 디자인 판단의 출처는 스크립트가 아니라 public `@artic start` agent workflow가 작성한 `.artic/strategy.json`입니다. ## What Changes In The Agent @@ -121,8 +116,6 @@ Artic이 호출되면 에이전트는 다음을 수행합니다: 4. 색상 역할, 타이포그래피, spacing, 컴포넌트, 모션, 접근성 규칙을 추출합니다. 5. 사용자 목표를 기준으로 레퍼런스 간 충돌을 해결합니다. 6. `@artic start`에서 public agent workflow가 `.artic/strategy.json`을 작성하고 `docs/artic-strategy.md`를 저장한 뒤 compiler를 실행해 `DESIGN.md`와 보조 문서를 생성합니다. -7. `@artic show`로 strategy artifacts 기반의 asset-first 시각 초안 bundle을 `.artic/show/` 아래 렌더링하며 앱 소스 파일은 바꾸지 않습니다. -8. `@artic review`로 구현을 `.artic/strategy.json`, `docs/artic-strategy.md`, `DESIGN.md`와 비교하고 생성 문서를 검증합니다. ## When To Use It @@ -146,8 +139,6 @@ Artic이 호출되면 에이전트는 다음을 수행합니다: | 디자인 인터뷰 시작 | `@artic init` | | 빠른 인터뷰 실행 | `@artic init quick` | | strategy 작성 및 문서 컴파일 | `@artic start` | -| strategy artifacts 기반 asset-first preview bundle 렌더링 | `@artic show` | -| strategy + `DESIGN.md` 기준 구현 결과 검토 | `@artic review the homepage against DESIGN.md` | | 설치/최신 버전 확인 | `@artic version` | | 안전한 업데이트 명령 출력 | `@artic update` | @@ -174,14 +165,6 @@ DESIGN.md docs/design-rules.md docs/design-qa-checklist.md docs/homepage-design-prompt.md -.artic/show/index.html -.artic/show/styles.css -.artic/show/tokens.json -.artic/show/assets/manifest.json # asset 출처 기록; 미검증 asset은 preview-only이며 production-cleared가 아님 -.artic/show/report.json -.artic/show/critique.md -.artic/show/selected.json -.artic/show/iterations//... # @artic show가 생성; 앱 파일은 변경하지 않음 ``` ## Repository Layout diff --git a/README.md b/README.md index a46e240..f61dd2f 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,9 @@ It keeps the public workflow small: ```text @artic init # collect intent and references @artic start # author strategy artifacts, then compile DESIGN.md docs -@artic show # render a provenance-recorded asset-first preview bundle -@artic review # check implementation against the docs ``` -The agent handles the design-direction work: interview the user, normalize the brief into search facets, search professional/open-source design references, author `.artic/strategy.json`, compile it into `DESIGN.md` and supporting docs, then validate the output. Scripts are validator/compiler/renderer helpers; they do not replace the agent's design judgment or strategy authorship. +The agent handles the design-direction work: interview the user, normalize the brief into search facets, search professional/open-source design references, author `.artic/strategy.json`, compile it into `DESIGN.md` and supporting docs, then validate the output. Scripts are validator/compiler helpers; they do not replace the agent's design judgment or strategy authorship. > Artic does **not** clone reference sites. It extracts reusable principles from professional and OSS design systems, then binds implementation to project-specific strategy and AI-native docs. @@ -45,7 +43,6 @@ Run the bundled skill: ```text /claude-artic:artic init /claude-artic:artic start -/claude-artic:artic show ``` Or ask naturally: @@ -65,7 +62,7 @@ codex plugin marketplace add baskduf/artic Pin a released version when you want a stable install: ```bash -codex plugin marketplace add baskduf/artic@v0.4.1 +codex plugin marketplace add baskduf/artic@v0.5.0 ``` Install `codex-artic` from the plugin browser: @@ -85,7 +82,6 @@ Then ask Codex: ```text @artic init @artic start -@artic show ``` ### Local checkout fallback @@ -104,12 +100,11 @@ python3 skills/artic/scripts/synthesize_reference_notes.py --query "ai product d python3 skills/artic/scripts/synthesize_reference_notes.py --query "ai product developer saas" --limit 3 --live-fetch --cache-dir /tmp/artic-cache --fixtures-dir /tmp/no-fixtures --output /tmp/artic-smoke/docs/live-reference-synthesis.md python3 skills/artic/scripts/scaffold_artic_files.py --root /tmp/artic-smoke python3 skills/artic/scripts/validate_artic_outputs.py --root /tmp/artic-smoke -python3 skills/artic/scripts/artic_show.py --root . python3 skills/artic/scripts/artic_version.py --root . python3 skills/artic/scripts/artic_update.py --root . ``` -Helper scripts are deterministic helpers: `validate_artic_outputs.py` validates contracts, `artic_start.py` compiles agent-authored strategy into docs, and `artic_show.py` renders previews. They are not the source of design judgment; the public `@artic start` agent workflow supplies that judgment in `.artic/strategy.json`. +Helper scripts are deterministic helpers: `validate_artic_outputs.py` validates contracts, and `artic_start.py` compiles agent-authored strategy into docs. They are not the source of design judgment; the public `@artic start` agent workflow supplies that judgment in `.artic/strategy.json`. ## What Changes In The Agent @@ -121,8 +116,6 @@ When invoked, Artic asks the agent to: 4. Extract reusable rules: color roles, type hierarchy, spacing rhythm, components, motion, accessibility. 5. Resolve conflicts between references based on the user's project goal. 6. Run `@artic start` so the public agent workflow authors `.artic/strategy.json`, writes `docs/artic-strategy.md`, then runs the compiler for `DESIGN.md` and supporting docs. -7. Run `@artic show` to render a provenance-recorded asset-first visual draft bundle under `.artic/show/` without changing app source files. -8. Run `@artic review` to compare implementation against `.artic/strategy.json`, `docs/artic-strategy.md`, and `DESIGN.md`, then validate the generated design docs before implementation. ## When To Use It @@ -146,8 +139,6 @@ Skip it for: | Start the design interview | `@artic init` | | Run the fast interview | `@artic init quick` | | Author strategy and compile docs | `@artic start` | -| Render an asset-first preview bundle from strategy artifacts | `@artic show` | -| Review implementation against strategy + `DESIGN.md` | `@artic review the homepage against DESIGN.md` | | Check installed/latest version | `@artic version` | | Print safe update commands | `@artic update` | @@ -174,14 +165,6 @@ DESIGN.md docs/design-rules.md docs/design-qa-checklist.md docs/homepage-design-prompt.md -.artic/show/index.html -.artic/show/styles.css -.artic/show/tokens.json -.artic/show/assets/manifest.json # asset provenance; unverified assets are preview-only, not production-cleared -.artic/show/report.json -.artic/show/critique.md -.artic/show/selected.json -.artic/show/iterations//... # generated by @artic show; app files unchanged ``` ## Repository Layout diff --git a/README.zh-CN.md b/README.zh-CN.md index 286e9f7..ea08b03 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -21,11 +21,9 @@ Artic 是一个兼容 Claude/Codex 的 agent design-direction protocol,也是 ```text @artic init # 收集意图和参考 @artic start # 编写 strategy artifacts,再编译 DESIGN.md 文档 -@artic show # 渲染记录来源的 asset-first preview bundle -@artic review # 按文档检查实现 ``` -Agent 会在内部处理用户访谈、把 brief 归一化为搜索 facets、检索专业/开源设计参考、编写 `.artic/strategy.json`、把它编译成 `DESIGN.md` 和辅助文档,并验证输出。脚本只是 validator/compiler/renderer helper,不是设计判断或 strategy authorship 的来源。 +Agent 会在内部处理用户访谈、把 brief 归一化为搜索 facets、检索专业/开源设计参考、编写 `.artic/strategy.json`、把它编译成 `DESIGN.md` 和辅助文档,并验证输出。脚本只是 validator/compiler helper,不是设计判断或 strategy authorship 的来源。 > Artic **不会** 克隆参考网站。它从专业/OSS 设计系统中提取可复用原则,并把实现绑定到项目专属 strategy 和 AI-native 文档。 @@ -45,7 +43,6 @@ Agent 会在内部处理用户访谈、把 brief 归一化为搜索 facets、检 ```text /claude-artic:artic init /claude-artic:artic start -/claude-artic:artic show ``` 也可以用自然语言请求: @@ -65,7 +62,7 @@ codex plugin marketplace add baskduf/artic 需要稳定安装时固定到发布标签: ```bash -codex plugin marketplace add baskduf/artic@v0.4.1 +codex plugin marketplace add baskduf/artic@v0.5.0 ``` 在插件浏览器中安装 `codex-artic`: @@ -85,7 +82,6 @@ codex plugin add codex-artic@artic ```text @artic init @artic start -@artic show ``` ### Local checkout fallback @@ -104,12 +100,11 @@ python3 skills/artic/scripts/synthesize_reference_notes.py --query "ai product d python3 skills/artic/scripts/synthesize_reference_notes.py --query "ai product developer saas" --limit 3 --live-fetch --cache-dir /tmp/artic-cache --fixtures-dir /tmp/no-fixtures --output /tmp/artic-smoke/docs/live-reference-synthesis.md python3 skills/artic/scripts/scaffold_artic_files.py --root /tmp/artic-smoke python3 skills/artic/scripts/validate_artic_outputs.py --root /tmp/artic-smoke -python3 skills/artic/scripts/artic_show.py --root . python3 skills/artic/scripts/artic_version.py --root . python3 skills/artic/scripts/artic_update.py --root . ``` -Helper scripts 是 deterministic helper:`validate_artic_outputs.py` 验证 contract,`artic_start.py` 把 agent-authored strategy 编译成文档,`artic_show.py` 渲染 preview。设计判断的来源不是脚本,而是 public `@artic start` agent workflow 写入的 `.artic/strategy.json`。 +Helper scripts 是 deterministic helper:`validate_artic_outputs.py` 验证 contract,`artic_start.py` 把 agent-authored strategy 编译成文档。设计判断的来源不是脚本,而是 public `@artic start` agent workflow 写入的 `.artic/strategy.json`。 ## What Changes In The Agent @@ -121,8 +116,6 @@ Artic 被调用后,Agent 会: 4. 提取可复用规则:颜色角色、字体层级、间距节奏、组件、动效与可访问性。 5. 根据用户的项目目标解决不同参考之间的冲突。 6. 运行 `@artic start`,让 public agent workflow 编写 `.artic/strategy.json`、保存 `docs/artic-strategy.md`,再运行 compiler 生成 `DESIGN.md` 和辅助文档。 -7. 运行 `@artic show`,基于 strategy artifacts 在 `.artic/show/` 下渲染 asset-first visual draft bundle,且不会修改应用源码。 -8. 运行 `@artic review`,把实现与 `.artic/strategy.json`、`docs/artic-strategy.md`、`DESIGN.md` 对比,并验证生成的设计文档。 ## When To Use It @@ -146,8 +139,6 @@ Artic 被调用后,Agent 会: | 开始设计访谈 | `@artic init` | | 运行快速访谈 | `@artic init quick` | | 编写 strategy 并编译文档 | `@artic start` | -| 基于 strategy artifacts 渲染 asset-first preview bundle | `@artic show` | -| 按 strategy + `DESIGN.md` 审查实现 | `@artic review the homepage against DESIGN.md` | | 检查已安装/最新版本 | `@artic version` | | 输出安全更新命令 | `@artic update` | @@ -172,14 +163,6 @@ DESIGN.md docs/design-rules.md docs/design-qa-checklist.md docs/homepage-design-prompt.md -.artic/show/index.html -.artic/show/styles.css -.artic/show/tokens.json -.artic/show/assets/manifest.json # asset provenance;未验证 asset 仅限 preview-only,不代表 production-cleared -.artic/show/report.json -.artic/show/critique.md -.artic/show/selected.json -.artic/show/iterations//... # 由 @artic show 生成;不修改应用文件 ``` ## Repository Layout diff --git a/README.zh-TW.md b/README.zh-TW.md index ff0cfd6..186b56f 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -21,11 +21,9 @@ Artic 是一個相容 Claude/Codex 的 agent design-direction protocol,也是 ```text @artic init # 收集意圖和參考 @artic start # 編寫 strategy artifacts,再編譯 DESIGN.md 文件 -@artic show # 渲染記錄來源的 asset-first preview bundle -@artic review # 依文件檢查實作 ``` -Agent 會在內部處理使用者訪談、把 brief 正規化為搜索 facets、檢索專業/開源設計參考、編寫 `.artic/strategy.json`、把它編譯成 `DESIGN.md` 和輔助文件,並驗證輸出。腳本只是 validator/compiler/renderer helper,不是設計判斷或 strategy authorship 的來源。 +Agent 會在內部處理使用者訪談、把 brief 正規化為搜索 facets、檢索專業/開源設計參考、編寫 `.artic/strategy.json`、把它編譯成 `DESIGN.md` 和輔助文件,並驗證輸出。腳本只是 validator/compiler helper,不是設計判斷或 strategy authorship 的來源。 > Artic **不會** 克隆參考網站。它從專業/OSS 設計系統中提取可重用原則,並把實作綁定到專案專屬 strategy 和 AI-native 文件。 @@ -45,7 +43,6 @@ Agent 會在內部處理使用者訪談、把 brief 正規化為搜索 facets、 ```text /claude-artic:artic init /claude-artic:artic start -/claude-artic:artic show ``` 也可以用自然語言請求: @@ -65,7 +62,7 @@ codex plugin marketplace add baskduf/artic 需要穩定安裝時固定到發布標籤: ```bash -codex plugin marketplace add baskduf/artic@v0.4.1 +codex plugin marketplace add baskduf/artic@v0.5.0 ``` 在外掛瀏覽器中安裝 `codex-artic`: @@ -85,7 +82,6 @@ codex plugin add codex-artic@artic ```text @artic init @artic start -@artic show ``` ### Local checkout fallback @@ -104,12 +100,11 @@ python3 skills/artic/scripts/synthesize_reference_notes.py --query "ai product d python3 skills/artic/scripts/synthesize_reference_notes.py --query "ai product developer saas" --limit 3 --live-fetch --cache-dir /tmp/artic-cache --fixtures-dir /tmp/no-fixtures --output /tmp/artic-smoke/docs/live-reference-synthesis.md python3 skills/artic/scripts/scaffold_artic_files.py --root /tmp/artic-smoke python3 skills/artic/scripts/validate_artic_outputs.py --root /tmp/artic-smoke -python3 skills/artic/scripts/artic_show.py --root . python3 skills/artic/scripts/artic_version.py --root . python3 skills/artic/scripts/artic_update.py --root . ``` -Helper scripts 是 deterministic helper:`validate_artic_outputs.py` 驗證 contract,`artic_start.py` 把 agent-authored strategy 編譯成文件,`artic_show.py` 渲染 preview。設計判斷的來源不是腳本,而是 public `@artic start` agent workflow 寫入的 `.artic/strategy.json`。 +Helper scripts 是 deterministic helper:`validate_artic_outputs.py` 驗證 contract,`artic_start.py` 把 agent-authored strategy 編譯成文件。設計判斷的來源不是腳本,而是 public `@artic start` agent workflow 寫入的 `.artic/strategy.json`。 ## What Changes In The Agent @@ -121,8 +116,6 @@ Artic 被呼叫後,Agent 會: 4. 萃取可重用規則:色彩角色、字體層級、間距節奏、元件、動效與可存取性。 5. 依據使用者的專案目標解決不同參考之間的衝突。 6. 執行 `@artic start`,讓 public agent workflow 編寫 `.artic/strategy.json`、儲存 `docs/artic-strategy.md`,再執行 compiler 生成 `DESIGN.md` 和輔助文件。 -7. 執行 `@artic show`,基於 strategy artifacts 在 `.artic/show/` 下渲染 asset-first visual draft bundle,且不會修改應用程式原始碼。 -8. 執行 `@artic review`,把實作與 `.artic/strategy.json`、`docs/artic-strategy.md`、`DESIGN.md` 對比,並驗證生成的設計文件。 ## When To Use It @@ -146,8 +139,6 @@ Artic 被呼叫後,Agent 會: | 開始設計訪談 | `@artic init` | | 執行快速访谈 | `@artic init quick` | | 編寫 strategy 並編譯文件 | `@artic start` | -| 基於 strategy artifacts 渲染 asset-first preview bundle | `@artic show` | -| 依 strategy + `DESIGN.md` 審查實作 | `@artic review the homepage against DESIGN.md` | | 检查已安裝/最新版本 | `@artic version` | | 輸出安全更新命令 | `@artic update` | @@ -172,14 +163,6 @@ DESIGN.md docs/design-rules.md docs/design-qa-checklist.md docs/homepage-design-prompt.md -.artic/show/index.html -.artic/show/styles.css -.artic/show/tokens.json -.artic/show/assets/manifest.json # asset provenance;未驗證 asset 僅限 preview-only,不代表 production-cleared -.artic/show/report.json -.artic/show/critique.md -.artic/show/selected.json -.artic/show/iterations//... # 由 @artic show 生成;不修改應用程式檔案 ``` ## Repository Layout diff --git a/examples/prompts.md b/examples/prompts.md index dab7791..c534873 100644 --- a/examples/prompts.md +++ b/examples/prompts.md @@ -18,11 +18,3 @@ References: Linear for clarity, Shopify Polaris for trust/forms, Material for to ``` Expected public workflow: the agent writes `.artic/strategy.json` and `docs/artic-strategy.md`, then runs the compiler. If you invoke the raw compiler and strategy is missing, it writes `.artic/strategy-prompt.md` and exits non-zero so the agent can provide design direction. - -```text -@artic show -``` - -```text -@artic review the implemented homepage against .artic/strategy.json, docs/artic-strategy.md, and DESIGN.md -``` diff --git a/plugins/claude-artic/.claude-plugin/plugin.json b/plugins/claude-artic/.claude-plugin/plugin.json index afce9c5..6130a70 100644 --- a/plugins/claude-artic/.claude-plugin/plugin.json +++ b/plugins/claude-artic/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "claude-artic", - "version": "0.4.1", + "version": "0.5.0", "description": "Artic design onboarding skill for Claude Code.", "skills": [ { diff --git a/plugins/claude-artic/skills/artic/SKILL.md b/plugins/claude-artic/skills/artic/SKILL.md index 1181f69..ef459ad 100644 --- a/plugins/claude-artic/skills/artic/SKILL.md +++ b/plugins/claude-artic/skills/artic/SKILL.md @@ -1,7 +1,7 @@ --- name: artic -description: "Use when creating or improving a homepage/website and design rules are missing or weak. Artic is an agent design-direction protocol: @artic init interviews the user, @artic start authors strategy artifacts, then the compiler produces AI-native DESIGN.md docs without copying protected brand assets." -version: 0.4.1 +description: "Use when creating or improving a homepage/website and design rules are missing or weak. Artic is an agent design-direction protocol: @artic init interviews the user, @artic start authors strategy artifacts, then the compiler produces AI-native DESIGN.md docs from reusable reference principles." +version: 0.5.0 author: Hermes Agent license: MIT metadata: @@ -12,7 +12,7 @@ metadata: # Artic -Artic is an agent design-direction protocol for AI-built websites: a contract-bound LLM design director that turns reference-informed judgment into implementation-ready design artifacts. +Artic is a reference-informed design-documentation protocol for AI-built websites: a contract-bound LLM design director that turns project intent and reusable reference principles into implementation-ready design artifacts before coding. Core flow: @@ -21,24 +21,16 @@ Core flow: → collect project/design intent → normalize into search facets → search professional and open-source design references -→ select and combine the best patterns +→ preserve a draft intake session @artic start → author `.artic/strategy.json` and `docs/artic-strategy.md` as the design-direction contract → run the compiler against that strategy → generate DESIGN.md and implementation guidance → validate outputs - -@artic show -→ render the strategy artifacts and sourced/generated assets into a provenance-recorded asset-first visual draft bundle -→ write `.artic/show/` preview files without modifying app source files - -@artic review -→ compare implementation against `.artic/strategy.json`, `docs/artic-strategy.md`, and `DESIGN.md` -→ report token, hierarchy, spacing, accessibility, CTA, mobile, and reference-safety drift ``` -Artic is not a generic design prompt or passive generator. Its core value is searching multiple professional/OSS design resources, extracting compatible patterns, and binding one project-specific AI-native design direction into a strategy contract. Scripts are validator/compiler/renderer helpers; they do not make the design judgment for the agent. +Artic is not a generic design prompt or passive generator. Its core value is searching multiple professional/OSS design resources, extracting compatible patterns, and binding one project-specific AI-native design direction into a strategy contract. Scripts are validator/compiler helpers; they do not make the design judgment for the agent. ## When to Use @@ -49,7 +41,7 @@ Use Artic when: - The user provides reference sites/images and wants the design improved. - The user wants AI-native docs before implementation. -Do not use Artic for exact cloning of a brand/site or copying logos, trademarks, proprietary illustrations, exact layouts, copywriting, or animations. +Do not use Artic for exact cloning of a brand/site or copying logos, trademarks, proprietary illustrations, exact layouts, copywriting, or animations. Use references to extract reusable design principles, then create original project-specific direction. ## Commands @@ -71,7 +63,7 @@ Language behavior: - When `@artic start` compiles the ready session, carry the language contract into `.artic/brief.json.language`, `.artic/state.json.language`, and generated docs using ``. - Preserve machine-readable terms such as `DESIGN.md`, `AI-native`, `Artic`, source names, and design token keys. - Localize user-facing interview questions and prose according to the language contract. -- Validate localized reference safety with the invariant `` marker, not exact English copy. +- Validate localized reference principle policy with the invariant `` marker, not exact English copy. Default interview questions: 1. What product/service is this homepage for? @@ -86,14 +78,11 @@ Default interview questions: 10. Tech stack? 11. Accessibility target? Default: WCAG AA. 12. Existing docs that must be reflected? -13. May Artic search/use owned or clearly licensed public assets, or should external sources stay as reference principles only? -External reference vs external asset boundary: +Reference boundary: - External references are design/runtime sources used for reusable principles, interaction patterns, accessibility/performance constraints, token relationships, and implementation guidance. -- External assets are concrete media that can appear in the generated site: GLB files, images, models, icons, textures, fonts, illustrations, or similar files. -- `@artic start` still treats external sources as reference-principles unless the user explicitly allows asset usage. -- `@artic show` may use sourced or generated assets for the preview bundle when doing so improves the first visual draft, but every asset must be recorded in `.artic/show/assets/manifest.json` and `.artic/show/report.json` with source/generation notes, license status when known, and preview-only status when unverified. -- Unverified assets are allowed only inside the preview bundle as provenance-recorded, preview-only material. Do not claim production license clearance; production/apply review belongs to a future `@artic apply` flow or a separate implementation review. +- Artic treats external sources as reference-principles for strategy and documentation, not as concrete assets to copy into the user's site. +- Use reference guidance as constructive creative direction: translate patterns into original tokens, hierarchy, components, motion, and information architecture. Fast path: if user says `@artic init quick`, ask only product, audience, goal, vibe, and references. @@ -142,31 +131,6 @@ Required behavior: Lifecycle transition rule: `@artic start` is the only transition that may finalize a ready init session. If `.artic/init-session.json` is `collecting`, stop and ask the remaining questions; if it is `ready`, finalize it and then generate docs. -### `@artic show` - -Purpose: render the current Artic strategy into a provenance-recorded, asset-first first visual draft bundle. The goal is a stronger strategy-grounded first visual draft by using sourced/generated assets when helpful, while recording where each asset came from and whether it is preview-only. - -Executable path for agents/hosts that expose shell-backed commands: - -```bash -python3 /scripts/artic_show.py --root -``` - -Required behavior: -1. Require `DESIGN.md`, `docs/homepage-design-prompt.md`, `.artic/brief.json`, `.artic/references.json`, and `.artic/strategy.json` from a completed `@artic start` flow. -2. Generate a strategy-grounded first visual draft bundle under `.artic/show/`, not just a minimal static preview. -3. Prefer asset-first draft quality: use user-provided, sourced, or generated images/models/icons/textures when they improve the first visual draft, and fall back to styled placeholders only when assets are unavailable or inappropriate. -4. Record provenance for every preview asset in `.artic/show/assets/manifest.json` and summarize asset decisions, gaps, and warnings in `.artic/show/report.json`. -5. Mark unverified assets as preview-only/provenance-recorded. Do not state or imply production license clearance for unverified sourced/generated assets. -6. Do not modify app/source files. Downstream files such as `app/page.tsx`, `src/App.tsx`, or `pages/index.tsx` are out of scope for `@artic show`; a future `@artic apply` or separate implementation review would handle production/apply decisions later. -7. Include the reference-safety policy in the preview so the preview remains reference-informed, not reference-copied. -8. If selected reference roles include `3d_runtime`, render a runtime-aware preview structure such as a `model-viewer`/Three.js placeholder, central interaction zone, poster/reduced-motion fallback notes, and asset provenance/license reminders instead of a generic landing-page-only preview. -9. Return JSON with at least `preview_file`, `bundle_dir`, `asset_manifest`, `report_file`, and `modified_app_files` for verification. - -### `@artic review` MVP-light - -After implementation, compare the current homepage against `.artic/strategy.json`, `docs/artic-strategy.md`, and `DESIGN.md`. Check token consistency, typography hierarchy, spacing rhythm, CTA hierarchy, mobile behavior, accessibility basics, and no-copy reference safety. The MVP review output contract may be chat-first; future hosts may persist `.artic/review.json` and `docs/artic-review.md`, but do not claim those files exist unless the review workflow writes them. - ### `@artic version` Purpose: report the installed Artic package versions and compare them with the latest GitHub release. @@ -201,7 +165,7 @@ Artic catalog entries are user-facing design intelligence, not an internal audit - Prefer wording such as “Use this for…”, “Apply its…”, “Translate… into project-specific…”, “Pair with…”, and “Keep identity, copy, and layout decisions original by…”. - Keep each entry source-specific; do not flatten the catalog into repeated boilerplate. -Use `application_guidance` for this product-facing catalog copy. Do not add `risk_notes` or other defensive/internal field names for new catalog work. +Use `application_guidance` for this product-facing catalog copy. Keep catalog guidance positive, specific, and application-oriented. Good pattern: ```text @@ -229,16 +193,16 @@ Synthesis rule: - Resolve conflicts based on project goal. - Preserve rationale in `docs/artic-brief.md`. -## Reference Safety +## Reference Principle Policy Allowed: token structure, color role relationships, typography hierarchy, spacing systems, component behavior, accessibility rules, motion principles. -Forbidden unless user owns rights: logos, trademarks, proprietary illustrations, exact page composition, exact brand palette as identity, copywriting, copyrighted imagery. +Not part of Artic's output: logos, trademarks, proprietary illustrations, exact page composition, exact brand palette as identity, copywriting, copyrighted imagery. Required phrase in generated docs: ```text -Reference policy: extract reusable principles only; do not copy logos, trademarks, proprietary illustrations, or exact layouts. +Reference policy: extract reusable principles only; create original project-specific direction. ``` ## Output Contract @@ -280,21 +244,6 @@ docs/design-qa-checklist.md docs/homepage-design-prompt.md ``` -`@artic show` creates a provenance-recorded preview bundle: - -```text -.artic/show/index.html -.artic/show/styles.css -.artic/show/tokens.json -.artic/show/assets/manifest.json -.artic/show/report.json -.artic/show/critique.md -.artic/show/selected.json -.artic/show/iterations//... -``` - -Show bundles are preview-only and do not modify app source files. Unverified sourced/generated assets may be included for first-draft visual quality only when marked preview-only and provenance-recorded; production/apply clearance is out of scope for `@artic show` and belongs to future `@artic apply`/implementation review. - Optional exports: `tailwind.theme.json`, `tokens.json`. ## Presets @@ -309,7 +258,7 @@ python3 /scripts/validate_artic_outputs.py --root python3 /scripts/search_reference_catalog.py --query "ai product developer saas premium" --limit 3 ``` -Validation, compilation, and rendering scripts enforce and materialize the contract. They are not a design judgment source; `@artic start` must provide that judgment through `.artic/strategy.json`. +Validation and compilation scripts enforce and materialize the contract. They are not a design judgment source; `@artic start` must provide that judgment through `.artic/strategy.json`. ## Common Pitfalls diff --git a/plugins/claude-artic/skills/artic/scripts/artic_init.py b/plugins/claude-artic/skills/artic/scripts/artic_init.py index 13f61ee..6433ec0 100644 --- a/plugins/claude-artic/skills/artic/scripts/artic_init.py +++ b/plugins/claude-artic/skills/artic/scripts/artic_init.py @@ -8,7 +8,6 @@ from pathlib import Path from design_intent_mapper import map_design_intent -from risk_readiness import analyze_risk_readiness from search_reference_catalog import load_catalog, search, terms POLICY_MARKER = "" @@ -242,32 +241,6 @@ def create_init_outputs(root: Path, args: argparse.Namespace) -> dict: requirements = parse_key_values(getattr(args, "requirement", [])) constraints = parse_key_values(getattr(args, "constraint", [])) policy = asset_policy_payload(str(getattr(args, "asset_policy", ""))) - risk_answers = { - "project": args.project, - "audience": args.audience, - "goal": args.goal, - "vibe": args.vibe, - "references": args.references, - "stack": args.stack, - "accessibility": args.accessibility, - "asset_policy": str(getattr(args, "asset_policy", "")), - **requirements, - **constraints, - } - risk_answers["locale"] = args.locale - if risk_answers.get("stack") and not risk_answers.get("technical_runtime"): - risk_answers["technical_runtime"] = risk_answers["stack"] - elif not risk_answers.get("technical_runtime"): - vibe_text = str(risk_answers.get("vibe") or "") - if re.search(r"runtime|런타임|webgl|model-viewer|3d", vibe_text, re.IGNORECASE): - risk_answers["technical_runtime"] = vibe_text - if risk_answers.get("asset_policy") and not risk_answers.get("license_clearance"): - risk_answers["license_clearance"] = risk_answers["asset_policy"] - interaction_answer = str(risk_answers.get("interaction_model") or "") - if interaction_answer and not risk_answers.get("performance_accessibility_plan"): - if re.search(r"reduced motion|reduced-motion|keyboard|키보드|대체|fallback|alt|접근성|성능|load|loading", interaction_answer, re.IGNORECASE): - risk_answers["performance_accessibility_plan"] = interaction_answer - risk_readiness = analyze_risk_readiness(risk_answers, intent) source_plan = [ { "source_id": src["id"], @@ -280,7 +253,7 @@ def create_init_outputs(root: Path, args: argparse.Namespace) -> dict: ] brief = { - "artic_version": "0.4.1", + "artic_version": "0.5.0", "project": { "name": normalized_project["name"], "type": "homepage", @@ -303,7 +276,6 @@ def create_init_outputs(root: Path, args: argparse.Namespace) -> dict: "constraints": constraints, "asset_policy": policy, "implementation": {"stack": args.stack or "unspecified", "mobile_first": "mobile" in args.vibe.lower(), "accessibility": args.accessibility}, - "risk_readiness": risk_readiness, "language": lang, "copy_policy": "artic-policy: reference-safety-v1", } @@ -314,7 +286,7 @@ def create_init_outputs(root: Path, args: argparse.Namespace) -> dict: "source_plan": source_plan, "synthesis": "Use selected sources as compatible patterns; localize prose according to the brief language contract while preserving source names and protected terms.", } - state = {"artic_version": "0.4.1", "last_generated_at": now, "status": "initialized", "language": lang, "intent_path": ".artic/intent.json"} + state = {"artic_version": "0.5.0", "last_generated_at": now, "status": "initialized", "language": lang, "intent_path": ".artic/intent.json"} write(root / ".artic" / "intent.json", json.dumps(intent, indent=2, ensure_ascii=False) + "\n") write(root / ".artic" / "brief.json", json.dumps(brief, indent=2, ensure_ascii=False) + "\n") diff --git a/plugins/claude-artic/skills/artic/scripts/artic_init_session.py b/plugins/claude-artic/skills/artic/scripts/artic_init_session.py index 9007921..4691a94 100644 --- a/plugins/claude-artic/skills/artic/scripts/artic_init_session.py +++ b/plugins/claude-artic/skills/artic/scripts/artic_init_session.py @@ -8,22 +8,12 @@ from typing import Any from locale_contract import detect_locale_from_text, language_contract -from risk_readiness import analyze_risk_readiness as analyze_risk_readiness_contract SESSION_PATH = Path(".artic") / "init-session.json" REQUIRED_FIELDS = ["project", "audience", "goal", "vibe"] -OPTIONAL_FIELDS = ["references", "stack", "avoid", "accessibility", "asset_policy"] -DYNAMIC_REQUIRED_FIELDS = [ - "asset_source", - "interaction_model", - "brand_constraints", - "conversion_details", - "performance_accessibility", - "asset_policy", - "license_policy", -] +OPTIONAL_FIELDS = ["references", "stack", "avoid", "accessibility"] CORE_ANSWER_FIELDS = set(REQUIRED_FIELDS + OPTIONAL_FIELDS) -CONSTRAINT_FIELD_HINTS = ("constraint", "constraints", "brand", "avoid", "asset_policy", "license_policy") +CONSTRAINT_FIELD_HINTS = ("constraint", "constraints", "brand", "avoid") QUESTION_SPECS: dict[str, dict[str, str]] = { "project": { @@ -66,69 +56,11 @@ "zh-CN": "目标技术栈是什么?", "zh-TW": "目標技術棧是什麼?", }, - "asset_policy": { - "intent": "Clarify whether external assets may be used or only referenced as principles.", - "en-US": "May Artic search/use owned or clearly licensed public assets, or should external sources stay as reference principles only?", - "ko-KR": "소유한 에셋 또는 라이선스 확인 가능한 공개 에셋을 검색/사용해도 되나요, 아니면 외부 소스는 원칙 참고로만 사용할까요?", - "ja-JP": "所有アセットまたはライセンス確認済みの公開アセットを検索/使用してよいですか、それとも外部ソースは原則の参考のみにしますか?", - "zh-CN": "是否可以搜索/使用自有资产或可验证授权的公开资产,还是外部来源仅作为原则参考?", - "zh-TW": "是否可以搜尋/使用自有資產或可驗證授權的公開資產,或外部來源只作為原則參考?", - }, - "asset_source": { - "intent": "Identify source and availability of required visual/3D assets.", - "en-US": "What is the source for required images, video, or 3D assets (owned files, generated placeholders, or licensed public assets)?", - "ko-KR": "필요한 이미지/영상/3D 에셋은 어디서 오나요? 자체 보유 파일, 임시 플레이스홀더, 라이선스 확인 가능한 공개 에셋 중 무엇인가요?", - }, - "interaction_model": { - "intent": "Clarify implementation-critical interaction behavior.", - "en-US": "How should key interactions work, including mouse/touch/keyboard behavior and fallbacks?", - "ko-KR": "핵심 상호작용은 어떻게 동작해야 하나요? 마우스/터치/키보드 조작과 대체 경험까지 알려주세요.", - }, - "brand_constraints": { - "intent": "Capture brand/legal constraints that affect design execution.", - "en-US": "What brand, legal, tone, color, logo, or copy constraints must the design follow?", - "ko-KR": "디자인이 따라야 할 브랜드/법무/톤/색상/로고/문구 제약은 무엇인가요?", - }, - "brand_assets": { - "intent": "Capture brand assets and brand-system constraints.", - "en-US": "What logo, color, font, or brand-system assets must the design follow?", - "ko-KR": "디자인이 따라야 할 로고, 색상, 폰트, 브랜드 시스템 에셋은 무엇인가요?", - }, - "conversion_details": { - "intent": "Clarify conversion path details.", - "en-US": "What exact CTA, form fields, destination, or success criteria should the conversion flow use?", - "ko-KR": "전환 흐름의 정확한 CTA, 폼 필드, 이동 경로, 성공 기준은 무엇인가요?", - }, - "conversion_path": { - "intent": "Clarify conversion path details.", - "en-US": "What exact CTA, form fields, destination, trust requirements, or success criteria should the conversion flow use?", - "ko-KR": "전환 흐름의 정확한 CTA, 폼 필드, 이동 경로, 신뢰 조건, 성공 기준은 무엇인가요?", - }, - "performance_accessibility": { - "intent": "Clarify performance and accessibility constraints for risky media or interaction.", - "en-US": "What performance and accessibility requirements apply (load budget, reduced motion, keyboard support, alt/fallback content)?", - "ko-KR": "성능/접근성 요구는 무엇인가요? 로딩 예산, reduced motion, 키보드 지원, 대체 콘텐츠를 알려주세요.", - }, - "performance_accessibility_plan": { - "intent": "Clarify performance and accessibility constraints for risky media or interaction.", - "en-US": "What performance and accessibility plan applies (load budget, reduced motion, keyboard support, alt/fallback content)?", - "ko-KR": "성능/접근성 계획은 무엇인가요? 로딩 예산, reduced motion, 키보드 지원, 대체 콘텐츠를 알려주세요.", - }, - "license_policy": { - "intent": "Clarify licensing and attribution obligations.", - "en-US": "What license and attribution policy should Artic follow for any third-party assets or references?", - "ko-KR": "제3자 에셋/레퍼런스의 라이선스와 출처 표기는 어떤 정책을 따라야 하나요?", - }, - "license_clearance": { - "intent": "Clarify licensing and attribution obligations.", - "en-US": "What license clearance and attribution policy should Artic follow for any third-party assets or references?", - "ko-KR": "제3자 에셋/레퍼런스의 라이선스 확인과 출처 표기는 어떤 정책을 따라야 하나요?", - }, } _STYLE_HINT_RE = re.compile(r"토스|신뢰|고급|깔끔|스타트업|모바일|saas|premium|trust|clean|mobile|startup", re.IGNORECASE) _LABELED_ANSWER_RE = re.compile( - r"(?:^|[.;\n]\s*)(project|product|service|audience|target user|target users|goal|conversion goal|vibe|impression|style|stack|tech stack|references?|avoid|accessibility|asset policy)\s*:\s*(.*?)(?=(?:[.;\n]\s*)(?:project|product|service|audience|target user|target users|goal|conversion goal|vibe|impression|style|stack|tech stack|references?|avoid|accessibility|asset policy)\s*:|$)", + r"(?:^|[.;\n]\s*)(project|product|service|audience|target user|target users|goal|conversion goal|vibe|impression|style|stack|tech stack|references?|avoid|accessibility)\s*:\s*(.*?)(?=(?:[.;\n]\s*)(?:project|product|service|audience|target user|target users|goal|conversion goal|vibe|impression|style|stack|tech stack|references?|avoid|accessibility)\s*:|$)", re.IGNORECASE | re.DOTALL, ) _LABELED_FIELD_MAP = { @@ -209,18 +141,6 @@ def _answer_corpus(answers: dict[str, str]) -> str: return "\n".join(str(value) for value in answers.values() if value).lower() -def analyze_risk_readiness(answers: dict[str, str], missing_core: list[str]) -> dict[str, Any]: - """Return the canonical Artic risk/readiness contract for dynamic init follow-ups.""" - payload = analyze_risk_readiness_contract(answers) - if missing_core: - payload["missing_core_fields"] = list(missing_core) - payload["readiness"] = {"strategy": "blocked", "preview": "blocked", "implementation": "blocked", "status": "core_fields_missing"} - payload["ready_for_strategy"] = False - payload["ready_for_preview"] = False - payload["ready_for_implementation"] = False - payload["implementation_blocked"] = True - return payload - def create_or_update_session(root: Path, user_text: str, explicit_locale: str | None = None, answers: dict[str, str] | None = None) -> dict[str, Any]: existing: dict[str, Any] @@ -246,36 +166,13 @@ def create_or_update_session(root: Path, user_text: str, explicit_locale: str | bilingual_terms=bool(previous_language.get("bilingual_terms", False)), ) missing = missing_required_fields(merged_answers) - risk_answers = dict(merged_answers) - if risk_answers.get("stack") and not risk_answers.get("technical_runtime"): - risk_answers["technical_runtime"] = risk_answers["stack"] - elif not risk_answers.get("technical_runtime"): - vibe_text = str(risk_answers.get("vibe") or "") - if re.search(r"runtime|런타임|webgl|model-viewer|3d", vibe_text, re.IGNORECASE): - risk_answers["technical_runtime"] = vibe_text - if risk_answers.get("asset_policy") and not risk_answers.get("license_clearance"): - risk_answers["license_clearance"] = risk_answers["asset_policy"] - interaction_answer = str(risk_answers.get("interaction_model") or "") - if interaction_answer and not risk_answers.get("performance_accessibility_plan"): - if re.search(r"reduced motion|reduced-motion|keyboard|키보드|대체|fallback|alt|접근성|성능|load|loading", interaction_answer, re.IGNORECASE): - risk_answers["performance_accessibility_plan"] = interaction_answer - risk_readiness = analyze_risk_readiness(risk_answers, missing) - canonical_missing_dynamic = [str(field) for field in risk_readiness.get("missing_dynamic_required_fields", [])] - missing_dynamic = list(canonical_missing_dynamic) - if "license_clearance" in canonical_missing_dynamic and "asset_policy" not in missing_dynamic: - missing_dynamic.append("asset_policy") - if "asset_source" in canonical_missing_dynamic and not merged_answers.get("asset_policy") and "asset_policy" not in missing_dynamic: - missing_dynamic.append("asset_policy") - last_question_ids = (missing + missing_dynamic)[:6] + last_question_ids = missing[:6] session = { "schema_version": 1, "status": "ready" if not missing else "collecting", "language": lang, "answers": merged_answers, "missing": missing, - "risk_readiness": risk_readiness, - "missing_dynamic_required_fields": missing_dynamic, - "readiness": {key: risk_readiness["readiness"].get(key) for key in ("strategy", "preview", "implementation")}, "last_question_ids": last_question_ids, } write_session(root, session) @@ -291,7 +188,6 @@ def render_questions(session: dict[str, Any], limit: int = 4) -> list[str]: field_ids = [str(field) for field in raw_ids] else: field_ids = [str(field) for field in session.get("missing", [])] - field_ids.extend(str(field) for field in session.get("missing_dynamic_required_fields", []) if str(field) not in field_ids) questions: list[str] = [] for field in field_ids[:limit]: spec = QUESTION_SPECS.get(str(field), {}) @@ -333,13 +229,6 @@ def render_ready_summary(session: dict[str, Any]) -> str: raw_answers = session.get("answers") answers: dict[str, Any] = raw_answers if isinstance(raw_answers, dict) else {} locale = str(language.get("locale") or "en-US") - raw_risk = session.get("risk_readiness") - risk: dict[str, Any] = raw_risk if isinstance(raw_risk, dict) else {} - raw_readiness = session.get("readiness") - readiness: dict[str, Any] = raw_readiness if isinstance(raw_readiness, dict) else {} - risk_level = str(risk.get("risk_level") or "low") - missing_dynamic = [str(field) for field in session.get("missing_dynamic_required_fields", [])] - signal_names = [str(row.get("signal")) for row in risk.get("signals", []) if isinstance(row, dict) and row.get("signal")] if locale.startswith("ko"): lines = [ "필수 정보는 충분히 모였습니다.", @@ -350,27 +239,9 @@ def render_ready_summary(session: dict[str, Any]) -> str: f"- 목표: {answers.get('goal', '')}", f"- 무드: {answers.get('vibe', '')}", "", - f"리스크 요약: {risk_level}" - + (f" ({', '.join(signal_names[:4])})" if signal_names else ""), - ] - if missing_dynamic: - lines.extend([ - f"구현 차단: 전략 문서는 시작할 수 있지만 실제 구현은 추가 확인이 필요합니다: {', '.join(missing_dynamic)}.", - "플레이스홀더/원칙 참고 경계는 유지되며, 에셋 사용을 명시적으로 허용하지 않으면 외부 소스는 원칙 참고로만 사용합니다.", - ]) - elif readiness.get("implementation") == "blocked": - lines.extend([ - "구현 차단: 핵심 품질 요구가 플레이스홀더나 낮은 품질 대체물에 의존하고 있어 구현 전에 해결해야 합니다.", - "플레이스홀더/원칙 참고 경계는 유지되며, 필요한 입력을 검증할 때까지 프로덕션 구현으로 진행하지 않습니다.", - ]) - elif readiness.get("implementation") == "ready_with_assumptions": - lines.append("구현 준비: 진행 가능하지만 성능/접근성 세부값은 보수적 기본값으로 가정합니다.") - lines.extend([ - "", - "더 다듬고 싶으면 레퍼런스, 피해야 할 스타일, 브랜드/에셋 제약을 추가로 알려주세요.", - "에셋 사용을 명시적으로 허용하지 않으면 외부 소스는 원칙 참고로만 사용합니다.", + "더 다듬고 싶으면 레퍼런스, 피해야 할 스타일, 브랜드 제약을 추가로 알려주세요.", "문서 생성을 시작하려면 `@artic start`를 실행하세요.", - ]) + ] else: lines = [ "The required Artic intake is ready.", @@ -381,27 +252,9 @@ def render_ready_summary(session: dict[str, Any]) -> str: f"- Goal: {answers.get('goal', '')}", f"- Vibe: {answers.get('vibe', '')}", "", - f"Risk summary: {risk_level}" - + (f" ({', '.join(signal_names[:4])})" if signal_names else ""), - ] - if missing_dynamic: - lines.extend([ - f"Implementation blocker: strategy docs may proceed, but implementation needs follow-up on: {', '.join(missing_dynamic)}.", - "Placeholder/reference boundaries remain active; external assets stay reference-principles only unless explicitly allowed.", - ]) - elif readiness.get("implementation") == "blocked": - lines.extend([ - "Implementation blocker: a quality-critical requirement depends on a placeholder or lower-quality substitute.", - "Placeholder/reference boundaries remain active; do not proceed to production implementation until the required input is verified.", - ]) - elif readiness.get("implementation") == "ready_with_assumptions": - lines.append("Implementation readiness: ready with conservative performance/accessibility assumptions.") - lines.extend([ - "", - "Add references, avoided styles, brand constraints, or asset policy if you want to refine the brief.", - "If you do not explicitly allow asset usage, external sources stay reference-principles only.", + "Add references, avoided styles, or brand constraints if you want to refine the brief.", "To generate Artic design docs, run `@artic start`.", - ]) + ] return "\n".join(lines) diff --git a/plugins/claude-artic/skills/artic/scripts/artic_show.py b/plugins/claude-artic/skills/artic/scripts/artic_show.py deleted file mode 100644 index 3ae2ad6..0000000 --- a/plugins/claude-artic/skills/artic/scripts/artic_show.py +++ /dev/null @@ -1,833 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import argparse -import html -import json -import re -import shutil -import sys -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -REQUIRED_INPUTS = [ - "DESIGN.md", - "docs/homepage-design-prompt.md", - ".artic/brief.json", - ".artic/references.json", - ".artic/strategy.json", -] -POLICY_FALLBACK = "Reference policy: extract reusable principles only; do not copy logos, trademarks, proprietary illustrations, or exact layouts." -POLICY_BY_LOCALE = { - "ko-KR": "참고 정책: 재사용 가능한 원칙만 추출하고, 로고, 상표, 독점 일러스트, 정확한 레이아웃은 복사하지 않습니다.", -} -SCORE_KEYS = [ - "strategy_alignment", - "visual_fidelity", - "asset_richness", - "asset_provenance_completeness", - "conversion_clarity", - "visual_specificity", - "accessibility_basics", - "mobile_first", - "genericness_penalty", - "overall", -] - - -def read_json(path: Path) -> dict[str, Any]: - try: - payload = json.loads(path.read_text(encoding="utf-8")) - except FileNotFoundError as exc: - raise ValueError(f"missing required input: {path}") from exc - except json.JSONDecodeError as exc: - raise ValueError(f"invalid JSON in {path}: {exc}") from exc - if not isinstance(payload, dict): - raise ValueError(f"expected JSON object in {path}") - return payload - - -def write_json(path: Path, payload: Any) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - - -def write_text(path: Path, content: str) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding="utf-8") - - -def required_missing(root: Path) -> list[str]: - return [rel for rel in REQUIRED_INPUTS if not (root / rel).exists()] - - -def frontmatter(text: str) -> str: - if not text.startswith("---\n"): - return "" - end = text.find("\n---\n", 4) - if end == -1: - return "" - return text[4:end] - - -def yaml_value(block: str, key: str, default: str) -> str: - match = re.search(rf"^\s*{re.escape(key)}\s*:\s*[\"']?([^\n\"']+)[\"']?\s*$", block, re.MULTILINE) - return match.group(1).strip() if match else default - - -def yaml_section_value(block: str, section: str, token: str, default: str) -> str: - lines = block.splitlines() - in_section = False - for line in lines: - if re.match(rf"^{re.escape(section)}\s*:", line): - in_section = True - continue - if in_section and line and not line.startswith(" "): - break - if in_section: - match = re.match(rf"^\s+{re.escape(token)}\s*:\s*[\"']?([^\n\"']+)[\"']?\s*$", line) - if match: - return match.group(1).strip() - return default - - -def safe_css_color(value: str, fallback: str) -> str: - candidate = value.strip() - safe_patterns = [ - r"#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?(?:[0-9a-fA-F]{2})?", - r"(?:rgb|rgba|hsl|hsla)\([0-9.%,\s]+\)", - r"[a-zA-Z]+", - ] - if any(re.fullmatch(pattern, candidate) for pattern in safe_patterns): - return candidate - return fallback - - -def markdown_section(text: str, heading: str) -> str: - marker = f"## {heading}" - start = text.find(marker) - if start == -1: - return "" - next_heading = text.find("\n## ", start + len(marker)) - end = next_heading if next_heading != -1 else len(text) - section = text[start + len(marker):end] - return " ".join(line.strip(" #") for line in section.splitlines() if line.strip()) - - -def selected_source_names(references: dict[str, Any]) -> list[str]: - selected = references.get("selected_sources", []) - if not isinstance(selected, list): - return [] - names: list[str] = [] - for row in selected: - if not isinstance(row, dict): - continue - name = str(row.get("name") or row.get("id") or "reference source").strip() - if name: - names.append(name) - return names - - -def brief_locale(brief: dict[str, Any]) -> str: - language = brief.get("language") - if isinstance(language, dict): - return str(language.get("locale") or "en-US") - return "en-US" - - -def has_3d_runtime_reference(brief: dict[str, Any], references: dict[str, Any]) -> bool: - roles = references.get("role_assignments", []) - if isinstance(roles, list): - for role in roles: - if isinstance(role, dict) and "3d_runtime" == str(role.get("role")): - return True - for bucket in (brief.get("style"), references, brief): - text = json.dumps(bucket, ensure_ascii=False).lower() if isinstance(bucket, (dict, list)) else str(bucket).lower() - if any(term in text for term in ("3d", "webgl", "glb", "model-viewer", "runtime")): - return True - return False - - -def _string_items(value: Any) -> list[str]: - if value is None: - return [] - if isinstance(value, str): - text = value.strip() - return [text] if text else [] - if isinstance(value, list): - items: list[str] = [] - for item in value: - items.extend(_string_items(item)) - return items - if isinstance(value, dict): - for key in ("summary", "description", "requirement", "condition", "reason", "message", "title", "name"): - if value.get(key): - return _string_items(value.get(key)) - return [json.dumps(value, ensure_ascii=False, sort_keys=True)] - return [str(value)] - - -def _has_missing_quality(value: Any) -> bool: - missing_markers = ("missing", "blocked", "incomplete", "not_met", "failed", "fail", "placeholder") - if isinstance(value, dict): - status = str(value.get("status") or value.get("state") or value.get("readiness") or "").lower() - if any(marker in status for marker in missing_markers): - return True - return any(_has_missing_quality(item) for item in value.values()) - if isinstance(value, list): - return any(_has_missing_quality(item) for item in value) - if isinstance(value, str): - return any(marker in value.lower() for marker in missing_markers) - return False - - -def risk_readiness_summary(brief: dict[str, Any]) -> dict[str, Any]: - risk = brief.get("risk_readiness") - if not isinstance(risk, dict): - return { - "preview_status": "static_preview", - "production_ready": True, - "placeholder_boundaries": [], - "implementation_blockers": [], - } - - placeholder_boundaries = _string_items(risk.get("placeholder_boundary")) + _string_items(risk.get("placeholder_fallback_boundary")) - implementation_blockers = _string_items(risk.get("stop_conditions")) + _string_items(risk.get("implementation_stop_conditions")) - - readiness = str(risk.get("readiness") or "").lower() - implementation_blocked = bool(risk.get("implementation_blocked")) - readiness_blocked = any(marker in readiness for marker in ("blocked", "placeholder", "not_ready", "not-ready")) - quality_missing = _has_missing_quality(risk.get("quality_critical_requirements")) or _has_missing_quality(risk.get("core_quality_requirements")) - production_ready = not (implementation_blocked or readiness_blocked or bool(placeholder_boundaries) or bool(implementation_blockers) or quality_missing) - - return { - "preview_status": "static_preview" if production_ready else "placeholder_preview", - "production_ready": production_ready, - "placeholder_boundaries": list(dict.fromkeys(placeholder_boundaries)), - "implementation_blockers": list(dict.fromkeys(implementation_blockers)), - } - - -def localized_copy(locale: str) -> dict[str, str]: - if locale.startswith("ko"): - return { - "preview_badge": "Artic 미리보기 · 에셋 우선 번들", - "for_label": "대상", - "headline_suffix": "첫 시각 초안", - "drive": "목표 이끌기", - "view_logic": "디자인 논리 보기", - "north_star": "Design north star", - "accessibility": "접근성 목표", - "reference_roles": "레퍼런스 역할", - "promise_title": "1. 약속", - "promise_body": "프로젝트 고유의 약속과 가장 중요한 전환 경로를 먼저 보여줍니다.", - "proof_title": "2. 증거", - "proof_body": "신뢰 근거와 에셋 출처를 hero 가까이에 배치해 첫 CTA를 뒷받침합니다.", - "system_title": "3. 시스템", - "system_body": "토큰, 간격 리듬, 시맨틱 컨트롤, 독창적 구성을 Artic 문서에서 가져옵니다.", - "reference_title": "레퍼런스 기반, 복제 아님", - "reference_body": "이 정적 번들은 Artic start 산출물에서 생성됩니다. 앱 소스 파일은 건드리지 않습니다.", - "policy_title": "참고 정책", - "generated": "생성 시각", - "root": "루트", - "model_label": "3D 모델 자리표시자", - "model_body": "실제 GLB/이미지/모델은 소유 에셋 또는 라이선스 확인 가능한 공개 에셋만 연결합니다. 이 preview-only 에셋은 poster fallback, reduced motion, 로딩 실패 상태를 검토하기 위한 구조입니다.", - "interaction_title": "상호작용 영역", - "interaction_body": "드래그/탭/키보드 포커스로 3D 오브젝트를 탐색하고, 실패 시 정적 포스터와 설명 텍스트를 제공합니다.", - } - return { - "preview_badge": "Artic Preview · asset-first bundle", - "for_label": "For", - "headline_suffix": "first visual draft", - "drive": "Drive", - "view_logic": "View design logic", - "north_star": "Design north star", - "accessibility": "accessibility target", - "reference_roles": "reference roles", - "promise_title": "1. Promise", - "promise_body": "Lead with one clear project-specific promise and one dominant conversion path.", - "proof_title": "2. Proof", - "proof_body": "Place evidence and asset provenance close to the hero so trust supports the first CTA.", - "system_title": "3. System", - "system_body": "Use tokens, spacing rhythm, semantic controls, and original composition from the Artic docs.", - "reference_title": "Reference-informed, not reference-copied", - "reference_body": "This static bundle is generated from Artic start outputs. It intentionally leaves app source files untouched.", - "policy_title": "Reference policy", - "generated": "Generated", - "root": "Root", - "model_label": "3D asset placeholder", - "model_body": "Attach only user-owned or license-verifiable public GLB/image/model assets when allowed. This preview-only asset validates poster fallback, reduced motion, and loading failure states.", - "interaction_title": "Interaction zone", - "interaction_body": "Design drag, tap, and keyboard focus affordances for the central 3D object, with a static poster and description fallback when loading fails.", - } - - -def project_name(brief: dict[str, Any], design_frontmatter: str) -> str: - project = brief.get("project") - if isinstance(project, dict): - value = str(project.get("name") or project.get("description") or "").strip() - if value: - return value - return yaml_value(design_frontmatter, "name", "Artic Preview") - - -def build_tokens(design_text: str) -> dict[str, Any]: - fm = frontmatter(design_text) - colors = { - "primary": safe_css_color(yaml_section_value(fm, "colors", "primary", "#1F4FD8"), "#1F4FD8"), - "accent": safe_css_color(yaml_section_value(fm, "colors", "accent", "#7C3AED"), "#7C3AED"), - "surface": safe_css_color(yaml_section_value(fm, "colors", "surface", "#FFFFFF"), "#FFFFFF"), - "neutral": safe_css_color(yaml_section_value(fm, "colors", "neutral", "#F6F8FB"), "#F6F8FB"), - "text": safe_css_color(yaml_section_value(fm, "colors", "text", "#111827"), "#111827"), - "muted": safe_css_color(yaml_section_value(fm, "colors", "muted", "#6B7280"), "#6B7280"), - "border": safe_css_color(yaml_section_value(fm, "colors", "border", "#DDE3EA"), "#DDE3EA"), - } - return { - "colors": colors, - "type": { - "family": yaml_section_value(fm, "typography", "fontFamily", "Inter, ui-sans-serif, system-ui"), - "hero": "clamp(3rem, 8vw, 5.7rem)", - "heading": "clamp(2rem, 4vw, 3rem)", - "body": "1rem", - }, - "radius": {"sm": "8px", "md": "14px", "lg": "24px", "xl": "32px", "pill": "999px"}, - "spacing": {"xs": "4px", "sm": "8px", "md": "16px", "lg": "24px", "xl": "40px", "section": "96px"}, - "shadow": {"card": "0 24px 80px rgba(15,23,42,.08)", "cta": "0 18px 40px rgba(31,79,216,.22)"}, - "motion": {"duration": "180ms", "easing": "cubic-bezier(.2,.8,.2,1)", "reduced_motion": "preserve layout without parallax"}, - } - - -def variant_names(max_iterations: int, runtime_3d: bool) -> list[str]: - pool = ["asset-hero", "conversion-proof", "immersive-runtime" if runtime_3d else "editorial-system"] - return pool[:max_iterations] - - -def make_placeholder_svg(kind: str, tokens: dict[str, Any]) -> str: - c = tokens["colors"] - title = "Model Poster" if kind == "model" else "Scene Fallback" - label = "preview-only 3D poster" if kind == "model" else "preview-only runtime fallback" - shape = "" if kind == "model" else "" - return f""" - - - - {shape} - {html.escape(title)} - {html.escape(label)} - -""" - - -def build_asset_manifest(root: Path, show_dir: Path, iteration: str, variant: str, brief: dict[str, Any], references: dict[str, Any], tokens: dict[str, Any], asset_mode: str, runtime_3d: bool) -> dict[str, Any]: - assets_dir = show_dir / "assets" - placeholders_dir = assets_dir / "placeholders" - sources = selected_source_names(references) - assets: list[dict[str, Any]] = [] - for idx, source in enumerate(sources or ["artic-generated-visual-direction"], start=1): - assets.append({ - "id": f"catalog-reference-{idx}", - "path": None, - "type": "catalog-reference", - "status": "catalog-reference", - "retrieval": "catalog-reference", - "provenance": "selected Artic reference source", - "kind": "catalog-reference", - "title": source, - "source": source, - "license": "unknown", - "license_status": "unknown-unverified", - "usage": "preview-only visual direction and provenance record; do not copy protected marks or exact layouts", - "downloaded": False, - "local_path": None, - }) - hero_name = f"hero-{variant}.svg" - write_text(placeholders_dir / hero_name, make_placeholder_svg("scene", tokens)) - assets.append({ - "id": f"generated-{variant}-hero", - "path": f"assets/placeholders/{hero_name}", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": f"{variant} hero placeholder", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "safe first-draft hero artwork", - "downloaded": False, - "local_path": f"assets/placeholders/{hero_name}", - }) - if runtime_3d: - model_svg = make_placeholder_svg("model", tokens) - scene_svg = make_placeholder_svg("scene", tokens) - write_text(placeholders_dir / "model-poster.svg", model_svg) - write_text(placeholders_dir / "scene-fallback.svg", scene_svg) - write_text(assets_dir / "model-poster.svg", model_svg) - write_text(assets_dir / "scene-fallback.svg", scene_svg) - assets.extend([ - { - "id": "generated-model-poster", - "path": "assets/placeholders/model-poster.svg", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": "3D model poster placeholder", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "3D poster fallback before verified GLB/model asset is supplied", - "downloaded": False, - "local_path": "assets/placeholders/model-poster.svg", - }, - { - "id": "generated-model-poster-root-alias", - "path": "assets/model-poster.svg", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": "3D model poster placeholder root alias", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "backward-compatible 3D poster fallback alias", - "downloaded": False, - "local_path": "assets/model-poster.svg", - }, - { - "id": "generated-scene-fallback", - "path": "assets/placeholders/scene-fallback.svg", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": "3D scene fallback placeholder", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "runtime failure and reduced-motion scene fallback", - "downloaded": False, - "local_path": "assets/placeholders/scene-fallback.svg", - }, - { - "id": "generated-scene-fallback-root-alias", - "path": "assets/scene-fallback.svg", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": "3D scene fallback placeholder root alias", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "backward-compatible runtime failure fallback alias", - "downloaded": False, - "local_path": "assets/scene-fallback.svg", - }, - ]) - return { - "schema_version": 1, - "mode": "asset-first-preview", - "asset_mode": asset_mode, - "iteration": iteration, - "variant": variant, - "root": str(root.resolve()), - "created_at": datetime.now(timezone.utc).isoformat(), - "notes": ["Unknown or unverified sources are recorded as preview-only/catalog-reference and do not block preview generation."], - "assets": assets, - } - - -def render_css(tokens: dict[str, Any], variant: str, runtime_3d: bool) -> str: - c = tokens["colors"] - hero_background = { - "asset-hero": "radial-gradient(circle at 80% 10%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 34%), linear-gradient(180deg, var(--neutral), var(--surface))", - "conversion-proof": "linear-gradient(180deg, var(--surface), var(--neutral))", - "immersive-runtime": "radial-gradient(circle at 62% 20%, color-mix(in srgb, var(--accent) 24%, transparent), transparent 32%), radial-gradient(circle at 20% 20%, color-mix(in srgb, var(--primary) 18%, transparent), transparent 26%), var(--surface)", - "editorial-system": "linear-gradient(135deg, var(--neutral), var(--surface) 48%, color-mix(in srgb, var(--primary) 8%, var(--surface)))", - }.get(variant, "linear-gradient(180deg, var(--neutral), var(--surface))") - return f""":root {{ - --primary: {c['primary']}; --accent: {c['accent']}; --surface: {c['surface']}; --neutral: {c['neutral']}; --text: {c['text']}; --muted: {c['muted']}; --border: {c['border']}; - --radius-sm: {tokens['radius']['sm']}; --radius-md: {tokens['radius']['md']}; --radius-lg: {tokens['radius']['lg']}; --radius-xl: {tokens['radius']['xl']}; --shadow-card: {tokens['shadow']['card']}; --shadow-cta: {tokens['shadow']['cta']}; -}} -* {{ box-sizing: border-box; }} -html {{ scroll-behavior: smooth; }} -body {{ margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--text); background: {hero_background}; }} -a {{ color: inherit; }} -.page {{ width: min(1160px, calc(100% - 32px)); margin: 0 auto; padding: 28px 0 64px; }} -.nav {{ display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: clamp(36px, 8vw, 76px); }} -.brand {{ font-weight: 850; letter-spacing: -0.045em; font-size: 1.12rem; }} -.pill {{ display: inline-flex; align-items: center; border: 1px solid var(--border); border-radius: 999px; padding: 8px 12px; color: var(--muted); background: rgba(255,255,255,.72); backdrop-filter: blur(12px); }} -.hero {{ display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(280px, .95fr); gap: clamp(24px, 5vw, 56px); align-items: center; }} -h1 {{ font-size: clamp(3rem, 8vw, 5.9rem); line-height: .91; letter-spacing: -0.075em; margin: 18px 0; max-width: 10ch; }} -h2 {{ font-size: clamp(2rem, 4vw, 3rem); letter-spacing: -0.045em; margin: 0 0 16px; }} -p {{ color: var(--muted); line-height: 1.7; font-size: 1.04rem; }} -.actions {{ display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px; }} -.button {{ border: 0; border-radius: var(--radius-md); padding: 14px 18px; font-weight: 780; text-decoration: none; transition: transform 180ms cubic-bezier(.2,.8,.2,1), box-shadow 180ms cubic-bezier(.2,.8,.2,1); }} -.button:focus-visible {{ outline: 3px solid color-mix(in srgb, var(--accent) 45%, white); outline-offset: 3px; }} -.button:hover {{ transform: translateY(-1px); }} -.primary {{ background: var(--primary); color: white; box-shadow: var(--shadow-cta); }} -.secondary {{ color: var(--text); border: 1px solid var(--border); background: white; }} -.panel {{ border: 1px solid var(--border); background: rgba(255,255,255,.84); border-radius: var(--radius-xl); padding: clamp(20px, 4vw, 32px); box-shadow: var(--shadow-card); }} -.visual-card {{ position: relative; overflow: hidden; min-height: 440px; display: flex; flex-direction: column; justify-content: flex-end; }} -.visual-card img {{ width: 100%; border-radius: 24px; border: 1px solid var(--border); background: var(--neutral); margin-bottom: 18px; }} -.asset-caption {{ font-size: .9rem; color: var(--muted); }} -.metric-grid {{ display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; margin-top: 18px; }} -.metric {{ padding: 18px; border-radius: 20px; background: var(--neutral); }} -.metric strong {{ display: block; font-size: 1.35rem; color: var(--primary); }} -.sections {{ display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 16px; margin-top: 56px; }} -.card {{ border: 1px solid var(--border); border-radius: 24px; padding: 22px; background: white; }} -.card b {{ display: block; margin-bottom: 8px; }} -.source-list {{ padding-left: 20px; color: var(--muted); line-height: 1.8; }} -.model-viewer-stub {{ display: grid; place-items: center; min-height: 300px; margin: 18px 0; border-radius: 28px; border: 1px dashed var(--border); color: var(--muted); background: radial-gradient(circle at 50% 36%, rgba(124,58,237,.18), transparent 34%), linear-gradient(145deg, var(--neutral), var(--surface)); font-weight: 800; letter-spacing: .08em; }} -.interaction-zone {{ border: 1px solid var(--border); border-radius: 20px; padding: 16px; background: var(--neutral); }} -.runtime-3d {{ margin-top: 24px; }} -.readiness-note {{ margin: -28px 0 40px; border: 1px solid color-mix(in srgb, var(--accent), var(--border) 45%); border-left: 4px solid var(--accent); border-radius: 20px; padding: 18px 20px; background: rgba(124,58,237,.08); }} -.readiness-note strong {{ display: block; margin-bottom: 8px; }} -.readiness-note ul {{ margin: 8px 0 0; padding-left: 20px; color: var(--muted); line-height: 1.7; }} -.policy {{ margin-top: 56px; padding: 18px; border-left: 4px solid var(--accent); background: white; border-radius: 16px; color: var(--muted); }} -@media (max-width: 820px) {{ .hero, .sections {{ grid-template-columns: 1fr; }} .nav {{ align-items: flex-start; flex-direction: column; }} .visual-card {{ min-height: 0; }} h1 {{ max-width: 12ch; }} }} -@media (prefers-reduced-motion: reduce) {{ html {{ scroll-behavior: auto; }} .button {{ transition: none; }} .button:hover {{ transform: none; }} }} -""" - - -def render_html(root: Path, brief: dict[str, Any], references: dict[str, Any], strategy: dict[str, Any], design_text: str, variant: str, iteration: str, runtime_3d: bool, readiness: dict[str, Any] | None = None) -> str: - readiness = readiness or risk_readiness_summary(brief) - fm = frontmatter(design_text) - name = project_name(brief, fm) - description = yaml_value(fm, "description", "Artic-generated homepage preview") - raw_project = brief.get("project") - project = raw_project if isinstance(raw_project, dict) else {} - audience = ", ".join(str(item) for item in project.get("target_users", []) if item) or "the target audience" - goal = str(project.get("primary_goal") or "the primary conversion") - raw_style = brief.get("style") - style = raw_style if isinstance(raw_style, dict) else {} - north_star = markdown_section(design_text, "Design North Star") or str(style.get("design_north_star") or strategy.get("design_north_star") or "") - overview = markdown_section(design_text, "Overview") or description - composition = markdown_section(design_text, "Page Composition") or str(strategy.get("implementation_guidance") or "Hero, proof, features, trust, conversion, FAQ, and final CTA.") - sources = selected_source_names(references) - locale = brief_locale(brief) - copy = localized_copy(locale) - policy_text = POLICY_BY_LOCALE.get(locale, POLICY_FALLBACK) - source_items = "\n".join(f"
  • {html.escape(source)} preview-only/catalog-reference
  • " for source in sources[:5]) or "
  • Artic selected references preview-only/catalog-reference
  • " - boundary_items = "\n".join(f"
  • {html.escape(item)}
  • " for item in readiness.get("placeholder_boundaries", [])) - blocker_items = "\n".join(f"
  • {html.escape(item)}
  • " for item in readiness.get("implementation_blockers", [])) - if not readiness.get("production_ready", True): - if locale.startswith("ko"): - note_title = "프로덕션 준비 완료가 아닙니다" - note_body = "이 화면은 확인된 에셋과 구현 조건이 충족되기 전까지 플레이스홀더 미리보기로만 사용해야 합니다." - boundary_label = "플레이스홀더 경계" - blocker_label = "구현 차단 조건" - else: - note_title = "Not production-ready" - note_body = "Use this screen only as a placeholder preview until required assets and implementation conditions are satisfied." - boundary_label = "Placeholder boundaries" - blocker_label = "Implementation blockers" - boundary_section = f"
    {html.escape(boundary_label)}
      {boundary_items}
    " if boundary_items else "" - blocker_section = f"
    {html.escape(blocker_label)}
      {blocker_items}
    " if blocker_items else "" - readiness_note = f""" -
    - {html.escape(note_title)} -

    {html.escape(note_body)}

    - {boundary_section} - {blocker_section} -
    """ - else: - readiness_note = "" - runtime_block = "" - if runtime_3d: - runtime_block = f""" -
    - {html.escape(copy['model_label'])} - \"Preview-only -
    model-viewer · GLB · poster fallback
    -
    - {html.escape(copy['interaction_title'])} -

    {html.escape(copy['interaction_body'])}

    -
    -

    {html.escape(copy['model_body'])}

    -
    """ - generated_at = datetime.now(timezone.utc).isoformat() - variant_label = variant.replace("-", " ").title() - return f""" - - - - - - {html.escape(name)} — Artic Preview {html.escape(iteration)} - - - - -
    - -{readiness_note} -
    -
    - {html.escape(copy['for_label'])}: {html.escape(audience)} -

    {html.escape(name)} {html.escape(copy['headline_suffix'])}

    -

    {html.escape(overview)}

    - -
    - -
    -
    -
    {html.escape(copy['promise_title'])}

    {html.escape(copy['promise_body'])}

    -
    {html.escape(copy['proof_title'])}

    {html.escape(copy['proof_body'])}

    -
    {html.escape(copy['system_title'])}

    {html.escape(copy['system_body'])}

    -
    -{runtime_block} -
    -

    {html.escape(copy['reference_title'])}

    -

    {html.escape(copy['reference_body'])}

    -
      {source_items}
    -
    -
    - {html.escape(copy['policy_title'])}
    - {html.escape(policy_text)} -
    {html.escape(copy['generated'])}: {html.escape(generated_at)} · {html.escape(copy['root'])}: {html.escape(str(root))} · iteration {html.escape(iteration)} -
    -
    - - -""" - - -def score_variant(variant: str, idx: int, runtime_3d: bool, sources_count: int) -> dict[str, Any]: - base = { - "asset-hero": 90, - "conversion-proof": 81, - "immersive-runtime": 86 if runtime_3d else 78, - "editorial-system": 82, - }.get(variant, 80) - richness = min(92, 74 + sources_count * 4 + (8 if runtime_3d else 0) + (4 if variant in ("asset-hero", "immersive-runtime") else 0)) - provenance = min(95, 80 + sources_count * 3 + (5 if runtime_3d else 0)) - generic_penalty = max(4, 13 - idx * 2 - (3 if variant in ("asset-hero", "immersive-runtime") else 0)) - dimensions: dict[str, float] = { - "strategy_alignment": float(min(94, base + 3)), - "visual_fidelity": float(min(92, base + (2 if variant != "conversion-proof" else 0))), - "asset_richness": float(richness), - "asset_provenance_completeness": float(provenance), - "conversion_clarity": float(min(94, base + (6 if variant == "conversion-proof" else 2))), - "visual_specificity": float(min(93, base + (5 if variant in ("asset-hero", "immersive-runtime") else 1))), - "accessibility_basics": 86.0, - "mobile_first": 88.0, - "genericness_penalty": float(generic_penalty), - } - positive = [v for k, v in dimensions.items() if k != "genericness_penalty"] - dimensions["overall"] = round((sum(positive) / len(positive)) - (generic_penalty * 0.35), 1) - return dimensions - - -def render_critique(reports: list[dict[str, Any]], selected: dict[str, Any]) -> str: - lines = ["# Artic Show Critique", "", "Asset-first visual draft review. Unknown asset sources are retained as preview-only/catalog-reference instead of blocking generation.", ""] - for report in reports: - scores = report["scores"] - lines.extend([ - f"## Iteration {report['iteration']} — {report['variant']}", - "", - f"- Overall: {scores['overall']}", - f"- Strength: {report['strength']}", - f"- Risk: {report['risk']}", - f"- Asset provenance: {scores['asset_provenance_completeness']} / asset richness: {scores['asset_richness']}", - "", - ]) - lines.extend([ - "## Selected", "", f"Iteration {selected['iteration']} (`{selected['variant']}`) was promoted to `.artic/show/` root.", "", "## App source impact", "", "`modified_app_files` is intentionally `[]`; this command only writes preview bundle artifacts under `.artic/show`.", "", - ]) - return "\n".join(lines) - - -def copy_iteration_to_root(iter_dir: Path, show_root: Path) -> None: - for filename in ("index.html", "styles.css", "tokens.json", "report.json"): - shutil.copy2(iter_dir / filename, show_root / filename) - dst_assets = show_root / "assets" - if dst_assets.exists(): - shutil.rmtree(dst_assets) - shutil.copytree(iter_dir / "assets", dst_assets) - - -def rel(root: Path, path: Path) -> str: - try: - return str(path.resolve().relative_to(root.resolve())) - except ValueError: - return str(path) - - -def create_show_preview(root: Path, max_iterations: int = 3, min_score: float = 75, strict: bool = False, asset_mode: str = "asset-first", asset_timeout: float = 10) -> dict[str, Any]: - del asset_timeout # reserved for future downloader; core implementation is local/offline-safe. - missing = required_missing(root) - if missing: - raise ValueError("missing required input(s) for @artic show: " + ", ".join(missing)) - if not 1 <= max_iterations <= 3: - raise ValueError("--max-iterations must be between 1 and 3") - design_text = (root / "DESIGN.md").read_text(encoding="utf-8") - brief = read_json(root / ".artic" / "brief.json") - references = read_json(root / ".artic" / "references.json") - strategy = read_json(root / ".artic" / "strategy.json") - readiness = risk_readiness_summary(brief) - show_root = root / ".artic" / "show" - iterations_root = show_root / "iterations" - show_root.mkdir(parents=True, exist_ok=True) - if iterations_root.exists(): - shutil.rmtree(iterations_root) - iterations_root.mkdir(parents=True, exist_ok=True) - - tokens = build_tokens(design_text) - runtime_3d = has_3d_runtime_reference(brief, references) - variants = variant_names(max_iterations, runtime_3d) - reports: list[dict[str, Any]] = [] - generated_preview_files: list[str] = [] - asset_files: list[str] = [] - - for idx, variant in enumerate(variants, start=1): - iteration = f"{idx:03d}" - iter_dir = iterations_root / iteration - if iter_dir.exists(): - shutil.rmtree(iter_dir) - (iter_dir / "assets").mkdir(parents=True, exist_ok=True) - manifest = build_asset_manifest(root, iter_dir, iteration, variant, brief, references, tokens, asset_mode, runtime_3d) - scores = score_variant(variant, idx, runtime_3d, len(selected_source_names(references))) - report = { - "schema_version": 1, - "mode": "asset-first-preview", - "iteration": iteration, - "variant": variant, - "created_at": datetime.now(timezone.utc).isoformat(), - "scores": scores, - "score_dimensions": SCORE_KEYS, - "strength": "Uses local generated placeholders plus catalog-reference provenance to make the first draft visually concrete.", - "risk": "Preview assets may need replacement with owned or license-verified production assets before implementation.", - "modified_app_files": [], - "asset_manifest": "assets/manifest.json", - **readiness, - } - write_json(iter_dir / "tokens.json", tokens) - write_text(iter_dir / "styles.css", render_css(tokens, variant, runtime_3d)) - write_text(iter_dir / "index.html", render_html(root, brief, references, strategy, design_text, variant, iteration, runtime_3d, readiness)) - write_json(iter_dir / "assets" / "manifest.json", manifest) - write_json(iter_dir / "report.json", report) - reports.append(report) - generated_preview_files.extend([rel(root, iter_dir / name) for name in ("index.html", "styles.css", "tokens.json", "report.json", "assets/manifest.json")]) - for asset in manifest.get("assets", []): - local_path = asset.get("local_path") - if local_path: - asset_files.append(rel(root, iter_dir / str(local_path))) - - selected_report = max(reports, key=lambda item: float(item["scores"]["overall"])) - selected_iteration = str(selected_report["iteration"]) - selected_dir = iterations_root / selected_iteration - copy_iteration_to_root(selected_dir, show_root) - root_asset_manifest = read_json(show_root / "assets" / "manifest.json") - asset_summary = { - "assets_used": len(root_asset_manifest.get("assets", [])), - "verified_assets": sum(1 for asset in root_asset_manifest.get("assets", []) if asset.get("status") == "verified"), - "unverified_preview_only_assets": sum(1 for asset in root_asset_manifest.get("assets", []) if asset.get("status") == "unverified-preview-only"), - "generated_placeholders": sum(1 for asset in root_asset_manifest.get("assets", []) if asset.get("status") == "generated"), - "catalog_references": sum(1 for asset in root_asset_manifest.get("assets", []) if asset.get("status") == "catalog-reference"), - } - status = "selected" - if float(selected_report["scores"]["overall"]) < min_score: - status = "below-threshold" - root_report = { - "schema_version": 1, - "mode": "asset-first-preview", - "status": status, - "generated_at": datetime.now(timezone.utc).isoformat(), - "selected_iteration": selected_iteration, - "candidate": selected_report["variant"], - "selected_candidate_id": selected_report["variant"], - "threshold": min_score, - "preview_bundle": str(show_root), - "entrypoint": str(show_root / "index.html"), - "scores": selected_report["scores"], - "asset_summary": asset_summary, - "integrity": { - "modified_app_files": [], - }, - "modified_app_files": [], - "iterations": reports, - "remaining_risks": ["Preview assets may need replacement with owned or license-verified production assets before apply."], - **readiness, - } - write_json(show_root / "report.json", root_report) - selected_payload = { - "schema_version": 1, - "selected_iteration": selected_iteration, - "candidate": selected_report["variant"], - "selected_candidate_id": selected_report["variant"], - "variant": selected_report["variant"], - "overall": selected_report["scores"]["overall"], - "entrypoint": ".artic/show/index.html", - "reason": "Highest overall asset-first preview score among generated candidates.", - } - write_json(show_root / "selected.json", selected_payload) - write_text(show_root / "critique.md", render_critique(reports, selected_report)) - - asset_files.extend(rel(root, show_root / str(asset["local_path"])) for asset in root_asset_manifest.get("assets", []) if asset.get("local_path")) - generated_preview_files.extend([rel(root, show_root / name) for name in ("index.html", "styles.css", "tokens.json", "report.json", "selected.json", "critique.md", "assets/manifest.json")]) - if strict and status == "below-threshold": - raise ValueError(f"quality threshold failure: selected preview score {selected_report['scores']['overall']} is below --min-score {min_score}") - - payload = { - "root": str(root.resolve()), - "preview_bundle": str(show_root), - "bundle_dir": str(show_root), - "entrypoint": str(show_root / "index.html"), - "preview_file": str(show_root / "index.html"), - "selected_iteration": selected_iteration, - "generated_preview_files": sorted(str(root / item) for item in dict.fromkeys(generated_preview_files)), - "asset_files": sorted(str(root / item) for item in dict.fromkeys(asset_files)) + [str(show_root / "assets" / "manifest.json")], - "asset_manifest": str(show_root / "assets" / "manifest.json"), - "report_file": str(show_root / "report.json"), - "critique_file": str(show_root / "critique.md"), - "modified_app_files": [], - **readiness, - } - return payload - - -def main() -> int: - parser = argparse.ArgumentParser(description="Render an asset-first static Artic homepage preview bundle from start-generated design docs.") - parser.add_argument("--root", required=True, help="Project root containing DESIGN.md and .artic outputs from @artic start") - parser.add_argument("--max-iterations", type=int, default=3, help="Number of preview candidates to generate (1..3)") - parser.add_argument("--min-score", type=float, default=75, help="Minimum selected score when --strict is enabled") - parser.add_argument("--strict", action="store_true", help="Fail if selected iteration is below --min-score") - parser.add_argument("--asset-mode", choices=["asset-first", "offline", "no-download"], default="asset-first", help="Asset handling mode; core show uses local placeholders/catalog provenance") - parser.add_argument("--asset-timeout", type=float, default=10, help="Reserved timeout for future asset fetchers") - args = parser.parse_args() - try: - payload = create_show_preview(Path(args.root), args.max_iterations, args.min_score, args.strict, args.asset_mode, args.asset_timeout) - except ValueError as exc: - print(json.dumps({"error": str(exc)}, ensure_ascii=False)) - return 1 - print(json.dumps(payload, indent=2, ensure_ascii=False)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/plugins/claude-artic/skills/artic/scripts/artic_start.py b/plugins/claude-artic/skills/artic/scripts/artic_start.py index 63e32e8..e4cb2bb 100644 --- a/plugins/claude-artic/skills/artic/scripts/artic_start.py +++ b/plugins/claude-artic/skills/artic/scripts/artic_start.py @@ -133,123 +133,6 @@ def as_markdown(value: Any, *, indent: int = 0) -> str: return str(value) -RISK_LABELS = { - "en": { - "heading": "Risk / Readiness Summary", - "summary_line": "Risk Summary: risk/quality readiness details follow.", - "core_quality_requirements": "Core quality requirements", - "known_missing_information": "Known missing information", - "safe_assumptions": "Safe assumptions", - "unsafe_assumptions": "Unsafe assumptions", - "placeholder_fallback_boundary": "Placeholder/fallback boundary", - "implementation_stop_conditions": "Implementation stop conditions / Stop Conditions", - "completion_acceptance_criteria": "Completion/acceptance criteria", - "status_ready": "Strategy/preview can proceed.", - "status_blocked": "Strategy/preview can proceed, but implementation is blocked until missing inputs are resolved.", - "none": "None declared.", - "check_intent": "requested product photo/3D/map/payment trust intent is satisfied or explicitly blocked", - "check_placeholder": "placeholder is not accepted as production substitute for quality-critical requirement", - }, - "ko": { - "heading": "위험/준비 상태 요약", - "summary_line": "위험 요약: 리스크와 품질 기준에 따른 준비 상태를 정리합니다.", - "core_quality_requirements": "핵심 품질 요구사항", - "known_missing_information": "알려진 누락 정보", - "safe_assumptions": "안전한 가정", - "unsafe_assumptions": "위험한 가정", - "placeholder_fallback_boundary": "플레이스홀더/대체 경계", - "implementation_stop_conditions": "구현 중단 조건", - "completion_acceptance_criteria": "완료/수용 기준", - "status_ready": "전략/프리뷰는 진행할 수 있습니다.", - "status_blocked": "전략/프리뷰는 진행할 수 있지만, 누락된 정보가 해결될 때까지 구현은 차단됩니다.", - "none": "명시된 항목 없음.", - "check_intent": "요청된 제품 사진/3D/지도/결제 신뢰 의도가 충족되었거나 명시적으로 차단됨", - "check_placeholder": "플레이스홀더는 품질 핵심 요구사항의 프로덕션 대체물로 인정되지 않음", - }, -} - - -RISK_SECTION_KEYS = [ - "core_quality_requirements", - "known_missing_information", - "safe_assumptions", - "unsafe_assumptions", - "placeholder_fallback_boundary", - "implementation_stop_conditions", - "completion_acceptance_criteria", -] - - -def risk_readiness(brief: dict[str, Any]) -> dict[str, Any]: - value = brief.get("risk_readiness") - return value if isinstance(value, dict) and value else {} - - -def risk_labels(brief: dict[str, Any]) -> dict[str, str]: - locale = str(brief_language(brief).get("locale") or "en-US") - return RISK_LABELS["ko"] if locale.startswith("ko") else RISK_LABELS["en"] - - -def risk_items(risk: dict[str, Any], key: str) -> list[str]: - aliases = { - "core_quality_requirements": ["quality_critical_requirements", "core_requirements"], - "known_missing_information": ["missing_information", "missing_inputs", "blockers"], - "placeholder_fallback_boundary": ["placeholder_boundary", "fallback_boundary"], - "implementation_stop_conditions": ["stop_conditions", "implementation_blockers"], - "completion_acceptance_criteria": ["acceptance_criteria", "completion_criteria"], - } - raw = risk.get(key) - if raw is None: - for alias in aliases.get(key, []): - raw = risk.get(alias) - if raw is not None: - break - if raw is None or raw == "": - return [] - if isinstance(raw, list): - items: list[str] = [] - for item in raw: - rendered = as_markdown(item).strip() - if rendered: - items.append(rendered) - return items - return [as_markdown(raw).strip()] - - -def risk_readiness_block(brief: dict[str, Any], *, checklist: bool = False) -> str: - risk = risk_readiness(brief) - if not risk: - return "" - labels = risk_labels(brief) - implementation_blocked = bool(risk.get("implementation_blocked")) or risk.get("ready_for_implementation") is False - readiness = risk.get("readiness") - if isinstance(readiness, dict) and str(readiness.get("implementation") or "").lower() == "blocked": - implementation_blocked = True - lines = [f"## {labels['heading']}", "", f"- {labels['summary_line']}", f"- {labels['status_blocked'] if implementation_blocked else labels['status_ready']}"] - if "ready_for_strategy" in risk: - lines.append(f"- ready_for_strategy: {bool(risk.get('ready_for_strategy'))}") - if "implementation_blocked" in risk: - lines.append(f"- implementation_blocked: {bool(risk.get('implementation_blocked'))}") - lines.append("") - for key in RISK_SECTION_KEYS: - lines.extend([f"### {labels[key]}", ""]) - items = risk_items(risk, key) - if items: - prefix = "- [ ]" if checklist else "-" - lines.extend(f"{prefix} {item}" for item in items) - else: - lines.append(f"- {labels['none']}") - lines.append("") - if checklist: - lines.extend([ - "### Intent-matched QA gates", - "", - f"- [ ] {labels['check_intent']}.", - f"- [ ] {labels['check_placeholder']}.", - "", - ]) - return "\n".join(lines).rstrip() - def role_lines(strategy: dict[str, Any]) -> list[str]: roles = strategy.get("reference_roles", []) @@ -273,7 +156,6 @@ def role_lines(strategy: dict[str, Any]) -> list[str]: def strategy_doc(brief: dict[str, Any], strategy: dict[str, Any]) -> str: policy = policy_block(brief) - risk_block = risk_readiness_block(brief) parts = [ f"# Artic Strategy: {project_name(brief)}", "", @@ -282,8 +164,6 @@ def strategy_doc(brief: dict[str, Any], strategy: dict[str, Any]) -> str: policy, "", ] - if risk_block: - parts.extend([risk_block, ""]) parts.extend([ "## Project Summary", "", @@ -417,8 +297,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] description = f"{project_description(brief, strategy)} Artic-generated homepage design system." policy = policy_block(brief) language = language_block(brief) - risk_block = risk_readiness_block(brief) - risk_checklist_block = risk_readiness_block(brief, checklist=True) strategy_markdown = strategy_doc(brief, strategy) design = load_template("DESIGN.template.md") @@ -443,8 +321,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] design = design.replace("Recommended homepage sequence: hero with one primary promise, proof immediately near the hero, feature/job sections, trust or comparison section, conversion area, FAQ, and final CTA.", as_markdown(strategy.get("implementation_guidance", "")) or "Recommended homepage sequence: hero, proof, feature sections, trust, conversion area, FAQ, and final CTA.") design = design.replace("Do not use generic gradient blobs, random glassmorphism, off-token colors, multiple primary CTAs in one viewport, low-contrast muted copy, centered long paragraphs, or exact reference layouts.", "Do not copy " + ", ".join(str(item) for item in strategy.get("forbidden_copy_elements", FORBIDDEN_COPY_ELEMENTS)) + ". " + as_markdown(strategy.get("conflict_resolution", ""))) design = replace_policy_text(design, policy) - if risk_block: - design += f"\n\n{risk_block}\n" write(root / "DESIGN.md", design) write(root / "docs" / "artic-strategy.md", strategy_markdown) @@ -452,8 +328,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] rules = rules.replace("{{PROJECT_NAME}}", name) rules = rules.replace("{{REFERENCE_SYNTHESIS}}", strategy_markdown) rules = replace_policy_text(rules, policy) - if risk_block: - rules += f"\n\n{risk_block}\n" write(root / "docs" / "design-rules.md", rules) checklist = load_template("design-qa-checklist.template.md") @@ -461,8 +335,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] checklist = replace_policy_text(checklist, policy) if "Accessibility" not in checklist: checklist = checklist.replace("- [ ] Text contrast targets WCAG AA.", "- [ ] Accessibility: text contrast targets WCAG AA, focus states are visible, controls are semantic, and forms are labeled.") - if risk_checklist_block: - checklist += f"\n\n{risk_checklist_block}\n" checklist += "\n## Strategy Gates\n\n- [ ] Visual hierarchy follows the strategy north star.\n- [ ] Brand coherence follows the visual_system field.\n- [ ] Conversion clarity follows the conversion_strategy field.\n- [ ] Mobile quality follows implementation_guidance.\n- [ ] Accessibility follows the accessibility field.\n- [ ] Reference safety forbids logos, trademarks, proprietary illustrations, exact layouts, and source copywriting.\n" write(root / "docs" / "design-qa-checklist.md", checklist) @@ -471,8 +343,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] prompt = prompt.replace("# Homepage Implementation Prompt\n", f"# Homepage Implementation Prompt\n\n{language}\n") prompt = prompt.replace("Rules:\n", f"Strategy source: `docs/artic-strategy.md`.\n\nRules:\n") prompt = replace_policy_text(prompt, policy) - if risk_block: - prompt += f"\n\n{risk_block}\n" write(root / "docs" / "homepage-design-prompt.md", prompt) update_state(root, brief) @@ -523,7 +393,7 @@ def update_state(root: Path, brief: dict[str, Any]) -> None: state = loaded except json.JSONDecodeError: state = {} - state.update({"artic_version": str(brief.get("artic_version") or state.get("artic_version") or "0.4.1"), "last_generated_at": datetime.now(timezone.utc).isoformat(), "status": "generated", "language": brief_language(brief), "strategy_path": ".artic/strategy.json"}) + state.update({"artic_version": str(brief.get("artic_version") or state.get("artic_version") or "0.5.0"), "last_generated_at": datetime.now(timezone.utc).isoformat(), "status": "generated", "language": brief_language(brief), "strategy_path": ".artic/strategy.json"}) write(state_path, json.dumps(state, indent=2, ensure_ascii=False) + "\n") @@ -591,7 +461,7 @@ def create_start_outputs(root: Path, *, no_validate: bool = False) -> dict[str, raise ValueError("cannot run @artic start before init is ready" + (f": missing {missing}" if missing else "") + (f". Next questions: {questions}" if questions else "")) if not strategy_path.exists() and (status == "ready" or not brief_path.exists() or not references_path.exists()): answers = session.get("answers") if isinstance(session.get("answers"), dict) else {} - prompt_brief = {"answers": answers, "language": session.get("language", {}), "risk_readiness": session.get("risk_readiness", {})} + prompt_brief = {"answers": answers, "language": session.get("language", {})} prompt_references = {"selected_sources": [], "source_plan": [], "note": "@artic start requires the agent-authored strategy before finalizing init outputs."} prompt_path = write_strategy_prompt(root, prompt_brief, prompt_references, None) raise ValueError(json.dumps({ diff --git a/plugins/claude-artic/skills/artic/scripts/risk_readiness.py b/plugins/claude-artic/skills/artic/scripts/risk_readiness.py deleted file mode 100644 index e057ef8..0000000 --- a/plugins/claude-artic/skills/artic/scripts/risk_readiness.py +++ /dev/null @@ -1,335 +0,0 @@ -#!/usr/bin/env python3 -"""Artic risk/readiness contract helpers. - -The functions in this module are intentionally pure and deterministic: no IO, no -network access, and no clock reads. They turn early Artic answers/intent into a -machine-readable readiness contract that distinguishes strategy/preview from -final implementation readiness. -""" -from __future__ import annotations - -from typing import Any - -SCHEMA_VERSION = "artic-risk-readiness-v1" - -CORE_FIELDS = ("project", "audience", "goal", "vibe") - -_CATEGORY_KEYWORDS: dict[str, tuple[str, ...]] = { - "core_visual_asset_dependency": ( - "image", "photo", "product photo", "3d model", "video", "illustration", "person", "place", - "제품 사진", "이미지", "3d", "석고상", "영상", "일러스트", "사진", - ), - "interaction_dependency": ( - "drag", "edit", "game", "map", "realtime preview", "real-time preview", "canvas", "webgl", "rotate", "zoom", "interactive", "interaction", - "마우스로", "드래그", "편집", "지도", "실시간", "캔버스", "상호작용", "회전", "줌", "만질", - ), - "brand_dependency": ( - "logo", "colors", "fonts", "brand system", "reference brands", - "로고", "색상", "폰트", "브랜드", "toss", "토스", "kakao", "카카오", "naver", "네이버", - ), - "technical_runtime_dependency": ( - "shopify", "webgl", "cms", "animation runtime", - ), - "legal_license_dependency": ( - "external assets", "external asset", "fonts", "data", "music", "video", "model", "trademark", "license", - "라이선스", "외부 에셋", "상표", - ), - "accessibility_performance": ( - "motion-heavy", "media-heavy", "mobile-first", "data-heavy", "fast mobile", "reduced motion", "webgl", "3d", - "모바일에서 빠르게", "접근성", "성능", "애니메이션", - ), - "conversion_business": ( - "payment", "booking", "lead", "contact", "download", "demo", - "결제", "예약", "문의", "리드", "다운로드", "데모", - ), -} - -_FIELD_BY_CATEGORY: dict[str, tuple[str, ...]] = { - "core_visual_asset_dependency": ("asset_source",), - "interaction_dependency": ("interaction_model",), - "brand_dependency": ("brand_assets",), - "technical_runtime_dependency": ("technical_runtime",), - "legal_license_dependency": ("license_clearance",), - "accessibility_performance": ("performance_accessibility_plan",), -} - -_CONVERSION_HARD_KEYWORDS = ("payment", "booking", "checkout", "purchase", "lead form", "lead collection", "contact form", "download", "결제", "예약", "리드", "다운로드") - -_QUALITY_CRITICAL_PATTERNS = ( - ("product photo", "Real product photography is a quality-critical visual requirement."), - ("제품 사진", "실제 제품 사진은 품질 핵심 시각 요구사항입니다."), - ("actual product", "Actual product imagery is a quality-critical visual requirement."), - ("실제 제품", "실제 제품 이미지는 품질 핵심 시각 요구사항입니다."), - ("real gallery", "A real gallery-like visual asset is quality-critical."), - ("실제 갤러리", "실제 갤러리 같은 시각 자산은 품질 핵심 요구사항입니다."), - ("plaster statue", "The plaster statue subject is a quality-critical core asset."), - ("석고상", "The 3D plaster-cast subject is a quality-critical core asset."), -) - -_PLACEHOLDER_BOUNDARY = ( - "Placeholders may be used to communicate layout or interaction intent in strategy and preview, " - "but implementation cannot use a placeholder as the final substitute for core requirements." -) - -_SUBSTITUTE_STOP = ( - "Substitute stop rule: stop before implementation if a missing core asset, interaction model, " - "license clearance, brand asset, runtime decision, or performance/accessibility plan would be replaced by a generic substitute." -) - - -def _flatten_text(value: Any) -> str: - if value is None: - return "" - if isinstance(value, dict): - parts: list[str] = [] - for key in sorted(value): - parts.append(_flatten_text(value[key])) - return " ".join(parts) - if isinstance(value, (list, tuple, set)): - return " ".join(_flatten_text(item) for item in value) - return str(value) - - -def _has_any(text: str, keywords: tuple[str, ...]) -> bool: - lowered = text.lower() - return any(keyword.lower() in lowered for keyword in keywords) - - -def _present(value: Any) -> bool: - if value is None: - return False - if isinstance(value, str): - return bool(value.strip()) - if isinstance(value, (list, tuple, set, dict)): - return bool(value) - return True - - -def _detect_categories(answers: dict[str, Any], intent: dict[str, Any] | None = None) -> list[str]: - text = _flatten_text({"answers": answers or {}, "intent": intent or {}}) - categories = [category for category, words in _CATEGORY_KEYWORDS.items() if _has_any(text, words)] - - # Avoid making a simple low-risk SaaS demo CTA implementation-blocking while - # still detecting explicitly requested business mechanics. - if "conversion_business" in categories and not _has_any(text, _CONVERSION_HARD_KEYWORDS): - categories = [category for category in categories if category != "conversion_business"] - return categories - - -def _quality_critical_requirements(text: str) -> list[dict[str, str]]: - lowered = text.lower() - requirements: list[dict[str, str]] = [] - seen: set[str] = set() - for pattern, label in _QUALITY_CRITICAL_PATTERNS: - if pattern.lower() in lowered and label not in seen: - requirements.append({ - "requirement": label, - "source_signal": pattern, - "rule": "quality_critical_requirement", - "placeholder_policy": "preview_only", - "completion_criterion": "Final implementation must use an approved/source-confirmed asset or explicitly revised requirement, not a generic substitute.", - }) - seen.add(label) - if ("photo" in lowered or "사진" in lowered) and ("core" in lowered or "핵심" in lowered) and not requirements: - requirements.append({ - "requirement": "Core photography is quality-critical to the requested experience.", - "source_signal": "core photo", - "rule": "quality_critical_requirement", - "placeholder_policy": "preview_only", - "completion_criterion": "Final implementation must use approved photography or an explicit requirement change.", - }) - return requirements - - -def _has_placeholder_substitute_answer(answers: dict[str, Any], fields: list[str]) -> bool: - substitute_markers = ( - "placeholder", "generic", "gradient", "stock", "dummy", "mock", "temporary", "temp", - "대체", "임시", "플레이스홀더", "더미", "일반", "제네릭", - ) - fields_to_check = {"asset_source", "license_clearance", "brand_assets"} - for field in fields_to_check: - value = answers.get(field) - if not _present(value): - continue - lowered = _flatten_text(value).lower() - if any(marker in lowered for marker in substitute_markers): - return True - return False - - -def dynamic_required_fields(payload: dict[str, Any]) -> list[str]: - """Return dynamic required field names for a risk payload or partial contract.""" - categories = payload.get("risk_categories") or [] - fields: list[str] = [] - for category in categories: - for field in _FIELD_BY_CATEGORY.get(str(category), ()): # conversion handled below - if field not in fields: - fields.append(field) - - text = _flatten_text(payload) - if "conversion_business" in categories and _has_any(text, _CONVERSION_HARD_KEYWORDS): - fields.append("conversion_path") - - if payload.get("quality_critical_requirements") and "asset_source" not in fields: - fields.insert(0, "asset_source") - - return fields - - -def _completion_criteria(answers: dict[str, Any], fields: list[str], qcr: list[dict[str, str]]) -> list[str]: - goal = str(answers.get("goal") or "the stated goal").strip() or "the stated goal" - criteria = [ - f"The final experience must directly support: {goal}.", - "Core project, audience, goal, and vibe are reflected in content hierarchy and visual direction.", - ] - for field in fields: - criteria.append(f"Dynamic requirement '{field}' is answered and traceable in the final implementation.") - for requirement in qcr: - criteria.append(requirement["completion_criterion"]) - criteria.append("No placeholder remains as a final substitute for a quality-critical or core requirement.") - return criteria - - -def render_risk_summary(payload: dict[str, Any], locale: str = "en-US") -> str: - categories = payload.get("risk_categories") or [] - missing = payload.get("missing_dynamic_required_fields") or [] - readiness = payload.get("readiness") or {} - if locale.lower().startswith("ko"): - return ( - f"위험 범주: {', '.join(categories) if categories else '없음'}\n" - f"누락된 동적 필수 항목: {', '.join(missing) if missing else '없음'}\n" - f"전략: {readiness.get('strategy', 'unknown')} / 미리보기: {readiness.get('preview', 'unknown')} / " - f"구현: {readiness.get('implementation', 'unknown')}\n" - f"경계: {payload.get('placeholder_boundary', _PLACEHOLDER_BOUNDARY)}" - ) - return ( - f"Risk categories: {', '.join(categories) if categories else 'none'}\n" - f"Missing dynamic required fields: {', '.join(missing) if missing else 'none'}\n" - f"Readiness: strategy={readiness.get('strategy', 'unknown')}, " - f"preview={readiness.get('preview', 'unknown')}, implementation={readiness.get('implementation', 'unknown')}\n" - f"Boundary: {payload.get('placeholder_boundary', _PLACEHOLDER_BOUNDARY)}" - ) - - -def analyze_risk_readiness(answers: dict[str, Any], intent: dict[str, Any] | None = None) -> dict[str, Any]: - answers = dict(answers or {}) - intent = dict(intent or {}) if intent else None - combined_text = _flatten_text(answers) - risk_categories = _detect_categories(answers, None) - qcr = _quality_critical_requirements(combined_text) - - partial = {"risk_categories": risk_categories, "quality_critical_requirements": qcr, "answers": answers, "intent": intent or {}} - required = dynamic_required_fields(partial) - missing = [field for field in required if not _present(answers.get(field)) and not _present((intent or {}).get(field))] - placeholder_substitute = bool(qcr) and _has_placeholder_substitute_answer(answers, required) - - core_missing = [field for field in CORE_FIELDS if not _present(answers.get(field))] - if core_missing: - strategy = "blocked" - preview = "blocked" - implementation = "blocked" - status = "core_fields_missing" - elif missing or placeholder_substitute: - strategy = "ready" - preview = "ready_with_placeholders" - implementation = "blocked" - status = "implementation_blocked" - elif risk_categories or qcr: - strategy = "ready" - preview = "ready" - implementation = "ready_with_assumptions" - status = "ready_with_assumptions" - else: - strategy = "ready" - preview = "ready" - implementation = "ready" - status = "ready" - - safe_assumptions = [ - "Strategy may proceed from stated project, audience, goal, and vibe." if not core_missing else "No strategy assumption is safe until core fields are answered.", - ] - if missing or placeholder_substitute: - safe_assumptions.append("Preview may use clearly labeled placeholders only to show structure, scale, or interaction intent.") - elif risk_categories or qcr: - safe_assumptions.append("Implementation may proceed only if detected dependencies are satisfied by supplied answers or explicit approvals.") - - unsafe_assumptions = [ - "Do not treat a generic substitute as equivalent to a missing quality-critical requirement.", - "Do not replace required product photos or core visual assets with a generic gradient in final implementation.", - ] - if missing or placeholder_substitute: - unsafe_assumptions.append("Do not implement final production UI by substituting missing dynamic requirements with invented assets, behaviors, licenses, or runtime choices.") - if core_missing: - unsafe_assumptions.append("Do not infer missing core fields without user confirmation.") - - stop_conditions: list[str] = [] - if core_missing: - stop_conditions.append(f"Stop: core fields missing: {', '.join(core_missing)}.") - if missing: - stop_conditions.append(f"Stop before implementation: missing dynamic required fields: {', '.join(missing)}.") - stop_conditions.append(_SUBSTITUTE_STOP) - if placeholder_substitute: - stop_conditions.append("Stop before implementation: placeholder or generic substitute was supplied for a quality-critical requirement.") - stop_conditions.append(_SUBSTITUTE_STOP) - if qcr: - stop_conditions.append("Stop before final implementation if quality-critical assets are unavailable, unlicensed, or replaced by placeholders.") - - payload: dict[str, Any] = { - "schema_version": SCHEMA_VERSION, - "risk_categories": risk_categories, - "quality_critical_requirements": qcr, - "dynamic_required_fields": required, - "missing_dynamic_required_fields": missing, - "readiness": { - "strategy": strategy, - "preview": preview, - "implementation": implementation, - "status": status, - }, - "safe_assumptions": safe_assumptions, - "unsafe_assumptions": unsafe_assumptions, - "placeholder_boundary": _PLACEHOLDER_BOUNDARY, - "stop_conditions": stop_conditions, - "risk_summary": "", - "completion_criteria": _completion_criteria(answers, required, qcr), - } - blockers = list(stop_conditions) - if "brand_dependency" in risk_categories: - payload["unsafe_assumptions"].append( - "Treat named brands such as Toss/토스 as inspiration only; do not clone or copy their palette, logo, layout, or product flow." - ) - payload["completion_criteria"].append( - "Brand references are translated into original project-specific constraints without clone/copy behavior." - ) - if "conversion_business" in risk_categories: - payload["completion_criteria"].append( - "Conversion success is judged by trust, information structure, recovery states, and the stated business outcome, not by merely rendering a form." - ) - if "accessibility_performance" in risk_categories: - payload["completion_criteria"].append( - "Mobile or motion/media-heavy experiences define performance, accessibility, reduced motion, and fallback requirements before implementation." - ) - - # Compatibility aliases for init/session, show, and broad integration tests. - payload["level"] = "high" if status in {"implementation_blocked", "core_fields_missing"} else ("medium" if risk_categories or qcr else "low") - payload["risk_level"] = payload["level"] - payload["ready_for_strategy"] = strategy == "ready" - payload["ready_for_preview"] = True if preview == "ready" else ("placeholder" if preview == "ready_with_placeholders" else False) - payload["ready_for_implementation"] = implementation in {"ready", "ready_with_assumptions"} - payload["blockers"] = blockers - payload["required_fields"] = list(required) - payload["questions"] = [ - {"id": field, "field": field, "reason": "risk/quality driven follow-up required"} - for field in required - ] - payload["implementation_blocked"] = implementation == "blocked" - payload["placeholder_fallback_boundary"] = [_PLACEHOLDER_BOUNDARY] - payload["implementation_stop_conditions"] = stop_conditions - payload["core_quality_requirements"] = [str(item.get("requirement", "")) for item in qcr if isinstance(item, dict)] - payload["known_missing_information"] = missing - payload["completion_acceptance_criteria"] = payload["completion_criteria"] - payload["risk_summary"] = render_risk_summary(payload, locale=str(answers.get("locale") or "en-US")) - return payload - - -__all__ = ["analyze_risk_readiness", "render_risk_summary", "dynamic_required_fields"] diff --git a/plugins/claude-artic/skills/artic/scripts/scaffold_artic_files.py b/plugins/claude-artic/skills/artic/scripts/scaffold_artic_files.py index ddec398..dae725e 100755 --- a/plugins/claude-artic/skills/artic/scripts/scaffold_artic_files.py +++ b/plugins/claude-artic/skills/artic/scripts/scaffold_artic_files.py @@ -100,7 +100,7 @@ def scaffold(root: Path, project_name: str, locale: str = "en-US") -> None: "forbidden_copy_elements": ["logos", "trademarks", "proprietary illustrations", "exact layouts", "source copywriting"], } brief = { - "artic_version": "0.4.1", + "artic_version": "0.5.0", "project": { "name": project_name, "type": "homepage", @@ -139,7 +139,7 @@ def scaffold(root: Path, project_name: str, locale: str = "en-US") -> None: ], "synthesis": "Clean SaaS hierarchy with token discipline and mobile-first accessibility.", } - state = {"artic_version": "0.4.1", "last_generated_at": now, "status": "scaffolded", "language": language, "intent_path": ".artic/intent.json", "strategy_path": ".artic/strategy.json"} + state = {"artic_version": "0.5.0", "last_generated_at": now, "status": "scaffolded", "language": language, "intent_path": ".artic/intent.json", "strategy_path": ".artic/strategy.json"} write(root / ".artic" / "intent.json", json.dumps(intent, indent=2, ensure_ascii=False) + "\n") write(root / ".artic" / "strategy.json", json.dumps(strategy, indent=2, ensure_ascii=False) + "\n") write(root / ".artic" / "brief.json", json.dumps(brief, indent=2, ensure_ascii=False) + "\n") diff --git a/plugins/claude-artic/skills/artic/scripts/validate_artic_outputs.py b/plugins/claude-artic/skills/artic/scripts/validate_artic_outputs.py index 74da914..699aaaf 100755 --- a/plugins/claude-artic/skills/artic/scripts/validate_artic_outputs.py +++ b/plugins/claude-artic/skills/artic/scripts/validate_artic_outputs.py @@ -20,27 +20,6 @@ REQUIRED_SPACING_TOKENS = ("xs", "sm", "md", "lg", "xl", "section") REQUIRED_COMPONENT_TOKENS = ("button-primary", "button-secondary", "card", "form-field", "proof-strip") REQUIRED_QA_TERMS = ("Visual hierarchy", "Brand coherence", "Conversion clarity", "Mobile quality", "Accessibility", "Reference safety") -RISK_SECTIONS_EN = ( - "Risk / Readiness Summary", - "Core quality requirements", - "Known missing information", - "Safe assumptions", - "Unsafe assumptions", - "Placeholder/fallback boundary", - "Implementation stop conditions", - "Completion/acceptance criteria", -) -RISK_SECTIONS_KO = ( - "위험/준비 상태 요약", - "핵심 품질 요구사항", - "알려진 누락 정보", - "안전한 가정", - "위험한 가정", - "플레이스홀더/대체 경계", - "구현 중단 조건", - "완료/수용 기준", -) -RISK_DOCS = ("DESIGN.md", "docs/artic-strategy.md", "docs/design-rules.md", "docs/design-qa-checklist.md", "docs/homepage-design-prompt.md") POLICY_FRAGMENT = "extract reusable principles only" POLICY_MARKER = "" LANGUAGE_MARKER_PREFIX = "" not in text: errors.append(f"ERROR: localized outputs missing language marker: {brief_locale} in {rel}") - if declared_risk_readiness: - required_risk_sections = RISK_SECTIONS_KO if brief_locale.startswith("ko") else RISK_SECTIONS_EN - for rel in RISK_DOCS: - path = root / rel - if not path.exists(): - continue - text = path.read_text(encoding="utf-8") - for section in required_risk_sections: - if section not in text: - errors.append(f"ERROR: {rel} missing risk_readiness section: {section}") - if risk_implementation_blocked: - blocked_terms = [ - "implementation is blocked until missing inputs are resolved", - "누락된 정보가 해결될 때까지 구현은 차단됩니다", - ] - if not any(term in text for term in blocked_terms): - errors.append(f"ERROR: {rel} missing risk_readiness implementation block notice") checklist = root / "docs" / "design-qa-checklist.md" if checklist.exists(): text = checklist.read_text(encoding="utf-8") diff --git a/plugins/claude-artic/skills/artic/templates/brief.schema.json b/plugins/claude-artic/skills/artic/templates/brief.schema.json index 4a6ce89..4b93137 100644 --- a/plugins/claude-artic/skills/artic/templates/brief.schema.json +++ b/plugins/claude-artic/skills/artic/templates/brief.schema.json @@ -17,27 +17,6 @@ "requirements": {"type": "object"}, "constraints": {"type": "object"}, "asset_policy": {"type": "object"}, - "risk_readiness": { - "type": "object", - "required": ["schema_version", "risk_categories", "quality_critical_requirements", "dynamic_required_fields", "missing_dynamic_required_fields", "readiness", "safe_assumptions", "unsafe_assumptions", "placeholder_boundary", "stop_conditions", "risk_summary", "completion_criteria"], - "properties": { - "schema_version": {"const": "artic-risk-readiness-v1"}, - "risk_categories": {"type": "array", "items": {"type": "string"}}, - "quality_critical_requirements": {"type": "array"}, - "dynamic_required_fields": {"type": "array", "items": {"type": "string"}}, - "missing_dynamic_required_fields": {"type": "array", "items": {"type": "string"}}, - "readiness": { - "type": "object", - "required": ["strategy", "preview", "implementation", "status"] - }, - "safe_assumptions": {"type": "array", "items": {"type": "string"}}, - "unsafe_assumptions": {"type": "array", "items": {"type": "string"}}, - "placeholder_boundary": {"type": "string"}, - "stop_conditions": {"type": "array", "items": {"type": "string"}}, - "risk_summary": {"type": "string"}, - "completion_criteria": {"type": "array", "items": {"type": "string"}} - } - }, "implementation": {"type": "object"}, "language": { "type": "object", diff --git a/plugins/claude-artic/skills/artic/templates/strategy.schema.json b/plugins/claude-artic/skills/artic/templates/strategy.schema.json index 650be4f..08711ad 100644 --- a/plugins/claude-artic/skills/artic/templates/strategy.schema.json +++ b/plugins/claude-artic/skills/artic/templates/strategy.schema.json @@ -54,27 +54,6 @@ "forbidden_copy_elements": { "type": "array", "contains": {"const": "logos"} - }, - "risk_readiness": { - "type": "object", - "required": ["schema_version", "risk_categories", "quality_critical_requirements", "dynamic_required_fields", "missing_dynamic_required_fields", "readiness", "safe_assumptions", "unsafe_assumptions", "placeholder_boundary", "stop_conditions", "risk_summary", "completion_criteria"], - "properties": { - "schema_version": {"const": "artic-risk-readiness-v1"}, - "risk_categories": {"type": "array", "items": {"type": "string"}}, - "quality_critical_requirements": {"type": "array"}, - "dynamic_required_fields": {"type": "array", "items": {"type": "string"}}, - "missing_dynamic_required_fields": {"type": "array", "items": {"type": "string"}}, - "readiness": { - "type": "object", - "required": ["strategy", "preview", "implementation", "status"] - }, - "safe_assumptions": {"type": "array", "items": {"type": "string"}}, - "unsafe_assumptions": {"type": "array", "items": {"type": "string"}}, - "placeholder_boundary": {"type": "string"}, - "stop_conditions": {"type": "array", "items": {"type": "string"}}, - "risk_summary": {"type": "string"}, - "completion_criteria": {"type": "array", "items": {"type": "string"}} - } } }, "additionalProperties": true diff --git a/plugins/codex-artic/.codex-plugin/plugin.json b/plugins/codex-artic/.codex-plugin/plugin.json index 6f6745e..f6f54d4 100644 --- a/plugins/codex-artic/.codex-plugin/plugin.json +++ b/plugins/codex-artic/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codex-artic", - "version": "0.4.1", + "version": "0.5.0", "description": "Artic design onboarding skill for Codex.", "skills": [ { diff --git a/plugins/codex-artic/skills/artic/SKILL.md b/plugins/codex-artic/skills/artic/SKILL.md index 1181f69..ef459ad 100644 --- a/plugins/codex-artic/skills/artic/SKILL.md +++ b/plugins/codex-artic/skills/artic/SKILL.md @@ -1,7 +1,7 @@ --- name: artic -description: "Use when creating or improving a homepage/website and design rules are missing or weak. Artic is an agent design-direction protocol: @artic init interviews the user, @artic start authors strategy artifacts, then the compiler produces AI-native DESIGN.md docs without copying protected brand assets." -version: 0.4.1 +description: "Use when creating or improving a homepage/website and design rules are missing or weak. Artic is an agent design-direction protocol: @artic init interviews the user, @artic start authors strategy artifacts, then the compiler produces AI-native DESIGN.md docs from reusable reference principles." +version: 0.5.0 author: Hermes Agent license: MIT metadata: @@ -12,7 +12,7 @@ metadata: # Artic -Artic is an agent design-direction protocol for AI-built websites: a contract-bound LLM design director that turns reference-informed judgment into implementation-ready design artifacts. +Artic is a reference-informed design-documentation protocol for AI-built websites: a contract-bound LLM design director that turns project intent and reusable reference principles into implementation-ready design artifacts before coding. Core flow: @@ -21,24 +21,16 @@ Core flow: → collect project/design intent → normalize into search facets → search professional and open-source design references -→ select and combine the best patterns +→ preserve a draft intake session @artic start → author `.artic/strategy.json` and `docs/artic-strategy.md` as the design-direction contract → run the compiler against that strategy → generate DESIGN.md and implementation guidance → validate outputs - -@artic show -→ render the strategy artifacts and sourced/generated assets into a provenance-recorded asset-first visual draft bundle -→ write `.artic/show/` preview files without modifying app source files - -@artic review -→ compare implementation against `.artic/strategy.json`, `docs/artic-strategy.md`, and `DESIGN.md` -→ report token, hierarchy, spacing, accessibility, CTA, mobile, and reference-safety drift ``` -Artic is not a generic design prompt or passive generator. Its core value is searching multiple professional/OSS design resources, extracting compatible patterns, and binding one project-specific AI-native design direction into a strategy contract. Scripts are validator/compiler/renderer helpers; they do not make the design judgment for the agent. +Artic is not a generic design prompt or passive generator. Its core value is searching multiple professional/OSS design resources, extracting compatible patterns, and binding one project-specific AI-native design direction into a strategy contract. Scripts are validator/compiler helpers; they do not make the design judgment for the agent. ## When to Use @@ -49,7 +41,7 @@ Use Artic when: - The user provides reference sites/images and wants the design improved. - The user wants AI-native docs before implementation. -Do not use Artic for exact cloning of a brand/site or copying logos, trademarks, proprietary illustrations, exact layouts, copywriting, or animations. +Do not use Artic for exact cloning of a brand/site or copying logos, trademarks, proprietary illustrations, exact layouts, copywriting, or animations. Use references to extract reusable design principles, then create original project-specific direction. ## Commands @@ -71,7 +63,7 @@ Language behavior: - When `@artic start` compiles the ready session, carry the language contract into `.artic/brief.json.language`, `.artic/state.json.language`, and generated docs using ``. - Preserve machine-readable terms such as `DESIGN.md`, `AI-native`, `Artic`, source names, and design token keys. - Localize user-facing interview questions and prose according to the language contract. -- Validate localized reference safety with the invariant `` marker, not exact English copy. +- Validate localized reference principle policy with the invariant `` marker, not exact English copy. Default interview questions: 1. What product/service is this homepage for? @@ -86,14 +78,11 @@ Default interview questions: 10. Tech stack? 11. Accessibility target? Default: WCAG AA. 12. Existing docs that must be reflected? -13. May Artic search/use owned or clearly licensed public assets, or should external sources stay as reference principles only? -External reference vs external asset boundary: +Reference boundary: - External references are design/runtime sources used for reusable principles, interaction patterns, accessibility/performance constraints, token relationships, and implementation guidance. -- External assets are concrete media that can appear in the generated site: GLB files, images, models, icons, textures, fonts, illustrations, or similar files. -- `@artic start` still treats external sources as reference-principles unless the user explicitly allows asset usage. -- `@artic show` may use sourced or generated assets for the preview bundle when doing so improves the first visual draft, but every asset must be recorded in `.artic/show/assets/manifest.json` and `.artic/show/report.json` with source/generation notes, license status when known, and preview-only status when unverified. -- Unverified assets are allowed only inside the preview bundle as provenance-recorded, preview-only material. Do not claim production license clearance; production/apply review belongs to a future `@artic apply` flow or a separate implementation review. +- Artic treats external sources as reference-principles for strategy and documentation, not as concrete assets to copy into the user's site. +- Use reference guidance as constructive creative direction: translate patterns into original tokens, hierarchy, components, motion, and information architecture. Fast path: if user says `@artic init quick`, ask only product, audience, goal, vibe, and references. @@ -142,31 +131,6 @@ Required behavior: Lifecycle transition rule: `@artic start` is the only transition that may finalize a ready init session. If `.artic/init-session.json` is `collecting`, stop and ask the remaining questions; if it is `ready`, finalize it and then generate docs. -### `@artic show` - -Purpose: render the current Artic strategy into a provenance-recorded, asset-first first visual draft bundle. The goal is a stronger strategy-grounded first visual draft by using sourced/generated assets when helpful, while recording where each asset came from and whether it is preview-only. - -Executable path for agents/hosts that expose shell-backed commands: - -```bash -python3 /scripts/artic_show.py --root -``` - -Required behavior: -1. Require `DESIGN.md`, `docs/homepage-design-prompt.md`, `.artic/brief.json`, `.artic/references.json`, and `.artic/strategy.json` from a completed `@artic start` flow. -2. Generate a strategy-grounded first visual draft bundle under `.artic/show/`, not just a minimal static preview. -3. Prefer asset-first draft quality: use user-provided, sourced, or generated images/models/icons/textures when they improve the first visual draft, and fall back to styled placeholders only when assets are unavailable or inappropriate. -4. Record provenance for every preview asset in `.artic/show/assets/manifest.json` and summarize asset decisions, gaps, and warnings in `.artic/show/report.json`. -5. Mark unverified assets as preview-only/provenance-recorded. Do not state or imply production license clearance for unverified sourced/generated assets. -6. Do not modify app/source files. Downstream files such as `app/page.tsx`, `src/App.tsx`, or `pages/index.tsx` are out of scope for `@artic show`; a future `@artic apply` or separate implementation review would handle production/apply decisions later. -7. Include the reference-safety policy in the preview so the preview remains reference-informed, not reference-copied. -8. If selected reference roles include `3d_runtime`, render a runtime-aware preview structure such as a `model-viewer`/Three.js placeholder, central interaction zone, poster/reduced-motion fallback notes, and asset provenance/license reminders instead of a generic landing-page-only preview. -9. Return JSON with at least `preview_file`, `bundle_dir`, `asset_manifest`, `report_file`, and `modified_app_files` for verification. - -### `@artic review` MVP-light - -After implementation, compare the current homepage against `.artic/strategy.json`, `docs/artic-strategy.md`, and `DESIGN.md`. Check token consistency, typography hierarchy, spacing rhythm, CTA hierarchy, mobile behavior, accessibility basics, and no-copy reference safety. The MVP review output contract may be chat-first; future hosts may persist `.artic/review.json` and `docs/artic-review.md`, but do not claim those files exist unless the review workflow writes them. - ### `@artic version` Purpose: report the installed Artic package versions and compare them with the latest GitHub release. @@ -201,7 +165,7 @@ Artic catalog entries are user-facing design intelligence, not an internal audit - Prefer wording such as “Use this for…”, “Apply its…”, “Translate… into project-specific…”, “Pair with…”, and “Keep identity, copy, and layout decisions original by…”. - Keep each entry source-specific; do not flatten the catalog into repeated boilerplate. -Use `application_guidance` for this product-facing catalog copy. Do not add `risk_notes` or other defensive/internal field names for new catalog work. +Use `application_guidance` for this product-facing catalog copy. Keep catalog guidance positive, specific, and application-oriented. Good pattern: ```text @@ -229,16 +193,16 @@ Synthesis rule: - Resolve conflicts based on project goal. - Preserve rationale in `docs/artic-brief.md`. -## Reference Safety +## Reference Principle Policy Allowed: token structure, color role relationships, typography hierarchy, spacing systems, component behavior, accessibility rules, motion principles. -Forbidden unless user owns rights: logos, trademarks, proprietary illustrations, exact page composition, exact brand palette as identity, copywriting, copyrighted imagery. +Not part of Artic's output: logos, trademarks, proprietary illustrations, exact page composition, exact brand palette as identity, copywriting, copyrighted imagery. Required phrase in generated docs: ```text -Reference policy: extract reusable principles only; do not copy logos, trademarks, proprietary illustrations, or exact layouts. +Reference policy: extract reusable principles only; create original project-specific direction. ``` ## Output Contract @@ -280,21 +244,6 @@ docs/design-qa-checklist.md docs/homepage-design-prompt.md ``` -`@artic show` creates a provenance-recorded preview bundle: - -```text -.artic/show/index.html -.artic/show/styles.css -.artic/show/tokens.json -.artic/show/assets/manifest.json -.artic/show/report.json -.artic/show/critique.md -.artic/show/selected.json -.artic/show/iterations//... -``` - -Show bundles are preview-only and do not modify app source files. Unverified sourced/generated assets may be included for first-draft visual quality only when marked preview-only and provenance-recorded; production/apply clearance is out of scope for `@artic show` and belongs to future `@artic apply`/implementation review. - Optional exports: `tailwind.theme.json`, `tokens.json`. ## Presets @@ -309,7 +258,7 @@ python3 /scripts/validate_artic_outputs.py --root python3 /scripts/search_reference_catalog.py --query "ai product developer saas premium" --limit 3 ``` -Validation, compilation, and rendering scripts enforce and materialize the contract. They are not a design judgment source; `@artic start` must provide that judgment through `.artic/strategy.json`. +Validation and compilation scripts enforce and materialize the contract. They are not a design judgment source; `@artic start` must provide that judgment through `.artic/strategy.json`. ## Common Pitfalls diff --git a/plugins/codex-artic/skills/artic/scripts/artic_init.py b/plugins/codex-artic/skills/artic/scripts/artic_init.py index 13f61ee..6433ec0 100644 --- a/plugins/codex-artic/skills/artic/scripts/artic_init.py +++ b/plugins/codex-artic/skills/artic/scripts/artic_init.py @@ -8,7 +8,6 @@ from pathlib import Path from design_intent_mapper import map_design_intent -from risk_readiness import analyze_risk_readiness from search_reference_catalog import load_catalog, search, terms POLICY_MARKER = "" @@ -242,32 +241,6 @@ def create_init_outputs(root: Path, args: argparse.Namespace) -> dict: requirements = parse_key_values(getattr(args, "requirement", [])) constraints = parse_key_values(getattr(args, "constraint", [])) policy = asset_policy_payload(str(getattr(args, "asset_policy", ""))) - risk_answers = { - "project": args.project, - "audience": args.audience, - "goal": args.goal, - "vibe": args.vibe, - "references": args.references, - "stack": args.stack, - "accessibility": args.accessibility, - "asset_policy": str(getattr(args, "asset_policy", "")), - **requirements, - **constraints, - } - risk_answers["locale"] = args.locale - if risk_answers.get("stack") and not risk_answers.get("technical_runtime"): - risk_answers["technical_runtime"] = risk_answers["stack"] - elif not risk_answers.get("technical_runtime"): - vibe_text = str(risk_answers.get("vibe") or "") - if re.search(r"runtime|런타임|webgl|model-viewer|3d", vibe_text, re.IGNORECASE): - risk_answers["technical_runtime"] = vibe_text - if risk_answers.get("asset_policy") and not risk_answers.get("license_clearance"): - risk_answers["license_clearance"] = risk_answers["asset_policy"] - interaction_answer = str(risk_answers.get("interaction_model") or "") - if interaction_answer and not risk_answers.get("performance_accessibility_plan"): - if re.search(r"reduced motion|reduced-motion|keyboard|키보드|대체|fallback|alt|접근성|성능|load|loading", interaction_answer, re.IGNORECASE): - risk_answers["performance_accessibility_plan"] = interaction_answer - risk_readiness = analyze_risk_readiness(risk_answers, intent) source_plan = [ { "source_id": src["id"], @@ -280,7 +253,7 @@ def create_init_outputs(root: Path, args: argparse.Namespace) -> dict: ] brief = { - "artic_version": "0.4.1", + "artic_version": "0.5.0", "project": { "name": normalized_project["name"], "type": "homepage", @@ -303,7 +276,6 @@ def create_init_outputs(root: Path, args: argparse.Namespace) -> dict: "constraints": constraints, "asset_policy": policy, "implementation": {"stack": args.stack or "unspecified", "mobile_first": "mobile" in args.vibe.lower(), "accessibility": args.accessibility}, - "risk_readiness": risk_readiness, "language": lang, "copy_policy": "artic-policy: reference-safety-v1", } @@ -314,7 +286,7 @@ def create_init_outputs(root: Path, args: argparse.Namespace) -> dict: "source_plan": source_plan, "synthesis": "Use selected sources as compatible patterns; localize prose according to the brief language contract while preserving source names and protected terms.", } - state = {"artic_version": "0.4.1", "last_generated_at": now, "status": "initialized", "language": lang, "intent_path": ".artic/intent.json"} + state = {"artic_version": "0.5.0", "last_generated_at": now, "status": "initialized", "language": lang, "intent_path": ".artic/intent.json"} write(root / ".artic" / "intent.json", json.dumps(intent, indent=2, ensure_ascii=False) + "\n") write(root / ".artic" / "brief.json", json.dumps(brief, indent=2, ensure_ascii=False) + "\n") diff --git a/plugins/codex-artic/skills/artic/scripts/artic_init_session.py b/plugins/codex-artic/skills/artic/scripts/artic_init_session.py index 9007921..4691a94 100644 --- a/plugins/codex-artic/skills/artic/scripts/artic_init_session.py +++ b/plugins/codex-artic/skills/artic/scripts/artic_init_session.py @@ -8,22 +8,12 @@ from typing import Any from locale_contract import detect_locale_from_text, language_contract -from risk_readiness import analyze_risk_readiness as analyze_risk_readiness_contract SESSION_PATH = Path(".artic") / "init-session.json" REQUIRED_FIELDS = ["project", "audience", "goal", "vibe"] -OPTIONAL_FIELDS = ["references", "stack", "avoid", "accessibility", "asset_policy"] -DYNAMIC_REQUIRED_FIELDS = [ - "asset_source", - "interaction_model", - "brand_constraints", - "conversion_details", - "performance_accessibility", - "asset_policy", - "license_policy", -] +OPTIONAL_FIELDS = ["references", "stack", "avoid", "accessibility"] CORE_ANSWER_FIELDS = set(REQUIRED_FIELDS + OPTIONAL_FIELDS) -CONSTRAINT_FIELD_HINTS = ("constraint", "constraints", "brand", "avoid", "asset_policy", "license_policy") +CONSTRAINT_FIELD_HINTS = ("constraint", "constraints", "brand", "avoid") QUESTION_SPECS: dict[str, dict[str, str]] = { "project": { @@ -66,69 +56,11 @@ "zh-CN": "目标技术栈是什么?", "zh-TW": "目標技術棧是什麼?", }, - "asset_policy": { - "intent": "Clarify whether external assets may be used or only referenced as principles.", - "en-US": "May Artic search/use owned or clearly licensed public assets, or should external sources stay as reference principles only?", - "ko-KR": "소유한 에셋 또는 라이선스 확인 가능한 공개 에셋을 검색/사용해도 되나요, 아니면 외부 소스는 원칙 참고로만 사용할까요?", - "ja-JP": "所有アセットまたはライセンス確認済みの公開アセットを検索/使用してよいですか、それとも外部ソースは原則の参考のみにしますか?", - "zh-CN": "是否可以搜索/使用自有资产或可验证授权的公开资产,还是外部来源仅作为原则参考?", - "zh-TW": "是否可以搜尋/使用自有資產或可驗證授權的公開資產,或外部來源只作為原則參考?", - }, - "asset_source": { - "intent": "Identify source and availability of required visual/3D assets.", - "en-US": "What is the source for required images, video, or 3D assets (owned files, generated placeholders, or licensed public assets)?", - "ko-KR": "필요한 이미지/영상/3D 에셋은 어디서 오나요? 자체 보유 파일, 임시 플레이스홀더, 라이선스 확인 가능한 공개 에셋 중 무엇인가요?", - }, - "interaction_model": { - "intent": "Clarify implementation-critical interaction behavior.", - "en-US": "How should key interactions work, including mouse/touch/keyboard behavior and fallbacks?", - "ko-KR": "핵심 상호작용은 어떻게 동작해야 하나요? 마우스/터치/키보드 조작과 대체 경험까지 알려주세요.", - }, - "brand_constraints": { - "intent": "Capture brand/legal constraints that affect design execution.", - "en-US": "What brand, legal, tone, color, logo, or copy constraints must the design follow?", - "ko-KR": "디자인이 따라야 할 브랜드/법무/톤/색상/로고/문구 제약은 무엇인가요?", - }, - "brand_assets": { - "intent": "Capture brand assets and brand-system constraints.", - "en-US": "What logo, color, font, or brand-system assets must the design follow?", - "ko-KR": "디자인이 따라야 할 로고, 색상, 폰트, 브랜드 시스템 에셋은 무엇인가요?", - }, - "conversion_details": { - "intent": "Clarify conversion path details.", - "en-US": "What exact CTA, form fields, destination, or success criteria should the conversion flow use?", - "ko-KR": "전환 흐름의 정확한 CTA, 폼 필드, 이동 경로, 성공 기준은 무엇인가요?", - }, - "conversion_path": { - "intent": "Clarify conversion path details.", - "en-US": "What exact CTA, form fields, destination, trust requirements, or success criteria should the conversion flow use?", - "ko-KR": "전환 흐름의 정확한 CTA, 폼 필드, 이동 경로, 신뢰 조건, 성공 기준은 무엇인가요?", - }, - "performance_accessibility": { - "intent": "Clarify performance and accessibility constraints for risky media or interaction.", - "en-US": "What performance and accessibility requirements apply (load budget, reduced motion, keyboard support, alt/fallback content)?", - "ko-KR": "성능/접근성 요구는 무엇인가요? 로딩 예산, reduced motion, 키보드 지원, 대체 콘텐츠를 알려주세요.", - }, - "performance_accessibility_plan": { - "intent": "Clarify performance and accessibility constraints for risky media or interaction.", - "en-US": "What performance and accessibility plan applies (load budget, reduced motion, keyboard support, alt/fallback content)?", - "ko-KR": "성능/접근성 계획은 무엇인가요? 로딩 예산, reduced motion, 키보드 지원, 대체 콘텐츠를 알려주세요.", - }, - "license_policy": { - "intent": "Clarify licensing and attribution obligations.", - "en-US": "What license and attribution policy should Artic follow for any third-party assets or references?", - "ko-KR": "제3자 에셋/레퍼런스의 라이선스와 출처 표기는 어떤 정책을 따라야 하나요?", - }, - "license_clearance": { - "intent": "Clarify licensing and attribution obligations.", - "en-US": "What license clearance and attribution policy should Artic follow for any third-party assets or references?", - "ko-KR": "제3자 에셋/레퍼런스의 라이선스 확인과 출처 표기는 어떤 정책을 따라야 하나요?", - }, } _STYLE_HINT_RE = re.compile(r"토스|신뢰|고급|깔끔|스타트업|모바일|saas|premium|trust|clean|mobile|startup", re.IGNORECASE) _LABELED_ANSWER_RE = re.compile( - r"(?:^|[.;\n]\s*)(project|product|service|audience|target user|target users|goal|conversion goal|vibe|impression|style|stack|tech stack|references?|avoid|accessibility|asset policy)\s*:\s*(.*?)(?=(?:[.;\n]\s*)(?:project|product|service|audience|target user|target users|goal|conversion goal|vibe|impression|style|stack|tech stack|references?|avoid|accessibility|asset policy)\s*:|$)", + r"(?:^|[.;\n]\s*)(project|product|service|audience|target user|target users|goal|conversion goal|vibe|impression|style|stack|tech stack|references?|avoid|accessibility)\s*:\s*(.*?)(?=(?:[.;\n]\s*)(?:project|product|service|audience|target user|target users|goal|conversion goal|vibe|impression|style|stack|tech stack|references?|avoid|accessibility)\s*:|$)", re.IGNORECASE | re.DOTALL, ) _LABELED_FIELD_MAP = { @@ -209,18 +141,6 @@ def _answer_corpus(answers: dict[str, str]) -> str: return "\n".join(str(value) for value in answers.values() if value).lower() -def analyze_risk_readiness(answers: dict[str, str], missing_core: list[str]) -> dict[str, Any]: - """Return the canonical Artic risk/readiness contract for dynamic init follow-ups.""" - payload = analyze_risk_readiness_contract(answers) - if missing_core: - payload["missing_core_fields"] = list(missing_core) - payload["readiness"] = {"strategy": "blocked", "preview": "blocked", "implementation": "blocked", "status": "core_fields_missing"} - payload["ready_for_strategy"] = False - payload["ready_for_preview"] = False - payload["ready_for_implementation"] = False - payload["implementation_blocked"] = True - return payload - def create_or_update_session(root: Path, user_text: str, explicit_locale: str | None = None, answers: dict[str, str] | None = None) -> dict[str, Any]: existing: dict[str, Any] @@ -246,36 +166,13 @@ def create_or_update_session(root: Path, user_text: str, explicit_locale: str | bilingual_terms=bool(previous_language.get("bilingual_terms", False)), ) missing = missing_required_fields(merged_answers) - risk_answers = dict(merged_answers) - if risk_answers.get("stack") and not risk_answers.get("technical_runtime"): - risk_answers["technical_runtime"] = risk_answers["stack"] - elif not risk_answers.get("technical_runtime"): - vibe_text = str(risk_answers.get("vibe") or "") - if re.search(r"runtime|런타임|webgl|model-viewer|3d", vibe_text, re.IGNORECASE): - risk_answers["technical_runtime"] = vibe_text - if risk_answers.get("asset_policy") and not risk_answers.get("license_clearance"): - risk_answers["license_clearance"] = risk_answers["asset_policy"] - interaction_answer = str(risk_answers.get("interaction_model") or "") - if interaction_answer and not risk_answers.get("performance_accessibility_plan"): - if re.search(r"reduced motion|reduced-motion|keyboard|키보드|대체|fallback|alt|접근성|성능|load|loading", interaction_answer, re.IGNORECASE): - risk_answers["performance_accessibility_plan"] = interaction_answer - risk_readiness = analyze_risk_readiness(risk_answers, missing) - canonical_missing_dynamic = [str(field) for field in risk_readiness.get("missing_dynamic_required_fields", [])] - missing_dynamic = list(canonical_missing_dynamic) - if "license_clearance" in canonical_missing_dynamic and "asset_policy" not in missing_dynamic: - missing_dynamic.append("asset_policy") - if "asset_source" in canonical_missing_dynamic and not merged_answers.get("asset_policy") and "asset_policy" not in missing_dynamic: - missing_dynamic.append("asset_policy") - last_question_ids = (missing + missing_dynamic)[:6] + last_question_ids = missing[:6] session = { "schema_version": 1, "status": "ready" if not missing else "collecting", "language": lang, "answers": merged_answers, "missing": missing, - "risk_readiness": risk_readiness, - "missing_dynamic_required_fields": missing_dynamic, - "readiness": {key: risk_readiness["readiness"].get(key) for key in ("strategy", "preview", "implementation")}, "last_question_ids": last_question_ids, } write_session(root, session) @@ -291,7 +188,6 @@ def render_questions(session: dict[str, Any], limit: int = 4) -> list[str]: field_ids = [str(field) for field in raw_ids] else: field_ids = [str(field) for field in session.get("missing", [])] - field_ids.extend(str(field) for field in session.get("missing_dynamic_required_fields", []) if str(field) not in field_ids) questions: list[str] = [] for field in field_ids[:limit]: spec = QUESTION_SPECS.get(str(field), {}) @@ -333,13 +229,6 @@ def render_ready_summary(session: dict[str, Any]) -> str: raw_answers = session.get("answers") answers: dict[str, Any] = raw_answers if isinstance(raw_answers, dict) else {} locale = str(language.get("locale") or "en-US") - raw_risk = session.get("risk_readiness") - risk: dict[str, Any] = raw_risk if isinstance(raw_risk, dict) else {} - raw_readiness = session.get("readiness") - readiness: dict[str, Any] = raw_readiness if isinstance(raw_readiness, dict) else {} - risk_level = str(risk.get("risk_level") or "low") - missing_dynamic = [str(field) for field in session.get("missing_dynamic_required_fields", [])] - signal_names = [str(row.get("signal")) for row in risk.get("signals", []) if isinstance(row, dict) and row.get("signal")] if locale.startswith("ko"): lines = [ "필수 정보는 충분히 모였습니다.", @@ -350,27 +239,9 @@ def render_ready_summary(session: dict[str, Any]) -> str: f"- 목표: {answers.get('goal', '')}", f"- 무드: {answers.get('vibe', '')}", "", - f"리스크 요약: {risk_level}" - + (f" ({', '.join(signal_names[:4])})" if signal_names else ""), - ] - if missing_dynamic: - lines.extend([ - f"구현 차단: 전략 문서는 시작할 수 있지만 실제 구현은 추가 확인이 필요합니다: {', '.join(missing_dynamic)}.", - "플레이스홀더/원칙 참고 경계는 유지되며, 에셋 사용을 명시적으로 허용하지 않으면 외부 소스는 원칙 참고로만 사용합니다.", - ]) - elif readiness.get("implementation") == "blocked": - lines.extend([ - "구현 차단: 핵심 품질 요구가 플레이스홀더나 낮은 품질 대체물에 의존하고 있어 구현 전에 해결해야 합니다.", - "플레이스홀더/원칙 참고 경계는 유지되며, 필요한 입력을 검증할 때까지 프로덕션 구현으로 진행하지 않습니다.", - ]) - elif readiness.get("implementation") == "ready_with_assumptions": - lines.append("구현 준비: 진행 가능하지만 성능/접근성 세부값은 보수적 기본값으로 가정합니다.") - lines.extend([ - "", - "더 다듬고 싶으면 레퍼런스, 피해야 할 스타일, 브랜드/에셋 제약을 추가로 알려주세요.", - "에셋 사용을 명시적으로 허용하지 않으면 외부 소스는 원칙 참고로만 사용합니다.", + "더 다듬고 싶으면 레퍼런스, 피해야 할 스타일, 브랜드 제약을 추가로 알려주세요.", "문서 생성을 시작하려면 `@artic start`를 실행하세요.", - ]) + ] else: lines = [ "The required Artic intake is ready.", @@ -381,27 +252,9 @@ def render_ready_summary(session: dict[str, Any]) -> str: f"- Goal: {answers.get('goal', '')}", f"- Vibe: {answers.get('vibe', '')}", "", - f"Risk summary: {risk_level}" - + (f" ({', '.join(signal_names[:4])})" if signal_names else ""), - ] - if missing_dynamic: - lines.extend([ - f"Implementation blocker: strategy docs may proceed, but implementation needs follow-up on: {', '.join(missing_dynamic)}.", - "Placeholder/reference boundaries remain active; external assets stay reference-principles only unless explicitly allowed.", - ]) - elif readiness.get("implementation") == "blocked": - lines.extend([ - "Implementation blocker: a quality-critical requirement depends on a placeholder or lower-quality substitute.", - "Placeholder/reference boundaries remain active; do not proceed to production implementation until the required input is verified.", - ]) - elif readiness.get("implementation") == "ready_with_assumptions": - lines.append("Implementation readiness: ready with conservative performance/accessibility assumptions.") - lines.extend([ - "", - "Add references, avoided styles, brand constraints, or asset policy if you want to refine the brief.", - "If you do not explicitly allow asset usage, external sources stay reference-principles only.", + "Add references, avoided styles, or brand constraints if you want to refine the brief.", "To generate Artic design docs, run `@artic start`.", - ]) + ] return "\n".join(lines) diff --git a/plugins/codex-artic/skills/artic/scripts/artic_show.py b/plugins/codex-artic/skills/artic/scripts/artic_show.py deleted file mode 100644 index 3ae2ad6..0000000 --- a/plugins/codex-artic/skills/artic/scripts/artic_show.py +++ /dev/null @@ -1,833 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import argparse -import html -import json -import re -import shutil -import sys -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -REQUIRED_INPUTS = [ - "DESIGN.md", - "docs/homepage-design-prompt.md", - ".artic/brief.json", - ".artic/references.json", - ".artic/strategy.json", -] -POLICY_FALLBACK = "Reference policy: extract reusable principles only; do not copy logos, trademarks, proprietary illustrations, or exact layouts." -POLICY_BY_LOCALE = { - "ko-KR": "참고 정책: 재사용 가능한 원칙만 추출하고, 로고, 상표, 독점 일러스트, 정확한 레이아웃은 복사하지 않습니다.", -} -SCORE_KEYS = [ - "strategy_alignment", - "visual_fidelity", - "asset_richness", - "asset_provenance_completeness", - "conversion_clarity", - "visual_specificity", - "accessibility_basics", - "mobile_first", - "genericness_penalty", - "overall", -] - - -def read_json(path: Path) -> dict[str, Any]: - try: - payload = json.loads(path.read_text(encoding="utf-8")) - except FileNotFoundError as exc: - raise ValueError(f"missing required input: {path}") from exc - except json.JSONDecodeError as exc: - raise ValueError(f"invalid JSON in {path}: {exc}") from exc - if not isinstance(payload, dict): - raise ValueError(f"expected JSON object in {path}") - return payload - - -def write_json(path: Path, payload: Any) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - - -def write_text(path: Path, content: str) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding="utf-8") - - -def required_missing(root: Path) -> list[str]: - return [rel for rel in REQUIRED_INPUTS if not (root / rel).exists()] - - -def frontmatter(text: str) -> str: - if not text.startswith("---\n"): - return "" - end = text.find("\n---\n", 4) - if end == -1: - return "" - return text[4:end] - - -def yaml_value(block: str, key: str, default: str) -> str: - match = re.search(rf"^\s*{re.escape(key)}\s*:\s*[\"']?([^\n\"']+)[\"']?\s*$", block, re.MULTILINE) - return match.group(1).strip() if match else default - - -def yaml_section_value(block: str, section: str, token: str, default: str) -> str: - lines = block.splitlines() - in_section = False - for line in lines: - if re.match(rf"^{re.escape(section)}\s*:", line): - in_section = True - continue - if in_section and line and not line.startswith(" "): - break - if in_section: - match = re.match(rf"^\s+{re.escape(token)}\s*:\s*[\"']?([^\n\"']+)[\"']?\s*$", line) - if match: - return match.group(1).strip() - return default - - -def safe_css_color(value: str, fallback: str) -> str: - candidate = value.strip() - safe_patterns = [ - r"#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?(?:[0-9a-fA-F]{2})?", - r"(?:rgb|rgba|hsl|hsla)\([0-9.%,\s]+\)", - r"[a-zA-Z]+", - ] - if any(re.fullmatch(pattern, candidate) for pattern in safe_patterns): - return candidate - return fallback - - -def markdown_section(text: str, heading: str) -> str: - marker = f"## {heading}" - start = text.find(marker) - if start == -1: - return "" - next_heading = text.find("\n## ", start + len(marker)) - end = next_heading if next_heading != -1 else len(text) - section = text[start + len(marker):end] - return " ".join(line.strip(" #") for line in section.splitlines() if line.strip()) - - -def selected_source_names(references: dict[str, Any]) -> list[str]: - selected = references.get("selected_sources", []) - if not isinstance(selected, list): - return [] - names: list[str] = [] - for row in selected: - if not isinstance(row, dict): - continue - name = str(row.get("name") or row.get("id") or "reference source").strip() - if name: - names.append(name) - return names - - -def brief_locale(brief: dict[str, Any]) -> str: - language = brief.get("language") - if isinstance(language, dict): - return str(language.get("locale") or "en-US") - return "en-US" - - -def has_3d_runtime_reference(brief: dict[str, Any], references: dict[str, Any]) -> bool: - roles = references.get("role_assignments", []) - if isinstance(roles, list): - for role in roles: - if isinstance(role, dict) and "3d_runtime" == str(role.get("role")): - return True - for bucket in (brief.get("style"), references, brief): - text = json.dumps(bucket, ensure_ascii=False).lower() if isinstance(bucket, (dict, list)) else str(bucket).lower() - if any(term in text for term in ("3d", "webgl", "glb", "model-viewer", "runtime")): - return True - return False - - -def _string_items(value: Any) -> list[str]: - if value is None: - return [] - if isinstance(value, str): - text = value.strip() - return [text] if text else [] - if isinstance(value, list): - items: list[str] = [] - for item in value: - items.extend(_string_items(item)) - return items - if isinstance(value, dict): - for key in ("summary", "description", "requirement", "condition", "reason", "message", "title", "name"): - if value.get(key): - return _string_items(value.get(key)) - return [json.dumps(value, ensure_ascii=False, sort_keys=True)] - return [str(value)] - - -def _has_missing_quality(value: Any) -> bool: - missing_markers = ("missing", "blocked", "incomplete", "not_met", "failed", "fail", "placeholder") - if isinstance(value, dict): - status = str(value.get("status") or value.get("state") or value.get("readiness") or "").lower() - if any(marker in status for marker in missing_markers): - return True - return any(_has_missing_quality(item) for item in value.values()) - if isinstance(value, list): - return any(_has_missing_quality(item) for item in value) - if isinstance(value, str): - return any(marker in value.lower() for marker in missing_markers) - return False - - -def risk_readiness_summary(brief: dict[str, Any]) -> dict[str, Any]: - risk = brief.get("risk_readiness") - if not isinstance(risk, dict): - return { - "preview_status": "static_preview", - "production_ready": True, - "placeholder_boundaries": [], - "implementation_blockers": [], - } - - placeholder_boundaries = _string_items(risk.get("placeholder_boundary")) + _string_items(risk.get("placeholder_fallback_boundary")) - implementation_blockers = _string_items(risk.get("stop_conditions")) + _string_items(risk.get("implementation_stop_conditions")) - - readiness = str(risk.get("readiness") or "").lower() - implementation_blocked = bool(risk.get("implementation_blocked")) - readiness_blocked = any(marker in readiness for marker in ("blocked", "placeholder", "not_ready", "not-ready")) - quality_missing = _has_missing_quality(risk.get("quality_critical_requirements")) or _has_missing_quality(risk.get("core_quality_requirements")) - production_ready = not (implementation_blocked or readiness_blocked or bool(placeholder_boundaries) or bool(implementation_blockers) or quality_missing) - - return { - "preview_status": "static_preview" if production_ready else "placeholder_preview", - "production_ready": production_ready, - "placeholder_boundaries": list(dict.fromkeys(placeholder_boundaries)), - "implementation_blockers": list(dict.fromkeys(implementation_blockers)), - } - - -def localized_copy(locale: str) -> dict[str, str]: - if locale.startswith("ko"): - return { - "preview_badge": "Artic 미리보기 · 에셋 우선 번들", - "for_label": "대상", - "headline_suffix": "첫 시각 초안", - "drive": "목표 이끌기", - "view_logic": "디자인 논리 보기", - "north_star": "Design north star", - "accessibility": "접근성 목표", - "reference_roles": "레퍼런스 역할", - "promise_title": "1. 약속", - "promise_body": "프로젝트 고유의 약속과 가장 중요한 전환 경로를 먼저 보여줍니다.", - "proof_title": "2. 증거", - "proof_body": "신뢰 근거와 에셋 출처를 hero 가까이에 배치해 첫 CTA를 뒷받침합니다.", - "system_title": "3. 시스템", - "system_body": "토큰, 간격 리듬, 시맨틱 컨트롤, 독창적 구성을 Artic 문서에서 가져옵니다.", - "reference_title": "레퍼런스 기반, 복제 아님", - "reference_body": "이 정적 번들은 Artic start 산출물에서 생성됩니다. 앱 소스 파일은 건드리지 않습니다.", - "policy_title": "참고 정책", - "generated": "생성 시각", - "root": "루트", - "model_label": "3D 모델 자리표시자", - "model_body": "실제 GLB/이미지/모델은 소유 에셋 또는 라이선스 확인 가능한 공개 에셋만 연결합니다. 이 preview-only 에셋은 poster fallback, reduced motion, 로딩 실패 상태를 검토하기 위한 구조입니다.", - "interaction_title": "상호작용 영역", - "interaction_body": "드래그/탭/키보드 포커스로 3D 오브젝트를 탐색하고, 실패 시 정적 포스터와 설명 텍스트를 제공합니다.", - } - return { - "preview_badge": "Artic Preview · asset-first bundle", - "for_label": "For", - "headline_suffix": "first visual draft", - "drive": "Drive", - "view_logic": "View design logic", - "north_star": "Design north star", - "accessibility": "accessibility target", - "reference_roles": "reference roles", - "promise_title": "1. Promise", - "promise_body": "Lead with one clear project-specific promise and one dominant conversion path.", - "proof_title": "2. Proof", - "proof_body": "Place evidence and asset provenance close to the hero so trust supports the first CTA.", - "system_title": "3. System", - "system_body": "Use tokens, spacing rhythm, semantic controls, and original composition from the Artic docs.", - "reference_title": "Reference-informed, not reference-copied", - "reference_body": "This static bundle is generated from Artic start outputs. It intentionally leaves app source files untouched.", - "policy_title": "Reference policy", - "generated": "Generated", - "root": "Root", - "model_label": "3D asset placeholder", - "model_body": "Attach only user-owned or license-verifiable public GLB/image/model assets when allowed. This preview-only asset validates poster fallback, reduced motion, and loading failure states.", - "interaction_title": "Interaction zone", - "interaction_body": "Design drag, tap, and keyboard focus affordances for the central 3D object, with a static poster and description fallback when loading fails.", - } - - -def project_name(brief: dict[str, Any], design_frontmatter: str) -> str: - project = brief.get("project") - if isinstance(project, dict): - value = str(project.get("name") or project.get("description") or "").strip() - if value: - return value - return yaml_value(design_frontmatter, "name", "Artic Preview") - - -def build_tokens(design_text: str) -> dict[str, Any]: - fm = frontmatter(design_text) - colors = { - "primary": safe_css_color(yaml_section_value(fm, "colors", "primary", "#1F4FD8"), "#1F4FD8"), - "accent": safe_css_color(yaml_section_value(fm, "colors", "accent", "#7C3AED"), "#7C3AED"), - "surface": safe_css_color(yaml_section_value(fm, "colors", "surface", "#FFFFFF"), "#FFFFFF"), - "neutral": safe_css_color(yaml_section_value(fm, "colors", "neutral", "#F6F8FB"), "#F6F8FB"), - "text": safe_css_color(yaml_section_value(fm, "colors", "text", "#111827"), "#111827"), - "muted": safe_css_color(yaml_section_value(fm, "colors", "muted", "#6B7280"), "#6B7280"), - "border": safe_css_color(yaml_section_value(fm, "colors", "border", "#DDE3EA"), "#DDE3EA"), - } - return { - "colors": colors, - "type": { - "family": yaml_section_value(fm, "typography", "fontFamily", "Inter, ui-sans-serif, system-ui"), - "hero": "clamp(3rem, 8vw, 5.7rem)", - "heading": "clamp(2rem, 4vw, 3rem)", - "body": "1rem", - }, - "radius": {"sm": "8px", "md": "14px", "lg": "24px", "xl": "32px", "pill": "999px"}, - "spacing": {"xs": "4px", "sm": "8px", "md": "16px", "lg": "24px", "xl": "40px", "section": "96px"}, - "shadow": {"card": "0 24px 80px rgba(15,23,42,.08)", "cta": "0 18px 40px rgba(31,79,216,.22)"}, - "motion": {"duration": "180ms", "easing": "cubic-bezier(.2,.8,.2,1)", "reduced_motion": "preserve layout without parallax"}, - } - - -def variant_names(max_iterations: int, runtime_3d: bool) -> list[str]: - pool = ["asset-hero", "conversion-proof", "immersive-runtime" if runtime_3d else "editorial-system"] - return pool[:max_iterations] - - -def make_placeholder_svg(kind: str, tokens: dict[str, Any]) -> str: - c = tokens["colors"] - title = "Model Poster" if kind == "model" else "Scene Fallback" - label = "preview-only 3D poster" if kind == "model" else "preview-only runtime fallback" - shape = "" if kind == "model" else "" - return f""" - - - - {shape} - {html.escape(title)} - {html.escape(label)} - -""" - - -def build_asset_manifest(root: Path, show_dir: Path, iteration: str, variant: str, brief: dict[str, Any], references: dict[str, Any], tokens: dict[str, Any], asset_mode: str, runtime_3d: bool) -> dict[str, Any]: - assets_dir = show_dir / "assets" - placeholders_dir = assets_dir / "placeholders" - sources = selected_source_names(references) - assets: list[dict[str, Any]] = [] - for idx, source in enumerate(sources or ["artic-generated-visual-direction"], start=1): - assets.append({ - "id": f"catalog-reference-{idx}", - "path": None, - "type": "catalog-reference", - "status": "catalog-reference", - "retrieval": "catalog-reference", - "provenance": "selected Artic reference source", - "kind": "catalog-reference", - "title": source, - "source": source, - "license": "unknown", - "license_status": "unknown-unverified", - "usage": "preview-only visual direction and provenance record; do not copy protected marks or exact layouts", - "downloaded": False, - "local_path": None, - }) - hero_name = f"hero-{variant}.svg" - write_text(placeholders_dir / hero_name, make_placeholder_svg("scene", tokens)) - assets.append({ - "id": f"generated-{variant}-hero", - "path": f"assets/placeholders/{hero_name}", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": f"{variant} hero placeholder", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "safe first-draft hero artwork", - "downloaded": False, - "local_path": f"assets/placeholders/{hero_name}", - }) - if runtime_3d: - model_svg = make_placeholder_svg("model", tokens) - scene_svg = make_placeholder_svg("scene", tokens) - write_text(placeholders_dir / "model-poster.svg", model_svg) - write_text(placeholders_dir / "scene-fallback.svg", scene_svg) - write_text(assets_dir / "model-poster.svg", model_svg) - write_text(assets_dir / "scene-fallback.svg", scene_svg) - assets.extend([ - { - "id": "generated-model-poster", - "path": "assets/placeholders/model-poster.svg", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": "3D model poster placeholder", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "3D poster fallback before verified GLB/model asset is supplied", - "downloaded": False, - "local_path": "assets/placeholders/model-poster.svg", - }, - { - "id": "generated-model-poster-root-alias", - "path": "assets/model-poster.svg", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": "3D model poster placeholder root alias", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "backward-compatible 3D poster fallback alias", - "downloaded": False, - "local_path": "assets/model-poster.svg", - }, - { - "id": "generated-scene-fallback", - "path": "assets/placeholders/scene-fallback.svg", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": "3D scene fallback placeholder", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "runtime failure and reduced-motion scene fallback", - "downloaded": False, - "local_path": "assets/placeholders/scene-fallback.svg", - }, - { - "id": "generated-scene-fallback-root-alias", - "path": "assets/scene-fallback.svg", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": "3D scene fallback placeholder root alias", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "backward-compatible runtime failure fallback alias", - "downloaded": False, - "local_path": "assets/scene-fallback.svg", - }, - ]) - return { - "schema_version": 1, - "mode": "asset-first-preview", - "asset_mode": asset_mode, - "iteration": iteration, - "variant": variant, - "root": str(root.resolve()), - "created_at": datetime.now(timezone.utc).isoformat(), - "notes": ["Unknown or unverified sources are recorded as preview-only/catalog-reference and do not block preview generation."], - "assets": assets, - } - - -def render_css(tokens: dict[str, Any], variant: str, runtime_3d: bool) -> str: - c = tokens["colors"] - hero_background = { - "asset-hero": "radial-gradient(circle at 80% 10%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 34%), linear-gradient(180deg, var(--neutral), var(--surface))", - "conversion-proof": "linear-gradient(180deg, var(--surface), var(--neutral))", - "immersive-runtime": "radial-gradient(circle at 62% 20%, color-mix(in srgb, var(--accent) 24%, transparent), transparent 32%), radial-gradient(circle at 20% 20%, color-mix(in srgb, var(--primary) 18%, transparent), transparent 26%), var(--surface)", - "editorial-system": "linear-gradient(135deg, var(--neutral), var(--surface) 48%, color-mix(in srgb, var(--primary) 8%, var(--surface)))", - }.get(variant, "linear-gradient(180deg, var(--neutral), var(--surface))") - return f""":root {{ - --primary: {c['primary']}; --accent: {c['accent']}; --surface: {c['surface']}; --neutral: {c['neutral']}; --text: {c['text']}; --muted: {c['muted']}; --border: {c['border']}; - --radius-sm: {tokens['radius']['sm']}; --radius-md: {tokens['radius']['md']}; --radius-lg: {tokens['radius']['lg']}; --radius-xl: {tokens['radius']['xl']}; --shadow-card: {tokens['shadow']['card']}; --shadow-cta: {tokens['shadow']['cta']}; -}} -* {{ box-sizing: border-box; }} -html {{ scroll-behavior: smooth; }} -body {{ margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--text); background: {hero_background}; }} -a {{ color: inherit; }} -.page {{ width: min(1160px, calc(100% - 32px)); margin: 0 auto; padding: 28px 0 64px; }} -.nav {{ display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: clamp(36px, 8vw, 76px); }} -.brand {{ font-weight: 850; letter-spacing: -0.045em; font-size: 1.12rem; }} -.pill {{ display: inline-flex; align-items: center; border: 1px solid var(--border); border-radius: 999px; padding: 8px 12px; color: var(--muted); background: rgba(255,255,255,.72); backdrop-filter: blur(12px); }} -.hero {{ display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(280px, .95fr); gap: clamp(24px, 5vw, 56px); align-items: center; }} -h1 {{ font-size: clamp(3rem, 8vw, 5.9rem); line-height: .91; letter-spacing: -0.075em; margin: 18px 0; max-width: 10ch; }} -h2 {{ font-size: clamp(2rem, 4vw, 3rem); letter-spacing: -0.045em; margin: 0 0 16px; }} -p {{ color: var(--muted); line-height: 1.7; font-size: 1.04rem; }} -.actions {{ display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px; }} -.button {{ border: 0; border-radius: var(--radius-md); padding: 14px 18px; font-weight: 780; text-decoration: none; transition: transform 180ms cubic-bezier(.2,.8,.2,1), box-shadow 180ms cubic-bezier(.2,.8,.2,1); }} -.button:focus-visible {{ outline: 3px solid color-mix(in srgb, var(--accent) 45%, white); outline-offset: 3px; }} -.button:hover {{ transform: translateY(-1px); }} -.primary {{ background: var(--primary); color: white; box-shadow: var(--shadow-cta); }} -.secondary {{ color: var(--text); border: 1px solid var(--border); background: white; }} -.panel {{ border: 1px solid var(--border); background: rgba(255,255,255,.84); border-radius: var(--radius-xl); padding: clamp(20px, 4vw, 32px); box-shadow: var(--shadow-card); }} -.visual-card {{ position: relative; overflow: hidden; min-height: 440px; display: flex; flex-direction: column; justify-content: flex-end; }} -.visual-card img {{ width: 100%; border-radius: 24px; border: 1px solid var(--border); background: var(--neutral); margin-bottom: 18px; }} -.asset-caption {{ font-size: .9rem; color: var(--muted); }} -.metric-grid {{ display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; margin-top: 18px; }} -.metric {{ padding: 18px; border-radius: 20px; background: var(--neutral); }} -.metric strong {{ display: block; font-size: 1.35rem; color: var(--primary); }} -.sections {{ display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 16px; margin-top: 56px; }} -.card {{ border: 1px solid var(--border); border-radius: 24px; padding: 22px; background: white; }} -.card b {{ display: block; margin-bottom: 8px; }} -.source-list {{ padding-left: 20px; color: var(--muted); line-height: 1.8; }} -.model-viewer-stub {{ display: grid; place-items: center; min-height: 300px; margin: 18px 0; border-radius: 28px; border: 1px dashed var(--border); color: var(--muted); background: radial-gradient(circle at 50% 36%, rgba(124,58,237,.18), transparent 34%), linear-gradient(145deg, var(--neutral), var(--surface)); font-weight: 800; letter-spacing: .08em; }} -.interaction-zone {{ border: 1px solid var(--border); border-radius: 20px; padding: 16px; background: var(--neutral); }} -.runtime-3d {{ margin-top: 24px; }} -.readiness-note {{ margin: -28px 0 40px; border: 1px solid color-mix(in srgb, var(--accent), var(--border) 45%); border-left: 4px solid var(--accent); border-radius: 20px; padding: 18px 20px; background: rgba(124,58,237,.08); }} -.readiness-note strong {{ display: block; margin-bottom: 8px; }} -.readiness-note ul {{ margin: 8px 0 0; padding-left: 20px; color: var(--muted); line-height: 1.7; }} -.policy {{ margin-top: 56px; padding: 18px; border-left: 4px solid var(--accent); background: white; border-radius: 16px; color: var(--muted); }} -@media (max-width: 820px) {{ .hero, .sections {{ grid-template-columns: 1fr; }} .nav {{ align-items: flex-start; flex-direction: column; }} .visual-card {{ min-height: 0; }} h1 {{ max-width: 12ch; }} }} -@media (prefers-reduced-motion: reduce) {{ html {{ scroll-behavior: auto; }} .button {{ transition: none; }} .button:hover {{ transform: none; }} }} -""" - - -def render_html(root: Path, brief: dict[str, Any], references: dict[str, Any], strategy: dict[str, Any], design_text: str, variant: str, iteration: str, runtime_3d: bool, readiness: dict[str, Any] | None = None) -> str: - readiness = readiness or risk_readiness_summary(brief) - fm = frontmatter(design_text) - name = project_name(brief, fm) - description = yaml_value(fm, "description", "Artic-generated homepage preview") - raw_project = brief.get("project") - project = raw_project if isinstance(raw_project, dict) else {} - audience = ", ".join(str(item) for item in project.get("target_users", []) if item) or "the target audience" - goal = str(project.get("primary_goal") or "the primary conversion") - raw_style = brief.get("style") - style = raw_style if isinstance(raw_style, dict) else {} - north_star = markdown_section(design_text, "Design North Star") or str(style.get("design_north_star") or strategy.get("design_north_star") or "") - overview = markdown_section(design_text, "Overview") or description - composition = markdown_section(design_text, "Page Composition") or str(strategy.get("implementation_guidance") or "Hero, proof, features, trust, conversion, FAQ, and final CTA.") - sources = selected_source_names(references) - locale = brief_locale(brief) - copy = localized_copy(locale) - policy_text = POLICY_BY_LOCALE.get(locale, POLICY_FALLBACK) - source_items = "\n".join(f"
  • {html.escape(source)} preview-only/catalog-reference
  • " for source in sources[:5]) or "
  • Artic selected references preview-only/catalog-reference
  • " - boundary_items = "\n".join(f"
  • {html.escape(item)}
  • " for item in readiness.get("placeholder_boundaries", [])) - blocker_items = "\n".join(f"
  • {html.escape(item)}
  • " for item in readiness.get("implementation_blockers", [])) - if not readiness.get("production_ready", True): - if locale.startswith("ko"): - note_title = "프로덕션 준비 완료가 아닙니다" - note_body = "이 화면은 확인된 에셋과 구현 조건이 충족되기 전까지 플레이스홀더 미리보기로만 사용해야 합니다." - boundary_label = "플레이스홀더 경계" - blocker_label = "구현 차단 조건" - else: - note_title = "Not production-ready" - note_body = "Use this screen only as a placeholder preview until required assets and implementation conditions are satisfied." - boundary_label = "Placeholder boundaries" - blocker_label = "Implementation blockers" - boundary_section = f"
    {html.escape(boundary_label)}
      {boundary_items}
    " if boundary_items else "" - blocker_section = f"
    {html.escape(blocker_label)}
      {blocker_items}
    " if blocker_items else "" - readiness_note = f""" -
    - {html.escape(note_title)} -

    {html.escape(note_body)}

    - {boundary_section} - {blocker_section} -
    """ - else: - readiness_note = "" - runtime_block = "" - if runtime_3d: - runtime_block = f""" -
    - {html.escape(copy['model_label'])} - \"Preview-only -
    model-viewer · GLB · poster fallback
    -
    - {html.escape(copy['interaction_title'])} -

    {html.escape(copy['interaction_body'])}

    -
    -

    {html.escape(copy['model_body'])}

    -
    """ - generated_at = datetime.now(timezone.utc).isoformat() - variant_label = variant.replace("-", " ").title() - return f""" - - - - - - {html.escape(name)} — Artic Preview {html.escape(iteration)} - - - - -
    - -{readiness_note} -
    -
    - {html.escape(copy['for_label'])}: {html.escape(audience)} -

    {html.escape(name)} {html.escape(copy['headline_suffix'])}

    -

    {html.escape(overview)}

    - -
    - -
    -
    -
    {html.escape(copy['promise_title'])}

    {html.escape(copy['promise_body'])}

    -
    {html.escape(copy['proof_title'])}

    {html.escape(copy['proof_body'])}

    -
    {html.escape(copy['system_title'])}

    {html.escape(copy['system_body'])}

    -
    -{runtime_block} -
    -

    {html.escape(copy['reference_title'])}

    -

    {html.escape(copy['reference_body'])}

    -
      {source_items}
    -
    -
    - {html.escape(copy['policy_title'])}
    - {html.escape(policy_text)} -
    {html.escape(copy['generated'])}: {html.escape(generated_at)} · {html.escape(copy['root'])}: {html.escape(str(root))} · iteration {html.escape(iteration)} -
    -
    - - -""" - - -def score_variant(variant: str, idx: int, runtime_3d: bool, sources_count: int) -> dict[str, Any]: - base = { - "asset-hero": 90, - "conversion-proof": 81, - "immersive-runtime": 86 if runtime_3d else 78, - "editorial-system": 82, - }.get(variant, 80) - richness = min(92, 74 + sources_count * 4 + (8 if runtime_3d else 0) + (4 if variant in ("asset-hero", "immersive-runtime") else 0)) - provenance = min(95, 80 + sources_count * 3 + (5 if runtime_3d else 0)) - generic_penalty = max(4, 13 - idx * 2 - (3 if variant in ("asset-hero", "immersive-runtime") else 0)) - dimensions: dict[str, float] = { - "strategy_alignment": float(min(94, base + 3)), - "visual_fidelity": float(min(92, base + (2 if variant != "conversion-proof" else 0))), - "asset_richness": float(richness), - "asset_provenance_completeness": float(provenance), - "conversion_clarity": float(min(94, base + (6 if variant == "conversion-proof" else 2))), - "visual_specificity": float(min(93, base + (5 if variant in ("asset-hero", "immersive-runtime") else 1))), - "accessibility_basics": 86.0, - "mobile_first": 88.0, - "genericness_penalty": float(generic_penalty), - } - positive = [v for k, v in dimensions.items() if k != "genericness_penalty"] - dimensions["overall"] = round((sum(positive) / len(positive)) - (generic_penalty * 0.35), 1) - return dimensions - - -def render_critique(reports: list[dict[str, Any]], selected: dict[str, Any]) -> str: - lines = ["# Artic Show Critique", "", "Asset-first visual draft review. Unknown asset sources are retained as preview-only/catalog-reference instead of blocking generation.", ""] - for report in reports: - scores = report["scores"] - lines.extend([ - f"## Iteration {report['iteration']} — {report['variant']}", - "", - f"- Overall: {scores['overall']}", - f"- Strength: {report['strength']}", - f"- Risk: {report['risk']}", - f"- Asset provenance: {scores['asset_provenance_completeness']} / asset richness: {scores['asset_richness']}", - "", - ]) - lines.extend([ - "## Selected", "", f"Iteration {selected['iteration']} (`{selected['variant']}`) was promoted to `.artic/show/` root.", "", "## App source impact", "", "`modified_app_files` is intentionally `[]`; this command only writes preview bundle artifacts under `.artic/show`.", "", - ]) - return "\n".join(lines) - - -def copy_iteration_to_root(iter_dir: Path, show_root: Path) -> None: - for filename in ("index.html", "styles.css", "tokens.json", "report.json"): - shutil.copy2(iter_dir / filename, show_root / filename) - dst_assets = show_root / "assets" - if dst_assets.exists(): - shutil.rmtree(dst_assets) - shutil.copytree(iter_dir / "assets", dst_assets) - - -def rel(root: Path, path: Path) -> str: - try: - return str(path.resolve().relative_to(root.resolve())) - except ValueError: - return str(path) - - -def create_show_preview(root: Path, max_iterations: int = 3, min_score: float = 75, strict: bool = False, asset_mode: str = "asset-first", asset_timeout: float = 10) -> dict[str, Any]: - del asset_timeout # reserved for future downloader; core implementation is local/offline-safe. - missing = required_missing(root) - if missing: - raise ValueError("missing required input(s) for @artic show: " + ", ".join(missing)) - if not 1 <= max_iterations <= 3: - raise ValueError("--max-iterations must be between 1 and 3") - design_text = (root / "DESIGN.md").read_text(encoding="utf-8") - brief = read_json(root / ".artic" / "brief.json") - references = read_json(root / ".artic" / "references.json") - strategy = read_json(root / ".artic" / "strategy.json") - readiness = risk_readiness_summary(brief) - show_root = root / ".artic" / "show" - iterations_root = show_root / "iterations" - show_root.mkdir(parents=True, exist_ok=True) - if iterations_root.exists(): - shutil.rmtree(iterations_root) - iterations_root.mkdir(parents=True, exist_ok=True) - - tokens = build_tokens(design_text) - runtime_3d = has_3d_runtime_reference(brief, references) - variants = variant_names(max_iterations, runtime_3d) - reports: list[dict[str, Any]] = [] - generated_preview_files: list[str] = [] - asset_files: list[str] = [] - - for idx, variant in enumerate(variants, start=1): - iteration = f"{idx:03d}" - iter_dir = iterations_root / iteration - if iter_dir.exists(): - shutil.rmtree(iter_dir) - (iter_dir / "assets").mkdir(parents=True, exist_ok=True) - manifest = build_asset_manifest(root, iter_dir, iteration, variant, brief, references, tokens, asset_mode, runtime_3d) - scores = score_variant(variant, idx, runtime_3d, len(selected_source_names(references))) - report = { - "schema_version": 1, - "mode": "asset-first-preview", - "iteration": iteration, - "variant": variant, - "created_at": datetime.now(timezone.utc).isoformat(), - "scores": scores, - "score_dimensions": SCORE_KEYS, - "strength": "Uses local generated placeholders plus catalog-reference provenance to make the first draft visually concrete.", - "risk": "Preview assets may need replacement with owned or license-verified production assets before implementation.", - "modified_app_files": [], - "asset_manifest": "assets/manifest.json", - **readiness, - } - write_json(iter_dir / "tokens.json", tokens) - write_text(iter_dir / "styles.css", render_css(tokens, variant, runtime_3d)) - write_text(iter_dir / "index.html", render_html(root, brief, references, strategy, design_text, variant, iteration, runtime_3d, readiness)) - write_json(iter_dir / "assets" / "manifest.json", manifest) - write_json(iter_dir / "report.json", report) - reports.append(report) - generated_preview_files.extend([rel(root, iter_dir / name) for name in ("index.html", "styles.css", "tokens.json", "report.json", "assets/manifest.json")]) - for asset in manifest.get("assets", []): - local_path = asset.get("local_path") - if local_path: - asset_files.append(rel(root, iter_dir / str(local_path))) - - selected_report = max(reports, key=lambda item: float(item["scores"]["overall"])) - selected_iteration = str(selected_report["iteration"]) - selected_dir = iterations_root / selected_iteration - copy_iteration_to_root(selected_dir, show_root) - root_asset_manifest = read_json(show_root / "assets" / "manifest.json") - asset_summary = { - "assets_used": len(root_asset_manifest.get("assets", [])), - "verified_assets": sum(1 for asset in root_asset_manifest.get("assets", []) if asset.get("status") == "verified"), - "unverified_preview_only_assets": sum(1 for asset in root_asset_manifest.get("assets", []) if asset.get("status") == "unverified-preview-only"), - "generated_placeholders": sum(1 for asset in root_asset_manifest.get("assets", []) if asset.get("status") == "generated"), - "catalog_references": sum(1 for asset in root_asset_manifest.get("assets", []) if asset.get("status") == "catalog-reference"), - } - status = "selected" - if float(selected_report["scores"]["overall"]) < min_score: - status = "below-threshold" - root_report = { - "schema_version": 1, - "mode": "asset-first-preview", - "status": status, - "generated_at": datetime.now(timezone.utc).isoformat(), - "selected_iteration": selected_iteration, - "candidate": selected_report["variant"], - "selected_candidate_id": selected_report["variant"], - "threshold": min_score, - "preview_bundle": str(show_root), - "entrypoint": str(show_root / "index.html"), - "scores": selected_report["scores"], - "asset_summary": asset_summary, - "integrity": { - "modified_app_files": [], - }, - "modified_app_files": [], - "iterations": reports, - "remaining_risks": ["Preview assets may need replacement with owned or license-verified production assets before apply."], - **readiness, - } - write_json(show_root / "report.json", root_report) - selected_payload = { - "schema_version": 1, - "selected_iteration": selected_iteration, - "candidate": selected_report["variant"], - "selected_candidate_id": selected_report["variant"], - "variant": selected_report["variant"], - "overall": selected_report["scores"]["overall"], - "entrypoint": ".artic/show/index.html", - "reason": "Highest overall asset-first preview score among generated candidates.", - } - write_json(show_root / "selected.json", selected_payload) - write_text(show_root / "critique.md", render_critique(reports, selected_report)) - - asset_files.extend(rel(root, show_root / str(asset["local_path"])) for asset in root_asset_manifest.get("assets", []) if asset.get("local_path")) - generated_preview_files.extend([rel(root, show_root / name) for name in ("index.html", "styles.css", "tokens.json", "report.json", "selected.json", "critique.md", "assets/manifest.json")]) - if strict and status == "below-threshold": - raise ValueError(f"quality threshold failure: selected preview score {selected_report['scores']['overall']} is below --min-score {min_score}") - - payload = { - "root": str(root.resolve()), - "preview_bundle": str(show_root), - "bundle_dir": str(show_root), - "entrypoint": str(show_root / "index.html"), - "preview_file": str(show_root / "index.html"), - "selected_iteration": selected_iteration, - "generated_preview_files": sorted(str(root / item) for item in dict.fromkeys(generated_preview_files)), - "asset_files": sorted(str(root / item) for item in dict.fromkeys(asset_files)) + [str(show_root / "assets" / "manifest.json")], - "asset_manifest": str(show_root / "assets" / "manifest.json"), - "report_file": str(show_root / "report.json"), - "critique_file": str(show_root / "critique.md"), - "modified_app_files": [], - **readiness, - } - return payload - - -def main() -> int: - parser = argparse.ArgumentParser(description="Render an asset-first static Artic homepage preview bundle from start-generated design docs.") - parser.add_argument("--root", required=True, help="Project root containing DESIGN.md and .artic outputs from @artic start") - parser.add_argument("--max-iterations", type=int, default=3, help="Number of preview candidates to generate (1..3)") - parser.add_argument("--min-score", type=float, default=75, help="Minimum selected score when --strict is enabled") - parser.add_argument("--strict", action="store_true", help="Fail if selected iteration is below --min-score") - parser.add_argument("--asset-mode", choices=["asset-first", "offline", "no-download"], default="asset-first", help="Asset handling mode; core show uses local placeholders/catalog provenance") - parser.add_argument("--asset-timeout", type=float, default=10, help="Reserved timeout for future asset fetchers") - args = parser.parse_args() - try: - payload = create_show_preview(Path(args.root), args.max_iterations, args.min_score, args.strict, args.asset_mode, args.asset_timeout) - except ValueError as exc: - print(json.dumps({"error": str(exc)}, ensure_ascii=False)) - return 1 - print(json.dumps(payload, indent=2, ensure_ascii=False)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/plugins/codex-artic/skills/artic/scripts/artic_start.py b/plugins/codex-artic/skills/artic/scripts/artic_start.py index 63e32e8..e4cb2bb 100644 --- a/plugins/codex-artic/skills/artic/scripts/artic_start.py +++ b/plugins/codex-artic/skills/artic/scripts/artic_start.py @@ -133,123 +133,6 @@ def as_markdown(value: Any, *, indent: int = 0) -> str: return str(value) -RISK_LABELS = { - "en": { - "heading": "Risk / Readiness Summary", - "summary_line": "Risk Summary: risk/quality readiness details follow.", - "core_quality_requirements": "Core quality requirements", - "known_missing_information": "Known missing information", - "safe_assumptions": "Safe assumptions", - "unsafe_assumptions": "Unsafe assumptions", - "placeholder_fallback_boundary": "Placeholder/fallback boundary", - "implementation_stop_conditions": "Implementation stop conditions / Stop Conditions", - "completion_acceptance_criteria": "Completion/acceptance criteria", - "status_ready": "Strategy/preview can proceed.", - "status_blocked": "Strategy/preview can proceed, but implementation is blocked until missing inputs are resolved.", - "none": "None declared.", - "check_intent": "requested product photo/3D/map/payment trust intent is satisfied or explicitly blocked", - "check_placeholder": "placeholder is not accepted as production substitute for quality-critical requirement", - }, - "ko": { - "heading": "위험/준비 상태 요약", - "summary_line": "위험 요약: 리스크와 품질 기준에 따른 준비 상태를 정리합니다.", - "core_quality_requirements": "핵심 품질 요구사항", - "known_missing_information": "알려진 누락 정보", - "safe_assumptions": "안전한 가정", - "unsafe_assumptions": "위험한 가정", - "placeholder_fallback_boundary": "플레이스홀더/대체 경계", - "implementation_stop_conditions": "구현 중단 조건", - "completion_acceptance_criteria": "완료/수용 기준", - "status_ready": "전략/프리뷰는 진행할 수 있습니다.", - "status_blocked": "전략/프리뷰는 진행할 수 있지만, 누락된 정보가 해결될 때까지 구현은 차단됩니다.", - "none": "명시된 항목 없음.", - "check_intent": "요청된 제품 사진/3D/지도/결제 신뢰 의도가 충족되었거나 명시적으로 차단됨", - "check_placeholder": "플레이스홀더는 품질 핵심 요구사항의 프로덕션 대체물로 인정되지 않음", - }, -} - - -RISK_SECTION_KEYS = [ - "core_quality_requirements", - "known_missing_information", - "safe_assumptions", - "unsafe_assumptions", - "placeholder_fallback_boundary", - "implementation_stop_conditions", - "completion_acceptance_criteria", -] - - -def risk_readiness(brief: dict[str, Any]) -> dict[str, Any]: - value = brief.get("risk_readiness") - return value if isinstance(value, dict) and value else {} - - -def risk_labels(brief: dict[str, Any]) -> dict[str, str]: - locale = str(brief_language(brief).get("locale") or "en-US") - return RISK_LABELS["ko"] if locale.startswith("ko") else RISK_LABELS["en"] - - -def risk_items(risk: dict[str, Any], key: str) -> list[str]: - aliases = { - "core_quality_requirements": ["quality_critical_requirements", "core_requirements"], - "known_missing_information": ["missing_information", "missing_inputs", "blockers"], - "placeholder_fallback_boundary": ["placeholder_boundary", "fallback_boundary"], - "implementation_stop_conditions": ["stop_conditions", "implementation_blockers"], - "completion_acceptance_criteria": ["acceptance_criteria", "completion_criteria"], - } - raw = risk.get(key) - if raw is None: - for alias in aliases.get(key, []): - raw = risk.get(alias) - if raw is not None: - break - if raw is None or raw == "": - return [] - if isinstance(raw, list): - items: list[str] = [] - for item in raw: - rendered = as_markdown(item).strip() - if rendered: - items.append(rendered) - return items - return [as_markdown(raw).strip()] - - -def risk_readiness_block(brief: dict[str, Any], *, checklist: bool = False) -> str: - risk = risk_readiness(brief) - if not risk: - return "" - labels = risk_labels(brief) - implementation_blocked = bool(risk.get("implementation_blocked")) or risk.get("ready_for_implementation") is False - readiness = risk.get("readiness") - if isinstance(readiness, dict) and str(readiness.get("implementation") or "").lower() == "blocked": - implementation_blocked = True - lines = [f"## {labels['heading']}", "", f"- {labels['summary_line']}", f"- {labels['status_blocked'] if implementation_blocked else labels['status_ready']}"] - if "ready_for_strategy" in risk: - lines.append(f"- ready_for_strategy: {bool(risk.get('ready_for_strategy'))}") - if "implementation_blocked" in risk: - lines.append(f"- implementation_blocked: {bool(risk.get('implementation_blocked'))}") - lines.append("") - for key in RISK_SECTION_KEYS: - lines.extend([f"### {labels[key]}", ""]) - items = risk_items(risk, key) - if items: - prefix = "- [ ]" if checklist else "-" - lines.extend(f"{prefix} {item}" for item in items) - else: - lines.append(f"- {labels['none']}") - lines.append("") - if checklist: - lines.extend([ - "### Intent-matched QA gates", - "", - f"- [ ] {labels['check_intent']}.", - f"- [ ] {labels['check_placeholder']}.", - "", - ]) - return "\n".join(lines).rstrip() - def role_lines(strategy: dict[str, Any]) -> list[str]: roles = strategy.get("reference_roles", []) @@ -273,7 +156,6 @@ def role_lines(strategy: dict[str, Any]) -> list[str]: def strategy_doc(brief: dict[str, Any], strategy: dict[str, Any]) -> str: policy = policy_block(brief) - risk_block = risk_readiness_block(brief) parts = [ f"# Artic Strategy: {project_name(brief)}", "", @@ -282,8 +164,6 @@ def strategy_doc(brief: dict[str, Any], strategy: dict[str, Any]) -> str: policy, "", ] - if risk_block: - parts.extend([risk_block, ""]) parts.extend([ "## Project Summary", "", @@ -417,8 +297,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] description = f"{project_description(brief, strategy)} Artic-generated homepage design system." policy = policy_block(brief) language = language_block(brief) - risk_block = risk_readiness_block(brief) - risk_checklist_block = risk_readiness_block(brief, checklist=True) strategy_markdown = strategy_doc(brief, strategy) design = load_template("DESIGN.template.md") @@ -443,8 +321,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] design = design.replace("Recommended homepage sequence: hero with one primary promise, proof immediately near the hero, feature/job sections, trust or comparison section, conversion area, FAQ, and final CTA.", as_markdown(strategy.get("implementation_guidance", "")) or "Recommended homepage sequence: hero, proof, feature sections, trust, conversion area, FAQ, and final CTA.") design = design.replace("Do not use generic gradient blobs, random glassmorphism, off-token colors, multiple primary CTAs in one viewport, low-contrast muted copy, centered long paragraphs, or exact reference layouts.", "Do not copy " + ", ".join(str(item) for item in strategy.get("forbidden_copy_elements", FORBIDDEN_COPY_ELEMENTS)) + ". " + as_markdown(strategy.get("conflict_resolution", ""))) design = replace_policy_text(design, policy) - if risk_block: - design += f"\n\n{risk_block}\n" write(root / "DESIGN.md", design) write(root / "docs" / "artic-strategy.md", strategy_markdown) @@ -452,8 +328,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] rules = rules.replace("{{PROJECT_NAME}}", name) rules = rules.replace("{{REFERENCE_SYNTHESIS}}", strategy_markdown) rules = replace_policy_text(rules, policy) - if risk_block: - rules += f"\n\n{risk_block}\n" write(root / "docs" / "design-rules.md", rules) checklist = load_template("design-qa-checklist.template.md") @@ -461,8 +335,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] checklist = replace_policy_text(checklist, policy) if "Accessibility" not in checklist: checklist = checklist.replace("- [ ] Text contrast targets WCAG AA.", "- [ ] Accessibility: text contrast targets WCAG AA, focus states are visible, controls are semantic, and forms are labeled.") - if risk_checklist_block: - checklist += f"\n\n{risk_checklist_block}\n" checklist += "\n## Strategy Gates\n\n- [ ] Visual hierarchy follows the strategy north star.\n- [ ] Brand coherence follows the visual_system field.\n- [ ] Conversion clarity follows the conversion_strategy field.\n- [ ] Mobile quality follows implementation_guidance.\n- [ ] Accessibility follows the accessibility field.\n- [ ] Reference safety forbids logos, trademarks, proprietary illustrations, exact layouts, and source copywriting.\n" write(root / "docs" / "design-qa-checklist.md", checklist) @@ -471,8 +343,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] prompt = prompt.replace("# Homepage Implementation Prompt\n", f"# Homepage Implementation Prompt\n\n{language}\n") prompt = prompt.replace("Rules:\n", f"Strategy source: `docs/artic-strategy.md`.\n\nRules:\n") prompt = replace_policy_text(prompt, policy) - if risk_block: - prompt += f"\n\n{risk_block}\n" write(root / "docs" / "homepage-design-prompt.md", prompt) update_state(root, brief) @@ -523,7 +393,7 @@ def update_state(root: Path, brief: dict[str, Any]) -> None: state = loaded except json.JSONDecodeError: state = {} - state.update({"artic_version": str(brief.get("artic_version") or state.get("artic_version") or "0.4.1"), "last_generated_at": datetime.now(timezone.utc).isoformat(), "status": "generated", "language": brief_language(brief), "strategy_path": ".artic/strategy.json"}) + state.update({"artic_version": str(brief.get("artic_version") or state.get("artic_version") or "0.5.0"), "last_generated_at": datetime.now(timezone.utc).isoformat(), "status": "generated", "language": brief_language(brief), "strategy_path": ".artic/strategy.json"}) write(state_path, json.dumps(state, indent=2, ensure_ascii=False) + "\n") @@ -591,7 +461,7 @@ def create_start_outputs(root: Path, *, no_validate: bool = False) -> dict[str, raise ValueError("cannot run @artic start before init is ready" + (f": missing {missing}" if missing else "") + (f". Next questions: {questions}" if questions else "")) if not strategy_path.exists() and (status == "ready" or not brief_path.exists() or not references_path.exists()): answers = session.get("answers") if isinstance(session.get("answers"), dict) else {} - prompt_brief = {"answers": answers, "language": session.get("language", {}), "risk_readiness": session.get("risk_readiness", {})} + prompt_brief = {"answers": answers, "language": session.get("language", {})} prompt_references = {"selected_sources": [], "source_plan": [], "note": "@artic start requires the agent-authored strategy before finalizing init outputs."} prompt_path = write_strategy_prompt(root, prompt_brief, prompt_references, None) raise ValueError(json.dumps({ diff --git a/plugins/codex-artic/skills/artic/scripts/risk_readiness.py b/plugins/codex-artic/skills/artic/scripts/risk_readiness.py deleted file mode 100644 index e057ef8..0000000 --- a/plugins/codex-artic/skills/artic/scripts/risk_readiness.py +++ /dev/null @@ -1,335 +0,0 @@ -#!/usr/bin/env python3 -"""Artic risk/readiness contract helpers. - -The functions in this module are intentionally pure and deterministic: no IO, no -network access, and no clock reads. They turn early Artic answers/intent into a -machine-readable readiness contract that distinguishes strategy/preview from -final implementation readiness. -""" -from __future__ import annotations - -from typing import Any - -SCHEMA_VERSION = "artic-risk-readiness-v1" - -CORE_FIELDS = ("project", "audience", "goal", "vibe") - -_CATEGORY_KEYWORDS: dict[str, tuple[str, ...]] = { - "core_visual_asset_dependency": ( - "image", "photo", "product photo", "3d model", "video", "illustration", "person", "place", - "제품 사진", "이미지", "3d", "석고상", "영상", "일러스트", "사진", - ), - "interaction_dependency": ( - "drag", "edit", "game", "map", "realtime preview", "real-time preview", "canvas", "webgl", "rotate", "zoom", "interactive", "interaction", - "마우스로", "드래그", "편집", "지도", "실시간", "캔버스", "상호작용", "회전", "줌", "만질", - ), - "brand_dependency": ( - "logo", "colors", "fonts", "brand system", "reference brands", - "로고", "색상", "폰트", "브랜드", "toss", "토스", "kakao", "카카오", "naver", "네이버", - ), - "technical_runtime_dependency": ( - "shopify", "webgl", "cms", "animation runtime", - ), - "legal_license_dependency": ( - "external assets", "external asset", "fonts", "data", "music", "video", "model", "trademark", "license", - "라이선스", "외부 에셋", "상표", - ), - "accessibility_performance": ( - "motion-heavy", "media-heavy", "mobile-first", "data-heavy", "fast mobile", "reduced motion", "webgl", "3d", - "모바일에서 빠르게", "접근성", "성능", "애니메이션", - ), - "conversion_business": ( - "payment", "booking", "lead", "contact", "download", "demo", - "결제", "예약", "문의", "리드", "다운로드", "데모", - ), -} - -_FIELD_BY_CATEGORY: dict[str, tuple[str, ...]] = { - "core_visual_asset_dependency": ("asset_source",), - "interaction_dependency": ("interaction_model",), - "brand_dependency": ("brand_assets",), - "technical_runtime_dependency": ("technical_runtime",), - "legal_license_dependency": ("license_clearance",), - "accessibility_performance": ("performance_accessibility_plan",), -} - -_CONVERSION_HARD_KEYWORDS = ("payment", "booking", "checkout", "purchase", "lead form", "lead collection", "contact form", "download", "결제", "예약", "리드", "다운로드") - -_QUALITY_CRITICAL_PATTERNS = ( - ("product photo", "Real product photography is a quality-critical visual requirement."), - ("제품 사진", "실제 제품 사진은 품질 핵심 시각 요구사항입니다."), - ("actual product", "Actual product imagery is a quality-critical visual requirement."), - ("실제 제품", "실제 제품 이미지는 품질 핵심 시각 요구사항입니다."), - ("real gallery", "A real gallery-like visual asset is quality-critical."), - ("실제 갤러리", "실제 갤러리 같은 시각 자산은 품질 핵심 요구사항입니다."), - ("plaster statue", "The plaster statue subject is a quality-critical core asset."), - ("석고상", "The 3D plaster-cast subject is a quality-critical core asset."), -) - -_PLACEHOLDER_BOUNDARY = ( - "Placeholders may be used to communicate layout or interaction intent in strategy and preview, " - "but implementation cannot use a placeholder as the final substitute for core requirements." -) - -_SUBSTITUTE_STOP = ( - "Substitute stop rule: stop before implementation if a missing core asset, interaction model, " - "license clearance, brand asset, runtime decision, or performance/accessibility plan would be replaced by a generic substitute." -) - - -def _flatten_text(value: Any) -> str: - if value is None: - return "" - if isinstance(value, dict): - parts: list[str] = [] - for key in sorted(value): - parts.append(_flatten_text(value[key])) - return " ".join(parts) - if isinstance(value, (list, tuple, set)): - return " ".join(_flatten_text(item) for item in value) - return str(value) - - -def _has_any(text: str, keywords: tuple[str, ...]) -> bool: - lowered = text.lower() - return any(keyword.lower() in lowered for keyword in keywords) - - -def _present(value: Any) -> bool: - if value is None: - return False - if isinstance(value, str): - return bool(value.strip()) - if isinstance(value, (list, tuple, set, dict)): - return bool(value) - return True - - -def _detect_categories(answers: dict[str, Any], intent: dict[str, Any] | None = None) -> list[str]: - text = _flatten_text({"answers": answers or {}, "intent": intent or {}}) - categories = [category for category, words in _CATEGORY_KEYWORDS.items() if _has_any(text, words)] - - # Avoid making a simple low-risk SaaS demo CTA implementation-blocking while - # still detecting explicitly requested business mechanics. - if "conversion_business" in categories and not _has_any(text, _CONVERSION_HARD_KEYWORDS): - categories = [category for category in categories if category != "conversion_business"] - return categories - - -def _quality_critical_requirements(text: str) -> list[dict[str, str]]: - lowered = text.lower() - requirements: list[dict[str, str]] = [] - seen: set[str] = set() - for pattern, label in _QUALITY_CRITICAL_PATTERNS: - if pattern.lower() in lowered and label not in seen: - requirements.append({ - "requirement": label, - "source_signal": pattern, - "rule": "quality_critical_requirement", - "placeholder_policy": "preview_only", - "completion_criterion": "Final implementation must use an approved/source-confirmed asset or explicitly revised requirement, not a generic substitute.", - }) - seen.add(label) - if ("photo" in lowered or "사진" in lowered) and ("core" in lowered or "핵심" in lowered) and not requirements: - requirements.append({ - "requirement": "Core photography is quality-critical to the requested experience.", - "source_signal": "core photo", - "rule": "quality_critical_requirement", - "placeholder_policy": "preview_only", - "completion_criterion": "Final implementation must use approved photography or an explicit requirement change.", - }) - return requirements - - -def _has_placeholder_substitute_answer(answers: dict[str, Any], fields: list[str]) -> bool: - substitute_markers = ( - "placeholder", "generic", "gradient", "stock", "dummy", "mock", "temporary", "temp", - "대체", "임시", "플레이스홀더", "더미", "일반", "제네릭", - ) - fields_to_check = {"asset_source", "license_clearance", "brand_assets"} - for field in fields_to_check: - value = answers.get(field) - if not _present(value): - continue - lowered = _flatten_text(value).lower() - if any(marker in lowered for marker in substitute_markers): - return True - return False - - -def dynamic_required_fields(payload: dict[str, Any]) -> list[str]: - """Return dynamic required field names for a risk payload or partial contract.""" - categories = payload.get("risk_categories") or [] - fields: list[str] = [] - for category in categories: - for field in _FIELD_BY_CATEGORY.get(str(category), ()): # conversion handled below - if field not in fields: - fields.append(field) - - text = _flatten_text(payload) - if "conversion_business" in categories and _has_any(text, _CONVERSION_HARD_KEYWORDS): - fields.append("conversion_path") - - if payload.get("quality_critical_requirements") and "asset_source" not in fields: - fields.insert(0, "asset_source") - - return fields - - -def _completion_criteria(answers: dict[str, Any], fields: list[str], qcr: list[dict[str, str]]) -> list[str]: - goal = str(answers.get("goal") or "the stated goal").strip() or "the stated goal" - criteria = [ - f"The final experience must directly support: {goal}.", - "Core project, audience, goal, and vibe are reflected in content hierarchy and visual direction.", - ] - for field in fields: - criteria.append(f"Dynamic requirement '{field}' is answered and traceable in the final implementation.") - for requirement in qcr: - criteria.append(requirement["completion_criterion"]) - criteria.append("No placeholder remains as a final substitute for a quality-critical or core requirement.") - return criteria - - -def render_risk_summary(payload: dict[str, Any], locale: str = "en-US") -> str: - categories = payload.get("risk_categories") or [] - missing = payload.get("missing_dynamic_required_fields") or [] - readiness = payload.get("readiness") or {} - if locale.lower().startswith("ko"): - return ( - f"위험 범주: {', '.join(categories) if categories else '없음'}\n" - f"누락된 동적 필수 항목: {', '.join(missing) if missing else '없음'}\n" - f"전략: {readiness.get('strategy', 'unknown')} / 미리보기: {readiness.get('preview', 'unknown')} / " - f"구현: {readiness.get('implementation', 'unknown')}\n" - f"경계: {payload.get('placeholder_boundary', _PLACEHOLDER_BOUNDARY)}" - ) - return ( - f"Risk categories: {', '.join(categories) if categories else 'none'}\n" - f"Missing dynamic required fields: {', '.join(missing) if missing else 'none'}\n" - f"Readiness: strategy={readiness.get('strategy', 'unknown')}, " - f"preview={readiness.get('preview', 'unknown')}, implementation={readiness.get('implementation', 'unknown')}\n" - f"Boundary: {payload.get('placeholder_boundary', _PLACEHOLDER_BOUNDARY)}" - ) - - -def analyze_risk_readiness(answers: dict[str, Any], intent: dict[str, Any] | None = None) -> dict[str, Any]: - answers = dict(answers or {}) - intent = dict(intent or {}) if intent else None - combined_text = _flatten_text(answers) - risk_categories = _detect_categories(answers, None) - qcr = _quality_critical_requirements(combined_text) - - partial = {"risk_categories": risk_categories, "quality_critical_requirements": qcr, "answers": answers, "intent": intent or {}} - required = dynamic_required_fields(partial) - missing = [field for field in required if not _present(answers.get(field)) and not _present((intent or {}).get(field))] - placeholder_substitute = bool(qcr) and _has_placeholder_substitute_answer(answers, required) - - core_missing = [field for field in CORE_FIELDS if not _present(answers.get(field))] - if core_missing: - strategy = "blocked" - preview = "blocked" - implementation = "blocked" - status = "core_fields_missing" - elif missing or placeholder_substitute: - strategy = "ready" - preview = "ready_with_placeholders" - implementation = "blocked" - status = "implementation_blocked" - elif risk_categories or qcr: - strategy = "ready" - preview = "ready" - implementation = "ready_with_assumptions" - status = "ready_with_assumptions" - else: - strategy = "ready" - preview = "ready" - implementation = "ready" - status = "ready" - - safe_assumptions = [ - "Strategy may proceed from stated project, audience, goal, and vibe." if not core_missing else "No strategy assumption is safe until core fields are answered.", - ] - if missing or placeholder_substitute: - safe_assumptions.append("Preview may use clearly labeled placeholders only to show structure, scale, or interaction intent.") - elif risk_categories or qcr: - safe_assumptions.append("Implementation may proceed only if detected dependencies are satisfied by supplied answers or explicit approvals.") - - unsafe_assumptions = [ - "Do not treat a generic substitute as equivalent to a missing quality-critical requirement.", - "Do not replace required product photos or core visual assets with a generic gradient in final implementation.", - ] - if missing or placeholder_substitute: - unsafe_assumptions.append("Do not implement final production UI by substituting missing dynamic requirements with invented assets, behaviors, licenses, or runtime choices.") - if core_missing: - unsafe_assumptions.append("Do not infer missing core fields without user confirmation.") - - stop_conditions: list[str] = [] - if core_missing: - stop_conditions.append(f"Stop: core fields missing: {', '.join(core_missing)}.") - if missing: - stop_conditions.append(f"Stop before implementation: missing dynamic required fields: {', '.join(missing)}.") - stop_conditions.append(_SUBSTITUTE_STOP) - if placeholder_substitute: - stop_conditions.append("Stop before implementation: placeholder or generic substitute was supplied for a quality-critical requirement.") - stop_conditions.append(_SUBSTITUTE_STOP) - if qcr: - stop_conditions.append("Stop before final implementation if quality-critical assets are unavailable, unlicensed, or replaced by placeholders.") - - payload: dict[str, Any] = { - "schema_version": SCHEMA_VERSION, - "risk_categories": risk_categories, - "quality_critical_requirements": qcr, - "dynamic_required_fields": required, - "missing_dynamic_required_fields": missing, - "readiness": { - "strategy": strategy, - "preview": preview, - "implementation": implementation, - "status": status, - }, - "safe_assumptions": safe_assumptions, - "unsafe_assumptions": unsafe_assumptions, - "placeholder_boundary": _PLACEHOLDER_BOUNDARY, - "stop_conditions": stop_conditions, - "risk_summary": "", - "completion_criteria": _completion_criteria(answers, required, qcr), - } - blockers = list(stop_conditions) - if "brand_dependency" in risk_categories: - payload["unsafe_assumptions"].append( - "Treat named brands such as Toss/토스 as inspiration only; do not clone or copy their palette, logo, layout, or product flow." - ) - payload["completion_criteria"].append( - "Brand references are translated into original project-specific constraints without clone/copy behavior." - ) - if "conversion_business" in risk_categories: - payload["completion_criteria"].append( - "Conversion success is judged by trust, information structure, recovery states, and the stated business outcome, not by merely rendering a form." - ) - if "accessibility_performance" in risk_categories: - payload["completion_criteria"].append( - "Mobile or motion/media-heavy experiences define performance, accessibility, reduced motion, and fallback requirements before implementation." - ) - - # Compatibility aliases for init/session, show, and broad integration tests. - payload["level"] = "high" if status in {"implementation_blocked", "core_fields_missing"} else ("medium" if risk_categories or qcr else "low") - payload["risk_level"] = payload["level"] - payload["ready_for_strategy"] = strategy == "ready" - payload["ready_for_preview"] = True if preview == "ready" else ("placeholder" if preview == "ready_with_placeholders" else False) - payload["ready_for_implementation"] = implementation in {"ready", "ready_with_assumptions"} - payload["blockers"] = blockers - payload["required_fields"] = list(required) - payload["questions"] = [ - {"id": field, "field": field, "reason": "risk/quality driven follow-up required"} - for field in required - ] - payload["implementation_blocked"] = implementation == "blocked" - payload["placeholder_fallback_boundary"] = [_PLACEHOLDER_BOUNDARY] - payload["implementation_stop_conditions"] = stop_conditions - payload["core_quality_requirements"] = [str(item.get("requirement", "")) for item in qcr if isinstance(item, dict)] - payload["known_missing_information"] = missing - payload["completion_acceptance_criteria"] = payload["completion_criteria"] - payload["risk_summary"] = render_risk_summary(payload, locale=str(answers.get("locale") or "en-US")) - return payload - - -__all__ = ["analyze_risk_readiness", "render_risk_summary", "dynamic_required_fields"] diff --git a/plugins/codex-artic/skills/artic/scripts/scaffold_artic_files.py b/plugins/codex-artic/skills/artic/scripts/scaffold_artic_files.py index ddec398..dae725e 100755 --- a/plugins/codex-artic/skills/artic/scripts/scaffold_artic_files.py +++ b/plugins/codex-artic/skills/artic/scripts/scaffold_artic_files.py @@ -100,7 +100,7 @@ def scaffold(root: Path, project_name: str, locale: str = "en-US") -> None: "forbidden_copy_elements": ["logos", "trademarks", "proprietary illustrations", "exact layouts", "source copywriting"], } brief = { - "artic_version": "0.4.1", + "artic_version": "0.5.0", "project": { "name": project_name, "type": "homepage", @@ -139,7 +139,7 @@ def scaffold(root: Path, project_name: str, locale: str = "en-US") -> None: ], "synthesis": "Clean SaaS hierarchy with token discipline and mobile-first accessibility.", } - state = {"artic_version": "0.4.1", "last_generated_at": now, "status": "scaffolded", "language": language, "intent_path": ".artic/intent.json", "strategy_path": ".artic/strategy.json"} + state = {"artic_version": "0.5.0", "last_generated_at": now, "status": "scaffolded", "language": language, "intent_path": ".artic/intent.json", "strategy_path": ".artic/strategy.json"} write(root / ".artic" / "intent.json", json.dumps(intent, indent=2, ensure_ascii=False) + "\n") write(root / ".artic" / "strategy.json", json.dumps(strategy, indent=2, ensure_ascii=False) + "\n") write(root / ".artic" / "brief.json", json.dumps(brief, indent=2, ensure_ascii=False) + "\n") diff --git a/plugins/codex-artic/skills/artic/scripts/validate_artic_outputs.py b/plugins/codex-artic/skills/artic/scripts/validate_artic_outputs.py index 74da914..699aaaf 100755 --- a/plugins/codex-artic/skills/artic/scripts/validate_artic_outputs.py +++ b/plugins/codex-artic/skills/artic/scripts/validate_artic_outputs.py @@ -20,27 +20,6 @@ REQUIRED_SPACING_TOKENS = ("xs", "sm", "md", "lg", "xl", "section") REQUIRED_COMPONENT_TOKENS = ("button-primary", "button-secondary", "card", "form-field", "proof-strip") REQUIRED_QA_TERMS = ("Visual hierarchy", "Brand coherence", "Conversion clarity", "Mobile quality", "Accessibility", "Reference safety") -RISK_SECTIONS_EN = ( - "Risk / Readiness Summary", - "Core quality requirements", - "Known missing information", - "Safe assumptions", - "Unsafe assumptions", - "Placeholder/fallback boundary", - "Implementation stop conditions", - "Completion/acceptance criteria", -) -RISK_SECTIONS_KO = ( - "위험/준비 상태 요약", - "핵심 품질 요구사항", - "알려진 누락 정보", - "안전한 가정", - "위험한 가정", - "플레이스홀더/대체 경계", - "구현 중단 조건", - "완료/수용 기준", -) -RISK_DOCS = ("DESIGN.md", "docs/artic-strategy.md", "docs/design-rules.md", "docs/design-qa-checklist.md", "docs/homepage-design-prompt.md") POLICY_FRAGMENT = "extract reusable principles only" POLICY_MARKER = "" LANGUAGE_MARKER_PREFIX = "" not in text: errors.append(f"ERROR: localized outputs missing language marker: {brief_locale} in {rel}") - if declared_risk_readiness: - required_risk_sections = RISK_SECTIONS_KO if brief_locale.startswith("ko") else RISK_SECTIONS_EN - for rel in RISK_DOCS: - path = root / rel - if not path.exists(): - continue - text = path.read_text(encoding="utf-8") - for section in required_risk_sections: - if section not in text: - errors.append(f"ERROR: {rel} missing risk_readiness section: {section}") - if risk_implementation_blocked: - blocked_terms = [ - "implementation is blocked until missing inputs are resolved", - "누락된 정보가 해결될 때까지 구현은 차단됩니다", - ] - if not any(term in text for term in blocked_terms): - errors.append(f"ERROR: {rel} missing risk_readiness implementation block notice") checklist = root / "docs" / "design-qa-checklist.md" if checklist.exists(): text = checklist.read_text(encoding="utf-8") diff --git a/plugins/codex-artic/skills/artic/templates/brief.schema.json b/plugins/codex-artic/skills/artic/templates/brief.schema.json index 4a6ce89..4b93137 100644 --- a/plugins/codex-artic/skills/artic/templates/brief.schema.json +++ b/plugins/codex-artic/skills/artic/templates/brief.schema.json @@ -17,27 +17,6 @@ "requirements": {"type": "object"}, "constraints": {"type": "object"}, "asset_policy": {"type": "object"}, - "risk_readiness": { - "type": "object", - "required": ["schema_version", "risk_categories", "quality_critical_requirements", "dynamic_required_fields", "missing_dynamic_required_fields", "readiness", "safe_assumptions", "unsafe_assumptions", "placeholder_boundary", "stop_conditions", "risk_summary", "completion_criteria"], - "properties": { - "schema_version": {"const": "artic-risk-readiness-v1"}, - "risk_categories": {"type": "array", "items": {"type": "string"}}, - "quality_critical_requirements": {"type": "array"}, - "dynamic_required_fields": {"type": "array", "items": {"type": "string"}}, - "missing_dynamic_required_fields": {"type": "array", "items": {"type": "string"}}, - "readiness": { - "type": "object", - "required": ["strategy", "preview", "implementation", "status"] - }, - "safe_assumptions": {"type": "array", "items": {"type": "string"}}, - "unsafe_assumptions": {"type": "array", "items": {"type": "string"}}, - "placeholder_boundary": {"type": "string"}, - "stop_conditions": {"type": "array", "items": {"type": "string"}}, - "risk_summary": {"type": "string"}, - "completion_criteria": {"type": "array", "items": {"type": "string"}} - } - }, "implementation": {"type": "object"}, "language": { "type": "object", diff --git a/plugins/codex-artic/skills/artic/templates/strategy.schema.json b/plugins/codex-artic/skills/artic/templates/strategy.schema.json index 650be4f..08711ad 100644 --- a/plugins/codex-artic/skills/artic/templates/strategy.schema.json +++ b/plugins/codex-artic/skills/artic/templates/strategy.schema.json @@ -54,27 +54,6 @@ "forbidden_copy_elements": { "type": "array", "contains": {"const": "logos"} - }, - "risk_readiness": { - "type": "object", - "required": ["schema_version", "risk_categories", "quality_critical_requirements", "dynamic_required_fields", "missing_dynamic_required_fields", "readiness", "safe_assumptions", "unsafe_assumptions", "placeholder_boundary", "stop_conditions", "risk_summary", "completion_criteria"], - "properties": { - "schema_version": {"const": "artic-risk-readiness-v1"}, - "risk_categories": {"type": "array", "items": {"type": "string"}}, - "quality_critical_requirements": {"type": "array"}, - "dynamic_required_fields": {"type": "array", "items": {"type": "string"}}, - "missing_dynamic_required_fields": {"type": "array", "items": {"type": "string"}}, - "readiness": { - "type": "object", - "required": ["strategy", "preview", "implementation", "status"] - }, - "safe_assumptions": {"type": "array", "items": {"type": "string"}}, - "unsafe_assumptions": {"type": "array", "items": {"type": "string"}}, - "placeholder_boundary": {"type": "string"}, - "stop_conditions": {"type": "array", "items": {"type": "string"}}, - "risk_summary": {"type": "string"}, - "completion_criteria": {"type": "array", "items": {"type": "string"}} - } } }, "additionalProperties": true diff --git a/pyproject.toml b/pyproject.toml index bc56a5c..93b4348 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "artic" -version = "0.4.1" +version = "0.5.0" description = "Reference-driven AI-native design docs for homepage generation" readme = "README.md" requires-python = ">=3.9" diff --git a/skills/artic/SKILL.md b/skills/artic/SKILL.md index 1181f69..ef459ad 100644 --- a/skills/artic/SKILL.md +++ b/skills/artic/SKILL.md @@ -1,7 +1,7 @@ --- name: artic -description: "Use when creating or improving a homepage/website and design rules are missing or weak. Artic is an agent design-direction protocol: @artic init interviews the user, @artic start authors strategy artifacts, then the compiler produces AI-native DESIGN.md docs without copying protected brand assets." -version: 0.4.1 +description: "Use when creating or improving a homepage/website and design rules are missing or weak. Artic is an agent design-direction protocol: @artic init interviews the user, @artic start authors strategy artifacts, then the compiler produces AI-native DESIGN.md docs from reusable reference principles." +version: 0.5.0 author: Hermes Agent license: MIT metadata: @@ -12,7 +12,7 @@ metadata: # Artic -Artic is an agent design-direction protocol for AI-built websites: a contract-bound LLM design director that turns reference-informed judgment into implementation-ready design artifacts. +Artic is a reference-informed design-documentation protocol for AI-built websites: a contract-bound LLM design director that turns project intent and reusable reference principles into implementation-ready design artifacts before coding. Core flow: @@ -21,24 +21,16 @@ Core flow: → collect project/design intent → normalize into search facets → search professional and open-source design references -→ select and combine the best patterns +→ preserve a draft intake session @artic start → author `.artic/strategy.json` and `docs/artic-strategy.md` as the design-direction contract → run the compiler against that strategy → generate DESIGN.md and implementation guidance → validate outputs - -@artic show -→ render the strategy artifacts and sourced/generated assets into a provenance-recorded asset-first visual draft bundle -→ write `.artic/show/` preview files without modifying app source files - -@artic review -→ compare implementation against `.artic/strategy.json`, `docs/artic-strategy.md`, and `DESIGN.md` -→ report token, hierarchy, spacing, accessibility, CTA, mobile, and reference-safety drift ``` -Artic is not a generic design prompt or passive generator. Its core value is searching multiple professional/OSS design resources, extracting compatible patterns, and binding one project-specific AI-native design direction into a strategy contract. Scripts are validator/compiler/renderer helpers; they do not make the design judgment for the agent. +Artic is not a generic design prompt or passive generator. Its core value is searching multiple professional/OSS design resources, extracting compatible patterns, and binding one project-specific AI-native design direction into a strategy contract. Scripts are validator/compiler helpers; they do not make the design judgment for the agent. ## When to Use @@ -49,7 +41,7 @@ Use Artic when: - The user provides reference sites/images and wants the design improved. - The user wants AI-native docs before implementation. -Do not use Artic for exact cloning of a brand/site or copying logos, trademarks, proprietary illustrations, exact layouts, copywriting, or animations. +Do not use Artic for exact cloning of a brand/site or copying logos, trademarks, proprietary illustrations, exact layouts, copywriting, or animations. Use references to extract reusable design principles, then create original project-specific direction. ## Commands @@ -71,7 +63,7 @@ Language behavior: - When `@artic start` compiles the ready session, carry the language contract into `.artic/brief.json.language`, `.artic/state.json.language`, and generated docs using ``. - Preserve machine-readable terms such as `DESIGN.md`, `AI-native`, `Artic`, source names, and design token keys. - Localize user-facing interview questions and prose according to the language contract. -- Validate localized reference safety with the invariant `` marker, not exact English copy. +- Validate localized reference principle policy with the invariant `` marker, not exact English copy. Default interview questions: 1. What product/service is this homepage for? @@ -86,14 +78,11 @@ Default interview questions: 10. Tech stack? 11. Accessibility target? Default: WCAG AA. 12. Existing docs that must be reflected? -13. May Artic search/use owned or clearly licensed public assets, or should external sources stay as reference principles only? -External reference vs external asset boundary: +Reference boundary: - External references are design/runtime sources used for reusable principles, interaction patterns, accessibility/performance constraints, token relationships, and implementation guidance. -- External assets are concrete media that can appear in the generated site: GLB files, images, models, icons, textures, fonts, illustrations, or similar files. -- `@artic start` still treats external sources as reference-principles unless the user explicitly allows asset usage. -- `@artic show` may use sourced or generated assets for the preview bundle when doing so improves the first visual draft, but every asset must be recorded in `.artic/show/assets/manifest.json` and `.artic/show/report.json` with source/generation notes, license status when known, and preview-only status when unverified. -- Unverified assets are allowed only inside the preview bundle as provenance-recorded, preview-only material. Do not claim production license clearance; production/apply review belongs to a future `@artic apply` flow or a separate implementation review. +- Artic treats external sources as reference-principles for strategy and documentation, not as concrete assets to copy into the user's site. +- Use reference guidance as constructive creative direction: translate patterns into original tokens, hierarchy, components, motion, and information architecture. Fast path: if user says `@artic init quick`, ask only product, audience, goal, vibe, and references. @@ -142,31 +131,6 @@ Required behavior: Lifecycle transition rule: `@artic start` is the only transition that may finalize a ready init session. If `.artic/init-session.json` is `collecting`, stop and ask the remaining questions; if it is `ready`, finalize it and then generate docs. -### `@artic show` - -Purpose: render the current Artic strategy into a provenance-recorded, asset-first first visual draft bundle. The goal is a stronger strategy-grounded first visual draft by using sourced/generated assets when helpful, while recording where each asset came from and whether it is preview-only. - -Executable path for agents/hosts that expose shell-backed commands: - -```bash -python3 /scripts/artic_show.py --root -``` - -Required behavior: -1. Require `DESIGN.md`, `docs/homepage-design-prompt.md`, `.artic/brief.json`, `.artic/references.json`, and `.artic/strategy.json` from a completed `@artic start` flow. -2. Generate a strategy-grounded first visual draft bundle under `.artic/show/`, not just a minimal static preview. -3. Prefer asset-first draft quality: use user-provided, sourced, or generated images/models/icons/textures when they improve the first visual draft, and fall back to styled placeholders only when assets are unavailable or inappropriate. -4. Record provenance for every preview asset in `.artic/show/assets/manifest.json` and summarize asset decisions, gaps, and warnings in `.artic/show/report.json`. -5. Mark unverified assets as preview-only/provenance-recorded. Do not state or imply production license clearance for unverified sourced/generated assets. -6. Do not modify app/source files. Downstream files such as `app/page.tsx`, `src/App.tsx`, or `pages/index.tsx` are out of scope for `@artic show`; a future `@artic apply` or separate implementation review would handle production/apply decisions later. -7. Include the reference-safety policy in the preview so the preview remains reference-informed, not reference-copied. -8. If selected reference roles include `3d_runtime`, render a runtime-aware preview structure such as a `model-viewer`/Three.js placeholder, central interaction zone, poster/reduced-motion fallback notes, and asset provenance/license reminders instead of a generic landing-page-only preview. -9. Return JSON with at least `preview_file`, `bundle_dir`, `asset_manifest`, `report_file`, and `modified_app_files` for verification. - -### `@artic review` MVP-light - -After implementation, compare the current homepage against `.artic/strategy.json`, `docs/artic-strategy.md`, and `DESIGN.md`. Check token consistency, typography hierarchy, spacing rhythm, CTA hierarchy, mobile behavior, accessibility basics, and no-copy reference safety. The MVP review output contract may be chat-first; future hosts may persist `.artic/review.json` and `docs/artic-review.md`, but do not claim those files exist unless the review workflow writes them. - ### `@artic version` Purpose: report the installed Artic package versions and compare them with the latest GitHub release. @@ -201,7 +165,7 @@ Artic catalog entries are user-facing design intelligence, not an internal audit - Prefer wording such as “Use this for…”, “Apply its…”, “Translate… into project-specific…”, “Pair with…”, and “Keep identity, copy, and layout decisions original by…”. - Keep each entry source-specific; do not flatten the catalog into repeated boilerplate. -Use `application_guidance` for this product-facing catalog copy. Do not add `risk_notes` or other defensive/internal field names for new catalog work. +Use `application_guidance` for this product-facing catalog copy. Keep catalog guidance positive, specific, and application-oriented. Good pattern: ```text @@ -229,16 +193,16 @@ Synthesis rule: - Resolve conflicts based on project goal. - Preserve rationale in `docs/artic-brief.md`. -## Reference Safety +## Reference Principle Policy Allowed: token structure, color role relationships, typography hierarchy, spacing systems, component behavior, accessibility rules, motion principles. -Forbidden unless user owns rights: logos, trademarks, proprietary illustrations, exact page composition, exact brand palette as identity, copywriting, copyrighted imagery. +Not part of Artic's output: logos, trademarks, proprietary illustrations, exact page composition, exact brand palette as identity, copywriting, copyrighted imagery. Required phrase in generated docs: ```text -Reference policy: extract reusable principles only; do not copy logos, trademarks, proprietary illustrations, or exact layouts. +Reference policy: extract reusable principles only; create original project-specific direction. ``` ## Output Contract @@ -280,21 +244,6 @@ docs/design-qa-checklist.md docs/homepage-design-prompt.md ``` -`@artic show` creates a provenance-recorded preview bundle: - -```text -.artic/show/index.html -.artic/show/styles.css -.artic/show/tokens.json -.artic/show/assets/manifest.json -.artic/show/report.json -.artic/show/critique.md -.artic/show/selected.json -.artic/show/iterations//... -``` - -Show bundles are preview-only and do not modify app source files. Unverified sourced/generated assets may be included for first-draft visual quality only when marked preview-only and provenance-recorded; production/apply clearance is out of scope for `@artic show` and belongs to future `@artic apply`/implementation review. - Optional exports: `tailwind.theme.json`, `tokens.json`. ## Presets @@ -309,7 +258,7 @@ python3 /scripts/validate_artic_outputs.py --root python3 /scripts/search_reference_catalog.py --query "ai product developer saas premium" --limit 3 ``` -Validation, compilation, and rendering scripts enforce and materialize the contract. They are not a design judgment source; `@artic start` must provide that judgment through `.artic/strategy.json`. +Validation and compilation scripts enforce and materialize the contract. They are not a design judgment source; `@artic start` must provide that judgment through `.artic/strategy.json`. ## Common Pitfalls diff --git a/skills/artic/scripts/artic_init.py b/skills/artic/scripts/artic_init.py index 13f61ee..6433ec0 100644 --- a/skills/artic/scripts/artic_init.py +++ b/skills/artic/scripts/artic_init.py @@ -8,7 +8,6 @@ from pathlib import Path from design_intent_mapper import map_design_intent -from risk_readiness import analyze_risk_readiness from search_reference_catalog import load_catalog, search, terms POLICY_MARKER = "" @@ -242,32 +241,6 @@ def create_init_outputs(root: Path, args: argparse.Namespace) -> dict: requirements = parse_key_values(getattr(args, "requirement", [])) constraints = parse_key_values(getattr(args, "constraint", [])) policy = asset_policy_payload(str(getattr(args, "asset_policy", ""))) - risk_answers = { - "project": args.project, - "audience": args.audience, - "goal": args.goal, - "vibe": args.vibe, - "references": args.references, - "stack": args.stack, - "accessibility": args.accessibility, - "asset_policy": str(getattr(args, "asset_policy", "")), - **requirements, - **constraints, - } - risk_answers["locale"] = args.locale - if risk_answers.get("stack") and not risk_answers.get("technical_runtime"): - risk_answers["technical_runtime"] = risk_answers["stack"] - elif not risk_answers.get("technical_runtime"): - vibe_text = str(risk_answers.get("vibe") or "") - if re.search(r"runtime|런타임|webgl|model-viewer|3d", vibe_text, re.IGNORECASE): - risk_answers["technical_runtime"] = vibe_text - if risk_answers.get("asset_policy") and not risk_answers.get("license_clearance"): - risk_answers["license_clearance"] = risk_answers["asset_policy"] - interaction_answer = str(risk_answers.get("interaction_model") or "") - if interaction_answer and not risk_answers.get("performance_accessibility_plan"): - if re.search(r"reduced motion|reduced-motion|keyboard|키보드|대체|fallback|alt|접근성|성능|load|loading", interaction_answer, re.IGNORECASE): - risk_answers["performance_accessibility_plan"] = interaction_answer - risk_readiness = analyze_risk_readiness(risk_answers, intent) source_plan = [ { "source_id": src["id"], @@ -280,7 +253,7 @@ def create_init_outputs(root: Path, args: argparse.Namespace) -> dict: ] brief = { - "artic_version": "0.4.1", + "artic_version": "0.5.0", "project": { "name": normalized_project["name"], "type": "homepage", @@ -303,7 +276,6 @@ def create_init_outputs(root: Path, args: argparse.Namespace) -> dict: "constraints": constraints, "asset_policy": policy, "implementation": {"stack": args.stack or "unspecified", "mobile_first": "mobile" in args.vibe.lower(), "accessibility": args.accessibility}, - "risk_readiness": risk_readiness, "language": lang, "copy_policy": "artic-policy: reference-safety-v1", } @@ -314,7 +286,7 @@ def create_init_outputs(root: Path, args: argparse.Namespace) -> dict: "source_plan": source_plan, "synthesis": "Use selected sources as compatible patterns; localize prose according to the brief language contract while preserving source names and protected terms.", } - state = {"artic_version": "0.4.1", "last_generated_at": now, "status": "initialized", "language": lang, "intent_path": ".artic/intent.json"} + state = {"artic_version": "0.5.0", "last_generated_at": now, "status": "initialized", "language": lang, "intent_path": ".artic/intent.json"} write(root / ".artic" / "intent.json", json.dumps(intent, indent=2, ensure_ascii=False) + "\n") write(root / ".artic" / "brief.json", json.dumps(brief, indent=2, ensure_ascii=False) + "\n") diff --git a/skills/artic/scripts/artic_init_session.py b/skills/artic/scripts/artic_init_session.py index 9007921..4691a94 100644 --- a/skills/artic/scripts/artic_init_session.py +++ b/skills/artic/scripts/artic_init_session.py @@ -8,22 +8,12 @@ from typing import Any from locale_contract import detect_locale_from_text, language_contract -from risk_readiness import analyze_risk_readiness as analyze_risk_readiness_contract SESSION_PATH = Path(".artic") / "init-session.json" REQUIRED_FIELDS = ["project", "audience", "goal", "vibe"] -OPTIONAL_FIELDS = ["references", "stack", "avoid", "accessibility", "asset_policy"] -DYNAMIC_REQUIRED_FIELDS = [ - "asset_source", - "interaction_model", - "brand_constraints", - "conversion_details", - "performance_accessibility", - "asset_policy", - "license_policy", -] +OPTIONAL_FIELDS = ["references", "stack", "avoid", "accessibility"] CORE_ANSWER_FIELDS = set(REQUIRED_FIELDS + OPTIONAL_FIELDS) -CONSTRAINT_FIELD_HINTS = ("constraint", "constraints", "brand", "avoid", "asset_policy", "license_policy") +CONSTRAINT_FIELD_HINTS = ("constraint", "constraints", "brand", "avoid") QUESTION_SPECS: dict[str, dict[str, str]] = { "project": { @@ -66,69 +56,11 @@ "zh-CN": "目标技术栈是什么?", "zh-TW": "目標技術棧是什麼?", }, - "asset_policy": { - "intent": "Clarify whether external assets may be used or only referenced as principles.", - "en-US": "May Artic search/use owned or clearly licensed public assets, or should external sources stay as reference principles only?", - "ko-KR": "소유한 에셋 또는 라이선스 확인 가능한 공개 에셋을 검색/사용해도 되나요, 아니면 외부 소스는 원칙 참고로만 사용할까요?", - "ja-JP": "所有アセットまたはライセンス確認済みの公開アセットを検索/使用してよいですか、それとも外部ソースは原則の参考のみにしますか?", - "zh-CN": "是否可以搜索/使用自有资产或可验证授权的公开资产,还是外部来源仅作为原则参考?", - "zh-TW": "是否可以搜尋/使用自有資產或可驗證授權的公開資產,或外部來源只作為原則參考?", - }, - "asset_source": { - "intent": "Identify source and availability of required visual/3D assets.", - "en-US": "What is the source for required images, video, or 3D assets (owned files, generated placeholders, or licensed public assets)?", - "ko-KR": "필요한 이미지/영상/3D 에셋은 어디서 오나요? 자체 보유 파일, 임시 플레이스홀더, 라이선스 확인 가능한 공개 에셋 중 무엇인가요?", - }, - "interaction_model": { - "intent": "Clarify implementation-critical interaction behavior.", - "en-US": "How should key interactions work, including mouse/touch/keyboard behavior and fallbacks?", - "ko-KR": "핵심 상호작용은 어떻게 동작해야 하나요? 마우스/터치/키보드 조작과 대체 경험까지 알려주세요.", - }, - "brand_constraints": { - "intent": "Capture brand/legal constraints that affect design execution.", - "en-US": "What brand, legal, tone, color, logo, or copy constraints must the design follow?", - "ko-KR": "디자인이 따라야 할 브랜드/법무/톤/색상/로고/문구 제약은 무엇인가요?", - }, - "brand_assets": { - "intent": "Capture brand assets and brand-system constraints.", - "en-US": "What logo, color, font, or brand-system assets must the design follow?", - "ko-KR": "디자인이 따라야 할 로고, 색상, 폰트, 브랜드 시스템 에셋은 무엇인가요?", - }, - "conversion_details": { - "intent": "Clarify conversion path details.", - "en-US": "What exact CTA, form fields, destination, or success criteria should the conversion flow use?", - "ko-KR": "전환 흐름의 정확한 CTA, 폼 필드, 이동 경로, 성공 기준은 무엇인가요?", - }, - "conversion_path": { - "intent": "Clarify conversion path details.", - "en-US": "What exact CTA, form fields, destination, trust requirements, or success criteria should the conversion flow use?", - "ko-KR": "전환 흐름의 정확한 CTA, 폼 필드, 이동 경로, 신뢰 조건, 성공 기준은 무엇인가요?", - }, - "performance_accessibility": { - "intent": "Clarify performance and accessibility constraints for risky media or interaction.", - "en-US": "What performance and accessibility requirements apply (load budget, reduced motion, keyboard support, alt/fallback content)?", - "ko-KR": "성능/접근성 요구는 무엇인가요? 로딩 예산, reduced motion, 키보드 지원, 대체 콘텐츠를 알려주세요.", - }, - "performance_accessibility_plan": { - "intent": "Clarify performance and accessibility constraints for risky media or interaction.", - "en-US": "What performance and accessibility plan applies (load budget, reduced motion, keyboard support, alt/fallback content)?", - "ko-KR": "성능/접근성 계획은 무엇인가요? 로딩 예산, reduced motion, 키보드 지원, 대체 콘텐츠를 알려주세요.", - }, - "license_policy": { - "intent": "Clarify licensing and attribution obligations.", - "en-US": "What license and attribution policy should Artic follow for any third-party assets or references?", - "ko-KR": "제3자 에셋/레퍼런스의 라이선스와 출처 표기는 어떤 정책을 따라야 하나요?", - }, - "license_clearance": { - "intent": "Clarify licensing and attribution obligations.", - "en-US": "What license clearance and attribution policy should Artic follow for any third-party assets or references?", - "ko-KR": "제3자 에셋/레퍼런스의 라이선스 확인과 출처 표기는 어떤 정책을 따라야 하나요?", - }, } _STYLE_HINT_RE = re.compile(r"토스|신뢰|고급|깔끔|스타트업|모바일|saas|premium|trust|clean|mobile|startup", re.IGNORECASE) _LABELED_ANSWER_RE = re.compile( - r"(?:^|[.;\n]\s*)(project|product|service|audience|target user|target users|goal|conversion goal|vibe|impression|style|stack|tech stack|references?|avoid|accessibility|asset policy)\s*:\s*(.*?)(?=(?:[.;\n]\s*)(?:project|product|service|audience|target user|target users|goal|conversion goal|vibe|impression|style|stack|tech stack|references?|avoid|accessibility|asset policy)\s*:|$)", + r"(?:^|[.;\n]\s*)(project|product|service|audience|target user|target users|goal|conversion goal|vibe|impression|style|stack|tech stack|references?|avoid|accessibility)\s*:\s*(.*?)(?=(?:[.;\n]\s*)(?:project|product|service|audience|target user|target users|goal|conversion goal|vibe|impression|style|stack|tech stack|references?|avoid|accessibility)\s*:|$)", re.IGNORECASE | re.DOTALL, ) _LABELED_FIELD_MAP = { @@ -209,18 +141,6 @@ def _answer_corpus(answers: dict[str, str]) -> str: return "\n".join(str(value) for value in answers.values() if value).lower() -def analyze_risk_readiness(answers: dict[str, str], missing_core: list[str]) -> dict[str, Any]: - """Return the canonical Artic risk/readiness contract for dynamic init follow-ups.""" - payload = analyze_risk_readiness_contract(answers) - if missing_core: - payload["missing_core_fields"] = list(missing_core) - payload["readiness"] = {"strategy": "blocked", "preview": "blocked", "implementation": "blocked", "status": "core_fields_missing"} - payload["ready_for_strategy"] = False - payload["ready_for_preview"] = False - payload["ready_for_implementation"] = False - payload["implementation_blocked"] = True - return payload - def create_or_update_session(root: Path, user_text: str, explicit_locale: str | None = None, answers: dict[str, str] | None = None) -> dict[str, Any]: existing: dict[str, Any] @@ -246,36 +166,13 @@ def create_or_update_session(root: Path, user_text: str, explicit_locale: str | bilingual_terms=bool(previous_language.get("bilingual_terms", False)), ) missing = missing_required_fields(merged_answers) - risk_answers = dict(merged_answers) - if risk_answers.get("stack") and not risk_answers.get("technical_runtime"): - risk_answers["technical_runtime"] = risk_answers["stack"] - elif not risk_answers.get("technical_runtime"): - vibe_text = str(risk_answers.get("vibe") or "") - if re.search(r"runtime|런타임|webgl|model-viewer|3d", vibe_text, re.IGNORECASE): - risk_answers["technical_runtime"] = vibe_text - if risk_answers.get("asset_policy") and not risk_answers.get("license_clearance"): - risk_answers["license_clearance"] = risk_answers["asset_policy"] - interaction_answer = str(risk_answers.get("interaction_model") or "") - if interaction_answer and not risk_answers.get("performance_accessibility_plan"): - if re.search(r"reduced motion|reduced-motion|keyboard|키보드|대체|fallback|alt|접근성|성능|load|loading", interaction_answer, re.IGNORECASE): - risk_answers["performance_accessibility_plan"] = interaction_answer - risk_readiness = analyze_risk_readiness(risk_answers, missing) - canonical_missing_dynamic = [str(field) for field in risk_readiness.get("missing_dynamic_required_fields", [])] - missing_dynamic = list(canonical_missing_dynamic) - if "license_clearance" in canonical_missing_dynamic and "asset_policy" not in missing_dynamic: - missing_dynamic.append("asset_policy") - if "asset_source" in canonical_missing_dynamic and not merged_answers.get("asset_policy") and "asset_policy" not in missing_dynamic: - missing_dynamic.append("asset_policy") - last_question_ids = (missing + missing_dynamic)[:6] + last_question_ids = missing[:6] session = { "schema_version": 1, "status": "ready" if not missing else "collecting", "language": lang, "answers": merged_answers, "missing": missing, - "risk_readiness": risk_readiness, - "missing_dynamic_required_fields": missing_dynamic, - "readiness": {key: risk_readiness["readiness"].get(key) for key in ("strategy", "preview", "implementation")}, "last_question_ids": last_question_ids, } write_session(root, session) @@ -291,7 +188,6 @@ def render_questions(session: dict[str, Any], limit: int = 4) -> list[str]: field_ids = [str(field) for field in raw_ids] else: field_ids = [str(field) for field in session.get("missing", [])] - field_ids.extend(str(field) for field in session.get("missing_dynamic_required_fields", []) if str(field) not in field_ids) questions: list[str] = [] for field in field_ids[:limit]: spec = QUESTION_SPECS.get(str(field), {}) @@ -333,13 +229,6 @@ def render_ready_summary(session: dict[str, Any]) -> str: raw_answers = session.get("answers") answers: dict[str, Any] = raw_answers if isinstance(raw_answers, dict) else {} locale = str(language.get("locale") or "en-US") - raw_risk = session.get("risk_readiness") - risk: dict[str, Any] = raw_risk if isinstance(raw_risk, dict) else {} - raw_readiness = session.get("readiness") - readiness: dict[str, Any] = raw_readiness if isinstance(raw_readiness, dict) else {} - risk_level = str(risk.get("risk_level") or "low") - missing_dynamic = [str(field) for field in session.get("missing_dynamic_required_fields", [])] - signal_names = [str(row.get("signal")) for row in risk.get("signals", []) if isinstance(row, dict) and row.get("signal")] if locale.startswith("ko"): lines = [ "필수 정보는 충분히 모였습니다.", @@ -350,27 +239,9 @@ def render_ready_summary(session: dict[str, Any]) -> str: f"- 목표: {answers.get('goal', '')}", f"- 무드: {answers.get('vibe', '')}", "", - f"리스크 요약: {risk_level}" - + (f" ({', '.join(signal_names[:4])})" if signal_names else ""), - ] - if missing_dynamic: - lines.extend([ - f"구현 차단: 전략 문서는 시작할 수 있지만 실제 구현은 추가 확인이 필요합니다: {', '.join(missing_dynamic)}.", - "플레이스홀더/원칙 참고 경계는 유지되며, 에셋 사용을 명시적으로 허용하지 않으면 외부 소스는 원칙 참고로만 사용합니다.", - ]) - elif readiness.get("implementation") == "blocked": - lines.extend([ - "구현 차단: 핵심 품질 요구가 플레이스홀더나 낮은 품질 대체물에 의존하고 있어 구현 전에 해결해야 합니다.", - "플레이스홀더/원칙 참고 경계는 유지되며, 필요한 입력을 검증할 때까지 프로덕션 구현으로 진행하지 않습니다.", - ]) - elif readiness.get("implementation") == "ready_with_assumptions": - lines.append("구현 준비: 진행 가능하지만 성능/접근성 세부값은 보수적 기본값으로 가정합니다.") - lines.extend([ - "", - "더 다듬고 싶으면 레퍼런스, 피해야 할 스타일, 브랜드/에셋 제약을 추가로 알려주세요.", - "에셋 사용을 명시적으로 허용하지 않으면 외부 소스는 원칙 참고로만 사용합니다.", + "더 다듬고 싶으면 레퍼런스, 피해야 할 스타일, 브랜드 제약을 추가로 알려주세요.", "문서 생성을 시작하려면 `@artic start`를 실행하세요.", - ]) + ] else: lines = [ "The required Artic intake is ready.", @@ -381,27 +252,9 @@ def render_ready_summary(session: dict[str, Any]) -> str: f"- Goal: {answers.get('goal', '')}", f"- Vibe: {answers.get('vibe', '')}", "", - f"Risk summary: {risk_level}" - + (f" ({', '.join(signal_names[:4])})" if signal_names else ""), - ] - if missing_dynamic: - lines.extend([ - f"Implementation blocker: strategy docs may proceed, but implementation needs follow-up on: {', '.join(missing_dynamic)}.", - "Placeholder/reference boundaries remain active; external assets stay reference-principles only unless explicitly allowed.", - ]) - elif readiness.get("implementation") == "blocked": - lines.extend([ - "Implementation blocker: a quality-critical requirement depends on a placeholder or lower-quality substitute.", - "Placeholder/reference boundaries remain active; do not proceed to production implementation until the required input is verified.", - ]) - elif readiness.get("implementation") == "ready_with_assumptions": - lines.append("Implementation readiness: ready with conservative performance/accessibility assumptions.") - lines.extend([ - "", - "Add references, avoided styles, brand constraints, or asset policy if you want to refine the brief.", - "If you do not explicitly allow asset usage, external sources stay reference-principles only.", + "Add references, avoided styles, or brand constraints if you want to refine the brief.", "To generate Artic design docs, run `@artic start`.", - ]) + ] return "\n".join(lines) diff --git a/skills/artic/scripts/artic_show.py b/skills/artic/scripts/artic_show.py deleted file mode 100644 index 3ae2ad6..0000000 --- a/skills/artic/scripts/artic_show.py +++ /dev/null @@ -1,833 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import argparse -import html -import json -import re -import shutil -import sys -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -REQUIRED_INPUTS = [ - "DESIGN.md", - "docs/homepage-design-prompt.md", - ".artic/brief.json", - ".artic/references.json", - ".artic/strategy.json", -] -POLICY_FALLBACK = "Reference policy: extract reusable principles only; do not copy logos, trademarks, proprietary illustrations, or exact layouts." -POLICY_BY_LOCALE = { - "ko-KR": "참고 정책: 재사용 가능한 원칙만 추출하고, 로고, 상표, 독점 일러스트, 정확한 레이아웃은 복사하지 않습니다.", -} -SCORE_KEYS = [ - "strategy_alignment", - "visual_fidelity", - "asset_richness", - "asset_provenance_completeness", - "conversion_clarity", - "visual_specificity", - "accessibility_basics", - "mobile_first", - "genericness_penalty", - "overall", -] - - -def read_json(path: Path) -> dict[str, Any]: - try: - payload = json.loads(path.read_text(encoding="utf-8")) - except FileNotFoundError as exc: - raise ValueError(f"missing required input: {path}") from exc - except json.JSONDecodeError as exc: - raise ValueError(f"invalid JSON in {path}: {exc}") from exc - if not isinstance(payload, dict): - raise ValueError(f"expected JSON object in {path}") - return payload - - -def write_json(path: Path, payload: Any) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - - -def write_text(path: Path, content: str) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding="utf-8") - - -def required_missing(root: Path) -> list[str]: - return [rel for rel in REQUIRED_INPUTS if not (root / rel).exists()] - - -def frontmatter(text: str) -> str: - if not text.startswith("---\n"): - return "" - end = text.find("\n---\n", 4) - if end == -1: - return "" - return text[4:end] - - -def yaml_value(block: str, key: str, default: str) -> str: - match = re.search(rf"^\s*{re.escape(key)}\s*:\s*[\"']?([^\n\"']+)[\"']?\s*$", block, re.MULTILINE) - return match.group(1).strip() if match else default - - -def yaml_section_value(block: str, section: str, token: str, default: str) -> str: - lines = block.splitlines() - in_section = False - for line in lines: - if re.match(rf"^{re.escape(section)}\s*:", line): - in_section = True - continue - if in_section and line and not line.startswith(" "): - break - if in_section: - match = re.match(rf"^\s+{re.escape(token)}\s*:\s*[\"']?([^\n\"']+)[\"']?\s*$", line) - if match: - return match.group(1).strip() - return default - - -def safe_css_color(value: str, fallback: str) -> str: - candidate = value.strip() - safe_patterns = [ - r"#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?(?:[0-9a-fA-F]{2})?", - r"(?:rgb|rgba|hsl|hsla)\([0-9.%,\s]+\)", - r"[a-zA-Z]+", - ] - if any(re.fullmatch(pattern, candidate) for pattern in safe_patterns): - return candidate - return fallback - - -def markdown_section(text: str, heading: str) -> str: - marker = f"## {heading}" - start = text.find(marker) - if start == -1: - return "" - next_heading = text.find("\n## ", start + len(marker)) - end = next_heading if next_heading != -1 else len(text) - section = text[start + len(marker):end] - return " ".join(line.strip(" #") for line in section.splitlines() if line.strip()) - - -def selected_source_names(references: dict[str, Any]) -> list[str]: - selected = references.get("selected_sources", []) - if not isinstance(selected, list): - return [] - names: list[str] = [] - for row in selected: - if not isinstance(row, dict): - continue - name = str(row.get("name") or row.get("id") or "reference source").strip() - if name: - names.append(name) - return names - - -def brief_locale(brief: dict[str, Any]) -> str: - language = brief.get("language") - if isinstance(language, dict): - return str(language.get("locale") or "en-US") - return "en-US" - - -def has_3d_runtime_reference(brief: dict[str, Any], references: dict[str, Any]) -> bool: - roles = references.get("role_assignments", []) - if isinstance(roles, list): - for role in roles: - if isinstance(role, dict) and "3d_runtime" == str(role.get("role")): - return True - for bucket in (brief.get("style"), references, brief): - text = json.dumps(bucket, ensure_ascii=False).lower() if isinstance(bucket, (dict, list)) else str(bucket).lower() - if any(term in text for term in ("3d", "webgl", "glb", "model-viewer", "runtime")): - return True - return False - - -def _string_items(value: Any) -> list[str]: - if value is None: - return [] - if isinstance(value, str): - text = value.strip() - return [text] if text else [] - if isinstance(value, list): - items: list[str] = [] - for item in value: - items.extend(_string_items(item)) - return items - if isinstance(value, dict): - for key in ("summary", "description", "requirement", "condition", "reason", "message", "title", "name"): - if value.get(key): - return _string_items(value.get(key)) - return [json.dumps(value, ensure_ascii=False, sort_keys=True)] - return [str(value)] - - -def _has_missing_quality(value: Any) -> bool: - missing_markers = ("missing", "blocked", "incomplete", "not_met", "failed", "fail", "placeholder") - if isinstance(value, dict): - status = str(value.get("status") or value.get("state") or value.get("readiness") or "").lower() - if any(marker in status for marker in missing_markers): - return True - return any(_has_missing_quality(item) for item in value.values()) - if isinstance(value, list): - return any(_has_missing_quality(item) for item in value) - if isinstance(value, str): - return any(marker in value.lower() for marker in missing_markers) - return False - - -def risk_readiness_summary(brief: dict[str, Any]) -> dict[str, Any]: - risk = brief.get("risk_readiness") - if not isinstance(risk, dict): - return { - "preview_status": "static_preview", - "production_ready": True, - "placeholder_boundaries": [], - "implementation_blockers": [], - } - - placeholder_boundaries = _string_items(risk.get("placeholder_boundary")) + _string_items(risk.get("placeholder_fallback_boundary")) - implementation_blockers = _string_items(risk.get("stop_conditions")) + _string_items(risk.get("implementation_stop_conditions")) - - readiness = str(risk.get("readiness") or "").lower() - implementation_blocked = bool(risk.get("implementation_blocked")) - readiness_blocked = any(marker in readiness for marker in ("blocked", "placeholder", "not_ready", "not-ready")) - quality_missing = _has_missing_quality(risk.get("quality_critical_requirements")) or _has_missing_quality(risk.get("core_quality_requirements")) - production_ready = not (implementation_blocked or readiness_blocked or bool(placeholder_boundaries) or bool(implementation_blockers) or quality_missing) - - return { - "preview_status": "static_preview" if production_ready else "placeholder_preview", - "production_ready": production_ready, - "placeholder_boundaries": list(dict.fromkeys(placeholder_boundaries)), - "implementation_blockers": list(dict.fromkeys(implementation_blockers)), - } - - -def localized_copy(locale: str) -> dict[str, str]: - if locale.startswith("ko"): - return { - "preview_badge": "Artic 미리보기 · 에셋 우선 번들", - "for_label": "대상", - "headline_suffix": "첫 시각 초안", - "drive": "목표 이끌기", - "view_logic": "디자인 논리 보기", - "north_star": "Design north star", - "accessibility": "접근성 목표", - "reference_roles": "레퍼런스 역할", - "promise_title": "1. 약속", - "promise_body": "프로젝트 고유의 약속과 가장 중요한 전환 경로를 먼저 보여줍니다.", - "proof_title": "2. 증거", - "proof_body": "신뢰 근거와 에셋 출처를 hero 가까이에 배치해 첫 CTA를 뒷받침합니다.", - "system_title": "3. 시스템", - "system_body": "토큰, 간격 리듬, 시맨틱 컨트롤, 독창적 구성을 Artic 문서에서 가져옵니다.", - "reference_title": "레퍼런스 기반, 복제 아님", - "reference_body": "이 정적 번들은 Artic start 산출물에서 생성됩니다. 앱 소스 파일은 건드리지 않습니다.", - "policy_title": "참고 정책", - "generated": "생성 시각", - "root": "루트", - "model_label": "3D 모델 자리표시자", - "model_body": "실제 GLB/이미지/모델은 소유 에셋 또는 라이선스 확인 가능한 공개 에셋만 연결합니다. 이 preview-only 에셋은 poster fallback, reduced motion, 로딩 실패 상태를 검토하기 위한 구조입니다.", - "interaction_title": "상호작용 영역", - "interaction_body": "드래그/탭/키보드 포커스로 3D 오브젝트를 탐색하고, 실패 시 정적 포스터와 설명 텍스트를 제공합니다.", - } - return { - "preview_badge": "Artic Preview · asset-first bundle", - "for_label": "For", - "headline_suffix": "first visual draft", - "drive": "Drive", - "view_logic": "View design logic", - "north_star": "Design north star", - "accessibility": "accessibility target", - "reference_roles": "reference roles", - "promise_title": "1. Promise", - "promise_body": "Lead with one clear project-specific promise and one dominant conversion path.", - "proof_title": "2. Proof", - "proof_body": "Place evidence and asset provenance close to the hero so trust supports the first CTA.", - "system_title": "3. System", - "system_body": "Use tokens, spacing rhythm, semantic controls, and original composition from the Artic docs.", - "reference_title": "Reference-informed, not reference-copied", - "reference_body": "This static bundle is generated from Artic start outputs. It intentionally leaves app source files untouched.", - "policy_title": "Reference policy", - "generated": "Generated", - "root": "Root", - "model_label": "3D asset placeholder", - "model_body": "Attach only user-owned or license-verifiable public GLB/image/model assets when allowed. This preview-only asset validates poster fallback, reduced motion, and loading failure states.", - "interaction_title": "Interaction zone", - "interaction_body": "Design drag, tap, and keyboard focus affordances for the central 3D object, with a static poster and description fallback when loading fails.", - } - - -def project_name(brief: dict[str, Any], design_frontmatter: str) -> str: - project = brief.get("project") - if isinstance(project, dict): - value = str(project.get("name") or project.get("description") or "").strip() - if value: - return value - return yaml_value(design_frontmatter, "name", "Artic Preview") - - -def build_tokens(design_text: str) -> dict[str, Any]: - fm = frontmatter(design_text) - colors = { - "primary": safe_css_color(yaml_section_value(fm, "colors", "primary", "#1F4FD8"), "#1F4FD8"), - "accent": safe_css_color(yaml_section_value(fm, "colors", "accent", "#7C3AED"), "#7C3AED"), - "surface": safe_css_color(yaml_section_value(fm, "colors", "surface", "#FFFFFF"), "#FFFFFF"), - "neutral": safe_css_color(yaml_section_value(fm, "colors", "neutral", "#F6F8FB"), "#F6F8FB"), - "text": safe_css_color(yaml_section_value(fm, "colors", "text", "#111827"), "#111827"), - "muted": safe_css_color(yaml_section_value(fm, "colors", "muted", "#6B7280"), "#6B7280"), - "border": safe_css_color(yaml_section_value(fm, "colors", "border", "#DDE3EA"), "#DDE3EA"), - } - return { - "colors": colors, - "type": { - "family": yaml_section_value(fm, "typography", "fontFamily", "Inter, ui-sans-serif, system-ui"), - "hero": "clamp(3rem, 8vw, 5.7rem)", - "heading": "clamp(2rem, 4vw, 3rem)", - "body": "1rem", - }, - "radius": {"sm": "8px", "md": "14px", "lg": "24px", "xl": "32px", "pill": "999px"}, - "spacing": {"xs": "4px", "sm": "8px", "md": "16px", "lg": "24px", "xl": "40px", "section": "96px"}, - "shadow": {"card": "0 24px 80px rgba(15,23,42,.08)", "cta": "0 18px 40px rgba(31,79,216,.22)"}, - "motion": {"duration": "180ms", "easing": "cubic-bezier(.2,.8,.2,1)", "reduced_motion": "preserve layout without parallax"}, - } - - -def variant_names(max_iterations: int, runtime_3d: bool) -> list[str]: - pool = ["asset-hero", "conversion-proof", "immersive-runtime" if runtime_3d else "editorial-system"] - return pool[:max_iterations] - - -def make_placeholder_svg(kind: str, tokens: dict[str, Any]) -> str: - c = tokens["colors"] - title = "Model Poster" if kind == "model" else "Scene Fallback" - label = "preview-only 3D poster" if kind == "model" else "preview-only runtime fallback" - shape = "" if kind == "model" else "" - return f""" - - - - {shape} - {html.escape(title)} - {html.escape(label)} - -""" - - -def build_asset_manifest(root: Path, show_dir: Path, iteration: str, variant: str, brief: dict[str, Any], references: dict[str, Any], tokens: dict[str, Any], asset_mode: str, runtime_3d: bool) -> dict[str, Any]: - assets_dir = show_dir / "assets" - placeholders_dir = assets_dir / "placeholders" - sources = selected_source_names(references) - assets: list[dict[str, Any]] = [] - for idx, source in enumerate(sources or ["artic-generated-visual-direction"], start=1): - assets.append({ - "id": f"catalog-reference-{idx}", - "path": None, - "type": "catalog-reference", - "status": "catalog-reference", - "retrieval": "catalog-reference", - "provenance": "selected Artic reference source", - "kind": "catalog-reference", - "title": source, - "source": source, - "license": "unknown", - "license_status": "unknown-unverified", - "usage": "preview-only visual direction and provenance record; do not copy protected marks or exact layouts", - "downloaded": False, - "local_path": None, - }) - hero_name = f"hero-{variant}.svg" - write_text(placeholders_dir / hero_name, make_placeholder_svg("scene", tokens)) - assets.append({ - "id": f"generated-{variant}-hero", - "path": f"assets/placeholders/{hero_name}", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": f"{variant} hero placeholder", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "safe first-draft hero artwork", - "downloaded": False, - "local_path": f"assets/placeholders/{hero_name}", - }) - if runtime_3d: - model_svg = make_placeholder_svg("model", tokens) - scene_svg = make_placeholder_svg("scene", tokens) - write_text(placeholders_dir / "model-poster.svg", model_svg) - write_text(placeholders_dir / "scene-fallback.svg", scene_svg) - write_text(assets_dir / "model-poster.svg", model_svg) - write_text(assets_dir / "scene-fallback.svg", scene_svg) - assets.extend([ - { - "id": "generated-model-poster", - "path": "assets/placeholders/model-poster.svg", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": "3D model poster placeholder", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "3D poster fallback before verified GLB/model asset is supplied", - "downloaded": False, - "local_path": "assets/placeholders/model-poster.svg", - }, - { - "id": "generated-model-poster-root-alias", - "path": "assets/model-poster.svg", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": "3D model poster placeholder root alias", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "backward-compatible 3D poster fallback alias", - "downloaded": False, - "local_path": "assets/model-poster.svg", - }, - { - "id": "generated-scene-fallback", - "path": "assets/placeholders/scene-fallback.svg", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": "3D scene fallback placeholder", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "runtime failure and reduced-motion scene fallback", - "downloaded": False, - "local_path": "assets/placeholders/scene-fallback.svg", - }, - { - "id": "generated-scene-fallback-root-alias", - "path": "assets/scene-fallback.svg", - "type": "image/svg+xml", - "status": "generated", - "retrieval": "generated-placeholder", - "provenance": "generated locally by Artic show", - "kind": "generated-placeholder", - "title": "3D scene fallback placeholder root alias", - "source": "generated locally by Artic show", - "license": "Artic generated preview placeholder", - "license_status": "preview-only-generated", - "usage": "backward-compatible runtime failure fallback alias", - "downloaded": False, - "local_path": "assets/scene-fallback.svg", - }, - ]) - return { - "schema_version": 1, - "mode": "asset-first-preview", - "asset_mode": asset_mode, - "iteration": iteration, - "variant": variant, - "root": str(root.resolve()), - "created_at": datetime.now(timezone.utc).isoformat(), - "notes": ["Unknown or unverified sources are recorded as preview-only/catalog-reference and do not block preview generation."], - "assets": assets, - } - - -def render_css(tokens: dict[str, Any], variant: str, runtime_3d: bool) -> str: - c = tokens["colors"] - hero_background = { - "asset-hero": "radial-gradient(circle at 80% 10%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 34%), linear-gradient(180deg, var(--neutral), var(--surface))", - "conversion-proof": "linear-gradient(180deg, var(--surface), var(--neutral))", - "immersive-runtime": "radial-gradient(circle at 62% 20%, color-mix(in srgb, var(--accent) 24%, transparent), transparent 32%), radial-gradient(circle at 20% 20%, color-mix(in srgb, var(--primary) 18%, transparent), transparent 26%), var(--surface)", - "editorial-system": "linear-gradient(135deg, var(--neutral), var(--surface) 48%, color-mix(in srgb, var(--primary) 8%, var(--surface)))", - }.get(variant, "linear-gradient(180deg, var(--neutral), var(--surface))") - return f""":root {{ - --primary: {c['primary']}; --accent: {c['accent']}; --surface: {c['surface']}; --neutral: {c['neutral']}; --text: {c['text']}; --muted: {c['muted']}; --border: {c['border']}; - --radius-sm: {tokens['radius']['sm']}; --radius-md: {tokens['radius']['md']}; --radius-lg: {tokens['radius']['lg']}; --radius-xl: {tokens['radius']['xl']}; --shadow-card: {tokens['shadow']['card']}; --shadow-cta: {tokens['shadow']['cta']}; -}} -* {{ box-sizing: border-box; }} -html {{ scroll-behavior: smooth; }} -body {{ margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--text); background: {hero_background}; }} -a {{ color: inherit; }} -.page {{ width: min(1160px, calc(100% - 32px)); margin: 0 auto; padding: 28px 0 64px; }} -.nav {{ display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: clamp(36px, 8vw, 76px); }} -.brand {{ font-weight: 850; letter-spacing: -0.045em; font-size: 1.12rem; }} -.pill {{ display: inline-flex; align-items: center; border: 1px solid var(--border); border-radius: 999px; padding: 8px 12px; color: var(--muted); background: rgba(255,255,255,.72); backdrop-filter: blur(12px); }} -.hero {{ display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(280px, .95fr); gap: clamp(24px, 5vw, 56px); align-items: center; }} -h1 {{ font-size: clamp(3rem, 8vw, 5.9rem); line-height: .91; letter-spacing: -0.075em; margin: 18px 0; max-width: 10ch; }} -h2 {{ font-size: clamp(2rem, 4vw, 3rem); letter-spacing: -0.045em; margin: 0 0 16px; }} -p {{ color: var(--muted); line-height: 1.7; font-size: 1.04rem; }} -.actions {{ display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px; }} -.button {{ border: 0; border-radius: var(--radius-md); padding: 14px 18px; font-weight: 780; text-decoration: none; transition: transform 180ms cubic-bezier(.2,.8,.2,1), box-shadow 180ms cubic-bezier(.2,.8,.2,1); }} -.button:focus-visible {{ outline: 3px solid color-mix(in srgb, var(--accent) 45%, white); outline-offset: 3px; }} -.button:hover {{ transform: translateY(-1px); }} -.primary {{ background: var(--primary); color: white; box-shadow: var(--shadow-cta); }} -.secondary {{ color: var(--text); border: 1px solid var(--border); background: white; }} -.panel {{ border: 1px solid var(--border); background: rgba(255,255,255,.84); border-radius: var(--radius-xl); padding: clamp(20px, 4vw, 32px); box-shadow: var(--shadow-card); }} -.visual-card {{ position: relative; overflow: hidden; min-height: 440px; display: flex; flex-direction: column; justify-content: flex-end; }} -.visual-card img {{ width: 100%; border-radius: 24px; border: 1px solid var(--border); background: var(--neutral); margin-bottom: 18px; }} -.asset-caption {{ font-size: .9rem; color: var(--muted); }} -.metric-grid {{ display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; margin-top: 18px; }} -.metric {{ padding: 18px; border-radius: 20px; background: var(--neutral); }} -.metric strong {{ display: block; font-size: 1.35rem; color: var(--primary); }} -.sections {{ display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 16px; margin-top: 56px; }} -.card {{ border: 1px solid var(--border); border-radius: 24px; padding: 22px; background: white; }} -.card b {{ display: block; margin-bottom: 8px; }} -.source-list {{ padding-left: 20px; color: var(--muted); line-height: 1.8; }} -.model-viewer-stub {{ display: grid; place-items: center; min-height: 300px; margin: 18px 0; border-radius: 28px; border: 1px dashed var(--border); color: var(--muted); background: radial-gradient(circle at 50% 36%, rgba(124,58,237,.18), transparent 34%), linear-gradient(145deg, var(--neutral), var(--surface)); font-weight: 800; letter-spacing: .08em; }} -.interaction-zone {{ border: 1px solid var(--border); border-radius: 20px; padding: 16px; background: var(--neutral); }} -.runtime-3d {{ margin-top: 24px; }} -.readiness-note {{ margin: -28px 0 40px; border: 1px solid color-mix(in srgb, var(--accent), var(--border) 45%); border-left: 4px solid var(--accent); border-radius: 20px; padding: 18px 20px; background: rgba(124,58,237,.08); }} -.readiness-note strong {{ display: block; margin-bottom: 8px; }} -.readiness-note ul {{ margin: 8px 0 0; padding-left: 20px; color: var(--muted); line-height: 1.7; }} -.policy {{ margin-top: 56px; padding: 18px; border-left: 4px solid var(--accent); background: white; border-radius: 16px; color: var(--muted); }} -@media (max-width: 820px) {{ .hero, .sections {{ grid-template-columns: 1fr; }} .nav {{ align-items: flex-start; flex-direction: column; }} .visual-card {{ min-height: 0; }} h1 {{ max-width: 12ch; }} }} -@media (prefers-reduced-motion: reduce) {{ html {{ scroll-behavior: auto; }} .button {{ transition: none; }} .button:hover {{ transform: none; }} }} -""" - - -def render_html(root: Path, brief: dict[str, Any], references: dict[str, Any], strategy: dict[str, Any], design_text: str, variant: str, iteration: str, runtime_3d: bool, readiness: dict[str, Any] | None = None) -> str: - readiness = readiness or risk_readiness_summary(brief) - fm = frontmatter(design_text) - name = project_name(brief, fm) - description = yaml_value(fm, "description", "Artic-generated homepage preview") - raw_project = brief.get("project") - project = raw_project if isinstance(raw_project, dict) else {} - audience = ", ".join(str(item) for item in project.get("target_users", []) if item) or "the target audience" - goal = str(project.get("primary_goal") or "the primary conversion") - raw_style = brief.get("style") - style = raw_style if isinstance(raw_style, dict) else {} - north_star = markdown_section(design_text, "Design North Star") or str(style.get("design_north_star") or strategy.get("design_north_star") or "") - overview = markdown_section(design_text, "Overview") or description - composition = markdown_section(design_text, "Page Composition") or str(strategy.get("implementation_guidance") or "Hero, proof, features, trust, conversion, FAQ, and final CTA.") - sources = selected_source_names(references) - locale = brief_locale(brief) - copy = localized_copy(locale) - policy_text = POLICY_BY_LOCALE.get(locale, POLICY_FALLBACK) - source_items = "\n".join(f"
  • {html.escape(source)} preview-only/catalog-reference
  • " for source in sources[:5]) or "
  • Artic selected references preview-only/catalog-reference
  • " - boundary_items = "\n".join(f"
  • {html.escape(item)}
  • " for item in readiness.get("placeholder_boundaries", [])) - blocker_items = "\n".join(f"
  • {html.escape(item)}
  • " for item in readiness.get("implementation_blockers", [])) - if not readiness.get("production_ready", True): - if locale.startswith("ko"): - note_title = "프로덕션 준비 완료가 아닙니다" - note_body = "이 화면은 확인된 에셋과 구현 조건이 충족되기 전까지 플레이스홀더 미리보기로만 사용해야 합니다." - boundary_label = "플레이스홀더 경계" - blocker_label = "구현 차단 조건" - else: - note_title = "Not production-ready" - note_body = "Use this screen only as a placeholder preview until required assets and implementation conditions are satisfied." - boundary_label = "Placeholder boundaries" - blocker_label = "Implementation blockers" - boundary_section = f"
    {html.escape(boundary_label)}
      {boundary_items}
    " if boundary_items else "" - blocker_section = f"
    {html.escape(blocker_label)}
      {blocker_items}
    " if blocker_items else "" - readiness_note = f""" -
    - {html.escape(note_title)} -

    {html.escape(note_body)}

    - {boundary_section} - {blocker_section} -
    """ - else: - readiness_note = "" - runtime_block = "" - if runtime_3d: - runtime_block = f""" -
    - {html.escape(copy['model_label'])} - \"Preview-only -
    model-viewer · GLB · poster fallback
    -
    - {html.escape(copy['interaction_title'])} -

    {html.escape(copy['interaction_body'])}

    -
    -

    {html.escape(copy['model_body'])}

    -
    """ - generated_at = datetime.now(timezone.utc).isoformat() - variant_label = variant.replace("-", " ").title() - return f""" - - - - - - {html.escape(name)} — Artic Preview {html.escape(iteration)} - - - - -
    - -{readiness_note} -
    -
    - {html.escape(copy['for_label'])}: {html.escape(audience)} -

    {html.escape(name)} {html.escape(copy['headline_suffix'])}

    -

    {html.escape(overview)}

    - -
    - -
    -
    -
    {html.escape(copy['promise_title'])}

    {html.escape(copy['promise_body'])}

    -
    {html.escape(copy['proof_title'])}

    {html.escape(copy['proof_body'])}

    -
    {html.escape(copy['system_title'])}

    {html.escape(copy['system_body'])}

    -
    -{runtime_block} -
    -

    {html.escape(copy['reference_title'])}

    -

    {html.escape(copy['reference_body'])}

    -
      {source_items}
    -
    -
    - {html.escape(copy['policy_title'])}
    - {html.escape(policy_text)} -
    {html.escape(copy['generated'])}: {html.escape(generated_at)} · {html.escape(copy['root'])}: {html.escape(str(root))} · iteration {html.escape(iteration)} -
    -
    - - -""" - - -def score_variant(variant: str, idx: int, runtime_3d: bool, sources_count: int) -> dict[str, Any]: - base = { - "asset-hero": 90, - "conversion-proof": 81, - "immersive-runtime": 86 if runtime_3d else 78, - "editorial-system": 82, - }.get(variant, 80) - richness = min(92, 74 + sources_count * 4 + (8 if runtime_3d else 0) + (4 if variant in ("asset-hero", "immersive-runtime") else 0)) - provenance = min(95, 80 + sources_count * 3 + (5 if runtime_3d else 0)) - generic_penalty = max(4, 13 - idx * 2 - (3 if variant in ("asset-hero", "immersive-runtime") else 0)) - dimensions: dict[str, float] = { - "strategy_alignment": float(min(94, base + 3)), - "visual_fidelity": float(min(92, base + (2 if variant != "conversion-proof" else 0))), - "asset_richness": float(richness), - "asset_provenance_completeness": float(provenance), - "conversion_clarity": float(min(94, base + (6 if variant == "conversion-proof" else 2))), - "visual_specificity": float(min(93, base + (5 if variant in ("asset-hero", "immersive-runtime") else 1))), - "accessibility_basics": 86.0, - "mobile_first": 88.0, - "genericness_penalty": float(generic_penalty), - } - positive = [v for k, v in dimensions.items() if k != "genericness_penalty"] - dimensions["overall"] = round((sum(positive) / len(positive)) - (generic_penalty * 0.35), 1) - return dimensions - - -def render_critique(reports: list[dict[str, Any]], selected: dict[str, Any]) -> str: - lines = ["# Artic Show Critique", "", "Asset-first visual draft review. Unknown asset sources are retained as preview-only/catalog-reference instead of blocking generation.", ""] - for report in reports: - scores = report["scores"] - lines.extend([ - f"## Iteration {report['iteration']} — {report['variant']}", - "", - f"- Overall: {scores['overall']}", - f"- Strength: {report['strength']}", - f"- Risk: {report['risk']}", - f"- Asset provenance: {scores['asset_provenance_completeness']} / asset richness: {scores['asset_richness']}", - "", - ]) - lines.extend([ - "## Selected", "", f"Iteration {selected['iteration']} (`{selected['variant']}`) was promoted to `.artic/show/` root.", "", "## App source impact", "", "`modified_app_files` is intentionally `[]`; this command only writes preview bundle artifacts under `.artic/show`.", "", - ]) - return "\n".join(lines) - - -def copy_iteration_to_root(iter_dir: Path, show_root: Path) -> None: - for filename in ("index.html", "styles.css", "tokens.json", "report.json"): - shutil.copy2(iter_dir / filename, show_root / filename) - dst_assets = show_root / "assets" - if dst_assets.exists(): - shutil.rmtree(dst_assets) - shutil.copytree(iter_dir / "assets", dst_assets) - - -def rel(root: Path, path: Path) -> str: - try: - return str(path.resolve().relative_to(root.resolve())) - except ValueError: - return str(path) - - -def create_show_preview(root: Path, max_iterations: int = 3, min_score: float = 75, strict: bool = False, asset_mode: str = "asset-first", asset_timeout: float = 10) -> dict[str, Any]: - del asset_timeout # reserved for future downloader; core implementation is local/offline-safe. - missing = required_missing(root) - if missing: - raise ValueError("missing required input(s) for @artic show: " + ", ".join(missing)) - if not 1 <= max_iterations <= 3: - raise ValueError("--max-iterations must be between 1 and 3") - design_text = (root / "DESIGN.md").read_text(encoding="utf-8") - brief = read_json(root / ".artic" / "brief.json") - references = read_json(root / ".artic" / "references.json") - strategy = read_json(root / ".artic" / "strategy.json") - readiness = risk_readiness_summary(brief) - show_root = root / ".artic" / "show" - iterations_root = show_root / "iterations" - show_root.mkdir(parents=True, exist_ok=True) - if iterations_root.exists(): - shutil.rmtree(iterations_root) - iterations_root.mkdir(parents=True, exist_ok=True) - - tokens = build_tokens(design_text) - runtime_3d = has_3d_runtime_reference(brief, references) - variants = variant_names(max_iterations, runtime_3d) - reports: list[dict[str, Any]] = [] - generated_preview_files: list[str] = [] - asset_files: list[str] = [] - - for idx, variant in enumerate(variants, start=1): - iteration = f"{idx:03d}" - iter_dir = iterations_root / iteration - if iter_dir.exists(): - shutil.rmtree(iter_dir) - (iter_dir / "assets").mkdir(parents=True, exist_ok=True) - manifest = build_asset_manifest(root, iter_dir, iteration, variant, brief, references, tokens, asset_mode, runtime_3d) - scores = score_variant(variant, idx, runtime_3d, len(selected_source_names(references))) - report = { - "schema_version": 1, - "mode": "asset-first-preview", - "iteration": iteration, - "variant": variant, - "created_at": datetime.now(timezone.utc).isoformat(), - "scores": scores, - "score_dimensions": SCORE_KEYS, - "strength": "Uses local generated placeholders plus catalog-reference provenance to make the first draft visually concrete.", - "risk": "Preview assets may need replacement with owned or license-verified production assets before implementation.", - "modified_app_files": [], - "asset_manifest": "assets/manifest.json", - **readiness, - } - write_json(iter_dir / "tokens.json", tokens) - write_text(iter_dir / "styles.css", render_css(tokens, variant, runtime_3d)) - write_text(iter_dir / "index.html", render_html(root, brief, references, strategy, design_text, variant, iteration, runtime_3d, readiness)) - write_json(iter_dir / "assets" / "manifest.json", manifest) - write_json(iter_dir / "report.json", report) - reports.append(report) - generated_preview_files.extend([rel(root, iter_dir / name) for name in ("index.html", "styles.css", "tokens.json", "report.json", "assets/manifest.json")]) - for asset in manifest.get("assets", []): - local_path = asset.get("local_path") - if local_path: - asset_files.append(rel(root, iter_dir / str(local_path))) - - selected_report = max(reports, key=lambda item: float(item["scores"]["overall"])) - selected_iteration = str(selected_report["iteration"]) - selected_dir = iterations_root / selected_iteration - copy_iteration_to_root(selected_dir, show_root) - root_asset_manifest = read_json(show_root / "assets" / "manifest.json") - asset_summary = { - "assets_used": len(root_asset_manifest.get("assets", [])), - "verified_assets": sum(1 for asset in root_asset_manifest.get("assets", []) if asset.get("status") == "verified"), - "unverified_preview_only_assets": sum(1 for asset in root_asset_manifest.get("assets", []) if asset.get("status") == "unverified-preview-only"), - "generated_placeholders": sum(1 for asset in root_asset_manifest.get("assets", []) if asset.get("status") == "generated"), - "catalog_references": sum(1 for asset in root_asset_manifest.get("assets", []) if asset.get("status") == "catalog-reference"), - } - status = "selected" - if float(selected_report["scores"]["overall"]) < min_score: - status = "below-threshold" - root_report = { - "schema_version": 1, - "mode": "asset-first-preview", - "status": status, - "generated_at": datetime.now(timezone.utc).isoformat(), - "selected_iteration": selected_iteration, - "candidate": selected_report["variant"], - "selected_candidate_id": selected_report["variant"], - "threshold": min_score, - "preview_bundle": str(show_root), - "entrypoint": str(show_root / "index.html"), - "scores": selected_report["scores"], - "asset_summary": asset_summary, - "integrity": { - "modified_app_files": [], - }, - "modified_app_files": [], - "iterations": reports, - "remaining_risks": ["Preview assets may need replacement with owned or license-verified production assets before apply."], - **readiness, - } - write_json(show_root / "report.json", root_report) - selected_payload = { - "schema_version": 1, - "selected_iteration": selected_iteration, - "candidate": selected_report["variant"], - "selected_candidate_id": selected_report["variant"], - "variant": selected_report["variant"], - "overall": selected_report["scores"]["overall"], - "entrypoint": ".artic/show/index.html", - "reason": "Highest overall asset-first preview score among generated candidates.", - } - write_json(show_root / "selected.json", selected_payload) - write_text(show_root / "critique.md", render_critique(reports, selected_report)) - - asset_files.extend(rel(root, show_root / str(asset["local_path"])) for asset in root_asset_manifest.get("assets", []) if asset.get("local_path")) - generated_preview_files.extend([rel(root, show_root / name) for name in ("index.html", "styles.css", "tokens.json", "report.json", "selected.json", "critique.md", "assets/manifest.json")]) - if strict and status == "below-threshold": - raise ValueError(f"quality threshold failure: selected preview score {selected_report['scores']['overall']} is below --min-score {min_score}") - - payload = { - "root": str(root.resolve()), - "preview_bundle": str(show_root), - "bundle_dir": str(show_root), - "entrypoint": str(show_root / "index.html"), - "preview_file": str(show_root / "index.html"), - "selected_iteration": selected_iteration, - "generated_preview_files": sorted(str(root / item) for item in dict.fromkeys(generated_preview_files)), - "asset_files": sorted(str(root / item) for item in dict.fromkeys(asset_files)) + [str(show_root / "assets" / "manifest.json")], - "asset_manifest": str(show_root / "assets" / "manifest.json"), - "report_file": str(show_root / "report.json"), - "critique_file": str(show_root / "critique.md"), - "modified_app_files": [], - **readiness, - } - return payload - - -def main() -> int: - parser = argparse.ArgumentParser(description="Render an asset-first static Artic homepage preview bundle from start-generated design docs.") - parser.add_argument("--root", required=True, help="Project root containing DESIGN.md and .artic outputs from @artic start") - parser.add_argument("--max-iterations", type=int, default=3, help="Number of preview candidates to generate (1..3)") - parser.add_argument("--min-score", type=float, default=75, help="Minimum selected score when --strict is enabled") - parser.add_argument("--strict", action="store_true", help="Fail if selected iteration is below --min-score") - parser.add_argument("--asset-mode", choices=["asset-first", "offline", "no-download"], default="asset-first", help="Asset handling mode; core show uses local placeholders/catalog provenance") - parser.add_argument("--asset-timeout", type=float, default=10, help="Reserved timeout for future asset fetchers") - args = parser.parse_args() - try: - payload = create_show_preview(Path(args.root), args.max_iterations, args.min_score, args.strict, args.asset_mode, args.asset_timeout) - except ValueError as exc: - print(json.dumps({"error": str(exc)}, ensure_ascii=False)) - return 1 - print(json.dumps(payload, indent=2, ensure_ascii=False)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/skills/artic/scripts/artic_start.py b/skills/artic/scripts/artic_start.py index 63e32e8..e4cb2bb 100644 --- a/skills/artic/scripts/artic_start.py +++ b/skills/artic/scripts/artic_start.py @@ -133,123 +133,6 @@ def as_markdown(value: Any, *, indent: int = 0) -> str: return str(value) -RISK_LABELS = { - "en": { - "heading": "Risk / Readiness Summary", - "summary_line": "Risk Summary: risk/quality readiness details follow.", - "core_quality_requirements": "Core quality requirements", - "known_missing_information": "Known missing information", - "safe_assumptions": "Safe assumptions", - "unsafe_assumptions": "Unsafe assumptions", - "placeholder_fallback_boundary": "Placeholder/fallback boundary", - "implementation_stop_conditions": "Implementation stop conditions / Stop Conditions", - "completion_acceptance_criteria": "Completion/acceptance criteria", - "status_ready": "Strategy/preview can proceed.", - "status_blocked": "Strategy/preview can proceed, but implementation is blocked until missing inputs are resolved.", - "none": "None declared.", - "check_intent": "requested product photo/3D/map/payment trust intent is satisfied or explicitly blocked", - "check_placeholder": "placeholder is not accepted as production substitute for quality-critical requirement", - }, - "ko": { - "heading": "위험/준비 상태 요약", - "summary_line": "위험 요약: 리스크와 품질 기준에 따른 준비 상태를 정리합니다.", - "core_quality_requirements": "핵심 품질 요구사항", - "known_missing_information": "알려진 누락 정보", - "safe_assumptions": "안전한 가정", - "unsafe_assumptions": "위험한 가정", - "placeholder_fallback_boundary": "플레이스홀더/대체 경계", - "implementation_stop_conditions": "구현 중단 조건", - "completion_acceptance_criteria": "완료/수용 기준", - "status_ready": "전략/프리뷰는 진행할 수 있습니다.", - "status_blocked": "전략/프리뷰는 진행할 수 있지만, 누락된 정보가 해결될 때까지 구현은 차단됩니다.", - "none": "명시된 항목 없음.", - "check_intent": "요청된 제품 사진/3D/지도/결제 신뢰 의도가 충족되었거나 명시적으로 차단됨", - "check_placeholder": "플레이스홀더는 품질 핵심 요구사항의 프로덕션 대체물로 인정되지 않음", - }, -} - - -RISK_SECTION_KEYS = [ - "core_quality_requirements", - "known_missing_information", - "safe_assumptions", - "unsafe_assumptions", - "placeholder_fallback_boundary", - "implementation_stop_conditions", - "completion_acceptance_criteria", -] - - -def risk_readiness(brief: dict[str, Any]) -> dict[str, Any]: - value = brief.get("risk_readiness") - return value if isinstance(value, dict) and value else {} - - -def risk_labels(brief: dict[str, Any]) -> dict[str, str]: - locale = str(brief_language(brief).get("locale") or "en-US") - return RISK_LABELS["ko"] if locale.startswith("ko") else RISK_LABELS["en"] - - -def risk_items(risk: dict[str, Any], key: str) -> list[str]: - aliases = { - "core_quality_requirements": ["quality_critical_requirements", "core_requirements"], - "known_missing_information": ["missing_information", "missing_inputs", "blockers"], - "placeholder_fallback_boundary": ["placeholder_boundary", "fallback_boundary"], - "implementation_stop_conditions": ["stop_conditions", "implementation_blockers"], - "completion_acceptance_criteria": ["acceptance_criteria", "completion_criteria"], - } - raw = risk.get(key) - if raw is None: - for alias in aliases.get(key, []): - raw = risk.get(alias) - if raw is not None: - break - if raw is None or raw == "": - return [] - if isinstance(raw, list): - items: list[str] = [] - for item in raw: - rendered = as_markdown(item).strip() - if rendered: - items.append(rendered) - return items - return [as_markdown(raw).strip()] - - -def risk_readiness_block(brief: dict[str, Any], *, checklist: bool = False) -> str: - risk = risk_readiness(brief) - if not risk: - return "" - labels = risk_labels(brief) - implementation_blocked = bool(risk.get("implementation_blocked")) or risk.get("ready_for_implementation") is False - readiness = risk.get("readiness") - if isinstance(readiness, dict) and str(readiness.get("implementation") or "").lower() == "blocked": - implementation_blocked = True - lines = [f"## {labels['heading']}", "", f"- {labels['summary_line']}", f"- {labels['status_blocked'] if implementation_blocked else labels['status_ready']}"] - if "ready_for_strategy" in risk: - lines.append(f"- ready_for_strategy: {bool(risk.get('ready_for_strategy'))}") - if "implementation_blocked" in risk: - lines.append(f"- implementation_blocked: {bool(risk.get('implementation_blocked'))}") - lines.append("") - for key in RISK_SECTION_KEYS: - lines.extend([f"### {labels[key]}", ""]) - items = risk_items(risk, key) - if items: - prefix = "- [ ]" if checklist else "-" - lines.extend(f"{prefix} {item}" for item in items) - else: - lines.append(f"- {labels['none']}") - lines.append("") - if checklist: - lines.extend([ - "### Intent-matched QA gates", - "", - f"- [ ] {labels['check_intent']}.", - f"- [ ] {labels['check_placeholder']}.", - "", - ]) - return "\n".join(lines).rstrip() - def role_lines(strategy: dict[str, Any]) -> list[str]: roles = strategy.get("reference_roles", []) @@ -273,7 +156,6 @@ def role_lines(strategy: dict[str, Any]) -> list[str]: def strategy_doc(brief: dict[str, Any], strategy: dict[str, Any]) -> str: policy = policy_block(brief) - risk_block = risk_readiness_block(brief) parts = [ f"# Artic Strategy: {project_name(brief)}", "", @@ -282,8 +164,6 @@ def strategy_doc(brief: dict[str, Any], strategy: dict[str, Any]) -> str: policy, "", ] - if risk_block: - parts.extend([risk_block, ""]) parts.extend([ "## Project Summary", "", @@ -417,8 +297,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] description = f"{project_description(brief, strategy)} Artic-generated homepage design system." policy = policy_block(brief) language = language_block(brief) - risk_block = risk_readiness_block(brief) - risk_checklist_block = risk_readiness_block(brief, checklist=True) strategy_markdown = strategy_doc(brief, strategy) design = load_template("DESIGN.template.md") @@ -443,8 +321,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] design = design.replace("Recommended homepage sequence: hero with one primary promise, proof immediately near the hero, feature/job sections, trust or comparison section, conversion area, FAQ, and final CTA.", as_markdown(strategy.get("implementation_guidance", "")) or "Recommended homepage sequence: hero, proof, feature sections, trust, conversion area, FAQ, and final CTA.") design = design.replace("Do not use generic gradient blobs, random glassmorphism, off-token colors, multiple primary CTAs in one viewport, low-contrast muted copy, centered long paragraphs, or exact reference layouts.", "Do not copy " + ", ".join(str(item) for item in strategy.get("forbidden_copy_elements", FORBIDDEN_COPY_ELEMENTS)) + ". " + as_markdown(strategy.get("conflict_resolution", ""))) design = replace_policy_text(design, policy) - if risk_block: - design += f"\n\n{risk_block}\n" write(root / "DESIGN.md", design) write(root / "docs" / "artic-strategy.md", strategy_markdown) @@ -452,8 +328,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] rules = rules.replace("{{PROJECT_NAME}}", name) rules = rules.replace("{{REFERENCE_SYNTHESIS}}", strategy_markdown) rules = replace_policy_text(rules, policy) - if risk_block: - rules += f"\n\n{risk_block}\n" write(root / "docs" / "design-rules.md", rules) checklist = load_template("design-qa-checklist.template.md") @@ -461,8 +335,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] checklist = replace_policy_text(checklist, policy) if "Accessibility" not in checklist: checklist = checklist.replace("- [ ] Text contrast targets WCAG AA.", "- [ ] Accessibility: text contrast targets WCAG AA, focus states are visible, controls are semantic, and forms are labeled.") - if risk_checklist_block: - checklist += f"\n\n{risk_checklist_block}\n" checklist += "\n## Strategy Gates\n\n- [ ] Visual hierarchy follows the strategy north star.\n- [ ] Brand coherence follows the visual_system field.\n- [ ] Conversion clarity follows the conversion_strategy field.\n- [ ] Mobile quality follows implementation_guidance.\n- [ ] Accessibility follows the accessibility field.\n- [ ] Reference safety forbids logos, trademarks, proprietary illustrations, exact layouts, and source copywriting.\n" write(root / "docs" / "design-qa-checklist.md", checklist) @@ -471,8 +343,6 @@ def render_outputs(root: Path, brief: dict[str, Any], references: dict[str, Any] prompt = prompt.replace("# Homepage Implementation Prompt\n", f"# Homepage Implementation Prompt\n\n{language}\n") prompt = prompt.replace("Rules:\n", f"Strategy source: `docs/artic-strategy.md`.\n\nRules:\n") prompt = replace_policy_text(prompt, policy) - if risk_block: - prompt += f"\n\n{risk_block}\n" write(root / "docs" / "homepage-design-prompt.md", prompt) update_state(root, brief) @@ -523,7 +393,7 @@ def update_state(root: Path, brief: dict[str, Any]) -> None: state = loaded except json.JSONDecodeError: state = {} - state.update({"artic_version": str(brief.get("artic_version") or state.get("artic_version") or "0.4.1"), "last_generated_at": datetime.now(timezone.utc).isoformat(), "status": "generated", "language": brief_language(brief), "strategy_path": ".artic/strategy.json"}) + state.update({"artic_version": str(brief.get("artic_version") or state.get("artic_version") or "0.5.0"), "last_generated_at": datetime.now(timezone.utc).isoformat(), "status": "generated", "language": brief_language(brief), "strategy_path": ".artic/strategy.json"}) write(state_path, json.dumps(state, indent=2, ensure_ascii=False) + "\n") @@ -591,7 +461,7 @@ def create_start_outputs(root: Path, *, no_validate: bool = False) -> dict[str, raise ValueError("cannot run @artic start before init is ready" + (f": missing {missing}" if missing else "") + (f". Next questions: {questions}" if questions else "")) if not strategy_path.exists() and (status == "ready" or not brief_path.exists() or not references_path.exists()): answers = session.get("answers") if isinstance(session.get("answers"), dict) else {} - prompt_brief = {"answers": answers, "language": session.get("language", {}), "risk_readiness": session.get("risk_readiness", {})} + prompt_brief = {"answers": answers, "language": session.get("language", {})} prompt_references = {"selected_sources": [], "source_plan": [], "note": "@artic start requires the agent-authored strategy before finalizing init outputs."} prompt_path = write_strategy_prompt(root, prompt_brief, prompt_references, None) raise ValueError(json.dumps({ diff --git a/skills/artic/scripts/risk_readiness.py b/skills/artic/scripts/risk_readiness.py deleted file mode 100644 index e057ef8..0000000 --- a/skills/artic/scripts/risk_readiness.py +++ /dev/null @@ -1,335 +0,0 @@ -#!/usr/bin/env python3 -"""Artic risk/readiness contract helpers. - -The functions in this module are intentionally pure and deterministic: no IO, no -network access, and no clock reads. They turn early Artic answers/intent into a -machine-readable readiness contract that distinguishes strategy/preview from -final implementation readiness. -""" -from __future__ import annotations - -from typing import Any - -SCHEMA_VERSION = "artic-risk-readiness-v1" - -CORE_FIELDS = ("project", "audience", "goal", "vibe") - -_CATEGORY_KEYWORDS: dict[str, tuple[str, ...]] = { - "core_visual_asset_dependency": ( - "image", "photo", "product photo", "3d model", "video", "illustration", "person", "place", - "제품 사진", "이미지", "3d", "석고상", "영상", "일러스트", "사진", - ), - "interaction_dependency": ( - "drag", "edit", "game", "map", "realtime preview", "real-time preview", "canvas", "webgl", "rotate", "zoom", "interactive", "interaction", - "마우스로", "드래그", "편집", "지도", "실시간", "캔버스", "상호작용", "회전", "줌", "만질", - ), - "brand_dependency": ( - "logo", "colors", "fonts", "brand system", "reference brands", - "로고", "색상", "폰트", "브랜드", "toss", "토스", "kakao", "카카오", "naver", "네이버", - ), - "technical_runtime_dependency": ( - "shopify", "webgl", "cms", "animation runtime", - ), - "legal_license_dependency": ( - "external assets", "external asset", "fonts", "data", "music", "video", "model", "trademark", "license", - "라이선스", "외부 에셋", "상표", - ), - "accessibility_performance": ( - "motion-heavy", "media-heavy", "mobile-first", "data-heavy", "fast mobile", "reduced motion", "webgl", "3d", - "모바일에서 빠르게", "접근성", "성능", "애니메이션", - ), - "conversion_business": ( - "payment", "booking", "lead", "contact", "download", "demo", - "결제", "예약", "문의", "리드", "다운로드", "데모", - ), -} - -_FIELD_BY_CATEGORY: dict[str, tuple[str, ...]] = { - "core_visual_asset_dependency": ("asset_source",), - "interaction_dependency": ("interaction_model",), - "brand_dependency": ("brand_assets",), - "technical_runtime_dependency": ("technical_runtime",), - "legal_license_dependency": ("license_clearance",), - "accessibility_performance": ("performance_accessibility_plan",), -} - -_CONVERSION_HARD_KEYWORDS = ("payment", "booking", "checkout", "purchase", "lead form", "lead collection", "contact form", "download", "결제", "예약", "리드", "다운로드") - -_QUALITY_CRITICAL_PATTERNS = ( - ("product photo", "Real product photography is a quality-critical visual requirement."), - ("제품 사진", "실제 제품 사진은 품질 핵심 시각 요구사항입니다."), - ("actual product", "Actual product imagery is a quality-critical visual requirement."), - ("실제 제품", "실제 제품 이미지는 품질 핵심 시각 요구사항입니다."), - ("real gallery", "A real gallery-like visual asset is quality-critical."), - ("실제 갤러리", "실제 갤러리 같은 시각 자산은 품질 핵심 요구사항입니다."), - ("plaster statue", "The plaster statue subject is a quality-critical core asset."), - ("석고상", "The 3D plaster-cast subject is a quality-critical core asset."), -) - -_PLACEHOLDER_BOUNDARY = ( - "Placeholders may be used to communicate layout or interaction intent in strategy and preview, " - "but implementation cannot use a placeholder as the final substitute for core requirements." -) - -_SUBSTITUTE_STOP = ( - "Substitute stop rule: stop before implementation if a missing core asset, interaction model, " - "license clearance, brand asset, runtime decision, or performance/accessibility plan would be replaced by a generic substitute." -) - - -def _flatten_text(value: Any) -> str: - if value is None: - return "" - if isinstance(value, dict): - parts: list[str] = [] - for key in sorted(value): - parts.append(_flatten_text(value[key])) - return " ".join(parts) - if isinstance(value, (list, tuple, set)): - return " ".join(_flatten_text(item) for item in value) - return str(value) - - -def _has_any(text: str, keywords: tuple[str, ...]) -> bool: - lowered = text.lower() - return any(keyword.lower() in lowered for keyword in keywords) - - -def _present(value: Any) -> bool: - if value is None: - return False - if isinstance(value, str): - return bool(value.strip()) - if isinstance(value, (list, tuple, set, dict)): - return bool(value) - return True - - -def _detect_categories(answers: dict[str, Any], intent: dict[str, Any] | None = None) -> list[str]: - text = _flatten_text({"answers": answers or {}, "intent": intent or {}}) - categories = [category for category, words in _CATEGORY_KEYWORDS.items() if _has_any(text, words)] - - # Avoid making a simple low-risk SaaS demo CTA implementation-blocking while - # still detecting explicitly requested business mechanics. - if "conversion_business" in categories and not _has_any(text, _CONVERSION_HARD_KEYWORDS): - categories = [category for category in categories if category != "conversion_business"] - return categories - - -def _quality_critical_requirements(text: str) -> list[dict[str, str]]: - lowered = text.lower() - requirements: list[dict[str, str]] = [] - seen: set[str] = set() - for pattern, label in _QUALITY_CRITICAL_PATTERNS: - if pattern.lower() in lowered and label not in seen: - requirements.append({ - "requirement": label, - "source_signal": pattern, - "rule": "quality_critical_requirement", - "placeholder_policy": "preview_only", - "completion_criterion": "Final implementation must use an approved/source-confirmed asset or explicitly revised requirement, not a generic substitute.", - }) - seen.add(label) - if ("photo" in lowered or "사진" in lowered) and ("core" in lowered or "핵심" in lowered) and not requirements: - requirements.append({ - "requirement": "Core photography is quality-critical to the requested experience.", - "source_signal": "core photo", - "rule": "quality_critical_requirement", - "placeholder_policy": "preview_only", - "completion_criterion": "Final implementation must use approved photography or an explicit requirement change.", - }) - return requirements - - -def _has_placeholder_substitute_answer(answers: dict[str, Any], fields: list[str]) -> bool: - substitute_markers = ( - "placeholder", "generic", "gradient", "stock", "dummy", "mock", "temporary", "temp", - "대체", "임시", "플레이스홀더", "더미", "일반", "제네릭", - ) - fields_to_check = {"asset_source", "license_clearance", "brand_assets"} - for field in fields_to_check: - value = answers.get(field) - if not _present(value): - continue - lowered = _flatten_text(value).lower() - if any(marker in lowered for marker in substitute_markers): - return True - return False - - -def dynamic_required_fields(payload: dict[str, Any]) -> list[str]: - """Return dynamic required field names for a risk payload or partial contract.""" - categories = payload.get("risk_categories") or [] - fields: list[str] = [] - for category in categories: - for field in _FIELD_BY_CATEGORY.get(str(category), ()): # conversion handled below - if field not in fields: - fields.append(field) - - text = _flatten_text(payload) - if "conversion_business" in categories and _has_any(text, _CONVERSION_HARD_KEYWORDS): - fields.append("conversion_path") - - if payload.get("quality_critical_requirements") and "asset_source" not in fields: - fields.insert(0, "asset_source") - - return fields - - -def _completion_criteria(answers: dict[str, Any], fields: list[str], qcr: list[dict[str, str]]) -> list[str]: - goal = str(answers.get("goal") or "the stated goal").strip() or "the stated goal" - criteria = [ - f"The final experience must directly support: {goal}.", - "Core project, audience, goal, and vibe are reflected in content hierarchy and visual direction.", - ] - for field in fields: - criteria.append(f"Dynamic requirement '{field}' is answered and traceable in the final implementation.") - for requirement in qcr: - criteria.append(requirement["completion_criterion"]) - criteria.append("No placeholder remains as a final substitute for a quality-critical or core requirement.") - return criteria - - -def render_risk_summary(payload: dict[str, Any], locale: str = "en-US") -> str: - categories = payload.get("risk_categories") or [] - missing = payload.get("missing_dynamic_required_fields") or [] - readiness = payload.get("readiness") or {} - if locale.lower().startswith("ko"): - return ( - f"위험 범주: {', '.join(categories) if categories else '없음'}\n" - f"누락된 동적 필수 항목: {', '.join(missing) if missing else '없음'}\n" - f"전략: {readiness.get('strategy', 'unknown')} / 미리보기: {readiness.get('preview', 'unknown')} / " - f"구현: {readiness.get('implementation', 'unknown')}\n" - f"경계: {payload.get('placeholder_boundary', _PLACEHOLDER_BOUNDARY)}" - ) - return ( - f"Risk categories: {', '.join(categories) if categories else 'none'}\n" - f"Missing dynamic required fields: {', '.join(missing) if missing else 'none'}\n" - f"Readiness: strategy={readiness.get('strategy', 'unknown')}, " - f"preview={readiness.get('preview', 'unknown')}, implementation={readiness.get('implementation', 'unknown')}\n" - f"Boundary: {payload.get('placeholder_boundary', _PLACEHOLDER_BOUNDARY)}" - ) - - -def analyze_risk_readiness(answers: dict[str, Any], intent: dict[str, Any] | None = None) -> dict[str, Any]: - answers = dict(answers or {}) - intent = dict(intent or {}) if intent else None - combined_text = _flatten_text(answers) - risk_categories = _detect_categories(answers, None) - qcr = _quality_critical_requirements(combined_text) - - partial = {"risk_categories": risk_categories, "quality_critical_requirements": qcr, "answers": answers, "intent": intent or {}} - required = dynamic_required_fields(partial) - missing = [field for field in required if not _present(answers.get(field)) and not _present((intent or {}).get(field))] - placeholder_substitute = bool(qcr) and _has_placeholder_substitute_answer(answers, required) - - core_missing = [field for field in CORE_FIELDS if not _present(answers.get(field))] - if core_missing: - strategy = "blocked" - preview = "blocked" - implementation = "blocked" - status = "core_fields_missing" - elif missing or placeholder_substitute: - strategy = "ready" - preview = "ready_with_placeholders" - implementation = "blocked" - status = "implementation_blocked" - elif risk_categories or qcr: - strategy = "ready" - preview = "ready" - implementation = "ready_with_assumptions" - status = "ready_with_assumptions" - else: - strategy = "ready" - preview = "ready" - implementation = "ready" - status = "ready" - - safe_assumptions = [ - "Strategy may proceed from stated project, audience, goal, and vibe." if not core_missing else "No strategy assumption is safe until core fields are answered.", - ] - if missing or placeholder_substitute: - safe_assumptions.append("Preview may use clearly labeled placeholders only to show structure, scale, or interaction intent.") - elif risk_categories or qcr: - safe_assumptions.append("Implementation may proceed only if detected dependencies are satisfied by supplied answers or explicit approvals.") - - unsafe_assumptions = [ - "Do not treat a generic substitute as equivalent to a missing quality-critical requirement.", - "Do not replace required product photos or core visual assets with a generic gradient in final implementation.", - ] - if missing or placeholder_substitute: - unsafe_assumptions.append("Do not implement final production UI by substituting missing dynamic requirements with invented assets, behaviors, licenses, or runtime choices.") - if core_missing: - unsafe_assumptions.append("Do not infer missing core fields without user confirmation.") - - stop_conditions: list[str] = [] - if core_missing: - stop_conditions.append(f"Stop: core fields missing: {', '.join(core_missing)}.") - if missing: - stop_conditions.append(f"Stop before implementation: missing dynamic required fields: {', '.join(missing)}.") - stop_conditions.append(_SUBSTITUTE_STOP) - if placeholder_substitute: - stop_conditions.append("Stop before implementation: placeholder or generic substitute was supplied for a quality-critical requirement.") - stop_conditions.append(_SUBSTITUTE_STOP) - if qcr: - stop_conditions.append("Stop before final implementation if quality-critical assets are unavailable, unlicensed, or replaced by placeholders.") - - payload: dict[str, Any] = { - "schema_version": SCHEMA_VERSION, - "risk_categories": risk_categories, - "quality_critical_requirements": qcr, - "dynamic_required_fields": required, - "missing_dynamic_required_fields": missing, - "readiness": { - "strategy": strategy, - "preview": preview, - "implementation": implementation, - "status": status, - }, - "safe_assumptions": safe_assumptions, - "unsafe_assumptions": unsafe_assumptions, - "placeholder_boundary": _PLACEHOLDER_BOUNDARY, - "stop_conditions": stop_conditions, - "risk_summary": "", - "completion_criteria": _completion_criteria(answers, required, qcr), - } - blockers = list(stop_conditions) - if "brand_dependency" in risk_categories: - payload["unsafe_assumptions"].append( - "Treat named brands such as Toss/토스 as inspiration only; do not clone or copy their palette, logo, layout, or product flow." - ) - payload["completion_criteria"].append( - "Brand references are translated into original project-specific constraints without clone/copy behavior." - ) - if "conversion_business" in risk_categories: - payload["completion_criteria"].append( - "Conversion success is judged by trust, information structure, recovery states, and the stated business outcome, not by merely rendering a form." - ) - if "accessibility_performance" in risk_categories: - payload["completion_criteria"].append( - "Mobile or motion/media-heavy experiences define performance, accessibility, reduced motion, and fallback requirements before implementation." - ) - - # Compatibility aliases for init/session, show, and broad integration tests. - payload["level"] = "high" if status in {"implementation_blocked", "core_fields_missing"} else ("medium" if risk_categories or qcr else "low") - payload["risk_level"] = payload["level"] - payload["ready_for_strategy"] = strategy == "ready" - payload["ready_for_preview"] = True if preview == "ready" else ("placeholder" if preview == "ready_with_placeholders" else False) - payload["ready_for_implementation"] = implementation in {"ready", "ready_with_assumptions"} - payload["blockers"] = blockers - payload["required_fields"] = list(required) - payload["questions"] = [ - {"id": field, "field": field, "reason": "risk/quality driven follow-up required"} - for field in required - ] - payload["implementation_blocked"] = implementation == "blocked" - payload["placeholder_fallback_boundary"] = [_PLACEHOLDER_BOUNDARY] - payload["implementation_stop_conditions"] = stop_conditions - payload["core_quality_requirements"] = [str(item.get("requirement", "")) for item in qcr if isinstance(item, dict)] - payload["known_missing_information"] = missing - payload["completion_acceptance_criteria"] = payload["completion_criteria"] - payload["risk_summary"] = render_risk_summary(payload, locale=str(answers.get("locale") or "en-US")) - return payload - - -__all__ = ["analyze_risk_readiness", "render_risk_summary", "dynamic_required_fields"] diff --git a/skills/artic/scripts/scaffold_artic_files.py b/skills/artic/scripts/scaffold_artic_files.py index ddec398..dae725e 100755 --- a/skills/artic/scripts/scaffold_artic_files.py +++ b/skills/artic/scripts/scaffold_artic_files.py @@ -100,7 +100,7 @@ def scaffold(root: Path, project_name: str, locale: str = "en-US") -> None: "forbidden_copy_elements": ["logos", "trademarks", "proprietary illustrations", "exact layouts", "source copywriting"], } brief = { - "artic_version": "0.4.1", + "artic_version": "0.5.0", "project": { "name": project_name, "type": "homepage", @@ -139,7 +139,7 @@ def scaffold(root: Path, project_name: str, locale: str = "en-US") -> None: ], "synthesis": "Clean SaaS hierarchy with token discipline and mobile-first accessibility.", } - state = {"artic_version": "0.4.1", "last_generated_at": now, "status": "scaffolded", "language": language, "intent_path": ".artic/intent.json", "strategy_path": ".artic/strategy.json"} + state = {"artic_version": "0.5.0", "last_generated_at": now, "status": "scaffolded", "language": language, "intent_path": ".artic/intent.json", "strategy_path": ".artic/strategy.json"} write(root / ".artic" / "intent.json", json.dumps(intent, indent=2, ensure_ascii=False) + "\n") write(root / ".artic" / "strategy.json", json.dumps(strategy, indent=2, ensure_ascii=False) + "\n") write(root / ".artic" / "brief.json", json.dumps(brief, indent=2, ensure_ascii=False) + "\n") diff --git a/skills/artic/scripts/validate_artic_outputs.py b/skills/artic/scripts/validate_artic_outputs.py index 74da914..699aaaf 100755 --- a/skills/artic/scripts/validate_artic_outputs.py +++ b/skills/artic/scripts/validate_artic_outputs.py @@ -20,27 +20,6 @@ REQUIRED_SPACING_TOKENS = ("xs", "sm", "md", "lg", "xl", "section") REQUIRED_COMPONENT_TOKENS = ("button-primary", "button-secondary", "card", "form-field", "proof-strip") REQUIRED_QA_TERMS = ("Visual hierarchy", "Brand coherence", "Conversion clarity", "Mobile quality", "Accessibility", "Reference safety") -RISK_SECTIONS_EN = ( - "Risk / Readiness Summary", - "Core quality requirements", - "Known missing information", - "Safe assumptions", - "Unsafe assumptions", - "Placeholder/fallback boundary", - "Implementation stop conditions", - "Completion/acceptance criteria", -) -RISK_SECTIONS_KO = ( - "위험/준비 상태 요약", - "핵심 품질 요구사항", - "알려진 누락 정보", - "안전한 가정", - "위험한 가정", - "플레이스홀더/대체 경계", - "구현 중단 조건", - "완료/수용 기준", -) -RISK_DOCS = ("DESIGN.md", "docs/artic-strategy.md", "docs/design-rules.md", "docs/design-qa-checklist.md", "docs/homepage-design-prompt.md") POLICY_FRAGMENT = "extract reusable principles only" POLICY_MARKER = "" LANGUAGE_MARKER_PREFIX = "" not in text: errors.append(f"ERROR: localized outputs missing language marker: {brief_locale} in {rel}") - if declared_risk_readiness: - required_risk_sections = RISK_SECTIONS_KO if brief_locale.startswith("ko") else RISK_SECTIONS_EN - for rel in RISK_DOCS: - path = root / rel - if not path.exists(): - continue - text = path.read_text(encoding="utf-8") - for section in required_risk_sections: - if section not in text: - errors.append(f"ERROR: {rel} missing risk_readiness section: {section}") - if risk_implementation_blocked: - blocked_terms = [ - "implementation is blocked until missing inputs are resolved", - "누락된 정보가 해결될 때까지 구현은 차단됩니다", - ] - if not any(term in text for term in blocked_terms): - errors.append(f"ERROR: {rel} missing risk_readiness implementation block notice") checklist = root / "docs" / "design-qa-checklist.md" if checklist.exists(): text = checklist.read_text(encoding="utf-8") diff --git a/skills/artic/templates/brief.schema.json b/skills/artic/templates/brief.schema.json index 4a6ce89..4b93137 100644 --- a/skills/artic/templates/brief.schema.json +++ b/skills/artic/templates/brief.schema.json @@ -17,27 +17,6 @@ "requirements": {"type": "object"}, "constraints": {"type": "object"}, "asset_policy": {"type": "object"}, - "risk_readiness": { - "type": "object", - "required": ["schema_version", "risk_categories", "quality_critical_requirements", "dynamic_required_fields", "missing_dynamic_required_fields", "readiness", "safe_assumptions", "unsafe_assumptions", "placeholder_boundary", "stop_conditions", "risk_summary", "completion_criteria"], - "properties": { - "schema_version": {"const": "artic-risk-readiness-v1"}, - "risk_categories": {"type": "array", "items": {"type": "string"}}, - "quality_critical_requirements": {"type": "array"}, - "dynamic_required_fields": {"type": "array", "items": {"type": "string"}}, - "missing_dynamic_required_fields": {"type": "array", "items": {"type": "string"}}, - "readiness": { - "type": "object", - "required": ["strategy", "preview", "implementation", "status"] - }, - "safe_assumptions": {"type": "array", "items": {"type": "string"}}, - "unsafe_assumptions": {"type": "array", "items": {"type": "string"}}, - "placeholder_boundary": {"type": "string"}, - "stop_conditions": {"type": "array", "items": {"type": "string"}}, - "risk_summary": {"type": "string"}, - "completion_criteria": {"type": "array", "items": {"type": "string"}} - } - }, "implementation": {"type": "object"}, "language": { "type": "object", diff --git a/skills/artic/templates/strategy.schema.json b/skills/artic/templates/strategy.schema.json index 650be4f..08711ad 100644 --- a/skills/artic/templates/strategy.schema.json +++ b/skills/artic/templates/strategy.schema.json @@ -54,27 +54,6 @@ "forbidden_copy_elements": { "type": "array", "contains": {"const": "logos"} - }, - "risk_readiness": { - "type": "object", - "required": ["schema_version", "risk_categories", "quality_critical_requirements", "dynamic_required_fields", "missing_dynamic_required_fields", "readiness", "safe_assumptions", "unsafe_assumptions", "placeholder_boundary", "stop_conditions", "risk_summary", "completion_criteria"], - "properties": { - "schema_version": {"const": "artic-risk-readiness-v1"}, - "risk_categories": {"type": "array", "items": {"type": "string"}}, - "quality_critical_requirements": {"type": "array"}, - "dynamic_required_fields": {"type": "array", "items": {"type": "string"}}, - "missing_dynamic_required_fields": {"type": "array", "items": {"type": "string"}}, - "readiness": { - "type": "object", - "required": ["strategy", "preview", "implementation", "status"] - }, - "safe_assumptions": {"type": "array", "items": {"type": "string"}}, - "unsafe_assumptions": {"type": "array", "items": {"type": "string"}}, - "placeholder_boundary": {"type": "string"}, - "stop_conditions": {"type": "array", "items": {"type": "string"}}, - "risk_summary": {"type": "string"}, - "completion_criteria": {"type": "array", "items": {"type": "string"}} - } } }, "additionalProperties": true diff --git a/tests/test_artic_package.py b/tests/test_artic_package.py index 40849a9..f322c27 100644 --- a/tests/test_artic_package.py +++ b/tests/test_artic_package.py @@ -14,7 +14,6 @@ artic_update = importlib.import_module("artic_update") artic_version = importlib.import_module("artic_version") artic_init_session = importlib.import_module("artic_init_session") -risk_readiness = importlib.import_module("risk_readiness") README_FILES = ["README.md", "README.ko.md", "README.ja.md", "README.zh-CN.md", "README.zh-TW.md"] @@ -151,62 +150,6 @@ def test_skill_copies_are_in_sync(): assert (copy / rel).read_bytes() == path.read_bytes(), rel -def test_low_risk_readiness_contract_is_implementation_ready(): - answers = { - "project": "AI meeting notes SaaS", - "audience": "startup teams", - "goal": "demo request", - "vibe": "trustworthy clean", - } - payload = risk_readiness.analyze_risk_readiness(answers) - - assert payload["schema_version"] == "artic-risk-readiness-v1" - assert payload["missing_dynamic_required_fields"] == [] - assert payload["readiness"]["strategy"] == "ready" - assert payload["readiness"]["preview"] == "ready" - assert payload["readiness"]["implementation"] in {"ready", "ready_with_assumptions"} - assert payload["readiness"]["status"] in {"ready", "ready_with_assumptions"} - assert "implementation" not in " ".join(payload["stop_conditions"]).lower() - - -def test_high_risk_korean_3d_site_blocks_implementation_until_dynamic_fields_are_answered(): - answers = { - "project": "마우스로 만지는 3D 석고상 홈페이지", - "audience": "디자인 학생", - "goal": "전시 예약", - "vibe": "고급스럽고 실제 갤러리처럼", - } - payload = risk_readiness.analyze_risk_readiness(answers) - - assert "core_visual_asset_dependency" in payload["risk_categories"] - assert "interaction_dependency" in payload["risk_categories"] - assert "conversion_business" in payload["risk_categories"] - assert {"asset_source", "interaction_model"}.issubset(set(payload["missing_dynamic_required_fields"])) - assert payload["readiness"]["strategy"] == "ready" - assert payload["readiness"]["preview"] == "ready_with_placeholders" - assert payload["readiness"]["implementation"] == "blocked" - assert payload["readiness"]["status"] == "implementation_blocked" - assert any("placeholder" in condition.lower() for condition in payload["stop_conditions"]) - assert any("substitute" in item.lower() for item in payload["unsafe_assumptions"]) - assert "placeholder" in payload["placeholder_boundary"].lower() - - -def test_risk_product_photo_quality_critical_requirement_is_preview_only_placeholder(): - payload = risk_readiness.analyze_risk_readiness({ - "project": "고급스러운 실제 제품 사진 핵심 랜딩페이지", - "audience": "premium shoppers", - "goal": "문의", - "vibe": "luxury editorial", - }) - - requirements = payload["quality_critical_requirements"] - assert requirements - assert any("제품 사진" in req["requirement"] or "product photo" in req["requirement"].lower() for req in requirements) - assert all(req["placeholder_policy"] == "preview_only" for req in requirements) - assert any("generic gradient" in item.lower() or "gradient" in item.lower() for item in payload["unsafe_assumptions"]) - assert payload["readiness"]["implementation"] == "blocked" - - def headings(path: Path): return [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.startswith("## ")] @@ -271,31 +214,59 @@ def test_readmes_document_version_and_update_commands(): assert phrase in text, (rel, phrase) -def test_readmes_document_init_start_show_lifecycle_boundary(): +def test_active_files_do_not_keep_previous_release_version_literals(): + previous_literals = ("0.4.1", "v0.4.1") + checked_roots = [ + ROOT / "README.md", + ROOT / "README.ko.md", + ROOT / "README.ja.md", + ROOT / "README.zh-CN.md", + ROOT / "README.zh-TW.md", + ROOT / "pyproject.toml", + ROOT / ".claude-plugin" / "marketplace.json", + ROOT / ".agents" / "plugins" / "marketplace.json", + ROOT / "skills" / "artic", + ROOT / "plugins" / "claude-artic", + ROOT / "plugins" / "codex-artic", + ] + ignored_parts = {"references", "__pycache__"} + text_suffixes = {".json", ".md", ".py", ".toml"} + offenders: list[str] = [] + for root in checked_roots: + paths = [root] if root.is_file() else [path for path in root.rglob("*") if path.is_file()] + for path in paths: + if ignored_parts & set(path.relative_to(ROOT).parts): + continue + if path.suffix not in text_suffixes: + continue + text = path.read_text(encoding="utf-8") + for literal in previous_literals: + if literal in text: + offenders.append(f"{path.relative_to(ROOT)} contains {literal}") + assert offenders == [] + + +def test_readmes_document_init_start_lifecycle_boundary(): required_phrases = [ ".artic/init-session.json", - ".artic/show/index.html", - ".artic/show/styles.css", - ".artic/show/tokens.json", - ".artic/show/assets/manifest.json", - ".artic/show/report.json", - ".artic/show/critique.md", - ".artic/show/selected.json", - ".artic/show/iterations//...", - "asset-first preview bundle", - "preview-only", ".artic/strategy.json", "docs/artic-strategy.md", "@artic init", "@artic start", + ] + forbidden_phrases = [ "@artic show", "@artic review", - "python3 skills/artic/scripts/artic_show.py --root .", + ".artic/show/index.html", + "artic_show.py", + "production_ready", ] for rel in README_FILES: text = (ROOT / rel).read_text(encoding="utf-8") for phrase in required_phrases: assert phrase in text, (rel, phrase) + for phrase in forbidden_phrases: + assert phrase not in text, (rel, phrase) def test_artic_version_no_network_marks_latest_unchecked(): @@ -1117,105 +1088,6 @@ def test_validator_accepts_scaffold_generated_strategy_artifacts(): assert result.returncode == 0, result.stdout -def inject_risk_readiness(root: Path, *, locale: str = "en-US") -> dict: - brief_path = root / ".artic" / "brief.json" - brief = json.loads(brief_path.read_text(encoding="utf-8")) - brief["language"]["locale"] = locale - if locale == "ko-KR": - brief["language"]["output_language"] = "Korean" - brief["language"]["tone"] = "명확하고 전문적인 한국어" - brief["risk_readiness"] = { - "ready_for_strategy": True, - "implementation_blocked": True, - "core_quality_requirements": ["requested product photo/3D/map/payment trust intent must be satisfied"], - "known_missing_information": ["licensed product photography and 3D model source files are not provided"], - "safe_assumptions": ["strategy and static preview can use labeled placeholders"], - "unsafe_assumptions": ["placeholder asset is production-equivalent"], - "placeholder_fallback_boundary": ["placeholder is not accepted as production substitute for quality-critical requirement"], - "implementation_stop_conditions": ["stop production implementation until missing licensed assets are resolved"], - "completion_acceptance_criteria": ["requested product photo/3D/map/payment trust intent is satisfied or explicitly blocked"], - } - brief_path.write_text(json.dumps(brief, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - return brief - - -def test_artic_start_propagates_risk_readiness_into_generated_docs_and_checklist(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/scaffold_artic_files.py"), "--root", tmp], check=True) - inject_risk_readiness(root) - write_fixture_strategy(root) - - result = subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/artic_start.py"), "--root", tmp], check=True, capture_output=True, text=True) - payload = json.loads(result.stdout) - - assert payload["validated"] is True - generated_docs = [ - root / "DESIGN.md", - root / "docs" / "artic-strategy.md", - root / "docs" / "design-rules.md", - root / "docs" / "design-qa-checklist.md", - root / "docs" / "homepage-design-prompt.md", - ] - for path in generated_docs: - text = path.read_text(encoding="utf-8") - assert "Risk / Readiness Summary" in text, path - for required in [ - "Core quality requirements", - "Known missing information", - "Safe assumptions", - "Unsafe assumptions", - "Placeholder/fallback boundary", - "Implementation stop conditions", - "Completion/acceptance criteria", - ]: - assert required in text, (path, required) - assert "implementation is blocked until missing inputs are resolved" in text, path - - checklist = (root / "docs" / "design-qa-checklist.md").read_text(encoding="utf-8") - assert "requested product photo/3D/map/payment trust intent is satisfied or explicitly blocked" in checklist - assert "placeholder is not accepted as production substitute for quality-critical requirement" in checklist - - -def test_artic_start_omits_risk_summary_when_brief_has_no_risk_readiness(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/scaffold_artic_files.py"), "--root", tmp], check=True) - write_fixture_strategy(root) - - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/artic_start.py"), "--root", tmp], check=True, capture_output=True, text=True) - - assert "Risk / Readiness Summary" not in (root / "DESIGN.md").read_text(encoding="utf-8") - assert "Risk / Readiness Summary" not in (root / "docs" / "design-qa-checklist.md").read_text(encoding="utf-8") - - -def test_artic_start_risk_readiness_uses_korean_labels_for_korean_brief(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/scaffold_artic_files.py"), "--root", tmp], check=True) - inject_risk_readiness(root, locale="ko-KR") - write_fixture_strategy(root) - - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/artic_start.py"), "--root", tmp], check=True, capture_output=True, text=True) - - checklist = (root / "docs" / "design-qa-checklist.md").read_text(encoding="utf-8") - assert "## 위험/준비 상태 요약" in checklist - assert "핵심 품질 요구사항" in checklist - assert "누락된 정보가 해결될 때까지 구현은 차단됩니다" in checklist - - -def test_validator_rejects_missing_risk_sections_when_brief_declares_risk_readiness(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/scaffold_artic_files.py"), "--root", tmp], check=True) - inject_risk_readiness(root) - - result = subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/validate_artic_outputs.py"), "--root", tmp], capture_output=True, text=True) - - assert result.returncode == 1 - assert "risk_readiness" in result.stdout - - def test_artic_init_role_assignments_only_reference_selected_sources(): with tempfile.TemporaryDirectory() as tmp: subprocess.run([ @@ -1286,16 +1158,6 @@ def test_artic_start_no_validate_skips_validator_but_writes_outputs(): -def run_artic_show(root: Path, *extra_args: str, check: bool = True) -> subprocess.CompletedProcess[str]: - return subprocess.run([ - sys.executable, - str(ROOT / "skills/artic/scripts/artic_show.py"), - "--root", - str(root), - *extra_args, - ], check=check, capture_output=True, text=True) - - def read_json_file(path: Path) -> dict: return json.loads(path.read_text(encoding="utf-8")) @@ -1305,316 +1167,6 @@ def parse_json_stdout(result: subprocess.CompletedProcess[str]) -> dict: return json.loads(result.stdout) -def assert_show_bundle_contract(root: Path, payload: dict) -> None: - show = root / ".artic" / "show" - final_files = [ - "index.html", - "styles.css", - "tokens.json", - "report.json", - "critique.md", - "selected.json", - "assets/manifest.json", - ] - for rel in final_files: - assert (show / rel).is_file(), rel - - iteration_files = [ - "index.html", - "styles.css", - "tokens.json", - "report.json", - "assets/manifest.json", - ] - for rel in iteration_files: - assert (show / "iterations" / "001" / rel).is_file(), rel - - selected = read_json_file(show / "selected.json") - report = read_json_file(show / "report.json") - assert selected["selected_iteration"] == report["selected_iteration"] == "001" - assert selected["candidate"] == report["candidate"] - assert payload["preview_bundle"] == str(show) - assert payload["entrypoint"] == str(show / "index.html") - assert payload.get("preview_file") in (None, str(show / "index.html")) - assert str(show / "report.json") == payload["report_file"] - assert str(show / "critique.md") == payload["critique_file"] - assert str(show / "index.html") in payload["generated_preview_files"] - assert str(show / "styles.css") in payload["generated_preview_files"] - assert str(show / "tokens.json") in payload["generated_preview_files"] - assert str(show / "assets" / "manifest.json") in payload["asset_files"] - assert payload["modified_app_files"] == [] - assert report["modified_app_files"] == [] - - manifest = read_json_file(show / "assets" / "manifest.json") - assert manifest["schema_version"] - assert manifest["mode"] == "asset-first-preview" - assert isinstance(manifest["assets"], list) and manifest["assets"] - allowed_statuses = {"generated", "catalog-reference", "unverified-preview-only"} - assert {asset["status"] for asset in manifest["assets"]} <= allowed_statuses - - -def test_artic_show_writes_asset_first_preview_bundle_and_payload_contract(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/scaffold_artic_files.py"), "--root", tmp], check=True) - app_file = root / "src" / "App.tsx" - app_file.parent.mkdir(parents=True) - original_app = "export default function App() { return
    Existing app
    ; }\n" - app_file.write_text(original_app, encoding="utf-8") - - result = run_artic_show(root) - - payload = parse_json_stdout(result) - assert_show_bundle_contract(root, payload) - assert app_file.read_text(encoding="utf-8") == original_app - html = (root / ".artic" / "show" / "index.html").read_text(encoding="utf-8") - css = (root / ".artic" / "show" / "styles.css").read_text(encoding="utf-8") - tokens = read_json_file(root / ".artic" / "show" / "tokens.json") - assert "" in html - assert "Artic Preview" in html - assert "Reference policy" in html - assert "DESIGN.md" in html - assert "--primary" in css - assert tokens["colors"]["primary"] - - -def test_artic_show_max_iterations_one_writes_only_iteration_001_and_selects_it(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/scaffold_artic_files.py"), "--root", tmp], check=True) - - result = run_artic_show(root, "--max-iterations", "1", check=False) - - assert result.returncode == 0, result.stderr or result.stdout - payload = parse_json_stdout(result) - assert_show_bundle_contract(root, payload) - iterations = sorted(path.name for path in (root / ".artic" / "show" / "iterations").iterdir() if path.is_dir()) - assert iterations == ["001"] - assert read_json_file(root / ".artic" / "show" / "selected.json")["selected_iteration"] == "001" - - -def test_artic_show_invalid_max_iterations_returns_json_error(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/scaffold_artic_files.py"), "--root", tmp], check=True) - - result = run_artic_show(root, "--max-iterations", "0", check=False) - - assert result.returncode != 0 - payload = parse_json_stdout(result) - assert "max-iterations" in payload["error"] - assert not (root / ".artic" / "show" / "iterations" / "001").exists() - - -def test_artic_show_strict_min_score_above_possible_fails_below_threshold_report(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/scaffold_artic_files.py"), "--root", tmp], check=True) - - result = run_artic_show(root, "--strict", "--min-score", "101", check=False) - - assert result.returncode != 0 - payload = parse_json_stdout(result) - assert "quality" in payload["error"].lower() or "threshold" in payload["error"].lower() - report_path = root / ".artic" / "show" / "report.json" - assert report_path.is_file() - report = read_json_file(report_path) - assert report["status"] == "below-threshold" - assert report["modified_app_files"] == [] - - -def test_artic_show_3d_runtime_bundle_records_asset_provenance_and_runtime_markers(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/scaffold_artic_files.py"), "--root", tmp], check=True) - brief_path = root / ".artic" / "brief.json" - brief = read_json_file(brief_path) - brief.setdefault("style", {})["search_facets"] = ["3d-webgl", "model-viewer", "interactive-hero"] - brief.setdefault("requirements", {})["must_have_feature"] = "interactive 3D WebGL model-viewer hero" - brief_path.write_text(json.dumps(brief, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - - references_path = root / ".artic" / "references.json" - references = read_json_file(references_path) - references.setdefault("role_assignments", []).append({ - "role": "3d_runtime", - "source_ids": ["model-viewer", "threejs-examples"], - "selected_source_ids": ["model-viewer"], - "selection_reason": "Exercise asset-first preview provenance for 3D runtime fixtures.", - }) - references_path.write_text(json.dumps(references, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - - result = run_artic_show(root) - - payload = parse_json_stdout(result) - assert_show_bundle_contract(root, payload) - show = root / ".artic" / "show" - assert (show / "assets" / "model-poster.svg").exists() or (show / "assets" / "scene-fallback.svg").exists() - manifest = read_json_file(show / "assets" / "manifest.json") - serialized_manifest = json.dumps(manifest, ensure_ascii=False).lower() - assert "3d" in serialized_manifest or "model-viewer" in serialized_manifest or "webgl" in serialized_manifest - assert "provenance" in serialized_manifest - manifest_paths = {asset["path"] for asset in manifest["assets"] if asset.get("path")} - asset_files = {str(path.relative_to(show)) for path in (show / "assets").rglob("*") if path.is_file()} - assert asset_files - {"assets/manifest.json"} <= manifest_paths - html = (show / "index.html").read_text(encoding="utf-8") - assert "runtime-3d" in html or "model-viewer" in html - assert "interaction-zone" in html or "3D" in html - - -def test_artic_show_cleans_stale_iteration_directories_between_runs(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/scaffold_artic_files.py"), "--root", tmp], check=True) - - run_artic_show(root) - assert sorted(path.name for path in (root / ".artic" / "show" / "iterations").iterdir() if path.is_dir()) == ["001", "002", "003"] - - result = run_artic_show(root, "--max-iterations", "1") - - assert result.returncode == 0 - assert sorted(path.name for path in (root / ".artic" / "show" / "iterations").iterdir() if path.is_dir()) == ["001"] - report = read_json_file(root / ".artic" / "show" / "report.json") - assert report["selected_iteration"] == "001" - - -def test_artic_show_blocks_missing_design_inputs_without_creating_preview(): - with tempfile.TemporaryDirectory() as tmp: - result = subprocess.run([ - sys.executable, - str(ROOT / "skills/artic/scripts/artic_show.py"), - "--root", - tmp, - ], capture_output=True, text=True) - assert result.returncode == 1 - payload = json.loads(result.stdout) - assert "missing required input" in payload["error"] - assert "DESIGN.md" in payload["error"] - assert not (Path(tmp) / ".artic" / "show").exists() - - -def test_artic_show_requires_strategy_contract_before_creating_preview(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/scaffold_artic_files.py"), "--root", tmp], check=True) - (root / ".artic" / "strategy.json").unlink() - - result = subprocess.run([ - sys.executable, - str(ROOT / "skills/artic/scripts/artic_show.py"), - "--root", - tmp, - ], capture_output=True, text=True) - - assert result.returncode == 1 - payload = json.loads(result.stdout) - assert ".artic/strategy.json" in payload["error"] - assert not (root / ".artic" / "show" / "index.html").exists() - - -def test_artic_show_generates_static_preview_without_modifying_app_files(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/scaffold_artic_files.py"), "--root", tmp], check=True) - app_file = root / "src" / "App.tsx" - app_file.parent.mkdir(parents=True) - original_app = "export default function App() { return
    Existing app
    ; }\n" - app_file.write_text(original_app, encoding="utf-8") - - result = subprocess.run([ - sys.executable, - str(ROOT / "skills/artic/scripts/artic_show.py"), - "--root", - tmp, - ], check=True, capture_output=True, text=True) - - payload = json.loads(result.stdout) - preview = root / ".artic" / "show" / "index.html" - assert payload["preview_file"] == str(preview) - assert payload["modified_app_files"] == [] - assert preview.exists() - assert app_file.read_text(encoding="utf-8") == original_app - html = preview.read_text(encoding="utf-8") - assert "" in html - assert "Artic Preview" in html - assert "Reference policy" in html - assert "DESIGN.md" in html - - -def test_artic_show_high_risk_3d_placeholder_reports_not_production_ready(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - (root / "docs").mkdir(parents=True) - (root / ".artic").mkdir(parents=True) - (root / "DESIGN.md").write_text( - """---\nname: \"한국어 3D 쇼룸\"\ndescription: \"3D 런타임 중심 프리뷰\"\ncolors:\n primary: \"#1F4FD8\"\n accent: \"#7C3AED\"\n---\n\n## Overview\n3D 제품 경험을 검증합니다.\n\n## Design North Star\n런타임 상호작용이 핵심인 쇼룸.\n\n## Page Composition\n히어로, 3D 런타임, 전환.\n""", - encoding="utf-8", - ) - (root / "docs" / "homepage-design-prompt.md").write_text("# Prompt\n", encoding="utf-8") - (root / ".artic" / "strategy.json").write_text("{}\n", encoding="utf-8") - (root / ".artic" / "brief.json").write_text(json.dumps({ - "project": { - "name": "한국어 3D 쇼룸", - "target_users": ["큐레이터"], - "primary_goal": "상담 요청", - }, - "language": {"locale": "ko-KR"}, - "risk_readiness": { - "implementation_blocked": True, - "placeholder_fallback_boundary": ["실제 GLB 에셋 미확보", "3D 조작 QA 미완료"], - "implementation_stop_conditions": ["라이선스 확인 가능한 모델 에셋 필요"], - "core_quality_requirements": [{"requirement": "모바일 포스터 폴백", "status": "missing"}], - }, - }, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - (root / ".artic" / "references.json").write_text(json.dumps({ - "selected_sources": [{"id": "model-viewer", "name": "model-viewer"}], - "role_assignments": [{"role": "3d_runtime", "source_ids": ["model-viewer"]}], - }, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - - result = subprocess.run([ - sys.executable, - str(ROOT / "skills/artic/scripts/artic_show.py"), - "--root", - tmp, - ], check=True, capture_output=True, text=True) - - payload = json.loads(result.stdout) - assert payload["production_ready"] is False - assert payload["preview_status"] == "placeholder_preview" - assert payload["modified_app_files"] == [] - assert "실제 GLB 에셋 미확보" in payload["placeholder_boundaries"] - assert "라이선스 확인 가능한 모델 에셋 필요" in payload["implementation_blockers"] - html = (root / ".artic" / "show" / "index.html").read_text(encoding="utf-8") - assert "프로덕션 준비 완료가 아닙니다" in html - assert "플레이스홀더 경계" in html - assert "실제 GLB 에셋 미확보" in html - assert "model-viewer · GLB" in html - - -def test_artic_show_sanitizes_design_token_values_before_css_output(): - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - subprocess.run([sys.executable, str(ROOT / "skills/artic/scripts/scaffold_artic_files.py"), "--root", tmp], check=True) - design = root / "DESIGN.md" - design.write_text( - design.read_text(encoding="utf-8").replace( - ' primary: "#1F4FD8"', - ' primary: "red;}