diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e62120e..4ae2ffa 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "superloopy", "source": "./", "description": "Strict evidence loop harness: repo-local loop state, success criteria, append-only ledgers, evidence-gated completion, plus skills (loop, research, frontend, clone) and a read-only/worker crew. Works on Claude Code and Codex from one repo.", - "version": "0.10.0", + "version": "0.11.0", "license": "MIT", "keywords": [ "claude-code", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 94e484e..c234e60 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "superloopy", - "version": "0.10.0", + "version": "0.11.0", "description": "Lightweight loop harness with strict evidence gates — Claude Code edition.", "homepage": "https://github.com/beefiker/superloopy#readme", "repository": "https://github.com/beefiker/superloopy", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 07aa88e..40d24d3 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "superloopy", - "version": "0.10.0", + "version": "0.11.0", "description": "Lightweight loop harness with strict evidence gates.", "homepage": "https://github.com/beefiker/superloopy#readme", "repository": "https://github.com/beefiker/superloopy", diff --git a/README.es.md b/README.es.md index d2133eb..61556dd 100644 --- a/README.es.md +++ b/README.es.md @@ -48,7 +48,7 @@ Superloopy mantiene pequeña la capa de comandos. Las skills guardan el flujo es | `superloopy-doctor` | Diagnosticas instalación, wrapper, plugin cache, hook/bootstrap, agentes, wiring de Codex/Claude Code o versiones stale. | Health report de solo lectura: evidencia de wrapper/cache/version, checks fallidos y el comando exacto de reparación que solo se ejecuta si lo apruebas. | | `superloopy-research` | Pides `loopy research`, deep research, exhaustive investigation o un informe con citas. | Ejes de investigación, expansion waves, un claim ledger, notas de verificación y un synthesis artifact citado. | | `superloopy-clone` | Pides `loopy clone`, clonación autorizada de un sitio, reconstrucción, migración o recuperación visual precisa de páginas. | Capturas de navegador, topología de página, design tokens, inventario de assets, notas de implementación, salida de build y evidencia de visual QA. | -| `superloopy-frontend` | Para una tarea de frontend web o GUI de escritorio Qt, invocas `$superloopy:superloopy-frontend` en Codex o `/superloopy:superloopy-frontend` en Claude Code, o inicias ese tipo de tarea con `loopy`/`루피` al principio. El trabajo visual que no sea web/Qt y la simple mención de UI no la activan. | Un contrato de tokens DESIGN.md; para web, la comprobación anti-slop previa y la evidencia del navegador; para Qt, la comprobación previa de Qt y la evidencia de la aplicación nativa renderizada. | +| `superloopy-frontend` | Solo para interfaces de aplicaciones en pantalla dentro del alcance —Web alojada en navegador (pública, autenticada, privada/interna, PWA instalada o extensión), Web interactiva de contenido ya desplegada (campañas, publicaciones o experiencias de landing con un recorrido de usuario), escritorio, móvil/tableta, clientes embebidos/híbridos, UI con renderizado personalizado, Qt o destinos mixtos—, invocas explícitamente `$superloopy:superloopy-frontend` en Codex o `/superloopy:superloopy-frontend` en Claude Code, o inicias ese trabajo con `loopy`/`루피` al principio. La mera terminología de UI, plataforma o framework no la activa; se excluyen TV, wearables, XR, automoción, UI de juegos, TUI, artefactos estáticos de medios/documentos y trabajo que no sea de UI. | Un contrato de UX compartido más rutas de plataforma/composición. La evidencia es proporcional a las afirmaciones modificadas y verifica por separado cada navegador, destino/shell nativo, renderizador y destino mixto. Las ejecuciones independientes conservan recibos por ejecución; los bucles activos los vinculan al goal y criterion. | | `humanize-korean` | Úsala cuando usuarios coreanos piden quitar el tono AI, corregir 번역투 o hacer que el texto coreano suene humano sin cambiar hechos. | Escribe `final.md`, `summary.md` y `audit.json`; en loops de Superloopy registra evidencia en `.superloopy/evidence/humanize-korean/`. | | `superloopy-slides` | Pides diapositivas, una presentación, un deck o convertir un PPT/PPTX a web. | Un deck HTML de un solo archivo sin dependencias en un escenario 16:9 fijo, tres previews de estilo para elegir y un artefacto de visual QA con capturas renderizadas bajo `.superloopy/evidence/slides/`. | diff --git a/README.ja.md b/README.ja.md index 86a25b6..d48dafc 100644 --- a/README.ja.md +++ b/README.ja.md @@ -48,7 +48,7 @@ Superloopy はコマンド層を小さく保ちます。専門的な進め方は | `superloopy-doctor` | install、wrapper、plugin cache、hook/bootstrap、agent、Codex/Claude Code host wiring、stale version の問題を診断するとき。 | 読み取り専用の health report: wrapper/cache/version の証拠、失敗した check、承認後にだけ実行する正確な修復コマンド。 | | `superloopy-research` | `loopy research`、deep research、exhaustive investigation、または引用付きレポートを求めるとき。 | 調査軸、拡張 wave、claim ledger、検証メモ、引用付き synthesis artifact。 | | `superloopy-clone` | `loopy clone`、許可された Web サイトのクローン、再構築、移行、ピクセル単位の復元を求めるとき。 | ブラウザ取得、ページ構造、デザイントークン、アセット一覧、実装メモ、build 出力、visual QA 証拠。 | -| `superloopy-frontend` | Web フロントエンドまたは Qt デスクトップ GUI の作業で Codex の `$superloopy:superloopy-frontend` または Claude Code の `/superloopy:superloopy-frontend` を明示的に呼び出すか、その作業を先頭の `loopy`/`루피` で始めるとき。Web/Qt 以外の視覚タスクや UI への単なる言及では起動しません。 | DESIGN.md のトークン契約を残し、Web は anti-slop の事前チェックとブラウザー証拠、Qt は Qt の事前チェックとネイティブアプリのレンダリング証拠で検証します。 | +| `superloopy-frontend` | 対応範囲の画面ベースのアプリ UI(公開・認証済み・非公開/社内向け・インストール型 PWA/拡張機能を含むブラウザーホスト Web、ユーザージャーニーを備えたインタラクティブなデプロイ済みコンテンツ主導 Web(キャンペーン、出版、ランディング体験など)、デスクトップ、モバイル/タブレット、組み込み/ハイブリッドクライアント、カスタムレンダリング UI、Qt、混在ターゲット)の作業で Codex の `$superloopy:superloopy-frontend` または Claude Code の `/superloopy:superloopy-frontend` を明示的に呼び出すか、その作業を先頭の `loopy`/`루피` で始める場合だけ。単なる UI・プラットフォーム・フレームワーク用語では起動せず、TV、ウェアラブル、XR、自動車、ゲーム UI、TUI、静的なメディア/文書成果物、非 UI 作業は除外します。 | 1 つの共通 UX 契約にプラットフォーム/コンポジション経路を加えます。証拠は変更した主張に比例し、ブラウザー、ネイティブのターゲット/シェル、レンダラー、混在ターゲットの各所有者を独立して検証します。単独実行では実行単位の証跡を保持し、アクティブなループでは goal と criterion に関連付けます。 | | `humanize-korean` | 韓国語テキストの AI っぽさや翻訳調を抑え、事実を変えずに人が書いたように整えるとき。 | `final.md`、`summary.md`、`audit.json` を書き、Superloopy loop では `.superloopy/evidence/humanize-korean/` に証拠を残します。 | | `superloopy-slides` | スライド・プレゼン・デッキを頼むとき、または PPT/PPTX を Web に変換するとき。 | 固定 16:9 ステージの依存ゼロ単一 HTML デッキ、選べる 3 種のスタイルプレビュー、`.superloopy/evidence/slides/` 配下のレンダリングスクリーンショット visual-QA artifact。 | diff --git a/README.ko.md b/README.ko.md index 9431274..4886e28 100644 --- a/README.ko.md +++ b/README.ko.md @@ -48,7 +48,7 @@ Superloopy는 명령을 작게 둡니다. 대신 스킬이 작업 방식을 잡 | `superloopy-doctor` | install, wrapper, plugin cache, hook/bootstrap, agent, Codex/Claude Code host wiring, stale version 문제를 진단할 때. | 읽기 전용 health report: wrapper/cache/version 근거, 실패한 체크, 승인 후 실행할 정확한 복구 명령. | | `superloopy-research` | `loopy research`, deep research, exhaustive investigation, 출처 있는 리서치 보고서를 요청할 때. | 리서치 축, 확장 wave, claim ledger, 검증 메모, 출처가 붙은 synthesis artifact. | | `superloopy-clone` | `loopy clone`, 허가된 웹사이트 클론, 리빌드, 마이그레이션, 픽셀 기준 복구를 요청할 때. | 브라우저 캡처, 페이지 구조, 디자인 토큰, asset 목록, 구현 메모, build 출력, visual QA 근거. | -| `superloopy-frontend` | 웹 프런트엔드 또는 Qt 데스크톱 GUI 작업에 Codex의 `$superloopy:superloopy-frontend`나 Claude Code의 `/superloopy:superloopy-frontend`를 직접 호출하거나, 그런 작업을 선행 `loopy`/`루피`로 시작할 때. 웹/Qt 이외의 시각 작업과 단순한 UI 언급으로는 켜지지 않습니다. | DESIGN.md 토큰 계약을 남기고, 웹은 anti-slop pre-flight와 브라우저 근거로, Qt는 Qt pre-flight와 네이티브 애플리케이션 렌더링 근거로 검증합니다. | +| `superloopy-frontend` | 지원되는 화면 기반 애플리케이션 UI(공개·인증·비공개/내부용·설치형 PWA/확장을 포함한 브라우저 호스팅 웹, 사용자 여정이 있는 인터랙티브 배포형 콘텐츠 중심 웹(캠페인·출판·랜딩 경험 등), 데스크톱, 모바일/태블릿, 임베디드/하이브리드 클라이언트, 커스텀 렌더링 UI, Qt, 혼합 타깃) 작업에 Codex의 `$superloopy:superloopy-frontend`나 Claude Code의 `/superloopy:superloopy-frontend`를 직접 호출하거나, 해당 작업을 선행 `loopy`/`루피`로 시작할 때만. 단순한 UI·플랫폼·프레임워크 용어로는 켜지지 않으며 TV·웨어러블·XR·자동차·게임 UI·TUI·정적 미디어/문서 결과물·비 UI 작업은 제외됩니다. | 하나의 공통 UX 계약에 플랫폼/컴포지션 경로를 더합니다. 근거는 변경한 주장에 비례하며 브라우저, 네이티브 타깃/셸, 렌더러, 혼합 타깃의 각 소유자를 독립적으로 검증합니다. 독립 실행에서는 실행별 영수증을 보존하고 활성 루프에서는 goal과 criterion에 연결합니다. | | `humanize-korean` | 한국어 글의 AI 티를 줄이거나 번역투를 고치고, 사실은 바꾸지 않은 채 사람이 쓴 것처럼 다듬어야 할 때. | `final.md`, `summary.md`, `audit.json`을 쓰고, Superloopy loop 안에서는 `.superloopy/evidence/humanize-korean/` 아래에 근거를 남깁니다. | | `superloopy-slides` | 슬라이드·프레젠테이션·덱을 요청하거나 PPT/PPTX를 웹으로 변환할 때. | 고정 16:9 스테이지의 의존성 없는 단일 HTML 덱, 직접 고르는 스타일 미리보기 3종, `.superloopy/evidence/slides/` 아래 렌더링 스크린샷 visual-QA artifact. | @@ -68,7 +68,7 @@ Superloopy는 명령을 작게 둡니다. 대신 스킬이 작업 방식을 잡 ## Qt 칸반 데모 -[Northstar Qt 칸반 데모](examples/qt-kanban/)는 `superloopy-frontend` Qt 경로로 만든 실행 가능한 Qt Quick 검증 예제입니다. Qt 6.11.1, CMake, Ninja가 준비된 상태에서 저장소 루트에서 설정, 빌드, 실행하세요. +[Northstar Qt 칸반 데모](examples/qt-kanban/)는 `superloopy-frontend` Qt 경로로 만든 실행 가능한 Qt Quick 프로토타입 인수 검증 픽스처이며, 프로덕션 편집기임을 증명하지 않습니다. Board·검색·필터·카드·드로어·작업 생성은 검증하지만 Timeline과 Inbox는 눈에 보이는 패시브 데모 전용 문맥이고 Settings와 Help는 없습니다. 영속성과 Undo는 범위 밖입니다. Qt 6.11.1, CMake, Ninja가 준비된 상태에서 저장소 루트에서 설정, 빌드, 실행하세요. ```bash qt-cmake -S examples/qt-kanban -B build/qt-kanban -G Ninja -DCMAKE_BUILD_TYPE=Release diff --git a/README.md b/README.md index 76cffd4..262d032 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Superloopy keeps the command layer small. Skills carry the specialist workflow: | `superloopy-doctor` | You diagnose install, wrapper, plugin cache, hook/bootstrap, agent, Codex/Claude Code host wiring, or stale-version problems. | A read-only health report with wrapper/cache/version evidence, failing checks, and the exact repair command to run only if approved. | | `superloopy-research` | You ask for `loopy research`, deep research, exhaustive investigation, or a cited report. | Research axes, expansion waves, a claim ledger, verification notes, and a cited synthesis artifact. | | `superloopy-clone` | You ask for `loopy clone`, authorized website cloning, rebuilding, migration, or pixel-focused page recovery. | Browser captures, page topology, design tokens, asset inventory, implementation notes, build output, and visual QA evidence. | -| `superloopy-frontend` | You explicitly invoke Codex `$superloopy:superloopy-frontend` or Claude Code `/superloopy:superloopy-frontend` for a web frontend or Qt desktop GUI task, or start one with a leading `loopy`/`루피`. Non-web/non-Qt visual work and plain UI mentions do not activate it. | A DESIGN.md token contract; for web, the anti-slop pre-flight and browser evidence; for Qt, the Qt pre-flight and native rendered-application evidence. | +| `superloopy-frontend` | You explicitly invoke Codex `$superloopy:superloopy-frontend` or Claude Code `/superloopy:superloopy-frontend` for supported screen-based application UI across browser-hosted Web (public, authenticated, private/internal, installed PWA, or extension), interactive deployed content-led Web (campaign, publication, or landing experiences with a user journey), desktop, mobile/tablet, embedded/hybrid clients, custom-rendered UI, Qt, or mixed targets, or start that work with a leading `loopy`/`루피`. Plain UI, platform, or framework terms do not activate it; TV, wearable, XR, automotive, game UI, TUI, static media/document artifacts, and non-UI work stay excluded. | One shared UX contract plus platform/composition routes. Evidence is proportional to changed claims and independently covers each browser, native target/shell, renderer, and mixed target. Standalone runs retain run-scoped receipts; active loops bind them to the goal and criterion. | | `humanize-korean` | Use when Korean users ask to remove AI tone, fix 번역투, or make Korean text sound human without changing facts. | Writes `final.md`, `summary.md`, and `audit.json`; in Superloopy loops it records evidence under `.superloopy/evidence/humanize-korean/`. | | `superloopy-slides` | You ask for slides, a presentation, a deck, or a PPT/PPTX-to-web conversion. | A zero-dependency single-file HTML deck on a fixed 16:9 stage, three style previews to pick from, and a rendered-screenshot visual-QA artifact under `.superloopy/evidence/slides/`. | @@ -68,7 +68,7 @@ The loop skill is the default guardrail. A complete leading `loopy` token starts ## Qt Kanban Demo -The [Northstar Qt Kanban demo](examples/qt-kanban/) is a runnable Qt Quick acceptance fixture built through the `superloopy-frontend` Qt route. With Qt 6.11.1, CMake, and Ninja available, configure, build, and launch it from the repository root: +The [Northstar Qt Kanban demo](examples/qt-kanban/) is a runnable Qt Quick prototype acceptance fixture built through the `superloopy-frontend` Qt route, not production-editor proof. Board, search, filters, cards, drawers, and task creation are exercised; Timeline and Inbox are visibly passive demo-only context, Settings and Help are absent, and persistence and Undo remain out of scope. With Qt 6.11.1, CMake, and Ninja available, configure, build, and launch it from the repository root: ```bash qt-cmake -S examples/qt-kanban -B build/qt-kanban -G Ninja -DCMAKE_BUILD_TYPE=Release diff --git a/README.zh-CN.md b/README.zh-CN.md index 4c8561e..b2ec151 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -48,7 +48,7 @@ Superloopy 保持命令层很小。具体工作方式由 skills 负责:什么 | `superloopy-doctor` | 诊断安装、wrapper、plugin cache、hook/bootstrap、agent、Codex/Claude Code host wiring 或版本过旧问题时。 | 只读 health report:wrapper/cache/version 证据、失败检查,以及只有批准后才运行的精确修复命令。 | | `superloopy-research` | 请求 `loopy research`、deep research、exhaustive investigation,或需要带引用的研究报告时。 | 研究轴、扩展 wave、claim ledger、验证笔记、带引用的 synthesis artifact。 | | `superloopy-clone` | 请求 `loopy clone`、已授权的网站克隆、重建、迁移,或需要按像素恢复页面时。 | 浏览器截图、页面结构、设计 token、资产清单、实现笔记、build 输出、visual QA 证据。 | -| `superloopy-frontend` | 为 Web 前端或 Qt 桌面 GUI 任务在 Codex 中显式调用 `$superloopy:superloopy-frontend`,或在 Claude Code 中调用 `/superloopy:superloopy-frontend`,也可用开头的 `loopy`/`루피` 启动此类任务。非 Web/Qt 可视化工作以及仅提到 UI 都不会激活它。 | 保留 DESIGN.md token 契约;Web 使用 anti-slop 预检和浏览器证据,Qt 使用 Qt 预检和原生应用渲染证据。 | +| `superloopy-frontend` | 仅在处理受支持的基于屏幕的应用 UI(浏览器托管 Web,包括公开、需认证、私有/内部、已安装 PWA 或扩展;具有用户旅程的已部署交互式内容型 Web,例如营销活动、出版物或着陆页体验;以及桌面、移动设备/平板、嵌入式/混合客户端、自定义渲染 UI、Qt 或混合目标)时,在 Codex 中显式调用 `$superloopy:superloopy-frontend`,或在 Claude Code 中调用 `/superloopy:superloopy-frontend`,也可用开头的 `loopy`/`루피` 启动该工作。仅出现 UI、平台或框架词汇不会激活它;TV、可穿戴设备、XR、汽车、游戏 UI、TUI、静态媒体/文档产物和非 UI 工作仍被排除。 | 采用一份共享 UX 契约,并叠加平台/界面构成路径。证据与变更声明成比例,并分别验证浏览器、原生目标/外壳、渲染器和每个混合目标。独立运行保留按次划分的证据;活动循环则把证据绑定到 goal 和 criterion。 | | `humanize-korean` | 需要去掉韩文内容里的 AI 腔、修正翻译腔,或在不改事实的前提下让韩文更像真人写作时。 | 写入 `final.md`、`summary.md`、`audit.json`;在 Superloopy loop 中把证据记录到 `.superloopy/evidence/humanize-korean/`。 | | `superloopy-slides` | 需要幻灯片、演示文稿、deck,或把 PPT/PPTX 转成网页时。 | 固定 16:9 舞台的零依赖单文件 HTML deck、可挑选的三种样式预览,以及 `.superloopy/evidence/slides/` 下的渲染截图 visual-QA 证据产物。 | diff --git a/docs/superloopy-design-audit.md b/docs/superloopy-design-audit.md index f4c8d85..fc10967 100644 --- a/docs/superloopy-design-audit.md +++ b/docs/superloopy-design-audit.md @@ -6,14 +6,15 @@ This doctor-verified audit records Superloopy's own design decisions. It exists | Decision | Reason | Effect | Guard | | --- | --- | --- | --- | -| `gate-shape-compatibility` | Existing users may have strict review or matrix gate JSON. | Superloopy keeps both shapes accepted through local validators. | `test/golden-review-gate.test.js`, `test/golden-matrix-gate.test.js`, and `src/artifacts.js`. | +| `gate-shape-compatibility` | Existing users may have strict review or matrix gate JSON, while broad UI work needs proof proportional to a concrete target, affected owner, and changed claim. | Superloopy keeps both gate shapes and their exact unscoped legacy surface literals accepted through local validators. New scoped rows provide all four of `target`, `owner`, `claims`, and `scopeReason`; `target` is a singular portable `{id, platform, environment}` object whose slug rejects aggregate vague tokens. Owner/claim floors select compatible proof, composite surfaces retain attributable owners, and each scoped surface target-ID/platform/owner slice is singular with resolved artifacts independent from other surface slices. Adversarial and matrix coverage rows reference artifacts or contract-bound proof rows without declaring another slice. Matrix coverage binds a surface or adversarial row with the same `contractRef`; HTTP/data artifacts remain confined to their families. | `src/review-gate.js`, `src/matrix-gate.js`, `docs/superloopy-gate-notes.md`, `test/golden-review-gate.test.js`, `test/golden-matrix-gate.test.js`, and `src/artifacts.js`. | | `actor-field-policy` | Exact external role literals made compatibility too specific. | Actor fields now require non-empty identity text instead of a hard-coded role name. | `src/review-gate.js` and review-gate golden tests. | | `native-naming` | Public code, docs, and tests should describe Superloopy behavior directly. | Modules, tests, docs, CLI flags, and metadata use Superloopy-owned terms. | `test/docs.test.js` and `docs/superloopy-gate-notes.md`. | | `recorded-thresholds` | Later analysis should compare against earlier judgments. | `docs/superloopy-loop-golden-set.md` stores score history and command evidence in one tracked place. | `docs/superloopy-loop-golden-set.md` and `test/docs.test.js`. | | `model-policy` | Crew lanes need explicit cost/depth pins, legacy upgrades must not overwrite user edits, and hosts may not attest the model they actually launch. | `model-policy.json` defines ordered complete model/effort/tier tuples. Managed installation resolves them before launch, adopts only complete hash-known legacy fleets, detects wrapper/plugin split-brain, and reports `model_unverified` unless the host attests role and model. Availability failure never causes a post-launch retry or model switch. | `legacy-agent-manifests.json`, `src/legacy-agents.js`, `src/model-resolution.js`, `src/managed-agents.js`, `src/installed-model-policy.js`, `src/wrapper-check.js`, `test/model-install.test.js`, `test/installed-model-policy.test.js`, and `test/wrapper-check.test.js`. | | `crew-lines` | Crew handoffs should feel alive without weakening evidence discipline. | Known crew lanes can emit original terminal-state lines in presentation output only; persisted state and gate authority stay mechanical. | `docs/superloopy-crew-lines.md`, `src/crew-lines.js`, `test/crew-lines.test.js`, and `test/fleet.test.js`. | -| `frontend-quality-skill` | UI work needs a repeatable anti-AI-slop discipline without letting a lexical classifier seize backend or mixed tasks. | A bundled, explicitly invoked `superloopy-frontend` skill routes active visual work across web, Qt Widgets, Qt Quick/QML, and mixed surfaces while keeping one DESIGN.md token contract. Completion requires browser evidence for web or native rendered-application evidence for Qt. The core Web/Qt router text remains original prose authored for Superloopy; adapted optional web context references retain their upstream MIT notice. Qt claims link to official Qt documentation, and no external runtime dependencies are vendored. The prompt hook still recognizes exact Superloopy invocation tokens and structured steering instead of frontend vocabulary. | `skills/superloopy-frontend/SKILL.md`, `skills/superloopy-frontend/references/web.md`, `skills/superloopy-frontend/references/qt.md`, `skills/superloopy-frontend/references/qt-widgets.md`, `skills/superloopy-frontend/references/qt-quick.md`, `skills/superloopy-frontend/references/qt-qa.md`, `test/frontend-qt-contract.test.js`, `test/plugin.test.js`, `test/engineer.test.js`, `test/hooks.test.js`, and the file-audit/golden-set inventories. | -| `frontend-context-routing` | Existing web guidance had no bounded lane for verified platform design-system contracts, implementation-level scroll motion, or redesigns that must preserve a living product. | The web route lazily loads three optional references. System adoption requires an actual contract, current official-doc verification, and dependency approval; motion templates preserve static reduced-motion layouts and require interactive browser proof; redesigns record unavailable facts as unverified and protect identity, accessibility, SEO, and analytics contracts. Selected mechanisms are adapted from Taste Skill under MIT with the full notice packaged locally. | `skills/superloopy-frontend/references/web.md`, `skills/superloopy-frontend/references/system-map.md`, `skills/superloopy-frontend/references/motion.md`, `skills/superloopy-frontend/references/redesign.md`, `skills/superloopy-frontend/references/upstream-notice.md`, and `test/plugin.test.js`. | +| `frontend-quality-skill` | Screen-based application UI needs functional UX truth across targets without letting a lexical classifier seize unrelated work. | A bundled, explicitly invoked `superloopy-frontend` skill loads one shared UX contract once, then selects and unions exact platform and composition routes across browser-hosted public/authenticated/private Web, interactive deployed content-led Web, installed PWA/extensions, desktop, mobile/tablet, embedded/hybrid, custom-rendered, Qt desktop/mobile, Qt Widgets, Qt Quick, and mixed Qt WebAssembly, and mixed surfaces; static media and document artifacts stay excluded. Every Qt WebAssembly composition retains browser, renderer, the applicable Qt specialization, and Qt QA ownership without inheriting native desktop proof. The shared contract starts from current-state and affected-user evidence, separates capability availability from enabled/visible/invocable/feedback state, requires capability reachability or an intentional context-preserving handoff across advertised frontends, covers async/auth/offline/conflict and recovery paths, and scales design, visual, accessibility, package, and real-target artifacts to the changed claims. It keeps resource identity attributable, makes reset provenance owner-backed, gives information hierarchy a distinct-purpose test, and chooses proportional disclosure without hiding task-critical state or invalidating task-required dense monitoring. Mixed targets require independent renderer, shell, package, and target evidence; standalone runs keep run-scoped receipts while active loops bind evidence to a goal and criterion. Native, hybrid, and custom-rendered claims cannot pass on screenshots alone. SEO applies when the current Web target is crawlable and public; a native or embedded current target evaluates only a distinct public Web deployment. The project's current design source remains authoritative and optional visual references cannot replace it. Qt specialization is preserved for desktop and mobile/tablet, with WebAssembly added through official Qt documentation and route-native proof. The compatibility scanner is a partial color/spacing token lint, not proof of typography, semantics, accessibility, or component adoption. A dependency-free helper creates portable run roots and rejects path escape, symlink, duplicate-root, and empty-artifact evidence. The shared UX and platform/composition contracts are original prose authored for Superloopy; adapted optional Web references retain their upstream MIT notice, and no external runtime dependencies are vendored. No external router or database is bundled. The prompt hook still recognizes exact Superloopy invocation tokens and structured steering instead of UI, platform, or framework vocabulary. | `skills/superloopy-frontend/SKILL.md`, `skills/superloopy-frontend/references/ux.md`, `skills/superloopy-frontend/references/desktop.md`, `skills/superloopy-frontend/references/mobile.md`, `skills/superloopy-frontend/references/hybrid.md`, `skills/superloopy-frontend/references/renderer.md`, `skills/superloopy-frontend/references/web.md`, `skills/superloopy-frontend/references/qt.md`, `skills/superloopy-frontend/references/qt-widgets.md`, `skills/superloopy-frontend/references/qt-quick.md`, `skills/superloopy-frontend/references/qt-qa.md`, `skills/superloopy-frontend/scripts/evidence-root.mjs`, `test/frontend-routing-scenarios.test.js`, `test/frontend-ux-contract.test.js`, `test/frontend-ux-semantics.test.js`, `test/frontend-qt-contract.test.js`, `test/plugin.test.js`, `test/engineer.test.js`, `test/hooks.test.js`, and the file-audit/golden-set inventories. | +| `frontend-quality-overlays` | StyleGallery is unrelated to the user-reported UX fixes already owned by the shared frontend base, but its architecture research exposed independent quality gaps in spatial ownership, temporal review, visual-reference authority, high-impact rationale, and bounded debt. | Superloopy adds a StyleGallery-informed but independently authored quality overlay: target routing remains unchanged; `layout.md`, `motion-core.md`, and visual-reference discipline activate only from changed claims; rationale, task-based usability evidence, and accepted debt scale by consequence; Web implementation guidance remains a specialization; and campaign heuristics cannot become unsupported universal blockers. Spatial disclosure uses target-derived exposure modes with generic source/summary and disclosed/destination state proof; collapsed/expanded proof applies only when that mechanism is selected. The overlay rejects fixed Web breakpoint matrices, CSS pattern catalogs, mandatory image generation, and universal motion values as cross-platform defaults. It does not add motion or haptic artifact kinds because file existence is not proof of temporal behavior or physical output; a future performance claim should wait for a machine-validated structured report tied to raw-trace hash, target, build, metric, and threshold. The reviewed StyleGallery revision is pinned only as research provenance because no root license was identified, so no code, prose, schema, pattern, generated file, or dependency is copied. | `skills/superloopy-frontend/SKILL.md`, `skills/superloopy-frontend/references/layout.md`, `skills/superloopy-frontend/references/motion-core.md`, `skills/superloopy-frontend/references/motion.md`, `skills/superloopy-frontend/references/image-first.md`, `skills/superloopy-frontend/references/anti-slop.md`, `skills/superloopy-frontend/references/ux.md`, `skills/superloopy-frontend/references/upstream-notice.md`, `test/frontend-quality-overlays.test.js`, and the file-audit/golden-set inventories. | +| `frontend-context-routing` | Existing Web guidance had no bounded lane for verified platform design-system contracts, implementation-level scroll motion, or redesigns that must preserve a living product. | The Web route classifies product/application DOM, marketing/editorial, PWA/extension, custom-rendered, and embedded-client delivery before loading optional references or selecting evidence. SEO is conditional on the current Web target being public and crawlable; native or embedded work scores only a distinct public Web deployment. Design-system and brand references remain subordinate to the repository's authoritative source and express color, type, spacing, component states, and motion in project-native tokens, units, and APIs rather than imposing CSS/hex/4px conventions on every target; image-first and motion routes activate only for affected visual/motion claims and derive proof from the target. System adoption requires an actual contract, current official-doc/package verification, and dependency approval; approved implementation executes one verified package-manager command and validates integration, while advice only presents it. Redesigns mark unavailable facts unverified, preserve IA by default, and change broken IA only when separately in scope and explicitly approved. Selected mechanisms are adapted from Taste Skill under MIT with the full notice packaged locally. | `skills/superloopy-frontend/references/design-system.md`, `skills/superloopy-frontend/references/design/_INDEX.md`, `skills/superloopy-frontend/references/image-first.md`, `skills/superloopy-frontend/references/web.md`, `skills/superloopy-frontend/references/system-map.md`, `skills/superloopy-frontend/references/motion.md`, `skills/superloopy-frontend/references/redesign.md`, `skills/superloopy-frontend/references/upstream-notice.md`, and `test/plugin.test.js`. | | `korean-humanizer-skill` | Korean users need a Codex-native AI-tone-removal workflow with stronger proof than a prompt-only port. | Superloopy ships a local `humanize-korean` skill with packaged references, attribution, deterministic audit metrics, and evidence artifacts. | `skills/humanize-korean/SKILL.md`, `skills/humanize-korean/scripts/audit-humanize-output.mjs`, `test/humanize-korean.test.js`, `test/plugin.test.js`, and the file-audit/golden-set inventories. | | `slides-skill` | Presentation work needs the same show-don't-tell style discovery and rendered-screenshot proof as other visual lanes, and rebuilding a mature template library from scratch would be weaker than adapting one. | A bundled `superloopy-slides` skill generates zero-dependency single-file HTML decks on a fixed 16:9 stage: three rendered style previews the user picks from, a vendored MIT `frontend-slides` template pack (audited by provenance and exempt from the line cap, like the landing-page orbit bundles), and completion gated on rendered-screenshot visual QA under `.superloopy/evidence/slides/`. | `skills/superloopy-slides/SKILL.md`, `skills/superloopy-slides/LICENSE`, `test/plugin.test.js`, `test/doctor.test.js`, and the file-audit/golden-set inventories. | | `dual-host-support` | Superloopy's core is host-agnostic and users want it on Claude Code as well as Codex. | A parallel `.claude-plugin/` manifest + marketplace, `hooks/hooks.json`, `agents/*.md`, and a Claude model-policy doc let one repo install on both hosts; a shared `src/receipt.js` recovers the SubagentStop receipt from last_assistant_message or the subagent transcript tail, and `isClaudeHost` makes bootstrap a no-op on Claude — the deterministic floor stays host-agnostic. | `.claude-plugin/plugin.json`, `src/receipt.js`, `src/agents.js`, `test/host-adapter.test.js`, and the file-audit/golden-set inventories. | diff --git a/docs/superloopy-file-audit.md b/docs/superloopy-file-audit.md index b80dc6b..96cd66f 100644 --- a/docs/superloopy-file-audit.md +++ b/docs/superloopy-file-audit.md @@ -51,26 +51,26 @@ Superloopy is its own lightweight loop harness: one small CLI, repo-local `.supe | `docs/superloopy-crew-lines.md` | Crew-line precedent, policy, and runtime contract for presentation-only handoff flavor. | Keeps lines Superloopy-original and non-authoritative beside mechanical evidence state. | | `docs/superloopy-design-audit.md` | Doctor-verified decision matrix for naming, compatibility, model resolution, and threshold records. | Records Superloopy decisions with concrete source/test guards, not source-project lineage. | | `docs/superloopy-file-audit.md` | File-by-file audit and reviewability note. | Proves every Git-visible file has a role and boundary. | -| `docs/superloopy-gate-notes.md` | Gate compatibility notes and golden scenario list. | Names review and matrix gates as Superloopy contracts. | +| `docs/superloopy-gate-notes.md` | Executable review/matrix gate contract with structured target metadata, accepted artifact kinds, owner/claim proof minimums, resolved-path attribution, exact legacy exceptions, a copyable scoped example, and golden scenarios. | Names Superloopy gate behavior directly and distinguishes new scoped target-owner evidence from preserved unscoped literal compatibility. | | `docs/superloopy-host-contract.md` | The host-runtime contract Superloopy rides: installable agents, the SubagentStop payload fields, and the host behaviors Superloopy cannot verify. | Superloopy-native; states advisory limits and the deterministic-floor backstop. | | `docs/superloopy-interop-superpowers.md` | Coexistence note: how Superloopy divides labor with the Superpowers plugin, how detection works, and the doctor visibility check. | Superloopy-native operator doc; advisory guidance only, no runtime logic or vendored code. | | `docs/superloopy-loop-golden-set.md` | Long-running golden set, threshold model, file evidence inventory, and run history. | Scores Superloopy on its own behavior. | | `docs/superloopy-model-policy-claude.md` | Advisory Claude model-alias policy for the bundled subagents. | Superloopy-native; treats model choice as steering, not proof. | | `docs/superloopy-model-policy.md` | Ordered complete Codex profile tuples, preferred pins, and explicit GPT-5.5 compatibility policy for bundled agents. | Keeps routing pre-launch and explicit while treating model choice as steering, not proof. | | `examples/qt-kanban/CMakeLists.txt` | Original Superloopy demo build entry for the Northstar Qt Quick application and tests. | Uses the declared Qt 6.11.1, CMake, and C++17 contracts; no vendored build logic. | -| `examples/qt-kanban/DESIGN.md` | Original Superloopy design contract for the Northstar Kanban tokens, responsive shell, components, and states. | Original prose and token values; no external design-system content. | +| `examples/qt-kanban/DESIGN.md` | Original Superloopy design contract for the Northstar Kanban tokens, responsive shell, components, states, and prototype acceptance boundary. | Original prose and token values; explicitly excludes production-editor, persistence, and Undo proof. | | `examples/qt-kanban/scripts/audit-macos-accessibility.sh` | Original Superloopy macOS helper for native accessibility acceptance. | Launches the real app and uses public macOS AX APIs to verify role, name, focus, and press behavior without adding a dependency. | | `examples/qt-kanban/scripts/capture-macos.sh` | Original Superloopy macOS helper for deterministic native-window capture. | Uses platform-provided shell, AppleScript, and `screencapture`; adds no capture dependency. | | `examples/qt-kanban/src/Northstar/Kanban/BoardView.qml` | Original Superloopy demo code for the scrollable four-column board and drop surfaces. | Uses public Qt Quick APIs; no copied QML component code. | -| `examples/qt-kanban/src/Northstar/Kanban/CMakeLists.txt` | Original Superloopy demo build code for the reusable `Northstar.Kanban` QML module. | Declares local QML and SVG resources through Qt CMake APIs only. | +| `examples/qt-kanban/src/Northstar/Kanban/CMakeLists.txt` | Original Superloopy demo build code for the reusable `Northstar.Kanban` QML module. | Declares only used local QML and SVG resources through Qt CMake APIs. | | `examples/qt-kanban/src/Northstar/Kanban/DetailDrawer.qml` | Original Superloopy demo code for task details, movement actions, and contained focus. | Uses public Qt Quick Controls and accessibility APIs; no external component library. | | `examples/qt-kanban/src/Northstar/Kanban/DragTaskVisual.qml` | Original Superloopy demo code for the task drag preview and move action. | Uses Qt Quick drag APIs with local theme tokens only. | | `examples/qt-kanban/src/Northstar/Kanban/KanbanColumn.qml` | Original Superloopy demo code for a stage column, task stack, and drop target. | Uses public Qt Quick APIs and the local deterministic store. | | `examples/qt-kanban/src/Northstar/Kanban/KanbanHeader.qml` | Original Superloopy demo code for board title, search, priority filter, and new-task action. | Uses Qt Quick Controls with local assets and tokens only. | -| `examples/qt-kanban/src/Northstar/Kanban/KanbanView.qml` | Original Superloopy demo code for the responsive sidebar, board, drawer, dialog, and focus flow. | Uses public Qt Quick layout and popup APIs; no web or external UI runtime. | +| `examples/qt-kanban/src/Northstar/Kanban/KanbanView.qml` | Original Superloopy demo code for the responsive sidebar, board, drawer, dialog, and focus flow. | Uses public Qt Quick layout and popup APIs for an idempotent Board return-to-overview result without fake unavailable-route toast behavior. | | `examples/qt-kanban/src/Northstar/Kanban/Main.qml` | Original Superloopy demo code for the Northstar application window. | Qt Quick application root only; keeps minimum and initial sizes explicit. | | `examples/qt-kanban/src/Northstar/Kanban/NewTaskDialog.qml` | Original Superloopy demo code for validated task creation and focus restoration. | Uses Qt Quick Controls and the local in-memory store only. | -| `examples/qt-kanban/src/Northstar/Kanban/Sidebar.qml` | Original Superloopy demo code for full and compact native navigation. | Uses local SVGs and Qt Quick Controls; no external navigation component. | +| `examples/qt-kanban/src/Northstar/Kanban/Sidebar.qml` | Original Superloopy demo code for functional Board navigation plus passive demo-only Timeline/Inbox context in full and compact layouts. | Emits the real Board return-to-overview action; Timeline/Inbox remain named static text with no pointer, keyboard, or accessibility press action, and Settings/Help are absent. | | `examples/qt-kanban/src/Northstar/Kanban/TaskCard.qml` | Original Superloopy demo code for task metadata, selection, keyboard movement, and pointer drag. | Uses public Qt Quick input and accessibility APIs with local tokens, including motion-aware drag feedback and cancellation. | | `examples/qt-kanban/src/Northstar/Kanban/TaskStore.qml` | Original Superloopy demo code for deterministic in-memory task data and mutations. | No persistence, network, generated fixtures, or external data. | | `examples/qt-kanban/src/Northstar/Kanban/Theme.qml` | Original Superloopy demo code for color, type, spacing, breakpoint, and motion tokens. | Values come from the local design contract and effective platform font. | @@ -78,20 +78,18 @@ Superloopy is its own lightweight loop harness: one small CLI, repo-local `.supe | `examples/qt-kanban/src/Northstar/Kanban/assets/icons/board.svg` | Original local SVG asset for board navigation. | Drawn for the Superloopy demo as a monochrome icon; no external artwork. | | `examples/qt-kanban/src/Northstar/Kanban/assets/icons/close.svg` | Original local SVG asset for close actions. | Drawn for the Superloopy demo as a monochrome icon; no external artwork. | | `examples/qt-kanban/src/Northstar/Kanban/assets/icons/filter.svg` | Original local SVG asset for priority filtering. | Drawn for the Superloopy demo as a monochrome icon; no external artwork. | -| `examples/qt-kanban/src/Northstar/Kanban/assets/icons/help.svg` | Original local SVG asset for help navigation. | Drawn for the Superloopy demo as a monochrome icon; no external artwork. | -| `examples/qt-kanban/src/Northstar/Kanban/assets/icons/inbox.svg` | Original local SVG asset for inbox navigation. | Drawn for the Superloopy demo as a monochrome icon; no external artwork. | +| `examples/qt-kanban/src/Northstar/Kanban/assets/icons/inbox.svg` | Original local SVG passive demo-context asset for Inbox. | Drawn for the Superloopy demo as a monochrome icon; no external artwork. | | `examples/qt-kanban/src/Northstar/Kanban/assets/icons/search.svg` | Original local SVG asset for search controls. | Drawn for the Superloopy demo as a monochrome icon; no external artwork. | -| `examples/qt-kanban/src/Northstar/Kanban/assets/icons/settings.svg` | Original local SVG asset for settings navigation. | Drawn for the Superloopy demo as a monochrome icon; no external artwork. | -| `examples/qt-kanban/src/Northstar/Kanban/assets/icons/timeline.svg` | Original local SVG asset for timeline navigation. | Drawn for the Superloopy demo as a monochrome icon; no external artwork. | +| `examples/qt-kanban/src/Northstar/Kanban/assets/icons/timeline.svg` | Original local SVG passive demo-context asset for Timeline. | Drawn for the Superloopy demo as a monochrome icon; no external artwork. | | `examples/qt-kanban/src/app/CMakeLists.txt` | Original Superloopy demo build code for the native executable and launch-option CTests. | Links the local QML module and public Qt targets only. | | `examples/qt-kanban/src/app/main.cpp` | Original Superloopy demo code for Basic-style startup, size validation, QML loading, and readiness logging. | Uses public Qt C++ APIs and no third-party runtime. | | `examples/qt-kanban/tests/quick/CMakeLists.txt` | Original Superloopy demo build code for the Qt Quick Test executable and QML cases. | Uses Qt Quick Test and the local QML module only. | -| `examples/qt-kanban/tests/quick/tst_accessibility.qml` | Original Superloopy demo test code for accessible roles, names, actions, focus, fonts, RTL, and motion. | Exercises the real QML controls and injected Qt accessibility probe. | +| `examples/qt-kanban/tests/quick/tst_accessibility.qml` | Original Superloopy demo test code for accessible roles, names, actions, focus, fonts, RTL, motion, and passive demo semantics. | Proves Timeline/Inbox expose useful static text without a button role, press action, or false success announcement. | | `examples/qt-kanban/tests/quick/tst_card.qml` | Original Superloopy demo test code for card priorities, metadata, selection, keyboard, and drag state. | Exercises the real local `TaskCard` component with Qt Quick Test. | | `examples/qt-kanban/tests/quick/tst_drag.qml` | Original Superloopy demo test code for pointer-driven cross-column task movement. | Exercises real Qt Quick drag/drop, genuine stolen-grab cancellation, and the deterministic store. | -| `examples/qt-kanban/tests/quick/tst_interactions.qml` | Original Superloopy demo test code for search, filtering, drawers, task creation, focus, and keyboard flow. | Exercises the real local view and controls through Qt Quick Test. | +| `examples/qt-kanban/tests/quick/tst_interactions.qml` | Original Superloopy demo test code for search, filtering, drawers, task creation, Board return-to-overview behavior, focus, and keyboard flow. | Proves Board clears task/detail state, closes overlay detail, remains current, restores focus, and still tabs directly to search without dead sidebar destinations. | | `examples/qt-kanban/tests/quick/tst_qtkanban.cpp` | Original Superloopy demo code for the Qt Quick Test harness and real accessibility adapter. | Uses public Qt test, QML, style, and accessibility APIs only. | -| `examples/qt-kanban/tests/quick/tst_responsive.qml` | Original Superloopy demo test code for sidebar and drawer breakpoints, board width, RTL, and large fonts. | Exercises real local layout behavior through Qt Quick Test. | +| `examples/qt-kanban/tests/quick/tst_responsive.qml` | Original Superloopy demo test code for sidebar and drawer breakpoints, passive demo layout, board width, RTL, and large fonts. | Proves Timeline/Inbox retain labeled layout space without focus and Settings/Help remain absent. | | `examples/qt-kanban/tests/quick/tst_smoke.qml` | Original Superloopy demo test code for QML-module import and root creation. | Minimal Qt Quick Test smoke coverage of the reusable module. | | `examples/qt-kanban/tests/quick/tst_store.qml` | Original Superloopy demo test code for deterministic seed data, filtering, moves, and task creation. | Exercises the real local in-memory singleton without mocks. | | `examples/qt-kanban/tests/quick/tst_theme.qml` | Original Superloopy demo test code for design tokens, typography, geometry, breakpoints, and motion state. | Verifies the local design contract through the real theme singleton. | @@ -215,11 +213,11 @@ Superloopy is its own lightweight loop harness: one small CLI, repo-local `.supe | `skills/superloopy-clone/agents/openai.yaml` | Minimal agent metadata for discovering the Superloopy clone skill. | Superloopy-native skill metadata only. | | `skills/superloopy-doctor/SKILL.md` | Skill instructions for read-only Superloopy install health diagnostics across wrappers, plugin caches, agents, hook bootstrap, and host wiring. | Superloopy-native diagnostic workflow only; repair commands require explicit user approval. | | `skills/superloopy-doctor/agents/openai.yaml` | Minimal agent metadata for discovering the Superloopy doctor skill. | Superloopy-native skill metadata only. | -| `skills/superloopy-frontend/SKILL.md` | Explicitly invoked UI/visual router with a DESIGN.md token gate, browser evidence for web, and native rendered-application evidence for Qt. | Superloopy-native frontend workflow; concepts independently authored, no external design code or files vendored. | -| `skills/superloopy-frontend/agents/openai.yaml` | Minimal agent metadata for discovering the routed Superloopy web/Qt frontend skill. | Superloopy-native skill metadata only. | -| `skills/superloopy-frontend/references/anti-slop.md` | Named-default bans, countable rules, consistency locks, real-asset mandate, and the pre-flight checklist for non-AI-looking UI. | Superloopy-native rule set; original prose, no external code or design files vendored. | -| `skills/superloopy-frontend/references/design-system.md` | The 7-section DESIGN.md token schema and loopy-native token-authoring guidance with illustrative example sets. | Superloopy-native schema; original token examples, no third-party design files vendored. | -| `skills/superloopy-frontend/references/design/_INDEX.md` | Brand design-token library index routing a named brand or mood to one loopy-native teardown. | Superloopy-native; original prose, public brand facts only, no external design files vendored. | +| `skills/superloopy-frontend/SKILL.md` | Explicitly invoked cross-platform application-UI router with shared UX, exact target routes, independent claim-triggered layout/motion/visual overlays, information hierarchy to spatial disclosure routing, proportional artifacts, and owner-specific evidence for browser, desktop, mobile/tablet, hybrid, renderer, Qt desktop/mobile, and Qt Widgets, Qt Quick, and mixed Qt WebAssembly surfaces. | Superloopy-native frontend workflow; target classification stays separate from conditional quality overlays, every Qt WebAssembly composition retains browser+renderer+applicable Qt proof without borrowing native desktop ownership, concepts are independently authored, and no external design code or files are vendored. | +| `skills/superloopy-frontend/agents/openai.yaml` | Minimal explicit-only agent metadata for discovering the cross-platform Superloopy frontend workflow without implicit activation. | Superloopy-native skill metadata only. | +| `skills/superloopy-frontend/references/anti-slop.md` | Conditional visual-direction check for marketing, editorial, campaign, greenfield presentation, or an explicitly new visual direction; prevents unintended root/page horizontal overflow while preserving intentional task-owned horizontal regions under the `layout.md` ownership contract. | Superloopy-native rule set with scoped named-default bans, evidence-bounded countable review flags rather than unsupported universal blockers, consistency locks, real-asset truth, and copy checks; original prose, no external code or design files vendored. | +| `skills/superloopy-frontend/references/design-system.md` | Proportional seven-section visual-contract schema that preserves the repository's authoritative design source, uses DESIGN.md only as that source or a synchronized scoped mapping/receipt, and records project-native tokens, units, states, and APIs. | Superloopy-native schema; narrow nonvisual changes do not acquire design ceremony, native runtime values remain authoritative, Web token examples are illustrative rather than cross-platform defaults, and no third-party design files are vendored. | +| `skills/superloopy-frontend/references/design/_INDEX.md` | Brand/mood index routing to one scoped reference teardown while keeping the project's current design source authoritative. | Superloopy-native original prose and public brand facts only; an approved direction is applied in the authoritative source and DESIGN.md is only established authority or a synchronized scoped mapping, never automatically driven by the teardown. | | `skills/superloopy-frontend/references/design/airbnb.md` | Defines Airbnb's photography-first consumer token set: #ff385c coral used scarcely, #222222 ink on white, single-family Cereal at 500-weight body, hairline borders and the signature 3-layer booking-panel shadow. | Superloopy-native; original prose, public brand facts only, no external design files vendored. | | `skills/superloopy-frontend/references/design/airtable.md` | Encodes Airtable's Swiss-clean enterprise look — white canvas, deep navy #181d26 ink, Airtable Blue #1b61c9, positive body letter-spacing, and blue-tinted multi-layer shadows. | Superloopy-native; original prose, public brand facts only, no external design files vendored. | | `skills/superloopy-frontend/references/design/amazon.md` | Dense utilitarian-marketplace teardown: navy #131921 chrome, 14px body on a 4px base, thin gray-border separation, and the warm #ffd814/#ffa41c yellow CTA as the single loudest action with dark text. | Superloopy-native; original prose, public brand facts only, no external design files vendored. | @@ -312,18 +310,26 @@ Superloopy is its own lightweight loop harness: one small CLI, repo-local `.supe | `skills/superloopy-frontend/references/design/wise.md` | Wise teardown: weight-900 Wise Sans display at 0.85 line-height, lime #9fe870 CTAs with #163300 text, scale(1.05)/scale(0.95) button motion. | Superloopy-native; original prose, public brand facts only, no external design files vendored. | | `skills/superloopy-frontend/references/design/xai.md` | Monochrome white-on-warm-near-black brutalist minimalism using monospace as extreme-scale display type and uppercase tracked-out mono buttons. | Superloopy-native; original prose, public brand facts only, no external design files vendored. | | `skills/superloopy-frontend/references/design/zapier.md` | Encodes Zapier's cream-paper warmth — #fffefb canvas, warm-black #201515 ink, Zapier Orange #ff4f00, Degular Display heros at 0.90 line-height with Inter UI, and sand-border structure instead of shadows. | Superloopy-native; original prose, public brand facts only, no external design files vendored. | -| `skills/superloopy-frontend/references/image-first.md` | Image-first discipline: secure a visual target before coding, deep-analyze to a spec, implement, then visual-QA, with a no-imagegen degrade path. | Superloopy-native; original prose, public brand facts only, no external design files vendored. | -| `skills/superloopy-frontend/references/motion.md` | Conditional motion guidance with single-driver ownership, responsive GSAP measurement, static reduced-motion layouts, and interactive browser evidence. | Superloopy adaptation of selected Taste Skill mechanisms under MIT; corrected templates and evidence rules are local, with provenance retained in `upstream-notice.md`. | -| `skills/superloopy-frontend/references/perfection.md` | Measured-quality methodology: design-system compliance script plus a real-browser Lighthouse protocol via npx with an anti-gaming list. | Superloopy-native; original prose, npx runtime fetch keeps package.json dependency-free, nothing vendored. | -| `skills/superloopy-frontend/references/redesign.md` | Evidence-first redesign protocol for preserve/overhaul modes, unknown source disclosure, contract preservation, and risk-ordered modernization. | Superloopy adaptation of selected Taste Skill mechanisms under MIT, integrated with local evidence gates and explicit unknown handling. | -| `skills/superloopy-frontend/references/system-map.md` | Contract-gated routing to maintained official design systems without overriding the existing stack or silently adding dependencies. | Superloopy adaptation of selected Taste Skill mechanisms under MIT; package-status and approval guardrails are locally maintained. | -| `skills/superloopy-frontend/references/upstream-notice.md` | Taste Skill source, adaptation boundary, and retained full MIT copyright/permission notice. | Attribution and license record for substantial adapted tables and code-template mechanisms; no upstream runtime dependency. | -| `skills/superloopy-frontend/references/web.md` | Web route preserving the anti-slop, breakpoint, browser-capture, design-compliance, and Lighthouse gates while lazily routing optional context references. | Core Superloopy web-route text remains original prose split from the existing skill; adapted optional references retain provenance and add no Superloopy runtime dependency. | -| `skills/superloopy-frontend/references/qt.md` | Common Qt contract for version gates, live platform ownership, accessibility, localization, high DPI, and native behavior. | Superloopy-native; original prose grounded in linked official [Qt application documentation](https://doc.qt.io/qt-6/qguiapplication.html) and related Qt references; no external source files vendored. | -| `skills/superloopy-frontend/references/qt-widgets.md` | Qt Widgets ownership, layout, style, interaction, delegate, and accessibility contract. | Superloopy-native; original prose grounded in linked official [QStyle documentation](https://doc.qt.io/qt-6/qstyle.html) and related Qt Widgets references; no external source files vendored. | -| `skills/superloopy-frontend/references/qt-quick.md` | Qt Quick/QML contract for Controls style selection, module ownership, state, accessibility, tooling, and mixed QQuickWidget boundaries. | Superloopy-native; original prose grounded in linked official [Qt Quick Controls style documentation](https://doc.qt.io/qt-6/qtquickcontrols-styles.html) and related Qt Quick references; no external source files vendored. | -| `skills/superloopy-frontend/references/qt-qa.md` | Qt-native command, state-matrix, capture, screenshot-interpretation, and disclosure gate. | Superloopy-native; original prose grounded in linked official [Qt Test documentation](https://doc.qt.io/qt-6/qttest-best-practices.html) and related Qt tooling references; no external source files vendored. | -| `skills/superloopy-frontend/scripts/ds-compliance.mjs` | Dependency-free design-system compliance gate: flags undeclared hex and off-scale spacing against DESIGN.md for loop proof. | Superloopy-native; node:fs only, no external dependency. | +| `skills/superloopy-frontend/references/image-first.md` | Conditional visual-reference discipline for a materially changed visual claim, separating current-surface deltas, non-authoritative exploration, and approved implementation references with semantic/spatial/platform/accessibility promotion gates. | Superloopy-native original prose; reference images and generated copy cannot replace existing content, brand, token, platform, accessibility, or behavior authority, artifact composition is selected by the claim, and narrow nonvisual work stays outside this route. | +| `skills/superloopy-frontend/references/desktop.md` | Named-target desktop ownership contract with a build/launch/journey/adjacent-regression floor and risk-selected chrome, input, accessibility, distribution, recovery, and package-lifecycle proof. | Superloopy-native proportional contract; original prose synthesized for Superloopy, no external source files or runtime dependencies vendored. | +| `skills/superloopy-frontend/references/hybrid.md` | Hybrid client/shell/service/bridge ownership contract with a real client-and-shell regression floor and independently attributable capability, lifecycle, and target evidence. | Superloopy-native proportional contract; original prose synthesized for Superloopy, no external source files or runtime dependencies vendored. | +| `skills/superloopy-frontend/references/layout.md` | Claim-triggered cross-platform spatial contract for region exposure modes, generic source and destination states, conditional collapsed and expanded states, size, scroll, overflow, reflow, adaptation, multi-pane, two-dimensional, and virtualized UI ownership with proportional evidence. | Superloopy-native independent synthesis informed by architecture research; target-derived semantics and toolkit ownership replace Web recipes or fixed widths, no StyleGallery text/code/schema/pattern is copied, and no runtime dependency is added. | +| `skills/superloopy-frontend/references/mobile.md` | Mobile/tablet contract for current-window navigation, insets, input, permissions, restoration, locale and accessibility, with a build/launch/journey/adjacent-regression floor and risk-selected device proof. | Superloopy-native proportional contract; original prose synthesized for Superloopy, no external source files or runtime dependencies vendored. | +| `skills/superloopy-frontend/references/renderer.md` | Composition and semantic ownership contract for text/input, target-applicable scaling, performance, accessibility, and crawlability, with screenshots explicitly insufficient for capability promotion. | Superloopy-native proportional contract; original prose synthesized for Superloopy, no external source files or runtime dependencies vendored. | +| `skills/superloopy-frontend/references/ux.md` | Shared proportional UX workflow for capability truth, input burden, Undo, state, resource identity, reset provenance, information hierarchy, proportional disclosure, i18n, accessibility, adaptation, high-impact rationale, task-based usability evidence, accepted-debt lifecycle, and claim-shaped proof. | Superloopy-native; original prose synthesized for Superloopy, judgment/usability/debt records activate by decision and consequence rather than every UI detail, and no external source files or runtime dependencies are vendored. | +| `skills/superloopy-frontend/references/motion-core.md` | Claim-triggered platform-neutral temporal contract for provider ownership, before/intermediate/after truth, repeated/interrupted/reversed/retargeted/cancelled behavior, reduced motion, optional haptics, and real-target evidence. | Superloopy-native independent synthesis informed by architecture research; it imposes no Web library, common timing curve, or sensory-feedback requirement, copies no StyleGallery material, and adds no dependency. | +| `skills/superloopy-frontend/references/motion.md` | Web-only changed-motion specialization with authoritative existing primitives, one driver per property, target-derived activation breakpoints, media-owned setup/cleanup, static reduced-motion document flow, responsive remeasurement, and real interaction evidence. | Superloopy adaptation of selected Taste Skill mechanisms under MIT; it follows the platform-neutral motion core only for browser-owned implementation, illustrative 1024px templates never create product support claims, dependencies require existing use or approval, and corrected local evidence rules retain provenance in `upstream-notice.md`. | +| `skills/superloopy-frontend/references/perfection.md` | Claim-selected measured-quality gate: partial color/spacing token lint, pinned Lighthouse/Node compatibility, indexed raw runs plus reproducible median summary, project budgets/regressions, current-target crawlable-public SEO, and privacy-safe optional React Doctor output. | Superloopy-native; original prose, pinned `npx` runtime fetches keep package.json dependency-free, and default React Doctor flags disable telemetry and supply-chain analysis unless explicitly approved. | +| `skills/superloopy-frontend/references/redesign.md` | Evidence-first preserve/overhaul protocol with REDESIGN_AUDIT.md, unavailable-source disclosure, authoritative design ownership, target-derived proof, stable content/analytics/discovery contracts, and risk-ordered modernization. | Superloopy adaptation of selected Taste Skill mechanisms under MIT; information architecture stays by default and broken IA changes only when actually in scope and explicitly approved from the audit. | +| `skills/superloopy-frontend/references/system-map.md` | Actual-contract-only routing to maintained official design systems with current-doc/license/version verification, one appearance owner, and proportional integration proof. | Superloopy adaptation of selected Taste Skill mechanisms under MIT; missing dependencies require approval, an implementation request then executes one verified package-manager command while advice-only presents one, and package/build/behavior integration is verified afterward. | +| `skills/superloopy-frontend/references/upstream-notice.md` | Taste Skill adapted-source notice plus the pinned StyleGallery architecture-research boundary. | Retains the full MIT notice for Taste Skill while recording that the separately reviewed StyleGallery snapshot had no identified root license and contributed no copied code, prose, schema, pattern, generated file, or dependency. | +| `skills/superloopy-frontend/references/web.md` | Target-derived Web route for product/application DOM, marketing/editorial, PWA/extension, custom-rendered, and embedded-client delivery, with a production build/journey/regression/browser floor and visual, breakpoint, Lighthouse, React Doctor, renderer, shell, and SEO proof selected by changed claim and risk. | Core Superloopy Web-route text remains original prose; 390/768/1280 are continuity samples rather than universal proof, SEO follows the current crawlable public Web target or a distinct public deployment for native/embedded work, adapted optional references retain provenance, and no runtime dependency is added. | +| `skills/superloopy-frontend/references/qt.md` | Target-neutral Qt 6 contract composed with desktop or mobile ownership, minimum-version guards, live platform values, applicable input/chrome/transitions, accessibility, localization, high DPI, and target-native behavior. | Superloopy-native; original prose grounded in linked official [Qt application documentation](https://doc.qt.io/qt-6/qguiapplication.html) and related Qt references; no desktop checklist is imposed on mobile/tablet and no external source files are vendored. | +| `skills/superloopy-frontend/references/qt-widgets.md` | Target-aware Qt Widgets contract for one appearance owner per subtree, proportional branded/multi-identity machinery, shared paint/hit-test/input/accessibility geometry, layout/state ownership, and native-inclusive behavior. | Superloopy-native original prose grounded in linked official [QStyle documentation](https://doc.qt.io/qt-6/qstyle.html); desktop-only obligations apply only when exposed, mobile/tablet uses its target contract, and ordinary native-adaptive UI does not inherit a skin/gallery system. | +| `skills/superloopy-frontend/references/qt-quick.md` | Qt Quick/QML contract for one Controls style route, module and geometry/state ownership, accessibility and tooling, target-specific desktop versus mobile transitions, and mixed QQuickWidget boundaries. | Superloopy-native; original prose grounded in linked official [Qt Quick Controls style documentation](https://doc.qt.io/qt-6/qtquickcontrols-styles.html) and related Qt Quick references; no external source files vendored. | +| `skills/superloopy-frontend/references/qt-qa.md` | Claim- and risk-scoped Qt command, state, behavioral, optional visual-capture, screenshot-interpretation, and unverified-surface disclosure gate. | Superloopy-native; original prose grounded in linked official [Qt Test documentation](https://doc.qt.io/qt-6/qttest-best-practices.html) and related Qt tooling references; a visual artifact is required only for changed visual or visibly consequential interaction claims, and browser/offscreen proof cannot substitute for native evidence. | +| `skills/superloopy-frontend/scripts/ds-compliance.mjs` | Dependency-free partial token lint for undeclared 3/4/6/8-digit hex and off-scale integer, fractional, or negative px values on exact CSS spacing properties, including multiline declarations, while allowing absolute 0/1px and ignoring border-side lookalikes. | Superloopy-native Node-built-in helper with value-line reporting and a Windows-safe CLI entry guard; non-zero violations remain bounded color/spacing proof, not typography, semantics, accessibility, or design-system adoption. | +| `skills/superloopy-frontend/scripts/evidence-root.mjs` | Dependency-free cross-shell helper that creates one portable timestamped frontend evidence root and verifies non-empty regular artifacts confined beneath it. | Superloopy-native Node-built-in helper; rejects invalid or Windows-reserved slugs, duplicate run roots, traversal, symlink ancestors/leaves, empty files, and out-of-root evidence without adding a project dependency. | | `skills/superloopy-frontend/scripts/visual-diff.mjs` | Dependency-free PNG visual-diff for the frontend visual-QA gate: similarityScore, alpha intactness, and ranked grid hotspots as review evidence. | Superloopy-native; uses only node:zlib/node:fs, no external image library or vendored code. | | `skills/superloopy-loop/SKILL.md` | Skill instructions for Superloopy guide, proof, capture, evidence, check, finish, review, checkpoint, and doctor flow. | Uses Superloopy-native workflow terms. | | `skills/superloopy-loop/agents/openai.yaml` | Minimal agent metadata for Superloopy discovery. | Search terms are Superloopy-native. | @@ -446,7 +452,7 @@ Superloopy is its own lightweight loop harness: one small CLI, repo-local `.supe | `src/loop.js` | Core plan lifecycle: create, status, next, evidence, review, checkpoint, and steering. | Original `.superloopy` state machine. | | `src/managed-agents.js` | Canonicalizes lock identities, recognizes complete hash-known legacy fleets, verifies inode identity, installs by no-replace hard link, rolls back safely, and writes state last. | Dependency-free transaction boundary; partial or edited legacy fleets remain conflicts. | | `src/plan-trust.js` | Trust boundary for plan-recorded commands: audit re-runs execute only commands captured locally or approved via `loop trust`; the approval ledger lives in the user home so repo contents cannot forge trust. | Superloopy-native repo-poisoning defense. | -| `src/matrix-gate.js` | Validator for strict matrix quality gates. | Keeps compatible shape under Superloopy-native module name. | +| `src/matrix-gate.js` | Validator for strict matrix gates with unique contract-bound rows, structured one-target/one-owner surface scope metadata, family-compatible claim proof, independent resolved artifacts, and explicit not-applicable boundaries. | Keeps exact unscoped legacy literal compatibility while new scoped surface rows require `target {id, platform, environment}` plus owner/claims/reason and reject aggregate-vague/multi-target metadata, duplicate target-owner slices, artifact-only or cross-contract coverage, duplicate IDs, empty adversarial evidence, hidden proof fields, and resolved-path reuse across different scoped surface slices. Adversarial and coverage rows reference proof without declaring another slice. | | `src/model-catalog.js` | Bounded read-only Codex app-server client that lists and normalizes visible model capabilities, sanitizes unknown results, and waits through bounded TERM-to-KILL child cleanup. | Uses only initialize and paginated `model/list`; never starts a thread, turn, or prompt and never persists raw server output. | | `src/model-policy.js` | Loads and validates `model-policy.json`, enforces the declared second-position compatibility model, resolves ordered complete Codex tuples, and checks bundled Codex/Claude policy documentation and agent pins. | Pure policy/resolver boundary; never launches agents, treats model choice as proof, or relies on parent/default inheritance. | | `src/model-resolution.js` | Prepares the managed Codex fleet resolution, reuses a valid 24-hour cache, handles policy/target/explicit refresh, and exports the shared state validator. | Unknown first probes choose policy compatibility; unknown refreshes preserve valid state; preparation validates but does not persist files. | @@ -455,7 +461,7 @@ Superloopy is its own lightweight loop harness: one small CLI, repo-local `.supe | `src/prove.js` | ID-free proof shortcut for the active next unresolved criterion. | Superloopy-specific proof path. | | `src/receipt.js` | Host-agnostic receipt recovery for SubagentStop: trusts last_assistant_message when present, else scans the decoded final turn of the subagent transcript, failing closed (re-prompt) when the final message exceeds the tail window. | Superloopy-native; node-only, shared by the Codex and Claude hook paths. | | `src/report.js` | Writes portable markdown evidence reports with Evidence Summary section, Evidence Warnings section, and next action. | Superloopy-only reporting layer. | -| `src/review-gate.js` | Validator for strict five-section review quality gates. | Keeps compatible shape under Superloopy-native module name. | +| `src/review-gate.js` | Validator for strict five-section review gates with composite classification, structured one-target/one-owner surface scope metadata, claim-shaped proof floors, compatible artifact families, independent resolved artifacts, and non-empty adversarial evidence. | Keeps exact unscoped legacy literal and gate-shape compatibility while new scoped surface rows require `target {id, platform, environment}` plus owner/claims/reason and reject aggregate-vague/multi-target metadata, duplicate target-owner slices, duplicate proof IDs, incompatible HTTP/data artifacts, or resolved-path reuse across different scoped surface slices. Adversarial rows reference proof without declaring another slice. | | `src/source-checkout.js` | Distinguishes a source checkout (own `.git` or tracked monorepo subdirectory) from an installed/packed root for doctor's Git-vs-filesystem semantics. | Original Superloopy root classifier. | | `src/spawn-command.js` | Cross-platform process invocation helper for npm/npx command shims. | Minimal Superloopy utility used by update planning only. | | `src/store.js` | `.superloopy/` path construction, session normalization, atomic JSON writes, and ledger appends. | Original storage layer. | @@ -470,12 +476,16 @@ Superloopy is its own lightweight loop harness: one small CLI, repo-local `.supe | `test/cli.test.js` | CLI smoke tests for managed install, conflict/force behavior, model refresh and compatibility help, symlinked execution, and loop commands. | Uses isolated install homes and deterministic `--compat`; tests the command surface without a live catalog query. | | `test/concurrency.test.js` | Cross-process lock tests plus managed rollback checks for same-content inode replacement and visible backup-cleanup failure. | Uses temporary files only; proves concurrent user saves are not removed by rollback. | | `test/crew-lines.test.js` | Unit and CLI coverage for presentation-only localized crew completion lines. | Prevents crew flavor from replacing status or speaking for pending/unknown lanes. | -| `test/docs.test.js` | Public documentation contract tests for Superloopy-native docs and threshold history. | Keeps docs aligned with product contract. | -| `test/frontend-qt-contract.test.js` | Focused contract tests for explicit web/Qt routing, native ownership, Qt Quick style selection, and Qt evidence. | Superloopy-native test; enforces original routing prose and official-source-linked reference contracts without external fixtures. | +| `test/docs.test.js` | Public documentation contract tests for localized frontend scope, structured gate target/artifact guidance, target-aware audit/golden inventory, portable evidence-helper coverage, authority/approval boundaries, the Qt Kanban prototype boundary, and threshold history. | Keeps docs aligned with the enforced cross-platform product contract and prevents stale universal Web/Qt, design-authority, dependency-action, or gate-compatibility claims. | +| `test/frontend-qt-contract.test.js` | Focused contract tests for explicit target-specific Web/Qt routing, native ownership, Qt Quick style selection, Qt evidence, and honest Kanban sidebar capabilities. | Superloopy-native test; rejects dead affordances, unavailable-route success UI, and orphaned sidebar resources without external fixtures. | +| `test/frontend-quality-overlays.test.js` | Focused contract tests for independent claim-triggered spatial/motion overlays, generic disclosure states, horizontal-scroll exceptions, visual-reference authority, high-impact rationale, task-based usability evidence, accepted-debt boundaries, provenance, and audit responsibilities. | Superloopy-native regression test; enforces proportional cross-platform guidance without importing StyleGallery code, prose, schema, patterns, or dependencies. | +| `test/frontend-routing-scenarios.test.js` | Exact routing scenarios for browser-hosted DOM/canvas, installed PWA/extension, desktop/mobile embedded HTML, native/custom, desktop/mobile/WebAssembly Qt, mobile/tablet, and mixed application UI, plus portable evidence-root confinement. | Superloopy-native regression test; also locks explicit exclusions, proportional artifacts, standalone versus active-loop evidence ownership, and unsafe slug/traversal/symlink/empty-proof rejection without dependencies. | +| `test/frontend-ux-contract.test.js` | Focused contract tests for explicit screen-based scope, user/baseline discovery, shared capability/state truth, cross-frontend reachability/handoff, target-aware Web/platform ownership, proportional evidence, and project-native design authority. | Superloopy-native test only; validates measured-quality privacy/reproducibility and adds no runtime dependency. | +| `test/frontend-ux-semantics.test.js` | Focused contract tests for shared resource identity and lifecycle transitions, reset provenance, distinct-purpose information hierarchy, proportional disclosure, and scoped universal-reversal guards. | Superloopy-native test only; preserves valid platform-, provider-, copy-, monitoring-, and expert-work exceptions without weakening the shared UX contract. | | `test/doctor-packed.test.js` | Regression coverage proving doctor accepts an npm-packed install (packaging-stripped repo-only files absent) from an arbitrary cwd, including one nested inside a parent Git repository. | Simulated pack extract; Superloopy-native coverage only. | | `test/doctor-review-feedback.test.js` | Regression tests for PR review hardening around doctor root selection and corrupt skills paths. | Superloopy-native review-feedback coverage only. | | `test/doctor.test.js` | Doctor coverage for package, audits, repository model policy, installed-routing summary, and reviewability checks. | Uses synthetic or isolated fixtures; absent personal installation remains healthy and informational. | -| `test/ds-compliance.test.js` | Unit tests for the design-system compliance scanner. | Superloopy-native test only. | +| `test/ds-compliance.test.js` | Unit and CLI tests for DESIGN.md parsing, 3/4/6/8-digit color normalization, negative/fractional spacing, exact CSS property matching, multiline value-line reporting, border-side exclusion, partial-match avoidance, allowed 0/1px, and non-zero violations. | Superloopy-native temporary-fixture test only; no external dependency. | | `test/engineer.test.js` | Loop-engineer trigger tests: team/crew escalation parsing and the baseline vs. crew fan-out directive. | Tests Superloopy `loopy`/`loopy team` directive behavior. | | `test/file-audit.test.js` | Direct unit coverage for row-level file audit verifier. | Tests Superloopy audit parser. | | `test/fleet.test.js` | Handoff registry, fleet reconciliation, verdict-normalization, and crew-line decoration tests. | Tests Superloopy parent-side coordination without persisting presentation flavor. | @@ -487,8 +497,9 @@ Superloopy is its own lightweight loop harness: one small CLI, repo-local `.supe | `test/golden-helpers.js` | Shared temporary repo, CLI, evidence, and gate fixture helpers for golden tests. | Superloopy-owned fixtures. | | `test/helpers/trust-isolate.js` | Test-only side effect isolating the plan-trust approval ledger to a temp dir. | Superloopy-native test helper. | | `test/golden-hooks.test.js` | Hook, scoped-state, steering, continuation, receipt, and manifest scenarios. | Exercises Superloopy hook behavior. | -| `test/golden-matrix-gate.test.js` | `@goal` and matrix gate scenarios. | Keeps matrix compatibility evidence isolated. | -| `test/golden-review-gate.test.js` | Review gate acceptance and rejection scenarios. | Tests five-section gate compatibility. | +| `test/golden-matrix-gate.test.js` | `@goal` plus matrix scenarios for structured targets, exact legacy absence compatibility, singular scoped surface owner/target rows, proportional proof, composite surfaces, resolved-path independence between surface slices, contract binding, unique IDs, adversarial evidence, and not-applicable boundaries. | Keeps matrix compatibility isolated while proving the strengthened scoped surface gate fails closed and adversarial/coverage references cannot declare another slice, without rewriting legacy literal meaning. | +| `test/golden-review-gate.test.js` | Five-section review scenarios for structured targets, exact legacy absence compatibility, singular scoped surface owner/target rows, proportional proof, composite surfaces, resolved-path independence between surface slices, artifact compatibility, unique IDs, and adversarial evidence. | Proves legacy compatibility remains exact while strengthened scoped surface native, hybrid, renderer, HTTP, data, and nonvisual claim handling fails closed; adversarial references do not declare another slice. | +| `test/golden-scoped-target-platforms.test.js` | Scoped-row identity scenarios proving one generic target ID may recur across distinct platforms for the same owner while one repeated ID/platform/owner slice still fails closed, in both gate shapes. | Keeps the platform-aware scoped-slice identity rule pinned without growing the per-gate golden files past the reviewability cap. | | `test/hooks.test.js` | Hook tests for fresh-manifest reuse, restart disclosure after managed definition changes, guards, receipts, steering, context, and stop behavior. | Uses isolated paths and injected resolution behavior; does not query a live catalog. | | `test/host-adapter.test.js` | Unit tests for host-agnostic receipt recovery (direct + transcript fallback, evidence + audit). | Superloopy-native test only. | | `test/humanize-korean.test.js` | Contract tests for the Korean humanizer audit script's accept and reject paths. | Superloopy-native test for dependency-free safeguards and protected-token gating. | @@ -513,7 +524,7 @@ Superloopy is its own lightweight loop harness: one small CLI, repo-local `.supe ## Weight Notes -- Current largest source file: `src/agents.js`, below the reviewability cap (550 lines). Keep managed-file mechanics in `src/managed-agents.js` and other new checks in focused helpers rather than growing orchestrators past the cap. +- Current largest source file: `src/review-gate.js`, at the reviewability cap (550 lines). Keep new classifier, target-schema, and proof mechanics in focused helpers or extract an existing concern before growing the gate further. - No package dependencies are added; `package.json` stays dependency-free and `superloopy doctor --json` checks that boundary. - Marketplace update checks are advisory and self-update only runs for a future npx-local snapshot; current marketplace and checkout installs keep their documented update commands. - Runtime state is ignored under `.superloopy/`; `superloopy doctor --json` verifies runtime samples are ignored and not tracked. diff --git a/docs/superloopy-gate-notes.md b/docs/superloopy-gate-notes.md index 7567288..977794b 100644 --- a/docs/superloopy-gate-notes.md +++ b/docs/superloopy-gate-notes.md @@ -12,6 +12,104 @@ Superloopy keeps strict completion evidence while using Superloopy-owned names i - Artifact references must resolve inside `.superloopy/evidence/` or the active scoped evidence root, must not be symlinks, and must be non-empty files. - `superloopy loop review --artifact` and `superloopy loop finish --artifact` write the machine quality gate and require a `.json` path. Human Markdown gate reports, such as a crew final-gate report, must be recorded as separate evidence artifacts. +### Claim-shaped surface evidence + +Review-gate `manualQa.surfaceEvidence` and matrix-gate `executorQa.surfaceEvidence` support proportional proof without letting one composite artifact silently stand in for every owner or target. A new scoped row represents exactly one concrete target and one affected owner. Its `target`, `owner`, non-empty `claims`, and `scopeReason` fields are an all-or-nothing group; partial, empty, duplicate, vague, unknown, or surface-incompatible metadata fails closed. + +`target` is an object with three required fields: + +- `id` is a stable portable slug: lowercase ASCII letters/digits joined by single hyphens. It identifies one execution target, such as `windows-11-tauri-shell` or `chrome-128-windows-11`. The aggregate tokens `all`, `any`, `every`, `multi`, `multiple`, `cross`, `universal`, `supported`, `targets`, `devices`, `platforms`, and `browsers` are rejected when they appear as slug tokens or form a compact aggregate-only ID such as `alltargets` or `supporteddevices`. A short ID such as `desktop`, `mobile`, or `web` is syntactically valid; the required `platform` and concrete `environment` fields keep it from acting as an aggregate target assertion. +- `platform` is one lowercase alphanumeric symbolic platform ID such as `windows`, `android`, `ios`, `macos`, `linux`, or `web`. It names one platform, never a list or free-text combination. +- `environment` names the exact runtime, device, application shell, or browser build used for proof and may include its host OS, for example `Tauri 2.8.2 native shell on Windows 11 24H2 x64` or `Chrome 128.0.6613.86 on Windows 11 24H2 x64`. Aggregate target-set phrases such as `all supported browsers`, `any devices`, or `cross-platform` are rejected. + +The `surface` label describes the delivery/composition being exercised; it does not replace the structured target. Reuse a `target.id` for multiple owners only when they are genuinely parts of the same execution target, and then create a separate row for each owner. Free-text framework names are not a complete registry. A named Chrome/Chromium, Firefox, Safari, or Edge build on Android, iOS, macOS, Windows, or Linux remains browser-owned unless the row names a native application owner; a WebView, Tauri/Electron/Capacitor/Cordova bridge, or separately named native application uses its own owner row. + +Owners are `browser`, `native`, `hybrid`, `renderer`, `cli`, `tui`, `http`, or `data` when the named surface actually exposes that owner. For an embedded or hybrid surface, `browser` means the client, `native` means the shell, and `hybrid` means the bridge/composition. Claims are the owner-applicable subset of `interaction`, `visual`, `accessibility`, `target`, `package-lifecycle`, `renderer`, `http`, and `data`. `scopeReason` explains why that one owner and its selected claims changed. These fields establish a mechanical minimum, not the truth of the declared scope; review must still detect an omitted owner, target, or claim. + +The complete accepted artifact-kind vocabulary is: `cli-transcript`, `log`, `failure-mode-test`, `browser-automation`, `screenshot`, `image`, `http-dump`, `data-diff`, `cli-replay`, `pty-capture`, `app-automation-transcript`, `client-automation-transcript`, `api-package-test-report`, `accessibility-tree`, `device-report`, `package-lifecycle-report`, and `renderer-trace`. Acceptance into this vocabulary does not make a kind compatible with every owner: `failure-mode-test`, for example, normally proves an adversarial case rather than a surface claim. Every referenced artifact must resolve to a non-empty, non-symlink file inside the active evidence boundary. + +### Scoped owner/claim proof minimums + +The validator unions the minimum for every selected claim. A structured target description never substitutes for a `device-report` when that report is required. + +| Owner | Allowed claim | Minimum referenced proof | +| --- | --- | --- | +| `cli` | `interaction` | One of `cli-transcript`, `log`, or `cli-replay`. | +| `tui` | `interaction` | One of `pty-capture`, `cli-transcript`, `log`, or `cli-replay`. | +| `tui` | `visual` | `screenshot`/`image`, or a `pty-capture`. | +| `browser` | `interaction` | `browser-automation` or `client-automation-transcript`. | +| `browser` | `visual` | `screenshot` or `image`. | +| `browser` | `accessibility` | `accessibility-tree` plus browser/client interaction proof. | +| `browser` | `target` | `device-report`. | +| `browser` | `package-lifecycle` | `package-lifecycle-report`. | +| `native` | `interaction` | `app-automation-transcript` plus `device-report`. | +| `native` | `visual` | `screenshot`/`image` plus `device-report`. | +| `native` | `accessibility` | `accessibility-tree`, `app-automation-transcript`, and `device-report`. | +| `native` | `target` | `device-report`. | +| `native` | `package-lifecycle` | `package-lifecycle-report` plus `device-report`. | +| `hybrid` | any allowed claim | Every scoped hybrid row first needs browser/client interaction, shell `app-automation-transcript`, and `device-report`; then add `screenshot`/`image` for `visual`, `accessibility-tree` for `accessibility`, or `package-lifecycle-report` for `package-lifecycle`. | +| `renderer` | `interaction` | One of browser, client, or application automation. | +| `renderer` | `visual` | `screenshot` or `image`. | +| `renderer` | `accessibility` | `accessibility-tree` plus browser/client/application interaction proof. | +| `renderer` | `target` | `device-report`. | +| `renderer` | `renderer` | `renderer-trace`. | +| `http` | `http` | `http-dump`. | +| `data` | `data` | `data-diff` or `api-package-test-report`. | + +Resolved proof is attributable to one scoped target-owner slice. Within `surfaceEvidence`, each scoped `target.id` + `target.platform` + `owner` slice may appear only once — one generic target ID may recur across distinct platforms — and two different scoped slices may not cite the same resolved artifact path. Duplicate resolved paths are also rejected when `artifactRefs` are declared, even under different artifact IDs. Reusing a path cannot turn one execution into independent target proof. `adversarialCases` and matrix `contractCoverage` do not declare their own `target` or `owner`; they reference artifact IDs and, for matrix coverage, contract-bound surface/adversarial rows. Those references inherit the relevant surface slice instead of creating a second slice. This exclusivity tightening does not reinterpret legacy unscoped literal rows. + +Matrix contract coverage must cite a surface or adversarial proof row with the same `contractRef`; direct artifact-only coverage, duplicate proof IDs, empty adversarial artifacts, or proof/scope fields hidden on `not_applicable` rows fail closed. Contract and adversarial references cannot declare a new target-owner slice or override the scoped surface row they cite. + +The following complete fragment can be copied under a review gate's `manualQa` object for a native shell slice. The files must already exist and be non-empty: + +```json +{ + "artifactRefs": [ + { + "id": "windows-shell-action", + "kind": "app-automation-transcript", + "description": "Native menu activation and resulting shell state on the named target.", + "path": ".superloopy/evidence/frontend/20260720T120000Z-tauri-menu/windows-shell-action.txt" + }, + { + "id": "windows-shell-device", + "kind": "device-report", + "description": "Exact Windows, architecture, Tauri runtime, and package identity.", + "path": ".superloopy/evidence/frontend/20260720T120000Z-tauri-menu/windows-shell-device.json" + } + ], + "surfaceEvidence": [ + { + "id": "surface-windows-tauri-shell", + "criterionRef": "C001", + "surface": "Tauri desktop native shell", + "target": { + "id": "windows-11-tauri-shell", + "platform": "windows", + "environment": "Tauri 2.8.2 native shell on Windows 11 24H2 x64" + }, + "owner": "native", + "claims": ["interaction", "target"], + "scopeReason": "The changed menu command and target integration are owned by the Windows shell.", + "invocation": "Launch the packaged app and activate File > Import with keyboard and pointer.", + "verdict": "passed", + "artifactRefs": ["windows-shell-action", "windows-shell-device"] + } + ] +} +``` + +For a matrix row, use `contractRef` in the surface row and bind `contractCoverage` to that same contract as described above. + +### Exact legacy absence compatibility + +When **all four** scoped fields (`target`, `owner`, `claims`, and `scopeReason`) are absent, the row remains unscoped and uses the prior surface-family behavior. Supplying only some of them is never legacy mode. + +- Review exact normalized `browser` or `gui` literals retain image-only (`screenshot`/`image`) compatibility. Exact review `web` and richer browser labels do not get that exception and require the normal browser-family floor. +- Matrix exact normalized `browser`, `gui`, or `web` literals retain automation plus image proof. Matrix exact normalized `native`, `desktop`, or `tui` literals retain the former one-of screenshot/image, PTY, or app-automation proof. +- Every other unscoped label uses its conservative classified family floor. Browser target adjectives such as `mobile Web`, `desktop browser`, PWA, or extension remain browser-owned; an embedded WebView remains browser-client plus native-shell hybrid. +- The structured-target and cross-target artifact-exclusivity rules apply to new scoped surface slices. Existing legacy unscoped literal contracts retain their former meaning and are not retroactively split into inferred target-owner combinations. + ## Golden Scenarios `test/golden-hooks.test.js`, `test/golden-review-gate.test.js`, and `test/golden-matrix-gate.test.js` verify: @@ -36,6 +134,10 @@ Superloopy keeps strict completion evidence while using Superloopy-owned names i - matrix gate acceptance - inline-only executor QA proof rejection - not-applicable adversarial case rejection +- structured singular target objects plus proportional owner-and-claim surface evidence +- invalid/vague/multi-target scope rejection and composite-owner enforcement +- one scoped surface row per target-platform-owner slice, independent resolved artifacts between different surface slices, and adversarial/coverage references that cannot invent a new slice +- exact unscoped review/matrix legacy literal compatibility - one-time SessionStart bootstrap for the command wrapper and bundled agents - quiet default plugin continuation hook registration - packaged Stop hook that remains runtime opt-in through `SUPERLOOPY_STOP_HOOK=on` diff --git a/docs/superloopy-loop-golden-set.md b/docs/superloopy-loop-golden-set.md index 2ea48df..0a999a9 100644 --- a/docs/superloopy-loop-golden-set.md +++ b/docs/superloopy-loop-golden-set.md @@ -1,6 +1,5 @@ # Superloopy Loop Golden Set -This is Superloopy's own long-running loop-engineering golden set. It scores Superloopy on explicit criteria, artifact-backed proof, strict continuation, local state, append-only history, repository clarity, and reviewable files. -Inventory scope: Git-visible repository files from `git ls-files --cached --others --exclude-standard`. +This is Superloopy's own long-running loop-engineering golden set. It scores Superloopy on explicit criteria, artifact-backed proof, strict continuation, local state, append-only history, repository clarity, and reviewable files. Inventory scope: Git-visible repository files from `git ls-files --cached --others --exclude-standard`. ## Strict Rules - Each improvement turn must record a score before claiming completion. - Each new score must be greater than the previous score. @@ -80,14 +79,14 @@ Total: 100 points. | `docs/superloopy-crew-lines.md` | `test/docs.test.js`, audit coverage. | Must record the precedent pattern, no-copied-quotes rule, terminal-only behavior, and presentation-only authority boundary. | | `docs/superloopy-design-audit.md` | `src/design-audit.js`, `test/doctor.test.js`. | Must keep required decision rows with reason, effect, and guard. | | `docs/superloopy-file-audit.md` | `test/audit.test.js`, `src/file-audit.js`, doctor file-audit check. | Must list every Git-visible file with non-empty role and compatibility-boundary cells. | -| `docs/superloopy-gate-notes.md` | Doctor gate-notes check. | Must keep gate compatibility, native naming, golden scenario, and host contract sections visible. | +| `docs/superloopy-gate-notes.md` | `test/docs.test.js` and doctor gate-notes check. | Must document structured singular surface targets, all accepted artifact kinds, owner/claim proof minimums, resolved-path independence between scoped surface slices, adversarial/coverage reference inheritance, a copyable artifact/surface example, exact unscoped legacy exceptions, native naming, golden scenarios, and the host contract. | | `docs/superloopy-host-contract.md` | `test/cli.test.js`, doctor hostContract/gate-notes. | Must document the SubagentStop payload contract and the host behaviors Superloopy cannot verify. | | `docs/superloopy-interop-superpowers.md` | `test/interop.test.js`, audit coverage. | Must document the Superpowers division of labor, best-effort detection, the override, and the doctor visibility check. | | `docs/superloopy-loop-golden-set.md` | `test/docs.test.js` golden-set assertions. | Must list every Git-visible file, score each accepted run, and keep threshold history append-only. | | `docs/superloopy-model-policy-claude.md` | `test/docs.test.js` model-policy assertions, audit coverage. | Must record the Claude model aliases and the advisory steering-not-proof rule mapped from the Codex policy. | | `docs/superloopy-model-policy.md` | `test/docs.test.js`, `test/doctor.test.js`. | Must record ordered complete Codex tuples, preferred Terra/Sol/Luna pins, explicit GPT-5.5 compatibility, and pre-launch-only routing while treating model choice as steering, not proof. | | `examples/qt-kanban/CMakeLists.txt` | Original Superloopy demo build code; fresh Qt configure/build/CTest. | Must require exact Qt 6.11.1, C++17, the reusable module, native app, and Quick tests. | -| `examples/qt-kanban/DESIGN.md` | Original-prose Northstar design contract; Qt theme and visual QA. | Must keep the approved tokens, responsive thresholds, state rules, and visible-copy constraints explicit. | +| `examples/qt-kanban/DESIGN.md` | Original-prose Northstar design contract; Qt theme and visual QA. | Must keep the approved tokens, responsive thresholds, state rules, functional Board return-to-overview result, prototype acceptance boundary, and explicit persistence/Undo exclusion. | | `examples/qt-kanban/scripts/audit-macos-accessibility.sh` | Original Superloopy shell and Swift code; native macOS AX evidence. | Must verify a real card's exported role, name, focused state, AXPress action, and resulting detail-drawer activation. | | `examples/qt-kanban/scripts/capture-macos.sh` | Original Superloopy shell code; native macOS capture evidence. | Must capture the real application window through platform tools and fail clearly when native capture is unavailable. | | `examples/qt-kanban/src/Northstar/Kanban/BoardView.qml` | Original Superloopy demo code; Qt Quick lint and tests. | Must preserve four responsive columns, horizontal overflow, filtering, and real drop surfaces. | @@ -96,23 +95,23 @@ Total: 100 points. | `examples/qt-kanban/src/Northstar/Kanban/DragTaskVisual.qml` | Original Superloopy demo code; Qt Quick drag tests. | Must provide a clear move-action preview without becoming accessible duplicate content. | | `examples/qt-kanban/src/Northstar/Kanban/KanbanColumn.qml` | Original Superloopy demo code; Qt Quick board and drag tests. | Must derive counts and tasks from the store and accept cross-column drops. | | `examples/qt-kanban/src/Northstar/Kanban/KanbanHeader.qml` | Original Superloopy demo code; Qt Quick interaction tests. | Must keep board identity, search, priority filtering, and new-task action functional and named. | -| `examples/qt-kanban/src/Northstar/Kanban/KanbanView.qml` | Original Superloopy demo code; responsive, interaction, and accessibility tests. | Must compose the native shell, breakpoints, overlays, focus restoration, RTL, and empty-state behavior. | +| `examples/qt-kanban/src/Northstar/Kanban/KanbanView.qml` | Original Superloopy demo code; responsive, interaction, and accessibility tests. | The Board command must clear selection, close overlay detail, keep Board current, and restore focus while the shell preserves breakpoints, RTL, and honest empty-state behavior. | | `examples/qt-kanban/src/Northstar/Kanban/Main.qml` | Original Superloopy demo code; launch CTests. | Must expose the native application window with deterministic initial and minimum sizes. | | `examples/qt-kanban/src/Northstar/Kanban/NewTaskDialog.qml` | Original Superloopy demo code; Qt Quick interaction tests. | Must validate titles, create through the store, contain focus, and restore the invoking control. | -| `examples/qt-kanban/src/Northstar/Kanban/Sidebar.qml` | Original Superloopy demo code; responsive and accessibility tests. | Must preserve branded full/compact navigation, local icons, names, and restrained inactive-route feedback. | +| `examples/qt-kanban/src/Northstar/Kanban/Sidebar.qml` | Original Superloopy demo code; responsive and accessibility tests for functional Board navigation and passive demo-only Timeline/Inbox context. | Must emit the real Board return-to-overview action, keep Timeline/Inbox visible and named without button, press, or tab semantics, and keep Settings/Help absent. | | `examples/qt-kanban/src/Northstar/Kanban/TaskCard.qml` | Original Superloopy demo code; card, drag, interaction, and accessibility tests. | Must preserve readable metadata plus distinct focus, selection, High-priority, and motion-aware drag states. | | `examples/qt-kanban/src/Northstar/Kanban/TaskStore.qml` | Original Superloopy demo code; deterministic store tests. | Must keep seed data, search, priority filters, moves, selection, and task creation in memory and deterministic. | | `examples/qt-kanban/src/Northstar/Kanban/Theme.qml` | Original Superloopy demo code; theme and responsive tests. | Must remain the single QML token source for color, type, geometry, breakpoints, and reduced motion. | -| `examples/qt-kanban/src/Northstar/Kanban/assets/icons/add.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/board.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/close.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/filter.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/help.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/inbox.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/search.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/settings.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/timeline.svg` | Original local SVG asset family; package inventory and native render. | Every named file must remain a coherent monochrome Qt Kanban icon with no external artwork. | +| `examples/qt-kanban/src/Northstar/Kanban/assets/icons/add.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/board.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/close.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/filter.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/inbox.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/search.svg`, `examples/qt-kanban/src/Northstar/Kanban/assets/icons/timeline.svg` | Original local SVG asset family; package inventory and native render. | Every named file must remain a coherent monochrome Qt Kanban icon with no external artwork; removed Settings/Help affordances have no retained assets. | | `examples/qt-kanban/src/app/CMakeLists.txt` | Original Superloopy demo build code; launch-option CTests. | Must build the native executable and prove valid, undersized, and out-of-range window-size paths. | | `examples/qt-kanban/src/app/main.cpp` | Original Superloopy demo code; launch CTests and native QA. | Must set Basic style before QML, validate options, load the module, and log readiness after rendering. | | `examples/qt-kanban/tests/quick/CMakeLists.txt` | Original Superloopy demo build code; CTest registration. | Must compile the real Qt Quick Test harness and register every QML scenario against the reusable module. | -| `examples/qt-kanban/tests/quick/tst_accessibility.qml` | Original Superloopy demo test code; `ctest --test-dir build/qt-kanban-release`. | Must prove real accessible names, roles, actions, focus, enlarged type, RTL, and reduced motion. | +| `examples/qt-kanban/tests/quick/tst_accessibility.qml` | Original Superloopy demo test code; focused Qt Quick CTest. | Must prove real control semantics and that passive demo context has useful static text but no press action or success announcement. | | `examples/qt-kanban/tests/quick/tst_card.qml` | Original Superloopy demo test code; `ctest --test-dir build/qt-kanban-release`. | Must prove priority styling, metadata, selection/focus, keyboard movement, and drag state. | | `examples/qt-kanban/tests/quick/tst_drag.qml` | Original Superloopy demo test code; `ctest --test-dir build/qt-kanban-release`. | Must prove a pointer drag moves a task exactly once and a genuine stolen grab cancels without mutation. | -| `examples/qt-kanban/tests/quick/tst_interactions.qml` | Original Superloopy demo test code; `ctest --test-dir build/qt-kanban-release`. | Must prove search/filter, empty results, drawers, creation, Escape, focus restoration, and keyboard flow. | +| `examples/qt-kanban/tests/quick/tst_interactions.qml` | Original Superloopy demo test code; focused Qt Quick CTest. | Must prove search/filter, empty results, drawers, creation, Escape, idempotent Board return-to-overview behavior, focus restoration, and Board-to-search keyboard flow without dead destinations. | | `examples/qt-kanban/tests/quick/tst_qtkanban.cpp` | Original Superloopy demo code; Qt Quick Test executable. | Must initialize Basic style and expose real Qt accessibility queries/actions without test-only product hooks. | -| `examples/qt-kanban/tests/quick/tst_responsive.qml` | Original Superloopy demo test code; `ctest --test-dir build/qt-kanban-release`. | Must prove the approved breakpoints, board overflow, RTL start edge, and enlarged-font layout. | +| `examples/qt-kanban/tests/quick/tst_responsive.qml` | Original Superloopy demo test code; focused Qt Quick CTest. | Must prove passive demo items fit full/compact sidebars without focus, plus the approved breakpoints, board overflow, RTL start edge, and enlarged-font layout. | | `examples/qt-kanban/tests/quick/tst_smoke.qml` | Original Superloopy demo test code; `ctest --test-dir build/qt-kanban-release`. | Must prove the `Northstar.Kanban` module imports and creates its stable root object. | | `examples/qt-kanban/tests/quick/tst_store.qml` | Original Superloopy demo test code; `ctest --test-dir build/qt-kanban-release`. | Must prove deterministic data, canonical columns, filtering, movement, and creation without mocks. | | `examples/qt-kanban/tests/quick/tst_theme.qml` | Original Superloopy demo test code; `ctest --test-dir build/qt-kanban-release`. | Must prove every approved token family, breakpoint, type scale, and motion override. | @@ -234,11 +233,11 @@ Total: 100 points. | `skills/superloopy-clone/agents/openai.yaml` | Audit coverage and reviewability check. | Must remain minimal Superloopy discovery metadata for website cloning. | | `skills/superloopy-doctor/SKILL.md` | `test/plugin.test.js`, doctor skill check, audit coverage. | Must diagnose Superloopy install health with `superloopy doctor --json`, plugin cache, wrapper, agents, hook/bootstrap, and no mutation before approval. | | `skills/superloopy-doctor/agents/openai.yaml` | `test/plugin.test.js`, audit coverage and reviewability check. | Must remain minimal Superloopy discovery metadata for install diagnostics. | -| `skills/superloopy-frontend/SKILL.md` | `test/plugin.test.js`, audit coverage. | Must require explicit invocation or active-loop routing and enforce a DESIGN.md token gate, browser evidence for web, native rendered-application evidence for Qt, and Superloopy evidence receipts. | -| `skills/superloopy-frontend/agents/openai.yaml` | `test/plugin.test.js`, audit coverage and reviewability check. | Must remain minimal Superloopy discovery metadata for frontend work. | -| `skills/superloopy-frontend/references/anti-slop.md` | Audit coverage and reviewability check. | Must keep the named-default bans, countable rules, consistency locks, real-asset mandate, and pre-flight checklist. | -| `skills/superloopy-frontend/references/design-system.md` | Audit coverage and reviewability check. | Must keep the 7-section DESIGN.md schema and loopy-native token-authoring guidance. | -| `skills/superloopy-frontend/references/design/_INDEX.md` | Audit coverage and reviewability check. | Must route brands/moods to one design-token file and point unknown brands at on-demand extraction. | +| `skills/superloopy-frontend/SKILL.md` | `test/plugin.test.js`, `test/frontend-routing-scenarios.test.js`, `test/frontend-quality-overlays.test.js`, `test/frontend-ux-contract.test.js`, `test/frontend-qt-contract.test.js`, and audit coverage. | Must require explicit invocation or active-loop routing, apply shared UX once, union exact target-specific platform/composition routes, route information hierarchy changes to layout only for spatial disclosure, select layout/motion/visual references separately by changed claim, retain Qt specialization on desktop and mobile/tablet plus Qt Widgets, Qt Quick, and mixed WebAssembly with browser+renderer proof, keep owner and target claims on independent proportional evidence, support standalone receipts without invented loop state, protect original Superloopy prose, and allow no vendored runtime dependencies. | +| `skills/superloopy-frontend/agents/openai.yaml` | `test/docs.test.js`, `test/plugin.test.js`, audit coverage, and reviewability check. | Must remain concise explicit-only discovery metadata for the cross-platform application-UI workflow, exclude unsupported specialized surfaces, and require proportional owner/target evidence. | +| `skills/superloopy-frontend/references/anti-slop.md` | Audit coverage and reviewability check. | Must apply only to marketing/editorial/campaign/greenfield presentation or an explicit new visual direction, preserve established product UI, prevent unintended root/page horizontal overflow while allowing intentional task-owned horizontal regions under the `layout.md` contract, keep countable composition heuristics as review flags unless an approved criterion selects a blocker, and retain real-asset and copy truth. | +| `skills/superloopy-frontend/references/design-system.md` | Audit coverage and reviewability check. | Must keep the repository's design source authoritative, make DESIGN.md either that source or a synchronized scoped mapping, scale the seven-section schema to changed visual claims, use project-native tokens, units, states, and APIs across Web and native targets, preserve native runtime values, and leave narrow nonvisual work without added design ceremony. | +| `skills/superloopy-frontend/references/design/_INDEX.md` | Audit coverage and reviewability check. | Must route a brand/mood to one scoped teardown without making it token authority, apply approved values through the project's authoritative source, keep DESIGN.md established or synchronized, and point unknown brands at authorized on-demand extraction. | | `skills/superloopy-frontend/references/design/airbnb.md` | Audit coverage and reviewability check. | Must keep coral #ff385c to one element per viewport, Cereal body weight at 500 (never 400), and zero shadow on listing cards (whitespace + photo radius only). | | `skills/superloopy-frontend/references/design/airtable.md` | Audit coverage and reviewability check. | Must keep positive letter-spacing (+0.08–0.28px) on body/labels and blue-tinted shadows (rgba(45,127,249,0.28)) rather than neutral-gray drops. | | `skills/superloopy-frontend/references/design/amazon.md` | Audit coverage and reviewability check. | Must keep the layout dense (14px body, 4px-base spacing, gray-border separation) with the warm yellow/amber pill as the brightest action carrying dark #0f1111 text — never white text, never a blue CTA. | @@ -331,18 +330,26 @@ Total: 100 points. | `skills/superloopy-frontend/references/design/wise.md` | Audit coverage and reviewability check. | Must keep weight-900 display at 0.85 line-height, green #9fe870 limited to buttons/accents with dark-green #163300 text (never green-on-green), and scale hover/active animation. | | `skills/superloopy-frontend/references/design/xai.md` | Audit coverage and reviewability check. | Must keep #1f2228 bg (never pure black), GeistMono display/buttons vs universalSans body with no role overlap, hover that dims to 0.5 opacity, sharp 0px corners, and no shadows or accent color. | | `skills/superloopy-frontend/references/design/zapier.md` | Audit coverage and reviewability check. | Must keep warm cream #fffefb / warm-black #201515 (never pure white or black) and define structure with sand #c5c0b1 borders rather than box-shadow elevation. | -| `skills/superloopy-frontend/references/image-first.md` | Audit coverage and reviewability check. | Must require a visual target + written spec before coding, one-image-per-section, anti-drift, and a degrade path when no imagegen tool exists. | -| `skills/superloopy-frontend/references/motion.md` | Adapted MIT contract; `test/plugin.test.js`. | Must keep one driver per property, media-owned setup/cleanup, static reduced-motion layouts, function-based horizontal remeasurement, and interactive browser evidence. | -| `skills/superloopy-frontend/references/perfection.md` | Audit coverage and reviewability check. | Must keep the design-system compliance check, the real-browser Lighthouse protocol (npx, no bundled dep), the anti-gaming list, and the evidence artifact. | -| `skills/superloopy-frontend/references/redesign.md` | Adapted MIT contract; `test/plugin.test.js`. | Must keep preserve/overhaul mode detection, REDESIGN_AUDIT.md, unavailable-source disclosure, contract preservation, and explicit approval boundaries. | -| `skills/superloopy-frontend/references/system-map.md` | Adapted MIT contract; `test/plugin.test.js` and current official docs. | Must require an actual platform contract, preserve the stack, ask before dependencies, use one appearance owner per surface, and flag maintenance drift. | -| `skills/superloopy-frontend/references/upstream-notice.md` | `test/plugin.test.js`, audit coverage, and package review. | Must retain the Taste Skill source, adaptation boundary, copyright, and full MIT permission notice. | -| `skills/superloopy-frontend/references/web.md` | Original-prose contract with adapted optional routing; `test/frontend-qt-contract.test.js` and `test/plugin.test.js`. | Must preserve web anti-slop, 390/768/1280 browser capture, design-compliance, Lighthouse, and web-only lazy context routing. | -| `skills/superloopy-frontend/references/qt.md` | Original-prose contract; `test/frontend-qt-contract.test.js` and official [Qt application documentation](https://doc.qt.io/qt-6/qguiapplication.html). | Must gate APIs by the declared minimum Qt version and preserve live system palette, platform font, native metrics, accessibility, localization, and high-DPI behavior. | -| `skills/superloopy-frontend/references/qt-widgets.md` | Original-prose contract; `test/frontend-qt-contract.test.js` and official [QStyle documentation](https://doc.qt.io/qt-6/qstyle.html). | Must choose one appearance owner per subtree and keep paint, hit-test, keyboard, and accessibility geometry aligned. | -| `skills/superloopy-frontend/references/qt-quick.md` | Original-prose contract; `test/frontend-qt-contract.test.js` and official [Qt Quick Controls style documentation](https://doc.qt.io/qt-6/qtquickcontrols-styles.html). | Must select the Controls style before loading QML, keep layout/state ownership singular, preserve accessibility, and disclose QQuickWidget limits. | -| `skills/superloopy-frontend/references/qt-qa.md` | Original-prose contract; `test/frontend-qt-contract.test.js` and official [Qt Test documentation](https://doc.qt.io/qt-6/qttest-best-practices.html). | Must require real-target commands, state coverage, native application capture, human screenshot judgment, and explicit unverified surfaces; browser proof cannot substitute. | -| `skills/superloopy-frontend/scripts/ds-compliance.mjs` | `test/ds-compliance.test.js`. | Must flag undeclared hex and off-scale spacing against DESIGN.md and exit non-zero on violations, dependency-free. | +| `skills/superloopy-frontend/references/image-first.md` | Audit coverage and reviewability check. | Must activate only for a materially changed visual claim that uses a reference, distinguish current-surface delta, non-authoritative exploration, and approved implementation authority, reconcile semantic/spatial/platform/accessibility constraints before production commitment, prevent generated copy or imagery from replacing product authority, allow claim-appropriate full/section/state artifacts, and retain a no-imagegen path. | +| `skills/superloopy-frontend/references/desktop.md` | Original-prose contract; `node --test test/frontend-ux-contract.test.js`. | Must resolve the named desktop target, keep a build/launch/journey/adjacent-regression floor, and add chrome, input, accessibility, distribution, recovery, and packaged-lifecycle proof only when affected, claimed, release-critical, or selected by risk. | +| `skills/superloopy-frontend/references/hybrid.md` | Original-prose contract; `node --test test/frontend-ux-contract.test.js`. | Must keep client, shell, service, bridge, engine, package, and evidence ownership independently attributable, prove the affected journey through the real client and shell, and scale additional lifecycle/target proof by claim and risk. | +| `skills/superloopy-frontend/references/layout.md` | Original-prose contract; `node --test test/frontend-quality-overlays.test.js`. | Must activate only for material spatial changes, scale from existing-receipt delta to proportional UX contract, name semantic/layout/size/scroll ownership and exposure modes with generic source and destination proof plus conditional collapsed and expanded proof, allow justified multiple-scroll/two-dimensional/virtualized work, derive change points from the target, and prohibit fixed Web recipes as cross-platform proof. | +| `skills/superloopy-frontend/references/mobile.md` | Original-prose contract; `node --test test/frontend-ux-contract.test.js`. | Must preserve navigation/back, current-window layout, insets, applicable input, permissions, restoration, locale/accessibility, a build/launch/journey/adjacent-regression floor, and risk-selected emulator/simulator/device proof. | +| `skills/superloopy-frontend/references/renderer.md` | Original-prose contract; `node --test test/frontend-ux-contract.test.js`. | Must classify DOM/native/custom composition and separately prove semantics, text/input, target-applicable scaling/performance, accessibility, and current-target crawlability without promoting screenshots. | +| `skills/superloopy-frontend/references/ux.md` | Original-prose contract; `node --test test/frontend-ux-contract.test.js`, `node --test test/frontend-ux-semantics.test.js`, and `node --test test/frontend-quality-overlays.test.js`. | Must keep the proportional five-stage workflow, orthogonal capability truth, input/Undo/state/resource identity/reset provenance/content/information hierarchy/proportional disclosure/i18n/accessibility rules, high-impact rationale, risk-selected task usability evidence, bounded accepted-debt lifecycle, and claim-shaped evidence. | +| `skills/superloopy-frontend/references/motion-core.md` | Original-prose contract; `node --test test/frontend-quality-overlays.test.js`. | Must activate only for changed temporal claims, preserve system/provider ownership, cover applicable repeated/interrupted/reversed/retargeted/cancelled states, reduced-motion semantic equivalence, optional non-sole haptics, proportional records, and real-target evidence without Web libraries or common timing constants. | +| `skills/superloopy-frontend/references/motion.md` | Adapted MIT Web specialization; `test/plugin.test.js` and `test/frontend-quality-overlays.test.js`. | Must load after motion-core only for browser-owned implementation, preserve existing primitives, use one driver per property, derive activation breakpoints from the target rather than illustrative 1024px, keep media-owned setup/cleanup and static reduced-motion document flow, remeasure horizontal travel, and retain real interaction/cleanup evidence. | +| `skills/superloopy-frontend/references/perfection.md` | `test/frontend-ux-contract.test.js`, audit coverage and reviewability check. | Must select measured-quality layers by claim/risk, keep the partial color/spacing token lint, pin Lighthouse and compatible Node versions, preserve indexed raw runs and a reproducible median summary, honor project budgets/regressions, apply SEO to the current crawlable public Web target or a distinct public deployment for native/embedded work, run optional React Doctor with telemetry and supply-chain analysis disabled by default, retain anti-gaming rules, and write `PERF.md`. | +| `skills/superloopy-frontend/references/redesign.md` | Adapted MIT contract; `test/plugin.test.js`. | Must keep preserve/overhaul mode detection, REDESIGN_AUDIT.md, unavailable-source disclosure, authoritative design and discovery/analytics contract preservation, target-derived proof, and stable IA by default; broken IA changes only when actually in scope and explicitly approved from the audit. | +| `skills/superloopy-frontend/references/system-map.md` | Adapted MIT contract; `test/plugin.test.js` and current official docs. | Must require an actual platform contract, preserve the stack and one appearance owner, verify current package/license/version status, ask before dependencies, execute one verified package-manager command after approval for an implementation request but only present it for an advice-only request, then verify package/build/behavior integration. | +| `skills/superloopy-frontend/references/upstream-notice.md` | `test/plugin.test.js`, `test/frontend-quality-overlays.test.js`, audit coverage, and package review. | Must retain the Taste Skill source, optional adapted Web-reference boundary, copyright and full MIT permission notice, and separately pin StyleGallery as no-license-identified architecture research with no copied material or dependency and no claim that it supplied the earlier user-issue fixes. | +| `skills/superloopy-frontend/references/web.md` | Original-prose contract with adapted optional routing; `test/frontend-ux-contract.test.js`, `test/frontend-qt-contract.test.js`, and `test/plugin.test.js`. | Must classify product/application DOM, marketing/editorial, PWA/extension, custom-rendered, and embedded-client delivery; keep a production build/journey/adjacent-regression/supported-browser floor; derive browsers, breakpoints, inputs, accessibility and locales from the target; treat 390/768/1280 as optional continuity samples; and select visual, React Doctor, renderer, shell, Lighthouse, and current-target SEO proof by changed claim and risk. | +| `skills/superloopy-frontend/references/qt.md` | Original-prose contract; `test/frontend-qt-contract.test.js` and official [Qt application documentation](https://doc.qt.io/qt-6/qguiapplication.html). | Must compose the target-neutral Qt contract with desktop or mobile ownership, gate APIs by the declared minimum Qt version, preserve live system palette/font/metrics and target-applicable input/chrome/transitions, and require accessibility, localization, high-DPI, and target-native proof without importing desktop-only checks into mobile/tablet. | +| `skills/superloopy-frontend/references/qt-widgets.md` | Original-prose contract; `test/frontend-qt-contract.test.js` and official [QStyle documentation](https://doc.qt.io/qt-6/qstyle.html). | Must choose one appearance owner per subtree, add branded/multi-identity machinery only when product-owned, keep models/controllers separate from presentation, align paint/hit-test/supported-input/accessibility geometry, and apply desktop versus mobile behavior only on named targets that expose it. | +| `skills/superloopy-frontend/references/qt-quick.md` | Original-prose contract; `test/frontend-qt-contract.test.js` and official [Qt Quick Controls style documentation](https://doc.qt.io/qt-6/qtquickcontrols-styles.html). | Must select one Controls style route before loading QML, keep module/layout/state ownership singular, preserve accessibility, use desktop resize/mixed-DPR checks only where exposed, use mobile window/orientation/posture/inset transitions on mobile/tablet, and disclose QQuickWidget limits. | +| `skills/superloopy-frontend/references/qt-qa.md` | Original-prose contract; `test/frontend-qt-contract.test.js` and official [Qt Test documentation](https://doc.qt.io/qt-6/qttest-best-practices.html). | Must classify repository gates, require executable builds and real-target behavioral proof, select states by changed journey/risk, require native capture only for changed visual or visibly consequential interaction claims, retain human screenshot judgment, disclose unverified surfaces, and never accept browser/offscreen proof as native evidence. | +| `skills/superloopy-frontend/scripts/ds-compliance.mjs` | `test/ds-compliance.test.js` and `test/plugin.test.js`. | Must remain dependency-free, normalize 3/4/6/8-digit hex, flag undeclared colors and off-scale integer/fractional/negative values only on exact CSS spacing properties including multiline declarations, report the value line, ignore border-side lookalikes, avoid partial px matches, allow absolute 0/1px, invoke correctly on Windows, and exit non-zero on violations without claiming broader design compliance. | +| `skills/superloopy-frontend/scripts/evidence-root.mjs` | `test/frontend-routing-scenarios.test.js`, `test/plugin.test.js`, and package inventory. | Must create one portable `.superloopy/evidence/frontend/YYYYMMDDTHHMMSSZ-` root from an external target project and verify non-empty regular artifacts while rejecting reserved/unsafe slugs, duplicate roots, traversal, symlink paths, empty files, and out-of-root proof without dependencies. | | `skills/superloopy-frontend/scripts/visual-diff.mjs` | `test/visual-diff.test.js`. | Must decode 8-bit PNGs and emit dimensionsMatch, diffRatio, similarityScore, alphaChannelIntact, and ranked grid hotspots with no dependencies. | | `skills/superloopy-loop/SKILL.md` | `test/docs.test.js`, doctor skill check. | Must describe guide, proof, capture, evidence, check, finish, gates, and receipt rules accurately. | | `skills/superloopy-loop/agents/openai.yaml` | Audit coverage and reviewability check. | Must remain minimal Superloopy discovery metadata. | @@ -465,7 +472,7 @@ Total: 100 points. | `src/legacy-agents.js` | `test/model-install.test.js`, `test/installed-model-policy.test.js`. | Must validate known legacy manifests, classify only complete exact regular-file fleets as migratable, and expose no personal file content. | | `src/managed-agents.js` | `test/model-install.test.js`, `test/concurrency.test.js`, installed-policy tests. | Must canonicalize lock identities, adopt only complete hash-known legacy fleets, verify inode identity, preserve concurrent user saves, roll back safely, and write state last. | | `src/plan-trust.js` | Audit trust-gate tests. | Must fail closed: an audit re-run of a command never executed or approved on this machine must refuse without executing. | -| `src/matrix-gate.js` | Matrix gate golden tests. | Must validate compatible matrix gate shape through Superloopy artifacts only. | +| `src/matrix-gate.js` | `test/golden-matrix-gate.test.js`, `test/golden-scoped-target-platforms.test.js`. | Must preserve exact unscoped legacy literals while new scoped surface rows require a portable `target {id, platform, environment}` whose slug rejects aggregate vague tokens, plus owner/claims/reason, one platform-aware target and owner slice per row, claim-compatible proof, and resolved-path independence between different scoped surface slices. Adversarial and coverage rows reference proof without declaring another slice; coverage additionally requires same-`contractRef` binding and rejects duplicates, empty adversarial proof, artifact-only coverage, or hidden not-applicable proof. | | `src/model-catalog.js` | `test/model-catalog.test.js`. | Must use only bounded `initialize` plus paginated `model/list`, normalize complete capabilities, sanitize failures, await bounded TERM-to-KILL cleanup, and never start a thread, turn, or prompt. | | `src/model-policy.js` | `test/model-resolution.test.js`, `test/doctor.test.js`. | Must validate ordered complete candidate tuples and the declared second-position compatibility model, resolve the first fully supported tuple per profile, and fail repository doctor when policy docs or bundled pins drift. | | `src/model-resolution.js` | `test/model-resolution-cache.test.js`, installed-policy tests. | Must reuse valid matching state for under 24 hours; refresh on missing/aged/policy/target/explicit causes; preserve valid state on unknown refresh; choose policy compatibility on unknown first probe; and perform no writes. | @@ -474,7 +481,7 @@ Total: 100 points. | `src/prove.js` | CLI evidence tests. | Must record command evidence against the active unresolved criterion and return the next guide. | | `src/receipt.js` | `test/host-adapter.test.js`, hook tests. | Must recover the worker receipt from last_assistant_message when present, else from the decoded final turn of the subagent transcript, failing closed (re-prompt) when the final message exceeds the tail window, host-agnostically. | | `src/report.js` | Report and CLI evidence tests. | Must write portable evidence reports with summary counts, warnings, timestamps, artifacts, timeline, and next action. | -| `src/review-gate.js` | Review gate golden tests. | Must validate strict five-section review gate shape through Superloopy artifacts only. | +| `src/review-gate.js` | `test/golden-review-gate.test.js`, `test/golden-scoped-target-platforms.test.js`. | Must preserve strict five-section shape and exact unscoped legacy literals while new scoped surface rows require a portable `target {id, platform, environment}` whose slug rejects aggregate vague tokens, plus owner/claims/reason, one platform-aware target and owner slice per row, claim-compatible proof, and resolved-path independence between different scoped surface slices. Adversarial rows reference proof without declaring another slice; the gate also rejects screenshot-only promoted native capabilities, incompatible HTTP/data proof, empty adversarial evidence, and duplicate IDs. | | `src/source-checkout.js` | `test/doctor-packed.test.js`, `test/file-audit.test.js`. | Must classify own-`.git` roots and tracked monorepo subdirectories as source checkouts and packed/ignored install roots as installs, so no enclosing repo answers for an install. | | `src/spawn-command.js` | `test/auto-update.test.js`. | Must route npm/npx through Windows `.cmd` shims and leave other commands unchanged. | | `src/store.js` | Loop, hook, and scoped-session tests. | Must normalize sessions, isolate `.superloopy/` state, write JSON atomically, and append ledger entries. | @@ -489,12 +496,16 @@ Total: 100 points. | `test/cli.test.js` | `npm test`. | Must cover managed Codex installs, user-file conflict/force behavior, model control help, pre-mutation help exits, symlinked execution, and existing loop smoke paths. | | `test/concurrency.test.js` | `npm test`. | Must prove shared locks serialize/reclaim/fail closed and managed rollback never deletes a same-content replacement inode or hides backup-cleanup failure. | | `test/crew-lines.test.js` | `npm test`. | Must prove crew completion lines are original deterministic localized presentation, pending/unknown lanes stay silent, and CLI status remains visible. | -| `test/docs.test.js` | `npm test`. | Must keep README, skill, gate notes, design audit, and this golden set aligned with enforced behavior. | -| `test/frontend-qt-contract.test.js` | `node --test test/frontend-qt-contract.test.js`. | Must prove explicit web/Qt routing, web-only gates, Qt ownership/style contracts, and native Qt validation. | +| `test/docs.test.js` | `npm test`. | Must keep localized interactive-content scope, structured gate target/artifact guidance, target-aware audits/goldens, evidence-helper inventory, design authority, dependency approval/action boundaries, and the Qt Kanban prototype boundary aligned with enforced behavior. | +| `test/frontend-qt-contract.test.js` | `node --test test/frontend-qt-contract.test.js`. | Must prove explicit routing, Qt ownership/style contracts, native validation, and the absence of dead Kanban navigation affordances or fake success UI. | +| `test/frontend-quality-overlays.test.js` | `node --test test/frontend-quality-overlays.test.js`. | Must prove claim-triggered overlay routing stays independent from target routing, generic disclosure and horizontal-scroll exceptions remain explicit with audit responsibilities, spatial and temporal contracts remain proportional and cross-platform, visual references carry bounded authority, judgment/usability/debt records are risk-selected, visual heuristics do not become universal blockers, and StyleGallery provenance forbids copied material or dependencies. | +| `test/frontend-routing-scenarios.test.js` | `node --test test/frontend-routing-scenarios.test.js`. | Must prove exact route sets for browser, PWA/extension, embedded desktop/mobile, native/custom, Qt desktop/mobile/WebAssembly, mobile/tablet, and mixed targets plus explicit exclusions, proportional artifacts, standalone/active-loop evidence ownership, and external-project evidence-root confinement. | +| `test/frontend-ux-contract.test.js` | `node --test test/frontend-ux-contract.test.js`. | Must prove explicit supported scope, fact-based route composition, shared capability truth, cross-frontend reachability/handoff, target-aware proportional Web/platform evidence, project-native authoritative design-source handling, and measured-quality privacy/reproducibility. | +| `test/frontend-ux-semantics.test.js` | `node --test test/frontend-ux-semantics.test.js`. | Must prove resource identity and lifecycle attribution, owner-backed reset provenance, distinct-purpose information hierarchy, proportional disclosure, and scoped guards against universal locator, copy-workflow, round-trip, dense-content, or label reversals. | | `test/doctor-packed.test.js` | `npm test`. | Must prove `doctor --json` reports ok against an npm-pack-shaped root (no `.git`, `.gitignore`, or `package-lock.json`) run from an arbitrary cwd, including a root nested in a parent Git repository that ignores it. | | `test/doctor-review-feedback.test.js` | `npm test`. | Must cover review-requested doctor hardening for broken checkout manifests and non-directory skills paths. | | `test/doctor.test.js` | `npm test`. | Must cover repository policy plus absent installed-routing summary alongside package, audits, comparison, generated install metadata, and reviewability. | -| `test/ds-compliance.test.js` | `npm test`. | Must verify DESIGN.md token parsing and undeclared-color/off-scale-spacing detection. | +| `test/ds-compliance.test.js` | `npm test`. | Must verify DESIGN.md parsing, 3/4/6/8-digit color normalization, negative/fractional spacing, exact property matching, multiline declarations and value-line reporting, partial-match avoidance, allowed 0/1px, CLI JSON, and non-zero violation status. | | `test/engineer.test.js` | `npm test`. | Must keep team/crew escalation parsing strict and inject the crew fan-out directive only on `loopy team`. | | `test/file-audit.test.js` | `npm test`. | Must prove the file-audit verifier fails stale inventory rows. | | `test/fleet.test.js` | `npm test`. | Must prove verdict normalization, artifact-bound accept verdicts, handoff recording/update, fleet reconciliation, crew-line decoration, and the parallel-cap warning. | @@ -506,8 +517,8 @@ Total: 100 points. | `test/golden-helpers.js` | Golden tests. | Must provide Superloopy-owned fixtures. | | `test/helpers/trust-isolate.js` | `npm test`. | Must isolate the plan-trust ledger to a temp dir so tests never touch the real user-home trust store. | | `test/golden-hooks.test.js` | `npm test`. | Must keep hook continuation, scoped state, steering, receipts, and manifest behavior strict. | -| `test/golden-matrix-gate.test.js` | `npm test`. | Must keep Superloopy's `@goal` and matrix compatibility strict. | -| `test/golden-review-gate.test.js` | `npm test`. | Must keep strict five-section review-gate acceptance and rejection behavior. | +| `test/golden-matrix-gate.test.js` | `npm test`. | Must keep `@goal`, structured singular surface targets, exact unscoped legacy exceptions, owner/claim proportional proof, composite ownership, cross-surface-slice resolved-path independence, adversarial/coverage reference inheritance, HTTP/data boundaries, contract binding, unique IDs, adversarial evidence, and not-applicable behavior strict. | +| `test/golden-review-gate.test.js` | `npm test`. | Must keep five-section compatibility plus structured singular surface targets, exact unscoped legacy exceptions, owner/claim proportional proof, composite ownership, cross-surface-slice resolved-path independence, adversarial reference inheritance, HTTP/data boundaries, unique IDs, and adversarial evidence strict. | | `test/hooks.test.js` | `npm test`. | Must prove SessionStart reuses a fresh managed manifest without querying or rewriting state, requests restart after definition changes, and preserves existing guard/receipt/steering behavior. | | `test/host-adapter.test.js` | `npm test`. | Must verify direct and transcript-fallback receipt recovery for both evidence and audit receipts, including trailing-newline, oversized-final-message, tool-use-only, and stale-token cases. | | `test/humanize-korean.test.js` | `node --test test/humanize-korean.test.js`. | Must prove the Korean humanizer audit script accepts preserved Korean output and rejects non-Korean or token-dropping output. | diff --git a/examples/qt-kanban/DESIGN.md b/examples/qt-kanban/DESIGN.md index bde1668..0e5d91d 100644 --- a/examples/qt-kanban/DESIGN.md +++ b/examples/qt-kanban/DESIGN.md @@ -4,6 +4,10 @@ Cool-paper planning desk. Dense information remains breathable through hairline structure, precise alignment, and tonal separation. Cobalt marks selection and action; coral and green are semantic only. No gradients, glow, glass, oversized pills, or decorative shadows. +## Acceptance Boundary + +This is a prototype acceptance fixture, not production-editor proof. Board is the only sidebar destination and remains fully functional: its idempotent return to Board overview command clears selected task and detail state, closes overlay detail when open, leaves Board visible and current, and restores focus to Board. Timeline and Inbox are passive demo-only context with static-text accessibility semantics; they do not accept pointer, keyboard, shortcut, or accessibility press actions and do not announce success. Settings and Help are absent because this fixture has no meaningful implementation for them. Persistence and Undo remain out of scope: task mutations live only in the deterministic in-memory store and must not be presented as surviving an application restart. + ## Color | Token | Value | Role | @@ -57,17 +61,17 @@ Use the inherited platform application font and scale every role from its effect Use a 4 px base scale: 4, 8, 12, 16, 20, 24, and 32. Sidebar width is 224 px, persistent detail drawer width is 300 px, board gutter is 12 px, column width is 244-280 px, card padding is 14 px, and focus gutter is 4 px. Radii are 6 px for controls/tags and 9 px for cards/panels. Borders are 1 px. -At 1560 px and wider, show the 224 px sidebar and persistent 300 px detail drawer. From 1180-1559 px, keep the full sidebar and overlay the drawer. From 900-1179 px, use a 72 px icon-and-initial sidebar and overlay the drawer. The header measures the title, search, filter, collaborators, action, and gaps at their real implicit widths; it uses two rows whenever that measured one-row requirement exceeds the available width and grows to its truthful implicit height. Overlay drawers use the scrim, contain focus, and restore focus to the current task card even if a move recreated that delegate. Focus restoration first reveals the task's horizontal board column and vertical card viewport in both LTR and RTL. The minimum supported window size is 900 x 640; the board scrolls horizontally rather than crushing columns. +At 1560 px and wider, show the 224 px sidebar and persistent 300 px detail drawer. From 1180-1559 px, keep the full sidebar and overlay the drawer. From 900-1179 px, use a 72 px icon-and-initial sidebar and overlay the drawer only at the default 13 px base font and while every translated passive label fits within two compact lines. Enlarged text or longer localized copy reflows to the 224 px sidebar so content remains readable without shrinking away the requested text scale, truncating, or overflowing vertically. The header measures the title, search, filter, collaborators, action, and gaps at their real implicit widths; it uses two rows whenever that measured one-row requirement exceeds the available width and grows to its truthful implicit height. Overlay drawers use the scrim, contain focus, and restore focus to the current task card even if a move recreated that delegate. Focus restoration first reveals the task's horizontal board column and vertical card viewport in both LTR and RTL. Sequential keyboard traversal reveals each task and Add task action inside both the horizontal board and the column's vertical viewport. The minimum supported window size is 900 x 640; the board scrolls horizontally rather than crushing columns. ## Components and States -- Sidebar: workspace identity, Board/Timeline/Inbox navigation, team initials, settings, and help. Only Board is functional; other entries show a restrained status message and make a polite accessibility announcement. +- Sidebar: workspace identity, a functional Board return-to-overview command, passive demo-only Timeline/Inbox context, and team initials. Board clears task/detail selection, closes overlay detail, keeps the board current, and restores its own focus; the passive entries remain visible but are not buttons, focus stops, or destinations. - Header: board title, date range, search, priority filter, collaborators, and cobalt New task action. - Column: title, store-derived count, fixed full-width stage accent, drop target, task stack, and Add task affordance. The accent communicates workflow stage, never fake progress. - Card: variable-height title/content, priority, due date, comments/checklist, assignee initials, selected outline, keyboard focus, and drag state. Cards, columns, header controls, drawer, and dialog expose stable `objectName` values for tests. -- Detail drawer: selected task title, status, assignee, due date, checklist, activity, and close action. +- Detail drawer: selected task title, status, assignee, due date, checklist, activity, and close action. The modal variant also exposes a focus-trapped Back to Board action for pointer, keyboard, and accessibility activation; it clears selection, closes the drawer, and restores focus to the real Board control. - New task dialog: title, column, priority, default Create action, cancel action, contained focus, and assertively announced validation. -- Icon family: local monochrome SVGs at 16 and 20 px, 1.75 px stroke, round caps/joins, and consistent optical bounds. Controls tint icons from Theme tokens and expose accessible names. Do not mix Unicode symbol icons, emoji, or unrelated icon styles. +- Icon family: local monochrome SVGs at 16 and 20 px, 1.75 px stroke, round caps/joins, and consistent optical bounds. Interactive controls tint icons from Theme tokens and expose accessible names; passive demo icons are decorative children of one named static-text item. Do not mix Unicode symbol icons, emoji, or unrelated icon styles. Every interactive control implements normal, hover where available, pressed, keyboard-focus, disabled, and selected/checked states as applicable. Primary actions use `primaryHover` and `primaryPressed`; `focus` is reserved for keyboard focus. Focus and selection are distinct: selection uses the cobalt outline; keyboard focus adds an outer `focus` ring with a 2 px gap inside a reserved clipping-safe gutter, while the dark sidebar uses `sidebarFocus` for at least 3:1 non-text contrast. State precedence is disabled, drag, keyboard focus, selected, pressed, hover, normal. Custom card interaction exposes accessible button semantics, selectable/selected state, and Enter/Space activation. Decorative avatar and initial glyphs are ignored by accessibility because equivalent names already belong to their controls or card descriptions. diff --git a/examples/qt-kanban/src/Northstar/Kanban/BoardView.qml b/examples/qt-kanban/src/Northstar/Kanban/BoardView.qml index f1351f2..5738c8e 100644 --- a/examples/qt-kanban/src/Northstar/Kanban/BoardView.qml +++ b/examples/qt-kanban/src/Northstar/Kanban/BoardView.qml @@ -10,6 +10,8 @@ Flickable { signal addTaskRequested(string columnId, Item invoker) property bool rightToLeft: false + property var focusBeforeTasks: null + property var focusAfterTasks: null property bool _viewportInitialized: false property bool _viewportUpdateScheduled: false property bool _trackingLogicalStart: true @@ -182,6 +184,61 @@ Flickable { return false } + function focusFirstTask() { + if (columnDefinitions.length === 0) + return false + + const firstColumn = columnDefinitions[0] + const tasks = TaskStore.visibleInColumn(firstColumn.key) + return tasks.length > 0 + ? focusTask(tasks[0].id) + : focusBoardControl("addTask-" + firstColumn.key) + } + + function focusLastControl() { + if (columnDefinitions.length === 0) + return false + return focusBoardControl("addTask-" + + columnDefinitions[columnDefinitions.length - 1].key) + } + + function focusBoardControl(controlId) { + const addTaskPrefix = "addTask-" + if (controlId.indexOf(addTaskPrefix) !== 0) + return focusTask(controlId) + + const columnId = controlId.slice(addTaskPrefix.length) + for (let index = 0; index < columnRepeater.count; ++index) { + const column = columnRepeater.itemAt(index) as KanbanColumn + if (column && column.columnId === columnId) { + revealColumn(column) + return column.focusAddTask() + } + } + return false + } + + function focusAdjacentTask(controlId, direction) { + const orderedControlIds = [] + for (const definition of columnDefinitions) { + const tasks = TaskStore.visibleInColumn(definition.key) + for (const task of tasks) + orderedControlIds.push(task.id) + orderedControlIds.push("addTask-" + definition.key) + } + + const currentIndex = orderedControlIds.indexOf(controlId) + const targetIndex = currentIndex + direction + if (currentIndex >= 0 && targetIndex >= 0 + && targetIndex < orderedControlIds.length) + return focusBoardControl(orderedControlIds[targetIndex]) + if (direction < 0 && focusBeforeTasks) + return focusBeforeTasks() + if (direction > 0 && focusAfterTasks) + return focusAfterTasks() + return false + } + clip: true interactive: !dragActive contentWidth: Math.max(width, boardContentWidth) @@ -216,6 +273,9 @@ Flickable { title: modelData.title accent: modelData.accent dragCoordinator: root + focusAdjacentControl: (taskId, direction) => + root.focusAdjacentTask(taskId, + direction) onTaskActivated: (taskId, invoker) => root.taskActivated(taskId, invoker) onAddTaskRequested: (columnId, invoker) => diff --git a/examples/qt-kanban/src/Northstar/Kanban/CMakeLists.txt b/examples/qt-kanban/src/Northstar/Kanban/CMakeLists.txt index cb3bd1e..b315e6e 100644 --- a/examples/qt-kanban/src/Northstar/Kanban/CMakeLists.txt +++ b/examples/qt-kanban/src/Northstar/Kanban/CMakeLists.txt @@ -29,10 +29,8 @@ qt_add_qml_module(qtkanban_ui assets/icons/board.svg assets/icons/close.svg assets/icons/filter.svg - assets/icons/help.svg assets/icons/inbox.svg assets/icons/search.svg - assets/icons/settings.svg assets/icons/timeline.svg ) diff --git a/examples/qt-kanban/src/Northstar/Kanban/DetailDrawer.qml b/examples/qt-kanban/src/Northstar/Kanban/DetailDrawer.qml index 0e536fe..d0f8728 100644 --- a/examples/qt-kanban/src/Northstar/Kanban/DetailDrawer.qml +++ b/examples/qt-kanban/src/Northstar/Kanban/DetailDrawer.qml @@ -9,6 +9,7 @@ Control { property bool closeVisible: true property bool trapFocus: false + property var focusPreviousControl: null readonly property int storeRevision: TaskStore.revision readonly property var task: { const ignoredRevision = storeRevision @@ -20,6 +21,7 @@ Control { : -1 signal closeRequested() + signal boardOverviewRequested(Item invoker) implicitWidth: Theme.drawerWidth padding: Theme.space5 @@ -48,10 +50,19 @@ Control { } function focusFirst() { - if (closeButton.visible) + if (closeButton.visible) { closeButton.forceActiveFocus(Qt.TabFocusReason) - else if (moveToColumn.enabled) + return closeButton.activeFocus + } + if (boardOverviewButton.visible) { + boardOverviewButton.forceActiveFocus(Qt.TabFocusReason) + return boardOverviewButton.activeFocus + } + if (moveToColumn.enabled) { moveToColumn.forceActiveFocus(Qt.TabFocusReason) + return moveToColumn.activeFocus + } + return false } function firstEnabledMoveButton() { @@ -106,10 +117,16 @@ Control { Accessible.name: text focusPolicy: Qt.StrongFocus onClicked: root.closeRequested() - KeyNavigation.tab: root.trapFocus && moveToColumn.enabled - ? moveToColumn : null + KeyNavigation.tab: root.trapFocus ? boardOverviewButton + : moveToColumn.enabled + ? moveToColumn : null KeyNavigation.backtab: root.trapFocus ? root.lastEnabledMoveControl() : null + Keys.onBacktabPressed: event => { + event.accepted = !root.trapFocus + && root.focusPreviousControl + && root.focusPreviousControl() + } background: Rectangle { color: closeButton.down ? Theme.pressed @@ -121,6 +138,36 @@ Control { } } + Button { + id: boardOverviewButton + objectName: "detailBoardOverviewButton" + visible: root.trapFocus + Layout.fillWidth: true + text: qsTr("Back to Board") + icon.source: Qt.resolvedUrl("assets/icons/board.svg") + icon.color: Theme.ink + icon.width: 18 + icon.height: 18 + palette.buttonText: Theme.ink + Accessible.name: text + focusPolicy: Qt.StrongFocus + onClicked: root.boardOverviewRequested(boardOverviewButton) + KeyNavigation.tab: moveToColumn.enabled ? moveToColumn + : closeButton + KeyNavigation.backtab: closeButton + + background: Rectangle { + color: boardOverviewButton.down ? Theme.pressed + : boardOverviewButton.hovered ? Theme.hover + : Theme.surface + radius: Theme.controlRadius + border.color: boardOverviewButton.visualFocus + ? Theme.focus : Theme.controlBorder + border.width: boardOverviewButton.visualFocus + ? 2 : Theme.borderWidth + } + } + Item { id: emptyState objectName: "detailEmptyState" @@ -307,7 +354,8 @@ Control { TaskStore.columnOrder[index]) } KeyNavigation.tab: root.firstEnabledMoveButton() - KeyNavigation.backtab: closeButton + KeyNavigation.backtab: root.trapFocus + ? boardOverviewButton : closeButton background: Rectangle { color: moveToColumn.enabled ? Theme.surface : Theme.disabledSurface diff --git a/examples/qt-kanban/src/Northstar/Kanban/KanbanColumn.qml b/examples/qt-kanban/src/Northstar/Kanban/KanbanColumn.qml index f96158d..0f87f55 100644 --- a/examples/qt-kanban/src/Northstar/Kanban/KanbanColumn.qml +++ b/examples/qt-kanban/src/Northstar/Kanban/KanbanColumn.qml @@ -14,6 +14,7 @@ Item { required property string title required property color accent property var dragCoordinator: null + property var focusAdjacentControl: null property var visibleTasks: [] readonly property int visibleCount: visibleTasks.length @@ -24,6 +25,7 @@ Item { required property var modelData property var dragCoordinator: null + property var focusAdjacentControl: null readonly property string delegateTaskId: modelData.id signal activated(string taskId, Item invoker) @@ -43,6 +45,7 @@ Item { width: cardDelegate.width - Theme.focusGutter * 2 taskId: cardDelegate.modelData.id dragCoordinator: cardDelegate.dragCoordinator + focusAdjacentControl: cardDelegate.focusAdjacentControl onActivated: (taskId, invoker) => cardDelegate.activated(taskId, invoker) } @@ -90,6 +93,12 @@ Item { return false } + function focusAddTask() { + revealDelegate(addTaskButton) + addTaskButton.forceActiveFocus(Qt.TabFocusReason) + return addTaskButton.activeFocus + } + onColumnIdChanged: refreshVisibleTasks() Component.onCompleted: refreshVisibleTasks() @@ -193,6 +202,7 @@ Item { delegate: CardDelegate { width: cardScroll.availableWidth dragCoordinator: root.dragCoordinator + focusAdjacentControl: root.focusAdjacentControl onActivated: (activatedTaskId, invoker) => { TaskStore.selectTask(activatedTaskId) root.taskActivated(activatedTaskId, invoker) @@ -210,6 +220,16 @@ Item { font.pixelSize: Theme.bodyFontPixelSize font.weight: Theme.labelFontWeight onClicked: root.addTaskRequested(root.columnId, addTaskButton) + Keys.onTabPressed: event => { + event.accepted = root.focusAdjacentControl + && root.focusAdjacentControl( + addTaskButton.objectName, 1) + } + Keys.onBacktabPressed: event => { + event.accepted = root.focusAdjacentControl + && root.focusAdjacentControl( + addTaskButton.objectName, -1) + } contentItem: Label { text: addTaskButton.text diff --git a/examples/qt-kanban/src/Northstar/Kanban/KanbanHeader.qml b/examples/qt-kanban/src/Northstar/Kanban/KanbanHeader.qml index 7d04434..061bd5b 100644 --- a/examples/qt-kanban/src/Northstar/Kanban/KanbanHeader.qml +++ b/examples/qt-kanban/src/Northstar/Kanban/KanbanHeader.qml @@ -9,6 +9,9 @@ Control { signal newTaskRequested(Item invoker) + property var focusNextControl: null + property var focusPreviousControl: null + readonly property real singleRowRequiredWidth: Math.max(176, headerIdentity.implicitWidth) + Math.max(176, searchField.implicitWidth) @@ -102,6 +105,11 @@ Control { font.pixelSize: Theme.bodyFontPixelSize Accessible.name: qsTr("Search tasks") onTextEdited: TaskStore.query = text + KeyNavigation.tab: priorityFilter + Keys.onBacktabPressed: event => { + event.accepted = root.focusPreviousControl + && root.focusPreviousControl() + } background: Rectangle { color: Theme.surface @@ -129,6 +137,8 @@ Control { } font.pixelSize: Theme.bodyFontPixelSize Accessible.name: qsTr("Priority filter") + KeyNavigation.tab: newTaskButton + KeyNavigation.backtab: searchField onActivated: index => { TaskStore.priorityFilter = index === 1 ? "high" : index === 2 ? "medium" @@ -202,6 +212,11 @@ Control { palette.buttonText: Theme.cobaltContent Accessible.name: text onClicked: root.newTaskRequested(newTaskButton) + KeyNavigation.backtab: priorityFilter + Keys.onTabPressed: event => { + event.accepted = root.focusNextControl + && root.focusNextControl() + } background: Rectangle { color: newTaskButton.enabled diff --git a/examples/qt-kanban/src/Northstar/Kanban/KanbanView.qml b/examples/qt-kanban/src/Northstar/Kanban/KanbanView.qml index ad7f79d..34e7c80 100644 --- a/examples/qt-kanban/src/Northstar/Kanban/KanbanView.qml +++ b/examples/qt-kanban/src/Northstar/Kanban/KanbanView.qml @@ -9,7 +9,10 @@ Item { objectName: "kanbanView" property bool rightToLeft: Qt.locale().textDirection === Qt.RightToLeft + readonly property bool compactSidebarFitsText: + Theme.baseFontPixelSize <= 13 && !sidebar.requiresWideLayout readonly property int sidebarWidth: width < Theme.compactSidebarBreakpoint + && compactSidebarFitsText ? Theme.sidebarCompact : Theme.sidebarWide readonly property bool drawerPersistent: width >= Theme.persistentDrawerBreakpoint readonly property bool drawerOverlay: !drawerPersistent @@ -18,12 +21,6 @@ Item { LayoutMirroring.enabled: rightToLeft LayoutMirroring.childrenInherit: true - function showStatus(message) { - statusToast.text = message - statusToast.Accessible.announce(message, Accessible.Polite) - statusTimer.restart() - } - function restoreFocus(item) { if (!item) return @@ -43,6 +40,19 @@ Item { newTaskDialog.openFrom(invoker) } + function returnToBoardOverview(invoker) { + if (overlayDetailDrawer.opened) { + overlayDetailDrawer.returnFocusToBoard = true + overlayDetailDrawer.close() + return + } + TaskStore.clearSelection() + if (invoker) + restoreFocus(invoker) + else + sidebar.focusBoard() + } + onDrawerPersistentChanged: { if (drawerPersistent && overlayDetailDrawer.opened) overlayDetailDrawer.close() @@ -58,13 +68,14 @@ Item { spacing: 0 Sidebar { + id: sidebar objectName: "sidebar" compact: root.sidebarWidth === Theme.sidebarCompact Layout.preferredWidth: root.sidebarWidth Layout.minimumWidth: root.sidebarWidth Layout.maximumWidth: root.sidebarWidth Layout.fillHeight: true - onUnavailableDestinationRequested: message => root.showStatus(message) + onBoardRequested: invoker => root.returnToBoardOverview(invoker) } ColumnLayout { @@ -77,6 +88,12 @@ Item { id: kanbanHeader objectName: "kanbanHeader" Layout.fillWidth: true + focusNextControl: function() { + return boardView.focusFirstTask() + } + focusPreviousControl: function() { + return sidebar.focusBoard() + } onNewTaskRequested: invoker => root.openNewTask("backlog", invoker) } @@ -85,6 +102,13 @@ Item { id: boardView objectName: "boardView" rightToLeft: root.rightToLeft + focusBeforeTasks: function() { + return kanbanHeader.focusNewTask() + } + focusAfterTasks: function() { + return root.drawerPersistent + ? persistentDetailDrawer.focusFirst() : false + } Layout.fillWidth: true Layout.fillHeight: true onTaskActivated: (taskId, invoker) => { @@ -110,6 +134,9 @@ Item { objectName: "persistentDetailDrawer" anchors.fill: parent closeVisible: true + focusPreviousControl: function() { + return boardView.focusLastControl() + } onCloseRequested: TaskStore.clearSelection() } } @@ -120,6 +147,7 @@ Item { objectName: "overlayDetailDrawer" property Item invokingItem: null property string invokingTaskId: "" + property bool returnFocusToBoard: false parent: Overlay.overlay edge: root.rightToLeft ? Qt.LeftEdge : Qt.RightEdge @@ -160,9 +188,17 @@ Item { onClosed: { const item = invokingItem const taskId = invokingTaskId + const shouldReturnToBoard = returnFocusToBoard invokingItem = null invokingTaskId = "" + returnFocusToBoard = false + if (shouldReturnToBoard) + TaskStore.clearSelection() Qt.callLater(function() { + if (shouldReturnToBoard) { + sidebar.focusBoard() + return + } if (taskId.length > 0 && boardView.focusTask(taskId)) return if (item) { @@ -189,6 +225,8 @@ Item { closeVisible: true trapFocus: true onCloseRequested: overlayDetailDrawer.close() + onBoardOverviewRequested: invoker => + root.returnToBoardOverview(invoker) } } @@ -208,30 +246,4 @@ Item { onActivated: TaskStore.moveSelectedAdjacent(root.rightToLeft ? -1 : 1) } - Label { - id: statusToast - objectName: "statusToast" - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: Theme.space5 - z: 10 - visible: text.length > 0 - padding: Theme.space3 - color: Theme.sidebarText - font.pixelSize: Theme.labelFontPixelSize - font.weight: Theme.labelFontWeight - - background: Rectangle { - color: Theme.sidebarActive - radius: Theme.controlRadius - border.color: Theme.borderStrong - border.width: Theme.borderWidth - } - } - - Timer { - id: statusTimer - interval: 2400 - onTriggered: statusToast.text = "" - } } diff --git a/examples/qt-kanban/src/Northstar/Kanban/Sidebar.qml b/examples/qt-kanban/src/Northstar/Kanban/Sidebar.qml index 85e7bbe..c9179ea 100644 --- a/examples/qt-kanban/src/Northstar/Kanban/Sidebar.qml +++ b/examples/qt-kanban/src/Northstar/Kanban/Sidebar.qml @@ -8,7 +8,20 @@ Control { id: root property bool compact: false - signal unavailableDestinationRequested(string message) + readonly property real compactLabelLineWidth: + Theme.sidebarCompact - 2 * padding + readonly property bool requiresWideLayout: + timelineCompactTitleProbe.lineCount > 2 + || timelineCompactStatusProbe.lineCount > 2 + || inboxCompactTitleProbe.lineCount > 2 + || inboxCompactStatusProbe.lineCount > 2 + + signal boardRequested(Item invoker) + + function focusBoard() { + boardButton.forceActiveFocus(Qt.TabFocusReason) + return boardButton.activeFocus + } padding: Theme.space3 @@ -16,6 +29,50 @@ Control { color: Theme.sidebar } + Text { + id: timelineCompactTitleProbe + visible: false + width: root.compactLabelLineWidth + font.pixelSize: Theme.metaFontPixelSize + font.weight: Theme.sectionFontWeight + wrapMode: Text.Wrap + text: timelineDemoItem.title + Accessible.ignored: true + } + + Text { + id: timelineCompactStatusProbe + visible: false + width: root.compactLabelLineWidth + font.pixelSize: Theme.metaFontPixelSize + font.weight: Theme.metaFontWeight + wrapMode: Text.Wrap + text: timelineDemoItem.demoOnlyLabel + Accessible.ignored: true + } + + Text { + id: inboxCompactTitleProbe + visible: false + width: root.compactLabelLineWidth + font.pixelSize: Theme.metaFontPixelSize + font.weight: Theme.sectionFontWeight + wrapMode: Text.Wrap + text: inboxDemoItem.title + Accessible.ignored: true + } + + Text { + id: inboxCompactStatusProbe + visible: false + width: root.compactLabelLineWidth + font.pixelSize: Theme.metaFontPixelSize + font.weight: Theme.metaFontWeight + wrapMode: Text.Wrap + text: inboxDemoItem.demoOnlyLabel + Accessible.ignored: true + } + component SidebarAction: Button { id: action @@ -51,6 +108,122 @@ Control { } } + component PassiveDemoItem: Item { + id: demoItem + + required property string title + required property url iconSource + required property string compactObjectPrefix + property string demoOnlyLabel: qsTr("Demo only") + readonly property string accessibleLabel: qsTr("%1 — %2") + .arg(title) + .arg(demoOnlyLabel) + + implicitWidth: root.compact ? 48 : wideContent.implicitWidth + 2 * Theme.space3 + implicitHeight: root.compact + ? Math.max(48, compactContent.implicitHeight + + 2 * Theme.space1) + : Math.max(42, wideContent.implicitHeight + + 2 * Theme.space1) + Layout.minimumWidth: 0 + activeFocusOnTab: false + Accessible.role: Accessible.StaticText + Accessible.name: accessibleLabel + + RowLayout { + id: wideContent + + anchors.fill: parent + anchors.leftMargin: Theme.space3 + anchors.rightMargin: Theme.space3 + anchors.topMargin: Theme.space1 + anchors.bottomMargin: Theme.space1 + visible: !root.compact + spacing: Theme.space3 + + Image { + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + source: demoItem.iconSource + sourceSize.width: 20 + sourceSize.height: 20 + opacity: 0.72 + Accessible.ignored: true + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + Label { + objectName: demoItem.compactObjectPrefix + "WideTitle" + Layout.fillWidth: true + text: demoItem.title + color: Theme.sidebarText + font.pixelSize: Theme.bodyFontPixelSize + font.weight: Theme.bodyFontWeight + wrapMode: Text.Wrap + Accessible.ignored: true + } + + Label { + objectName: demoItem.compactObjectPrefix + "WideStatus" + Layout.fillWidth: true + text: demoItem.demoOnlyLabel + color: Theme.sidebarMutedText + font.pixelSize: Theme.metaFontPixelSize + font.weight: Theme.metaFontWeight + wrapMode: Text.Wrap + Accessible.ignored: true + } + } + } + + ColumnLayout { + id: compactContent + anchors.fill: parent + anchors.topMargin: Theme.space1 + anchors.bottomMargin: Theme.space1 + visible: root.compact + spacing: 0 + + Image { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 + source: demoItem.iconSource + sourceSize.width: 16 + sourceSize.height: 16 + opacity: 0.72 + Accessible.ignored: true + } + + Text { + objectName: demoItem.compactObjectPrefix + "CompactTitle" + Layout.fillWidth: true + text: demoItem.title + color: Theme.sidebarText + font.pixelSize: Theme.metaFontPixelSize + font.weight: Theme.sectionFontWeight + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + Accessible.ignored: true + } + + Text { + objectName: demoItem.compactObjectPrefix + "CompactStatus" + Layout.fillWidth: true + text: demoItem.demoOnlyLabel + color: Theme.sidebarMutedText + font.pixelSize: Theme.metaFontPixelSize + font.weight: Theme.metaFontWeight + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + Accessible.ignored: true + } + } + } + contentItem: ColumnLayout { spacing: Theme.space2 @@ -100,29 +273,31 @@ Control { } SidebarAction { + id: boardButton objectName: "boardButton" Layout.fillWidth: true text: qsTr("Board") icon.source: Qt.resolvedUrl("assets/icons/board.svg") selected: true + onClicked: root.boardRequested(boardButton) } - SidebarAction { - objectName: "timelineButton" + PassiveDemoItem { + id: timelineDemoItem + objectName: "timelineDemoItem" Layout.fillWidth: true - text: qsTr("Timeline") - icon.source: Qt.resolvedUrl("assets/icons/timeline.svg") - onClicked: root.unavailableDestinationRequested( - qsTr("Timeline is not available in this demo")) + title: qsTr("Timeline") + iconSource: Qt.resolvedUrl("assets/icons/timeline.svg") + compactObjectPrefix: "timeline" } - SidebarAction { - objectName: "inboxButton" + PassiveDemoItem { + id: inboxDemoItem + objectName: "inboxDemoItem" Layout.fillWidth: true - text: qsTr("Inbox") - icon.source: Qt.resolvedUrl("assets/icons/inbox.svg") - onClicked: root.unavailableDestinationRequested( - qsTr("Inbox is not available in this demo")) + title: qsTr("Inbox") + iconSource: Qt.resolvedUrl("assets/icons/inbox.svg") + compactObjectPrefix: "inbox" } Item { @@ -177,22 +352,5 @@ Control { } } - SidebarAction { - objectName: "settingsButton" - Layout.fillWidth: true - text: qsTr("Settings") - icon.source: Qt.resolvedUrl("assets/icons/settings.svg") - onClicked: root.unavailableDestinationRequested( - qsTr("Settings are not available in this demo")) - } - - SidebarAction { - objectName: "helpButton" - Layout.fillWidth: true - text: qsTr("Help") - icon.source: Qt.resolvedUrl("assets/icons/help.svg") - onClicked: root.unavailableDestinationRequested( - qsTr("Help is not available in this demo")) - } } } diff --git a/examples/qt-kanban/src/Northstar/Kanban/TaskCard.qml b/examples/qt-kanban/src/Northstar/Kanban/TaskCard.qml index ab3208f..98917ef 100644 --- a/examples/qt-kanban/src/Northstar/Kanban/TaskCard.qml +++ b/examples/qt-kanban/src/Northstar/Kanban/TaskCard.qml @@ -9,6 +9,7 @@ FocusScope { required property string taskId property var dragCoordinator: null + property var focusAdjacentControl: null property bool dragging: false readonly property int storeRevision: TaskStore.revision readonly property var task: { @@ -163,6 +164,14 @@ FocusScope { root.activate() event.accepted = true } + Keys.onTabPressed: event => { + event.accepted = root.focusAdjacentControl + && root.focusAdjacentControl(root.taskId, 1) + } + Keys.onBacktabPressed: event => { + event.accepted = root.focusAdjacentControl + && root.focusAdjacentControl(root.taskId, -1) + } background: Rectangle { color: root.visualState === "disabled" ? Theme.disabledSurface diff --git a/examples/qt-kanban/src/Northstar/Kanban/assets/icons/help.svg b/examples/qt-kanban/src/Northstar/Kanban/assets/icons/help.svg deleted file mode 100644 index 2348da6..0000000 --- a/examples/qt-kanban/src/Northstar/Kanban/assets/icons/help.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/examples/qt-kanban/src/Northstar/Kanban/assets/icons/inbox.svg b/examples/qt-kanban/src/Northstar/Kanban/assets/icons/inbox.svg index 9e4ef7d..03689f9 100644 --- a/examples/qt-kanban/src/Northstar/Kanban/assets/icons/inbox.svg +++ b/examples/qt-kanban/src/Northstar/Kanban/assets/icons/inbox.svg @@ -1,4 +1,5 @@ - + + diff --git a/examples/qt-kanban/src/Northstar/Kanban/assets/icons/settings.svg b/examples/qt-kanban/src/Northstar/Kanban/assets/icons/settings.svg deleted file mode 100644 index 02751de..0000000 --- a/examples/qt-kanban/src/Northstar/Kanban/assets/icons/settings.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/examples/qt-kanban/src/Northstar/Kanban/assets/icons/timeline.svg b/examples/qt-kanban/src/Northstar/Kanban/assets/icons/timeline.svg index c4240da..13ed7ac 100644 --- a/examples/qt-kanban/src/Northstar/Kanban/assets/icons/timeline.svg +++ b/examples/qt-kanban/src/Northstar/Kanban/assets/icons/timeline.svg @@ -1,5 +1,6 @@ - + + diff --git a/examples/qt-kanban/tests/quick/tst_accessibility.qml b/examples/qt-kanban/tests/quick/tst_accessibility.qml index 40a7fb3..bcd5ecd 100644 --- a/examples/qt-kanban/tests/quick/tst_accessibility.qml +++ b/examples/qt-kanban/tests/quick/tst_accessibility.qml @@ -75,11 +75,7 @@ TestCase { function test_icon_only_controls_are_named_and_decoration_is_ignored() { const view = createView(1000) const namedControls = [ - ["boardButton", "Board"], - ["timelineButton", "Timeline"], - ["inboxButton", "Inbox"], - ["settingsButton", "Settings"], - ["helpButton", "Help"] + ["boardButton", "Board"] ] for (const definition of namedControls) { @@ -154,17 +150,26 @@ TestCase { verify(accessibilityProbe.selected(second)) } - function test_unavailable_destination_announces_polite_status() { + function test_demo_context_is_static_and_does_not_announce_success() { const view = createView() - const timeline = findChild(view, "timelineButton") - verify(timeline) + const demoItems = [ + [findChild(view, "timelineDemoItem"), "Timeline — Demo only"], + [findChild(view, "inboxDemoItem"), "Inbox — Demo only"] + ] - timeline.clicked() - tryCompare(accessibilityProbe, "announcementCount", 1) - compare(accessibilityProbe.lastAnnouncement, - "Timeline is not available in this demo") - compare(accessibilityProbe.lastPoliteness, - accessibilityProbe.politePoliteness) + for (const definition of demoItems) { + const item = definition[0] + verify(item) + compare(accessibilityProbe.name(item), definition[1]) + verify(accessibilityProbe.role(item) + !== accessibilityProbe.buttonRole) + verify(!accessibilityProbe.hasPressAction(item)) + verify(!accessibilityProbe.press(item)) + } + + compare(accessibilityProbe.announcementCount, 0) + compare(findChild(view, "settingsButton"), null) + compare(findChild(view, "helpButton"), null) } function test_blank_task_title_announces_assertive_validation() { diff --git a/examples/qt-kanban/tests/quick/tst_interactions.qml b/examples/qt-kanban/tests/quick/tst_interactions.qml index dc4b748..2bd47ea 100644 --- a/examples/qt-kanban/tests/quick/tst_interactions.qml +++ b/examples/qt-kanban/tests/quick/tst_interactions.qml @@ -67,6 +67,16 @@ TestCase { return owner } + function verifyItemInside(item, viewport, label) { + const origin = item.mapToItem(viewport, 0, 0) + verify(origin.x >= -0.5, label + " begins left of its viewport") + verify(origin.y >= -0.5, label + " begins above its viewport") + verify(origin.x + item.width <= viewport.width + 0.5, + label + " exceeds its viewport width") + verify(origin.y + item.height <= viewport.height + 0.5, + label + " exceeds its viewport height") + } + function test_columns_bind_store_visible_arrays_and_counts() { const view = createView() const keys = ["backlog", "ready", "inProgress", "review"] @@ -204,6 +214,68 @@ TestCase { "Define launch goals") } + function test_board_action_returns_from_overlay_details_to_overview_data() { + return [ + { "tag": "pointer", "method": "pointer" }, + { "tag": "keyboard", "method": "keyboard" }, + { "tag": "accessibility", "method": "accessibility" } + ] + } + + function test_board_action_returns_from_overlay_details_to_overview(data) { + const view = createView(1300) + const boardButton = findChild(view, "boardButton") + const board = findChild(view, "boardView") + const overlayDrawer = findChild(view, "overlayDetailDrawer") + const card = findChild(view, "taskCard-task-define-goals") + const interaction = findChild(card, "cardInteraction") + + verify(boardButton) + verify(board) + verify(overlayDrawer) + verify(interaction) + + interaction.forceActiveFocus(Qt.TabFocusReason) + keyClick(Qt.Key_Return) + tryVerify(function() { return overlayDrawer.opened }) + compare(TaskStore.selectedTaskId, "task-define-goals") + + const drawerBoardAction = findChild(overlayDrawer, + "detailBoardOverviewButton") + verify(drawerBoardAction, + "The modal must expose a reachable return-to-Board action") + if (data.method === "pointer") { + mouseClick(drawerBoardAction) + } else if (data.method === "keyboard") { + compare(namedFocusOwner(testCase.Window.window.activeFocusItem), + "detailCloseButton") + keyClick(Qt.Key_Tab) + compare(namedFocusOwner(testCase.Window.window.activeFocusItem), + "detailBoardOverviewButton") + keyClick(Qt.Key_Space) + } else { + verify(accessibilityProbe.hasPressAction(drawerBoardAction)) + verify(accessibilityProbe.press(drawerBoardAction)) + } + + verify(overlayDrawer.visible, + "The animated drawer must remain visible while closing") + compare(TaskStore.selectedTaskId, "task-define-goals", + "Selection must remain stable during the exit animation") + + tryVerify(function() { return !overlayDrawer.opened }) + tryCompare(TaskStore, "selectedTaskId", "") + verify(board.visible) + verify(boardButton.checked) + tryVerify(function() { return boardButton.activeFocus }) + + keyClick(Qt.Key_Space) + compare(TaskStore.selectedTaskId, "") + verify(board.visible) + verify(boardButton.checked) + tryVerify(function() { return boardButton.activeFocus }) + } + function test_move_to_control_and_adjacent_shortcuts() { const view = createView(1600) const details = findChild(view, "persistentDetailDrawer") @@ -510,6 +582,7 @@ TestCase { const drawer = findChild(view, "overlayDetailDrawer") const allowedOwners = [ "detailCloseButton", + "detailBoardOverviewButton", "moveToColumn", "movePreviousButton", "moveNextButton" @@ -540,21 +613,29 @@ TestCase { "detailCloseButton") const moveControl = findChild(drawer, "moveToColumn") + const boardOverviewButton = findChild(drawer, + "detailBoardOverviewButton") const previousButton = findChild(drawer, "movePreviousButton") const nextButton = findChild(drawer, "moveNextButton") + verify(boardOverviewButton) verify(!previousButton.enabled) verify(nextButton.enabled) + compare(findChild(drawer, "detailCloseButton").KeyNavigation.tab, + boardOverviewButton) + compare(boardOverviewButton.KeyNavigation.tab, moveControl) + compare(moveControl.KeyNavigation.backtab, boardOverviewButton) compare(moveControl.KeyNavigation.tab, nextButton) compare(nextButton.KeyNavigation.backtab, moveControl) - const forwardOwners = ["moveToColumn", "moveNextButton", - "detailCloseButton"] + const forwardOwners = ["detailBoardOverviewButton", "moveToColumn", + "moveNextButton", "detailCloseButton"] for (const owner of forwardOwners) { keyClick(Qt.Key_Tab) compare(namedFocusOwner(testCase.Window.window.activeFocusItem), owner) } const reverseOwners = ["moveNextButton", "moveToColumn", + "detailBoardOverviewButton", "detailCloseButton"] for (const owner of reverseOwners) { keyClick(Qt.Key_Backtab) @@ -576,23 +657,30 @@ TestCase { "detailCloseButton") const closeButton = findChild(drawer, "detailCloseButton") + const boardOverviewButton = findChild(drawer, + "detailBoardOverviewButton") const moveControl = findChild(drawer, "moveToColumn") const previousButton = findChild(drawer, "movePreviousButton") const nextButton = findChild(drawer, "moveNextButton") + verify(boardOverviewButton) verify(previousButton.enabled) verify(!nextButton.enabled) compare(closeButton.KeyNavigation.backtab, previousButton) + compare(closeButton.KeyNavigation.tab, boardOverviewButton) + compare(boardOverviewButton.KeyNavigation.tab, moveControl) + compare(moveControl.KeyNavigation.backtab, boardOverviewButton) compare(previousButton.KeyNavigation.tab, closeButton) compare(previousButton.KeyNavigation.backtab, moveControl) - const forwardOwners = ["moveToColumn", "movePreviousButton", - "detailCloseButton"] + const forwardOwners = ["detailBoardOverviewButton", "moveToColumn", + "movePreviousButton", "detailCloseButton"] for (const owner of forwardOwners) { keyClick(Qt.Key_Tab) compare(namedFocusOwner(testCase.Window.window.activeFocusItem), owner) } const reverseOwners = ["movePreviousButton", "moveToColumn", + "detailBoardOverviewButton", "detailCloseButton"] for (const owner of reverseOwners) { keyClick(Qt.Key_Backtab) @@ -626,49 +714,122 @@ TestCase { tryVerify(function() { return start.activeFocus }) const forwardOwners = [ - "timelineButton", "inboxButton", "settingsButton", "helpButton", "searchField", "priorityFilter", "newTaskButton", "taskCard-task-define-goals", "taskCard-task-audience-research", "taskCard-task-localize-launch", + "addTask-backlog", "taskCard-task-finalize-messaging", "taskCard-task-design-system", "taskCard-task-email-campaign", + "addTask-ready", "taskCard-task-build-landing", "taskCard-task-integrate-analytics", "taskCard-task-mobile-qa", + "addTask-inProgress", "taskCard-task-demo-video", "taskCard-task-go-to-market", "taskCard-task-launch-readiness-review", + "addTask-review", "detailCloseButton", "moveToColumn", "movePreviousButton", "moveNextButton" ] - let forwardIndex = 0 - while (forwardIndex < forwardOwners.length) { + for (const owner of forwardOwners) { keyClick(Qt.Key_Tab) - const owner = namedFocusPath( - testCase.Window.window.activeFocusItem) - if (owner.indexOf("addTask-") === 0) - continue - compare(owner, forwardOwners[forwardIndex]) - ++forwardIndex + compare(namedFocusPath(testCase.Window.window.activeFocusItem), + owner) } const backwardOwners = forwardOwners.slice(0, -1).reverse() backwardOwners.push("boardButton") for (const owner of backwardOwners) { keyClick(Qt.Key_Backtab) - let actualOwner = namedFocusPath( - testCase.Window.window.activeFocusItem) - while (actualOwner.indexOf("addTask-") === 0) { - keyClick(Qt.Key_Backtab) - actualOwner = namedFocusPath( - testCase.Window.window.activeFocusItem) - } - compare(actualOwner, owner) + compare(namedFocusPath(testCase.Window.window.activeFocusItem), + owner) } } + function test_primary_tab_order_keeps_empty_leading_columns() { + const view = createView(1600) + TaskStore.query = "landing" + waitForRendering(view) + + const newTask = findChild(view, "newTaskButton") + verify(newTask) + newTask.forceActiveFocus(Qt.TabFocusReason) + tryVerify(function() { return newTask.activeFocus }) + + const forwardOwners = [ + "addTask-backlog", + "addTask-ready", + "taskCard-task-build-landing" + ] + for (const owner of forwardOwners) { + keyClick(Qt.Key_Tab) + compare(namedFocusPath(testCase.Window.window.activeFocusItem), + owner) + } + } + + function test_tab_reveals_add_task_below_a_long_column() { + const view = createView(900) + const board = findChild(view, "boardView") + let lastTaskId = "" + for (let index = 0; index < 12; ++index) { + lastTaskId = TaskStore.addTask("Additional backlog task " + index, + "backlog", "Low") + } + waitForRendering(view) + + const lastCard = findChild(view, "taskCard-" + lastTaskId) + const lastInteraction = findChild(lastCard, "cardInteraction") + const addTask = findChild(view, "addTask-backlog") + verify(board) + verify(lastInteraction) + verify(addTask) + verify(board.focusTask(lastTaskId)) + tryVerify(function() { return lastInteraction.activeFocus }) + + keyClick(Qt.Key_Tab) + compare(namedFocusPath(testCase.Window.window.activeFocusItem), + "addTask-backlog") + + let verticalViewport = addTask.parent + while (verticalViewport && !verticalViewport.clip) + verticalViewport = verticalViewport.parent + verify(verticalViewport) + verifyItemInside(addTask, verticalViewport, + "Backlog Add task action") + } + + function test_tab_reveals_add_task_in_an_empty_offscreen_column() { + const view = createView(900) + const board = findChild(view, "boardView") + TaskStore.query = "landing" + waitForRendering(view) + + const landingCard = findChild(view, + "taskCard-task-build-landing") + const landingInteraction = findChild(landingCard, "cardInteraction") + const addInProgress = findChild(view, "addTask-inProgress") + const addReview = findChild(view, "addTask-review") + verify(board) + verify(landingInteraction) + verify(addInProgress) + verify(addReview) + verify(board.focusTask("task-build-landing")) + tryVerify(function() { return landingInteraction.activeFocus }) + + keyClick(Qt.Key_Tab) + compare(namedFocusPath(testCase.Window.window.activeFocusItem), + "addTask-inProgress") + keyClick(Qt.Key_Tab) + compare(namedFocusPath(testCase.Window.window.activeFocusItem), + "addTask-review") + verifyItemInside(addReview, board, + "Offscreen Review Add task action") + } + function test_compact_rtl_shortcuts_follow_physical_direction() { const view = createTemporaryObject(viewComponent, testCase, { "width": 1000, diff --git a/examples/qt-kanban/tests/quick/tst_responsive.qml b/examples/qt-kanban/tests/quick/tst_responsive.qml index 20e79db..3f779e9 100644 --- a/examples/qt-kanban/tests/quick/tst_responsive.qml +++ b/examples/qt-kanban/tests/quick/tst_responsive.qml @@ -110,11 +110,9 @@ TestCase { const content = [ findChild(view, "boardButton"), - findChild(view, "timelineButton"), - findChild(view, "inboxButton"), - findChild(view, "teamCluster"), - findChild(view, "settingsButton"), - findChild(view, "helpButton") + findChild(view, "timelineDemoItem"), + findChild(view, "inboxDemoItem"), + findChild(view, "teamCluster") ] for (const item of content) { @@ -127,41 +125,92 @@ TestCase { } } + function test_compact_demo_items_render_distinct_titles_and_status() { + const view = createTemporaryObject( + viewComponent, + testCase.Window.window.contentItem, + { "width": 1000 }) + verify(view) + waitForRendering(view) + + const definitions = [ + ["timelineCompactTitle", "timelineCompactStatus", "Timeline"], + ["inboxCompactTitle", "inboxCompactStatus", "Inbox"] + ] + + for (const definition of definitions) { + const title = findChild(view, definition[0]) + const status = findChild(view, definition[1]) + verify(title, definition[0] + " must be rendered") + verify(status, definition[1] + " must be rendered") + verify(title.visible, definition[0] + " must be visible") + verify(status.visible, definition[1] + " must be visible") + compare(title.text, definition[2]) + compare(status.text, "Demo only") + verify(!title.truncated, + definition[0] + " must show its full distinct title") + verify(title.fontInfo.pixelSize + 0.5 + >= Theme.metaFontPixelSize, + definition[0] + " must not shrink below the requested size") + verify(status.fontInfo.pixelSize + 0.5 + >= Theme.metaFontPixelSize, + definition[1] + " must not shrink below the requested size") + } + } + + function test_default_scale_word_wrapping_reflows_before_three_lines() { + const view = createView(1000) + const sidebar = findChild(view, "sidebar") + const timeline = findChild(view, "timelineDemoItem") + verify(sidebar) + verify(timeline) + verify(sidebar.compact) + + timeline.title = "Wide Wide Wide" + waitForRendering(view) + + verify(!sidebar.compact, + "A three-line compact translation must reflow wide") + compare(view.sidebarWidth, 224) + } + function test_board_navigation_exposes_current_checked_state() { const view = createView(1000) const boardButton = findChild(view, "boardButton") - const timelineButton = findChild(view, "timelineButton") verify(boardButton) - verify(timelineButton) verify(boardButton.checkable) verify(boardButton.checked) compare(boardButton.palette.brightText.toString(), Theme.sidebarText.toString()) - verify(!timelineButton.checkable) - verify(!timelineButton.checked) - compare(timelineButton.palette.buttonText.toString(), - Theme.sidebarText.toString()) boardButton.forceActiveFocus(Qt.TabFocusReason) tryVerify(function() { return boardButton.visualFocus }) compare(boardButton.background.border.color, Theme.sidebarFocus) } - function test_sidebar_unavailable_destinations_share_one_status_surface() { + function test_sidebar_demo_context_is_visible_and_passive() { const view = createView(1300) - const timeline = findChild(view, "timelineButton") - const inbox = findChild(view, "inboxButton") - const status = findChild(view, "statusToast") - verify(timeline) - verify(inbox) - verify(status) + const demoItems = [ + [findChild(view, "timelineDemoItem"), "Timeline"], + [findChild(view, "inboxDemoItem"), "Inbox"] + ] - timeline.clicked() - compare(status.text, "Timeline is not available in this demo") + for (const definition of demoItems) { + const item = definition[0] + verify(item) + verify(item.width > 0 && item.height > 0, + item.objectName + " must retain visible layout space") + verify(item.opacity > 0, + item.objectName + " must not hide its demo context") + compare(item.title, definition[1]) + compare(item.demoOnlyLabel, "Demo only") + verify(!item.activeFocusOnTab) + } - inbox.clicked() - compare(status.text, "Inbox is not available in this demo") + compare(findChild(view, "settingsButton"), null) + compare(findChild(view, "helpButton"), null) + compare(findChild(view, "statusToast"), null) } function test_rtl_mirrors_shell_columns_and_directional_controls() { @@ -312,6 +361,121 @@ TestCase { <= card.height + 0.5) } + function test_passive_demo_copy_scales_and_wraps_data() { + return [ + { "tag": "default-long-reflows-wide", "viewWidth": 1000, + "scale": 1.0, "labelSuffix": "Wide", + "expectedCompact": false, "expectedSidebarWidth": 224 }, + { "tag": "wide-135", "viewWidth": 1300, "scale": 1.35, + "labelSuffix": "Wide", "expectedCompact": false, + "expectedSidebarWidth": 224 }, + { "tag": "wide-200", "viewWidth": 1300, "scale": 2.0, + "labelSuffix": "Wide", "expectedCompact": false, + "expectedSidebarWidth": 224 }, + { "tag": "minimum-135-reflows-wide", "viewWidth": 1000, + "scale": 1.35, "labelSuffix": "Wide", + "expectedCompact": false, "expectedSidebarWidth": 224 }, + { "tag": "minimum-200-reflows-wide", "viewWidth": 1000, + "scale": 2.0, + "labelSuffix": "Wide", "expectedCompact": false, + "expectedSidebarWidth": 224 } + ] + } + + function test_passive_demo_copy_scales_and_wraps(data) { + Theme.baseFontPixelSize = 13 * data.scale + const view = createView(data.viewWidth) + const sidebar = findChild(view, "sidebar") + verify(sidebar) + const definitions = [ + { + "item": findChild(view, "timelineDemoItem"), + "prefix": "timeline", + "title": "Zeitachsenplanung für internationale Produkte", + "status": "Nur für Demonstrationszwecke verfügbar" + }, + { + "item": findChild(view, "inboxDemoItem"), + "prefix": "inbox", + "title": "팀 전체의 읽지 않은 메시지와 검토 요청", + "status": "현재 데모에서만 제공됩니다" + } + ] + + for (const definition of definitions) { + verify(definition.item) + definition.item.title = definition.title + definition.item.demoOnlyLabel = definition.status + } + waitForRendering(view) + compare(sidebar.compact, data.expectedCompact) + compare(view.sidebarWidth, data.expectedSidebarWidth) + + for (const definition of definitions) { + const title = findChild( + view, + definition.prefix + data.labelSuffix + "Title") + const status = findChild( + view, + definition.prefix + data.labelSuffix + "Status") + verify(title, "The localized title must have a stable test name") + verify(status, "The localized status must have a stable test name") + compare(title.text, definition.title) + compare(status.text, definition.status) + compare(definition.item.Accessible.name, + definition.title + " — " + definition.status) + verify(!title.truncated, + title.objectName + " truncates localized copy") + verify(!status.truncated, + status.objectName + " truncates localized copy") + const requestedTitleSize = data.labelSuffix === "Wide" + ? Theme.bodyFontPixelSize + : Theme.metaFontPixelSize + verify(title.fontInfo.pixelSize + 0.5 >= requestedTitleSize, + title.objectName + " shrinks below the requested text scale") + verify(status.fontInfo.pixelSize + 0.5 + >= Theme.metaFontPixelSize, + status.objectName + " shrinks below the requested text scale") + verify(title.height + 0.5 >= title.contentHeight, + title.objectName + " clips scaled localized copy") + verify(status.height + 0.5 >= status.contentHeight, + status.objectName + " clips scaled localized copy") + verify(definition.item.height + 0.5 + >= definition.item.implicitHeight, + definition.item.objectName + " clips its content") + + for (const label of [title, status]) { + const origin = label.mapToItem(definition.item, 0, 0) + verify(origin.x >= -0.5, + label.objectName + " begins outside its item") + verify(origin.y >= -0.5, + label.objectName + " begins above its item") + verify(origin.x + label.width + <= definition.item.width + 0.5, + label.objectName + " exceeds its item width") + verify(origin.y + label.height + <= definition.item.height + 0.5, + label.objectName + " exceeds its item height") + } + } + + const team = findChild(view, "teamCluster") + verify(team) + for (const item of [definitions[0].item, definitions[1].item, team]) { + const origin = item.mapToItem(sidebar, 0, 0) + verify(origin.y >= sidebar.topPadding - 0.5, + item.objectName + " begins above the sidebar") + verify(origin.y + item.height + <= sidebar.height - sidebar.bottomPadding + 0.5, + item.objectName + " exceeds the sidebar height") + } + const timelineOrigin = definitions[0].item.mapToItem(sidebar, 0, 0) + const inboxOrigin = definitions[1].item.mapToItem(sidebar, 0, 0) + verify(timelineOrigin.y + definitions[0].item.height + <= inboxOrigin.y + 0.5, + "Localized passive items overlap") + } + function test_two_hundred_percent_header_stacks_without_overlap() { Theme.baseFontPixelSize = 26 const view = createView(900) diff --git a/package-lock.json b/package-lock.json index 1202e53..45a3ee1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "superloopy", - "version": "0.10.0", + "version": "0.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "superloopy", - "version": "0.10.0", + "version": "0.11.0", "license": "MIT", "bin": { "superloopy": "src/cli.js" diff --git a/package.json b/package.json index 8dd8858..6012eae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "superloopy", - "version": "0.10.0", + "version": "0.11.0", "description": "A lightweight loop harness for Codex and Claude Code with strict evidence gates.", "type": "module", "bin": { diff --git a/skills/superloopy-frontend/SKILL.md b/skills/superloopy-frontend/SKILL.md index 3830e8d..29fa702 100644 --- a/skills/superloopy-frontend/SKILL.md +++ b/skills/superloopy-frontend/SKILL.md @@ -1,6 +1,6 @@ --- name: superloopy-frontend -description: Use only after explicit Codex `$superloopy:superloopy-frontend` or Claude Code `/superloopy:superloopy-frontend` invocation for a web frontend or Qt desktop GUI task, such a task started with a leading `loopy` or `루피`, or an active Superloopy loop explicitly routing a web frontend, Qt Widgets, Qt Quick/QML, or mixed web/Qt subtask here. Do not activate from UI, frontend, Qt, QML, Widgets, or other semantic vocabulary alone, or for non-web/non-Qt visual deliverables. +description: Use only after explicit Codex `$superloopy:superloopy-frontend` or Claude Code `/superloopy:superloopy-frontend` invocation for supported screen-based application UI across browser-hosted Web, interactive deployed content-led Web, desktop, mobile/tablet, embedded/hybrid, Qt, custom-rendered, or mixed targets, such a task started with a leading `loopy` or `루피`, or an active Superloopy loop explicitly routing it here. Do not activate from UI, frontend, desktop, mobile, or framework vocabulary alone, or for TV, wearable, XR, automotive, game UI, TUI, static media/document artifacts, or non-UI work. --- # Superloopy Frontend @@ -9,30 +9,73 @@ description: Use only after explicit Codex `$superloopy:superloopy-frontend` or Open your reply with `SUPERLOOPY FRONTEND ENABLED`. If another active Superloopy mode mandates its own first line, print that first and this marker on the next line. -**Explicit activation only.** Engage when the user invokes `$superloopy:superloopy-frontend` in Codex or `/superloopy:superloopy-frontend` in Claude Code for a web frontend or Qt desktop GUI task, begins such a task with a leading `loopy` or `루피`, or an already-active Superloopy loop explicitly assigns a web frontend, Qt Widgets, Qt Quick/QML, or mixed web/Qt subtask to this skill. A plain mention of UI, frontend, CSS, layout, responsiveness, Qt, QML, Widgets, or a visible symptom is not authorization to activate this workflow. Non-web/non-Qt visual deliverables and backend, API, data, concurrency, or infrastructure work stay with their primary workflows. +**Explicit activation only.** Engage when the user invokes `$superloopy:superloopy-frontend` in Codex or `/superloopy:superloopy-frontend` in Claude Code for a supported screen-based application UI task, begins such a task with a leading `loopy` or `루피`, or an already-active Superloopy loop explicitly routes that task here. Interactive deployed content-led Web such as a campaign, publication, or landing experience is supported when navigation, forms, consent, localization, or another user journey is being built or validated; a static image, video, slide deck, document, or other non-interactive artifact is not. A plain mention of UI, frontend, desktop, mobile, SwiftUI, Tauri, Flutter, Qt, QML, Widgets, or a visible symptom is not authorization to activate this workflow. TV, wearable, XR, automotive, game UI, TUI, static media/document artifacts, and backend, API, data, concurrency, or infrastructure work stay with their primary workflows. ## Inspect and route -Resolve the requested surface, existing stack, target platform, existing styling or tokens, and available validation. For a Qt route, also resolve the minimum Qt version. Ask one question only when a material ambiguity would change the route or result. Load the smallest route-specific set: +Resolve the facts that determine ownership and proof before choosing references: + +- affected users, job, and outcome, marking each as evidence, assumption, or unknown with confidence; +- deployed OS, device, desktop environment, and session; +- public, authenticated, private/internal, installed, embedded, native-control, custom-rendered, or mixed composition; +- renderer and its semantic and accessibility model; +- client, shell, service, document, and state ownership; +- framework, runtime, provider, backend, and supported version; +- package, sandbox, update or distribution channel, and persistence boundary; and +- supported input, locale, accessibility services, and target validation capability. + +For a Qt route, also resolve the minimum Qt version. Inspect repository evidence first. Ask the minimum necessary questions only when material unknowns would change the route or result; batch independent unknowns when more than one must be answered. Load the smallest applicable union: | Requested surface | Load | | --- | --- | -| web UI | [`references/web.md`](references/web.md) | -| Qt Widgets | [`references/qt.md`](references/qt.md), [`references/qt-widgets.md`](references/qt-widgets.md), and [`references/qt-qa.md`](references/qt-qa.md) | -| Qt Quick/QML | [`references/qt.md`](references/qt.md), [`references/qt-quick.md`](references/qt-quick.md), and [`references/qt-qa.md`](references/qt-qa.md) | -| mixed Qt Widgets and Qt Quick/QML | [`references/qt.md`](references/qt.md), [`references/qt-widgets.md`](references/qt-widgets.md), [`references/qt-quick.md`](references/qt-quick.md), and [`references/qt-qa.md`](references/qt-qa.md) | -| mixed web UI and Qt | [`references/web.md`](references/web.md) plus [`references/qt.md`](references/qt.md), the relevant one or both of [`references/qt-widgets.md`](references/qt-widgets.md) and [`references/qt-quick.md`](references/qt-quick.md), and [`references/qt-qa.md`](references/qt-qa.md) | +| Browser-hosted DOM/document Web | [`references/ux.md`](references/ux.md) + [`references/web.md`](references/web.md) | +| Browser-hosted canvas/custom-rendered Web | [`references/ux.md`](references/ux.md) + [`references/web.md`](references/web.md) + [`references/renderer.md`](references/renderer.md) | +| Installed PWA or browser extension | [`references/ux.md`](references/ux.md) + [`references/web.md`](references/web.md) | +| Embedded HTML on desktop | [`references/ux.md`](references/ux.md) + [`references/web.md`](references/web.md) + [`references/desktop.md`](references/desktop.md) + [`references/hybrid.md`](references/hybrid.md) | +| Embedded HTML on mobile | [`references/ux.md`](references/ux.md) + [`references/web.md`](references/web.md) + [`references/mobile.md`](references/mobile.md) + [`references/hybrid.md`](references/hybrid.md) | +| Native/custom desktop | [`references/ux.md`](references/ux.md) + [`references/desktop.md`](references/desktop.md) | +| Qt Widgets on desktop | [`references/ux.md`](references/ux.md) + [`references/desktop.md`](references/desktop.md) + [`references/qt.md`](references/qt.md) + [`references/qt-widgets.md`](references/qt-widgets.md) + [`references/qt-qa.md`](references/qt-qa.md) | +| Qt Widgets on mobile or tablet | [`references/ux.md`](references/ux.md) + [`references/mobile.md`](references/mobile.md) + [`references/qt.md`](references/qt.md) + [`references/qt-widgets.md`](references/qt-widgets.md) + [`references/qt-qa.md`](references/qt-qa.md) | +| Qt Quick/QML on desktop | [`references/ux.md`](references/ux.md) + [`references/desktop.md`](references/desktop.md) + [`references/qt.md`](references/qt.md) + [`references/qt-quick.md`](references/qt-quick.md) + [`references/qt-qa.md`](references/qt-qa.md) | +| Qt Quick/QML on mobile or tablet | [`references/ux.md`](references/ux.md) + [`references/mobile.md`](references/mobile.md) + [`references/qt.md`](references/qt.md) + [`references/qt-quick.md`](references/qt-quick.md) + [`references/qt-qa.md`](references/qt-qa.md) | +| Qt Widgets on WebAssembly | [`references/ux.md`](references/ux.md) + [`references/web.md`](references/web.md) + [`references/renderer.md`](references/renderer.md) + [`references/qt.md`](references/qt.md) + [`references/qt-widgets.md`](references/qt-widgets.md) + [`references/qt-qa.md`](references/qt-qa.md) | +| Qt Quick/QML on WebAssembly | [`references/ux.md`](references/ux.md) + [`references/web.md`](references/web.md) + [`references/renderer.md`](references/renderer.md) + [`references/qt.md`](references/qt.md) + [`references/qt-quick.md`](references/qt-quick.md) + [`references/qt-qa.md`](references/qt-qa.md) | +| Mixed Qt Widgets + Qt Quick on WebAssembly | [`references/ux.md`](references/ux.md) + [`references/web.md`](references/web.md) + [`references/renderer.md`](references/renderer.md) + [`references/qt.md`](references/qt.md) + [`references/qt-widgets.md`](references/qt-widgets.md) + [`references/qt-quick.md`](references/qt-quick.md) + [`references/qt-qa.md`](references/qt-qa.md) | +| Mixed Qt Widgets + Qt Quick on desktop | [`references/ux.md`](references/ux.md) + [`references/desktop.md`](references/desktop.md) + [`references/qt.md`](references/qt.md) + [`references/qt-widgets.md`](references/qt-widgets.md) + [`references/qt-quick.md`](references/qt-quick.md) + [`references/qt-qa.md`](references/qt-qa.md) | +| Mixed Qt Widgets + Qt Quick on mobile or tablet | [`references/ux.md`](references/ux.md) + [`references/mobile.md`](references/mobile.md) + [`references/qt.md`](references/qt.md) + [`references/qt-widgets.md`](references/qt-widgets.md) + [`references/qt-quick.md`](references/qt-quick.md) + [`references/qt-qa.md`](references/qt-qa.md) | +| Native/cross-platform mobile or tablet | [`references/ux.md`](references/ux.md) + [`references/mobile.md`](references/mobile.md) | +| Mixed or multi-target | Union of the applicable references above, beginning with [`references/ux.md`](references/ux.md) | + +### Claim-triggered cross-cutting quality overlays + +After selecting the target route, add only the references selected by the changed claim. These overlays are independent of target classification and do not expand a narrow task merely because every interface has layout or may contain animation: + +- When information hierarchy, content exposure, or disclosure changes spatial presentation, or when placement, region topology, sizing, scroll, overflow, reflow, or adaptation changes, add [`references/layout.md`](references/layout.md). Behavioral-only label or purpose cleanup remains in shared UX and does not select layout when geometry and traversal stay unchanged; neither do other geometry-neutral copy, color, data, accessible-name, or behavior changes. +- When motion, transition, gesture progress, animated continuity, or haptic feedback changes, add [`references/motion-core.md`](references/motion-core.md). A Web implementation may additionally load the Web-only `references/motion.md`; native, Qt, and other routes do not inherit its React, browser, or animation-library rules. +- When a materially changed visual claim uses a generated, captured, sketched, or supplied reference, add [`references/image-first.md`](references/image-first.md) and classify its authority. Do not require an image for an existing-system delta that the authoritative product source already resolves. +- When the delivery is marketing, editorial, campaign, or an explicitly new visual direction, add [`references/anti-slop.md`](references/anti-slop.md) as the visual-direction check. It stays unselected for authenticated, internal, or convention-preserving product UI. +- High-impact decisions, task-based usability evidence, and accepted UX debt remain proportional sections of `references/ux.md`; they do not create separate project artifacts for every change. -For a mixed web/Qt task, apply gates and evidence independently for each surface. Web proof cannot substitute for Qt proof, and Qt proof cannot substitute for web proof. +`references/redesign.md` is a Web-only living-site specialization and is selected from the Web route. A native or embedded-shell redesign uses `ux.md` plus claim-selected `layout.md` and `image-first.md`, then its platform, renderer, and composition references; it does not inherit browser, SEO, or real-browser completion rules. -## Shared design gate +`layout.md` and `motion-core.md` are skill references, not mandatory output filenames. Keep the smallest applicable union and write into the existing receipt or proportional `UX_CONTRACT.md` unless the target repository already owns a more suitable artifact. -`DESIGN.md` owns app-defined semantics. Preserve the project's architecture and existing styling infrastructure. Before using an app-defined color, typography, spacing, radius, depth, motion, or component value that the design contract lacks, add its token to `DESIGN.md`. Platform runtime values remain authoritative when native. +Add `references/renderer.md` to native/custom or cross-platform routes when an engine or custom renderer owns pixels, semantics, text, or input. Route framework names by deployed facts, not by brand. Tauri and pywebview follow the actual desktop or mobile target and embedded client ownership. Electron is a desktop hybrid with bundled Chromium, not an OS WebView. CustomTkinter follows native/custom desktop. Flutter and Compose add renderer and semantics proof where their engine owns pixels or accessibility. React Native follows the actual provider and target. MAUI Hybrid combines a native host with an embedded client. Qt uses the matching desktop, mobile/tablet, or WebAssembly row above. Qt Widgets on WebAssembly, Qt Quick/QML on WebAssembly, and mixed Qt WebAssembly all keep browser and renderer proof; they must not borrow native-shell proof or be routed as desktop merely because Qt built them. For HTML embedded in a Qt-owned shell, compose the Web + target platform + hybrid route, then add `qt.md`, `qt-qa.md`, and only the actual shell specialization (`qt-widgets.md`, `qt-quick.md`, or both). Qt preflight replaces browser gates only for Qt-owned non-Web pixels; it never removes Web proof for an embedded HTML client. Mac Catalyst is UIKit-on-desktop with AppKit augmentation; iPadOS stays on the mobile route with a desktop-capabilities overlay rather than inheriting macOS wholesale. + +Treat framework capability claims as versioned facts: compare the repository-pinned version with current official documentation, record the provider and target, and keep unknown or unavailable capabilities unverified rather than inferring them from a brand name. + +For every new scoped `surfaceEvidence` row—single, mixed, or multi-target—record `target: { id, platform, environment }`, one affected `owner`, non-empty `claims`, and `scopeReason`; use the [Gate Notes claim-shaped evidence table](../../docs/superloopy-gate-notes.md#claim-shaped-surface-evidence) to select compatible artifact kinds and proof minimums. For mixed or multi-target work, load shared UX once, then create one evidence row per target and affected owner and require independent, attributable proof. Ambiguous free-text such as `Web + iOS` does not establish a native owner; name `native iOS app`, the framework, or a separate owner row. One surface cannot substitute for another. + +## Shared UX and design gate + +Apply [`references/ux.md`](references/ux.md) before platform checklists. The project's existing design source of truth remains authoritative. Use `DESIGN.md` only as a scoped mapping/receipt when the repository has no equivalent or when it links each changed app-defined visual semantic back to that authoritative source; never create a competing token source. `UX_CONTRACT.md` owns expanded journeys and high-consequence behavioral claims when the proportional UX contract requires it. Preserve the project's architecture and existing styling infrastructure. A narrow nonvisual change may record `Design impact: unchanged` with its reason instead of creating design artifacts. Before adding an app-defined visual value that the existing contract lacks, add and synchronize its token in the project's design source of truth. Platform runtime values remain authoritative when native. ## Build, dispatch, and evidence -Preserve the existing stack. For parallel work, dispatch self-contained crew slices with the relevant requirements and tokens inline, then judge each lane by delivered evidence. Capture real rendered-surface evidence and write `VISUAL_QA.md` under `.superloopy/evidence/frontend/`. Finish with a `SUPERLOOPY_EVIDENCE` artifact and record the final evidence through the active Superloopy loop. +Preserve the existing stack. Resolve and announce `FRONTEND_SKILL_DIR` as the absolute directory containing this loaded `SKILL.md`; packaged helper scripts live there, never under the target project's working directory. For parallel work, dispatch self-contained crew slices with the relevant requirements and tokens inline, then judge each lane by delivered evidence. Use the portable `YYYYMMDDTHHMMSSZ-` run ID created by `$FRONTEND_SKILL_DIR/scripts/evidence-root.mjs`; its slug is 1-48 lowercase ASCII letters or digits joined by single hyphens, so the same path works across supported shells and filesystems. Store the run under `.superloopy/evidence/frontend//` in the target project. Capture real rendered-surface evidence and write `VISUAL_QA.md` when a visual claim changed, or when an interaction claim has a visible-state or layout consequence; a purely behavioral interaction uses behavioral evidence. Match functional, accessibility, usability, renderer, shell, package, and target evidence to each claim. A static policy check proves only that the contract is packaged. + +With an active Superloopy loop, run `superloopy loop guide --json` and record each artifact against the returned goal and criterion; never omit their identifiers from `loop evidence`. Without an active Superloopy loop, keep the run-scoped receipts and finish with `SUPERLOOPY_EVIDENCE: ` instead of inventing loop state. ## Completion -Apply only the selected platform checklist. The design contract, interaction states, real rendered-surface evidence, and no weakened UX are mandatory for every route. +Apply only the selected shared, platform, composition, and specialization checklists. Truthful interaction states, claim-shaped evidence, disclosed limitations, and no weakened UX are mandatory. Require design, UX, visual, accessibility, package, or real-target artifacts only when the affected claims and risk call for them. Promote simulated work capability by capability only after new real-target evidence. diff --git a/skills/superloopy-frontend/agents/openai.yaml b/skills/superloopy-frontend/agents/openai.yaml index 38a4e3b..e188315 100644 --- a/skills/superloopy-frontend/agents/openai.yaml +++ b/skills/superloopy-frontend/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Superloopy Frontend" - short_description: "Explicit web and Qt desktop GUI workflow" - default_prompt: "Use Codex `$superloopy:superloopy-frontend` or Claude Code `/superloopy:superloopy-frontend` only for a web frontend or Qt desktop GUI task after explicit invocation or an explicit route from an active `loopy`/`루피` task; do not use it for non-web/non-Qt visual deliverables. Enforce the DESIGN.md token gate and require real-browser rendered-surface evidence for web or native rendered-application evidence for Qt." + short_description: "Explicit cross-platform application UI workflow" + default_prompt: "Use `$superloopy:superloopy-frontend` only after explicit invocation for browser-hosted or interactive content-led Web, desktop, mobile/tablet, embedded/hybrid, Qt, custom-rendered, or mixed application UI; exclude TV, wearable, XR, automotive, game UI, TUI, static media/document artifacts, and non-UI work, and require proportional evidence per owner and target." diff --git a/skills/superloopy-frontend/references/anti-slop.md b/skills/superloopy-frontend/references/anti-slop.md index f948a3a..128b23e 100644 --- a/skills/superloopy-frontend/references/anti-slop.md +++ b/skills/superloopy-frontend/references/anti-slop.md @@ -1,64 +1,70 @@ # Anti-Slop Rules -Generic AI UI is recognizable because models converge on the same defaults and ship them unseen. This file converts "taste" into rules a model can self-audit against: each rule **names the default, bans it, and gives a concrete replacement plus an override path**. Phrase every limit as binary (`zero`), never graduated (`sparingly`) — models honor a hard zero and quietly defeat "use sparingly". +Use this visual-direction check for marketing, editorial, campaign, greenfield presentation pages, or work that explicitly creates a new visual direction. It prevents unexamined model defaults; it is not a universal product-UI style guide. + +For authenticated, private, or internal product Web, preserve the existing design system and product conventions first. Apply only checks relevant to the changed visual claim. This contract does not require imagery, layout-family variety, or font replacement for a workflow screen, dense tool, settings form, accessibility fix, or narrow nonvisual change. + +## Non-waivable core + +Proportionality selects which rows apply; it does not make waivers free. Whenever this contract is selected at all, these minimums hold and no row below is eligible for a bare `N/A`: + +- **Honest assets:** div-based mock UI presented as a real product capture, unauthorized marks, and invented metrics or testimonials are defects at any scope. +- **No shipped placeholders:** accidental placeholder names, brands, or filler copy in shipped strings are defects at any scope. +- **Named waiver authority:** every `N/A`, override, or kept default cites the specific authority that justifies it — the approved criterion, brand rule, source text, or file. "Not selected by risk" with no named authority does not close a row. +- **Grounded impact records:** a `Design impact: unchanged`, `Layout impact: unchanged`, or `Motion impact: unchanged` record is valid only when the change introduces no new visible string, style, composition, or motion; otherwise the relevant rows apply and are answered with evidence. ## Named-default bans -Each ban: the signature → why it's a tell → what to do instead. Override only when the brand genuinely demands it (state the reason). +For a generated visual direction, each check names a common default and a deliberate replacement. An existing brand, approved target, organizational system, or user requirement is authority and must be recorded as the override. -- **AI-purple / glow (the LILA tell)**: purple→blue gradients, glowing mesh backgrounds. The #1 visual tell. → Pick a committed accent from the DESIGN.md palette (emerald, electric blue, deep rose, amber) and use it once, consistently. Override: brand color is genuinely violet. -- **Inter/Roboto by default**: the most-tested typographic tell. → Choose a deliberate stack per the Design Read (e.g. Geist, Satoshi, Cabinet Grotesk, a real serif with intent). Override: existing brand ships Inter. -- **Premium beige + brass palette**: `#f5f1ea`/`#f7f5f1` backgrounds with `#b08947`/`#b6553a` accents and espresso text — the "premium consumer" cliché that makes every such site look identical. → Use a distinct committed palette (cold luxury, forest, cobalt+cream, terracotta+slate). Don't reuse the same family twice in a row. -- **True black + one generic shadow**: `#000000` text and a single flat `box-shadow` everywhere. → Use the DESIGN.md foreground token (near-black, not pure) and a multi-level, optionally tinted elevation ladder. -- **Glassmorphism on everything / three equal feature cards / centered hero on dark mesh**: the default landing composition. → Vary composition; above `DESIGN_VARIANCE` 4, avoid the symmetric three-card row and the centered hero. +- **AI-purple or glow:** do not reach for a purple-to-blue gradient or glowing mesh without brand evidence. Choose and consistently apply the approved accent. +- **Inter/Roboto by reflex:** use the existing product type system. For a new campaign direction, choose a deliberate, licensed stack rather than replacing a working font merely to look different. +- **Premium beige plus brass:** do not reuse the stock beige, brass, espresso palette without an approved brand reason. +- **True black plus one generic shadow:** use the project foreground and elevation tokens. +- **Glass everywhere, three equal cards, centered hero on a dark mesh:** compose from the content and approved direction instead of a landing-page template. -## Countable rules (verify by counting, no judgment needed) +## Countable review flags -- **Em-dash: zero.** No `—` or separator `–` anywhere visible (headlines, body, captions, buttons, alt text). A single one = fail. Rewrite with commas, periods, or parentheses. -- **Eyebrow restraint**: count uppercase tracked labels across sections; if `count > ceil(sectionCount / 3)`, fail. -- **Zigzag cap**: the 3rd consecutive image+text split row = fail. Break the pattern. -- **Bento integrity**: N items → exactly N cells, no empty filler cell. -- **Layout-family variety**: 8 sections must use ≥4 distinct layout families; reject if one composition anchor repeats 3+ rows. +Apply these only to campaign or editorial composition selected by the delivery contract; mark each irrelevant row `N/A` with a reason. A count is a review flag, not an automatic blocker, unless an approved brand, content, or campaign criterion selects that threshold. Otherwise record the task/content warrant for keeping or changing the composition. -## Consistency locks (one decision holds page-wide) +- **Em-dash:** zero in newly generated campaign copy unless the language, brand style, quotation, or source text requires one. +- **Eyebrow restraint:** review uppercase tracked labels when they exceed `ceil(sectionCount / 3)` and keep each one that serves navigation, hierarchy, or an approved editorial voice. +- **Zigzag cap:** break a third consecutive image-and-text split row. +- **Bento integrity:** N items use N meaningful cells; never add an empty filler cell. +- **Layout-family variety:** for eight or more editorial sections, use at least four content-appropriate layout families and avoid repeating one anchor three or more times. -- **Color lock**: one accent everywhere — a warm-grey site does not get a blue CTA in section 7. -- **Shape lock**: one corner-radius scale across all components. -- **Theme lock**: no section inverts light/dark mid-page (a lone dark section reads as a copy-paste accident). -- **One system lock**: one palette, one icon family, one type system, one component library per project. +## Consistency locks -## Real-asset mandate +- **Color lock:** accents follow the approved token hierarchy. +- **Shape lock:** radii follow the component and platform system. +- **Theme lock:** a theme change is intentional, accessible, and documented rather than pasted in mid-page. +- **One-system lock:** do not mix icon, type, palette, or component systems accidentally. A migration names its boundary. -- **No div-based fake screenshots** — the strongest "AI-made wireframe" tell. Use a real image, a real source, or a clearly-labeled placeholder slot. -- **Real SVG logos** for logo walls (not styled text wordmarks); **icon libraries** (Phosphor, Radix, Tabler, Lucide), never hand-rolled decorative SVG paths. -- Even minimalist pages need real imagery — a pure-text page is incomplete work, not minimalism. +## Real-asset mandate -## Copy self-audit (slop prose is as strong a tell as slop visuals) +- Do not present div-based mock UI as a real product screenshot. Use a real authorized capture, an approved generated asset, or a clearly labeled placeholder. +- Logo walls use authorized real marks; interface icons follow the existing icon system. Do not add an icon-library dependency without approval. +- Imagery is required only when the content and approved direction call for it. Text-led editorial pages and product workflows may be intentionally image-free. -Re-read every visible string before shipping. Ban: +## Copy self-audit -- AI clichés: *Elevate, Seamless, Unleash, Delve, Empower, Supercharge*. -- "Quietly trusted by…" / performative-craftsman labels (*Field notes, From the field*). -- Generic names (*John Doe*), placeholder brands (*Acme*). -- Fake-perfect stats (`99.99%`) — prefer organic figures (`47.2%`) or omit. No fake-precise spec numbers unless real or labeled mock. +Check newly authored copy for generic claims, fake authority, placeholders, and invented precision. Avoid unsupported superlatives, fake-perfect statistics, generic names, and placeholder brands. Keep source quotations and established product terminology intact. -## Micro-tell catalogue (small details that collectively scream "AI") +## Micro-tell catalogue -Banned by default (narrow override only): version labels in the hero (`v0.6`, `BETA`), section-number eyebrows (`00 / INDEX`, `001 Capabilities`), middle-dot overuse, decorative status dots, br-broken italicized headlines, vertical rotated text, fake version footers (`v1.4.2 · last sync 4s ago`), weather/locale strips (`LIS 14:23 18°C`), pills overlaid on images, fake photo-credit captions, generic step labels (`Stage 1/2/3`). +For generated campaign directions, review ungrounded hero version labels, section-number eyebrows, decorative status dots, fake version footers, weather or locale strips, fake photo credits, and generic stage labels. Keep any item that serves a documented user, content, or brand need. -## Pre-Flight checklist (run before declaring done; any unticked box = not done) +## Pre-flight checklist -- [ ] Zero em-dashes anywhere visible. -- [ ] Eyebrow count ≤ `ceil(sectionCount / 3)`. -- [ ] No AI-purple/glow default (or brand-justified). -- [ ] Non-default, deliberate font stack. -- [ ] No banned premium beige+brass palette as default. -- [ ] Color / shape / theme consistency locks hold across all sections. -- [ ] ≥4 layout families across the page; no anchor repeats 3+ rows. -- [ ] No div-based fake screenshots; real or generated imagery; real SVG logos. -- [ ] Copy passed the self-audit (no clichés, no fake-perfect numbers, no placeholder names). -- [ ] No micro-tells from the catalogue. -- [ ] Motion claimed = motion implemented (transform/opacity/filter only); reduced-motion respected. -- [ ] Every color/size/spacing/radius/shadow traces to a DESIGN.md token; no orphan hex, no magic spacing. -- [ ] All interactive states (hover/active/focus/disabled) and empty/loading/error states handled. -- [ ] No horizontal scroll at 390 / 768 / 1280 px. +- [ ] Non-waivable core holds: honest assets, no shipped placeholders, named waiver authority, grounded impact records. +- [ ] Scope is marketing, editorial, campaign, or an explicit new visual direction; otherwise only relevant rows were applied. +- [ ] Existing brand, design-system, platform, and product conventions remain authoritative. +- [ ] Named defaults have evidence-backed decisions or recorded overrides. +- [ ] Applicable countable composition flags have evidence-backed decisions; selected pass/fail criteria and `N/A` rows have reasons. +- [ ] Color, shape, theme, icon, and type decisions are internally consistent. +- [ ] Captures, logos, metrics, testimonials, and other assets are real, authorized, generated as disclosed, or clearly labeled placeholders. +- [ ] New copy contains no unsupported claims, fake precision, or accidental placeholder content. +- [ ] Motion claimed is implemented and respects reduced motion. +- [ ] Applicable visual values trace to the existing or scoped design contract. +- [ ] Interactive and operating states required by the shared UX contract are handled. +- [ ] No unintended root/page horizontal overflow at the target-derived minimum, maximum, breakpoints, zoom, or text scaling; 390 / 768 / 1280 are baseline samples only when selected. Intentional task- or content-owned horizontal regions remain valid and follow `layout.md`'s spatial and scroll ownership contract. diff --git a/skills/superloopy-frontend/references/design-system.md b/skills/superloopy-frontend/references/design-system.md index c5f8152..1e62e9f 100644 --- a/skills/superloopy-frontend/references/design-system.md +++ b/skills/superloopy-frontend/references/design-system.md @@ -1,31 +1,44 @@ -# Design System (DESIGN.md) Schema +# Design-system contract and `DESIGN.md` schema -A DESIGN.md is the single source of truth for a UI: tokens live here, and every component traces back to them. This is what turns per-component improvisation (the root of inconsistent slop) into one coherent vocabulary. **No design system = no UI work.** +Use this reference when a changed visual claim needs new or updated app-defined semantics, or when an approved new/redesigned direction needs a coherent token contract. It does not expand a narrow nonvisual change into design work: that path may record `Design impact: unchanged` and `Visual evidence: not applicable` with behavioral, accessibility, and regression evidence. -## The 7 sections +## Choose the authority before the format -Author DESIGN.md with these sections. Every value is a token; no raw hex or magic numbers live in components. +Inspect the repository's existing tokens, theme, style layer, component library, platform conventions, and representative UI before writing a design artifact. The project's current design source of truth remains authoritative. + +- If `DESIGN.md` is already the established source, update it directly. +- If another source owns the system, update that source first. Use `DESIGN.md` only as a scoped mapping/receipt that links every changed app-defined semantic back to, and stays synchronized with, that owner. +- If the repository has no equivalent and a visual claim genuinely needs app-defined semantics, a scoped `DESIGN.md` may establish those affected decisions without requiring a repository-wide redesign. +- The absence of a formal design-system document is not a blocker and never means “no UI work.” Preserve working conventions and introduce only the minimum coherent contract required by the changed visual claim. + +When material unknowns would change the visual direction or source of truth, ask the minimum necessary questions and batch independent unknowns. Otherwise document the evidence, assumption, and confidence and continue within the existing authority. + +Loading this schema does not by itself select anti-slop, SEO, measured-quality, or a universal visual matrix. Anti-slop stays limited to its declared marketing/editorial or approved new-direction scope. SEO stays limited to the current crawlable public Web target or a distinct deployed public Web target in scope. Verify changed visual semantics across the target-derived browser/OS/input and breakpoint matrix, and create visual artifacts only for changed visual claims or interactions with a visible-state/layout consequence. + +## Seven-section schema for a new or redesigned direction + +Use the complete schema for an approved new or redesigned visual direction. For a visual delta inside an existing system, document only the affected sections and roles. Every new app-owned visual value traces to one authoritative token or an explicitly synchronized platform value; do not move runtime-owned native values into product tokens. 1. **Atmosphere / signature** — one paragraph naming how it *feels* (the one recognizable idea), not what it does. e.g. "weight-300 elegance, shadows tinted toward twilight" or "dark-native, content emerges from black, one signature heading weight". The signature is the compression key that makes every later token cohere. -2. **Color** — every color as `hex + CSS variable + semantic role`. Define the full set: background, foreground, primary, on-primary, secondary, accent, card, muted, border, destructive, ring. Pre-check contrast (text/background pairs ≥ 4.5:1, large/UI ≥ 3:1) and note any adjusted value. -3. **Typography** — a full ramp per role: `size (px/rem) · weight · line-height · letter-spacing` (+ OpenType features if used). Name the font stack and its intent. -4. **Spacing** — a base unit (4px) and a named scale (`--space-1 … --space-24`); every margin/padding/gap is a multiple. Note deliberate off-grid optical values if any. -5. **Components** — per component: background, text, padding, radius, border, font, and every state (hover/active/focus/disabled). Spell out any signature component as a buildable recipe. -6. **Motion** — the physics (duration ranges, easing/spring), GPU-composited only (transform/opacity/filter). Define reduced-motion behavior. -7. **Depth** — commit to one strategy (tonal shift vs shadows vs borders) and a single elevation ladder. Do not mix. +2. **Color** — record each project-native color token, its resolved value or dynamic-value rule, and semantic role. Preserve the authoritative representation: CSS custom properties and CSS color syntax on Web, Android resources or Compose tokens, Apple asset catalogs or Swift values, Qt palette/QColor roles, or the repository's equivalent. Define only the roles the product needs, including state and dynamic light/dark/high-contrast variants where applicable. Pre-check contrast for the target's applicable text and non-text criteria and note adjusted values. +3. **Typography** — define a ramp per semantic role using the project's native units and APIs: CSS `rem`/`px`, Android `sp`, Apple points and text styles, Qt logical pixels or inherited platform fonts, or the repository equivalent. Record weight, line height, tracking, fallback, scaling behavior, and platform text-style ownership where applicable. +4. **Spacing** — preserve or establish a project-native base and named scale in the authoritative unit system (`rem`, logical px, dp, points, toolkit metrics, or equivalent). Values should follow that scale unless a platform metric, content constraint, or documented optical correction owns the exception; 4 px is an example, not a universal base. +5. **Components** — per component, define structure, semantic roles, content behavior, geometry, and every target-supported state and input path. Use the platform's native state and styling vocabulary rather than importing hover, radius, border, or CSS concepts where they do not apply. +6. **Motion** — define duration or spring behavior, continuity, interruption, and reduced-motion behavior. Prefer compositor-friendly or platform-native mechanisms appropriate to the actual renderer; do not impose Web-only transform/opacity/filter rules on native toolkits. +7. **Depth** — choose a coherent strategy supported by the platform and product, such as tonal hierarchy, material/elevation, borders, shadows, or native grouping. Do not mix unrelated systems without an explicit ownership reason. ## Authoring loopy-native token sets Write your own token references — do not copy third-party design files. Two ways in: -- **From an existing UI**: extract ground-truth values with the browser (computed styles, as in `superloopy-clone`) rather than estimating, then encode them in the 7-section schema. -- **From a direction**: pick a committed aesthetic from the Design Read, then define exact tokens. Encode each color as `hex + variable + role`, add a short Do/Don't list that names the wrong instinct (e.g. "Don't use weight 600-700 for display — 300 is the voice"), and a few copy-paste example component prompts at the right altitude (cite px and hex, not "dark gray"). +- **From an existing UI**: inspect ground-truth values with the appropriate rendered-surface and repository tools rather than estimating. Keep the existing source authoritative; when a receipt is needed, map only the affected values into the schema and link them back to that source. +- **From a direction**: pick a committed aesthetic from the Design Read, then define exact tokens in the project's native representation. Record each color token, resolved value or dynamic rule, and role; add a short Do/Don't list that names the wrong instinct (for example, “Don't use a heavy display weight when the established voice is light”); and provide buildable examples using the target's real token names, units, and APIs rather than vague color or spacing language. -Keep the system lean — a design system that grows every week is dying. Add a token only when a component needs it, and add it to DESIGN.md first. +Keep the system lean. Add a token only when a component needs it, and add it to the authoritative design source first; synchronize any scoped `DESIGN.md` mapping in the same change. -## Example token sets (loopy-native, illustrative) +## Web-oriented example token sets (loopy-native, illustrative) -Author your own per project; these show the shape and altitude. +Author your own only when a new or redesigned direction requires it. These examples intentionally use Web syntax to show shape and altitude; they are never defaults and their CSS variables, hex colors, pixel values, and interaction states must not be copied into a native target. Translate semantic roles into that target's authoritative tokens, units, platform states, and APIs. ### Calm SaaS (light, trustworthy) - **Signature**: quiet confidence; generous whitespace, one restrained accent, content over chrome. diff --git a/skills/superloopy-frontend/references/design/_INDEX.md b/skills/superloopy-frontend/references/design/_INDEX.md index 5667a86..228458a 100644 --- a/skills/superloopy-frontend/references/design/_INDEX.md +++ b/skills/superloopy-frontend/references/design/_INDEX.md @@ -1,6 +1,6 @@ # Brand Design Token Library — Index -Loopy-native brand teardowns (92 brands). Match a named brand or a mood to ONE file, load it, and let its tokens drive DESIGN.md. Original prose; public brand facts only. For a brand not listed, extract it on-demand from a live reference (see `design-system.md`). +Loopy-native brand teardowns (92 brands). Match a named brand or mood to ONE file and use it as scoped reference input, never as automatic token authority. The project's current design source remains authoritative; apply an approved direction there first, and use `DESIGN.md` only when it is already that source or as a synchronized scoped mapping/receipt. Original prose; public brand facts only. For a brand not listed, extract it on demand from an authorized live reference (see `design-system.md`). ## fintech/crypto - [`binance.md`](./binance.md) — gold ingot accent on monochrome, 50px pill CTAs, trading-floor urgency diff --git a/skills/superloopy-frontend/references/desktop.md b/skills/superloopy-frontend/references/desktop.md new file mode 100644 index 0000000..3ce5d31 --- /dev/null +++ b/skills/superloopy-frontend/references/desktop.md @@ -0,0 +1,26 @@ +# Desktop Application Contract + +Use this after the shared UX contract for native, custom, or hybrid desktop UI. A desktop label is not enough: resolve the actual OS and version, desktop environment and session, toolkit and runtime, package format and update channel, window ownership, supported input methods, and available native validation. + +## Platform ownership + +Keep system or platform chrome as the default. Custom chrome is a platform integration project, not a visual upgrade: + +- Windows supported titlebar customization may extend content into the titlebar while system caption buttons are retained; record which hit-test regions and drag behavior the application owns and which caption, snap, accessibility, and window-state behavior remains system-owned. A fully custom non-client frame transfers caption hit testing, resize, snap, system menu, caption-button semantics, accessibility, high contrast, and window-state responsibilities to the application. +- GNOME commonly uses application-owned header bars and client-side decoration, but the supported compositor and session still determine behavior. +- KDE commonly leaves decoration to the compositor; do not copy GNOME ownership assumptions. +- Apple APIs may customize supported titlebar regions while retaining native window behavior. Preserve traffic-light controls, full screen, tabs, accessibility, and system interaction. + +Name the menu/command owner, keyboard conventions, focus and selection model, file/font/account discovery, clipboard and drag behavior, notifications, permissions, and multi-window or document lifecycle. Native-adaptive values from the current palette, font, metrics, accessibility settings, and input method (IME) remain authoritative unless the product explicitly owns a branded alternative. + +## State and distribution + +Separate application defaults, user settings, document/model data, sensitive credentials, and scene/window restoration. Scope persistence and migration claims to the concrete installer, sandbox, package identity, update channel, and supported version path. A development launch does not prove an installed upgrade; a local settings API does not prove uninstall/reinstall retention. + +Handle bootstrap and dependency failures before the main surface depends on them. Preserve user input and state through recoverable failures, and expose a specific recovery path rather than an endless loading state. + +## Desktop evidence + +Keep a **minimum regression floor** on each changed target: configure/build or package as applicable, launch, the affected core journey, and its closest regression path. Add install or unpack, first run, permission prompts, restart, supported update, and recovery only when that lifecycle is affected, explicitly claimed, release-critical, or selected by risk. Exercise supported mouse, keyboard, IME, screen reader, scaling, high-contrast/theme, and multi-window paths according to the same risk model. Inspect the native accessibility tree and packaged lifecycle when those claims apply; browser screenshots or offscreen widget renders cannot substitute for either. + +For custom chrome, capture real native behavior for move, resize, snap/tile, maximize, minimize, close, full screen, system menu, focus, and accessibility on the named OS/session. Mark unsupported environments explicitly instead of presenting one universal desktop checklist. diff --git a/skills/superloopy-frontend/references/hybrid.md b/skills/superloopy-frontend/references/hybrid.md new file mode 100644 index 0000000..f86309a --- /dev/null +++ b/skills/superloopy-frontend/references/hybrid.md @@ -0,0 +1,28 @@ +# Hybrid Shell and Client Contract + +Use this when a Web or custom-rendered client runs inside a native shell, or when multiple UI/runtime processes share one journey. Apply the shared UX contract plus the relevant desktop/mobile and Web references. Add the renderer reference only when a custom renderer actually owns pixels, semantics, text, or input; an ordinary DOM WebView does not inherit renderer proof merely because it is embedded. + +## Ownership map + +Name the owner of every boundary: + +- the client owns document rendering and in-client interaction; +- the shell owns native windows/scenes, menus, pickers, permissions, accessibility integration, package lifecycle, and system handoff unless the inspected stack proves otherwise; +- a service or backend owns durable remote results; and +- the bridge owns serialization, authorization, cancellation, ordering, retries, and error translation across processes. + +Map client, shell, service, document, and state owners for each journey. A WebView uses an OS/provider engine whose version and capability may vary; a bundled browser such as Chromium has a different update and security boundary. Do not treat them as interchangeable. + +When multiple clients, shells, or presentation surfaces advertise the same product journey, add a capability reachability matrix. For every applicable capability, prove direct reachability on each promised surface or define an intentional handoff with a clear transition, return path, and preserved context/state; otherwise mark it not applicable with a concrete reason. Shared backend ownership or visual similarity does not prove that users can actually reach the capability from every advertised frontend. + +## Bridge truth + +Specify every IPC or bridge command by semantic input, validation, owner, result, failure, cancellation, lifetime, and observable state. A JavaScript handler, message receipt, spinner, or toast is not proof that the native or durable operation completed. Prevent duplicate submissions and stale replies; preserve enough context to recover after client reload, shell recreation, or service failure. + +Keep privileged native operations behind a minimal authorized surface. Validate untrusted client data, avoid exposing raw filesystem or process capability, and prove that navigation or content injection cannot widen the bridge contract. + +## Native and client evidence + +Client proof cannot substitute for shell proof. Keep a **minimum regression floor** that builds and launches the real client and shell, executes the affected journey through the bridge, checks the resulting owner state, and covers the closest regression path. Add independent native picker, window, menu, permission, accessibility, package, update, and shutdown proof when that behavior is affected, claimed, release-critical, or selected by risk. Likewise, a native shell screenshot cannot prove client semantics, focus, text input, localization, or responsive rendering. + +Record engine/provider/version, shell/runtime/version, OS/device, package channel, bridge mode, service state, and each artifact's owner. For a public surface deployed separately from the embedded client, evaluate its crawlability and Web evidence as a separate target. diff --git a/skills/superloopy-frontend/references/image-first.md b/skills/superloopy-frontend/references/image-first.md index bd2a6cb..8eb0fde 100644 --- a/skills/superloopy-frontend/references/image-first.md +++ b/skills/superloopy-frontend/references/image-first.md @@ -1,27 +1,47 @@ -# Image-First Discipline +# Visual Reference Discipline -Models build mediocre UI from a text prompt because prose underspecifies a layout — the gaps get filled with trained averages (centered dark hero, purple gradient, three equal cards). Fix it by committing to a concrete **visual target before coding**, so implementation becomes *translation* (high-fidelity, low-freedom) instead of *invention* (low-fidelity, infinite defaults). +Use this reference for an approved new/redesigned direction or another materially changed visual claim when a generated, captured, sketched, or supplied visual reference will influence implementation. Do not use this reference to expand a narrow nonvisual change into visual work. -## Order (mandatory for visually-important work) +Inspect the project's current design source and representative surface first; they remain authoritative. A generated image, captured inspiration, or layout sketch is a scoped reference, never permission to replace existing brand, component, token, content, platform, accessibility, or behavior contracts. Ask the minimum necessary questions only when material unknowns would change the result, and batch independent unknowns. This reference does not make anti-slop or SEO applicable; SEO remains limited to the current crawlable public Web target or a distinct deployed public Web target in scope. -1. **Secure a visual target.** In priority order: - - Generate the design image(s) with an image-generation tool if one is available in this runtime. - - Else capture a real reference site at a fixed viewport with the browser (reuse `superloopy-clone`'s computed-style + screenshot tooling). - - Else use a user-provided mock/Figma, or — last resort — an authored layout sketch. -2. **Deep-analyze the target into a written spec.** Extract, enumerated: exact text/headlines/CTA wording, type scale + weight relationships, spacing/gutter rhythm, radius logic, button shapes/hierarchy, full color palette, grid logic, repeated motifs. This spec is the de-facto DESIGN.md (or its deltas) — write it to the evidence root. -3. **Implement to match the spec**, not "inspired by" it. -4. **Visual-QA compares the built UI back to the target** (Phase 3 of the skill). +## Authority modes -## Decomposition (when generating images) +Classify the reference before using it: -One image **per section**, never one compressed multi-section board — resolution budget per section is what keeps type, spacing, and component detail legible enough to extract faithfully. If a section is unclear, generate a fresh, closer image rather than cropping (cropping destroys spacing/scale relationships). Force a non-default composition: pick a deliberate hero architecture and section system; do not default to the left-text/right-image hero. +- **Current-surface delta:** the existing product, implemented surface, and authoritative design source resolve the direction. Use the current state plus the changed visual claim as the target; do not generate replacement concepts merely to restyle a narrow delta. +- **Exploratory direction:** concepts may explore hierarchy, density, atmosphere, and composition, but remain explicitly non-authoritative and cannot direct production implementation or count as quality evidence. +- **Approved implementation reference:** a user, design owner, or established product source has approved the reference for a named target, state, and visual claim. Fidelity is an acceptance claim, subject to documented platform, content, accessibility, adaptive-layout, interaction, and performance changes. -## Anti-drift implementation rules +When a material visual-direction unknown cannot be resolved from existing evidence, exploration may precede implementation approval. Do not mistake producing an image for approving it. -- Be visually faithful to the target, translated into real frontend — not a generic reinterpretation. -- Do not simplify distinctive sections into default templates, collapse generous spacing, or swap a signature component for a plain row. -- Every token still traces to DESIGN.md; the target sets the values, the contract enforces them. +## Promotion to implementation authority -## Degrade path (no imagegen available) +Before an exploratory or external reference becomes an approved implementation reference, reconcile and record the task, audience, authoritative content and states, semantic order, spatial owners and constraints, target platform and inputs, accessibility boundaries, and the project's existing design authority. If layout is material, also apply `references/layout.md` before committing production visual implementation. -If this runtime has no image-generation tool, do **not** skip the discipline — secure a reference site capture or a provided mock instead, and still produce the **written target spec** before coding. The value is the explicit, inspectable target; the generation method is interchangeable. Record the target (image, screenshot, or spec) as an artifact under `.superloopy/evidence/frontend/` so visual-QA has something to compare against. +Write a scoped implement-versus-ignore handoff: + +- implement: approved hierarchy, composition, spacing relationships, media treatment, component emphasis, and state-specific visual intent; +- adapt explicitly: platform conventions, real content, localization, text scaling, accessibility, interaction, performance, and supported target behavior; and +- ignore: invented controls or data, unsupported claims, fake metrics, decorative artifacts, and details outside the approved visual claim. + +Use `DESIGN.md` only when already established as the source or as a synchronized scoped mapping/receipt linked to the real owner; never create a competing source. + +## Reference composition + +Choose the artifact shape that makes the claim inspectable. A full-surface reference can preserve overall relationships; section or component references can add detail; a labelled state or responsive board can compare several individually legible panels. Do not crop away spatial context required by the claim, and request a clearer source when text, state, or geometry cannot be read reliably. + +Generated-image text is not content or copy authority. Use approved product strings, localization keys, legal text, and content-owner decisions; otherwise label generated wording provisional and exclude it from fidelity scoring. + +For an external capture, record the actual representative target-derived viewport or window, environment, state, content, date, source, and permission. For native or embedded work, a browser reference never becomes native-behavior proof. + +## Implementation and evidence + +Implement against the approved handoff rather than an undefined impression. Record every intentional deviation and its owner. Compare the production result with the applicable target across the target-derived matrix, then separately prove behavior, accessibility, content, and performance claims. + +A generated image or screenshot is not proof of usability, accessibility, brand correctness, semantic order, or implementation correctness. Pixel similarity may locate differences but cannot decide whether task behavior, platform integration, reflow, localization, focus, or state truth is correct. + +A pixel-exact claim is meaningful only after recording the approved reference, named target and build, rendering environment, state and content, fonts and assets, comparison method, and project-specific tolerance. Without those criteria, treat the artifact as visual direction, not exactness acceptance. Do not invent one universal tolerance; accessibility, platform, content, and adaptive deviations remain separately owned even when the scoped fidelity comparison passes. + +## Degrade path + +No image-generation tool is required. Use the approved current surface, user-provided mock, authorized capture, authored sketch, or a low-fidelity prototype suited to the uncertainty. Store a visual artifact under the active evidence root only when the changed visual claim and proportional visual-QA gate require it. diff --git a/skills/superloopy-frontend/references/layout.md b/skills/superloopy-frontend/references/layout.md new file mode 100644 index 0000000..4a6c71b --- /dev/null +++ b/skills/superloopy-frontend/references/layout.md @@ -0,0 +1,51 @@ +# Cross-Platform Spatial Contract + +Load this reference only when a material spatial claim changes. It owns task-relative placement, content exposure, size negotiation, scrolling, overflow, reflow, and adaptation across supported UI targets; the selected platform, renderer, and toolkit references own their concrete APIs. + +## Activation and proportionality + +Add this reference when a new or redesigned surface changes visible regions, when a region is added, removed, reordered, reparented, collapsed, or expanded, or when information hierarchy or disclosure changes spatial presentation. Also select it when scroll/fixed/sticky/overlay ownership, virtualization, breakpoints, window or container behavior, reflow, overflow, content measure, reading order, or focus traversal changes, and for a reproduced clipping, overlap, hidden-action, lost-context, or scroll failure. + +Do not add it for a copy, color, accessible-name, data, or behavior-only change whose geometry and traversal stay unchanged. Record `Layout impact: unchanged — ` instead. + +A narrow spatial fix stays in the existing receipt with the changed invariant, owner, failure case, adjacent regression, and proof. This remains true when the existing surface is multi-region or virtualized; that architecture alone does not expand the artifact. A new or materially redesigned multi-region, virtualized, or high-consequence surface adds the relevant fields below to the proportional `UX_CONTRACT.md`; it does not require a separate layout artifact. + +## Spatial ownership record + +Record only the fields selected by the changed claim: + +- primary task, primary content, supporting content, and the state in which each region exists; +- each region's exposure mode—`always visible`, `summary`, `on demand`, or `omitted`—and the task condition that selects it; +- disclosure label and controlling affordance when present, the source or summary state, the disclosed or destination state, and the relationship that remains understandable across them. `Always visible` and `omitted` are valid exposure decisions and do not imply a two-state control; +- collapsed and expanded states only when the selected disclosure mechanism uses collapse and expand. A details route, modal, drill-down, staged flow, or contextual reveal records its own source and destination states instead; +- semantic/model order, reading order, visual placement, and focus/traversal order, including intentional differences and how relationships remain understandable; +- layout owner and every scroll owner, plus the region that determines available width, height, and overlay bounds; +- size constraints, intrinsic or platform-derived minimums and maximums, and which regions may shrink, wrap, clip, scroll, or overflow; +- fixed, sticky, floating, modal, inspector, frozen-axis, or overlay responsibilities and the content they must not cover; +- target-derived change points grounded in content and supported bounds rather than copied device widths; +- each disclosure transition and the focus, context, and state it preserves; and +- state, selection, focus, scroll, and context that must survive each adaptation. Preserve semantic task context by default, not an exact object identity or pixel offset when content is hidden, reparented, recycled, or resized; require exact preservation only when the product contract makes it meaningful. + +A reusable primitive should have one primary spatial responsibility. A shell or composite may coordinate several responsibilities when each child owner and boundary is explicit. + +## Scroll, panes, and two-dimensional work + +Multiple scroll regions are valid when each has a named user task and owner. Test entry, exit, wheel or gesture routing, focus visibility, restoration, and the context users need to understand which region moved. Nested scrolling without distinct task ownership is a defect; multiple scrolling is not itself a defect. + +Two-dimensional scrolling is valid when the task is inherently spatial, as in a spreadsheet, canvas, timeline, map, or dense comparison surface. Preserve headers, coordinates, selection, zoom anchors, navigation alternatives, and reachable commands instead of forcing a one-column document model. + +At narrow bounds, list-detail, inspector, and multi-pane work may stack, collapse, overlay, or navigate to another view. Choose from the actual task and platform convention. Preserve selection, back/return behavior, focus restoration, scroll position, and unsaved state rather than imposing one transformation on every target. + +## Adaptation and content stress + +Derive the matrix from supported window and container bounds, orientation or posture, system insets, text scaling, zoom, density, input, and actual layout change points. Exercise just below and above affected change points plus relevant minimum, typical, and maximum bounds. + +Select applicable content stress cases: empty content, short and long labels, long paragraphs, unbroken strings, dynamic errors, taller scripts, mixed-direction text, and RTL. A visual reference or baseline width never substitutes for these target-derived cases. + +## Virtualized collections + +A virtualized surface preserves logical collection order, stable item identity, count or range semantics, selection, focus, scroll anchoring, and restoration even when only a subset of items is instantiated. Do not infer that every logical item must exist simultaneously in the renderer or accessibility tree. Select only affected cases from recycling, insertion and removal while scrolled, first/middle/last boundaries, variable item sizes, and offscreen navigation according to the target's semantic model; mark unaffected boundaries `N/A` with a reason instead of broadening a narrow change. + +## Evidence + +Prove the affected spatial claim on the actual target. For affected disclosure on that actual target, exercise its source or summary state and disclosed or destination state at the affected bounds and inputs. Exercise collapsed and expanded states only when that selected mechanism uses them. Use rendered captures for visible geometry, interaction evidence for scroll, resize, selection, focus, disclosure, and restoration, accessibility evidence for the semantics claimed, and performance evidence only when performance is claimed or risk-selected. Record the exact target, state, content, bounds, input, result, and limitation; a static screenshot cannot prove traversal or scroll ownership. diff --git a/skills/superloopy-frontend/references/mobile.md b/skills/superloopy-frontend/references/mobile.md new file mode 100644 index 0000000..6faf609 --- /dev/null +++ b/skills/superloopy-frontend/references/mobile.md @@ -0,0 +1,23 @@ +# Mobile and Tablet Application Contract + +Use this after the shared UX contract for native or cross-platform phone and tablet UI. Resolve platform and API/OS version, form factors, current window size, orientation and posture, renderer/provider, package and update channel, supported inputs, permissions, restoration model, and validation devices. + +## Navigation, windows, and input + +Respect platform navigation and back behavior, system bars, insets and safe areas, edge-to-edge content, keyboard avoidance, multitasking, resizing, rotation, and foldable posture where supported. Do not infer layout from a device model or orientation alone; respond to the current window and content constraints. + +Support touch, pointer, hardware keyboard, stylus, editable text, and the platform input method (IME) where applicable to the target and product. Preserve semantic outcomes across supported inputs without requiring identical gestures. Provide alternatives for drag, multi-touch, hover-only disclosure, and precision actions. Exercise selection, clipboard, composition, commit/cancel, and keyboard avoidance with representative scripts when text input is material. + +Use platform or provider permission and picker contracts for photos, files, cameras, location, accounts, and other protected resources. Explain denial, limited access, cancellation, and recovery. A simulated picker or granted emulator permission does not prove physical-device integration. + +## State, locale, and accessibility + +Define process and scene restoration, background/foreground transitions, low-memory or process death behavior, migration, backup eligibility, and sensitive-value ownership. Name the durability boundary rather than promising that preferences survive every update, reinstall, transfer, or account change. + +Use platform locale, per-app language, RTL, formatting, pluralization, dynamic type or text scaling, reduced motion or animation scale, and accessibility semantics. Test long and bidirectional content, larger text, focus/traversal order, labels/roles/states/actions, announcements, contrast, target size, and alternatives to gesture-only operations. Respect the system motion preference without removing the semantic outcome or essential progress and status feedback. + +## Mobile evidence + +Build a device and API matrix from integration risk, not market-share theater. Emulator or simulator evidence and physical-device evidence are complementary: use automation for deterministic state and broad API coverage, then physical devices for sensors, permissions, performance, input, accessibility services, rendering, and lifecycle risks that simulation cannot prove. + +Keep a **minimum regression floor** that builds and launches the real target, executes the affected core journey, and covers the closest adjacent regression on a supported emulator, simulator, or device. Add install/first-run, navigation/back, interruption, permission denial/recovery, rotation/resizing, process recreation, locale/RTL, text scaling, accessibility services, offline or degraded service behavior, restart, physical-device integration, performance, and supported upgrade only when affected, claimed, release-critical, or selected by risk. Record which device, OS/API, build, account/state, input, and service conditions produced each artifact. diff --git a/skills/superloopy-frontend/references/motion-core.md b/skills/superloopy-frontend/references/motion-core.md new file mode 100644 index 0000000..a3c1295 --- /dev/null +++ b/skills/superloopy-frontend/references/motion-core.md @@ -0,0 +1,35 @@ +# Cross-Platform Motion Contract + +Load this reference only when a motion, transition, gesture-progress, animated continuity, or haptic-feedback claim changes. It defines temporal state truth and evidence without selecting a framework or visual character. + +## Activation and proportionality + +Add it when app-defined motion is added, removed, or changed; when a journey changes integration with a system or provider transition; when gesture progress, commit, cancellation, reduced-motion behavior, or an app-owned haptic changes; or when comfort, smoothness, continuity, or motion performance is claimed. + +Do not add it for unchanged system-controlled animation, a static visual delta, or a behavior change with no temporal claim. Record `Motion impact: unchanged — ` instead. + +A narrow change records one compact row in the existing receipt. Expand the record only for custom gestures, frequent or interruptible motion, sensory feedback, broad reach, performance risk, or a high-consequence journey. + +## Authority and purpose + +Record the purpose, trigger, frequency, affected state, owner, supported input, preference behavior, and observable semantic result. The system or provider remains authoritative for its own transitions, gesture physics, timing, settings, and capability limits. Verify correct integration and avoid adding a second app-owned transition over the provider behavior. + +Haptic feedback is optional and never the sole carrier of required information, error, or durable success. Record its semantic event, provider, hardware and settings availability, repeat suppression, synchronization, and a visual, textual, or audible alternative selected by the target contract. + +## Temporal state contract + +Name the before, intermediate, and after states and the durable owner that commits the result. Exercise repeated input, interruption, reversal, retargeting, and cancellation when the interaction or provider supports them. Mark an unsupported boundary `N/A` with a reason instead of inventing custom behavior. + +- Repeated input does not duplicate commands or completion feedback. +- Interruption and retargeting settle on the newest durable truth rather than an animation callback. +- Reversal and cancellation restore a coherent model, surface, focus, selection, and accessible state until a real commit occurs. +- Focus, selection, and assistive-technology continuity remain logical throughout the state change. +- Reduced motion preserves the semantic outcome, status, focus, and spatial comprehension without requiring animation. + +Numeric timing, easing, spring, and frame-rate targets come from the system/provider, the approved product motion source, and measured target behavior. Cross-platform equality is not a goal: equivalent semantic outcomes may use different provider-owned timing, physics, and feedback. A toolkit or property name does not prove acceleration, comfort, or performance. + +## Evidence + +A resting screenshot does not prove motion. Exercise the actual interaction in every applicable default, repeated, interrupted, reversed, retargeted, cancelled, and preference state. Build a representative target-and-state matrix from changed ownership, provider differences, reach, uncertainty, and consequence; do not demand a full device-by-state Cartesian product when attributable evidence establishes the same behavior. Record real-target interaction evidence for user-visible continuity; add a trace or measurement only for the performance claim it can support. Unit tests can prove state transitions and cleanup but cannot substitute for rendered timing, device integration, comfort, or haptic behavior. + +Use existing owner-scoped interaction, visual, accessibility, target, adversarial-case, and contract-coverage records to bind the claim. An artifact label or non-empty note is not temporal or haptic proof: distinguish a provider call, observed physical output, measured behavior, and what remains unverified. Do not invent a motion or haptic artifact kind until its validator can verify structure, producer and provenance, target/build binding, and the claim-specific result; a new filename alone would create false confidence. diff --git a/skills/superloopy-frontend/references/motion.md b/skills/superloopy-frontend/references/motion.md index 5a1692a..7742911 100644 --- a/skills/superloopy-frontend/references/motion.md +++ b/skills/superloopy-frontend/references/motion.md @@ -1,9 +1,19 @@ -# Motion Implementation Guide +# Web Motion Implementation Guide -Motion must communicate hierarchy, storytelling, feedback, or state change. Give each non-trivial animation a one-sentence reason; “it looks cool” is not enough. If the repository cannot support a working and verified effect within scope, lower `MOTION_INTENSITY` and ship a clear static surface. +This is the Web implementation specialization for [`references/motion-core.md`](motion-core.md). Load the cross-platform temporal contract first, then use this file only when browser-owned nodes and Web motion mechanisms implement the changed claim. Native, Qt, and other routes do not inherit these React, browser, or animation-library examples. + +Motion must communicate hierarchy, storytelling, feedback, or state change. Give each non-trivial animation a one-sentence reason; “it looks cool” is not enough. If the repository cannot support a working and verified effect within scope, reduce the animation quantity and complexity and ship a clear static surface. The React examples below apply only when the project already uses the named libraries, or the user approves adding them. They are **starting templates**, not proof. Adapt them to the project and collect real-browser interaction evidence before calling the motion complete. +## Applicability and inherited authority + +Use this reference only when a motion or animated-interaction claim changes. It does not expand a narrow nonvisual change into a motion redesign, anti-slop pass, or visual artifact. The project's existing motion primitives and authoritative design source remain authoritative; use `DESIGN.md` only when it is already that source or as a scoped mapping/receipt synchronized with it. A repository without a formal design-system document is not blocked. + +Resolve timing, interaction, supported input, and breakpoint behavior from the current product and target contract. Ask the minimum necessary questions only when material unknowns would change the implementation, and batch independent unknowns. This reference does not make SEO applicable: assess SEO only for the current crawlable public Web target or a distinct deployed public Web target in scope, never because animation code runs in a browser. + +The `1024px` media conditions in the templates below are illustrative. Replace them with the actual target-derived breakpoint that has enough space for the effect, and keep the static layout outside that condition; never add or claim a breakpoint merely because it appears in an example. + ## Ownership and performance rules - Avoid raw `window.addEventListener("scroll", …)` loops for continuous animation. Use a project-native motion value, GSAP ScrollTrigger, IntersectionObserver, or a progressively enhanced CSS scroll-driven animation. @@ -201,11 +211,11 @@ export function RevealStagger({ items }: { items: string[] }) { ## Evidence checklist -- Exercise the interaction, not only its resting screenshot, at 390 / 768 / 1280 px. -- Resize while pin/scrub motion is active and verify the endpoint remains aligned. +- Exercise the interaction, not only its resting screenshot, across the target-derived browser/OS/input and breakpoint matrix. Treat 390 / 768 / 1280 px only as optional baseline samples when selected by the product's supported range; they are never universal proof. +- Exercise each target-supported viewport, resize, orientation, or container transition while pin/scrub motion is active and verify the endpoint remains aligned. - Toggle reduced motion and confirm every item stays visible in static document flow. - Navigate away and back; verify no duplicate triggers, listeners, or transforms remain. -- Check keyboard and touch interaction around the animated section. -- Record the observed states and any project-specific adaptation in `VISUAL_QA.md`. +- Check each supported input around the animated section, including keyboard, touch, or pointer only where the target contract includes it. +- When the changed motion has a visible-state or layout consequence, record the observed states and project-specific adaptation in `VISUAL_QA.md`; retain real interaction, accessibility, cleanup, and regression evidence rather than treating a resting screenshot as proof. Selected motion mechanisms were adapted under MIT from Taste Skill; see `references/upstream-notice.md`. diff --git a/skills/superloopy-frontend/references/perfection.md b/skills/superloopy-frontend/references/perfection.md index 60d8dd6..51d6cb6 100644 --- a/skills/superloopy-frontend/references/perfection.md +++ b/skills/superloopy-frontend/references/perfection.md @@ -1,25 +1,62 @@ # Measured Quality Gate (Perfection) -"Looks good" is subjective; this gate makes UI quality an objective pass/fail the loop can enforce. It has two layers: a **runnable design-system check** (loopy-native, dependency-free) and a **real-browser Lighthouse protocol** (run via `npx`, so nothing is added to loopy's dependency-free `package.json`). Both produce an evidence artifact under `.superloopy/evidence/frontend/`. +"Looks good" is subjective; this gate makes selected UI claims measurable. It has two layers: a **partial color/spacing token lint** (Superloopy-native, dependency-free) and a **real-browser Lighthouse protocol** (run via `npx`, so nothing is added to Superloopy's dependency-free `package.json`). Use either layer only when its claim and risk are in scope. Both produce run-scoped evidence under `.superloopy/evidence/frontend/`, but neither substitutes for usability, renderer, or native-shell proof. -## Layer 1 — Design System Compliance (runnable, no deps) +The token-lint layer accepts `DESIGN.md` only when that file is the project's established design source or a scoped mapping/receipt synchronized with the real owner. If the project uses another authoritative source and no mapping is required, validate that source with its repository-native checks instead of creating `DESIGN.md` merely to run this helper. A narrow nonvisual change does not select either layer unless an existing gate, changed claim, regression signal, or risk makes it relevant. -The "measurable taste" gate: output must conform to its own DESIGN.md tokens. Run it and record the result as proof: +Create `EVIDENCE_ROOT` with the portable helper from `references/web.md`. The returned path already ends in the generated `YYYYMMDDTHHMMSSZ-` run ID; do not separately synthesize or set `RUN_ID`. Reuse that exact evidence root for the run. POSIX shells use: +```sh +set -u +EVIDENCE_ROOT="$(node "${FRONTEND_SKILL_DIR}/scripts/evidence-root.mjs" create frontend-quality)" || exit $? +[ -n "$EVIDENCE_ROOT" ] || exit 2 ``` -superloopy loop prove -- node skills/superloopy-frontend/scripts/ds-compliance.mjs DESIGN.md + +PowerShell uses: + +```powershell +$EVIDENCE_ROOT = node "${FRONTEND_SKILL_DIR}/scripts/evidence-root.mjs" create frontend-quality +if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($EVIDENCE_ROOT)) { exit 2 } +``` + +## Layer 1 — Partial color/spacing token lint (runnable, no deps) + +Run the compatibility filename `ds-compliance.mjs` directly when changed color/spacing tokens or their drift are in scope: + +```sh +node "${FRONTEND_SKILL_DIR}/scripts/ds-compliance.mjs" DESIGN.md dist/assets/app.css > "${EVIDENCE_ROOT}/token-lint.txt" 2>&1 +VALIDATION_STATUS=$? +node "${FRONTEND_SKILL_DIR}/scripts/evidence-root.mjs" verify "$EVIDENCE_ROOT" token-lint.txt +VERIFY_STATUS=$? +[ "$VALIDATION_STATUS" -eq 0 ] || exit "$VALIDATION_STATUS" +[ "$VERIFY_STATUS" -eq 0 ] || exit "$VERIFY_STATUS" ``` -It exits non-zero on any **undeclared hex color** or **off-scale spacing** (px not on the base unit; 0 and 1px allowed), with file:line. "Lighthouse 100 but 14 undeclared hex codes and 8 magic spacing values = NOT DONE." This is the part loopy can hard-gate by itself. +PowerShell uses the same inputs with native stream capture: + +```powershell +node "${FRONTEND_SKILL_DIR}/scripts/ds-compliance.mjs" DESIGN.md dist/assets/app.css *> "$EVIDENCE_ROOT/token-lint.txt" +$ValidationStatus = $LASTEXITCODE +node "${FRONTEND_SKILL_DIR}/scripts/evidence-root.mjs" verify $EVIDENCE_ROOT token-lint.txt +$VerifyStatus = $LASTEXITCODE +if ($ValidationStatus -ne 0) { exit $ValidationStatus } +if ($VerifyStatus -ne 0) { exit $VerifyStatus } +``` + +Inside an active loop, first run `superloopy loop guide --json`, then capture the active criterion with `superloopy loop prove --artifact "${EVIDENCE_ROOT}/token-lint.txt" -- node "${FRONTEND_SKILL_DIR}/scripts/ds-compliance.mjs" DESIGN.md dist/assets/app.css`. Replace the sample built-file path with the actual production output. + +It exits non-zero on undeclared **3-, 4-, 6-, and 8-digit hex** colors or off-scale integer/fractional/negative spacing values in the supported px declarations (absolute 0 and 1px are allowed), with file:line. A pass proves only those bounded checks; it says nothing about CSS color functions, variables, typography, component rules, semantics, accessibility, or overall system adoption. -## Layer 2 — Lighthouse (real browser, high floor) +## Layer 2 — Lighthouse (real browser, surface-selected categories) -Make performance/accessibility/best-practices/SEO a number the agent iterates against, not a vibe. +Select Lighthouse categories by the deployed surface and changed claim. Performance, accessibility, and best practices are selected when a changed claim, existing project gate, regression signal, or risk makes that browser audit relevant. SEO applies when the audited target itself is crawlable public Web. When the current task target is native or embedded, assess SEO only on a distinct deployed public Web target. HTML/CSS, WebView, canvas, or an embedded browser engine never makes SEO applicable by itself. -- **Measure through a real browser, never the CLI headless-shell or a dev server.** Build for production first, then audit the served build: `npx --yes lighthouse --output=json --output-path=.superloopy/evidence/frontend/lighthouse.json --only-categories=performance,accessibility,best-practices,seo`. (`npx` fetches at runtime; do not add lighthouse to `package.json`.) -- **Floor is high.** Aim 100 in every category; treat <90 as broken and 90-99 as work remaining. A 100 forces real fixes: semantic HTML, contrast, focus order, ARIA, sized media (CLS), LCP, SEO meta. -- **Discipline:** mobile preset primary (CPU throttle / slow network) + desktop secondary; run 3-5 times and take the median; parse the JSON `audits[*].score < 1` programmatically to locate offenders instead of eyeballing. -- **React:** for React projects, run `npx react-doctor@latest --json` (static render-perf scan) first and treat perf findings as blockers; optionally inject react-scan/lite to assert zero unnecessary renders. +- **Choose categories before running.** When the audited target is crawlable public Web, audit `performance,accessibility,best-practices,seo`. Otherwise omit SEO and record `SEO: N/A` with the concrete deployment reason, such as authentication, private access, native-only delivery, or an embedded client with no distinct public Web target. If Lighthouse cannot represent the deployed client, record why and use renderer or target-native proof for that claim. +- **Pin the audit tool to the actual runtime.** Set `LIGHTHOUSE_VERSION` to an exact reviewed semver, not `latest`. The [Lighthouse](https://www.npmjs.com/package/lighthouse) release reviewed on 2026-07-20 is `13.4.0`, which requires Node `>=22.19`; the retained Node 20 compatibility baseline is `12.8.2`. Record the chosen version and runtime reason, run `npx --yes "lighthouse@${LIGHTHOUSE_VERSION}" --version`, and treat a version change as an explicit toolchain change. +- **Measure through a real supported browser, never an unspecified bundled shell or a dev server.** Build for production, select the exact installed browser build and profile from the target-derived browser/OS/input and breakpoint matrix, then audit the served build: `npx --yes "lighthouse@${LIGHTHOUSE_VERSION}" https://staging.example.test/ --output=json --output-path="${EVIDENCE_ROOT}/lighthouse-mobile-run-1.json" --only-categories=performance,accessibility,best-practices`. Replace the sample URL and profile label, increment the run index for every execution, and add `seo` only when applicable. Never overwrite raw runs. (`npx` fetches the pinned tool at runtime; do not add Lighthouse to `package.json`.) +- **Use project budgets and regression truth.** Existing category budgets and the comparable production baseline take precedence. Without a project budget, aim for 100 where the product owns the result and investigate every regression. A score of 90-99 is not an automatic failure: inspect failed audits, attribute owned versus third-party/environment effects, and document accepted limitations. A score below 90 blocks the applicable category unless a concrete third-party or environment limitation is documented and accepted by the responsible owner; a material regression can block at any score. +- **Discipline:** choose representative mobile and/or desktop profiles from the deployed-surface contract; run 3-5 times, retain every indexed raw JSON, compute the median into `lighthouse-summary.json`, and parse each `audits[*].score < 1` programmatically to locate offenders instead of eyeballing. The summary must list its input filenames so the median is reproducible. +- **React Doctor:** when affected React code or risk justifies the scan, pin an exact `REACT_DOCTOR_VERSION` (the [React Doctor](https://www.npmjs.com/package/react-doctor) baseline reviewed on 2026-07-20 is `0.8.1`, requiring Node `^20.19.0 || >=22.13.0`), record it, and default to `npx --yes "react-doctor@${REACT_DOCTOR_VERSION}" --json --no-telemetry --no-supply-chain > "${EVIDENCE_ROOT}/react-doctor.json"`. Its normal CLI mode reports telemetry and enables an external supply-chain scan, so require informed opt-in before omitting either privacy flag; enable supply-chain analysis only when that external dependency-health claim is selected and approved. Triage each finding against the changed, affected, and owned code; only a reproduced issue, configured project gate, or material owned finding blocks completion. Ask before adding any runtime scanner dependency. ## Anti-gaming (reject-on-sight — never weaken UX to win the number) @@ -33,4 +70,4 @@ Win the score *in the architecture* (bundle splitting, hydration strategy, asset ## Evidence -Record a `PERF.md` under the evidence root summarizing: ds-compliance result, Lighthouse median scores per category (mobile + desktop), the specific audits fixed, and links to `lighthouse.json`. Close with the Superloopy evidence record pointing at it. A perf claim without the artifact is inconclusive, never a pass. +When measured-quality claims are selected, record `PERF.md` under `$EVIDENCE_ROOT` summarizing: partial color/spacing token lint result when used; selected Lighthouse categories and applicability reasons; project budget and comparable baseline; exact Lighthouse version, browser version/build and engine, OS, hardware or device profile, viewport/device scale factor, network/CPU profile, build identifier, URL, and run count; median scores per representative profile; owned versus external findings; accepted limitations; the specific audits fixed; and links to every indexed Lighthouse JSON plus `lighthouse-summary.json`. Record Node, React Doctor, or other tool versions when used, link `react-doctor.json`, and state whether telemetry and supply-chain analysis were disabled or explicitly accepted. Close with the Superloopy evidence record pointing at it. A selected performance claim without the artifact is inconclusive, never a pass. diff --git a/skills/superloopy-frontend/references/qt-qa.md b/skills/superloopy-frontend/references/qt-qa.md index 6d0a6e2..e8c53d7 100644 --- a/skills/superloopy-frontend/references/qt-qa.md +++ b/skills/superloopy-frontend/references/qt-qa.md @@ -1,20 +1,20 @@ # Qt QA -Apply this evidence gate to Qt Widgets, Qt Quick/QML, and mixed work. A successful build is necessary but does not prove native rendering or interaction. +Apply this evidence gate to Qt Widgets, Qt Quick/QML, and mixed work, including their WebAssembly routes when selected. Define the **validation scope** from the changed claims, affected journey, adjacent regression surface, and consequence risk. A successful build is necessary for executable changes but does not by itself prove real-target rendering or interaction. ## Commands -Record and run the repository's exact commands, with the candidate revision and build directory identifiable: +Record the repository's exact commands with the candidate revision and build directory identifiable. An implementation must run and pass all applicable commands; a plan must name the intended commands and clearly mark them as not yet run. -Every implementation or release-proof plan must explicitly name the configure/build, Qt Test/ctest, and repository lint/static gates. All applicable commands must run and pass. When no relevant repository lint/static check exists, mark that gate `N/A` with evidence instead of omitting it or claiming a pass. Missing required build or test infrastructure is a disclosed blocker, not `N/A` or a pass. +Every implementation or release-proof plan must explicitly classify the configure/build, Qt Test/ctest, and repository lint/static gates. An executable Qt UI change requires a real configure/build. A docs-only or otherwise non-executable contract change may mark configure/build `not applicable` with a reason. Run affected existing tests and add focused automated coverage for changed behavior where the repository supports it. Missing required build infrastructure is a blocker for an executable claim. Missing test infrastructure is a disclosed gap, not an automatic blocker for every narrow change; it becomes a blocker when the changed risk cannot be covered truthfully by another reproducible behavioral check or when release criteria require that test layer. Before returning a plan, include a **Repository gates** block: -- **Configure/build:** the applicable command that must run and pass, or `BLOCKED` when required infrastructure is missing. -- **Qt Test/ctest:** the applicable command that must run and pass, or `BLOCKED` when required infrastructure is missing. +- **Configure/build:** the applicable command and result, `BLOCKED` when required infrastructure is missing, or `N/A with reason` for a docs-only/non-executable change. +- **Qt Test/ctest:** the affected command and result; otherwise `GAP with evidence`, or `BLOCKED` when an unverified high-consequence behavior or release criterion requires it. - **Lint/static:** the applicable command that must run and pass, or `N/A with evidence` that no relevant repository check exists. -Never use `N/A` for a required build or test gate. +Never use `N/A` for a required executable build, and never turn an unverified changed behavior into a pass. - **Project configure/build:** the existing CMake, qmake, preset, or wrapper command for the real UI target. - **Qt Test/ctest:** the affected Qt Test executable or focused `ctest` invocation, followed by the project's required suite. @@ -26,15 +26,15 @@ Keep the command output as evidence. Use official [`qmllint`](https://doc.qt.io/ ## State matrix -Exercise every applicable row with both behavior checks and visual inspection: +Build the state matrix from the applicable changed journey and adjacent regression risk. Exercise every selected row with behavior checks; add visual inspection for a changed visual claim or an interaction with a visible-state/layout consequence. | Surface/state | Required coverage | | --- | --- | | Core control | Normal, hover when supported, pressed, focused, selected/checked, disabled, and inactive | | Transient/data | Popups/editors and empty, loading, and error states | -| Environment | Localization including CJK/RTL/long text, resizing at minimum and expanded sizes, and representative DPR/mixed-screen movement | +| Environment | Applicable localization including CJK/RTL/long text, target window classes or orientations, and representative supported DPR/display transitions | -Record a reason for every non-applicable state. Test keyboard, pointer, touch when supported, and assistive-technology actions against the same semantic outcome; interrupt motion and asynchronous state changes before also checking their settled state. +Record a reason for every non-applicable state. Test keyboard, pointer, touch, pen, switch, and assistive-technology actions when supported against the same semantic outcome; interrupt motion and asynchronous state changes before also checking their settled state. ## Deterministic branded gallery @@ -44,13 +44,13 @@ Keep Qt Test behavior and accessibility checks beside the gallery. An offscreen ## Capture contract -Capture the real target application built from the candidate revision in the named platform, style, theme, graphics backend, locale, and DPR. Wait for fonts, data, layout, transitions, and scene-graph presentation to settle, then show the exercised state rather than a replica or isolated mock. +When a visual claim changes, or an interaction claim has a visible-state or layout consequence, capture the real target application built from the candidate revision in the named platform, style, theme, graphics backend, locale, and DPR. Wait for fonts, data, layout, transitions, and scene-graph presentation to settle, then show the exercised state rather than a replica or isolated mock. A narrow nonvisual change still needs real-target behavioral evidence for the affected claim, but it may do so without a decorative screenshot. Use an OS-level capture for native window chrome, platform dialogs, IME/candidate UI, native menus, and separate-window popups. A `QQuickWidget::grabFramebuffer()`, `QQuickWindow`/client grab, offscreen renderer, virtual display, or headless image is functional evidence for the pixels it contains; **offscreen capture is not native evidence** and cannot verify surfaces outside that client/framebuffer boundary. If OS capture of a required target is unavailable, list that surface as unverified rather than substituting a browser or synthetic frame. ## `VISUAL_QA.md` fields -Create `VISUAL_QA.md` under the active evidence root and fill every field: +Create `VISUAL_QA.md` under the active evidence root for a visual claim or an interaction claim with a visible-state or layout consequence. For a narrow nonvisual change, record `VISUAL_QA.md: not applicable` with the reason in the run receipt and retain behavioral, accessibility, and regression evidence instead. When the file is required, fill every applicable field and mark unsupported fields not applicable with a reason: ```markdown Platform: @@ -77,6 +77,6 @@ Pixel diffs and ranked hotspots are **screenshot guidance, never a verdict**. Th ## Qt exclusions -Lighthouse is not Qt proof. React Doctor, CSS compliance, and a browser viewport matrix are also not evidence for a Qt surface. Use them only for a separately scoped web route; never substitute them for Qt build/tests, real-target interaction, or native capture. +Lighthouse is not Qt proof. React Doctor, CSS compliance, and a browser viewport matrix do not prove Qt build, QML behavior, or renderer semantics. Use them only for the separately scoped Web owner of an embedded client or Qt WebAssembly target; never substitute them for Qt build/tests, Qt-owned interaction/renderer proof, or applicable native capture. -The Qt gate passes only when the commands succeed, applicable states are exercised, findings are fixed and recaptured, and every uncaptured target surface is disclosed under `Unverified surfaces`. +The Qt gate passes only when the applicable commands succeed, selected states are exercised, visual findings are fixed and recaptured when relevant, and every claimed but unverified target surface is disclosed. diff --git a/skills/superloopy-frontend/references/qt-quick.md b/skills/superloopy-frontend/references/qt-quick.md index b5c1a29..221f9f8 100644 --- a/skills/superloopy-frontend/references/qt-quick.md +++ b/skills/superloopy-frontend/references/qt-quick.md @@ -31,9 +31,9 @@ Drive appearance from semantic model/control states such as enabled, pressed, ch ## Preserve interaction and accessibility -Use a standard Control when it supplies the required behavior. A custom interactive item is complete only when pointer, touch, keyboard, and assistive-technology actions invoke the same semantic command. Documented `Accessible` attached properties provide accessibility metadata, supported state flags, relationships, and actions; they do not replace ordinary properties required by a role. +Use a standard Control when it supplies the required behavior. A custom interactive item is complete only when every target-supported pointer, touch, keyboard, switch, and assistive-technology action invokes the same semantic command; unsupported input modes are not invented. Documented `Accessible` attached properties provide accessibility metadata, supported state flags, relationships, and actions; they do not replace ordinary properties required by a role. -For value roles, expose the declared-version-supported ordinary properties `value`, `minimumValue`, `maximumValue`, and `stepSize`. Availability derives from the item's `enabled` property. Keep supported attached metadata and state such as role, name, checked, and selected synchronized with the same visual/model truth, and wire supported accessibility actions to the same command as keyboard and pointer input. If required semantics are unavailable at the declared minimum version, use a standard Control that exposes them or a validated custom accessibility-interface path. Verify logical focus traversal, visible focus, cancellation, action parity, and change notifications. Follow [Accessibility for Qt Quick](https://doc.qt.io/qt-6/qml-qtquick-accessible.html). +For value roles, expose the declared-version-supported ordinary properties `value`, `minimumValue`, `maximumValue`, and `stepSize`. Availability derives from the item's `enabled` property. Keep supported attached metadata and state such as role, name, checked, and selected synchronized with the same visual/model truth, and wire each supported accessibility or input action to the same command. If required semantics are unavailable at the declared minimum version, use a standard Control that exposes them or a validated custom accessibility-interface path. Verify target-applicable focus traversal, visible focus, cancellation, action parity, and change notifications. Follow [Accessibility for Qt Quick](https://doc.qt.io/qt-6/qml-qtquick-accessible.html). Treat popup presentation as part of the component contract. Test the actual supported `Popup.Item`, `Popup.Window`, or `Popup.Native` path and its documented fallback; a separate or platform-native popup is not part of the parent scene's pixels. Guard `popupType` or any other newer API at the declared minimum Qt version. See the [Popup type](https://doc.qt.io/qt-6/qml-qtquick-controls-popup.html). @@ -46,7 +46,7 @@ Use repository commands and module metadata rather than linting an isolated file - run affected C++/Qt tests and [Qt Quick Test](https://doc.qt.io/qt-6/qtquicktest-index.html) cases; and - inspect startup and scene-graph warnings on the named graphics backend. -Exercise long translations, CJK and emoji fallback, RTL and mixed direction, enlarged text, keyboard and IME input, minimum and expanded window sizes, live resizing, representative DPRs, mixed-DPR screen moves, every popup/editor, and scene-graph initialization/failure handling. Profile an observed problem with the QML Profiler before rewriting bindings, delegates, loaders, or animations; speculative performance rewrites are not evidence. Follow Qt's [performance guidance](https://doc.qt.io/qt-6/qtquick-performance.html). +Exercise long translations, CJK and emoji fallback, RTL and mixed direction, enlarged text, supported keyboard and IME input, target-applicable minimum and expanded window classes, representative DPRs, affected popup/editor paths, and selected scene-graph initialization/failure handling. Add live resizing and mixed-DPR screen moves only when the named desktop/windowing target exposes them; on mobile/tablet, use the current-window, orientation, posture, inset, and display-transition cases selected by `mobile.md` instead of importing desktop-only behavior. Profile an observed problem with the QML Profiler before rewriting bindings, delegates, loaders, or animations; speculative performance rewrites are not evidence. Follow Qt's [performance guidance](https://doc.qt.io/qt-6/qtquick-performance.html). ## `QQuickWidget` mixed boundary @@ -54,10 +54,10 @@ Use this branch only when QML is embedded in a Widgets hierarchy, and apply [`qt Validate the boundary on the real target: -- traverse focus in both directions between Widgets and QML, then exercise text selection, CJK composition/preedit, candidate placement, and input-method dismissal; synthetic key events are not IME proof; -- resize continuously and move the host across screens with different DPRs, checking geometry, pointer coordinates, text/icon sharpness, and scene-graph recovery; -- compare active, inactive, disabled, light/dark, and runtime palette changes across the widget and QML sides instead of assuming a custom token bridge follows `QPalette`; -- open in-scene, separate-window, and native popups used by the product, checking focus, modality, placement, stacking, and fallback; and +- exercise bidirectional focus traversal between Widgets and QML when the named target supports hardware keyboard focus; on touch-only or switch-driven targets, prove the corresponding touch and assistive action crosses the boundary with the same result. Exercise text selection, CJK composition/preedit, candidate placement, and input-method dismissal when editable text is supported; synthetic key events are not IME proof; +- exercise continuous resize and mixed-DPR screen movement on desktop/windowing targets only when the named target exposes them. On mobile/tablet, use the selected current-window, orientation, posture, inset, and display-transition cases instead, checking target-applicable geometry, input coordinates, text/icon sharpness, and scene-graph recovery; +- compare active, inactive, disabled, light/dark, and runtime palette changes across the widget and QML sides only when the selected style and named target expose or claim those states, rather than assuming a custom token bridge follows `QPalette`; +- open the in-scene, separate-window, or native popup forms actually used by the affected journey, checking target-applicable focus, modality, placement, stacking, and fallback; and - treat `grabFramebuffer()` as an expensive readback of the Quick scene only. An offscreen/client grab cannot prove native widget chrome, platform dialogs, IME UI, or separate/native popups; capture those through the OS under [`qt-qa.md`](qt-qa.md). These constraints come from the official [`QQuickWidget` documentation](https://doc.qt.io/qt-6/qquickwidget.html). Do not claim native parity until both the embedded scene and every surface outside its framebuffer have real-target evidence. diff --git a/skills/superloopy-frontend/references/qt-widgets.md b/skills/superloopy-frontend/references/qt-widgets.md index e8eff73..0edeb37 100644 --- a/skills/superloopy-frontend/references/qt-widgets.md +++ b/skills/superloopy-frontend/references/qt-widgets.md @@ -1,6 +1,6 @@ # Qt Widgets route -Apply [`qt.md`](qt.md), then choose exactly one appearance strategy for each affected widget subtree. Preserve the repository's existing strategy unless the task explicitly changes ownership. +Apply [`qt.md`](qt.md) with the actual desktop or mobile/tablet target contract, or with the Web plus renderer contracts for Qt Widgets WebAssembly, then choose exactly one appearance strategy for each affected widget subtree. Preserve the repository's existing strategy unless the task explicitly changes ownership. Desktop window, menu, multi-screen, pointer, keyboard, shortcut, and mnemonic obligations apply only when the named target and supported inputs expose them; the mobile contract owns system bars, insets, lifecycle, orientation/posture, and touch behavior on mobile/tablet. WebAssembly keeps browser loading, input, accessibility integration, renderer, and supported-browser proof and does not inherit native-shell behavior. ## Strategy quick reference @@ -28,19 +28,19 @@ New presentation hooks default to no-op or legacy-equivalent output, and a froze Use layouts, truthful `sizeHint()`/`minimumSizeHint()`, and `QSizePolicy` to negotiate geometry. Do not position responsive content with fixed rectangles or compensate for a style by hard-coding its current metrics. Let [layout management](https://doc.qt.io/qt-6/layout.html), [size policies](https://doc.qt.io/qt-6/qsizepolicy.html), and the selected [QStyle](https://doc.qt.io/qt-6/qstyle.html) determine usable sizes; verify minimum, preferred, expanded, translated, and enlarged-font cases. -Consume semantic [QPalette](https://doc.qt.io/qt-6/qpalette.html) roles from the effective widget/style palette. Handle and test the `Active`, `Inactive`, and `Disabled` color groups instead of painting one literal "normal" state. Do not assume every native style paints every palette brush; inspect the rendered target when product color ownership is required. +Consume semantic [QPalette](https://doc.qt.io/qt-6/qpalette.html) roles from the effective widget/style palette. Handle and test the `Active`, `Inactive`, and `Disabled` color groups that the selected style and named target expose or that the product claims, rather than painting one literal "normal" state; record unsupported groups as not applicable with a reason. Do not assume every native style paints every palette brush; inspect the rendered target when product color ownership is required. ## Interaction and geometry Initialize the correct `QStyleOption` from the widget or delegate and carry its enabled, active, selected, pressed, direction, and focus state into style drawing. Ask [QStyle](https://doc.qt.io/qt-6/qstyle.html) for metrics, content sizes, sub-element rectangles, and subcontrol rectangles rather than duplicating native geometry. -A custom or composite control must have one semantic part model: the same part identifiers and rectangles drive **paint, event hit-testing, keyboard focus/activation, and accessibility**. A part that is only painted or pointer-clickable is incomplete. Use `QStyleOption` state for visible focus and preserve standard keyboard activation. For a `QStyle::ComplexControl`, make painting and `hitTestComplexControl()` agree; for an ordinary custom widget, use the same geometry in its pointer-event hit testing. Verify both paths at every scale and layout direction. +A custom or composite control must have one semantic part model: the same part identifiers and rectangles drive **paint, event hit-testing, supported-input focus/activation, and accessibility**. A part that is only painted or invocable through one supported input is incomplete. Use `QStyleOption` state for visible focus and preserve standard keyboard activation when hardware keyboard input is supported. For a `QStyle::ComplexControl`, make painting and `hitTestComplexControl()` agree; for an ordinary custom widget, use the same geometry in its applicable pointer, touch, or pen-event hit testing. Verify every supported path at each selected target scale and layout direction. For dynamic model/view rows, keep data in the model and implement appearance, `sizeHint()`, editing, and hit behavior with [QStyledItemDelegate](https://doc.qt.io/qt-6/qstyleditemdelegate.html). Do not create a persistent child-widget tree per row merely to style repeated content. Initialize and use the supplied style option so selection, focus, enabled state, palette, direction, and the current style remain coherent. ## Native and inclusive behavior -Keep native top-level chrome, menus, standard dialogs, focus conventions, and system shortcuts by default; brand the content area inside that boundary. If custom chrome is an explicit requirement, verify window movement/resizing, system controls, modality, multi-screen behavior, keyboard access, and assistive technology on every named target. +Keep target-native top-level chrome or system bars, standard dialogs, focus conventions, and platform actions by default as applicable; brand the content area inside that boundary. Desktop menus, window movement/resizing, system controls, system shortcuts, and multi-screen behavior are required only on named targets that expose those capabilities. If a custom replacement is explicit, verify the target-applicable window or inset behavior, modality, supported input, system actions, lifecycle, and assistive technology on every named target. Stock widgets already expose accessibility semantics. Custom widgets and virtual subparts must provide the corresponding `QAccessibleInterface` roles, names, states, values, relationships, and actions, then emit the appropriate accessibility event after state changes. Keep the accessible tree and bounds aligned with the shared paint/hit-test geometry; follow [Accessibility for QWidget Applications](https://doc.qt.io/qt-6/accessible-qwidget.html). @@ -50,10 +50,10 @@ Keep geometry and custom painting device-independent and provide high-density ra Use [Qt Test](https://doc.qt.io/qt-6/qtest-overview.html) to exercise behavior, not screenshots alone: -- pointer and keyboard paths trigger the same action exactly once; -- focus traversal, visible focus, shortcuts/mnemonics, enabled/disabled behavior, and cancellation work; -- paint and hit-test boundaries agree in LTR and RTL at representative scale factors; -- `Active`, `Inactive`, and `Disabled` palette groups remain legible under each supported style/theme; +- every supported pointer, touch, pen, keyboard, switch, or assistive action path triggers the same action exactly once; +- focus traversal, visible focus, shortcuts/mnemonics, enabled/disabled behavior, and cancellation work where the named target and supported inputs expose or require them; +- paint and hit-test boundaries agree in applicable LTR and RTL layouts at target-selected scale factors; +- each target-applicable `Active`, `Inactive`, and `Disabled` palette group remains legible under the selected supported style/theme; - dynamic rows preserve selection, focus, editing, accessibility, and size hints with long/CJK/emoji text; and - custom accessibility state/value changes emit the expected events. diff --git a/skills/superloopy-frontend/references/qt.md b/skills/superloopy-frontend/references/qt.md index 804bde1..8baccc0 100644 --- a/skills/superloopy-frontend/references/qt.md +++ b/skills/superloopy-frontend/references/qt.md @@ -1,6 +1,6 @@ # Qt common route -Load this contract before the Qt Widgets, Qt Quick/QML, or mixed route. It assumes a Qt 6 C++ desktop application unless the boundary section says otherwise. Preserve the repository's architecture and use the Qt route's evidence gates, not the web route's browser gates. +Load this contract before the Qt Widgets, Qt Quick/QML, or mixed route. This is a target-neutral Qt 6 common contract: compose it with `references/desktop.md` for a desktop target, `references/mobile.md` for a mobile/tablet target, or `references/web.md` plus `references/renderer.md` for Qt Widgets, Qt Quick/QML, or mixed Qt WebAssembly. Preserve the repository's architecture and use the Qt route's target-native evidence gates for Qt-owned non-Web pixels. A Qt WebEngine or embedded HTML client still composes `references/web.md` and `references/hybrid.md` with the target platform route; Qt build, interaction, or capture never substitutes for that client's browser, accessibility, lifecycle, and client-shell boundary proof. ## Establish the repository and target facts @@ -9,18 +9,20 @@ Before editing UI code, record: - whether the build uses CMake, qmake, or both, and which targets own the UI; - whether the surface is Widgets, QML, or mixed, including each technology boundary; - the declared **minimum Qt version**, taken from repository configuration rather than the locally installed SDK; -- every target OS and desktop environment, including the display server when it affects behavior; +- every target OS, device/window class, and supported input; on desktop, include the desktop environment and display server when they affect behavior; - the selected Qt style, where it is selected, and whether users or deployment can change it; - existing unit, interaction, accessibility, and visual tests; and -- the command and real target environment available for rendering and capture. +- the command and real target environment available for behavioral validation and, when a visual claim changes, capture. -Do not infer an unknown fact from the development machine. Ask one focused question when an unknown would change the implementation strategy; otherwise disclose it and constrain the claim. +Do not infer an unknown fact from the development machine. Ask the minimum necessary questions when unknowns would change the implementation strategy, and batch independent unknowns when more than one answer is required; otherwise disclose them and constrain the claim. ## Authority and ownership -Resolve conflicts in this order: existing architecture; target behavior and accessibility; product appearance expressed by `DESIGN.md`; version-matched Qt documentation; the target platform's HIG; optional visual inspiration. A lower authority may refine a decision but cannot overturn a higher one. +Existing architecture and the project's authoritative design source own app-defined product decisions, but they cannot override required target behavior or accessibility. Use version-matched Qt documentation for API and toolkit facts, the target platform's HIG for platform conventions, and optional visual inspiration only to refine decisions inside those boundaries. -`DESIGN.md` owns app-defined semantic roles for color, spacing, type, and motion. Map those roles at component boundaries and keep runtime-owned values live: do not freeze the **system palette, platform font, native metrics, focus geometry, or accessibility preferences** into product tokens. Use [QGuiApplication](https://doc.qt.io/qt-6/qguiapplication.html) palette and font only as application defaults, not as substitutes for the effective values after component inheritance or overrides. +The project's existing design source of truth remains authoritative. Use `DESIGN.md` as that owner only when the repository already establishes it. Otherwise, use `DESIGN.md` only as a scoped mapping/receipt when the repository has no equivalent or when it links every changed app-defined semantic back to, and stays synchronized with, the authoritative source. Never create a competing token source. A repository without a formal design-system document is not blocked: preserve its working Qt style, theme, palette, component, and platform conventions and document only the affected visual decisions. A narrow nonvisual change may record `Design impact: unchanged` and `Visual evidence: not applicable` with behavioral, accessibility, and regression evidence instead of creating design artifacts. + +Map app-defined semantic roles for color, spacing, type, and motion at component boundaries and keep runtime-owned values live: do not freeze the **system palette, platform font, native metrics, focus geometry, or accessibility preferences** into product tokens. Use [QGuiApplication](https://doc.qt.io/qt-6/qguiapplication.html) palette and font only as application defaults, not as substitutes for the effective values after component inheritance or overrides. - **Widgets** use the effective [QPalette](https://doc.qt.io/qt-6/qpalette.html) of the widget or style option and the current [QStyle](https://doc.qt.io/qt-6/qstyle.html) for style-owned geometry and metrics. - **Quick** resolves each live value at the type that owns it: palette comes from the effective [Item](https://doc.qt.io/qt-6/qml-qtquick-item.html#palette-prop), [Window](https://doc.qt.io/qt-6/qml-qtquick-window.html#palette-prop), [Control](https://doc.qt.io/qt-6/qml-qtquick-controls-control.html), or [ApplicationWindow](https://doc.qt.io/qt-6/qml-qtquick-controls-applicationwindow.html), while font comes from a concrete font-owning `Control`, `ApplicationWindow`, or [Text](https://doc.qt.io/qt-6/qml-qtquick-text.html) type. Combine those effective values with the selected Controls style, truthful [implicit sizes](https://doc.qt.io/qt-6/qml-qtquick-item.html#implicitWidth-prop), and [`Layout.*`](https://doc.qt.io/qt-6/qtquicklayouts-overview.html) constraints. A generic `Item` or `Window` must not be assumed to expose a font property. @@ -34,11 +36,11 @@ Check every referenced API and style behavior against the declared minimum Qt ve ## Common behavior contract -- **Focus and keyboard:** Preserve a logical focus chain, a visible focus indicator, standard activation keys, mnemonics/shortcuts where appropriate, and parity between pointer and keyboard actions. Derive platform timing, hover, activation, and focus behavior from [QStyleHints](https://doc.qt.io/qt-6/qstylehints.html) instead of hard-coded desktop assumptions. +- **Supported input and focus:** Enumerate the target-supported input modes, including pointer, keyboard, touch, pen, switch, and assistive actions where applicable. Preserve a logical focus chain, visible focus, standard activation, and parity across every supported way to invoke the same action. Do not impose hover, desktop mnemonics, or hardware-keyboard traversal on a touch-only target; do not omit them where the supported target requires them. Derive available platform timing, hover, activation, and focus behavior from [QStyleHints](https://doc.qt.io/qt-6/qstylehints.html) instead of hard-coded desktop assumptions. - **Accessibility:** Prefer controls with built-in accessibility. Every custom interactive element must expose its role, name, state, value, and available action, and must notify assistive technology when those change. Color, sound, animation, or pointer input cannot be the only carrier of meaning; see Qt's [accessibility guidance](https://doc.qt.io/qt-6/accessible.html). - **Text and direction:** Layouts must survive CJK line breaking, RTL and mixed-direction text, emoji, font fallback, translations longer than the source, and enlarged text. Use logical alignment and locale direction; do not encode left/right assumptions or fixed text boxes. Follow Qt's [internationalization guidance](https://doc.qt.io/qt-6/internationalization.html). -- **High DPI:** Keep UI geometry in device-independent coordinates, provide suitable image/icon representations, and test mixed-density multi-screen movement without assuming adjacent physical-pixel coordinates. Follow Qt's [High DPI model](https://doc.qt.io/qt-6/highdpi.html). -- **Motion and native chrome:** Motion must not carry required state and must honor the platform's available motion/accessibility preference; an older-version fallback removes nonessential motion. Keep top-level window chrome and platform dialogs native by default. A deliberate custom replacement must retain move, resize, system-menu, focus, keyboard, accessibility, and target-platform behavior. +- **High DPI and display transitions:** Keep UI geometry in device-independent coordinates, provide suitable image/icon representations, and test each target-supported DPR and display transition. Exercise mixed-density multi-screen movement only on targets that support it; exercise orientation, window-class, or external-display changes when those are the actual target transitions. Follow Qt's [High DPI model](https://doc.qt.io/qt-6/highdpi.html). +- **Motion and native chrome:** Motion must not carry required state and must honor the platform's available motion/accessibility preference; an older-version fallback removes nonessential motion. Keep target-native system chrome, system bars, top-level window chrome, and platform dialogs native by default as applicable. A deliberate custom replacement must retain the target's required move, resize, insets, system-menu, focus, input, accessibility, and lifecycle behavior. ## Disclosed boundaries @@ -46,19 +48,22 @@ This reference does not silently generalize across Qt editions or targets: - **Qt 5:** use its archived, version-matched documentation and audit every Qt 6 assumption and guard; - **language bindings:** verify binding names, ownership/lifetime, threading, and packaging in that binding's official documentation; -- **mobile or Qt for MCUs:** replace desktop interaction, windowing, and HIG assumptions with target-specific guidance and device evidence; +- **desktop:** load `references/desktop.md` and apply its window, chrome, desktop input, session, distribution, and packaged-lifecycle rules only where the named desktop target supports them; +- **mobile/tablet:** load `references/mobile.md`; its mobile guidance replaces desktop-specific input, window, display, chrome, lifecycle, and package checks while this common Qt ownership, version, accessibility, text, and rendering contract remains active; +- **WebAssembly:** load `references/web.md` and `references/renderer.md`; prove the production browser journey, Qt-owned renderer/semantics boundary, browser accessibility and input integration, loading/failure behavior, and supported browser matrix independently. Do not claim native shell, package, or platform-dialog behavior unless a separately named host owns and proves it; +- **Qt for MCUs:** use the product's constrained-device requirements and real-device evidence rather than borrowing desktop or general mobile assumptions; - **unknown Linux desktop:** identify desktop environment, style/theme, display server, scale, and input method before claiming native behavior. If they remain unknown, test stated representative combinations and mark the rest unverified. ## Qt pre-flight -This checklist replaces the web anti-slop, browser-breakpoint, and Lighthouse checklist for Qt work: +For Qt-owned non-Web pixels, this checklist replaces the Web anti-slop, browser-breakpoint, and Lighthouse checklist. It does not replace the Web + hybrid proof retained by a Qt WebEngine or embedded HTML client: - [ ] Build system, surface type, minimum Qt version, targets, selected style, tests, and capture path are recorded. - [ ] One appearance owner is selected for each surface, with mixed Widgets/QML boundaries explicit. - [ ] Newer APIs have guards, a fallback, and a testable boundary. -- [ ] `DESIGN.md` maps app semantics without freezing the system palette, platform font, native metrics, or accessibility preferences. -- [ ] Pointer and keyboard activation, focus order/visibility, and assistive-technology semantics agree. +- [ ] The existing design source remains authoritative; any `DESIGN.md` is either that established source or a synchronized scoped mapping, and app semantics do not freeze the system palette, platform font, native metrics, or accessibility preferences. +- [ ] Every supported input path (pointer, keyboard, touch, pen, switch, or assistive action as applicable) reaches the same truthful semantic outcome. - [ ] CJK, RTL, emoji/font fallback, long translations, and enlarged text remain usable. -- [ ] High-DPI and mixed-screen behavior use device-independent geometry and suitable assets. -- [ ] Motion preferences and native top-level chrome are preserved or an explicit replacement is fully verified. -- [ ] Validation runs on the named target style/platform and produces a real Qt capture plus behavioral evidence. +- [ ] High-DPI geometry and assets are correct across each target-supported DPR and display transition; unsupported transitions are recorded as not applicable. +- [ ] Motion preferences and target-native system chrome are preserved or an explicit replacement is fully verified. +- [ ] Validation runs on the named target style/platform and produces behavioral evidence; a changed visual claim or interaction with a visible-state/layout consequence also produces the applicable real-target capture. diff --git a/skills/superloopy-frontend/references/redesign.md b/skills/superloopy-frontend/references/redesign.md index 59ea5f9..6cd869e 100644 --- a/skills/superloopy-frontend/references/redesign.md +++ b/skills/superloopy-frontend/references/redesign.md @@ -1,32 +1,34 @@ -# Redesign Protocol +# Web Site Redesign Protocol -Greenfield rules applied to a living site are how redesigns go wrong: the model overwrites a working brand with its own taste, breaks SEO, and silently renames things analytics depends on. A redesign is evidence-first work — audit what exists, classify the mode, then change the least that satisfies the brief. +This is the Web-only protocol for redesigning a living site. Native and embedded-shell redesigns use the shared UX and claim-triggered visual/spatial overlays plus their platform and composition references; they do not inherit the browser, SEO, analytics, or real-browser rules below. + +Greenfield rules applied to a living site are how redesigns go wrong: the model overwrites a working brand with its own taste, breaks applicable discovery contracts, and silently renames things analytics depends on. A redesign is evidence-first work — audit what exists, classify the mode, then change the least that satisfies the brief. Apply this protocol only when a visual Web redesign is actually in scope; a narrow nonvisual change does not require a redesign audit, design artifact, anti-slop pass, or visual capture. ## Mode detection (first action) - **Greenfield** — no existing site, or a full restart is explicitly approved. Normal skill flow applies. -- **Preserve** — modernize without breaking the brand. Audit first, extract the existing tokens into DESIGN.md, evolve gradually. -- **Overhaul** — new visual language over existing content. Treat visuals as greenfield; preserve content and information architecture. +- **Preserve** — modernize without breaking the brand. Audit the existing authoritative design source first and evolve it gradually; do not move ownership into a new file. +- **Overhaul** — new visual language over existing content. Treat visuals as greenfield; preserve content and information architecture unless a separate IA change is actually in scope and explicitly approved from the recorded audit. -If the mode is ambiguous, ask exactly once: *"Should this redesign preserve the existing brand, or start visually from scratch?"* Misclassifying the mode is the single biggest source of bad redesign output. +If the mode is a material unknown that cannot be resolved from the brief, repository, and current surface, ask whether to preserve the existing brand or start visually from scratch. Ask the minimum necessary questions and batch independent material unknowns instead of forcing a separate one-question round trip. Otherwise state the evidence-based mode, assumption, and confidence. ## Audit before touching (recorded as evidence) Write `REDESIGN_AUDIT.md` under the evidence root before proposing changes, covering: -- **Brand tokens** — primary/accent colors, type stack, logo treatment, radii. These seed DESIGN.md; in preserve mode they *are* the token contract's starting values. +- **Brand tokens** — primary/accent colors, type stack, logo treatment, radii, and their authoritative repository owner. In preserve mode that existing owner remains the starting contract; `DESIGN.md` is used only when already established or as a synchronized scoped mapping/receipt. - **Information architecture** — page tree, primary nav, key conversion paths. - **Content blocks** — what exists, what is doing work, what is filler. - **Patterns to preserve** — signature interactions, a recognizable hero, the copy voice. - **Patterns to retire** — anti-slop tells, broken layouts, dead links, generic stock imagery, performance traps. -- **Dial reading of the existing site** — infer its current `DESIGN_VARIANCE` / `MOTION_INTENSITY` / `VISUAL_DENSITY`; that reading, not the greenfield baseline, is the starting point. -- **SEO baseline** — ranking pages, meta titles, structured data, OG cards. Losing rankings is the #1 redesign risk. +- **Visual-direction reading of the existing site** — record how far its visual language departs from the established system, the quantity and complexity of motion, and its information and spacing density. Ground each description in concrete evidence such as component variation, transition count and duration, content per viewport, and whitespace rhythm; this observed reading, not a greenfield preset, is the starting point. +- **Discovery baseline** — for the current crawlable public Web target, or a distinct deployed public Web target in scope, record ranking pages, meta titles, structured data, and share metadata. For authenticated, private, native-only, or embedded-only delivery without a distinct public deployment, record `SEO: N/A` with the concrete deployment reason; visual Web technology alone does not make SEO applicable. Evidence availability is part of the audit. When analytics, Search Console, ranking history, or another source is unavailable, mark that field **unavailable and unverified, never guessed**. State whether the missing source blocks a named success criterion; otherwise continue with the observable repository/browser evidence and preserve the unknown contract conservatively. ## Preservation rules -- **Information architecture stays** unless the user asks: page slugs, anchor IDs, and primary nav labels remain stable for SEO and muscle memory. +- **Information architecture stays by default.** Page slugs, anchor IDs, primary nav labels, and conversion paths remain stable for discovery and muscle memory. Change broken IA only when information architecture is actually in scope, the audit identifies the concrete failure and downstream impact, and the user explicitly approves that IA change; a visual overhaul alone is not approval. - **Extract brand colors before applying the anti-slop palette bans.** A brand that is already purple stays purple — that is the named override path, not a violation. - **Copy voice is preserved** unless a rewrite is requested; visual modernization is not a content rewrite. - **Existing accessibility wins never regress** — focus states, alt text, keyboard nav, contrast. @@ -41,16 +43,16 @@ URL structure and route slugs; primary nav labels; form field names; the brand l 1. **Typography refresh** — the biggest visual lift per unit of risk. 2. **Spacing and rhythm** — section padding, vertical rhythm. 3. **Color recalibration** — desaturate, unify neutrals, keep the brand accent. -4. **Motion layer** — dial-appropriate micro-interactions on existing components (`references/motion.md`). +4. **Motion layer** — evidence-backed micro-interactions that fit the preserved visual direction and existing components (`references/motion-core.md`, plus `references/motion.md` only for Web implementation). 5. **Hero and key-section recomposition** — restructure the top of the funnel. 6. **Full block replacement** — only when a block is unsalvageable. ## Decision tree - IA, content, and SEO are sound → **targeted evolution** (levers 1–4): most of the value at a fraction of the risk. -- Visual debt is structural (broken IA, no design system, broken mobile) → **full redesign** with strict content preservation. +- Visual debt is structural (for example incoherent or unowned visual semantics or broken supported-target adaptation) → **full visual redesign** with strict content and information-architecture preservation. Broken IA is a separate change: include it only when it is actually in scope and explicitly approved from the audit. The absence of a formal design-system document alone is not structural failure. - The brand itself is changing → treat as **greenfield**. -Completion still runs the full skill gates: DESIGN.md contract (seeded from the audit in preserve mode), anti-slop pre-flight, real-browser visual QA at all breakpoints, and the evidence record — plus `REDESIGN_AUDIT.md` proving the before-state was read, not guessed. +For an actual redesign, completion preserves the project's authoritative design source and uses `DESIGN.md` only when it is established or a synchronized scoped mapping. Run anti-slop only for its declared marketing/editorial or approved new-visual-direction scope. Capture real-browser visual QA across the target-derived browser/OS/input and breakpoint matrix, exercise the affected and adjacent journeys, and retain the evidence record plus `REDESIGN_AUDIT.md` proving the before-state was read rather than guessed. SEO proof remains limited to the current crawlable public Web target or a distinct deployed public Web target in scope. Selected redesign mechanisms were adapted under MIT from Taste Skill; see `references/upstream-notice.md`. diff --git a/skills/superloopy-frontend/references/renderer.md b/skills/superloopy-frontend/references/renderer.md new file mode 100644 index 0000000..4f79619 --- /dev/null +++ b/skills/superloopy-frontend/references/renderer.md @@ -0,0 +1,27 @@ +# Renderer and Composition Contract + +Use this when the UI is canvas-, engine-, scene-, texture-, or otherwise custom-rendered, or when one surface mixes DOM, native control, and custom regions. Renderer choice changes what can be assumed about semantics, text, input, accessibility, performance, and crawlability. + +## Classify composition + +For each region, record whether DOM, native control, custom renderer, or mixed composition owns pixels, layout, hit testing, text, focus, and semantics. Name overlays and portals explicitly. Do not infer a semantic tree from visual resemblance or infer native behavior from a toolkit brand. + +The semantic and accessibility owner must expose useful names, roles, states, actions, relationships, bounds, reading/traversal order, live status, and focus. If an accessibility bridge or overlay mirrors scene data, prove synchronization during mutation, scroll, zoom, occlusion, virtualization, and teardown. + +## Text and input + +Prove text shaping, fallback, locale, bidirectionality, selection, clipboard, input, focus, IME composition, caret geometry, editing, validation, and assistive-technology interaction. A drawn text field is not an input capability until it implements the relevant semantic edit/validate/commit/cancel contract. + +Map pointer, touch, keyboard, stylus, controller, and accessibility actions only where supported. Preserve equivalent semantic results and failure truth. Avoid invisible DOM or native controls that drift from the pixels they claim to represent. + +## Scaling and performance + +Build a **target-applicable scaling matrix** from device-pixel ratio (DPR), zoom, text scaling, resize/window class, orientation, safe areas/insets, large content, reduced motion, contrast/theme changes, and capture/compositing paths. Test orientation and safe areas only when the named target supports or exposes them, and record the rest as not applicable rather than simulating a foreign platform. Measure frame pacing, input latency, memory, startup, and power only when those claims matter; a fast synthetic scene does not prove a real journey. + +Crawlability is a separate capability to prove when the current renderer target is deployed as crawlable public Web. For native or embedded current targets, assess it only on a distinct public Web deployment. Canvas, WebAssembly, engine output, or a hidden DOM fallback does not become indexable by declaration. + +## Renderer evidence + +Use semantic-tree inspection, input/IME journeys, screen-reader or platform accessibility checks, representative scaling/content matrices, and performance traces as applicable. Screenshot or pixel similarity can aim a visual review but is not capability promotion and cannot prove semantics, interaction, native integration, or crawlability. + +Record renderer/backend/provider/version, composition boundaries, accessibility bridge, target hardware/software, build flags, and known fallbacks. Treat missing target tooling as an explicit unverified claim or blocker. diff --git a/skills/superloopy-frontend/references/system-map.md b/skills/superloopy-frontend/references/system-map.md index 749e319..417554a 100644 --- a/skills/superloopy-frontend/references/system-map.md +++ b/skills/superloopy-frontend/references/system-map.md @@ -2,14 +2,18 @@ Use this reference only when the work has an **actual platform contract**: the repository already depends on a system, the UI runs inside a host that requires one, the user explicitly requests adoption, or an organization or public-service policy names it. Visual resemblance is not a contract. “Microsoft-like,” “GitHub-like,” or “Material-inspired” without one of those signals stays on the brand/mood route. +This map does not replace the project's authoritative design source or make a migration, redesign, anti-slop pass, SEO audit, or visual artifact automatically applicable. A narrow nonvisual change may retain the current system and use behavioral, accessibility, and regression proof. SEO remains limited to the current crawlable public Web target or a distinct deployed public Web target in scope. + ## Decision sequence -1. Inspect `package.json`, lockfiles, imports, component directories, and DESIGN.md. Preserve the existing stack and its working accessibility behavior. -2. Identify the contract owner: existing dependency, embedded host, explicit user choice, or documented policy. Record that evidence in DESIGN.md. +1. Inspect `package.json`, lockfiles, imports, component directories, the current rendered surface, and the repository's authoritative design source (`DESIGN.md` only when the repository establishes it). Preserve the existing stack and its working accessibility behavior. +2. Identify the contract owner: existing dependency, embedded host, explicit user choice, or documented policy. Record that evidence in the authoritative source or existing project receipt. If a separate `DESIGN.md` is useful, it must be a scoped mapping that links back to and stays synchronized with that owner, never a competing source. 3. Verify the current official documentation, maintenance status, framework/version support, license, and migration guidance. Package recommendations drift. -4. If the required package is missing, explain the impact and get **explicit approval before installing** it. Never run an install command merely because a brief resembles a brand. +4. If the required package is missing, explain the impact and get **explicit approval before installing** it. Never run an install command merely because a brief resembles a brand. After approval, re-check the package's current official setup and the repository's package manager: when implementation was requested, execute the one verified package-manager command; when the user requested advice only, present that one command without running it. 5. Keep **one primary appearance owner per surface**. Migration shells and low-level utilities may coexist behind explicit boundaries, but two competing component systems must not style the same subtree. -6. Validate labels, semantics, keyboard behavior, focus, contrast, and application states locally. An official system provides primitives; it does not make the application accessible automatically. +6. After any approved install or migration step, verify lockfile/package resolution, imports/build integration, and the affected labels, semantics, keyboard behavior, focus, contrast, and application states in the target-derived browser/OS/input and breakpoint matrix, expanding only for changed claims or risk. An official system provides primitives; it does not make the application accessible automatically. + +When an unknown owner, policy, migration boundary, or package choice would materially change the result, ask the minimum necessary questions and batch independent unknowns. Otherwise disclose the uncertainty and constrain the claim. ## Contract map @@ -44,9 +48,9 @@ Glassmorphism, bento grids, brutalism, editorial layouts, dark-tech terminals, a Apple Liquid Glass is an Apple-platform material. A web build using `backdrop-filter`, layered borders, and highlights is a labeled approximation. Start with a readable solid fill, enhance only when transparency support is available, and honor `prefers-reduced-transparency` where the browser implements it; that experimental query cannot be the only fallback. -## Commands are proposals, not authorization +## Approved implementation versus advice-only commands -After approval, output only the command that matches the verified route and package manager. Re-check official setup instructions first; examples include: +The examples below are candidates, not authorization. Re-check current official setup instructions and the repository package manager first. After explicit approval, an implementation request executes exactly the verified matching command and then performs the integration checks from step 6; an advice-only request presents exactly that command and does not execute it. Do not dump alternatives after the route is known. ```bash npm install @fluentui/react-components diff --git a/skills/superloopy-frontend/references/upstream-notice.md b/skills/superloopy-frontend/references/upstream-notice.md index a020260..7176aaa 100644 --- a/skills/superloopy-frontend/references/upstream-notice.md +++ b/skills/superloopy-frontend/references/upstream-notice.md @@ -1,14 +1,24 @@ -# Taste Skill Upstream Notice +# Frontend Reference Provenance -Selected dial-inference, design-system selection, motion-template, and redesign-audit mechanisms in this frontend skill were adapted from `Leonxlnx/taste-skill`. +Selected dial-inference, design-system selection, motion-template, and redesign-audit mechanisms in the optional adapted Web references were adapted from `Leonxlnx/taste-skill`. - Source repository: https://github.com/Leonxlnx/taste-skill - Source file at adaptation time: `skills/taste-skill/SKILL.md` - Source license: MIT, copyright 2026 Leonxlnx -- Superloopy boundary: the Web/Qt router, evidence gates, dependency-approval rule, corrected responsive/reduced-motion templates, and audit integration are maintained here. No upstream runtime dependency is installed. +- Superloopy boundary: the shared UX, platform, and composition router is Superloopy-native, as are its cross-platform evidence ownership rules. The optional adapted Web references retain their attribution here; Superloopy maintains the evidence gates, dependency-approval rule, corrected responsive/reduced-motion templates, and audit integration. No upstream runtime dependency is installed. The upstream copyright and permission notice is retained below because substantial tables and code-template mechanisms were adapted. +## StyleGallery research boundary + +StyleGallery was reviewed as an independent architecture research reference for conditional spatial ownership, implementation-neutral motion review, visual-reference authority, high-impact decision records, and bounded design debt. It is not a source for the separate user-reported UX fixes that preceded this quality overlay. + +- Source repository: https://github.com/changeroa/StyleGallery +- Reviewed revision: `24ae581e92666a8864d42ffa64f7c34caf3a2156` +- License boundary: no root license was identified in the reviewed snapshot. +- Reuse boundary: no code, prose, schema, or pattern was copied. Superloopy's contracts and tests are independently authored from general interface-design principles and repository observations. +- Dependency boundary: no StyleGallery runtime, package, generated file, or vendored dependency is installed. + ## MIT License Copyright (c) 2026 Leonxlnx diff --git a/skills/superloopy-frontend/references/ux.md b/skills/superloopy-frontend/references/ux.md new file mode 100644 index 0000000..0da7a1f --- /dev/null +++ b/skills/superloopy-frontend/references/ux.md @@ -0,0 +1,162 @@ +# Shared UX Contract + +Use this reference once for every supported screen-based application UI route. It defines user-facing truth, implementation traceability, and proportional evidence; platform, shell, renderer, Web, and Qt references add their own ownership rules. + +## Five-stage workflow + +1. **Baseline and frame the delta.** Reproduce the current-state journey on a representative target before proposing changes. Locate the existing component, state owner, command or service owner, and tests. Name the affected users, job, desired outcome, affected journey, and adjacent journey that could regress. Mark every material statement as evidence, assumption, or unknown and record confidence. +2. **Map users, journeys, and capabilities.** Trace entry, action, system response, recovery, and exit. Inventory the capabilities and owners at every step. Apply the risk-gated user coverage matrix below; do not turn one report or an assumed persona into a universal requirement. +3. **Specify behavior, states, and invariants.** Define semantic results, operating states, failure truth, content, editable-text behavior, accessibility, adaptation, persistence, and privacy boundaries before polishing appearance. +4. **Trace implementation and choose claim-shaped evidence.** Maintain one row per material change: `contract clause or invariant -> acceptance case or criterion -> implementation owner and file -> test -> evidence artifact`. A requirement without an implementation and proof path is incomplete. +5. **Promote capability by capability.** Match each claim to the narrowest evidence that proves it on the actual target. Move simulated work toward production only with new real-target evidence for that capability; a neighboring proven capability does not promote it. + +## Proportional artifacts + +`UX_CONTRACT.md` is required for an expanded journey and for a new/redesigned/high-consequence surface; a narrow fix uses the compact evidence below. + +- **Narrow fix:** record the UX delta, affected journey, adjacent journey regression risk, and behavioral, accessibility, and regression evidence in the existing receipt. For a narrow nonvisual change, record `Design impact: unchanged` and `Visual evidence: not applicable` with reasons; neither a new design-system artifact nor `VISUAL_QA.md` is required. If behavior also does not change, record `UX impact: unchanged` and why. +- **Expanded journey:** write a scoped `UX_CONTRACT.md` with the baseline, journey, user coverage, capabilities, owners, operating states, invariants, traceability rows, risks, and evidence matrix. +- **New/redesigned/high-consequence surface:** write `UX_CONTRACT.md` with fuller discovery, content, accessibility, localization, adaptation, privacy, usability, and promotion evidence. + +Do not demand exhaustive personas, every-route screenshots, or a new design-system artifact for a narrow change whose risk does not justify them. Visual artifacts are conditional on visual claims; behavioral and accessibility claims require their own evidence. + +## High-impact decisions and genuine alternatives + +A high-impact judgment is one that can approve, block, redirect, or materially expand implementation; change information architecture, capability reachability, accessibility, user coverage, dependency, or design authority; or assert that a result is usable, clear, better, premium, or otherwise successful for people. Ordinary descriptive prose and mechanical conformance to an existing contract are not high-impact decisions. + +Use a compact record for a narrow judgment: `decision -> affected user/task/target -> authority or criteria -> selected option -> warrant and evidence -> limitation or counterexample`. Add alternatives and tradeoffs only when more than one plausible option was genuinely considered. When the existing authoritative source fixes the result, record `Alternatives: not applicable — existing authoritative contract fixes the result` rather than manufacturing choices. + +An approved design source proves intended visual direction and fidelity, not usability, accessibility, performance, privacy, or platform fit. Expert judgment may open `review_required`; it becomes a blocker only when linked to an applicable contract, consequence, or evidence. `review_required` is a decision disposition, not a capability-record Verification value; keep the underlying capability `failed`, `inconclusive`, `blocked`, or `unverified` until its evidence changes. A screenshot proves rendered state, and a validator proves only the rule it executes. + +## Task-based usability evidence + +For a new, redesigned, or high-consequence journey whose task success or usability is claimed or materially uncertain, record the task, representative context, participant or reviewer, observed result and friction, and evidence limitation. Scale the participant set and protocol to reach, novelty, uncertainty, reversibility, and consequence; a narrow correction does not require a study merely to close its regression. + +A cognitive walkthrough or expert inspection is heuristic evidence, not user evidence. It may identify a plausible failure and justify focused validation, but it cannot establish prevalence, user preference, or successful real-world task completion. + +## Accepted UX debt + +UX debt is a known, consciously accepted gap inside an otherwise bounded delivery. It is not an unknown claim, a `not applicable` capability, an uninvestigated risk, an out-of-scope future feature, or a deferred capability. Debt never changes failed or unverified evidence into a pass. + +Record `gap -> affected users/journey/target/capability -> consequence and claim limitation -> known evidence and remaining gap -> mitigation or fallback -> acceptance authority -> accountable owner -> review trigger -> exit condition and required proof`. The acceptance authority must own the affected release or risk boundary; absent that authority, keep the decision `review_required` or blocked according to consequence. Here, accountable owner means the person or qualified role responsible for resolution; it is distinct from the technical `surfaceEvidence.owner` such as `native`, `browser`, or `renderer`. Use the project's existing issue tracker when it already owns follow-up; do not create a shadow backlog. + +False success, a blocked core task, a critical accessibility barrier, an active privacy or security violation, or data loss without truthful containment and recovery cannot be accepted as ordinary UX debt. Treat an accessibility gap as critical when it prevents a supported user from completing a core task, escaping a state, perceiving necessary state or feedback, or recovering safely, or when it breaches an applicable release contract; use an established project severity rubric when one exists instead of inventing a universal score. Reduce scope or keep the result blocked or unverified. An unknown owner yields `review_required` for a low-consequence limitation and remains blocked when the consequence requires accountability; never invent a person or use an unqualified `team` placeholder. + +## Risk-gated user coverage matrix + +For a narrow low-risk fix, include only materially affected and adjacent users and explain why broader rows are not applicable. Expand coverage with reach, novelty, uncertainty, reversibility, and consequence. A new, redesigned, broadly used, accessibility-sensitive, privacy-sensitive, or high-consequence journey records: + +| Coverage | Record | +| --- | --- | +| Population | Primary, secondary, affected, not affected, and counterexample users | +| Context | Role, expertise, accessibility needs, locale/direction, device/window, input, connectivity, account state, and entitlement | +| Impact | Job, current evidence, assumption/unknown, frequency, severity, and success measure | + +Use representative research or product evidence where the claim requires it. A single issue is valid evidence for that reported failure, not prevalence across all users. Record excluded groups and unknown reach rather than inventing certainty. + +## Capability record + +Track these dimensions independently and scope every record to the named target, build, and version: + +| Dimension | Values | Meaning | +| --- | --- | --- | +| Role | `action`, `navigation`, `input`, `output` | The semantic job. Decoration is not a capability. | +| Applicability | `applicable`, `not applicable` | Whether the capability belongs to this target and delivery scope. | +| Fidelity | `production`, `simulated`, `deferred` | Whether the real owner performs the result, a coherent prototype substitutes for it, or it is intentionally absent. | +| Availability | `available`, `temporarily unavailable`, `unavailable`, `not evaluated` | Whether the real capability owner can currently provide the result. | +| Presentation | `visible`, `hidden`, `omitted`, `passive` | How the current surface exposes or intentionally does not expose it. | +| Invocability | `enabled`, `disabled`, `not invocable` | Whether this affordance can currently initiate the capability. | +| Feedback/result | `idle`, `pending`, `succeeded`, `failed`, `blocked`, `not evaluated` | The observable state of the current attempt, tied to the real owner. | +| Verification | `proven`, `failed`, `inconclusive`, `blocked`, `unverified` | What current evidence establishes on the named target. | + +For early design, record semantic intent and intended owner. Before production promotion, name the concrete command or handler, state owner, observable outcome, failure and recovery, lifetime, and evidence. Conditions such as role, entitlement, policy, permission, platform, connectivity, or service health are facts beside Availability, not hidden inside a status label. + +Keep the dimensions orthogonal. For a capability that is `not applicable`, set Availability to `not evaluated`, Presentation to `omitted`, Invocability to `not invocable`, and Feedback/result to `not evaluated`. An available capability may have a disabled affordance because a current prerequisite is unmet; disabled does not mean unavailable. A hidden capability is not unavailable merely because policy, role, or context suppresses its presentation. Pending, succeeded, failed, and blocked feedback/result states must follow the durable owner rather than a local animation, handler call, or toast. + +## Functional truth + +- A visible, enabled production affordance performs its advertised semantic outcome. Calling a handler, showing a toast or spinner, or reaching a mock adapter is not completion. +- A real failed operation is honest only when it avoids false success, reports the resulting state, and offers recovery or a next step. +- Simulated prototype capabilities may be enabled and coherent, but they cannot satisfy production or native acceptance until reclassified and proved against the real owner. +- A deferred future capability is normally omitted and has no operable affordance. For the current delivery its Applicability is `not applicable`, Availability is `not evaluated`, Presentation is `omitted`, Invocability is `not invocable`, Feedback/result is `not evaluated`, and Verification remains `unverified`. +- A stable top-level information architecture may retain an honestly unavailable signpost when product or platform convention makes that location meaningful. Record it as a separate passive output capability with `production` fidelity, `available` availability, `passive` presentation, and `not invocable` invocability, then verify it independently as `proven`, `failed`, `inconclusive`, `blocked`, or `unverified`. It is never the deferred navigation affordance and must not be a button, link, or focus action. +- A temporarily unavailable state requires a real prerequisite, an accessible reason, and a useful next step. `unavailable` represents a durable role, policy, platform, or product constraint and must not masquerade as a temporary outage. +- Decoration has no action, focus stop, interactive role, or misleading hover or pressed treatment. +- An editable-looking control must support edit, validate, commit, and cancel behavior. A permanent read-only value appears as output or is clearly read-only in appearance and semantics. + +## Operating-state contract + +Select states from real ownership and risk, not a universal screenshot quota. For each applicable state name its trigger, owner, visible and semantic result, allowed actions, preserved data, recovery, and proof: + +- initial, loading, empty, partial, stale, offline, degraded, error, retry, and cancel; +- optimistic updates, duplicate submission, conflict, and concurrent edit; +- authentication, session expiry, authorization, permission denial, role, and entitlement changes; and +- privacy, consent, and sensitive-data display, storage, sharing, or redaction. + +Do not strand users in indefinite loading, erase recoverable input, expose protected data while permissions settle, or report success before the durable owner confirms it. + +## Reduce input burden and support editable text + +For a constrained identifier with an authoritative source, prefer a suitable platform, toolkit, provider, or portal picker, then search, detection, suggestion, and recent values as the source permits. There is no universal picker for every path, font, executable, account, or expert identifier. Validated manual entry remains legitimate for arbitrary or expert data and as an explicit fallback. + +Name validation timing, format, recovery, privacy, permissions, cancellation, and the resulting stored value. A picker dialog alone does not prove that the chosen value works after packaging or restart. + +Editable text defines typing, selection, clipboard, undo/redo, replacement, composition and IME, validation, commit, and cancel. Never validate or submit an incomplete composition; apply validation after composition commit unless the target's documented contract requires otherwise. Test the actual input method and representative scripts when text input is material. + +## Undo and reversible work + +Undo is a domain command model, not a dump of UI events. Record user-visible intent, coalesce continuous changes, remove no-op commands, show a meaningful label for the next operation, and restore visible state. Passive hover, focus, scroll, and navigation noise is excluded by default; selection, navigation, and resize belong only when one is itself a meaningful mutation users reasonably expect to reverse. State Undo applicability explicitly for editor-like or reversible workflows instead of forcing it onto every form. + +Test command boundaries, coalescing, no-op removal, redo invalidation, labels, restored selection/focus where relevant, and the real model result. + +## State and persistence + +Separate application defaults, user preferences, system-owned preferences, document or model state, scene or window restoration, sensitive values, and ephemeral UI. Name one owner and serialization boundary for each. + +Every durability claim names its boundary: same view, revisit, restart, process recreation, supported upgrade, device transfer, or durable account state. Do not turn a toolkit API into a promise about reinstall or cross-device survival. A migration is explicit, idempotent where required, and scoped to supported version, package, and distribution channel. Prove old-to-new behavior with representative stored data and failure recovery. + +## Resource identity, lifecycle, and reset provenance + +Map lifecycle verbs to distinct semantic transitions in the product model. For each displayed operation such as Save, Save As, Apply, Revert, or Reset, name its source state, destination identity, durable or runtime owner, and resulting dirty or pending state rather than assuming the label fixes the behavior. Keep the canonical resource, editable working copy, last-saved resource, active resource, and dirty or pending state visible and attributable whenever their difference changes the next action, loss risk, or runtime result. A copy or identity change discloses the resulting identity and whether the canonical, saved, or active resource changed. + +When the product contract requires portability, preserve a stable logical identifier or an owned-root-relative locator. An absolute locator remains valid when the actual platform, provider, or integration owner requires it; record the explicit reason and resolution boundary, then provide a relink or recovery path when it no longer resolves. A copy-based workflow remains valid when its identity and lifecycle effects are truthful. + +For each user intent, prefer the shortest truthful transition. When the current owner can safely update or reload the original, do not require an unnecessary import-modify-apply round trip or its staged copy-edit-apply variant. When a staged copy/edit-and-apply flow is required by the real owner or boundary, expose the reason and name the original identity; preserve or reconcile it, then provide an apply or save-back path or an equivalently explicit commit path. + +Revert, current/inherited defaults, and versioned factory defaults are distinct authoritative baselines with named scopes and owners. Reset values are read from the real owner when the operation runs, not from duplicated UI literals; describing a duplicate as authoritative does not transfer ownership. Proof covers non-default values, inherited and changed-default values, dirty state, Undo, save and apply, restart, and relocation only where each case is applicable; record an inapplicable case and reason instead of fabricating a transition. + +## Information architecture, content, and recovery + +Information architecture (IA) uses stable grouping and terminology, a visible location, clear entry and exit, an evident next step, task-oriented labels, meaningful headings and links, and one clear primary action where the task has one. Remove duplicate commands whose semantic result and context are the same; distinguish commands whose owners or consequences differ. + +Within the affected surface or journey, for every task-bearing region or affordance, name a distinct user-recognizable job, outcome, decision, or information gain relative to its parent and siblings. When that purpose is redundant or unsupported, merge it, relabel it, render it as truthful output, or omit it instead of retaining a control merely to make the surface look complete. + +Prioritize content by task criticality, frequency, consequence or urgency, and actionability. Keep essential state, blockers or errors, recovery, and the next action in context. Simultaneous dense presentation for comparison, monitoring, or expert work remains valid when the task requires concurrent visibility. Labels such as Advanced and More are not a mechanical ban or permission: their disclosed purpose, content, state, and consequence must remain predictable. + +When a product advertises the same job through multiple frontends, shells, skins, or platform clients, maintain a capability reachability matrix across those supported surfaces. Each material capability must be reachable on each surface where it applies, intentionally handed off through a clear transition with return path and preserved context/state, or marked not applicable with a concrete product or platform reason. Visual parity is not required, but a newer or alternate surface must not silently strand settings, recovery, account, or core-task capabilities that users are told it supports. + +Use consequence-scaled error handling. Preserve input, identify the specific correction, prevent duplicate submission, and use review, reversal, or confirmation only when the consequence warrants it. + +## Internationalization and bidirectionality + +Internationalization begins during discovery. Avoid display-string concatenation; use locale-aware formatting, sorting, searching, pluralization, and input. Carry language and direction metadata, and define whether a product language override follows the system, persists locally, syncs, or is unavailable. + +Test representative long text, unbroken content, bidirectional content, and taller-script content without assuming one universal expansion percentage. Mirroring is selective: media controls, charts, maps, and user-authored direction may not mirror with surrounding navigation. + +## Accessibility, motion, and adaptation + +Prove names, roles, states, actions, focus order, visible focus, focus restoration, absence of traps, status announcements, and alternatives for complex pointer gestures. Equivalent semantic outcome and failure truth matter more than identical gestures. Apply keyboard, switch, touch, pointer, stylus, voice, and assistive-technology checks only where the platform or supported input makes them relevant. + +Reduced motion follows the system preference while preserving the semantic outcome, state feedback, focus, and spatial comprehension without requiring animation. Never remove essential status or progress information as the reduced-motion treatment. + +Adaptive invariants preserve reachability and avoid unintended information or function loss across supported window sizes, orientation, zoom, text scaling, content variation, and input changes. Platform references own exact insets, posture, windowing, breakpoints, and legitimate two-dimensional exceptions. + +## Evidence and promotion + +- An executable target journey plus the resulting state proves function. +- Automation plus applicable keyboard, assistive-technology, and device checks supports an accessibility claim. +- Observing representative users completing believable tasks supports usability; a reviewer preference does not. +- Telemetry and outcome measures support live quality when their collection and interpretation are valid. +- Static contract tests prove policy packaging and routing, not that downstream applications are usable. + +Record the target, build, state, owner, command or procedure, artifact, result, limitation, and reviewer for every material claim. An unavailable proof path is a blocker or explicit unverified claim, never a reason to substitute a prettier artifact. diff --git a/skills/superloopy-frontend/references/web.md b/skills/superloopy-frontend/references/web.md index fafd5d6..03814f3 100644 --- a/skills/superloopy-frontend/references/web.md +++ b/skills/superloopy-frontend/references/web.md @@ -1,104 +1,186 @@ # Web route -You are the frontend orchestrator. The user wants UI that reads as *designed by a person*, not generated by a model. Generic AI output is recognizable because models converge on the same defaults (Inter, purple gradients, three equal cards, one flat shadow, em-dashes everywhere) and ship the first draft unseen. This route removes that failure in three moves: it forces a **token contract** before any pixel, it bans the **named defaults** that signal slop, and it gates completion on **real-browser visual evidence** — never on "it compiles." +Use this route for browser-hosted DOM, installed Web, custom-rendered Web, and embedded browser clients. Preserve the shared UX contract and the project's existing stack. Design quality, functional truth, accessibility, performance, crawlability, and native-shell integration are separate claims with separate proof. -The orchestrator owns files, dispatch is self-contained, and a pass requires a real artifact under the evidence root via `SUPERLOOPY_EVIDENCE`, judged by what was delivered, never by a worker's status sentence. +Real-browser visual evidence is required for changed visual claims, not as a substitute for behavioral or accessibility evidence. A narrow nonvisual change may complete without a new design system or screenshot set when its own regression evidence is sufficient. -## Authority while active +## Delivery classes and proof ownership -This mode supersedes the model's two strongest UI failure instincts: *jumping to a default aesthetic* and *shipping the first attempt*. While active, "looks fine to me" is not a completion signal — the DESIGN.md gate, the anti-slop rules, and the visual-QA evidence gate are. Under `loopy team`/`ultrawork`, map each surface to a success criterion whose evidence is the DESIGN.md token contract, the anti-slop pre-flight result, and the captured visual-QA artifact. +Classify the deployed surface before selecting a Web checklist. Public versus private deployment, DOM versus custom rendering, installed capabilities, and shell ownership matter more than a framework label. -## The acceptance bar: design AND quality, never one alone +### Product/application DOM Web -A page is done only when both hold: +This includes authenticated, private, internal, administrative, editor, and public application surfaces. Preserve the existing product design system, workflow, information architecture, and density unless the task explicitly changes them. Run the production build, affected journey, adjacent regression, and supported-browser floor. Add operating-state, accessibility-tree, adaptation, and performance checks when affected, claimed, or selected by risk. SEO applies when the current Web target is public and crawlable and that claim or risk is selected; authentication, privacy, and application UI do not imply crawlability. -- **Design**: every value traces to a DESIGN.md token; the anti-slop pre-flight passes; the surface is coherent across all sections. -- **Quality**: it renders correctly in a real browser at every breakpoint, every interactive/empty/loading/error state works, and motion is GPU-composited (transform/opacity/filter only). +### Marketing/editorial Web -A beautiful page that ships a broken layout has failed; a fast page that looks like a template has failed. Both win or neither does. +For a campaign, publication, landing page, or explicit new visual direction, load `references/anti-slop.md`. Ground brand, editorial hierarchy, composition, imagery, and copy in real content and the approved visual direction. Prove affected forms, navigation, consent, localization, accessibility, performance, and product handoff claims as behavior rather than decoration. + +### PWA and browser extension + +For a PWA, map service worker registration, installability, cache policy, offline/stale behavior, update activation, data migration, permission recovery, and standalone/browser display modes. Always run the build, launch, affected journey, and adjacent regression floor; prove the mapped capabilities when affected, claimed, or selected by risk. For a browser extension, separate extension-page, browser chrome, background/service-worker, content script, host-page, storage, and permission ownership. Run the same minimum floor in a supported browser, then add install/update/removal, cross-browser, storage, and least-privilege proof when those claims or risks are in scope. + +### Canvas/custom-rendered Web + +Load `references/renderer.md`. Separately prove semantics, accessibility, text and input behavior, focus/selection/IME, scaling, and performance through the renderer that owns them. Treat crawlability as a capability to separately prove for a deployed public surface. A canvas screenshot is insufficient evidence for behavior, semantics, text input, or crawlability. + +### Embedded client Web + +Load the applicable `references/desktop.md` or `references/mobile.md`, then `references/hybrid.md`. Browser-side client evidence covers only the client. Require shell proof for native ownership, lifecycle, permissions, accessibility, menus, windowing, and packaging on the actual target; client proof cannot substitute for shell proof. + +When the current target is native or embedded, evaluate SEO only for a distinct deployed public Web target; do not score the shell or embedded client as though it were that site. HTML/CSS, WebView, canvas, or an embedded browser engine never makes SEO applicable by itself. ## Reference loading -Load the *smallest* set that covers the request and state which you loaded in one sentence: +Load the smallest applicable set and state it in one sentence: + +- **Marketing, editorial, campaign, or explicit new visual direction:** `references/anti-slop.md`. +- **Canvas/custom renderer:** `references/renderer.md`. +- **Embedded client:** applicable desktop or mobile reference plus `references/hybrid.md`. +- **Named brand or mood:** `references/design/_INDEX.md`, then one matching teardown. Extract an unlisted brand only when authorized. +- **Visually important build from intent:** `references/image-first.md`. +- **Creating or extending tokens:** `references/design-system.md`. +- **Adopting an existing platform or organizational system:** `references/system-map.md`; ask before adding a dependency. +- **Any changed temporal claim:** the shared router selects `references/motion-core.md`; add the Web specialization `references/motion.md` only for browser-owned scroll-driven, pointer-physics, or animated-node implementation. +- **Redesigning a living site:** `references/redesign.md`. +- **Measured browser quality:** `references/perfection.md` when a performance, Lighthouse, or token-compliance claim is affected or selected by risk. + +Authenticated, private, and internal product work does not automatically load a marketing aesthetic. Existing design-system and product conventions remain authoritative. + +## Proportional design contract + +Inspect the existing design system, tokens, components, and representative current surface before changing visuals. The project's current design source of truth remains authoritative; `DESIGN.md` is a scoped mapping/receipt only when the repository has no equivalent or when every changed value links back to and stays synchronized with that source. + +- **Narrow nonvisual change:** record `Design impact: unchanged` and `Visual evidence: not applicable`, with behavioral, accessibility, and regression evidence. `DESIGN.md` and `VISUAL_QA.md` are required only for a changed visual claim, not for copy semantics, focus order, accessible naming, or an equivalent nonvisual fix. +- **Visual delta inside an existing system:** document only the changed tokens, components, responsive invariants, and states. Reuse existing CSS variables and primitives. +- **New or redesigned visual direction:** update the project's authoritative design source before UI code. If `DESIGN.md` is the repository's established source, update it; otherwise use it only as a scoped evidence mapping when needed. Define atmosphere, color, typography, spacing, components, motion, and depth. Every new visual value traces to one authoritative token or an explicitly synchronized platform value. + +When a visual direction is in scope, emit a one-line Design Read with the surface kind, audience, existing or named direction, and evidence. Dial guidance is heuristic: user intent wins first, then the existing interface and an approved visual target; accessibility, performance, and platform constraints narrow the remaining choices. + +## Baseline and implementation + +Reproduce the current journey in the production-equivalent browser, then locate the route, components, state and service owners, existing CSS variables, tests, and adjacent journeys. Preserve working behavior and supported deep links. Map each shared UX contract clause to its acceptance case, implementation file, test, and evidence artifact. + +Build operating states from actual ownership: initial/loading/empty/partial/stale/offline/degraded/error/retry/cancel, authentication/session/permission/entitlement changes, duplicate or optimistic submissions, and conflicts where applicable. Do not animate layout properties; respect reduced motion; use the established icon and styling infrastructure before proposing additions. + +## Target-derived verification matrix + +Derive supported browser engine, browser version range, OS, and input from repository Browserslist/configuration, package and runtime constraints, documented product support, and valid usage analytics. Analytics may prioritize execution but cannot silently remove a contractual or accessibility target. + +Derive widths from actual breakpoints, container behavior, minimum and maximum supported windows, embedded bounds, content extremes, zoom, and text scaling. Include keyboard, touch, pointer, assistive technology, IME, locale, direction, color scheme, reduced motion, and contrast modes when supported or material. -- **Always, for web UI work** → `references/anti-slop.md` (named-default bans, countable rules, consistency locks, the pre-flight checklist). -- **A named brand or a mood** ("like Stripe", "premium", "dev-tool dark", "editorial") → `references/design/_INDEX.md`, then load the ONE matching brand teardown and let its tokens drive DESIGN.md. ~47 brands are bundled; for a brand not listed, extract it on-demand per `references/design-system.md`. -- **Visually-important build (page/screen from intent)** → `references/image-first.md` (secure a visual target before coding, deep-analyze to a spec, then implement). -- **Creating or extending a design system / tokens** → `references/design-system.md` (the 7-section DESIGN.md schema and how to author loopy-native brand token sets). -- **An actual platform or organizational design-system contract** (an existing package, embedded host surface, explicit adoption request, or policy requirement) → `references/system-map.md` (preserve the stack, verify the maintained official implementation, and ask before adding a dependency). A merely Microsoft-like or GitHub-like mood stays on the named-brand route above. -- **Scroll-driven or pointer-physics motion** (pin, scrub, stagger, parallax, magnetic hover) → `references/motion.md` (driver ownership, responsive measurement, reduced-motion-safe starting templates, and interactive browser proof). -- **Redesigning a living site** → `references/redesign.md` (mode detection, before-state evidence, preservation rules, and risk-ordered modernization). -- **Measuring quality (compliance + performance)** → `references/perfection.md` (the design-system compliance script + the real-browser Lighthouse protocol). +390 / 768 / 1280 px remain useful baseline samples for continuity, but they are not universal proof and never replace target-derived breakpoints or browser/OS/input coverage. Record the exact browser build, engine, OS, input, viewport, device scale factor, zoom, locale, and state behind each artifact. -Loading nothing produces generic slop; loading everything dilutes the signal. Load deliberately — at most one brand teardown at a time (they encode opposing systems). +For each served Web implementation or plan, define a **minimum validation floor**: production build, affected journey, adjacent regression, and at least one supported real browser. Add design compliance, visual capture, 390 / 768 / 1280 baseline samples, target-derived additions, Lighthouse, and React Doctor only when the changed claim or risk selects them; React Doctor also requires a React repository. -## Phase 0 — DESIGN.md gate (no design system, no UI work) +## Visual-QA evidence gate -Before any UI code, find `DESIGN.md` / `design-system.md` / `design-tokens.md` in the repo. +For a changed visual claim, or an interaction claim with a visible-state or layout consequence, capture the production build in the target-derived matrix and write `VISUAL_QA.md` under `.superloopy/evidence/frontend//`. Record reference, actual target, state, viewport, browser/OS/input, observed difference, source cause, result, and limitation. A purely behavioral interaction with no visible consequence uses behavioral proof instead. -- **If one exists**: read it fully. Every color, font-size, spacing, radius, and shadow you write MUST trace to a token in it. Need a value it lacks? Add the token to DESIGN.md *first*, then use it. No raw hex outside DESIGN.md, no magic spacing numbers. -- **If none exists**: author one from the `references/design-system.md` 7-section schema (atmosphere, color, typography, spacing, components, motion, depth). Extract tokens from existing code when the project already has UI; on greenfield, ask at most one question (brand/mood), commit to a named direction, and write the file. Record it under the evidence root as `DESIGN_TOKENS.md`. +When a visual reference exists, `node "${FRONTEND_SKILL_DIR}/scripts/visual-diff.mjs" path/to/reference.png path/to/actual.png --json` can aim review at hotspots. Pixel similarity is not the verdict: inspect the rendered journey, text and localization, interaction states, focus, reflow, overflow, and reduced-motion behavior. A nonvisual claim uses behavioral or accessibility evidence instead of a decorative screenshot. -The token contract is the single highest-leverage mechanism: it converts per-component improvisation (the root of inconsistent slop) into one fixed vocabulary applied page-wide. +Run the applicable `references/anti-slop.md` pre-flight only for its declared scope. Do not weaken UX, hide content, remove necessary feedback, or replace real assets merely to clear a metric. -## Phase 1 — Brief read and direction commitment +## Measured quality -Before generating, emit a one-line **Design Read**: `Reading this as: for , language, leaning `. Do not default to a house aesthetic. From the read, fix three dials that gate downstream decisions: +Use `references/perfection.md` only when the changed token, performance, accessibility-audit, best-practice, or public SEO claim and its risk justify that layer. The `ds-compliance.mjs` compatibility filename is a partial color/spacing token lint only. Lighthouse measures selected production-browser claims only; it does not prove usability, renderer semantics, native shell behavior, or packaging. Pin and record tool and browser versions as that reference requires. -- `DESIGN_VARIANCE` (1-10): layout boldness. Above ~4, avoid the centered hero and symmetric three-card row. -- `MOTION_INTENSITY` (1-10): if you claim motion, motion must be shown (implemented, not described). -- `VISUAL_DENSITY` (1-10): low = generous whitespace; high = line separators, mono numerals, no cards. +## Evidence modes and executable commands -Dial guidance is heuristic, never authority. Explicit user intent wins first, then the existing interface and an approved visual target; audience, accessibility, performance, and platform constraints narrow the remaining choices. Do not assign fixed numeric presets from a page label such as “marketing,” “premium,” or “editorial.” Record the evidence behind each value in the Design Read, and lower motion whenever the scope cannot support verified implementation. +`FRONTEND_SKILL_DIR` below is the actual absolute directory of this loaded frontend skill, announced as `FRONTEND_SKILL_DIR` during activation. Packaged helpers live under `${FRONTEND_SKILL_DIR}/scripts/`; project inputs and `.superloopy/evidence/` stay under the target project's current working directory. Never assume the target repository contains a `skills/superloopy-frontend/` checkout. -## Phase 2 — Build against the contract +### Standalone frontend invocation -Establish the foundation first (fonts, global token CSS variables, shared icon set as SVG components, base layout primitives), then build components — each one reading DESIGN.md, each interactive state (hover/active/focus/disabled) and each empty/loading/error state explicitly handled. Match the project's existing styling infrastructure before introducing a new one. Never animate layout properties; never use emojis as icons. +Run each validation command directly and write its stdout/stderr or a structured report to a non-empty artifact. Use the portable run ID `YYYYMMDDTHHMMSSZ-`; the UTC timestamp has no colon and the 1-48 character slug uses lowercase ASCII letters or digits joined by single hyphens. Create the root once per logical run with the dependency-free Node helper. The helper rejects path traversal, Windows reserved names, empty evidence, and files outside that root. -## Dispatch model (reuse the crew, stay self-contained) +POSIX (`sh`, Bash, or zsh): -Superloopy never spawns from its CLI — it rides the host's native dispatch. For parallel frontend work, reuse the bundled crew with self-contained messages (paste the full slice + the relevant DESIGN.md tokens inline; never "go read the docs"): +```sh +set -u +EVIDENCE_ROOT="$(node "${FRONTEND_SKILL_DIR}/scripts/evidence-root.mjs" create frontend-check)" || exit $? +[ -n "$EVIDENCE_ROOT" ] || exit 2 +BUILT_FILE=dist/assets/app.css +node "${FRONTEND_SKILL_DIR}/scripts/ds-compliance.mjs" DESIGN.md "$BUILT_FILE" > "${EVIDENCE_ROOT}/token-lint.txt" 2>&1 +VALIDATION_STATUS=$? +node "${FRONTEND_SKILL_DIR}/scripts/evidence-root.mjs" verify "$EVIDENCE_ROOT" token-lint.txt +VERIFY_STATUS=$? +[ "$VALIDATION_STATUS" -eq 0 ] || exit "$VALIDATION_STATUS" +[ "$VERIFY_STATUS" -eq 0 ] || exit "$VERIFY_STATUS" +``` -- **franky** — build one bounded component/slice (≤~150 lines or split). Owns its file before the parent edits it. -- **zoro** — drift + slop review: compare the implementation against the component's tokens and the anti-slop rules; flag simplified lookalikes, orphan hex, magic spacing, and any named-default tell. -- **usopp** — visual QA: run the app in a real browser, screenshot, exercise states. -- **nami** — read-only navigation for finding existing tokens/components. +PowerShell: -Role routing is not guaranteed, so judge each lane by delivered evidence, not the role label. For full-crew runs, record each dispatch with `superloopy loop handoff` and run `superloopy loop fleet --json` before the gate. A worker that produces a UI artifact ends its reply with `SUPERLOOPY_EVIDENCE: `. +```powershell +$EVIDENCE_ROOT = node "${FRONTEND_SKILL_DIR}/scripts/evidence-root.mjs" create frontend-check +if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($EVIDENCE_ROOT)) { exit 2 } +$BUILT_FILE = "dist/assets/app.css" +node "${FRONTEND_SKILL_DIR}/scripts/ds-compliance.mjs" DESIGN.md $BUILT_FILE *> "$EVIDENCE_ROOT/token-lint.txt" +$ValidationStatus = $LASTEXITCODE +node "${FRONTEND_SKILL_DIR}/scripts/evidence-root.mjs" verify $EVIDENCE_ROOT token-lint.txt +$VerifyStatus = $LASTEXITCODE +if ($ValidationStatus -ne 0) { exit $ValidationStatus } +if ($VerifyStatus -ne 0) { exit $VerifyStatus } +``` -## Phase 3 — Visual-QA evidence gate +Return the final artifact receipt without calling loop-only commands: -For any served web-app implementation or validation plan, explicitly name the production build, design compliance, real-browser state capture at 390 / 768 / 1280 px, and Lighthouse gates; include React Doctor only when the repository uses React. +```text +SUPERLOOPY_EVIDENCE: +``` -A build that compiles is not verified. Before declaring done, capture real-browser evidence and write `VISUAL_QA.md` under the evidence root: +### Active Superloopy loop -1. Screenshot at 390 / 768 / 1280 px (set the viewport before capturing). -2. When there is a reference (a clone target, a mock, or an image-first target), diff it: `node skills/superloopy-frontend/scripts/visual-diff.mjs --json`. It returns `similarityScore`, `diffRatio`, `alphaChannelIntact`, and ranked 8×8 grid `hotspots`. **The JSON is evidence to AIM the review, not the verdict** — a 99/100 score can still hide a faked screenshot, a broken state, or clipped CJK text. Open every hotspot and map it to a cause in source; a high score with an open finding is still a FAIL. -3. Walk the checklist: colors/type/spacing trace to DESIGN.md; all interactive states; empty/loading/error states; dark mode if in scope; no horizontal scroll; motion smooth and reduced-motion respected. -4. Run the anti-slop **pre-flight** from `references/anti-slop.md` — any unticked box is a fail. -5. On any failure, classify (token-wrong vs implementation-drift), fix, and re-capture. Do not weaken UX to pass — removing an animation, swapping the hero for a placeholder, or hiding content to clear a check is a reject-on-sight shortcut, not a fix. +Ask the loop for the current criterion, start the goal if the guide requests it, create one run root, then capture the validation against that criterion. -This is the frontend instance of Superloopy's evidence gate: a "looks done" claim cannot land without a captured artifact. +POSIX: -## Phase 4 — Measured quality gate +```sh +set -u +EVIDENCE_ROOT="$(node "${FRONTEND_SKILL_DIR}/scripts/evidence-root.mjs" create frontend-check)" || exit $? +[ -n "$EVIDENCE_ROOT" ] || exit 2 +BUILT_FILE=dist/assets/app.css +superloopy loop guide --json || exit $? +superloopy loop prove --artifact "${EVIDENCE_ROOT}/token-lint.txt" -- node "${FRONTEND_SKILL_DIR}/scripts/ds-compliance.mjs" DESIGN.md "$BUILT_FILE" +VALIDATION_STATUS=$? +node "${FRONTEND_SKILL_DIR}/scripts/evidence-root.mjs" verify "$EVIDENCE_ROOT" token-lint.txt +VERIFY_STATUS=$? +[ "$VALIDATION_STATUS" -eq 0 ] || exit "$VALIDATION_STATUS" +[ "$VERIFY_STATUS" -eq 0 ] || exit "$VERIFY_STATUS" +``` -Make quality objective, not subjective (`references/perfection.md`). Two layers, both producing evidence: +PowerShell: -- **Design System Compliance (runnable, no deps)** — `superloopy loop prove -- node skills/superloopy-frontend/scripts/ds-compliance.mjs DESIGN.md `. Exits non-zero on undeclared hex or off-scale spacing. This is the part loopy hard-gates itself. -- **Lighthouse (real browser)** — via `npx` against a production build (never the CLI headless-shell or dev server), high floor across performance/accessibility/best-practices/SEO, mobile + desktop, median of 3-5 runs; for React, `npx react-doctor`. Record a `PERF.md` artifact. Never weaken UX to win the number (see the anti-gaming list). +```powershell +$EVIDENCE_ROOT = node "${FRONTEND_SKILL_DIR}/scripts/evidence-root.mjs" create frontend-check +if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($EVIDENCE_ROOT)) { exit 2 } +$BUILT_FILE = "dist/assets/app.css" +superloopy loop guide --json +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +superloopy loop prove --artifact "$EVIDENCE_ROOT/token-lint.txt" -- node "${FRONTEND_SKILL_DIR}/scripts/ds-compliance.mjs" DESIGN.md $BUILT_FILE +$ValidationStatus = $LASTEXITCODE +node "${FRONTEND_SKILL_DIR}/scripts/evidence-root.mjs" verify $EVIDENCE_ROOT token-lint.txt +$VerifyStatus = $LASTEXITCODE +if ($ValidationStatus -ne 0) { exit $ValidationStatus } +if ($VerifyStatus -ne 0) { exit $VerifyStatus } +``` -Scale Phase 4 to the project: always run Layer 1; run Lighthouse when the deliverable is a real, served app. +To attach an existing non-empty artifact instead, copy the actual IDs and target from `loop guide --json`: -## Evidence contract +```sh +superloopy loop evidence --goal-id G001 --criterion-id C001 --status pass --artifact "${EVIDENCE_ROOT}/VISUAL_QA.md" --notes "verified changed visual claims" --json +``` -- The orchestrator owns files; workers return findings and artifacts, not session-file writes. -- Keep a session directory under `.superloopy/evidence/frontend/-/` holding `DESIGN_TOKENS.md`, component artifacts, and `VISUAL_QA.md`. -- End completed frontend work with a Superloopy record, e.g. `superloopy loop evidence --status pass --artifact .superloopy/evidence/frontend//VISUAL_QA.md --notes ""`. +`loop prove` requires an active goal. `loop evidence` requires both goal and criterion IDs. Do not use either command for a standalone frontend invocation with no loop plan. ## Completion checklist -- A DESIGN.md token contract exists and every written value traces to it. -- The anti-slop pre-flight passed with no unticked box. -- The surface renders correctly in a real browser at 390/768/1280 with all states handled. -- No UX was weakened to pass a check. -- `VISUAL_QA.md` exists and the final Superloopy evidence record points at it. +- The baseline, affected journey, adjacent regression surface, and contract-to-code/test traceability are recorded. +- Delivery class and proof owners are named. +- Product/application Web preserves its established workflow and system; marketing/editorial work passes the applicable anti-slop review. +- PWA, extension, renderer, and embedded-shell claims have owner-specific proof when in scope. +- The surface renders correctly in a real browser from a production build across the target-derived browser/OS/input/breakpoint matrix. +- Functional, operating-state, accessibility, editable-text/IME, localization, motion, and performance claims have matching evidence. +- SEO is proven when the current target is crawlable public Web, or for a distinct public Web deployment when the current target is native/embedded; otherwise it is recorded as not applicable with a reason. +- A narrow nonvisual change records design and visual non-applicability; a changed visual claim or visibly consequential interaction has `VISUAL_QA.md`. +- Tool, browser, OS, build, state, and limitation versions are recorded. +- The final response includes `SUPERLOOPY_EVIDENCE: `. diff --git a/skills/superloopy-frontend/scripts/ds-compliance.mjs b/skills/superloopy-frontend/scripts/ds-compliance.mjs index 83b53f9..5c3a07d 100644 --- a/skills/superloopy-frontend/scripts/ds-compliance.mjs +++ b/skills/superloopy-frontend/scripts/ds-compliance.mjs @@ -1,8 +1,7 @@ #!/usr/bin/env node -// Dependency-free Design System Compliance gate for the Superloopy frontend skill. -// Turns "is the UI on-system?" into a measurable pass/fail: it flags raw hex colors -// not declared in DESIGN.md and off-scale spacing (px not on the base unit) — the -// "Lighthouse 100 but 14 undeclared hex codes" failure that reads as AI slop. +// Compatibility filename for the dependency-free partial color/spacing token lint. +// It flags raw hex colors not declared in DESIGN.md and off-scale spacing (px not +// on the base unit); it makes no broader typography, component, or accessibility claim. // // Exits non-zero when violations exist, so it drops straight into the loop: // superloopy loop prove -- node skills/superloopy-frontend/scripts/ds-compliance.mjs DESIGN.md src/**/*.css @@ -11,15 +10,17 @@ import { readFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; -const HEX = /#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})\b/g; -const SPACING_PROP = /\b(padding|margin|gap|inset|top|right|bottom|left|row-gap|column-gap)\b[^:;{}]*:\s*([^;{}]+)/gi; -const PX = /\b(\d+)px\b/g; +const HEX = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})(?![0-9a-fA-F])/g; +const BOX_AXIS = "(?:top|right|bottom|left|block(?:-start|-end)?|inline(?:-start|-end)?)"; +const SPACING_NAME = `(?:(?:padding|margin|scroll-padding|scroll-margin)(?:-${BOX_AXIS})?|gap|row-gap|column-gap|grid-gap|grid-row-gap|grid-column-gap|inset(?:-(?:block(?:-start|-end)?|inline(?:-start|-end)?))?|top|right|bottom|left)`; +const SPACING_DECL = new RegExp(`(?:^|[;{])\\s*(${SPACING_NAME})\\s*:\\s*([^;{}]+)(?=;|\\})`, "gimu"); +const PX = /(? c + c).join(""); + if (h.length === 3 || h.length === 4) h = h.split("").map((c) => c + c).join(""); return `#${h}`; } @@ -27,7 +28,7 @@ function normalizeHex(hex) { export function parseDesignTokens(designText) { const colors = new Set(); for (const m of designText.matchAll(HEX)) colors.add(normalizeHex(m[0])); - const baseMatch = designText.match(/base[^\n]*?\b(\d+)\s*px/i); + const baseMatch = designText.match(/\bbase\b[^\n]*?\b(\d+)\s*px/i); const base = baseMatch ? Number.parseInt(baseMatch[1], 10) : 4; return { colors, base: base > 0 ? base : 4 }; } @@ -43,15 +44,26 @@ export function scanContent(content, tokens, file = "") { violations.push({ file, line: i + 1, kind: "undeclared-color", value: m[0], snippet: line.trim().slice(0, 120) }); } } - for (const decl of line.matchAll(SPACING_PROP)) { - for (const px of decl[2].matchAll(PX)) { - const n = Number.parseInt(px[1], 10); - if (!ALLOWED_PX.has(n) && n % tokens.base !== 0) { - violations.push({ file, line: i + 1, kind: "off-scale-spacing", value: `${n}px`, snippet: line.trim().slice(0, 120) }); - } + }); + + for (const decl of content.matchAll(SPACING_DECL)) { + const valueOffset = decl[0].lastIndexOf(decl[2]); + const valueStart = decl.index + valueOffset; + for (const px of decl[2].matchAll(PX)) { + const n = Number.parseFloat(px[1]); + const magnitude = Math.abs(n); + if (!ALLOWED_PX.has(magnitude) && magnitude % tokens.base !== 0) { + const line = content.slice(0, valueStart + px.index).split("\n").length; + violations.push({ + file, + line, + kind: "off-scale-spacing", + value: `${px[1]}px`, + snippet: lines[line - 1].trim().slice(0, 120), + }); } } - }); + } return violations; } diff --git a/skills/superloopy-frontend/scripts/evidence-root.mjs b/skills/superloopy-frontend/scripts/evidence-root.mjs new file mode 100644 index 0000000..5612c5a --- /dev/null +++ b/skills/superloopy-frontend/scripts/evidence-root.mjs @@ -0,0 +1,117 @@ +#!/usr/bin/env node +// Create and verify cross-shell frontend evidence roots without relying on a +// platform-specific date command, path separator, or mkdir syntax. + +import { lstatSync, mkdirSync, realpathSync } from "node:fs"; +import { isAbsolute, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const PORTABLE_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u; +const WINDOWS_RESERVED = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu; +const RUN_ROOT = /^\.superloopy\/evidence\/frontend\/\d{8}T\d{6}Z-([a-z0-9]+(?:-[a-z0-9]+)*)$/u; + +export function validateSlug(value) { + if (typeof value !== "string" || value.length > 48 || !PORTABLE_SLUG.test(value) || WINDOWS_RESERVED.test(value)) { + throw new Error("slug must be 1-48 lowercase ASCII letters/digits joined by single hyphens and must not be a Windows reserved name"); + } + return value; +} + +export function createEvidenceRoot(slug, cwd = process.cwd(), now = new Date()) { + const safeSlug = validateSlug(slug); + const stamp = now.toISOString().replaceAll("-", "").replaceAll(":", "").replace(/\.\d{3}Z$/u, "Z"); + const root = `.superloopy/evidence/frontend/${stamp}-${safeSlug}`; + const base = realpathSync(cwd); + ensureDirectoryTree(base, root.split("/")); + return root; +} + +export function verifyEvidenceFiles(root, files, cwd = process.cwd()) { + const normalizedRoot = root.replaceAll("\\", "/"); + const match = normalizedRoot.match(RUN_ROOT); + if (!match) throw new Error("evidence root must match .superloopy/evidence/frontend/YYYYMMDDTHHMMSSZ-"); + validateSlug(match[1]); + if (!Array.isArray(files) || files.length === 0) throw new Error("at least one evidence file is required"); + + const base = realpathSync(cwd); + const absoluteRoot = requireDirectoryTree(base, normalizedRoot.split("/")); + for (const file of files) { + if (typeof file !== "string" || file.length === 0 || isAbsolute(file)) throw new Error("evidence filenames must be non-empty relative paths"); + const candidate = resolve(absoluteRoot, file); + const inside = relative(absoluteRoot, candidate); + if (inside === "" || inside === ".." || inside.startsWith("../") || inside.startsWith("..\\") || isAbsolute(inside)) { + throw new Error(`evidence file escapes its run root: ${file}`); + } + requireNoSymlinkPath(absoluteRoot, inside); + const metadata = lstatSync(candidate); + if (!metadata.isFile() || metadata.size === 0) throw new Error(`evidence file must be a non-empty regular file: ${file}`); + } + return normalizedRoot; +} + +function ensureDirectoryTree(base, segments) { + let current = base; + for (const [index, segment] of segments.entries()) { + current = resolve(current, segment); + try { + const metadata = lstatSync(current); + if (metadata.isSymbolicLink()) throw new Error(`evidence directory must not be a symlink: ${segment}`); + if (!metadata.isDirectory()) throw new Error(`evidence path component must be a directory: ${segment}`); + if (index === segments.length - 1) throw new Error(`evidence run root already exists: ${current}`); + } catch (error) { + if (error?.code !== "ENOENT") throw error; + mkdirSync(current); + } + } + return current; +} + +function requireDirectoryTree(base, segments) { + let current = base; + for (const segment of segments) { + current = resolve(current, segment); + const metadata = lstatSync(current); + if (metadata.isSymbolicLink()) throw new Error(`evidence directory must not be a symlink: ${segment}`); + if (!metadata.isDirectory()) throw new Error(`evidence path component must be a directory: ${segment}`); + } + return current; +} + +function requireNoSymlinkPath(base, relativePath) { + let current = base; + for (const segment of relativePath.split(/[\\/]/u)) { + current = resolve(current, segment); + if (lstatSync(current).isSymbolicLink()) throw new Error(`evidence file path must not contain symlinks: ${relativePath}`); + } +} + +function main(argv) { + const [command, value, ...files] = argv; + if (command === "create" && value && files.length === 0) { + process.stdout.write(`${createEvidenceRoot(value)}\n`); + return; + } + if (command === "verify" && value) { + process.stdout.write(`${verifyEvidenceFiles(value, files)}\n`); + return; + } + throw new Error("usage: evidence-root.mjs create | verify "); +} + +function isCliEntry(argvPath) { + if (!argvPath) return false; + try { + return realpathSync(argvPath) === realpathSync(fileURLToPath(import.meta.url)); + } catch { + return false; + } +} + +if (isCliEntry(process.argv[1])) { + try { + main(process.argv.slice(2)); + } catch (error) { + process.stderr.write(`evidence-root: ${error.message}\n`); + process.exitCode = 2; + } +} diff --git a/src/matrix-gate.js b/src/matrix-gate.js index 4d71910..69909a7 100644 --- a/src/matrix-gate.js +++ b/src/matrix-gate.js @@ -1,8 +1,24 @@ import { validateAuditSection } from "./audit-verdict.js"; -import { artifactPath, emptyBlockers, fail, isRecord, literal, passedVerdict, referencedArtifacts, section, stringArray, textField } from "./review-gate.js"; +import { + artifactPath, + emptyBlockers, + fail, + isRecord, + literal, + passedVerdict, + qualityGateArtifactCompatible, + qualityGateArtifactKind, + qualityGateEvidenceScope, + qualityGateSurfaceFamilies, + rejectQualityGateScopeFields, + referencedArtifacts, + requireQualityGateSurfaceProof, + section, + stringArray, + textField +} from "./review-gate.js"; const REQUIRED_SECTIONS = ["architectReview", "executorQa", "iteration"]; -const TERMINAL_MULTIPLEXER_SURFACE = ["t", "m", "u", "x"].join(""); export function isMatrixQualityGate(value) { return REQUIRED_SECTIONS.every((key) => isRecord(value[key])); @@ -55,34 +71,49 @@ export function validateMatrixQualityGate(value, resolveArtifactPath) { function parseArtifactRefs(value, resolveArtifactPath) { if (!Array.isArray(value) || value.length === 0) fail("executorQa.artifactRefs must not be empty."); - const seen = new Set(); + const seenIds = new Set(); + const seenPaths = new Set(); return value.map((item, index) => { const ref = section(item, `executorQa.artifactRefs[${index}]`); const id = textField(ref.id, `executorQa.artifactRefs[${index}].id`); - if (seen.has(id)) fail(`executorQa.artifactRefs contains duplicate ${id}.`); - seen.add(id); + if (seenIds.has(id)) fail(`executorQa.artifactRefs contains duplicate ${id}.`); + seenIds.add(id); if (!("path" in ref)) { fail("inlineEvidence" in ref ? "executorQa.artifactRefs inlineEvidence alone is not sufficient." : `executorQa.artifactRefs[${index}].path is required.`); } + const path = artifactPath(ref.path, `executorQa.artifactRefs[${index}].path`, resolveArtifactPath); + if (seenPaths.has(path)) fail(`executorQa.artifactRefs contains duplicate resolved path ${path}.`); + seenPaths.add(path); return { id, - kind: artifactKind(ref.kind, `executorQa.artifactRefs[${index}].kind`), + kind: qualityGateArtifactKind(ref.kind, `executorQa.artifactRefs[${index}].kind`), description: textField(ref.description, `executorQa.artifactRefs[${index}].description`), - path: artifactPath(ref.path, `executorQa.artifactRefs[${index}].path`, resolveArtifactPath) + path }; }); } function parseSurfaceEvidence(value, byId) { if (!Array.isArray(value) || value.length === 0) fail("executorQa.surfaceEvidence must not be empty."); + const seen = new Set(); + const seenScopes = new Set(); + const artifactScopes = new Map(); return value.map((item, index) => { const row = section(item, `executorQa.surfaceEvidence[${index}]`); const status = optionalText(row.status); + const id = textField(row.id, `executorQa.surfaceEvidence[${index}].id`); + if (seen.has(id)) fail(`executorQa.surfaceEvidence contains duplicate ${id}.`); + seen.add(id); const base = { - id: textField(row.id, `executorQa.surfaceEvidence[${index}].id`), + id, contractRef: textField(row.contractRef, `executorQa.surfaceEvidence[${index}].contractRef`) }; if (status === "not_applicable") { + const forbidden = ["surface", "invocation", "verdict", "result", "artifactRefs", "target", "owner", "claims", "scopeReason"] + .filter((key) => Object.prototype.hasOwnProperty.call(row, key)); + if (forbidden.length > 0) { + fail(`executorQa.surfaceEvidence[${index}] with status not_applicable must not include proof or scope fields: ${forbidden.join(", ")}.`); + } return { ...base, status, @@ -91,29 +122,45 @@ function parseSurfaceEvidence(value, byId) { }; } const surface = textField(row.surface, `executorQa.surfaceEvidence[${index}].surface`); - const family = surfaceFamily(surface); + const families = qualityGateSurfaceFamilies(surface, `executorQa.surfaceEvidence[${index}].surface`); + const scope = qualityGateEvidenceScope(row, families, `executorQa.surfaceEvidence[${index}]`); const artifactRefs = referencedArtifacts(row.artifactRefs, `executorQa.surfaceEvidence[${index}].artifactRefs`, byId); + registerScopedEvidence(scope, artifactRefs, `executorQa.surfaceEvidence[${index}]`, seenScopes, artifactScopes); + for (const artifact of artifactRefs) { + if (!qualityGateArtifactCompatible(scope.proofFamilies, artifact.kind)) fail(`executorQa.surfaceEvidence ${surface} artifact ${artifact.kind} is incompatible.`); + } requirePassedOutcome(row, `executorQa.surfaceEvidence[${index}]`); - requireSurfaceProof(family, artifactRefs, `executorQa.surfaceEvidence[${index}]`); + requireQualityGateSurfaceProof(scope.proofFamilies, artifactRefs, `executorQa.surfaceEvidence[${index}]`, { + claims: scope.claims, + legacyMatrixSurface: !scope.scoped && ["browser", "gui", "web", "native", "desktop", "tui"].includes(surface.trim().toLowerCase()) + ? surface.trim().toLowerCase() + : undefined + }); return { ...base, surface, invocation: textField(row.invocation, `executorQa.surfaceEvidence[${index}].invocation`), verdict: row.verdict === undefined ? "passed" : passedVerdict(row.verdict, `executorQa.surfaceEvidence[${index}].verdict`), - artifactRefs: artifactRefs.map((artifact) => artifact.id) + artifactRefs: artifactRefs.map((artifact) => artifact.id), + ...(scope.scoped ? { target: scope.target, owner: scope.owner, claims: scope.claims, scopeReason: scope.scopeReason } : {}) }; }); } function parseAdversarialCases(value, byId) { if (!Array.isArray(value) || value.length === 0) fail("executorQa.adversarialCases must not be empty."); + const seen = new Set(); return value.map((item, index) => { const row = section(item, `executorQa.adversarialCases[${index}]`); + rejectQualityGateScopeFields(row, `executorQa.adversarialCases[${index}]`); + const id = textField(row.id, `executorQa.adversarialCases[${index}].id`); + if (seen.has(id)) fail(`executorQa.adversarialCases contains duplicate ${id}.`); + seen.add(id); if (optionalText(row.status) === "not_applicable") fail(`executorQa.adversarialCases[${index}].status must not be not_applicable.`); const artifactRefs = referencedArtifacts(row.artifactRefs, `executorQa.adversarialCases[${index}].artifactRefs`, byId); requirePassedOutcome(row, `executorQa.adversarialCases[${index}]`); return { - id: textField(row.id, `executorQa.adversarialCases[${index}].id`), + id, contractRef: textField(row.contractRef, `executorQa.adversarialCases[${index}].contractRef`), scenario: textField(row.scenario, `executorQa.adversarialCases[${index}].scenario`), expectedBehavior: textField(row.expectedBehavior, `executorQa.adversarialCases[${index}].expectedBehavior`), @@ -125,28 +172,37 @@ function parseAdversarialCases(value, byId) { function parseContractCoverage(value, surfaceById, adversarialById, artifactById) { if (!Array.isArray(value) || value.length === 0) fail("executorQa.contractCoverage must not be empty."); + const seen = new Set(); return value.map((item, index) => { const row = section(item, `executorQa.contractCoverage[${index}]`); + rejectQualityGateScopeFields(row, `executorQa.contractCoverage[${index}]`); + const id = textField(row.id, `executorQa.contractCoverage[${index}].id`); + if (seen.has(id)) fail(`executorQa.contractCoverage contains duplicate ${id}.`); + seen.add(id); + const contractRef = textField(row.contractRef, `executorQa.contractCoverage[${index}].contractRef`); const surfaceRefs = optionalStringArray(row.surfaceEvidenceRefs, `executorQa.contractCoverage[${index}].surfaceEvidenceRefs`); const adversarialRefs = optionalStringArray(row.adversarialCaseRefs, `executorQa.contractCoverage[${index}].adversarialCaseRefs`); const artifactRefs = optionalStringArray(row.artifactRefs, `executorQa.contractCoverage[${index}].artifactRefs`); - if (surfaceRefs.length + adversarialRefs.length + artifactRefs.length === 0) { - fail(`executorQa.contractCoverage[${index}] must link to proof rows or artifacts.`); + if (surfaceRefs.length + adversarialRefs.length === 0) { + fail(`executorQa.contractCoverage[${index}] must link to at least one contract-bound surfaceEvidenceRefs or adversarialCaseRefs proof row; artifactRefs alone are insufficient.`); } for (const id of surfaceRefs) { const surface = surfaceById.get(id); if (surface === undefined) fail(`executorQa.contractCoverage[${index}].surfaceEvidenceRefs references unknown ${id}.`); if (surface.status === "not_applicable") fail(`executorQa.contractCoverage[${index}].surfaceEvidenceRefs.${id}.status must be passed.`); + if (surface.contractRef !== contractRef) fail(`executorQa.contractCoverage[${index}].surfaceEvidenceRefs.${id}.contractRef must match ${contractRef}.`); } for (const id of adversarialRefs) { - if (!adversarialById.has(id)) fail(`executorQa.contractCoverage[${index}].adversarialCaseRefs references unknown ${id}.`); + const adversarial = adversarialById.get(id); + if (adversarial === undefined) fail(`executorQa.contractCoverage[${index}].adversarialCaseRefs references unknown ${id}.`); + if (adversarial.contractRef !== contractRef) fail(`executorQa.contractCoverage[${index}].adversarialCaseRefs.${id}.contractRef must match ${contractRef}.`); } for (const id of artifactRefs) { if (!artifactById.has(id)) fail(`executorQa.contractCoverage[${index}].artifactRefs references unknown ${id}.`); } return { - id: textField(row.id, `executorQa.contractCoverage[${index}].id`), - contractRef: textField(row.contractRef, `executorQa.contractCoverage[${index}].contractRef`), + id, + contractRef, obligation: textField(row.obligation, `executorQa.contractCoverage[${index}].obligation`), status: coveredStatus(row.status, `executorQa.contractCoverage[${index}].status`), surfaceEvidenceRefs: surfaceRefs, @@ -170,53 +226,24 @@ function requirePassedOutcome(row, field) { } } -function surfaceFamily(value) { - const normalized = value.toLowerCase(); - if (normalized === TERMINAL_MULTIPLEXER_SURFACE) return "cli"; - if (/\b(cli|shell|terminal)\b/u.test(normalized)) return "cli"; - if (/\b(gui|web|browser)\b/u.test(normalized)) return "web"; - if (/\b(native|desktop|tui)\b/u.test(normalized)) return "native"; - if (/\b(http|data|api|package|algorithm|math)\b/u.test(normalized)) return "data"; - fail(`executorQa surface ${value} is unsupported.`); -} - -function artifactKind(value, field) { - const normalized = textField(value, field).toLowerCase().replaceAll("_", "-"); - const allowed = [ - "cli-transcript", - "log", - "failure-mode-test", - "browser-automation", - "screenshot", - "image", - "http-dump", - "data-diff", - "cli-replay", - "pty-capture", - "app-automation-transcript", - "api-package-test-report" - ]; - if (allowed.includes(normalized)) return normalized; - fail(`${field} must be a supported executor QA artifact kind.`); -} - function coveredStatus(value, field) { const status = textField(value, field); if (["covered", "passed", "verified"].includes(status)) return status; fail(`${field} must be covered, passed, or verified.`); } -function requireSurfaceProof(family, artifacts, field) { - const kinds = artifacts.map((artifact) => artifact.kind); - if (family === "cli" && !kinds.some((kind) => ["cli-transcript", "log", "cli-replay"].includes(kind))) { - fail(`${field} for CLI surfaces must reference CLI transcript, log, or replay artifacts.`); +function registerScopedEvidence(scope, artifacts, field, seenScopes, artifactScopes) { + if (!scope.scoped) return; + const scopeKey = `${scope.target.id}\u0000${scope.target.platform}\u0000${scope.owner}`; + if (seenScopes.has(scopeKey)) { + fail(`${field} duplicates scoped target ${scope.target.id} on platform ${scope.target.platform} for owner ${scope.owner}.`); } - if (family === "web") { - const hasAutomation = kinds.some((kind) => ["browser-automation", "app-automation-transcript"].includes(kind)); - const hasScreenshot = kinds.some((kind) => ["screenshot", "image"].includes(kind)); - if (!hasAutomation || !hasScreenshot) fail(`${field} for GUI/web surfaces must reference automation plus screenshot artifacts.`); - } - if (family === "native" && !kinds.some((kind) => ["screenshot", "image", "pty-capture", "app-automation-transcript"].includes(kind))) { - fail(`${field} for native surfaces must reference screenshot, PTY, or app automation artifacts.`); + seenScopes.add(scopeKey); + for (const artifact of artifacts) { + const priorScope = artifactScopes.get(artifact.path); + if (priorScope !== undefined && priorScope !== scopeKey) { + fail(`${field} artifact path ${artifact.path} is reused across distinct scoped target/platform/owner slices.`); + } + artifactScopes.set(artifact.path, scopeKey); } } diff --git a/src/review-gate.js b/src/review-gate.js index 6e24e7e..f71423b 100644 --- a/src/review-gate.js +++ b/src/review-gate.js @@ -1,12 +1,9 @@ import { validateAuditSection } from "./audit-verdict.js"; - const REQUIRED_SECTIONS = ["codeReview", "manualQa", "gateReview", "iteration", "criteriaCoverage"]; const TERMINAL_MULTIPLEXER_SURFACE = ["t", "m", "u", "x"].join(""); - -export function isReviewQualityGate(value) { - return REQUIRED_SECTIONS.every((key) => isRecord(value[key])); -} - +const VAGUE_TARGET_TOKENS = new Set(["all", "any", "every", "multi", "multiple", "cross", "universal", "supported", "targets", "devices", "platforms", "browsers"]); +const VAGUE_AGGREGATE_SYMBOL = /^(?:(?:all|any|every|multi|multiple|cross|universal|supported)(?:targets?|devices?|platforms?|browsers?)|targets|devices|platforms|browsers)$/u; +export function isReviewQualityGate(value) { return REQUIRED_SECTIONS.every((key) => isRecord(value[key])); } export function validateReviewQualityGate(value, resolveArtifactPath) { const codeReview = section(value.codeReview, "codeReview"); const manualQa = section(value.manualQa, "manualQa"); @@ -53,48 +50,74 @@ export function validateReviewQualityGate(value, resolveArtifactPath) { function parseArtifactRefs(value, resolveArtifactPath) { if (!Array.isArray(value) || value.length === 0) fail("manualQa.artifactRefs must not be empty."); - const seen = new Set(); + const seenIds = new Set(); + const seenPaths = new Set(); return value.map((item, index) => { const ref = section(item, `manualQa.artifactRefs[${index}]`); const id = textField(ref.id, `manualQa.artifactRefs[${index}].id`); - if (seen.has(id)) fail(`manualQa.artifactRefs contains duplicate ${id}.`); - seen.add(id); + if (seenIds.has(id)) fail(`manualQa.artifactRefs contains duplicate ${id}.`); + seenIds.add(id); + const path = artifactPath(ref.path, `manualQa.artifactRefs[${index}].path`, resolveArtifactPath); + if (seenPaths.has(path)) fail(`manualQa.artifactRefs contains duplicate resolved path ${path}.`); + seenPaths.add(path); return { id, - kind: artifactKind(ref.kind, `manualQa.artifactRefs[${index}].kind`), + kind: qualityGateArtifactKind(ref.kind, `manualQa.artifactRefs[${index}].kind`), description: textField(ref.description, `manualQa.artifactRefs[${index}].description`), - path: artifactPath(ref.path, `manualQa.artifactRefs[${index}].path`, resolveArtifactPath) + path }; }); } function parseSurfaceEvidence(value, byId) { if (!Array.isArray(value) || value.length === 0) fail("manualQa.surfaceEvidence must not be empty."); + const seen = new Set(); + const seenScopes = new Set(); + const artifactScopes = new Map(); return value.map((item, index) => { const row = section(item, `manualQa.surfaceEvidence[${index}]`); - const surface = surfaceKind(row.surface, `manualQa.surfaceEvidence[${index}].surface`); + const id = textField(row.id, `manualQa.surfaceEvidence[${index}].id`); + if (seen.has(id)) fail(`manualQa.surfaceEvidence contains duplicate ${id}.`); + seen.add(id); + const surfaceField = `manualQa.surfaceEvidence[${index}].surface`; + const surface = textField(row.surface, surfaceField); + const families = qualityGateSurfaceFamilies(surface, surfaceField); + const scope = qualityGateEvidenceScope(row, families, `manualQa.surfaceEvidence[${index}]`); const artifactRefs = referencedArtifacts(row.artifactRefs, `manualQa.surfaceEvidence[${index}].artifactRefs`, byId); + registerScopedEvidence(scope, artifactRefs, `manualQa.surfaceEvidence[${index}]`, seenScopes, artifactScopes); for (const artifact of artifactRefs) { - if (!artifactCompatible(surface, artifact.kind)) fail(`manualQa.surfaceEvidence ${surface} artifact ${artifact.kind} is incompatible.`); + if (!qualityGateArtifactCompatible(scope.proofFamilies, artifact.kind)) fail(`manualQa.surfaceEvidence ${surface} artifact ${artifact.kind} is incompatible.`); } + requireQualityGateSurfaceProof(scope.proofFamilies, artifactRefs, `manualQa.surfaceEvidence[${index}]`, { + claims: scope.claims, + legacyBrowserProof: !scope.scoped && ["browser", "gui"].includes(surface.trim().toLowerCase()) + }); + requireReviewPassedOutcome(row, `manualQa.surfaceEvidence[${index}]`); return { - id: textField(row.id, `manualQa.surfaceEvidence[${index}].id`), + id, criterionRef: textField(row.criterionRef, `manualQa.surfaceEvidence[${index}].criterionRef`), surface, invocation: textField(row.invocation, `manualQa.surfaceEvidence[${index}].invocation`), verdict: passedVerdict(row.verdict, `manualQa.surfaceEvidence[${index}].verdict`), - artifactRefs: artifactRefs.map((artifact) => artifact.id) + artifactRefs: artifactRefs.map((artifact) => artifact.id), + ...(scope.scoped ? { target: scope.target, owner: scope.owner, claims: scope.claims, scopeReason: scope.scopeReason } : {}) }; }); } function parseAdversarialCases(value, byId) { if (!Array.isArray(value) || value.length === 0) fail("manualQa.adversarialCases must not be empty."); + const seen = new Set(); return value.map((item, index) => { const row = section(item, `manualQa.adversarialCases[${index}]`); + rejectQualityGateScopeFields(row, `manualQa.adversarialCases[${index}]`); + const id = textField(row.id, `manualQa.adversarialCases[${index}].id`); + if (seen.has(id)) fail(`manualQa.adversarialCases contains duplicate ${id}.`); + seen.add(id); const artifactRefs = referencedArtifacts(row.artifactRefs, `manualQa.adversarialCases[${index}].artifactRefs`, byId); + requireReviewPassedOutcome(row, `manualQa.adversarialCases[${index}]`); return { - id: textField(row.id, `manualQa.adversarialCases[${index}].id`), + id, criterionRef: textField(row.criterionRef, `manualQa.adversarialCases[${index}].criterionRef`), scenario: textField(row.scenario, `manualQa.adversarialCases[${index}].scenario`), expectedBehavior: textField(row.expectedBehavior, `manualQa.adversarialCases[${index}].expectedBehavior`), @@ -118,14 +141,8 @@ function validateCriteriaCoverage(coverage) { }; } -export function artifactPath(value, field, resolveArtifactPath) { - return resolveArtifactPath(textField(value, field)); -} - -export function section(value, field) { - if (!isRecord(value)) fail(`${field} must be an object.`); - return value; -} +export function artifactPath(value, field, resolveArtifactPath) { return resolveArtifactPath(textField(value, field)); } +export function section(value, field) { if (!isRecord(value)) fail(`${field} must be an object.`); return value; } export function textField(value, field) { if (typeof value !== "string" || value.trim().length === 0) fail(`${field} must be a non-empty string.`); @@ -134,64 +151,400 @@ export function textField(value, field) { return trimmed; } -function numberField(value, field) { - if (typeof value !== "number" || !Number.isFinite(value)) fail(`${field} must be a number.`); - return value; -} +function numberField(value, field) { if (typeof value !== "number" || !Number.isFinite(value)) fail(`${field} must be a number.`); return value; } -export function literal(value, expected, field) { - if (value !== expected) fail(`${field} must be ${expected}.`); - return expected; -} +export function literal(value, expected, field) { if (value !== expected) fail(`${field} must be ${expected}.`); return expected; } -export function emptyBlockers(value, field) { - if (!Array.isArray(value)) fail(`${field} must be an array.`); - if (value.length !== 0) fail(`${field} must be empty.`); - return []; -} +export function emptyBlockers(value, field) { if (!Array.isArray(value)) fail(`${field} must be an array.`); if (value.length !== 0) fail(`${field} must be empty.`); return []; } export function stringArray(value, field) { - if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && item.trim().length > 0)) { - fail(`${field} must be a string array.`); - } + if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && item.trim().length > 0)) fail(`${field} must be a string array.`); return value.map((item) => item.trim()); } export function referencedArtifacts(value, field, byId) { - return stringArray(value, field).map((id) => { + const ids = stringArray(value, field); + if (ids.length === 0) fail(`${field} must not be empty.`); + return ids.map((id) => { const artifact = byId.get(id); if (artifact === undefined) fail(`${field} references unknown artifact ${id}.`); return artifact; }); } -function surfaceKind(value, field) { - if (["cli", "http", "terminal", "browser", "gui", "data", TERMINAL_MULTIPLEXER_SURFACE].includes(value)) return value; - fail(`${field} must be a supported manual QA surface.`); +export function passedVerdict(value, field) { if (value === "not_applicable") fail(`${field} must not be not_applicable.`); return literal(value, "passed", field); } + +export function qualityGateSurfaceFamily(value, field) { return qualityGateSurfaceFamilies(value, field)[0]; } + +export function qualityGateSurfaceFamilies(value, field) { + const surface = textField(value, field); + const normalized = surface.toLowerCase(); + if (normalized === TERMINAL_MULTIPLEXER_SURFACE) return ["cli"]; + const tui = /\btui\b/u.test(normalized); + const namedExtension = /\b(?:chrome|firefox|safari|edge)\s+(?:extension|add-on)\b/u.test(normalized) + || /\bwebextensions?\b/u.test(normalized); + const namedBrowser = /\b(?:chrome|chromium|firefox|safari)\s+(?:(?:desktop|mobile)\s+)?browser\b/u.test(normalized) + || /\b(?:chrome|chromium|firefox|safari)\s+on\s+(?:android|ios|ipados|macos|windows|linux|iphone|ipad|mac|macintosh)\b/u.test(normalized) + || /\b(?:android|ios|ipados|macos|windows|linux|iphone|ipad|mac|macintosh)\s+(?:chrome|chromium|firefox|safari)\b/u.test(normalized) + || /\bmicrosoft\s+edge(?:\s+browser)?(?:\s+on\s+(?:android|ios|ipados|macos|windows|linux|iphone|ipad|mac|macintosh))?\b/u.test(normalized) + || /\bedge\s+(?:browser|on\s+(?:android|ios|ipados|macos|windows|linux|iphone|ipad|mac|macintosh))\b/u.test(normalized) + || /^(?:chrome|chromium|firefox|safari|microsoft edge)$/u.test(normalized); + const reactNativeBrand = /\breact[- ]native\b/u.test(normalized); + const reactNativeWeb = /\breact[- ]native[- ]web\b/u.test(normalized) + || /\breact[- ]native\s+(?:app\s+)?(?:for|on)\s+(?:the\s+)?web\b/u.test(normalized); + const rendererWebLabel = /\b(?:pixijs|webgpu)\b/u.test(normalized) + || /\b(?:three|babylon)\.js\b/u.test(normalized); + const explicitBrowser = /\b(web|browser|pwa|html|webassembly|wasm|webgl|blazor)\b/u.test(normalized) + || namedExtension + || namedBrowser + || rendererWebLabel + || normalized === "gui"; + const reactNativeBrowserDelivery = reactNativeWeb || (explicitBrowser && reactNativeBrand); + const compatibleWebBrand = /\b(?:avalonia|slint|kivy|juce|capacitor|cordova|maui)\b/u.test(normalized) + || reactNativeBrowserDelivery; + const browserDeployedCompatibleBrand = explicitBrowser && compatibleWebBrand; + + const embeddedWeb = /\bembedded\b/u.test(normalized) && /\b(web|html|browser)\b/u.test(normalized); + const inherentHybrid = /\b(hybrid|webview2?|electron|tauri|pywebview|wkwebview|cef|webkitgtk|sfsafariviewcontroller)\b/u.test(normalized) + || embeddedWeb + || /\b(?:maui\s+hybrid|qt\s+webengine)\b/u.test(normalized); + const compatibleHybrid = /\b(?:capacitor|cordova)\b/u.test(normalized) && !browserDeployedCompatibleBrand; + const hybrid = inherentHybrid || compatibleHybrid; + + const qtWebAssembly = /\b(?:qt|qml)\b.*\b(webassembly|wasm)\b/u.test(normalized) + || /\b(webassembly|wasm)\b.*\b(?:qt|qml)\b/u.test(normalized); + const contentCompose = /\b(?:email|message)\s+compose\b/u.test(normalized) + || /\bcompose\s+(?:email|message)\b/u.test(normalized); + const composeFramework = !contentCompose && ( + /\bjetpack\s+compose\b/u.test(normalized) + || /\bcompose\s+(?:ui|desktop|multiplatform|for\s+desktop)\b/u.test(normalized) + || /\b(?:android|desktop)\s+compose\s+(?:app|ui)\b/u.test(normalized) + ); + const qtRenderer = /\bqt\s+quick\b/u.test(normalized) || /\bqml\b/u.test(normalized); + const visualToolkitWeb = explicitBrowser && /\b(?:avalonia|slint|kivy|juce)\b/u.test(normalized); + const renderer = /\b(renderer|canvas|texture|flutter|skia|webgl|webgpu)\b/u.test(normalized) + || /\bscene[- ]?graph\b/u.test(normalized) + || /\bcustom[- ]rendered\b/u.test(normalized) + || composeFramework + || qtRenderer + || qtWebAssembly + || rendererWebLabel + || visualToolkitWeb; + + const nativeAppTarget = /\bnative\s+(?:android|ios|ipados|macos|windows|linux|iphone|ipad|mac|macintosh)\b/u.test(normalized) + || /\b(?:android|ios|ipados|macos|windows|linux|iphone|ipad|mac|macintosh)\s+(?:native\s+)?(?:app|application)\b/u.test(normalized); + const browserOnlyNativeWord = explicitBrowser && !nativeAppTarget; + const embeddedNative = /\bembedded\b.*\b(system|device|touchscreen|firmware|native)\b/u.test(normalized); + const nativeKeyword = /\bnative\b/u.test(normalized) && !reactNativeBrowserDelivery && !browserOnlyNativeWord; + const qtNative = /\b(qt|qml)\b/u.test(normalized) && !qtWebAssembly; + const compatibleNativeFramework = /\b(?:avalonia|slint|kivy|juce|maui)\b/u.test(normalized) + && !browserDeployedCompatibleBrand; + const explicitNative = nativeKeyword + || qtNative + || /\b(swiftui|uikit|appkit|winui|wpf|gtk|tkinter|customtkinter|wxwidgets|wxpython|javafx|swing|winforms|windows\s+forms|xamarin|nativescript)\b/u.test(normalized) + || compatibleNativeFramework + || (reactNativeBrand && !reactNativeBrowserDelivery) + || /\bmac\s+catalyst\b/u.test(normalized) + || embeddedNative; + const deviceUi = /\bdevice\s+(?:ui|app|application)\b/u.test(normalized); + const genericTarget = /\b(desktop|mobile|tablet)\b/u.test(normalized); + const concreteNativeTarget = /\b(android|ios|ipados|macos|windows|linux|iphone|ipad|mac|macintosh)\b/u.test(normalized); + + const strongCli = /\b(?:cli|terminal|tmux|command[- ]line)\b/u.test(normalized); + const shell = /\bshell\b/u.test(normalized); + const http = /\bhttp\b/u.test(normalized); + const data = /\b(?:data|api|package|algorithm|math)\b/u.test(normalized); + const gui = /\bgui\b/u.test(normalized); + const platformGui = /\b(?:android|ios|ipados|macos|windows|linux|iphone|ipad|mac|macintosh)\s+gui\b/u.test(normalized); + const supportedGui = normalized === "gui" || explicitBrowser || hybrid || renderer || explicitNative || deviceUi || genericTarget || platformGui; + if (gui && !supportedGui) { + fail(`${field} contains an ambiguous GUI; name a supported framework, delivery target, or owner.`); + } + const uiContext = tui || explicitBrowser || hybrid || renderer || explicitNative || deviceUi || /\b(?:gui|ui|app|application)\b/u.test(normalized); + if (!uiContext && (strongCli || shell)) return ["cli"]; + if (!uiContext && http) return ["http"]; + if (!uiContext && data) return ["data"]; + + const browser = explicitBrowser || hybrid; + const native = explicitNative + || (deviceUi && !explicitBrowser) + || hybrid + || (!tui && concreteNativeTarget && (!explicitBrowser || nativeAppTarget)) + || (!tui && genericTarget && !explicitBrowser); + + const families = []; + if (tui) families.push("tui"); + if (strongCli) families.push("cli"); + if (hybrid) families.push("hybrid"); + if (renderer) families.push("renderer"); + if (native) families.push("native"); + if (browser) families.push("browser"); + if (tui && http) families.push("http"); + if (tui && data && !http) families.push("data"); + if (families.length > 0) return families; + fail(`${field} must be a supported QA surface.`); +} + +export function qualityGateEvidenceScope(row, surfaceFamilies, field) { + const scopedFields = ["target", "owner", "claims", "scopeReason"]; + const present = scopedFields.filter((key) => Object.prototype.hasOwnProperty.call(row, key)); + if (present.length === 0) { + return { scoped: false, proofFamilies: surfaceFamilies, claims: undefined }; + } + if (present.length !== scopedFields.length) { + fail(`${field}.target, ${field}.owner, ${field}.claims, and ${field}.scopeReason must be provided together.`); + } + + const target = parseScopedTarget(row.target, `${field}.target`); + const owner = textField(row.owner, `${field}.owner`).toLowerCase().replaceAll("_", "-"); + const allowedOwners = new Set(surfaceFamilies); + if (allowedOwners.has("hybrid")) { + allowedOwners.add("browser"); + allowedOwners.add("native"); + } + if (!allowedOwners.has(owner)) { + fail(`${field}.owner ${owner} is not an owner of the named surface.`); + } + + const rawClaims = stringArray(row.claims, `${field}.claims`); + if (rawClaims.length === 0) fail(`${field}.claims must not be empty.`); + const claims = rawClaims.map((claim) => claim.toLowerCase().replaceAll("_", "-")); + if (new Set(claims).size !== claims.length) fail(`${field}.claims must not contain duplicate claims.`); + + const claimsByOwner = { + cli: ["interaction"], + tui: ["interaction", "visual"], + browser: ["interaction", "visual", "accessibility", "target", "package-lifecycle"], + native: ["interaction", "visual", "accessibility", "target", "package-lifecycle"], + hybrid: ["interaction", "visual", "accessibility", "target", "package-lifecycle"], + renderer: ["interaction", "visual", "accessibility", "target", "renderer"], + http: ["http"], + data: ["data"] + }; + for (const claim of claims) { + if (!claimsByOwner[owner].includes(claim)) { + fail(`${field}.claims ${claim} is not supported for owner ${owner}.`); + } + } + + return { + scoped: true, + proofFamilies: [owner], + target, + owner, + claims, + scopeReason: textField(row.scopeReason, `${field}.scopeReason`) + }; +} + +export function rejectQualityGateScopeFields(row, field) { const key = ["target", "owner", "claims", "scopeReason"].find((name) => Object.prototype.hasOwnProperty.call(row, name)); if (key !== undefined) fail(`${field}.${key} is not allowed; target/owner slices belong only on surfaceEvidence rows.`); } + +function parseScopedTarget(value, field) { + const target = section(value, field); + const id = textField(target.id, `${field}.id`); + if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(id)) fail(`${field}.id must be a portable lowercase hyphen slug.`); + if (id.split("-").some((token) => VAGUE_TARGET_TOKENS.has(token)) || VAGUE_AGGREGATE_SYMBOL.test(id)) fail(`${field}.id must not contain vague target tokens or aggregate forms.`); + + const platform = textField(target.platform, `${field}.platform`); + if (!/^[a-z0-9]+$/u.test(platform)) fail(`${field}.platform must be one lowercase alphanumeric symbolic ID.`); + if (VAGUE_TARGET_TOKENS.has(platform) || VAGUE_AGGREGATE_SYMBOL.test(platform)) fail(`${field}.platform must not be a vague target token or aggregate form.`); + const environment = textField(target.environment, `${field}.environment`); + if (/\b(?:(?:all|any|every|multi|multiple|universal)[- ]+(?:supported[- ]+)?|supported[- ]+)(?:browsers?|devices?|platforms?|targets?)\b|\bcross[- ]platform\b/iu.test(environment)) fail(`${field}.environment must identify one concrete execution context, not an aggregate target set.`); + return { id, platform, environment }; } -function artifactKind(value, field) { - if (["cli-transcript", "log", "screenshot", "image", "http-dump", "data-diff"].includes(value)) return value; - fail(`${field} must be a supported artifact kind.`); +function registerScopedEvidence(scope, artifacts, field, seenScopes, artifactScopes) { + if (!scope.scoped) return; + const scopeKey = `${scope.target.id}\u0000${scope.target.platform}\u0000${scope.owner}`; + if (seenScopes.has(scopeKey)) fail(`${field} duplicates scoped target ${scope.target.id} on platform ${scope.target.platform} for owner ${scope.owner}.`); + seenScopes.add(scopeKey); + for (const artifact of artifacts) { + const priorScope = artifactScopes.get(artifact.path); + if (priorScope !== undefined && priorScope !== scopeKey) fail(`${field} artifact path ${artifact.path} is reused across distinct scoped target/platform/owner slices.`); + artifactScopes.set(artifact.path, scopeKey); + } } -export function passedVerdict(value, field) { - if (value === "not_applicable") fail(`${field} must not be not_applicable.`); - return literal(value, "passed", field); +function requireReviewPassedOutcome(row, field) { for (const key of ["status", "result"]) if (row[key] !== undefined) passedVerdict(row[key], `${field}.${key}`); } + +export function qualityGateArtifactKind(value, field) { + const normalized = textField(value, field).toLowerCase().replaceAll("_", "-"); + const allowed = [ + "cli-transcript", "log", "failure-mode-test", "browser-automation", "screenshot", "image", "http-dump", "data-diff", + "cli-replay", "pty-capture", "app-automation-transcript", "client-automation-transcript", "api-package-test-report", + "accessibility-tree", "device-report", "package-lifecycle-report", "renderer-trace" + ]; + if (allowed.includes(normalized)) return normalized; + fail(`${field} must be a supported QA artifact kind.`); } -function artifactCompatible(surface, kind) { - if (surface === "cli" || surface === "terminal" || surface === TERMINAL_MULTIPLEXER_SURFACE) return kind === "cli-transcript" || kind === "log"; - if (surface === "http") return kind === "http-dump"; - if (surface === "browser" || surface === "gui") return kind === "screenshot" || kind === "image"; - if (surface === "data") return kind === "data-diff"; - return false; +export function requireQualityGateSurfaceProof(familyOrFamilies, artifacts, field, options = {}) { + const families = Array.isArray(familyOrFamilies) ? familyOrFamilies : [familyOrFamilies]; + const kinds = artifacts.map((artifact) => artifact.kind); + const has = (...expected) => kinds.some((kind) => expected.includes(kind)); + const visual = has("screenshot", "image"); + const appInteraction = has("app-automation-transcript"); + + if (options.legacyMatrixSurface !== undefined) { + const surface = options.legacyMatrixSurface; + if (["browser", "gui", "web"].includes(surface)) { + const automation = has("browser-automation", "app-automation-transcript"); + if (!automation || !visual) fail(`${field} for legacy GUI/web surfaces must reference automation plus screenshot artifacts.`); + return; + } + if (["native", "desktop", "tui"].includes(surface)) { + if (!has("screenshot", "image", "pty-capture", "app-automation-transcript")) { + fail(`${field} for legacy native surfaces must reference screenshot, PTY, or app automation artifacts.`); + } + return; + } + } + + if (options.claims !== undefined) { + for (const family of families) { + requireClaimShapedProof(family, options.claims, { has, visual, appInteraction }, field); + } + return; + } + + for (const family of families) { + if (family === "cli" && !has("cli-transcript", "log", "cli-replay")) { + fail(`${field} for CLI surfaces must reference CLI transcript, log, or replay artifacts.`); + } + if (family === "tui" && !has("pty-capture", "cli-transcript", "log", "cli-replay")) { + fail(`${field} for TUI surfaces must reference PTY, CLI transcript, log, or replay artifacts.`); + } + if (family === "browser") { + const automation = has("browser-automation", "client-automation-transcript", "app-automation-transcript"); + if (!automation && !(options.legacyBrowserProof && visual)) { + fail(`${field} for browser surfaces must reference automation artifacts or exact-legacy visual proof.`); + } + } + if (family === "native") { + if (!appInteraction) { + fail(`${field} for native/mobile/tablet surfaces must reference interaction artifacts; static visual proof alone is insufficient.`); + } + if (!has("device-report")) { + fail(`${field} for native/mobile/tablet surfaces must reference a device report that identifies the real target.`); + } + } + if (family === "hybrid") { + const clientInteraction = has("browser-automation", "client-automation-transcript"); + if (!clientInteraction || !appInteraction) { + fail(`${field} for hybrid surfaces must reference both client interaction and shell interaction artifacts.`); + } + if (!has("device-report")) { + fail(`${field} for hybrid surfaces must reference a device report that identifies the real shell target.`); + } + } + if (family === "renderer") { + const rendererInteraction = has("browser-automation", "client-automation-transcript", "app-automation-transcript"); + const semantic = has("accessibility-tree"); + const renderer = has("renderer-trace"); + if (!rendererInteraction || !semantic || !renderer) { + fail(`${field} for renderer surfaces must reference interaction, accessibility tree, and renderer trace artifacts.`); + } + } + if (family === "http" && !has("http-dump")) { + fail(`${field} for HTTP surfaces must reference an HTTP dump artifact.`); + } + if (family === "data" && !has("data-diff", "api-package-test-report")) { + fail(`${field} for data/package surfaces must reference a data diff or API/package test report artifact.`); + } + } } -export function isRecord(value) { - return value !== null && typeof value === "object" && !Array.isArray(value); +export function qualityGateArtifactCompatible(familyOrFamilies, kind) { + const families = Array.isArray(familyOrFamilies) ? familyOrFamilies : [familyOrFamilies]; + return families.some((family) => { + if (family === "cli") return ["cli-transcript", "log", "cli-replay"].includes(kind); + if (family === "tui") return ["pty-capture", "cli-transcript", "log", "cli-replay", "screenshot", "image", "app-automation-transcript"].includes(kind); + if (family === "browser") return ["browser-automation", "client-automation-transcript", "app-automation-transcript", "screenshot", "image", "accessibility-tree", "device-report", "package-lifecycle-report"].includes(kind); + if (family === "native") { + return ["app-automation-transcript", "screenshot", "image", "accessibility-tree", "device-report", "package-lifecycle-report", "pty-capture"].includes(kind); + } + if (family === "hybrid") { + return ["browser-automation", "client-automation-transcript", "app-automation-transcript", "screenshot", "image", "accessibility-tree", "device-report", "package-lifecycle-report", "renderer-trace"].includes(kind); + } + if (family === "renderer") { + return ["browser-automation", "client-automation-transcript", "app-automation-transcript", "screenshot", "image", "accessibility-tree", "device-report", "renderer-trace"].includes(kind); + } + if (family === "http") return kind === "http-dump"; + if (family === "data") return ["data-diff", "api-package-test-report"].includes(kind); + return false; + }); } -export function fail(message) { - throw new Error(message); +function requireClaimShapedProof(family, claims, proof, field) { + const { has, visual, appInteraction } = proof; + const browserInteraction = has("browser-automation", "client-automation-transcript"); + const rendererInteraction = browserInteraction || appInteraction; + + const requireInteraction = (claim) => { + if (family === "cli" && !has("cli-transcript", "log", "cli-replay")) { + fail(`${field} ${claim} claim for CLI owner must reference CLI transcript, log, or replay artifacts.`); + } + if (family === "tui" && !has("pty-capture", "cli-transcript", "log", "cli-replay")) { + fail(`${field} ${claim} claim for TUI owner must reference PTY, CLI transcript, log, or replay artifacts.`); + } + if (family === "browser" && !browserInteraction) { + fail(`${field} ${claim} claim for browser owner must reference browser or client interaction artifacts.`); + } + if (family === "native" && !appInteraction) { + fail(`${field} ${claim} claim for native owner must reference shell interaction artifacts.`); + } + if (family === "hybrid" && (!browserInteraction || !appInteraction)) { + fail(`${field} ${claim} claim for hybrid owner must reference both client interaction and shell interaction artifacts.`); + } + if (family === "renderer" && !rendererInteraction) { + fail(`${field} ${claim} claim for renderer owner must reference interaction artifacts.`); + } + if (["native", "hybrid"].includes(family) && !has("device-report")) { + fail(`${field} ${claim} claim for ${family} owner must reference a device report that identifies the real target.`); + } + }; + + if (family === "hybrid") requireInteraction("hybrid owner"); + + for (const claim of claims) { + if (claim === "interaction") requireInteraction("interaction"); + if (claim === "visual") { + if (!visual && !(family === "tui" && has("pty-capture"))) { + fail(`${field} visual claim for ${family} owner must reference screenshot or image artifacts.`); + } + if (["native", "hybrid"].includes(family) && !has("device-report")) { + fail(`${field} visual claim for ${family} owner must reference a device report that identifies the real target.`); + } + } + if (claim === "accessibility") { + if (!has("accessibility-tree")) { + fail(`${field} accessibility claim for ${family} owner must reference an accessibility tree artifact.`); + } + requireInteraction("accessibility"); + } + if (claim === "target" && !has("device-report")) { + fail(`${field} target claim for ${family} owner must reference a device report.`); + } + if (claim === "package-lifecycle") { + if (!has("package-lifecycle-report")) { + fail(`${field} package-lifecycle claim for ${family} owner must reference a package lifecycle report.`); + } + if (["native", "hybrid"].includes(family) && !has("device-report")) { + fail(`${field} package-lifecycle claim for ${family} owner must reference a device report that identifies the real target.`); + } + } + if (claim === "renderer" && !has("renderer-trace")) { + fail(`${field} renderer claim must reference a renderer trace artifact.`); + } + if (claim === "http" && !has("http-dump")) { + fail(`${field} HTTP claim must reference an HTTP dump artifact.`); + } + if (claim === "data" && !has("data-diff", "api-package-test-report")) { + fail(`${field} data claim must reference a data diff or API/package test report artifact.`); + } + } } + +export function isRecord(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } + +export function fail(message) { throw new Error(message); } diff --git a/test/docs.test.js b/test/docs.test.js index fecba3a..865e0fe 100644 --- a/test/docs.test.js +++ b/test/docs.test.js @@ -26,6 +26,64 @@ test("public docs describe guide, trace, report, and check evidence surfaces", a assert.match(audit, /src\/report\.js.*Evidence Summary section/is); }); +test("gate notes document proportional owner-and-claim surface evidence", async () => { + const notes = await readFile("docs/superloopy-gate-notes.md", "utf8"); + + assert.match(notes, /scoped row.*exactly one concrete target and one affected owner/is); + assert.match(notes, /target.*owner.*claims.*scopeReason.*all-or-nothing/is); + assert.match(notes, /target.*object.*id.*platform.*environment/is); + assert.match(notes, /id.*stable portable slug.*lowercase ASCII.*single hyphens/is); + assert.match(notes, /aggregate tokens.*`all`.*`any`.*`every`.*`multi`.*`multiple`.*`cross`.*`universal`.*`supported`.*`targets`.*`devices`.*`platforms`.*`browsers`/is); + assert.match(notes, /short ID.*`desktop`.*`mobile`.*`web`.*syntactically valid.*concrete `environment`/is); + assert.match(notes, /platform.*one lowercase alphanumeric symbolic platform ID/is); + assert.match(notes, /environment.*exact runtime.*device.*browser build.*host OS/is); + assert.match(notes, /browser.*native.*hybrid.*renderer.*cli.*tui.*http.*data/is); + assert.match(notes, /interaction.*visual.*accessibility.*target.*package-lifecycle.*renderer.*http.*data/is); + assert.match(notes, /hybrid.*client.*shell.*bridge/is); + assert.match(notes, /complete accepted artifact-kind vocabulary.*cli-transcript.*renderer-trace/is); + assert.match(notes, /Scoped owner\/claim proof minimums/); + assert.match(notes, /structured target description never substitutes for a `device-report`/i); + assert.match(notes, /Within `surfaceEvidence`.*each scoped `target\.id` \+ `target\.platform` \+ `owner` slice.*only once.*different scoped slices.*same resolved artifact path/is); + assert.match(notes, /`adversarialCases`.*`contractCoverage`.*do not declare their own `target` or `owner`.*inherit the relevant surface slice/is); + assert.match(notes, /same `contractRef`.*direct artifact-only coverage.*fail closed/is); + assert.match(notes, /"artifactRefs".*"surfaceEvidence".*"target".*"id".*"platform".*"environment"/is); + assert.match(notes, /all four.*target.*owner.*claims.*scopeReason.*absent.*unscoped/is); + assert.match(notes, /Review exact normalized `browser` or `gui`.*image-only/is); + assert.match(notes, /Matrix exact normalized `browser`.*`gui`.*`web`.*automation plus image/is); + assert.match(notes, /Matrix exact normalized `native`.*`desktop`.*`tui`.*screenshot\/image, PTY, or app-automation/is); + assert.match(notes, /legacy unscoped literal contracts retain their former meaning/is); + assert.match(notes, /mechanical minimum.*truth of the declared scope/is); + + for (const kind of [ + "cli-transcript", "log", "failure-mode-test", "browser-automation", "screenshot", "image", "http-dump", + "data-diff", "cli-replay", "pty-capture", "app-automation-transcript", "client-automation-transcript", + "api-package-test-report", "accessibility-tree", "device-report", "package-lifecycle-report", "renderer-trace" + ]) { + assert.ok(notes.includes(`\`${kind}\``), `gate notes must list accepted artifact kind ${kind}`); + } +}); + +test("frontend context references preserve authority and approval boundaries", async () => { + const designIndex = await readFile("skills/superloopy-frontend/references/design/_INDEX.md", "utf8"); + const redesign = await readFile("skills/superloopy-frontend/references/redesign.md", "utf8"); + const systemMap = await readFile("skills/superloopy-frontend/references/system-map.md", "utf8"); + const fileAudit = await readFile("docs/superloopy-file-audit.md", "utf8"); + const golden = await readFile("docs/superloopy-loop-golden-set.md", "utf8"); + + assert.match(designIndex, /scoped reference input.*never as automatic token authority/is); + assert.match(designIndex, /current design source remains authoritative.*DESIGN\.md.*synchronized scoped mapping\/receipt/is); + assert.match(redesign, /Information architecture stays by default/is); + assert.match(redesign, /broken IA.*actually in scope.*explicitly approved/is); + assert.match(systemMap, /implementation was requested.*execute the one verified package-manager command/is); + assert.match(systemMap, /advice only.*present that one command without running it/is); + assert.match(systemMap, /verify lockfile\/package resolution, imports\/build integration/is); + for (const audit of [fileAudit, golden]) { + assert.match(audit, /Qt Widgets.*Qt Quick.*mixed (?:Qt )?WebAssembly.*browser.*renderer/is); + assert.match(audit, /information architecture stays by default.*explicitly approved|IA by default.*explicitly approved/is); + assert.match(audit, /(?:implementation request.*one verified package-manager command|one verified package-manager command.*implementation request).*advice-only|advice only.*one verified package-manager command/is); + } +}); + test("public docs describe doctor checks", async () => { const readme = await readFile("README.md", "utf8"); const audit = await readFile("docs/superloopy-file-audit.md", "utf8"); @@ -155,58 +213,45 @@ test("README lists the packaged Superloopy skills and their jobs", async () => { assert.match(await readFile("README.ko.md", "utf8"), /guidance alias는 상태를 바꾸지 않습니다/); }); -test("frontend discovery rows publish routed web and Qt evidence", async () => { +test("frontend discovery rows publish explicit screen-based scope and claim-shaped evidence", async () => { const locales = [ { file: "README.md", - invocation: "You explicitly invoke Codex `$superloopy:superloopy-frontend` or Claude Code `/superloopy:superloopy-frontend` for a web frontend or Qt desktop GUI task, or start one with a leading `loopy`/`루피`. Non-web/non-Qt visual work and plain UI mentions do not activate it.", - webEvidence: /for web,.*browser evidence/i, - qtEvidence: /for Qt,.*native rendered-application evidence/i, - scopedPreflight: /for web, the anti-slop pre-flight[^|]*for Qt, the Qt pre-flight/i + invocation: "You explicitly invoke Codex `$superloopy:superloopy-frontend` or Claude Code `/superloopy:superloopy-frontend` for supported screen-based application UI across browser-hosted Web (public, authenticated, private/internal, installed PWA, or extension), interactive deployed content-led Web (campaign, publication, or landing experiences with a user journey), desktop, mobile/tablet, embedded/hybrid clients, custom-rendered UI, Qt, or mixed targets, or start that work with a leading `loopy`/`루피`. Plain UI, platform, or framework terms do not activate it; TV, wearable, XR, automotive, game UI, TUI, static media/document artifacts, and non-UI work stay excluded.", + evidence: "One shared UX contract plus platform/composition routes. Evidence is proportional to changed claims and independently covers each browser, native target/shell, renderer, and mixed target. Standalone runs retain run-scoped receipts; active loops bind them to the goal and criterion." }, { file: "README.ko.md", - invocation: "웹 프런트엔드 또는 Qt 데스크톱 GUI 작업에 Codex의 `$superloopy:superloopy-frontend`나 Claude Code의 `/superloopy:superloopy-frontend`를 직접 호출하거나, 그런 작업을 선행 `loopy`/`루피`로 시작할 때. 웹/Qt 이외의 시각 작업과 단순한 UI 언급으로는 켜지지 않습니다.", - webEvidence: /웹.*브라우저.*근거/u, - qtEvidence: /Qt.*네이티브.*애플리케이션.*렌더링.*근거/u, - scopedPreflight: /웹은 anti-slop pre-flight[^|]*Qt는 Qt pre-flight/u + invocation: "지원되는 화면 기반 애플리케이션 UI(공개·인증·비공개/내부용·설치형 PWA/확장을 포함한 브라우저 호스팅 웹, 사용자 여정이 있는 인터랙티브 배포형 콘텐츠 중심 웹(캠페인·출판·랜딩 경험 등), 데스크톱, 모바일/태블릿, 임베디드/하이브리드 클라이언트, 커스텀 렌더링 UI, Qt, 혼합 타깃) 작업에 Codex의 `$superloopy:superloopy-frontend`나 Claude Code의 `/superloopy:superloopy-frontend`를 직접 호출하거나, 해당 작업을 선행 `loopy`/`루피`로 시작할 때만. 단순한 UI·플랫폼·프레임워크 용어로는 켜지지 않으며 TV·웨어러블·XR·자동차·게임 UI·TUI·정적 미디어/문서 결과물·비 UI 작업은 제외됩니다.", + evidence: "하나의 공통 UX 계약에 플랫폼/컴포지션 경로를 더합니다. 근거는 변경한 주장에 비례하며 브라우저, 네이티브 타깃/셸, 렌더러, 혼합 타깃의 각 소유자를 독립적으로 검증합니다. 독립 실행에서는 실행별 영수증을 보존하고 활성 루프에서는 goal과 criterion에 연결합니다." }, { file: "README.ja.md", - invocation: "Web フロントエンドまたは Qt デスクトップ GUI の作業で Codex の `$superloopy:superloopy-frontend` または Claude Code の `/superloopy:superloopy-frontend` を明示的に呼び出すか、その作業を先頭の `loopy`/`루피` で始めるとき。Web/Qt 以外の視覚タスクや UI への単なる言及では起動しません。", - webEvidence: /Web.*ブラウザー.*証拠/u, - qtEvidence: /Qt.*ネイティブアプリ.*レンダリング.*証拠/u, - scopedPreflight: /Web は anti-slop の事前チェック[^|]*Qt は Qt の事前チェック/u + invocation: "対応範囲の画面ベースのアプリ UI(公開・認証済み・非公開/社内向け・インストール型 PWA/拡張機能を含むブラウザーホスト Web、ユーザージャーニーを備えたインタラクティブなデプロイ済みコンテンツ主導 Web(キャンペーン、出版、ランディング体験など)、デスクトップ、モバイル/タブレット、組み込み/ハイブリッドクライアント、カスタムレンダリング UI、Qt、混在ターゲット)の作業で Codex の `$superloopy:superloopy-frontend` または Claude Code の `/superloopy:superloopy-frontend` を明示的に呼び出すか、その作業を先頭の `loopy`/`루피` で始める場合だけ。単なる UI・プラットフォーム・フレームワーク用語では起動せず、TV、ウェアラブル、XR、自動車、ゲーム UI、TUI、静的なメディア/文書成果物、非 UI 作業は除外します。", + evidence: "1 つの共通 UX 契約にプラットフォーム/コンポジション経路を加えます。証拠は変更した主張に比例し、ブラウザー、ネイティブのターゲット/シェル、レンダラー、混在ターゲットの各所有者を独立して検証します。単独実行では実行単位の証跡を保持し、アクティブなループでは goal と criterion に関連付けます。" }, { file: "README.zh-CN.md", - invocation: "为 Web 前端或 Qt 桌面 GUI 任务在 Codex 中显式调用 `$superloopy:superloopy-frontend`,或在 Claude Code 中调用 `/superloopy:superloopy-frontend`,也可用开头的 `loopy`/`루피` 启动此类任务。非 Web/Qt 可视化工作以及仅提到 UI 都不会激活它。", - webEvidence: /Web.*浏览器证据/u, - qtEvidence: /Qt.*原生应用渲染证据/u, - scopedPreflight: /Web 使用 anti-slop 预检[^|]*Qt 使用 Qt 预检/u + invocation: "仅在处理受支持的基于屏幕的应用 UI(浏览器托管 Web,包括公开、需认证、私有/内部、已安装 PWA 或扩展;具有用户旅程的已部署交互式内容型 Web,例如营销活动、出版物或着陆页体验;以及桌面、移动设备/平板、嵌入式/混合客户端、自定义渲染 UI、Qt 或混合目标)时,在 Codex 中显式调用 `$superloopy:superloopy-frontend`,或在 Claude Code 中调用 `/superloopy:superloopy-frontend`,也可用开头的 `loopy`/`루피` 启动该工作。仅出现 UI、平台或框架词汇不会激活它;TV、可穿戴设备、XR、汽车、游戏 UI、TUI、静态媒体/文档产物和非 UI 工作仍被排除。", + evidence: "采用一份共享 UX 契约,并叠加平台/界面构成路径。证据与变更声明成比例,并分别验证浏览器、原生目标/外壳、渲染器和每个混合目标。独立运行保留按次划分的证据;活动循环则把证据绑定到 goal 和 criterion。" }, { file: "README.es.md", - invocation: "Para una tarea de frontend web o GUI de escritorio Qt, invocas `$superloopy:superloopy-frontend` en Codex o `/superloopy:superloopy-frontend` en Claude Code, o inicias ese tipo de tarea con `loopy`/`루피` al principio. El trabajo visual que no sea web/Qt y la simple mención de UI no la activan.", - webEvidence: /para web,.*evidencia del navegador/i, - qtEvidence: /para Qt,.*aplicación nativa renderizada/i, - scopedPreflight: /para web, la comprobación anti-slop previa[^|]*para Qt, la comprobación previa de Qt/i + invocation: "Solo para interfaces de aplicaciones en pantalla dentro del alcance —Web alojada en navegador (pública, autenticada, privada/interna, PWA instalada o extensión), Web interactiva de contenido ya desplegada (campañas, publicaciones o experiencias de landing con un recorrido de usuario), escritorio, móvil/tableta, clientes embebidos/híbridos, UI con renderizado personalizado, Qt o destinos mixtos—, invocas explícitamente `$superloopy:superloopy-frontend` en Codex o `/superloopy:superloopy-frontend` en Claude Code, o inicias ese trabajo con `loopy`/`루피` al principio. La mera terminología de UI, plataforma o framework no la activa; se excluyen TV, wearables, XR, automoción, UI de juegos, TUI, artefactos estáticos de medios/documentos y trabajo que no sea de UI.", + evidence: "Un contrato de UX compartido más rutas de plataforma/composición. La evidencia es proporcional a las afirmaciones modificadas y verifica por separado cada navegador, destino/shell nativo, renderizador y destino mixto. Las ejecuciones independientes conservan recibos por ejecución; los bucles activos los vinculan al goal y criterion." } ]; - for (const { file, invocation, webEvidence, qtEvidence, scopedPreflight } of locales) { + for (const { file, invocation, evidence } of locales) { const content = await readFile(file, "utf8"); const row = content.split("\n").find((line) => line.startsWith("| `superloopy-frontend` |")); assert.ok(row, `${file} is missing the superloopy-frontend row`); const cells = row.split("|").map((cell) => cell.trim()); assert.equal(cells[2], invocation, `${file} must preserve its complete localized invocation cell`); + assert.equal(cells[3], evidence, `${file} must preserve its complete localized evidence cell`); assert.match(row, /\$superloopy:superloopy-frontend/); assert.match(row, /\/superloopy:superloopy-frontend/); assert.match(row, /loopy.*루피/u); - assert.match(row, webEvidence); - assert.match(row, qtEvidence); - assert.match(row, scopedPreflight, `${file} must scope anti-slop to web and Qt pre-flight to Qt`); - assert.doesNotMatch(row, /for web[^|]*browser evidence for web|for Qt[^|]*evidence for Qt|para web[^|]*evidencia del navegador para web|para Qt[^|]*aplicación nativa renderizada para Qt/i); assert.equal(row.match(/\$superloopy:superloopy-frontend/g)?.length, 1, `${file} must preserve the Codex invocation once`); assert.equal(row.match(/\/superloopy:superloopy-frontend/g)?.length, 1, `${file} must preserve the Claude invocation once`); } @@ -231,13 +276,53 @@ test("English and Korean READMEs publish the runnable Qt Kanban demo", async () } }); -test("frontend agent metadata keeps explicit activation and routed rendered evidence", async () => { +test("Qt Kanban docs publish the prototype acceptance boundary", async () => { + const design = await readFile("examples/qt-kanban/DESIGN.md", "utf8"); + const english = await readFile("README.md", "utf8"); + const korean = await readFile("README.ko.md", "utf8"); + const audit = await readFile("docs/superloopy-file-audit.md", "utf8"); + const golden = await readFile("docs/superloopy-loop-golden-set.md", "utf8"); + + assert.match(design, /prototype acceptance fixture.*not production-editor proof/is); + assert.match(design, /Timeline.*Inbox.*passive.*demo-only/is); + assert.match(design, /Settings.*Help.*absent/is); + assert.match(design, /persistence.*Undo.*out of scope/is); + assert.match( + design, + /Board.*return to Board overview.*clear.*selected task.*detail.*close.*overlay.*Board.*focus/is, + ); + assert.match(english, /prototype acceptance fixture.*not production-editor proof/is); + assert.match(english, /persistence.*Undo.*out of scope/is); + assert.match(korean, /프로토타입 인수 검증 픽스처.*프로덕션 편집기.*증명하지/is); + assert.match(korean, /영속성.*Undo.*범위 밖/is); + assert.match(audit, /Sidebar\.qml.*passive demo-only Timeline\/Inbox/is); + assert.match(golden, /Sidebar\.qml.*passive demo-only Timeline\/Inbox/is); + assert.match( + golden, + /KanbanView\.qml.*Board.*clear.*selection.*close.*overlay.*focus/is, + ); + for (const icon of ["inbox", "timeline"]) { + const row = audit.split("\n").find((line) => + line.startsWith(`| \`examples/qt-kanban/src/Northstar/Kanban/assets/icons/${icon}.svg\` |`)); + assert.ok(row, `missing ${icon} icon audit row`); + assert.match(row, /passive demo-context asset/i); + assert.doesNotMatch(row, /navigation asset/i); + } + assert.doesNotMatch(audit, /assets\/icons\/(?:help|settings)\.svg/u); + assert.doesNotMatch(golden, /assets\/icons\/(?:help|settings)\.svg/u); +}); + +test("frontend agent metadata keeps explicit screen-based routing and claim-shaped evidence", async () => { const agent = await readFile("skills/superloopy-frontend/agents/openai.yaml", "utf8"); - assert.match(agent, /only for a web frontend or Qt desktop GUI task after explicit invocation or an explicit route from an active `loopy`\/`루피` task/iu); - assert.match(agent, /do not use it for non-web\/non-Qt visual deliverables/iu); - assert.match(agent, /real-browser rendered-surface evidence for web/i); - assert.match(agent, /native rendered-application evidence for Qt/i); + assert.match(agent, /short_description: "Explicit cross-platform application UI workflow"/u); + assert.match(agent, /Use `\$superloopy:superloopy-frontend` only after explicit invocation/iu); + assert.match(agent, /browser-hosted or interactive content-led Web, desktop, mobile\/tablet, embedded\/hybrid, Qt, custom-rendered, or mixed application UI/iu); + assert.match(agent, /exclude TV, wearable, XR, automotive, game UI, TUI, static media\/document artifacts, and non-UI work/iu); + assert.match(agent, /proportional evidence per owner and target/iu); + assert.doesNotMatch(agent, /^policy:/mu); + assert.equal(agent.match(/\$superloopy:superloopy-frontend/g)?.length, 1); + assert.equal(agent.match(/\/superloopy:superloopy-frontend/g)?.length ?? 0, 0); assert.doesNotMatch(agent, /auto-activat|when in doubt|plain UI mention|frontend vocabulary/i); }); @@ -279,6 +364,16 @@ test("frontend audit inventories cover each routed reference and contract test o path: "test/frontend-qt-contract.test.js", fileProvenance: /Superloopy-native test/u, goldenProvenance: /node --test test\/frontend-qt-contract\.test\.js/u + }, + { + path: "test/frontend-routing-scenarios.test.js", + fileProvenance: /Superloopy-native regression test/u, + goldenProvenance: /node --test test\/frontend-routing-scenarios\.test\.js/u + }, + { + path: "skills/superloopy-frontend/scripts/evidence-root.mjs", + fileProvenance: /Superloopy-native Node-built-in helper/u, + goldenProvenance: /test\/frontend-routing-scenarios\.test\.js/u } ]; @@ -302,8 +397,28 @@ test("frontend audit inventories cover each routed reference and contract test o } } - assert.match(designRow, /browser evidence for web.*native rendered-application evidence for Qt/iu); - assert.match(designRow, /original prose.*official Qt documentation/iu); + assert.match(designRow, /one shared UX contract.*exact platform and composition routes/iu); + assert.match(designRow, /browser-hosted public\/authenticated\/private Web.*interactive deployed content-led Web.*PWA\/extensions.*desktop.*mobile\/tablet.*embedded\/hybrid.*custom-rendered.*Qt.*mixed surfaces/iu); + assert.match(designRow, /current-state and affected-user evidence.*capability availability.*enabled\/visible\/invocable\/feedback state.*async\/auth\/offline\/conflict.*recovery paths/iu); + assert.match(designRow, /scales design, visual, accessibility, package, and real-target artifacts.*changed claims/iu); + assert.match(designRow, /mixed targets.*independent.*renderer.*shell.*package.*target evidence/iu); + assert.match(designRow, /standalone runs.*run-scoped receipts.*active loops.*goal and criterion/iu); + assert.match(designRow, /Native, hybrid, and custom-rendered claims cannot pass on screenshots alone/iu); + assert.match(designRow, /SEO applies when the current Web target is crawlable and public.*native or embedded.*distinct public Web deployment/iu); + assert.match(designRow, /Qt specialization.*desktop and mobile\/tablet.*official Qt documentation/iu); + assert.match(designRow, /dependency-free helper.*portable run roots.*path escape.*symlink.*duplicate-root.*empty-artifact evidence/iu); + assert.match(designRow, /shared UX and platform\/composition contracts.*original prose.*Superloopy/iu); + assert.match(designRow, /no external runtime dependencies.*vendored/iu); + + const skillGoldenRows = golden.split("\n").filter((line) => line.startsWith("| `skills/superloopy-frontend/SKILL.md` |")); + const agentGoldenRows = golden.split("\n").filter((line) => line.startsWith("| `skills/superloopy-frontend/agents/openai.yaml` |")); + assert.equal(skillGoldenRows.length, 1, "frontend SKILL.md must have one exact golden-set row"); + assert.equal(agentGoldenRows.length, 1, "frontend agent metadata must have one exact golden-set row"); + assert.match(skillGoldenRows[0], /shared UX.*target-specific platform\/composition routes.*Qt specialization.*desktop and mobile\/tablet.*independent proportional evidence/iu); + assert.match(skillGoldenRows[0], /standalone receipts.*without invented loop state/iu); + assert.match(skillGoldenRows[0], /original Superloopy prose/iu); + assert.match(skillGoldenRows[0], /no vendored runtime dependenc(?:y|ies)/iu); + assert.match(agentGoldenRows[0], /explicit-only discovery metadata.*cross-platform application-UI workflow.*unsupported specialized surfaces.*proportional owner\/target evidence/iu); }); test("public docs describe loose prompt triggers as guidance-only", async () => { diff --git a/test/ds-compliance.test.js b/test/ds-compliance.test.js index bb43ed6..151ca84 100644 --- a/test/ds-compliance.test.js +++ b/test/ds-compliance.test.js @@ -8,15 +8,26 @@ const DESIGN = [ "## Color", "- `#2563EB` `--primary`", "- `#0F1729` `--fg`", + "- `#6617212B` `--scrim`", + "- `#abcd` `--compact-alpha`", "## Spacing", "- base 4px scale", ].join("\n"); +test("parseDesignTokens reads the base unit from the base word, not baseline or database prose", () => { + const baseline = parseDesignTokens("The baseline is aligned to a 3px sub-grid.\n- base 4px scale\n"); + const database = parseDesignTokens("The database view uses 7px badges.\n- base 4px scale\n"); + assert.equal(baseline.base, 4); + assert.equal(database.base, 4); +}); + test("parseDesignTokens collects declared colors (normalized) and the base unit", () => { const t = parseDesignTokens(DESIGN); assert.equal(t.base, 4); assert.equal(t.colors.has("#2563eb"), true); assert.equal(t.colors.has("#0f1729"), true); + assert.equal(t.colors.has("#6617212b"), true); + assert.equal(t.colors.has("#aabbccdd"), true); }); test("scanContent flags undeclared hex and off-scale spacing, not on-system values", () => { @@ -40,6 +51,64 @@ test("scanContent flags undeclared hex and off-scale spacing, not on-system valu test("a fully on-system file yields zero violations", () => { const t = parseDesignTokens(DESIGN); - const css = ".ok { color: #2563eb; padding: 8px 16px; margin: 24px; border: 1px solid #0F1729; }"; + const css = ".ok { color: #2563eb; background: #6617212B; outline-color: #ABCD; padding: 8px 16px; margin: -24px; border: 1px solid #0F1729; }"; assert.deepEqual(scanContent(css, t, "ok.css"), []); }); + +test("scanner handles alpha hex plus fractional and negative spacing without partial matches", () => { + const t = parseDesignTokens(DESIGN); + const css = [ + ".a { color: #12345678; }", + ".b { margin: -3px; gap: 0.5px; padding: -8px; }", + ].join("\n"); + const violations = scanContent(css, t, "alpha.css"); + + assert.deepEqual( + violations.map(({ kind, value, line }) => ({ kind, value, line })), + [ + { kind: "undeclared-color", value: "#12345678", line: 1 }, + { kind: "off-scale-spacing", value: "-3px", line: 2 }, + { kind: "off-scale-spacing", value: "0.5px", line: 2 }, + ], + ); +}); + +test("spacing scan matches exact CSS properties without treating border sides as spacing", () => { + const t = parseDesignTokens(DESIGN); + const css = [ + ".frame { border-top: 2px solid #2563EB; border-left-width: 3px; outline-offset: 5px; }", + ".panel { padding-inline-start: 3px; scroll-margin-block: 6px; }", + ].join("\n"); + const spacing = scanContent(css, t, "exact.css") + .filter((entry) => entry.kind === "off-scale-spacing"); + + assert.deepEqual( + spacing.map(({ value, line }) => ({ value, line })), + [ + { value: "3px", line: 2 }, + { value: "6px", line: 2 }, + ], + ); +}); + +test("spacing scan covers multiline declarations and reports the value line", () => { + const t = parseDesignTokens(DESIGN); + const css = [ + ".panel {", + " padding:", + " 3px 8px;", + " margin-block:", + " -5px;", + "}", + ].join("\n"); + const spacing = scanContent(css, t, "multiline.css") + .filter((entry) => entry.kind === "off-scale-spacing"); + + assert.deepEqual( + spacing.map(({ value, line }) => ({ value, line })), + [ + { value: "3px", line: 3 }, + { value: "-5px", line: 5 }, + ], + ); +}); diff --git a/test/frontend-qt-contract.test.js b/test/frontend-qt-contract.test.js index a2cbed0..78dff2a 100644 --- a/test/frontend-qt-contract.test.js +++ b/test/frontend-qt-contract.test.js @@ -12,7 +12,7 @@ const activationContract = `## Activation Open your reply with \`SUPERLOOPY FRONTEND ENABLED\`. If another active Superloopy mode mandates its own first line, print that first and this marker on the next line. -**Explicit activation only.** Engage when the user invokes \`$superloopy:superloopy-frontend\` in Codex or \`/superloopy:superloopy-frontend\` in Claude Code for a web frontend or Qt desktop GUI task, begins such a task with a leading \`loopy\` or \`루피\`, or an already-active Superloopy loop explicitly assigns a web frontend, Qt Widgets, Qt Quick/QML, or mixed web/Qt subtask to this skill. A plain mention of UI, frontend, CSS, layout, responsiveness, Qt, QML, Widgets, or a visible symptom is not authorization to activate this workflow. Non-web/non-Qt visual deliverables and backend, API, data, concurrency, or infrastructure work stay with their primary workflows. +**Explicit activation only.** Engage when the user invokes \`$superloopy:superloopy-frontend\` in Codex or \`/superloopy:superloopy-frontend\` in Claude Code for a supported screen-based application UI task, begins such a task with a leading \`loopy\` or \`루피\`, or an already-active Superloopy loop explicitly routes that task here. Interactive deployed content-led Web such as a campaign, publication, or landing experience is supported when navigation, forms, consent, localization, or another user journey is being built or validated; a static image, video, slide deck, document, or other non-interactive artifact is not. A plain mention of UI, frontend, desktop, mobile, SwiftUI, Tauri, Flutter, Qt, QML, Widgets, or a visible symptom is not authorization to activate this workflow. TV, wearable, XR, automotive, game UI, TUI, static media/document artifacts, and backend, API, data, concurrency, or infrastructure work stay with their primary workflows. `; async function read(path) { @@ -29,44 +29,50 @@ test("frontend contract reader normalizes platform line endings", async (context assert.equal(await read(path), "## Activation\n\nBody\n"); }); -test("frontend skill routes web and Qt only after explicit activation", async () => { +test("frontend skill preserves explicit activation while routing Qt through target-specific UX", async () => { const skill = await read(`${root}/SKILL.md`); const frontmatter = skill.match(/^---\n([\s\S]*?)\n---/u)?.[1] ?? ""; const activation = skill.match(/## Activation\n[\s\S]*?(?=\n## )/)?.[0]; assert.equal(activation, activationContract, "the activation section must remain byte-for-byte unchanged"); - assert.match(frontmatter, /web frontend.*Qt Widgets.*Qt Quick\/QML.*mixed web\/Qt/is); - assert.match(frontmatter, /non-web\/non-Qt visual deliverables/i); + assert.match(frontmatter, /screen-based application UI/is); + assert.match(frontmatter, /browser-hosted Web.*interactive deployed content-led Web.*desktop.*mobile\/tablet.*embedded\/hybrid.*Qt.*custom-rendered.*mixed/is); + assert.match(frontmatter, /TV.*wearable.*XR.*automotive.*game UI.*TUI/is); assert.doesNotMatch(frontmatter, /other visual-deliverable/i); assert.match(skill, /Explicit activation only/i); - assert.match(skill, /web.*Qt Widgets.*Qt Quick\/QML.*mixed/is); - for (const name of ["web", "qt", "qt-widgets", "qt-quick", "qt-qa"]) { + assert.match(skill, /browser-hosted DOM.*Qt Widgets on desktop.*Qt Widgets on mobile or tablet.*Qt Quick\/QML on desktop.*Qt Quick\/QML on mobile or tablet.*Qt Quick\/QML on WebAssembly.*Mixed Qt Widgets.*Qt Quick.*desktop.*Mixed Qt Widgets.*Qt Quick.*mobile or tablet.*mixed/is); + for (const name of ["ux", "desktop", "mobile", "hybrid", "renderer", "web", "qt", "qt-widgets", "qt-quick", "qt-qa"]) { assert.match(skill, new RegExp(`references/${name}\\.md`)); } - assert.match(skill, /mixed web UI and Qt.*references\/web\.md.*references\/qt\.md.*relevant one or both.*references\/qt-widgets\.md.*references\/qt-quick\.md.*references\/qt-qa\.md/is); - assert.match(skill, /mixed web\/Qt.*gates and evidence independently for each surface/is); - assert.match(skill, /web proof cannot substitute for Qt proof.*Qt proof cannot substitute for web proof/is); + assert.match(skill, /mixed or multi-target.*union.*shared UX.*once.*independent.*evidence/is); + assert.match(skill, /Qt Widgets on desktop.*references\/ux\.md.*references\/desktop\.md.*references\/qt\.md.*references\/qt-widgets\.md.*references\/qt-qa\.md/is); + assert.match(skill, /Qt Widgets on mobile or tablet.*references\/ux\.md.*references\/mobile\.md.*references\/qt\.md.*references\/qt-widgets\.md.*references\/qt-qa\.md/is); + assert.match(skill, /Mixed Qt Widgets.*Qt Quick on desktop.*qt-widgets\.md.*qt-quick\.md.*qt-qa\.md/is); + assert.match(skill, /Qt Quick\/QML on desktop.*references\/ux\.md.*references\/desktop\.md.*references\/qt\.md.*references\/qt-quick\.md.*references\/qt-qa\.md/is); + assert.match(skill, /Qt Quick\/QML on mobile or tablet.*references\/ux\.md.*references\/mobile\.md.*references\/qt\.md.*references\/qt-quick\.md.*references\/qt-qa\.md/is); + assert.match(skill, /Qt Quick\/QML on WebAssembly.*references\/ux\.md.*references\/web\.md.*references\/renderer\.md.*references\/qt\.md.*references\/qt-quick\.md.*references\/qt-qa\.md/is); assert.doesNotMatch(skill, /Auto-activate|When in doubt/i); }); -test("web route excludes Qt discovery and references", async () => { +test("browser-hosted DOM web route excludes Qt specialization", async () => { const skill = await read(`${root}/SKILL.md`); const routing = skill.match(/## Inspect and route\n[\s\S]*?(?=\n## )/)?.[0]; assert.ok(routing, "missing inspect-and-route contract"); - const webRow = routing.split("\n").find((line) => line.startsWith("| web UI |")); - assert.equal(webRow, "| web UI | [`references/web.md`](references/web.md) |"); + const webRow = routing.split("\n").find((line) => line.startsWith("| Browser-hosted DOM/document Web |")); + assert.equal(webRow, "| Browser-hosted DOM/document Web | [`references/ux.md`](references/ux.md) + [`references/web.md`](references/web.md) |"); assert.match(routing, /For a Qt route, also resolve the minimum Qt version\./u); - assert.doesNotMatch(routing, /requested surface, existing stack, minimum Qt version/u); const web = await read(reference("web")); assert.doesNotMatch(web, /\b(?:QML|QStyle|QWidget|Qt Test|Qt Quick|Qt Widgets|qmllint)\b/iu); }); -test("web route preserves browser-only gates", async () => { +test("web route preserves browser-only tools without making them universal", async () => { const web = await read(reference("web")); for (const pattern of [/anti-slop/i, /390.*768.*1280/s, /ds-compliance\.mjs/, /Lighthouse/, /real browser/i]) { assert.match(web, pattern); } - assert.match(web, /for any served web-app implementation or validation plan.*production build.*design compliance.*real-browser state capture.*390.*768.*1280.*Lighthouse.*React Doctor only when.*React/is); + assert.match(web, /served Web implementation or plan.*minimum validation floor.*production build.*affected journey.*adjacent regression.*supported real browser/is); + assert.match(web, /design compliance.*visual capture.*390.*768.*1280.*Lighthouse.*React Doctor.*only when.*changed claim.*risk/is); + assert.doesNotMatch(web, /for any served web-app implementation or validation plan.*390.*768.*1280.*Lighthouse/is); }); test("Qt common and Widgets references preserve native ownership", async () => { @@ -82,7 +88,47 @@ test("Qt common and Widgets references preserve native ownership", async () => { assert.match(widgets, /native-adaptive.*branded-deterministic.*QSS/is); assert.match(widgets, /QSS.*QProxyStyle.*same (?:widget )?subtree/is); assert.match(widgets, /QStyledItemDelegate/); - assert.match(widgets, /paint.*hit.test.*keyboard.*accessibility/is); + assert.match(widgets, /semantic part model.*paint.*hit-testing.*supported-input focus\/activation.*accessibility/is); + assert.match(widgets, /keyboard activation when hardware keyboard input is supported/is); +}); + +test("Qt common contract stays target-neutral and composes desktop or mobile ownership", async () => { + const common = await read(reference("qt")); + + assert.doesNotMatch(common, /assumes? a Qt 6 C\+\+ desktop application/iu); + assert.match(common, /target-neutral.*Qt 6/iu); + assert.match(common, /desktop.*references\/desktop\.md.*mobile.*references\/mobile\.md/is); + assert.match( + common, + /target-native evidence gates for Qt-owned non-Web pixels.*Qt WebEngine.*embedded HTML client.*references\/web\.md.*references\/hybrid\.md.*never substitutes.*browser.*client-shell boundary proof/is, + ); + assert.match(common, /mobile.*replaces.*desktop-specific.*input.*window.*display.*chrome.*lifecycle.*package/is); + assert.match(common, /minimum necessary questions.*batch independent unknowns/is); + assert.match(common, /supported input.*pointer.*keyboard.*touch.*switch.*assistive/is); + assert.match(common, /target-supported.*DPR.*display transition/is); + assert.match(common, /target-native system chrome/iu); + assert.match(common, /existing architecture and.*authoritative design source.*cannot override required target behavior or accessibility/is); + assert.match(common, /existing design source of truth remains authoritative.*DESIGN\.md.*only when.*already establishes it.*scoped mapping\/receipt.*links every changed.*synchronized/is); + assert.match(common, /without a formal design-system document is not blocked.*narrow nonvisual change.*Design impact: unchanged.*Visual evidence: not applicable/is); + assert.match( + common, + /For Qt-owned non-Web pixels.*replaces the Web.*checklist.*does not replace.*Web \+ hybrid proof.*Qt WebEngine.*embedded HTML client/is, + ); +}); + +test("Qt Widgets obligations follow the actual target and supported inputs", async () => { + const widgets = await read(reference("qt-widgets")); + + assert.match(widgets, /actual desktop or mobile\/tablet target contract/is); + assert.match(widgets, /Desktop window.*multi-screen.*pointer.*keyboard.*shortcut.*mnemonic.*only when.*named target and supported inputs expose them/is); + assert.match(widgets, /mobile contract owns system bars.*insets.*lifecycle.*orientation\/posture.*touch behavior/is); + assert.match(widgets, /Active.*Inactive.*Disabled.*selected style and named target expose.*record unsupported groups as not applicable/is); + assert.match(widgets, /supported-input focus\/activation.*keyboard activation when hardware keyboard input is supported.*pointer, touch, or pen-event hit testing/is); + assert.match(widgets, /Desktop menus.*window movement\/resizing.*system shortcuts.*multi-screen behavior.*only on named targets that expose those capabilities/is); + assert.match(widgets, /target-applicable window or inset behavior.*supported input.*lifecycle.*assistive technology/is); + assert.match(widgets, /every supported pointer, touch, pen, keyboard, switch, or assistive action path.*same action exactly once/is); + assert.match(widgets, /shortcuts\/mnemonics.*where the named target and supported inputs expose or require them/is); + assert.match(widgets, /target-applicable.*palette group.*selected supported style\/theme/is); }); test("Qt Widgets branded identities stay differentiated and behavior-neutral", async () => { @@ -122,11 +168,18 @@ test("Qt Quick and QA references enforce native validation", async () => { assert.match(quick, /semantics.*unavailable.*standard Control.*validated custom accessibility-interface path/is); assert.doesNotMatch(quick, /Accessible\.(?:value|disabled)\b/); assert.match(quick, /QQuickWidget.*threaded render loop/is); - assert.match(qa, /every implementation or release-proof plan.*configure\/build.*Qt Test\/ctest.*repository lint\/static gates/is); - assert.match(qa, /applicable commands.*run and pass/is); - assert.match(qa, /no relevant repository lint\/static check exists.*N\/A.*evidence/is); - assert.match(qa, /missing.*build.*test infrastructure.*disclosed blocker/is); - assert.match(qa, /before returning.*repository gates.*configure\/build.*BLOCKED.*Qt Test\/ctest.*BLOCKED.*lint\/static.*N\/A with evidence/is); + assert.match(quick, /QQuickWidget.*focus traversal.*hardware keyboard.*touch.*assistive action.*named target/is); + assert.match(quick, /continuous resize.*mixed-DPR.*desktop.*only when.*target.*exposes.*mobile\/tablet.*window.*orientation.*posture.*inset.*display[- ]transition/is); + assert.match(quick, /active.*inactive.*palette.*only when.*selected style.*named target.*expose/is); + assert.match(qa, /every implementation or release-proof plan.*classify.*configure\/build.*Qt Test\/ctest.*repository lint\/static gates/is); + assert.match(qa, /implementation.*run and pass all applicable commands/is); + assert.match( + qa, + /when no relevant check exists.*record the evidenced.*N\/A.*instead of silently omitting the gate/is, + ); + assert.match(qa, /missing required build infrastructure.*blocker.*executable claim/is); + assert.match(qa, /missing test infrastructure.*disclosed gap.*not an automatic blocker/is); + assert.match(qa, /before returning.*repository gates.*configure\/build.*BLOCKED.*N\/A with reason.*Qt Test\/ctest.*GAP with evidence.*BLOCKED.*lint\/static.*N\/A with evidence/is); assert.doesNotMatch(qa, /run and pass all three/i); assert.match(qa, /VISUAL_QA\.md/); assert.match(qa, /offscreen.*not.*native/is); @@ -134,6 +187,19 @@ test("Qt Quick and QA references enforce native validation", async () => { assert.match(qa, /screenshot.*guidance.*verdict/is); }); +test("Qt QA scales commands, state coverage, and visual artifacts to changed claims", async () => { + const qa = await read(reference("qt-qa")); + + assert.match(qa, /validation scope.*changed claims.*risk/is); + assert.match(qa, /implementation.*run.*applicable.*commands.*plan.*name.*intended.*commands/is); + assert.match(qa, /docs-only.*configure\/build.*not applicable.*reason/is); + assert.match(qa, /missing.*test infrastructure.*gap.*not.*automatic blocker/is); + assert.match(qa, /state matrix.*applicable.*changed journey.*adjacent regression/is); + assert.match(qa, /narrow nonvisual.*VISUAL_QA\.md.*not applicable/is); + assert.match(qa, /visual claim.*interaction claim.*visible-state or layout consequence.*VISUAL_QA\.md/is); + assert.match(qa, /real-target behavioral evidence.*without.*decorative screenshot/is); +}); + test("Qt Quick Kanban component colors come from Theme tokens", async () => { const qmlRoot = "examples/qt-kanban/src/Northstar/Kanban"; const qmlFiles = (await readdir(qmlRoot)) @@ -157,6 +223,69 @@ test("Qt Quick Kanban component colors come from Theme tokens", async () => { ); }); +test("Qt Quick Kanban exposes one real sidebar destination and passive demo context", async () => { + const sidebar = await read("examples/qt-kanban/src/Northstar/Kanban/Sidebar.qml"); + const view = await read("examples/qt-kanban/src/Northstar/Kanban/KanbanView.qml"); + const module = await read("examples/qt-kanban/src/Northstar/Kanban/CMakeLists.txt"); + const passive = sidebar.match( + / component PassiveDemoItem:\s*Item \{[\s\S]*?(?=\n contentItem:)/u, + )?.[0]; + + assert.ok(passive, "missing PassiveDemoItem component"); + assert.match(passive, /objectName:\s*demoItem\.compactObjectPrefix \+ "CompactTitle"/u); + assert.match(passive, /objectName:\s*demoItem\.compactObjectPrefix \+ "CompactStatus"/u); + assert.doesNotMatch( + passive, + /\b(?:AbstractButton|Button|MouseArea|PointerHandler|TapHandler|HoverHandler|DragHandler|WheelHandler|Shortcut)\b|(?:Keys\.)?on(?:Clicked|DoubleClicked|Tapped|Pressed|Released|Activated|Triggered|ShortcutOverride)\s*:/u, + "passive demo context must not acquire an action path", + ); + assert.match( + sidebar, + /objectName:\s*"boardButton"[\s\S]*?text:\s*qsTr\("Board"\)[\s\S]*?selected:\s*true/u, + "Board must remain the real selected navigation action", + ); + assert.match(sidebar, /signal boardRequested\(Item invoker\)/u); + assert.match( + sidebar, + /id:\s*boardButton[\s\S]*?onClicked:\s*root\.boardRequested\(boardButton\)/u, + "Board must emit its real sidebar action", + ); + assert.match( + view, + /function returnToBoardOverview\(invoker\)\s*\{[\s\S]*?if \(overlayDetailDrawer\.opened\) \{[\s\S]*?overlayDetailDrawer\.returnFocusToBoard = true[\s\S]*?overlayDetailDrawer\.close\(\)[\s\S]*?return[\s\S]*?\}[\s\S]*?TaskStore\.clearSelection\(\)[\s\S]*?restoreFocus\(invoker\)[\s\S]*?sidebar\.focusBoard\(\)/su, + "Board command must defer overlay cleanup while preserving direct-path selection and focus behavior", + ); + assert.match( + view, + /id:\s*overlayDetailDrawer[\s\S]*?property bool returnFocusToBoard:\s*false[\s\S]*?onClosed:\s*\{[\s\S]*?shouldReturnToBoard = returnFocusToBoard[\s\S]*?returnFocusToBoard = false[\s\S]*?if \(shouldReturnToBoard\)\s*TaskStore\.clearSelection\(\)[\s\S]*?if \(shouldReturnToBoard\) \{[\s\S]*?sidebar\.focusBoard\(\)/su, + "Overlay close must clear selection after dismissal and restore Board focus without flicker", + ); + assert.match( + view, + /onBoardRequested:\s*invoker\s*=>\s*root\.returnToBoardOverview\(invoker\)/u, + "KanbanView must handle the real Sidebar Board signal", + ); + assert.match( + sidebar, + /objectName:\s*"timelineDemoItem"[\s\S]*?title:\s*qsTr\("Timeline"\)/u, + ); + assert.match( + sidebar, + /objectName:\s*"inboxDemoItem"[\s\S]*?title:\s*qsTr\("Inbox"\)/u, + ); + assert.match(sidebar, /demoOnlyLabel:\s*qsTr\("Demo only"\)/u); + assert.match(sidebar, /Accessible\.role:\s*Accessible\.StaticText/u); + assert.doesNotMatch( + sidebar, + /unavailableDestinationRequested|timelineButton|inboxButton|settingsButton|helpButton|not available in this demo/u, + ); + assert.doesNotMatch( + view, + /showStatus|statusToast|statusTimer|onUnavailableDestinationRequested/u, + ); + assert.doesNotMatch(module, /assets\/icons\/(?:settings|help)\.svg/u); +}); + test("Qt Quick accessibility probe cannot chain its own update handler", async () => { const harness = await read("examples/qt-kanban/tests/quick/tst_qtkanban.cpp"); assert.match( diff --git a/test/frontend-quality-overlays.test.js b/test/frontend-quality-overlays.test.js new file mode 100644 index 0000000..21d72b1 --- /dev/null +++ b/test/frontend-quality-overlays.test.js @@ -0,0 +1,180 @@ +import assert from "node:assert/strict"; +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const root = "skills/superloopy-frontend"; +const reference = (name) => `${root}/references/${name}.md`; + +async function read(path) { + assert.equal(existsSync(path), true, `missing ${path}`); + return (await readFile(path, "utf8")).replace(/\r\n?/gu, "\n"); +} + +test("frontend quality overlays are claim-triggered and independent from target routes", async () => { + const skill = await read(`${root}/SKILL.md`); + const targetTable = skill.match(/\| Requested surface \|[\s\S]*?(?=\n### Claim-triggered)/u)?.[0] ?? ""; + const targetRows = targetTable.split("\n").filter((line) => line.startsWith("| ")); + + assert.match(skill, /claim-triggered|cross-cutting quality overlays/iu); + assert.match(skill, /references\/layout\.md/u); + assert.match(skill, /references\/motion-core\.md/u); + assert.match(skill, /layout.*placement.*scroll.*overflow.*reflow.*adaptation/is); + assert.match(skill, /information hierarchy.*content exposure.*disclosure.*spatial presentation.*references\/layout\.md/is); + assert.match(skill, /behavioral-only.*label.*shared UX.*not.*layout/is); + assert.match(skill, /motion.*transition.*gesture.*haptic/is); + assert.match(skill, /smallest applicable union|smallest applicable set/iu); + assert.ok(targetRows.every((line) => !/references\/(?:layout|motion-core)\.md/u.test(line))); +}); + +test("layout overlay defines proportional cross-platform spatial ownership without Web absolutes", async () => { + const layout = await read(reference("layout")); + + assert.match(layout, /only when.*spatial|material spatial/isu); + assert.match(layout, /Layout impact: unchanged/u); + assert.match(layout, /semantic.*model.*reading.*focus.*traversal/is); + assert.match(layout, /layout owner.*scroll owner|scroll owner.*layout owner/is); + assert.match(layout, /size.*constraint.*shrink.*wrap.*overflow/is); + assert.match(layout, /window.*container.*orientation.*text scal/is); + assert.match(layout, /empty.*long.*unbroken.*RTL/is); + assert.match(layout, /multiple.*scroll.*named.*task.*owner/is); + assert.match(layout, /two-dimensional.*task|task.*two-dimensional/is); + assert.match(layout, /virtuali[sz].*logical.*instantiat/is); + assert.match(layout, /narrow.*existing receipt.*new|existing receipt.*narrow/is); + assert.match(layout, /narrow.*existing receipt.*existing surface.*multi-region.*virtuali[sz].*does not expand/is); + assert.match(layout, /only.*affected.*(?:case|boundary)|unaffected.*N\/A/is); + assert.match(layout, /semantic.*task context.*not.*exact.*(?:object|offset)/is); + assert.match(layout, /target-derived.*change point/is); + assert.match(layout, /always visible.*summary.*on demand.*omitted/is); + assert.match(layout, /disclosure label[^\n]*source or summary state[^\n]*disclosed or destination state/is); + assert.match(layout, /always visible[^\n]*omitted[^\n]*do not imply a two-state control/is); + assert.match(layout, /collapsed[^\n]*expanded[^\n]*only when[^\n]*selected disclosure mechanism/is); + assert.match(layout, /details route[^\n]*modal[^\n]*drill-down[^\n]*staged flow[^\n]*contextual reveal[^\n]*source and destination/is); + assert.match(layout, /transition.*focus.*context.*state/is); + assert.match(layout, /actual target[^\n]*source or summary[^\n]*disclosed or destination[^\n]*affected bounds and inputs/is); + assert.doesNotMatch(layout, /(?:(?:each|every|all)(?: affected)? disclosures?[^\n]*(?:must|require|shall)|disclosure always[^\n]*(?:must|require|shall))[^\n]*(?:collaps|expand)/iu); + assert.doesNotMatch(layout, /320\s*\/\s*375\s*\/\s*768|1024\s*\/\s*1440/u); + assert.doesNotMatch(layout, /(?:exactly|at most|no more than) \d+ (?:items|controls)|universal whitespace|Web-only (?:accordion|drawer)/iu); + assert.doesNotMatch(layout, /issue\s*#?28/iu); +}); + +test("motion core preserves provider ownership and temporal truth across platforms", async () => { + const motionCore = await read(reference("motion-core")); + const webMotion = await read(reference("motion")); + + assert.match(motionCore, /only when.*motion|motion.*claim.*changes/isu); + assert.match(motionCore, /Motion impact: unchanged/u); + assert.match(motionCore, /purpose.*trigger.*frequency.*owner/is); + assert.match(motionCore, /before.*intermediate.*after/is); + assert.match(motionCore, /repeated.*interrupt.*revers.*retarget.*cancel/is); + assert.match(motionCore, /focus.*selection.*assistive/is); + assert.match(motionCore, /reduced motion.*semantic (?:outcome|result)/is); + assert.match(motionCore, /system.*provider.*authoritative|provider.*system.*authoritative/is); + assert.match(motionCore, /haptic.*optional.*sole|haptic.*never.*sole/is); + assert.match(motionCore, /not applicable.*reason|N\/A.*reason/is); + assert.match(motionCore, /cross-platform equality.*not.*goal|not.*goal.*cross-platform equality/is); + assert.match(motionCore, /representative.*target.*state.*matrix.*not.*Cartesian/is); + assert.match(motionCore, /screenshot.*not.*motion|motion.*not.*screenshot/is); + assert.match(motionCore, /real-target|actual target/iu); + assert.match(motionCore, /artifact label.*non-empty note.*not.*temporal.*haptic.*proof/is); + assert.match(motionCore, /do not invent.*motion.*haptic.*artifact kind.*validator.*structure.*provenance/is); + assert.doesNotMatch(motionCore, /React|GSAP|ScrollTrigger|DOM|CSS/iu); + assert.doesNotMatch(motionCore, /universal.*(?:duration|easing|spring|FPS)|(?:duration|easing|spring|FPS).*universal/iu); + assert.match(webMotion, /Web.*speciali[sz]ation|speciali[sz].*Web/is); + assert.match(webMotion, /references\/motion-core\.md/u); + assert.doesNotMatch(motionCore, /issue\s*#?28/iu); +}); + +test("visual references distinguish current deltas, exploration, and approved implementation authority", async () => { + const imageFirst = await read(reference("image-first")); + + assert.match(imageFirst, /current-surface delta/iu); + assert.match(imageFirst, /exploratory direction/iu); + assert.match(imageFirst, /approved implementation reference/iu); + assert.match(imageFirst, /exploratory.*non-authoritative.*(?:not|cannot).*implementation/is); + assert.match(imageFirst, /task.*content.*semantic.*spatial.*platform.*accessibility/is); + assert.match(imageFirst, /generated.*text.*not.*(?:content|copy).*authorit/is); + assert.match(imageFirst, /full-surface.*section.*state.*board/is); + assert.match(imageFirst, /pixel-exact.*target.*rendering environment.*project-specific tolerance/is); + assert.match(imageFirst, /without.*(?:criterion|criteria).*visual direction.*not.*exactness/is); + assert.match(imageFirst, /not.*proof.*usability.*accessibility|usability.*accessibility.*not.*proof/is); + assert.doesNotMatch(imageFirst, /Order \(mandatory for visually-important work\)/u); + assert.doesNotMatch(imageFirst, /One image \*\*per section\*\*, never/u); +}); + +test("shared UX scales high-impact rationale, usability evidence, and accepted debt", async () => { + const ux = await read(reference("ux")); + + assert.match(ux, /high-impact.*approve.*block.*redirect|approve.*block.*redirect.*high-impact/is); + assert.match(ux, /authority.*criteria.*warrant.*evidence.*limitation/is); + assert.match(ux, /alternative.*not applicable.*existing.*authoritative/is); + assert.match(ux, /new.*redesign.*high-consequence.*task.*context.*result.*limitation/is); + assert.match(ux, /walkthrough.*not.*user evidence|user evidence.*not.*walkthrough/is); + assert.match(ux, /known.*accepted.*gap|accepted.*known.*gap/is); + assert.match(ux, /affected users.*owner.*review trigger.*exit/is); + assert.match(ux, /unknown.*not.*debt|debt.*not.*unknown/is); + assert.match(ux, /false success.*core task.*accessibility.*privacy.*security.*data loss/is); + assert.match(ux, /unknown owner.*review_required|review_required.*unknown owner/is); + assert.match(ux, /review_required.*decision disposition.*not.*capability/is); + assert.match(ux, /acceptance authority.*own.*(?:release|risk).*boundary/is); + assert.match(ux, /accountable owner.*distinct from.*technical.*surfaceEvidence\.owner/is); + assert.match(ux, /critical.*core task.*escap.*necessary state.*recover/is); +}); + +test("Web redesign specialization does not silently route native redesigns through browser rules", async () => { + const skill = await read(`${root}/SKILL.md`); + const redesign = await read(reference("redesign")); + + assert.match(skill, /redesign\.md.*Web-only|Web-only.*redesign\.md/is); + assert.match(skill, /native.*redesign.*ux\.md.*layout\.md.*image-first\.md/is); + assert.match(redesign, /Web-only.*living site|living site.*Web-only/is); +}); + +test("campaign composition heuristics cannot become unsupported universal blockers", async () => { + const antiSlop = await read(reference("anti-slop")); + + assert.match(antiSlop, /review flag.*approved.*(?:brand|content|campaign).*criterion/is); + assert.match(antiSlop, /unintended[^\n]*(?:root|page)[^\n]*horizontal overflow/is); + assert.match(antiSlop, /intentional[^\n]*task[^\n]*content-owned[^\n]*horizontal[^\n]*valid[^\n]*layout\.md[^\n]*spatial and scroll ownership/is); + assert.doesNotMatch(antiSlop, /^\s*(?:-\s*\[[ x]\]\s*)?(?:No horizontal scroll(?:ing)?(?:\s+is allowed|\s+at\b)|Horizontal scroll(?:ing)? is forbidden)\b/imu); + assert.doesNotMatch(antiSlop, /fail when/iu); +}); + +test("StyleGallery remains an independent research reference with bounded provenance", async () => { + const notice = await read(reference("upstream-notice")); + const designAudit = await read("docs/superloopy-design-audit.md"); + const fileAudit = await read("docs/superloopy-file-audit.md"); + const golden = await read("docs/superloopy-loop-golden-set.md"); + const commit = "24ae581e92666a8864d42ffa64f7c34caf3a2156"; + + assert.match(notice, /github\.com\/changeroa\/StyleGallery/u); + assert.ok(notice.includes(commit)); + assert.match(notice, /architecture research|research reference/iu); + assert.match(notice, /no root license|root license.*not.*identified/iu); + assert.match(notice, /no.*(?:code|prose).*schema.*pattern.*copied/is); + assert.match(notice, /no.*runtime dependency|no.*dependency/is); + assert.match(designAudit, /StyleGallery.*independent.*quality overlay/is); + assert.match(designAudit, /rejects.*fixed Web.*CSS pattern.*mandatory image.*universal motion/is); + assert.match(designAudit, /does not add.*motion.*haptic.*artifact kind.*file existence.*not.*proof/is); + + for (const path of [reference("layout"), reference("motion-core"), "test/frontend-quality-overlays.test.js"]) { + assert.equal(fileAudit.split("\n").filter((line) => line.startsWith(`| \`${path}\` |`)).length, 1); + assert.equal(golden.split("\n").filter((line) => line.startsWith(`| \`${path}\` |`)).length, 1); + } +}); + +test("audits assign resource, hierarchy, and exposure responsibilities to existing contracts", async () => { + const designAudit = await read("docs/superloopy-design-audit.md"); + const fileAudit = await read("docs/superloopy-file-audit.md"); + const golden = await read("docs/superloopy-loop-golden-set.md"); + const row = (content, path) => content.split("\n").find((line) => line.startsWith(`| \`${path}\` |`)) ?? ""; + assert.match(designAudit, /frontend-quality-skill.*resource identity.*reset provenance.*information hierarchy.*proportional disclosure/is); + for (const content of [fileAudit, golden]) { + assert.match(row(content, `${root}/SKILL.md`), /information hierarchy.*spatial disclosure/is); + assert.match(row(content, reference("ux")), /resource identity.*reset provenance.*proportional disclosure/is); + assert.match(row(content, reference("layout")), /exposure mode.*source.*destination.*conditional.*collaps.*expand/is); + assert.match(row(content, reference("anti-slop")), /unintended.*root.*horizontal.*task-owned.*layout/is); + assert.match(row(content, "test/frontend-ux-semantics.test.js"), /resource identity.*reset.*information hierarchy/is); + assert.match(row(content, "test/frontend-quality-overlays.test.js"), /generic disclosure.*horizontal-scroll.*audit responsibilit/is); + } +}); diff --git a/test/frontend-routing-scenarios.test.js b/test/frontend-routing-scenarios.test.js new file mode 100644 index 0000000..fa88f75 --- /dev/null +++ b/test/frontend-routing-scenarios.test.js @@ -0,0 +1,166 @@ +import assert from "node:assert/strict"; +import { existsSync } from "node:fs"; +import { mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { spawnSync } from "node:child_process"; +import test from "node:test"; + +const skillPath = "skills/superloopy-frontend/SKILL.md"; + +async function read(path) { + return (await readFile(path, "utf8")).replace(/\r\n?/gu, "\n"); +} + +function routeReferences(skill, label) { + const row = skill.split("\n").find((line) => line.startsWith(`| ${label} |`)); + assert.ok(row, `missing route row: ${label}`); + return [...row.matchAll(/\]\(references\/([a-z-]+)\.md\)/gu)].map((match) => match[1]); +} + +test("frontend routes exact target-derived reference sets", async () => { + const skill = await read(skillPath); + const expected = new Map([ + ["Browser-hosted DOM/document Web", ["ux", "web"]], + ["Browser-hosted canvas/custom-rendered Web", ["ux", "web", "renderer"]], + ["Installed PWA or browser extension", ["ux", "web"]], + ["Embedded HTML on desktop", ["ux", "web", "desktop", "hybrid"]], + ["Embedded HTML on mobile", ["ux", "web", "mobile", "hybrid"]], + ["Native/custom desktop", ["ux", "desktop"]], + ["Qt Widgets on desktop", ["ux", "desktop", "qt", "qt-widgets", "qt-qa"]], + ["Qt Widgets on mobile or tablet", ["ux", "mobile", "qt", "qt-widgets", "qt-qa"]], + ["Qt Quick/QML on desktop", ["ux", "desktop", "qt", "qt-quick", "qt-qa"]], + ["Qt Quick/QML on mobile or tablet", ["ux", "mobile", "qt", "qt-quick", "qt-qa"]], + ["Qt Widgets on WebAssembly", ["ux", "web", "renderer", "qt", "qt-widgets", "qt-qa"]], + ["Qt Quick/QML on WebAssembly", ["ux", "web", "renderer", "qt", "qt-quick", "qt-qa"]], + ["Mixed Qt Widgets + Qt Quick on WebAssembly", ["ux", "web", "renderer", "qt", "qt-widgets", "qt-quick", "qt-qa"]], + ["Mixed Qt Widgets + Qt Quick on desktop", ["ux", "desktop", "qt", "qt-widgets", "qt-quick", "qt-qa"]], + ["Mixed Qt Widgets + Qt Quick on mobile or tablet", ["ux", "mobile", "qt", "qt-widgets", "qt-quick", "qt-qa"]], + ["Native/cross-platform mobile or tablet", ["ux", "mobile"]], + ["Mixed or multi-target", ["ux"]], + ]); + + for (const [label, references] of expected) { + assert.deepEqual(routeReferences(skill, label), references, label); + } +}); + +test("frontend scope excludes every intentionally unsupported specialized surface", async () => { + const skill = await read(skillPath); + const metadata = await read("skills/superloopy-frontend/agents/openai.yaml"); + + for (const content of [skill, metadata]) { + assert.match(content, /TV.*wearable.*XR.*automotive.*game UI.*TUI/is); + } + assert.match(skill, /authenticated.*private|private.*authenticated/is); + assert.match(skill, /PWA.*browser extension/is); +}); + +test("frontend completion is proportional and works with or without an active loop", async () => { + const skill = await read(skillPath); + + assert.doesNotMatch(skill, /Ask one question only/u); + assert.match(skill, /minimum necessary questions|fewest necessary questions/iu); + assert.match(skill, /Design impact: unchanged/u); + assert.match(skill, /visual claim.*interaction claim.*visible-state or layout consequence/isu); + assert.match(skill, /without an active.*loop|no active.*loop/iu); + assert.match(skill, /active.*loop.*goal.*criterion|goal.*criterion.*active.*loop/isu); + assert.match(skill, /YYYYMMDDTHHMMSSZ-/u); + assert.match(skill, /portable.*lowercase ASCII.*single hyphen/iu); +}); + +test("frontend routing keeps renderer proof conditional and scopes single-target evidence", async () => { + const skill = await read(skillPath); + const hybrid = await read("skills/superloopy-frontend/references/hybrid.md"); + + assert.match(hybrid, /renderer.*only when.*owns.*pixels.*semantics.*text.*input/isu); + assert.match(skill, /Qt Widgets on WebAssembly.*Qt Quick\/QML on WebAssembly.*mixed Qt WebAssembly.*browser and renderer proof/isu); + assert.match(skill, /every new scoped.*single.*mixed.*multi-target.*target.*id.*platform.*environment.*owner.*claims.*scopeReason.*Gate Notes/isu); +}); + +test("frontend evidence helper creates and verifies portable run-scoped roots", async (t) => { + const repoRoot = process.cwd(); + const helper = join(repoRoot, "skills/superloopy-frontend/scripts/evidence-root.mjs"); + const tokenLint = join(repoRoot, "skills/superloopy-frontend/scripts/ds-compliance.mjs"); + const sandbox = await mkdtemp(join(tmpdir(), "superloopy-frontend-evidence-")); + t.after(() => rm(sandbox, { recursive: true, force: true })); + + const created = spawnSync(process.execPath, [helper, "create", "frontend-check"], { + cwd: sandbox, + encoding: "utf8" + }); + assert.equal(created.status, 0, created.stderr); + const evidenceRoot = created.stdout.trim(); + assert.match(evidenceRoot, /^\.superloopy\/evidence\/frontend\/\d{8}T\d{6}Z-frontend-check$/u); + assert.equal((await stat(join(sandbox, evidenceRoot))).isDirectory(), true); + + await mkdir(join(sandbox, "dist"), { recursive: true }); + await writeFile(join(sandbox, "DESIGN.md"), "Base spacing: 4px\nColor: #112233\n", "utf8"); + await writeFile(join(sandbox, "dist", "app.css"), ".card { color: #112233; margin: 8px; }\n", "utf8"); + const scanned = spawnSync(process.execPath, [tokenLint, "DESIGN.md", "dist/app.css"], { + cwd: sandbox, + encoding: "utf8" + }); + assert.equal(scanned.status, 0, scanned.stderr); + await writeFile(join(sandbox, evidenceRoot, "token-lint.txt"), scanned.stdout, "utf8"); + const verified = spawnSync(process.execPath, [helper, "verify", evidenceRoot, "token-lint.txt"], { + cwd: sandbox, + encoding: "utf8" + }); + assert.equal(verified.status, 0, verified.stderr); + + for (const slug of ["..", "Bad-Slug", "two--parts", "con", "bad/slug"]) { + const rejected = spawnSync(process.execPath, [helper, "create", slug], { cwd: sandbox, encoding: "utf8" }); + assert.notEqual(rejected.status, 0, slug); + } + const escaped = spawnSync(process.execPath, [helper, "verify", evidenceRoot, "../outside.txt"], { + cwd: sandbox, + encoding: "utf8" + }); + assert.notEqual(escaped.status, 0); +}); + +test("frontend evidence helper rejects symlinked proofs and roots while running through a linked entrypoint", { + skip: process.platform === "win32" ? "file symlink creation is not reliably available on Windows CI" : false +}, async (t) => { + const repoRoot = process.cwd(); + const helper = join(repoRoot, "skills/superloopy-frontend/scripts/evidence-root.mjs"); + const sandbox = await mkdtemp(join(tmpdir(), "superloopy-frontend-evidence-symlink-")); + t.after(() => rm(sandbox, { recursive: true, force: true })); + + const created = spawnSync(process.execPath, [helper, "create", "frontend-check"], { + cwd: sandbox, + encoding: "utf8" + }); + assert.equal(created.status, 0, created.stderr); + const evidenceRoot = created.stdout.trim(); + await writeFile(join(sandbox, evidenceRoot, "token-lint.txt"), "token lint proof\n", "utf8"); + + await symlink(join(sandbox, evidenceRoot, "token-lint.txt"), join(sandbox, evidenceRoot, "linked-proof.txt")); + const linkedProof = spawnSync(process.execPath, [helper, "verify", evidenceRoot, "linked-proof.txt"], { + cwd: sandbox, + encoding: "utf8" + }); + assert.notEqual(linkedProof.status, 0); + + const linkedHelper = join(sandbox, "evidence-root-link.mjs"); + await symlink(helper, linkedHelper); + const throughLinkedHelper = spawnSync(process.execPath, [linkedHelper, "create", "linked-helper"], { + cwd: sandbox, + encoding: "utf8" + }); + assert.equal(throughLinkedHelper.status, 0, throughLinkedHelper.stderr); + assert.match(throughLinkedHelper.stdout, /linked-helper/u); + + const escapedProject = await mkdtemp(join(tmpdir(), "superloopy-frontend-symlink-project-")); + const outside = await mkdtemp(join(tmpdir(), "superloopy-frontend-symlink-outside-")); + t.after(() => rm(escapedProject, { recursive: true, force: true })); + t.after(() => rm(outside, { recursive: true, force: true })); + await symlink(outside, join(escapedProject, ".superloopy")); + const escapedCreate = spawnSync(process.execPath, [helper, "create", "escape-check"], { + cwd: escapedProject, + encoding: "utf8" + }); + assert.notEqual(escapedCreate.status, 0); + assert.equal(existsSync(join(outside, "evidence")), false); +}); diff --git a/test/frontend-ux-contract.test.js b/test/frontend-ux-contract.test.js new file mode 100644 index 0000000..84c05ad --- /dev/null +++ b/test/frontend-ux-contract.test.js @@ -0,0 +1,537 @@ +import assert from "node:assert/strict"; +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const root = "skills/superloopy-frontend"; +const reference = (name) => `${root}/references/${name}.md`; + +async function read(path) { + assert.equal(existsSync(path), true, `missing ${path}`); + return (await readFile(path, "utf8")).replace(/\r\n?/gu, "\n"); +} + +test("frontend scope stays explicit while covering supported screen-based application UI", async () => { + const skill = await read(`${root}/SKILL.md`); + const frontmatter = skill.match(/^---\n([\s\S]*?)\n---/u)?.[1] ?? ""; + const activation = skill.match(/## Activation\n[\s\S]*?(?=\n## )/u)?.[0] ?? ""; + + assert.match(frontmatter, /explicit.*screen-based application UI/is); + assert.match(frontmatter, /browser-hosted Web.*interactive deployed content-led Web.*desktop.*mobile\/tablet.*embedded\/hybrid.*Qt.*custom-rendered.*mixed/is); + assert.match(frontmatter, /Do not activate.*UI.*frontend.*desktop.*mobile.*framework.*vocabulary alone/is); + assert.match(frontmatter, /TV.*wearable.*XR.*game UI.*TUI.*static media\/document artifacts/is); + assert.match(activation, /interactive deployed content-led Web.*campaign.*publication.*landing.*navigation.*forms.*consent.*localization/is); + assert.match(activation, /SUPERLOOPY FRONTEND ENABLED/u); + assert.match(activation, /Explicit activation only/iu); + assert.match(activation, /\$superloopy:superloopy-frontend/u); + assert.match(activation, /\/superloopy:superloopy-frontend/u); + assert.match(activation, /leading `loopy` or `루피`/u); + assert.match(activation, /plain mention.*UI.*desktop.*mobile.*SwiftUI.*Tauri.*Flutter.*Qt.*QML.*not authorization/is); + assert.doesNotMatch(skill, /Auto-activate|When in doubt/iu); +}); + +test("frontend discovery resolves the user outcome, target, ownership, runtime, and proof before routing", async () => { + const skill = await read(`${root}/SKILL.md`); + const routing = skill.match(/## Inspect and route\n[\s\S]*?(?=\n## )/u)?.[0] ?? ""; + + for (const pattern of [ + /affected users.*job.*outcome.*evidence.*assumption.*confidence/is, + /OS.*device.*desktop environment.*session/is, + /public.*embedded.*native-control.*custom-rendered.*mixed.*composition/is, + /renderer.*semantic.*accessibility model/is, + /client.*shell.*service.*document.*state ownership/is, + /framework.*runtime.*provider.*backend.*version/is, + /package.*sandbox.*update.*distribution channel.*persistence boundary/is, + /input.*locale.*accessibility services.*validation capability/is, + ]) { + assert.match(routing, pattern); + } + assert.match(routing, /Ask the minimum necessary questions only when material unknowns would change the route or result/u); + assert.match(routing, /batch independent unknowns when more than one must be answered/u); +}); + +test("frontend route matrix composes shared, platform, hybrid, renderer, and Qt contracts", async () => { + const skill = await read(`${root}/SKILL.md`); + const routing = skill.match(/## Inspect and route\n[\s\S]*?(?=\n## )/u)?.[0] ?? ""; + const expected = [ + ["Browser-hosted DOM/document Web", ["ux", "web"]], + ["Browser-hosted canvas/custom-rendered Web", ["ux", "web", "renderer"]], + ["Installed PWA or browser extension", ["ux", "web"]], + ["Embedded HTML on desktop", ["ux", "web", "desktop", "hybrid"]], + ["Embedded HTML on mobile", ["ux", "web", "mobile", "hybrid"]], + ["Native/custom desktop", ["ux", "desktop"]], + ["Qt Widgets on desktop", ["ux", "desktop", "qt", "qt-widgets", "qt-qa"]], + ["Qt Widgets on mobile or tablet", ["ux", "mobile", "qt", "qt-widgets", "qt-qa"]], + ["Qt Quick/QML on desktop", ["ux", "desktop", "qt", "qt-quick", "qt-qa"]], + ["Qt Quick/QML on mobile or tablet", ["ux", "mobile", "qt", "qt-quick", "qt-qa"]], + ["Qt Widgets on WebAssembly", ["ux", "web", "renderer", "qt", "qt-widgets", "qt-qa"]], + ["Qt Quick/QML on WebAssembly", ["ux", "web", "renderer", "qt", "qt-quick", "qt-qa"]], + ["Mixed Qt Widgets + Qt Quick on WebAssembly", ["ux", "web", "renderer", "qt", "qt-widgets", "qt-quick", "qt-qa"]], + ["Mixed Qt Widgets + Qt Quick on desktop", ["ux", "desktop", "qt", "qt-widgets", "qt-quick", "qt-qa"]], + ["Mixed Qt Widgets + Qt Quick on mobile or tablet", ["ux", "mobile", "qt", "qt-widgets", "qt-quick", "qt-qa"]], + ["Native/cross-platform mobile or tablet", ["ux", "mobile"]], + ["Mixed or multi-target", ["ux"]], + ]; + + for (const [label, references] of expected) { + const row = routing.split("\n").find((line) => line.startsWith(`| ${label} |`)); + assert.ok(row, `missing route row: ${label}`); + for (const name of references) { + assert.match(row, new RegExp(`references/${name}\\.md`), `${label} must load ${name}.md`); + } + } + assert.match(routing, /add.*renderer\.md.*engine|engine.*add.*renderer\.md/is); + assert.match( + routing, + /load shared UX once.*one evidence row per target and affected owner.*independent, attributable proof/is, + ); +}); + +test("framework examples route by deployed facts instead of brand", async () => { + const skill = await read(`${root}/SKILL.md`); + const routing = skill.match(/## Inspect and route\n[\s\S]*?(?=\n## )/u)?.[0] ?? ""; + + for (const pattern of [ + /Tauri.*pywebview.*actual desktop or mobile target.*embedded client/is, + /Electron.*desktop.*bundled Chromium/is, + /CustomTkinter.*native\/custom desktop/is, + /Flutter.*Compose.*renderer.*semantics/is, + /React Native.*provider.*target/is, + /MAUI Hybrid.*native host.*embedded client/is, + /Qt.*matching desktop, mobile\/tablet, or WebAssembly row/is, + ]) { + assert.match(routing, pattern); + } + assert.match(routing, /Mac Catalyst.*UIKit-on-desktop.*AppKit/is); + assert.match(routing, /iPadOS.*mobile.*desktop-capabilities overlay/is); +}); + +test("shared UX contract scales ceremony and keeps capability claims truthful", async () => { + const ux = await read(reference("ux")); + + assert.match(ux, /frame the delta.*users.*job.*outcome.*evidence.*assumption.*confidence/is); + assert.match(ux, /map.*journeys.*capabilities/is); + assert.match(ux, /behavior.*invariants/is); + assert.match(ux, /claim-shaped evidence/is); + assert.match(ux, /promot.*capability by capability.*real-target evidence/is); + assert.match(ux, /narrow fix.*UX impact: unchanged.*expanded journey.*new\/redesigned\/high-consequence/is); + assert.match(ux, /UX_CONTRACT\.md.*expanded journey.*new\/redesigned\/high-consequence/is); + + assert.match(ux, /role.*action.*navigation.*input.*output/is); + assert.match(ux, /decoration.*not a capability/is); + assert.match(ux, /fidelity.*production.*simulated.*deferred/is); + assert.match(ux, /applicability.*applicable.*not applicable/is); + assert.match(ux, /availability.*available.*temporarily unavailable.*unavailable.*not evaluated/is); + assert.match(ux, /presentation.*visible.*hidden.*omitted.*passive/is); + assert.match(ux, /invocability.*enabled.*disabled.*not invocable/is); + assert.match(ux, /feedback\/result.*idle.*pending.*succeeded.*failed.*blocked.*not evaluated/is); + assert.match(ux, /verification.*proven.*failed.*inconclusive.*blocked.*unverified/is); + assert.match(ux, /not applicable.*availability.*not evaluated.*presentation.*omitted.*invocability.*not invocable.*feedback\/result.*not evaluated/is); + assert.match(ux, /visible, enabled production affordance.*advertised semantic (?:outcome|result)/is); + assert.match(ux, /available capability.*disabled.*prerequisite.*not.*unavailable/is); + assert.match(ux, /hidden.*not.*unavailable.*policy.*role.*context/is); + assert.match(ux, /pending.*succeeded.*failed.*durable owner/is); + assert.match(ux, /handler.*toast.*spinner.*mock.*not completion/is); + assert.match(ux, /failed operation.*resulting state.*recovery/is); + assert.match(ux, /simulated.*cannot.*production.*native acceptance/is); + assert.match(ux, /deferred.*normally omitted.*stable top-level.*honestly unavailable/is); + assert.match( + ux, + /deferred future capability.*no operable affordance.*applicability.*not applicable.*availability.*not evaluated.*presentation.*omitted.*invocability.*not invocable.*feedback\/result.*not evaluated.*verification.*unverified/is, + ); + assert.match( + ux, + /stable top-level.*signpost.*separate passive output capability.*production.*available.*passive.*not invocable.*independently.*proven.*unverified.*never.*deferred navigation affordance.*button.*link.*focus action/is, + ); + assert.match(ux, /temporarily unavailable.*prerequisite.*accessible reason.*next step/is); + assert.match(ux, /editable-looking.*edit.*validate.*commit.*cancel/is); + assert.match(ux, /read-only.*output.*clearly read-only/is); +}); + +test("shared workflow starts from the current experience and traces contract to implementation", async () => { + const ux = await read(reference("ux")); + + assert.match(ux, /baseline|current[- ]state/iu); + assert.match(ux, /reproduce.*current.*journey|current.*journey.*reproduce/is); + assert.match(ux, /affected journey.*adjacent journey.*regression/is); + assert.match(ux, /existing.*component.*state owner.*test/is); + assert.match( + ux, + /contract (?:clause|invariant).*acceptance (?:case|criterion).*implementation (?:owner|file).*test.*evidence/is, + ); +}); + +test("shared workflow scales user coverage and operating states by consequence", async () => { + const ux = await read(reference("ux")); + + assert.match(ux, /risk-gated user coverage matrix/is); + assert.match(ux, /primary.*secondary.*affected.*not affected.*counterexample/is); + assert.match(ux, /role.*expertise.*accessibility.*locale.*device.*input.*connectivity.*account.*entitlement/is); + assert.match(ux, /frequency.*severity.*success measure/is); + assert.match(ux, /narrow.*low-risk.*only.*material|material.*narrow.*low-risk/is); + assert.match( + ux, + /initial.*loading.*empty.*partial.*stale.*offline.*degraded.*error.*retry.*cancel/is, + ); + assert.match(ux, /optimistic.*duplicate.*conflict.*concurrent/is); + assert.match(ux, /authentication.*session expiry.*authori[sz]ation.*permission.*role.*entitlement/is); + assert.match(ux, /privacy.*consent.*sensitive/is); +}); + +test("shared input and motion rules cover editable text, IME, and reduced motion", async () => { + const ux = await read(reference("ux")); + const mobile = await read(reference("mobile")); + + assert.match(ux, /editable text.*selection.*clipboard.*composition.*IME.*commit.*cancel/is); + assert.match(ux, /composition.*validation.*after.*commit|validation.*composition.*commit/is); + assert.match(ux, /reduced motion.*system.*semantic outcome/is); + assert.match(mobile, /IME|input method/u); + assert.match(mobile, /reduced motion|animation scale/iu); +}); +test("narrow nonvisual changes do not require design or visual artifacts", async () => { + const ux = await read(reference("ux")); + const web = await read(reference("web")); + + for (const contract of [ux, web]) { + assert.match(contract, /narrow nonvisual.*Design impact: unchanged/is); + assert.match(contract, /Visual evidence: not applicable/is); + assert.match(contract, /behavioral.*accessibility.*regression evidence/is); + } + assert.match(web, /DESIGN\.md.*only.*visual|visual.*DESIGN\.md.*required/is); + assert.match(web, /VISUAL_QA\.md.*only.*visual|visual.*VISUAL_QA\.md.*required/is); + assert.match( + web, + /current design source of truth remains authoritative.*DESIGN\.md.*scoped mapping\/receipt.*no equivalent.*changed value.*links back.*synchronized/is, + ); +}); + +test("optional visual references inherit design authority and proportional evidence gates", async () => { + const designSystem = await read(reference("design-system")); + const redesign = await read(reference("redesign")); + const systemMap = await read(reference("system-map")); + const imageFirst = await read(reference("image-first")); + const motion = await read(reference("motion")); + + assert.match(designSystem, /changed visual claim.*narrow nonvisual change.*Design impact: unchanged.*Visual evidence: not applicable/is); + assert.match(designSystem, /current design source of truth remains authoritative/is); + assert.match(designSystem, /DESIGN\.md.*already the established source.*another source owns.*scoped mapping\/receipt.*links every changed.*synchronized/is); + assert.match(designSystem, /absence of a formal design-system document is not a blocker.*never means.*no UI work/is); + assert.match(designSystem, /material unknowns.*minimum necessary questions.*batch independent unknowns/is); + assert.match(designSystem, /does not by itself select anti-slop.*SEO.*measured-quality.*universal visual matrix/is); + assert.match(designSystem, /SEO.*current crawlable public Web target.*distinct deployed public Web target/is); + assert.match(designSystem, /target-derived browser\/OS\/input and breakpoint matrix.*visual artifacts only for changed visual claims.*visible-state\/layout consequence/is); + assert.match(designSystem, /complete schema.*approved new or redesigned.*visual delta inside an existing system.*only the affected sections/is); + assert.match(designSystem, /project-native.*color.*token.*resolved value.*semantic role/is); + assert.match(designSystem, /project-native.*unit.*spacing.*scale/is); + assert.match(designSystem, /CSS.*Android.*Apple.*Qt|Qt.*Apple.*Android.*CSS/is); + assert.doesNotMatch(designSystem, /every color as `hex \+ CSS variable \+ semantic role`/iu); + assert.doesNotMatch(designSystem, /base unit \(4px\).*every margin\/padding\/gap is a multiple/iu); + assert.doesNotMatch(designSystem, /\*\*No design system = no UI work\.\*\*/u); + + assert.match(redesign, /only when a visual Web redesign is actually in scope.*narrow nonvisual change.*does not require.*redesign audit.*design artifact.*anti-slop.*visual capture/is); + assert.match(redesign, /material unknown.*minimum necessary questions.*batch independent material unknowns/is); + assert.match(redesign, /existing authoritative design source.*do not move ownership/is); + assert.match(redesign, /DESIGN\.md.*already established.*synchronized scoped mapping\/receipt/is); + assert.match(redesign, /current crawlable public Web target.*distinct deployed public Web target/is); + assert.match(redesign, /authenticated.*private.*native-only.*embedded-only.*SEO: N\/A.*concrete deployment reason/is); + assert.match(redesign, /absence of a formal design-system document alone is not structural failure/is); + assert.match(redesign, /anti-slop only for its declared.*scope.*target-derived browser\/OS\/input and breakpoint matrix/is); + assert.match(redesign, /visual language.*depart.*quantity.*complexity.*motion.*information.*density/is); + assert.match(redesign, /concrete evidence.*component variation.*transition.*content per viewport.*whitespace/is); + assert.doesNotMatch(redesign, /DESIGN_VARIANCE|MOTION_INTENSITY|VISUAL_DENSITY/u); + + assert.match(systemMap, /actual platform contract/is); + assert.match(systemMap, /does not replace.*authoritative design source.*narrow nonvisual change.*behavioral.*accessibility.*regression proof/is); + assert.match(systemMap, /SEO remains limited.*current crawlable public Web target.*distinct deployed public Web target/is); + assert.match(systemMap, /DESIGN\.md.*repository establishes it.*scoped mapping.*links back.*synchronized.*never a competing source/is); + assert.match(systemMap, /target-derived browser\/OS\/input and breakpoint matrix.*changed claims or risk/is); + assert.match(systemMap, /materially change.*minimum necessary questions.*batch independent unknowns/is); + + assert.match(imageFirst, /approved new\/redesigned direction.*materially changed visual claim/is); + assert.match(imageFirst, /Do not use this reference to expand a narrow nonvisual change into visual work/is); + assert.match(imageFirst, /current design source.*remain authoritative.*generated image.*scoped reference.*never permission to replace/is); + assert.match(imageFirst, /minimum necessary questions.*batch independent unknowns/is); + assert.match(imageFirst, /does not make anti-slop or SEO applicable.*current crawlable public Web target.*distinct deployed public Web target/is); + assert.match(imageFirst, /representative target-derived viewport/is); + assert.match(imageFirst, /DESIGN\.md.*already established.*synchronized scoped mapping\/receipt.*never create a competing source/is); + assert.doesNotMatch(imageFirst, /Phase \d+ of the skill/iu); + + assert.match(motion, /only when a motion or animated-interaction claim changes.*does not expand a narrow nonvisual change/is); + assert.match(motion, /existing motion primitives and authoritative design source remain authoritative.*DESIGN\.md.*already that source.*scoped mapping\/receipt.*synchronized/is); + assert.match(motion, /minimum necessary questions.*material unknowns.*batch independent unknowns/is); + assert.match(motion, /SEO.*current crawlable public Web target.*distinct deployed public Web target.*never because animation code runs in a browser/is); + assert.match(motion, /target-derived browser\/OS\/input and breakpoint matrix.*390.*768.*1280.*optional baseline.*never universal proof/is); + assert.match(motion, /1024px.*illustrative.*actual target-derived breakpoint.*never add or claim a breakpoint.*example/is); + assert.match(motion, /target-supported viewport.*resize.*orientation.*container transition/is); + assert.match(motion, /each supported input.*keyboard.*touch.*pointer.*only where the target contract includes it/is); + assert.match(motion, /visible-state or layout consequence.*VISUAL_QA\.md.*real interaction.*accessibility.*cleanup.*regression evidence/is); + assert.match(motion, /reduce.*animation.*quantity.*complexity.*clear static surface/is); + assert.doesNotMatch(motion, /DESIGN_VARIANCE|MOTION_INTENSITY|VISUAL_DENSITY/u); +}); + +test("shared UX contract covers input burden, undo, state, content, i18n, access, and evidence", async () => { + const ux = await read(reference("ux")); + + assert.match(ux, /constrained identifier.*platform.*toolkit.*provider.*portal.*picker.*search.*detection.*suggestion.*recent/is); + assert.match(ux, /no universal.*picker.*validated manual entry.*arbitrary.*expert/is); + assert.match(ux, /Undo.*domain command.*coalesc.*no-op.*meaningful label.*visible state/is); + assert.match(ux, /hover.*focus.*scroll.*noise.*excluded.*selection.*navigation.*resize.*meaningful mutation/is); + assert.match(ux, /Undo.*applicability.*explicit/is); + assert.match(ux, /application defaults.*user preferences.*system.*document.*model.*scene.*sensitive.*ephemeral/is); + assert.match(ux, /same view.*revisit.*restart.*process recreation.*supported upgrade.*device transfer.*account/is); + assert.match(ux, /migration.*explicit.*idempotent.*version.*package.*distribution channel/is); + assert.match(ux, /information architecture|\bIA\b/u); + assert.match( + ux, + /multiple advertised.*frontends|frontends.*capability reachability matrix/is, + ); + assert.match( + ux, + /reachable on each surface.*intention.*handed off.*return path.*context\/state.*not applicable/is, + ); + assert.match(ux, /consequence-scaled.*error.*preserve input.*specific correction.*duplicate submission/is); + assert.match(ux, /internationali[sz]ation.*discovery.*concatenat.*locale-aware.*language.*direction.*override/is); + assert.match(ux, /long.*unbroken.*bidirectional.*taller-script/is); + assert.match(ux, /names.*roles.*states.*actions.*focus order.*restoration.*traps.*status announcement/is); + assert.match(ux, /adaptive.*window.*orientation.*zoom.*text scaling.*content.*input/is); + assert.match(ux, /executable target journey.*resulting state.*function/is); + assert.match(ux, /representative users.*believable tasks.*usability/is); + assert.match(ux, /telemetry.*outcome.*live quality/is); + assert.match(ux, /static contract tests.*not.*downstream.*usable/is); +}); + +test("desktop, mobile, hybrid, and renderer references own distinct proof", async () => { + const desktop = await read(reference("desktop")); + const mobile = await read(reference("mobile")); + const hybrid = await read(reference("hybrid")); + const renderer = await read(reference("renderer")); + + assert.match(desktop, /OS.*version.*desktop environment.*session/is); + assert.match(desktop, /toolkit.*runtime.*package.*update channel/is); + assert.match(desktop, /system.*chrome.*default/is); + assert.match(desktop, /Windows.*GNOME.*KDE.*Apple/is); + assert.match(desktop, /Windows.*supported titlebar.*system caption buttons.*retained/is); + assert.match(desktop, /fully custom.*frame.*caption hit testing.*resize.*snap.*system menu/is); + assert.match(desktop, /input method|\bIME\b/u); + assert.match(desktop, /native accessibility tree.*packaged lifecycle/is); + + assert.match(mobile, /navigation.*back.*insets.*safe areas/is); + assert.match(mobile, /window.*orientation.*posture/is); + assert.match(mobile, /touch.*pointer.*keyboard.*stylus.*applicable/is); + assert.match(mobile, /permission.*picker.*process.*scene.*restoration/is); + assert.match(mobile, /locale.*RTL.*text scaling.*accessibility/is); + assert.match(mobile, /device.*API.*matrix.*emulator.*physical device.*risk/is); + + assert.match(hybrid, /client.*shell.*service.*owner/is); + assert.match(hybrid, /IPC.*bridge.*semantic.*failure/is); + assert.match(hybrid, /WebView.*bundled browser/is); + assert.match(hybrid, /picker.*window.*menu.*permission.*accessibility.*package/is); + assert.match(hybrid, /client proof.*cannot substitute.*shell proof/is); + assert.match(hybrid, /multiple.*clients.*shells.*capability reachability.*intentional handoff/is); + + assert.match(renderer, /DOM.*native control.*custom.*mixed/is); + assert.match(renderer, /semantic.*accessibility.*owner/is); + assert.match(renderer, /text.*input.*selection.*focus.*IME/is); + assert.match(renderer, /scal.*performance/is); + assert.match(renderer, /crawlability.*separate.*capability.*prove/is); + assert.match(renderer, /screenshot.*pixel similarity.*not.*promotion|not.*promot.*screenshot.*pixel similarity/is); +}); + +test("web delivery classes separate product, campaign, installed, renderer, and shell concerns", async () => { + const web = await read(reference("web")); + const product = web.match(/### Product\/application DOM Web[\s\S]*?(?=\n### )/u)?.[0] ?? ""; + const campaign = web.match(/### Marketing\/editorial Web[\s\S]*?(?=\n### )/u)?.[0] ?? ""; + const installed = web.match(/### PWA and browser extension[\s\S]*?(?=\n### )/u)?.[0] ?? ""; + const custom = web.match(/### Canvas\/custom-rendered Web[\s\S]*?(?=\n### )/u)?.[0] ?? ""; + const embedded = web.match(/### Embedded client Web[\s\S]*?(?=\n## )/u)?.[0] ?? ""; + + assert.match(web, /classify.*deployed surface.*before.*Web checklist/is); + + assert.match(product, /authenticated.*private.*internal/is); + assert.match(product, /existing product.*design system.*workflow/is); + assert.match( + product, + /SEO applies.*current Web target.*public and crawlable.*authentication.*privacy.*do not imply crawlability/is, + ); + + assert.match(campaign, /anti-slop\.md/u); + assert.match(campaign, /brand.*editorial.*composition/is); + + assert.match(installed, /service worker.*offline.*update/is); + assert.match(installed, /extension.*permission.*browser chrome.*content script/is); + + assert.match(custom, /references\/renderer\.md/u); + assert.match(custom, /semantics.*accessibility.*text.*input/is); + assert.match(custom, /crawlability.*separately prove/is); + assert.match(custom, /canvas screenshot.*insufficient/is); + + assert.match(embedded, /references\/(?:desktop|mobile)\.md.*references\/hybrid\.md/is); + assert.match(embedded, /shell proof.*ownership.*lifecycle.*permissions.*accessibility.*menus.*windowing.*packaging/is); + assert.match(embedded, /client proof.*cannot substitute.*shell proof/is); +}); + +test("web verification matrix is derived from supported targets rather than three universal viewports", async () => { + const web = await read(reference("web")); + + assert.match(web, /target-derived verification matrix/is); + assert.match(web, /supported browser.*engine.*OS.*input/is); + assert.match(web, /Browserslist.*package.*product support.*analytics/is); + assert.match(web, /actual.*breakpoint.*minimum.*maximum.*zoom.*text scaling/is); + assert.match(web, /390.*768.*1280.*baseline samples.*not.*universal proof/is); + assert.match(web, /minimum validation floor.*production build.*affected journey.*adjacent regression.*supported real browser/is); + assert.match(web, /design compliance.*visual capture.*Lighthouse.*React Doctor.*changed claim.*risk/is); + assert.match( + web, + /changed visual claim.*interaction claim.*visible-state or layout consequence.*VISUAL_QA\.md.*purely behavioral interaction.*behavioral proof/is, + ); + assert.doesNotMatch(web, /For any served web-app implementation or validation plan/iu); +}); + +test("web evidence instructions work in standalone and active-loop modes", async () => { + const web = await read(reference("web")); + const perfection = await read(reference("perfection")); + + assert.match(web, /Standalone frontend invocation/is); + assert.match(web, /run.*validation command directly.*write.*artifact/is); + assert.match(web, /YYYYMMDDTHHMMSSZ-/u); + assert.match(web, /lowercase ASCII.*single hyphens/is); + assert.match(web, /evidence-root\.mjs"? create frontend-check/u); + assert.match(web, /FRONTEND_SKILL_DIR.*actual.*skill.*directory/is); + assert.match(web, /\$\{FRONTEND_SKILL_DIR\}\/scripts\/evidence-root\.mjs/u); + assert.match(web, /\$\{FRONTEND_SKILL_DIR\}\/scripts\/ds-compliance\.mjs/u); + assert.match(web, /\$\{FRONTEND_SKILL_DIR\}\/scripts\/visual-diff\.mjs/u); + assert.match(web, /POSIX.*EVIDENCE_ROOT=.*evidence-root\.mjs/is); + assert.match(web, /PowerShell.*\$EVIDENCE_ROOT = node .*evidence-root\.mjs/is); + assert.match(web, /evidence-root\.mjs"? verify/u); + assert.doesNotMatch(web, /date -u|mkdir -p/u); + assert.match(web, /SUPERLOOPY_EVIDENCE: /u); + assert.match(web, /Active Superloopy loop/is); + assert.match(web, /superloopy loop guide --json/u); + assert.match(web, /superloopy loop prove --artifact .* -- node/u); + assert.match( + web, + /superloopy loop evidence --goal-id G001 --criterion-id C001 --status pass --artifact .* --notes .* --json/u, + ); + assert.doesNotMatch(web, /superloopy loop evidence --status pass/u); + assert.doesNotMatch(web, /frontend-check\/token-lint\.txt/u); + assert.match( + perfection, + /Create .*EVIDENCE_ROOT.*portable helper.*returned path.*run ID.*do not separately synthesize or set .*RUN_ID.*Reuse that exact evidence root/is, + ); + assert.doesNotMatch(perfection, /Set RUN_ID through/iu); + assert.match(perfection, /evidence-root\.mjs"? create frontend-quality/u); + assert.doesNotMatch(perfection, /frontend-check\/token-lint\.txt/u); +}); + +test("platform and composition evidence keeps a small floor and expands only with affected claims or risk", async () => { + const desktop = await read(reference("desktop")); + const mobile = await read(reference("mobile")); + const hybrid = await read(reference("hybrid")); + const renderer = await read(reference("renderer")); + const web = await read(reference("web")); + + assert.match(desktop, /minimum regression floor.*build.*launch.*affected core journey/is); + assert.match(desktop, /install.*first run.*permission.*restart.*update.*recovery.*only when.*affected.*claimed.*risk/is); + assert.match( + mobile, + /minimum regression floor.*builds and launches the real target.*affected core journey.*closest adjacent regression.*supported emulator, simulator, or device/is, + ); + assert.match(mobile, /install\/first-run.*permission denial\/recovery.*process recreation.*only when affected, claimed, release-critical, or selected by risk/is); + assert.match(hybrid, /minimum regression floor.*builds.*launches.*client.*shell.*affected journey/is); + assert.match(hybrid, /picker.*window.*menu.*permission.*accessibility.*package.*update.*shutdown.*when.*affected.*claimed.*risk/is); + assert.match(renderer, /target-applicable scaling matrix.*DPR.*zoom.*orientation.*safe areas/is); + assert.match(renderer, /orientation.*safe areas.*only.*target.*supports|target.*supports.*orientation.*safe areas/is); + assert.match(web, /performance.*when affected.*claimed.*risk/is); +}); + +test("frontend shell examples do not turn documentation placeholders into redirects", async () => { + for (const contract of [await read(reference("web")), await read(reference("perfection"))]) { + const shellBlocks = [...contract.matchAll(/```(?:sh|shell)?\n([\s\S]*?)```/gu)].map((match) => match[1]).join("\n"); + assert.doesNotMatch(shellBlocks, /<(?:slug|built-files|built CSS\/TSX files…|url|selected-categories)>/u); + } +}); + +test("web perfection applies SEO to the crawlable Web target, never merely to an embedded shell", async () => { + const web = await read(reference("web")); + const perfection = await read(reference("perfection")); + + for (const contract of [web, perfection]) { + assert.match(contract, /SEO.*current.*target.*crawlable public Web|audited target.*crawlable public Web/is); + assert.match(contract, /native.*embedded.*distinct.*public Web target/is); + assert.match(contract, /HTML\/CSS.*WebView.*canvas.*embedded browser engine.*(?:does not|never).*SEO/is); + } + + assert.match(perfection, /Lighthouse.*categor(?:y|ies).*deployed surface/is); + assert.match( + perfection, + /token-lint layer accepts .*DESIGN\.md.*only when.*established design source.*scoped mapping\/receipt.*synchronized.*another authoritative source.*repository-native checks.*instead of creating .*DESIGN\.md/is, + ); + assert.match(perfection, /narrow nonvisual change.*does not select either layer.*existing gate.*changed claim.*regression signal.*risk/is); + assert.match( + perfection, + /SEO: N\/A.*concrete deployment reason.*authentication.*private access.*native-only delivery.*embedded client.*no distinct public Web target/is, + ); + assert.match(perfection, /performance.*accessibility.*best practices.*selected.*changed claim.*risk/is); + assert.match(perfection, /exact Lighthouse version.*browser version.*OS.*profile/is); + assert.match(perfection, /LIGHTHOUSE_VERSION.*exact.*semver/is); + assert.match(perfection, /lighthouse@\$\{LIGHTHOUSE_VERSION\}/u); + assert.match(perfection, /target-derived browser\/OS\/input and breakpoint matrix/is); + assert.match(perfection, /13\.4\.0.*Node.*22\.19.*Node 20.*12\.8\.2/is); + assert.match( + perfection, + /lighthouse-mobile-run-1\.json.*increment the run index.*Never overwrite raw runs/is, + ); + assert.match( + perfection, + /retain every indexed raw JSON.*lighthouse-summary\.json.*summary must list its input filenames.*median is reproducible/is, + ); + assert.match(perfection, /project.*budget.*regression.*baseline/is); + assert.match(perfection, /90-99.*not.*automatic failure/is); + assert.match(perfection, /below 90.*block.*applicable.*category.*documented.*limitation/is); + assert.match(perfection, /React Doctor.*triage.*affected.*owned.*finding/is); + assert.match(perfection, /React Doctor.*0\.8\.1.*Node.*\^20\.19\.0.*>=22\.13\.0/is); + assert.match( + perfection, + /react-doctor@\$\{REACT_DOCTOR_VERSION\}.*--json.*--no-telemetry.*--no-supply-chain.*react-doctor\.json/is, + ); + assert.match( + perfection, + /require informed opt-in before omitting either privacy flag.*supply-chain analysis only when.*claim is selected and approved/is, + ); + assert.doesNotMatch(perfection, /90-99 as work remaining/iu); + assert.doesNotMatch(perfection, /applicable static render-performance findings as blockers/iu); + assert.match(perfection, /3-, 4-, 6-, and 8-digit hex/is); +}); + +test("anti-slop is a conditional visual-direction check, not a product-UI mandate", async () => { + const antiSlop = await read(reference("anti-slop")); + + assert.match(antiSlop, /marketing.*editorial.*new visual direction/is); + assert.match(antiSlop, /authenticated.*private.*internal.*product/is); + assert.match(antiSlop, /existing design system.*product conventions/is); + assert.match(antiSlop, /not.*require.*imagery.*layout-family.*font replacement/is); +}); + +test("new UX references and contract tests are inventoried as original Superloopy work", async () => { + const designAudit = await read("docs/superloopy-design-audit.md"); + const fileAudit = await read("docs/superloopy-file-audit.md"); + const golden = await read("docs/superloopy-loop-golden-set.md"); + const paths = [ + reference("ux"), + reference("desktop"), + reference("mobile"), + reference("hybrid"), + reference("renderer"), + "test/frontend-ux-contract.test.js", + "test/frontend-ux-semantics.test.js", + ]; + + for (const path of paths) { + assert.ok(designAudit.includes(`\`${path}\``), `${path} missing from design audit`); + assert.equal(fileAudit.split("\n").filter((line) => line.startsWith(`| \`${path}\` |`)).length, 1); + assert.equal(golden.split("\n").filter((line) => line.startsWith(`| \`${path}\` |`)).length, 1); + } + assert.match(designAudit, /shared UX.*platform.*composition.*original (?:Superloopy prose|prose authored for Superloopy).*no external runtime dependencies/is); + assert.match(fileAudit, /references\/ux\.md.*Superloopy-native.*original prose/is); + assert.match(golden, /test\/frontend-ux-contract\.test\.js.*node --test/is); +}); diff --git a/test/frontend-ux-semantics.test.js b/test/frontend-ux-semantics.test.js new file mode 100644 index 0000000..6b9e65e --- /dev/null +++ b/test/frontend-ux-semantics.test.js @@ -0,0 +1,207 @@ +import assert from "node:assert/strict"; +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const root = "skills/superloopy-frontend"; +const reference = (name) => `${root}/references/${name}.md`; + +async function read(path) { + assert.equal(existsSync(path), true, `missing ${path}`); + return (await readFile(path, "utf8")).replace(/\r\n?/gu, "\n"); +} + +function joinedPattern(flags, ...parts) { + return new RegExp(parts.join(""), flags); +} + +function assertContracts(content, contracts) { + for (const [meaning, pattern] of contracts) { + assert.match(content, pattern, `missing UX contract: ${meaning}`); + } +} + +function assertNoUniversalReversals(content, reversals) { + for (const [meaning, pattern] of reversals) { + assert.doesNotMatch(content, pattern, `unscoped UX reversal: ${meaning}`); + } +} + +const resourceIdentityContracts = [ + ["lifecycle verbs map to distinct transitions", /lifecycle verbs?.*distinct semantic transitions?/is], + [ + "canonical, working, saved, and active identity stays attributable", + joinedPattern( + "is", + String.raw`canonical resource.*working copy.*last[- ]saved.*active resource.*`, + String.raw`dirty.*pending.*visible.*attributable`, + ), + ], + ["copies disclose identity changes", /copy.*identity change.*disclos/is], + [ + "portable contracts may use stable logical or owned-root-relative locators", + /product contract.*portability.*stable logical identifier.*owned-root-relative locator/is, + ], + [ + "owner-required absolute locators state their boundary and recovery", + joinedPattern( + "is", + String.raw`absolute locator remains valid.*actual platform.*provider.*integration owner.*`, + String.raw`requires.*explicit reason.*resolution boundary.*relink.*recovery`, + ), + ], + [ + "copy-based workflows remain valid when their lifecycle is truthful", + /copy-based workflow remains valid.*identity.*lifecycle effects.*truthful/is, + ], + ["each intent takes its shortest truthful transition", /each user intent.*shortest.*truthful.*transition/is], + [ + "safe owner updates avoid mandatory copy round trips", + joinedPattern( + "is", + String.raw`current owner.*safely.*update.*reload.*original.*(?:do not|does not).*`, + String.raw`require.*import.*modify.*apply.*round trip.*copy.*edit.*apply.*variant`, + ), + ], + [ + "necessary staged copies preserve identity and expose reconciliation", + joinedPattern( + "is", + String.raw`staged copy.*edit.*apply.*real owner.*boundary.*reason.*original identity.*`, + String.raw`preserv.*reconcil.*apply.*save-back.*explicit.*commit path`, + ), + ], +]; + +const resourceIdentityUniversalReversals = [ + [ + "absolute locators are forbidden without a scoped exception", + joinedPattern( + "im", + String.raw`(?:^|[.!?]\s+)(?:`, + String.raw`absolute locators?\b[^.\n]{0,40}(?:are|must be)\s+`, + String.raw`(?:(?:always|universally)\s+)?(?:banned|forbidden)`, + String.raw`|(?:(?:always|universally)\s+)?(?:ban|forbid)\b`, + String.raw`[^.\n]{0,60}absolute locators?\b`, + String.raw`|never use (?:any\s+)?absolute locators?\b`, + String.raw`|absolute locators?\b\s+(?:are|must be)\s+never\s+(?:allowed|valid|used)`, + String.raw`)(?!\s+(?:only|for|when|within|on|in|by|where|if|unless|except)\b)`, + ), + ], + [ + "copy-based workflows are forbidden without a scoped exception", + joinedPattern( + "im", + String.raw`(?:^|[.!?]\s+)(?:`, + String.raw`copy-based workflows?\b[^.\n]{0,40}(?:are|must be)\s+`, + String.raw`(?:(?:always|universally)\s+)?(?:banned|forbidden)`, + String.raw`|(?:(?:always|universally)\s+)?(?:ban|forbid)\b`, + String.raw`[^.\n]{0,60}copy-based workflows?\b`, + String.raw`|never use (?:any\s+)?copy-based workflows?\b`, + String.raw`|copy-based workflows?\b\s+(?:are|must be)\s+never\s+(?:allowed|valid|used)`, + String.raw`)(?!\s+(?:only|for|when|within|on|in|by|where|if|unless|except)\b)`, + ), + ], + [ + "safe direct owner updates still mandate import-copy-apply round trips", + joinedPattern( + "im", + String.raw`(?:^|[.!?]\s+)when\s+(?:(?:the\s+)?current owner`, + String.raw`[^.\n]{0,100}(?:can|is able to)\s+safely[^.\n]{0,60}`, + String.raw`(?:update|reload)[^.\n]{0,60}(?:the\s+)?original`, + String.raw`|(?:a\s+)?safe direct owner update[^.\n]{0,40}(?:is\s+)?\bpossible\b)`, + String.raw`[^.\n]{0,100}(?:(?:(?:always|universally|must(?:\s+always)?)\s+`, + String.raw`(?:require|use|perform)|(?:still\s+)?requires)[^.\n]{0,120}`, + String.raw`(?:import[^.\n]{0,60}(?:copy|modify|edit)[^.\n]{0,60}apply`, + String.raw`|(?:import|copy|modify|edit)[^.\n]{0,80}round trip)`, + String.raw`|[^.\n]{0,120}(?:import[^.\n]{0,60}(?:copy|modify|edit)`, + String.raw`[^.\n]{0,60}apply|(?:import|copy|modify|edit)[^.\n]{0,80}round trip)`, + String.raw`[^.\n]{0,40}(?:is|remains)\s+(?:always\s+)?(?:required|mandatory))`, + ), + ], +]; + +const resetProvenanceContracts = [ + [ + "revert, inherited defaults, and factory defaults remain distinct", + /Revert.*current(?: or |\/)inherited defaults.*versioned factory defaults.*distinct.*baseline.*scope/is, + ], + ["reset values come from their real owner", /reset values.*read.*real owner.*not.*duplicated UI literals/is], + [ + "reset proof covers applicable provenance and persistence cases", + /proof.*non-default.*inherited.*changed[- ]default.*dirty.*Undo.*save.*apply.*restart.*relocation.*applicable/is, + ], +]; + +const informationArchitectureContracts = [ + [ + "each task-bearing region has a distinct user-recognizable purpose", + joinedPattern( + "is", + String.raw`within the affected surface or journey.*every task-bearing region.*affordance.*`, + String.raw`distinct user-recognizable.*job.*outcome.*decision.*information gain.*parent.*siblings`, + ), + ], + ["redundant or unsupported UI is resolved truthfully", /redundant.*unsupported.*merge.*relabel.*truthful output.*omit/is], + ["disclosure priority is proportional", /task criticality.*frequency.*consequence.*urgency.*actionability/is], + [ + "essential state and recovery remain in context", + /essential state.*blockers.*errors.*recovery.*next action.*in context/is, + ], + [ + "dense expert work may require simultaneous visibility", + /simultaneous.*dense.*comparison.*monitoring.*expert work.*requires/is, + ], + ["Advanced and More are not mechanically banned", /Advanced.*More.*not.*mechanical.*ban/is], +]; + +const informationArchitectureUniversalReversals = [ + [ + "dense comparison, monitoring, or expert content must always be hidden", + joinedPattern( + "im", + String.raw`(?:^|[.!?]\s+)(?:(?:dense|comparison|monitoring|expert)`, + String.raw`[^.\n]{0,60}(?:content|work)\b[^.\n]{0,40}`, + String.raw`(?:(?:must|should)\s+(?:(?:always|universally)\s+)?`, + String.raw`|(?:is|are)\s+(?:always|universally)\s+)(?:be\s+)?`, + String.raw`(?:collaps(?:e|ed)\b|hid(?:e|den)\b|mov(?:e|ed)\s+behind disclosure\b)`, + String.raw`|(?:(?:always|universally)\s+)?(?:collapse|hide)\b[^.\n]{0,80}`, + String.raw`(?:dense|comparison|monitoring|expert)[^.\n]{0,40}(?:content|work)\b`, + String.raw`|(?:(?:always|universally)\s+)?move\b[^.\n]{0,80}`, + String.raw`(?:dense|comparison|monitoring|expert)[^.\n]{0,40}(?:content|work)\b`, + String.raw`[^.\n]{0,40}behind disclosure\b)`, + String.raw`(?!\s+(?:only|for|when|within|on|in|by|where|if|unless|except)\b)`, + ), + ], + [ + "Advanced or More labels are universally forbidden", + joinedPattern( + "im", + String.raw`(?:^|[.!?]\s+)(?:(?:Advanced(?:\s*(?:and|\/)\s*More)?|More)`, + String.raw`[^.\n]{0,30}labels?\b[^.\n]{0,30}`, + String.raw`(?:(?:are|must be|should be)\s+(?:(?:always|universally)\s+)?`, + String.raw`(?:banned|forbidden|avoided)|(?:must|should)\s+never\s+be\s+used)`, + String.raw`|(?:(?:always|universally)\s+)?(?:ban|forbid)\b[^.\n]{0,60}`, + String.raw`(?:Advanced|More)[^.\n]{0,30}labels?\b`, + String.raw`|never use\b[^.\n]{0,40}(?:Advanced|More)[^.\n]{0,30}labels?\b)`, + String.raw`(?!\s+(?:only|for|when|within|on|in|by|where|if|unless|except)\b)`, + ), + ], +]; + +test("shared UX keeps resource identity and lifecycle transitions attributable", async () => { + const ux = await read(reference("ux")); + assertContracts(ux, resourceIdentityContracts); + assertNoUniversalReversals(ux, resourceIdentityUniversalReversals); +}); + +test("shared UX distinguishes revert and reset provenance", async () => { + const ux = await read(reference("ux")); + assertContracts(ux, resetProvenanceContracts); +}); + +test("shared UX gives each task-bearing region purpose and proportional disclosure", async () => { + const ux = await read(reference("ux")); + assertContracts(ux, informationArchitectureContracts); + assertNoUniversalReversals(ux, informationArchitectureUniversalReversals); +}); diff --git a/test/golden-matrix-gate.test.js b/test/golden-matrix-gate.test.js index 36474a2..5b92ae9 100644 --- a/test/golden-matrix-gate.test.js +++ b/test/golden-matrix-gate.test.js @@ -4,8 +4,82 @@ import { join } from "node:path"; import test from "node:test"; import { createLoop } from "../src/loop.js"; +import { validateMatrixQualityGate } from "../src/matrix-gate.js"; +import { qualityGateSurfaceFamilies, qualityGateSurfaceFamily } from "../src/review-gate.js"; import { cloneJson, matrixStyleQualityGate, runCli, tempRepo, writeEvidence, writeGenuineAuditVerdict, writeMatrixGateArtifacts } from "./golden-helpers.js"; +function matrixGateForSurface(surface, kinds, scope = {}) { + const gate = matrixStyleQualityGate({ cliRun: ".superloopy/evidence/matrix-cli-run.txt", redTeam: ".superloopy/evidence/matrix-risk-probe.txt", auditVerdict: ".superloopy/evidence/matrix-audit-verdict.json" }); + gate.executorQa.artifactRefs = kinds.map((kind, index) => ({ id: `proof-${index}`, kind, path: `.superloopy/evidence/${kind}-${index}.txt`, description: `${kind} proof for ${surface}.` })); + gate.executorQa.surfaceEvidence[0].surface = surface; + Object.assign(gate.executorQa.surfaceEvidence[0], scope); + gate.executorQa.surfaceEvidence[0].artifactRefs = gate.executorQa.artifactRefs.map((artifact) => artifact.id); + gate.executorQa.adversarialCases[0].artifactRefs = [gate.executorQa.artifactRefs[0].id]; + return gate; +} + +const identityArtifactPath = (value) => value; +const target = (id, platform, environment) => ({ id, platform, environment }); + +test("quality-gate surface classification honors composed and named application targets", () => { + const surfacesByFamily = { + cli: ["shell"], + hybrid: ["hybrid shell", "embedded HTML desktop", "mobile WebView", "Electron client", "Tauri shell", "MAUI Hybrid", "WKWebView", "WebView2", "Qt WebEngine", "CEF"], + renderer: ["custom-rendered Web", "canvas UI", "Flutter", "Compose Desktop", "Qt Quick/QML"], + browser: ["PWA", "gui", "browser extension", "Chrome extension", "Firefox extension", "Safari extension", "WebExtension", "mobile Web", "desktop browser", "mobile PWA", "Chrome extension on desktop"], + native: ["Android", "iOS", "iPadOS", "macOS", "Windows", "SwiftUI", "React Native", "Tkinter GUI", "wxWidgets GUI", "JavaFX GUI", "Swing GUI", "Avalonia GUI", "WinForms GUI", "desktop GUI", "native shell"], + tui: ["TUI"] + }; + for (const [family, surfaces] of Object.entries(surfacesByFamily)) { + for (const surface of surfaces) assert.equal(qualityGateSurfaceFamily(surface, "surface"), family, surface); + } + const composed = [ + ["embedded mobile WebView", ["hybrid", "native", "browser"]], ["Web + iOS + Android", ["browser"]], + ["Web + native iOS app + native Android app", ["native", "browser"]], ["browser app and Android app", ["native", "browser"]], + ["mobile Web on Android", ["browser"]], ["Safari PWA on iOS", ["browser"]], ["Chrome extension on Windows", ["browser"]], + ["authenticated Web on macOS", ["browser"]], ["Safari on iOS", ["browser"]], ["Chrome on Android", ["browser"]], + ["Firefox on Windows", ["browser"]], ["Chromium on Linux", ["browser"]], ["Microsoft Edge on Windows", ["browser"]], + ["Edge browser on Windows", ["browser"]], ["edge device UI", ["native"]], ["edge computing desktop GUI", ["native"]], + ["iPhone app", ["native"]], ["iPad app", ["native"]], ["Mac app", ["native"]], ["React Native Web", ["browser"]], + ["Capacitor iOS", ["hybrid", "native", "browser"]], ["Cordova Android", ["hybrid", "native", "browser"]], + ["Blazor WebAssembly", ["browser"]], ["Qt WebAssembly", ["renderer", "browser"]], + ["QML on WebAssembly", ["renderer", "browser"]], ["WebGL browser", ["renderer", "browser"]], + ["SwiftUI settings scene on iOS", ["native"]], ["SwiftUI WindowGroup scene", ["native"]], + ["custom scene graph desktop", ["renderer", "native"]], ["embedded Linux Qt", ["native"]], + ["embedded native touchscreen", ["native"]], ["embedded system GUI", ["native"]] + ]; + for (const [surface, families] of composed) assert.deepEqual(qualityGateSurfaceFamilies(surface, "surface"), families, surface); + assert.throws(() => qualityGateSurfaceFamilies("UnknownToolkit GUI", "surface"), /supported QA surface|ambiguous.*GUI/i); +}); + +test("quality-gate classifier prefers deployed facts over host OS and compatible framework brands", () => { + const cases = [ + ["CLI on Windows 11", ["cli"]], ["shell on macOS", ["cli"]], ["tmux on Linux", ["cli"]], + ["HTTP API on Windows", ["http"]], ["data package on macOS", ["data"]], ["algorithm on Android", ["data"]], + ["native Safari browser on iOS", ["browser"]], ["native Chrome browser on Android", ["browser"]], + ["React Native for Web", ["browser"]], ["react-native-web browser delivery", ["browser"]], ["React Native running in browser", ["browser"]], + ["Avalonia WebAssembly", ["renderer", "browser"]], ["Slint browser delivery", ["renderer", "browser"]], + ["Kivy WebAssembly", ["renderer", "browser"]], ["JUCE browser delivery", ["renderer", "browser"]], + ["Capacitor PWA", ["browser"]], ["Cordova browser platform", ["browser"]], ["MAUI Blazor WebAssembly", ["browser"]], + ["custom chrome desktop", ["native"]], ["window chrome desktop", ["native"]], ["native chrome desktop", ["native"]], + ["Qt Quick/QML native app", ["renderer", "native"]], ["QML WASM browser", ["renderer", "browser"]], + ["Three.js Web UI", ["renderer", "browser"]], ["PixiJS browser UI", ["renderer", "browser"]], + ["Babylon.js Web UI", ["renderer", "browser"]], ["WebGPU browser UI", ["renderer", "browser"]] + ]; + for (const [surface, families] of cases) assert.deepEqual(qualityGateSurfaceFamilies(surface, "surface"), families, surface); + assert.deepEqual(qualityGateSurfaceFamilies("TUI on Windows", "surface"), ["tui"]); + assert.deepEqual(qualityGateSurfaceFamilies("TUI plus browser dashboard", "surface"), ["tui", "browser"]); + assert.deepEqual(qualityGateSurfaceFamilies("TUI inside native Qt Quick app", "surface"), ["tui", "renderer", "native"]); + assert.deepEqual(qualityGateSurfaceFamilies("CLI inside native Qt Quick app", "surface"), ["cli", "renderer", "native"]); + assert.deepEqual(qualityGateSurfaceFamilies("CLI inside Electron app", "surface"), ["cli", "hybrid", "native", "browser"]); + assert.throws(() => qualityGateSurfaceFamilies("TUI plus UnknownToolkit GUI", "surface"), /supported QA surface|ambiguous.*GUI/i); + assert.deepEqual(qualityGateSurfaceFamilies("email compose desktop", "surface"), ["native"]); + assert.deepEqual(qualityGateSurfaceFamilies("message compose screen on iOS", "surface"), ["native"]); + assert.throws(() => qualityGateSurfaceFamilies("UnknownToolkit GUI on Windows", "surface"), /supported QA surface|ambiguous.*GUI/i); + assert.throws(() => qualityGateSurfaceFamilies("email compose", "surface"), /supported QA surface/i); + assert.throws(() => qualityGateSurfaceFamilies("message compose", "surface"), /supported QA surface/i); +}); + test("golden: @goal delimiters split executable stories and keep literals safe", async () => { const repo = await tempRepo(); const result = runCli([ @@ -149,3 +223,323 @@ test("golden: matrix quality gate rejects not-applicable adversarial cases", asy assert.equal(result.status, 1); assert.match(result.stderr, /adversarialCases|not_applicable/); }); + +test("matrix quality gate accepts literal mobile and tablet surfaces with native interaction and visual proof", () => { + for (const surface of ["mobile", "tablet"]) { + const gate = matrixGateForSurface(surface, ["app-automation-transcript", "screenshot", "accessibility-tree", "device-report"]); + assert.doesNotThrow(() => validateMatrixQualityGate(gate, identityArtifactPath), surface); + } +}); + +test("matrix quality gate rejects screenshot-only native proof", () => { + const gate = matrixGateForSurface("native mobile app", ["screenshot"]); + + assert.throws( + () => validateMatrixQualityGate(gate, identityArtifactPath), + /native.*interaction|interaction.*native/i + ); +}); + +test("matrix quality gate preserves legacy full-surface proof when owner and claims are absent", () => { + const cases = [ + ["browser", ["browser-automation", "screenshot", "accessibility-tree"]], + ["native", ["app-automation-transcript", "screenshot", "accessibility-tree", "device-report", "package-lifecycle-report"]], + ["hybrid", ["browser-automation", "app-automation-transcript", "screenshot", "accessibility-tree", "device-report", "package-lifecycle-report"]], + ["renderer", ["app-automation-transcript", "screenshot", "accessibility-tree", "renderer-trace"]] + ]; + + for (const [surface, kinds] of cases) { + const gate = matrixGateForSurface(surface, kinds); + assert.doesNotThrow(() => validateMatrixQualityGate(gate, identityArtifactPath), surface); + } +}); + +test("matrix quality gate scopes composite proof to one affected owner and explicit claims", () => { + const shellMenu = matrixGateForSurface( + "Tauri desktop", + ["app-automation-transcript", "device-report"], + { target: target("windows-menu", "windows", "Windows 11 desktop"), owner: "native", claims: ["interaction", "target"], scopeReason: "Only the native menu owner changed." } + ); + const embeddedClient = matrixGateForSurface( + "Tauri desktop", + ["client-automation-transcript"], + { target: target("windows-client", "windows", "Embedded client on Windows 11 desktop"), owner: "browser", claims: ["interaction"], scopeReason: "Only the embedded client interaction changed." } + ); + const flutterColor = matrixGateForSurface( + "Flutter Android", + ["screenshot"], + { target: target("android-phone", "android", "Android 15 phone"), owner: "renderer", claims: ["visual"], scopeReason: "Only renderer-owned color output changed." } + ); + + assert.doesNotThrow(() => validateMatrixQualityGate(shellMenu, identityArtifactPath)); + assert.doesNotThrow(() => validateMatrixQualityGate(embeddedClient, identityArtifactPath)); + assert.doesNotThrow(() => validateMatrixQualityGate(flutterColor, identityArtifactPath)); +}); + +test("matrix quality gate requires a structured portable target without counting OS names in environment", () => { + const missingTarget = matrixGateForSurface( + "Tauri desktop", + ["app-automation-transcript", "device-report"], + { owner: "native", claims: ["interaction", "target"], scopeReason: "Only the native menu owner changed." } + ); + const hostNamesInEnvironment = matrixGateForSurface( + "native iOS app", + ["app-automation-transcript", "device-report"], + { + owner: "native", + claims: ["interaction", "target"], + scopeReason: "The iOS deployment is exercised through cross-host tooling.", + target: target("ios-simulator", "ios", "iOS simulator hosted on macOS and observed from a Windows runner") + } + ); + const stringTarget = matrixGateForSurface( + "native iOS app", + ["app-automation-transcript", "device-report"], + { target: "iOS simulator", owner: "native", claims: ["interaction", "target"], scopeReason: "Old free-text schema." } + ); + + assert.throws(() => validateMatrixQualityGate(missingTarget, identityArtifactPath), /target/i); + assert.throws(() => validateMatrixQualityGate(stringTarget, identityArtifactPath), /target.*object|object.*target/i); + const parsed = validateMatrixQualityGate(hostNamesInEnvironment, identityArtifactPath); + assert.deepEqual(parsed.executorQa.surfaceEvidence[0].target, target("ios-simulator", "ios", "iOS simulator hosted on macOS and observed from a Windows runner")); + + const genericIds = [ + matrixGateForSurface("browser", ["browser-automation"], { + target: target("web", "web", "Chrome 126 on Ubuntu 24.04"), owner: "browser", claims: ["interaction"], scopeReason: "One concrete Web run." + }), + matrixGateForSurface("browser", ["browser-automation"], { target: target("browser", "browser", "Firefox 128 on Windows 11"), owner: "browser", claims: ["interaction"], scopeReason: "One concrete browser run." }), + matrixGateForSurface("native", ["app-automation-transcript", "device-report"], { + target: target("desktop", "desktop", "Windows 11 workstation"), owner: "native", claims: ["interaction"], scopeReason: "One concrete desktop run." + }), + matrixGateForSurface("native", ["app-automation-transcript", "device-report"], { + target: target("mobile", "mobile", "iPhone 15 running iOS 18"), owner: "native", claims: ["interaction"], scopeReason: "One concrete mobile run." + }), matrixGateForSurface("native", ["app-automation-transcript", "device-report"], { + target: target("android-emulator", "android", "Android emulator on Linux"), owner: "native", claims: ["interaction"], scopeReason: "One concrete emulator run." + }) + ]; + for (const gate of genericIds) assert.doesNotThrow(() => validateMatrixQualityGate(gate, identityArtifactPath)); +}); + +test("matrix quality gate rejects nonportable or vague scoped target identifiers", () => { + const invalidTargets = [ + target("Windows_Menu", "windows", "Windows desktop"), + target("windows/menu", "windows", "Windows desktop"), + target("all-windows", "windows", "Windows desktop"), + target("supported-targets", "windows", "Windows desktop"), + target("alltargets", "windows", "Windows desktop"), target("supporteddevices", "windows", "Windows desktop"), target("crossplatforms", "windows", "Windows desktop"), + target("web", "web", "all supported browsers"), target("mobile", "mobile", "any devices"), target("desktop", "desktop", "cross-platform"), + target("web", "web", "multiple browsers"), target("mobile", "mobile", "multi-device targets"), target("desktop", "desktop", "universal targets"), + target("windows-menu", "Windows", "Windows desktop"), + target("windows-menu", "windows-11", "Windows desktop"), + target("windows-menu", "universal", "Windows desktop"), + target("windows-menu", "windows", "") + ]; + + for (const invalidTarget of invalidTargets) { + const gate = matrixGateForSurface( + "native Windows app", + ["app-automation-transcript", "device-report"], + { target: invalidTarget, owner: "native", claims: ["interaction", "target"], scopeReason: "Target schema probe." } + ); + assert.throws(() => validateMatrixQualityGate(gate, identityArtifactPath), /target|portable|platform|environment|vague/i, JSON.stringify(invalidTarget)); + } +}); + +test("matrix quality gate keeps scoped rows and resolved artifacts one-to-one", () => { + const duplicateScope = matrixGateForSurface("browser", ["browser-automation", "browser-automation"]); + Object.assign(duplicateScope.executorQa.surfaceEvidence[0], { + target: target("chrome-linux", "web", "Chrome on Linux"), + owner: "browser", + claims: ["interaction"], + scopeReason: "Primary interaction proof.", + artifactRefs: ["proof-0"] + }); + duplicateScope.executorQa.surfaceEvidence.push({ + ...structuredClone(duplicateScope.executorQa.surfaceEvidence[0]), + id: "surface-second", + artifactRefs: ["proof-1"] + }); + duplicateScope.executorQa.contractCoverage[0].surfaceEvidenceRefs.push("surface-second"); + + const sharedArtifact = structuredClone(duplicateScope); + sharedArtifact.executorQa.surfaceEvidence[1].target = target("firefox-linux", "web", "Firefox on Linux"); + sharedArtifact.executorQa.surfaceEvidence[1].artifactRefs = ["proof-0"]; + + const duplicateResolvedDeclaration = matrixGateForSurface("browser", ["browser-automation", "browser-automation"]); + duplicateResolvedDeclaration.executorQa.artifactRefs[0].path = ".superloopy/evidence/primary-proof.txt"; + duplicateResolvedDeclaration.executorQa.artifactRefs[1].path = ".superloopy/evidence/alias-proof.txt"; + const resolveAlias = (value) => value.endsWith("primary-proof.txt") || value.endsWith("alias-proof.txt") + ? "/resolved/shared-proof.txt" + : value; + + assert.throws(() => validateMatrixQualityGate(duplicateScope, identityArtifactPath), /duplicate.*target.*owner|target.*owner.*duplicate/i); + assert.throws(() => validateMatrixQualityGate(sharedArtifact, identityArtifactPath), /artifact.*reused|reused.*artifact|distinct.*target/i); + assert.throws(() => validateMatrixQualityGate(duplicateResolvedDeclaration, resolveAlias), /duplicate.*resolved.*path|resolved.*path.*duplicate/i); +}); + +test("matrix quality gate rejects incomplete or incoherent claim-shaped scope", () => { + const bridgeWithoutShell = matrixGateForSurface( + "Tauri desktop", + ["client-automation-transcript"], + { target: target("windows-bridge", "windows", "Windows 11 desktop"), owner: "hybrid", claims: ["interaction"], scopeReason: "The bridge journey changed." } + ); + const visualWithoutImage = matrixGateForSurface( + "Flutter Android", + ["app-automation-transcript"], + { target: target("android-phone", "android", "Android 15 phone"), owner: "renderer", claims: ["visual"], scopeReason: "Renderer-owned visual output changed." } + ); + const impossibleOwner = matrixGateForSurface( + "native desktop", + ["browser-automation"], + { target: target("windows-client", "windows", "Windows 11 desktop"), owner: "browser", claims: ["interaction"], scopeReason: "Incorrectly attributes a native-only surface to a browser owner." } + ); + const missingOwner = matrixGateForSurface( + "native desktop", + ["app-automation-transcript"], + { claims: ["interaction"] } + ); + const hybridVisualWithoutBothSides = matrixGateForSurface( + "Tauri desktop", + ["screenshot", "device-report"], + { target: target("windows-bridge", "windows", "Windows 11 desktop"), owner: "hybrid", claims: ["visual"], scopeReason: "Bridge-owned visible state changed." } + ); + + assert.throws(() => validateMatrixQualityGate(bridgeWithoutShell, identityArtifactPath), /shell interaction/i); + assert.throws(() => validateMatrixQualityGate(visualWithoutImage, identityArtifactPath), /visual.*screenshot|screenshot.*visual/i); + assert.throws(() => validateMatrixQualityGate(impossibleOwner, identityArtifactPath), /owner.*browser.*not.*surface|browser.*owner.*surface/i); + assert.throws(() => validateMatrixQualityGate(missingOwner, identityArtifactPath), /owner.*claims|claims.*owner/i); + assert.throws(() => validateMatrixQualityGate(hybridVisualWithoutBothSides, identityArtifactPath), /client interaction.*shell interaction|shell interaction.*client interaction/i); +}); + +test("matrix quality gate accepts browser automation as custom-renderer interaction proof", () => { + const gate = matrixGateForSurface("custom-rendered Web", ["browser-automation", "screenshot", "accessibility-tree", "renderer-trace"]); + + assert.doesNotThrow(() => validateMatrixQualityGate(gate, identityArtifactPath)); +}); + +test("matrix quality gate keeps browser targets Web-only while embedded WebView remains composite", () => { + for (const surface of ["mobile Web", "desktop browser", "mobile PWA", "Chrome extension on desktop"]) { + const gate = matrixGateForSurface(surface, ["browser-automation"]); + assert.doesNotThrow(() => validateMatrixQualityGate(gate, identityArtifactPath), surface); + } + const embedded = matrixGateForSurface( + "embedded mobile WebView", + ["client-automation-transcript", "app-automation-transcript", "device-report"] + ); + assert.doesNotThrow(() => validateMatrixQualityGate(embedded, identityArtifactPath)); +}); + +test("matrix quality gate preserves PTY proof for the existing TUI surface", () => { + const gate = matrixGateForSurface("tui", ["pty-capture"]); + + assert.doesNotThrow(() => validateMatrixQualityGate(gate, identityArtifactPath)); +}); + +test("matrix quality gate accepts nonvisual claim-shaped proof without a decorative screenshot", () => { + const cases = [ + ["browser", ["browser-automation"], "browser", ["interaction"]], + ["native", ["app-automation-transcript", "accessibility-tree", "device-report"], "native", ["accessibility"]], + ["hybrid", ["browser-automation", "app-automation-transcript", "device-report", "package-lifecycle-report"], "hybrid", ["interaction", "package-lifecycle"]], + ["renderer", ["app-automation-transcript", "accessibility-tree", "renderer-trace"], "renderer", ["accessibility", "renderer"]] + ]; + + for (const [surface, kinds, owner, claims] of cases) { + const scopedTarget = surface === "browser" + ? target("linux-chrome", "web", "Chrome on Linux") + : surface === "native" || surface === "hybrid" + ? target("windows-desktop", "windows", "Windows 11 desktop") + : target("linux-renderer", "linux", "Linux desktop"); + const gate = matrixGateForSurface(surface, kinds, { target: scopedTarget, owner, claims, scopeReason: "Only the named nonvisual claim changed." }); + assert.doesNotThrow(() => validateMatrixQualityGate(gate, identityArtifactPath), surface); + } +}); + +test("matrix quality gate preserves exact legacy surface proof while scoped rows use stronger claims", () => { + for (const surface of ["native", "desktop", "tui"]) { + assert.doesNotThrow(() => validateMatrixQualityGate(matrixGateForSurface(surface, ["screenshot"]), identityArtifactPath), surface); + } + assert.doesNotThrow(() => validateMatrixQualityGate(matrixGateForSurface("tui", ["app-automation-transcript"]), identityArtifactPath)); + assert.doesNotThrow(() => validateMatrixQualityGate(matrixGateForSurface("browser", ["browser-automation", "screenshot"]), identityArtifactPath)); + assert.throws(() => validateMatrixQualityGate(matrixGateForSurface("browser", ["browser-automation"]), identityArtifactPath), /screenshot|visual/i); +}); + +test("matrix quality gate rejects incompatible or missing HTTP and data proof", () => { + const browserWithHttpDump = matrixGateForSurface("browser", ["browser-automation", "http-dump"]); + const httpWithScreenshot = matrixGateForSurface("http", ["screenshot"]); + const dataWithLog = matrixGateForSurface("data", ["log"]); + + assert.throws(() => validateMatrixQualityGate(browserWithHttpDump, identityArtifactPath), /incompatible/i); + assert.throws(() => validateMatrixQualityGate(httpWithScreenshot, identityArtifactPath), /incompatible|HTTP.*dump/i); + assert.throws(() => validateMatrixQualityGate(dataWithLog, identityArtifactPath), /incompatible|data.*diff|package.*report/i); +}); + +test("matrix quality gate enforces composite hybrid renderer ownership", () => { + const incomplete = matrixGateForSurface( + "custom-rendered WebView hybrid", + ["app-automation-transcript", "accessibility-tree", "device-report", "renderer-trace"] + ); + const complete = matrixGateForSurface( + "custom-rendered WebView hybrid", + ["app-automation-transcript", "client-automation-transcript", "accessibility-tree", "device-report", "renderer-trace"] + ); + + assert.throws(() => validateMatrixQualityGate(incomplete, identityArtifactPath), /client interaction/i); + assert.doesNotThrow(() => validateMatrixQualityGate(complete, identityArtifactPath)); +}); + +test("matrix contract coverage cannot borrow proof from an unrelated contract", () => { + const mismatchedSurface = matrixGateForSurface( + "Flutter Android", + ["screenshot"], + { target: target("android-phone", "android", "Android 15 phone"), owner: "renderer", claims: ["visual"], scopeReason: "Only renderer-owned color output changed." } + ); + mismatchedSurface.executorQa.contractCoverage[0].contractRef = "approved-plan:critical-bridge"; + + const mismatchedAdversarial = cloneJson(mismatchedSurface); + mismatchedAdversarial.executorQa.surfaceEvidence[0].contractRef = "approved-plan:critical-bridge"; + + const artifactOnly = matrixGateForSurface("mobile Web", ["browser-automation"]); + artifactOnly.executorQa.contractCoverage[0].surfaceEvidenceRefs = []; + artifactOnly.executorQa.contractCoverage[0].adversarialCaseRefs = []; + artifactOnly.executorQa.contractCoverage[0].artifactRefs = ["proof-0"]; + + assert.throws(() => validateMatrixQualityGate(mismatchedSurface, identityArtifactPath), /contractRef.*match|match.*contractRef/i); + assert.throws(() => validateMatrixQualityGate(mismatchedAdversarial, identityArtifactPath), /contractRef.*match|match.*contractRef/i); + assert.throws(() => validateMatrixQualityGate(artifactOnly, identityArtifactPath), /proof row|surfaceEvidenceRefs|adversarialCaseRefs/i); +}); + +test("matrix proof rows and coverage rows reject duplicate IDs", () => { + const duplicateSurface = matrixGateForSurface("mobile Web", ["browser-automation"]); + duplicateSurface.executorQa.surfaceEvidence.push(cloneJson(duplicateSurface.executorQa.surfaceEvidence[0])); + + const duplicateAdversarial = matrixGateForSurface("mobile Web", ["browser-automation"]); + duplicateAdversarial.executorQa.adversarialCases.push(cloneJson(duplicateAdversarial.executorQa.adversarialCases[0])); + + const duplicateCoverage = matrixGateForSurface("mobile Web", ["browser-automation"]); + duplicateCoverage.executorQa.contractCoverage.push(cloneJson(duplicateCoverage.executorQa.contractCoverage[0])); + + assert.throws(() => validateMatrixQualityGate(duplicateSurface, identityArtifactPath), /duplicate.*surface|surface.*duplicate/i); + assert.throws(() => validateMatrixQualityGate(duplicateAdversarial, identityArtifactPath), /duplicate.*adversarial|adversarial.*duplicate/i); + assert.throws(() => validateMatrixQualityGate(duplicateCoverage, identityArtifactPath), /duplicate.*contract|contract.*duplicate/i); +}); + +test("matrix rejects empty adversarial proof and proof fields hidden behind not-applicable", () => { + const emptyAdversarial = matrixGateForSurface("mobile Web", ["browser-automation"]); + emptyAdversarial.executorQa.adversarialCases[0].artifactRefs = []; + + const hiddenScope = matrixGateForSurface("browser", ["browser-automation"]); + Object.assign(hiddenScope.executorQa.surfaceEvidence[0], { + status: "not_applicable", + reason: "This contract is outside the selected target.", + owner: "browser" + }); + const scopedAdversarial = matrixGateForSurface("mobile Web", ["browser-automation"]); + scopedAdversarial.executorQa.adversarialCases[0].owner = "browser"; + const scopedCoverage = matrixGateForSurface("mobile Web", ["browser-automation"]); + scopedCoverage.executorQa.contractCoverage[0].scopeReason = "Pretends to declare a target slice."; + + assert.throws(() => validateMatrixQualityGate(emptyAdversarial, identityArtifactPath), /artifactRefs.*empty|must not be empty/i); + assert.throws(() => validateMatrixQualityGate(hiddenScope, identityArtifactPath), /not_applicable.*proof|proof.*not_applicable|owner/i); + assert.throws(() => validateMatrixQualityGate(scopedAdversarial, identityArtifactPath), /adversarial.*scope|scope.*adversarial|owner.*not.*allowed/i); + assert.throws(() => validateMatrixQualityGate(scopedCoverage, identityArtifactPath), /contractCoverage.*scope|scope.*contractCoverage|scopeReason.*not.*allowed/i); +}); diff --git a/test/golden-review-gate.test.js b/test/golden-review-gate.test.js index 851bb3c..d984624 100644 --- a/test/golden-review-gate.test.js +++ b/test/golden-review-gate.test.js @@ -5,8 +5,22 @@ import test from "node:test"; import { enforceAuditProvenance } from "../src/audit-gate-verify.js"; import { createLoop } from "../src/loop.js"; +import { validateReviewQualityGate } from "../src/review-gate.js"; import { reviewStyleQualityGate, runCli, tempRepo, writeEvidence, writeGenuineAuditVerdict, writeQualityGateArtifacts } from "./golden-helpers.js"; +function reviewGateForSurface(surface, kinds, scope = {}) { + const gate = reviewStyleQualityGate({ codeReview: ".superloopy/evidence/code-review.md", gateReview: ".superloopy/evidence/gate-review.md", cliPass: ".superloopy/evidence/cli-pass.txt", malformedReject: ".superloopy/evidence/malformed-reject.txt", auditVerdict: ".superloopy/evidence/review-audit-verdict.json" }); + gate.manualQa.artifactRefs = kinds.map((kind, index) => ({ id: `proof-${index}`, kind, path: `.superloopy/evidence/${kind}-${index}.txt`, description: `${kind} proof for ${surface}.` })); + gate.manualQa.surfaceEvidence[0].surface = surface; + Object.assign(gate.manualQa.surfaceEvidence[0], scope); + gate.manualQa.surfaceEvidence[0].artifactRefs = gate.manualQa.artifactRefs.map((artifact) => artifact.id); + gate.manualQa.adversarialCases[0].artifactRefs = [gate.manualQa.artifactRefs[0].id]; + return gate; +} + +const identityArtifactPath = (value) => value; +const target = (id, platform, environment) => ({ id, platform, environment }); + test("golden: checkpoint accepts Superloopy review quality gate", async () => { const repo = await tempRepo(); await createLoop(repo, ["--brief", "Ship"]); @@ -205,3 +219,329 @@ test("golden: review quality gate now requires an audit section", async () => { assert.equal(result.status, 1); assert.match(result.stderr, /audit/i); }); + +test("review quality gate accepts literal mobile and tablet surfaces with native interaction and visual proof", () => { + for (const surface of ["mobile", "tablet"]) { + const gate = reviewGateForSurface(surface, ["app-automation-transcript", "screenshot", "accessibility-tree", "device-report"]); + assert.doesNotThrow(() => validateReviewQualityGate(gate, identityArtifactPath), surface); + } +}); + +test("review quality gate rejects screenshot-only native proof", () => { + const gate = reviewGateForSurface("native", ["screenshot"]); + + assert.throws( + () => validateReviewQualityGate(gate, identityArtifactPath), + /native.*interaction|interaction.*native/i + ); +}); + +test("review quality gate preserves legacy full-surface proof when owner and claims are absent", () => { + const cases = [ + ["browser", ["browser-automation", "screenshot", "accessibility-tree"]], + ["native", ["app-automation-transcript", "screenshot", "accessibility-tree", "device-report", "package-lifecycle-report"]], + ["hybrid", ["browser-automation", "app-automation-transcript", "screenshot", "accessibility-tree", "device-report", "package-lifecycle-report"]], + ["renderer", ["app-automation-transcript", "screenshot", "accessibility-tree", "renderer-trace"]] + ]; + + for (const [surface, kinds] of cases) { + const gate = reviewGateForSurface(surface, kinds); + assert.doesNotThrow(() => validateReviewQualityGate(gate, identityArtifactPath), surface); + } +}); + +test("review quality gate scopes composite proof to one affected owner and explicit claims", () => { + const shellMenu = reviewGateForSurface( + "Tauri desktop", + ["app-automation-transcript", "device-report"], + { target: target("windows-menu", "windows", "Windows 11 desktop"), owner: "native", claims: ["interaction", "target"], scopeReason: "Only the native menu owner changed." } + ); + const embeddedClient = reviewGateForSurface( + "Tauri desktop", + ["client-automation-transcript"], + { target: target("windows-client", "windows", "Embedded client on Windows 11 desktop"), owner: "browser", claims: ["interaction"], scopeReason: "Only the embedded client interaction changed." } + ); + const flutterColor = reviewGateForSurface( + "Flutter Android", + ["screenshot"], + { target: target("android-phone", "android", "Android 15 phone"), owner: "renderer", claims: ["visual"], scopeReason: "Only renderer-owned color output changed." } + ); + + assert.doesNotThrow(() => validateReviewQualityGate(shellMenu, identityArtifactPath)); + assert.doesNotThrow(() => validateReviewQualityGate(embeddedClient, identityArtifactPath)); + assert.doesNotThrow(() => validateReviewQualityGate(flutterColor, identityArtifactPath)); +}); + +test("review quality gate requires a structured portable target without counting OS names in environment", () => { + const missingTarget = reviewGateForSurface( + "Tauri desktop", + ["app-automation-transcript", "device-report"], + { owner: "native", claims: ["interaction", "target"], scopeReason: "Only the native menu owner changed." } + ); + const hostNamesInEnvironment = reviewGateForSurface( + "native iOS app", + ["app-automation-transcript", "device-report"], + { + owner: "native", + claims: ["interaction", "target"], + scopeReason: "The iOS deployment is exercised through cross-host tooling.", + target: target("ios-simulator", "ios", "iOS simulator hosted on macOS and observed from a Windows runner") + } + ); + const stringTarget = reviewGateForSurface( + "native iOS app", + ["app-automation-transcript", "device-report"], + { target: "iOS simulator", owner: "native", claims: ["interaction", "target"], scopeReason: "Old free-text schema." } + ); + + assert.throws(() => validateReviewQualityGate(missingTarget, identityArtifactPath), /target/i); + assert.throws(() => validateReviewQualityGate(stringTarget, identityArtifactPath), /target.*object|object.*target/i); + const parsed = validateReviewQualityGate(hostNamesInEnvironment, identityArtifactPath); + assert.deepEqual(parsed.manualQa.surfaceEvidence[0].target, target("ios-simulator", "ios", "iOS simulator hosted on macOS and observed from a Windows runner")); + + const genericIds = [ + reviewGateForSurface("browser", ["browser-automation"], { target: target("web", "web", "Chrome 126 on Ubuntu 24.04"), owner: "browser", claims: ["interaction"], scopeReason: "One concrete Web run." }), + reviewGateForSurface("browser", ["browser-automation"], { target: target("browser", "browser", "Firefox 128 on Windows 11"), owner: "browser", claims: ["interaction"], scopeReason: "One concrete browser run." }), + reviewGateForSurface("native", ["app-automation-transcript", "device-report"], { target: target("desktop", "desktop", "Windows 11 workstation"), owner: "native", claims: ["interaction"], scopeReason: "One concrete desktop run." }), + reviewGateForSurface("native", ["app-automation-transcript", "device-report"], { target: target("mobile", "mobile", "iPhone 15 running iOS 18"), owner: "native", claims: ["interaction"], scopeReason: "One concrete mobile run." }), + reviewGateForSurface("native", ["app-automation-transcript", "device-report"], { target: target("android-emulator", "android", "Android emulator on Linux"), owner: "native", claims: ["interaction"], scopeReason: "One concrete emulator run." }) + ]; + for (const gate of genericIds) assert.doesNotThrow(() => validateReviewQualityGate(gate, identityArtifactPath)); +}); + +test("review quality gate rejects nonportable or vague scoped target identifiers", () => { + const invalidTargets = [ + target("Windows_Menu", "windows", "Windows desktop"), + target("windows/menu", "windows", "Windows desktop"), + target("all-windows", "windows", "Windows desktop"), + target("supported-targets", "windows", "Windows desktop"), + target("alltargets", "windows", "Windows desktop"), target("supporteddevices", "windows", "Windows desktop"), target("crossplatforms", "windows", "Windows desktop"), + target("web", "web", "all supported browsers"), target("mobile", "mobile", "any devices"), target("desktop", "desktop", "cross-platform"), + target("web", "web", "multiple browsers"), target("mobile", "mobile", "multi-device targets"), target("desktop", "desktop", "universal targets"), + target("windows-menu", "Windows", "Windows desktop"), + target("windows-menu", "windows-11", "Windows desktop"), + target("windows-menu", "universal", "Windows desktop"), + target("windows-menu", "windows", "") + ]; + + for (const invalidTarget of invalidTargets) { + const gate = reviewGateForSurface( + "native Windows app", + ["app-automation-transcript", "device-report"], + { target: invalidTarget, owner: "native", claims: ["interaction", "target"], scopeReason: "Target schema probe." } + ); + assert.throws(() => validateReviewQualityGate(gate, identityArtifactPath), /target|portable|platform|environment|vague/i, JSON.stringify(invalidTarget)); + } +}); + +test("review quality gate keeps scoped rows and resolved artifacts one-to-one", () => { + const duplicateScope = reviewGateForSurface("browser", ["browser-automation", "browser-automation"]); + Object.assign(duplicateScope.manualQa.surfaceEvidence[0], { target: target("chrome-linux", "web", "Chrome on Linux"), owner: "browser", claims: ["interaction"], scopeReason: "Primary interaction proof.", artifactRefs: ["proof-0"] }); + duplicateScope.manualQa.surfaceEvidence.push({ ...structuredClone(duplicateScope.manualQa.surfaceEvidence[0]), id: "surface-second", artifactRefs: ["proof-1"] }); + + const sharedArtifact = structuredClone(duplicateScope); + sharedArtifact.manualQa.surfaceEvidence[1].target = target("firefox-linux", "web", "Firefox on Linux"); + sharedArtifact.manualQa.surfaceEvidence[1].artifactRefs = ["proof-0"]; + + const duplicateResolvedDeclaration = reviewGateForSurface("browser", ["browser-automation", "browser-automation"]); + duplicateResolvedDeclaration.manualQa.artifactRefs[0].path = ".superloopy/evidence/primary-proof.txt"; + duplicateResolvedDeclaration.manualQa.artifactRefs[1].path = ".superloopy/evidence/alias-proof.txt"; + const resolveAlias = (value) => value.endsWith("primary-proof.txt") || value.endsWith("alias-proof.txt") + ? "/resolved/shared-proof.txt" + : value; + + assert.throws(() => validateReviewQualityGate(duplicateScope, identityArtifactPath), /duplicate.*target.*owner|target.*owner.*duplicate/i); + assert.throws(() => validateReviewQualityGate(sharedArtifact, identityArtifactPath), /artifact.*reused|reused.*artifact|distinct.*target/i); + assert.throws(() => validateReviewQualityGate(duplicateResolvedDeclaration, resolveAlias), /duplicate.*resolved.*path|resolved.*path.*duplicate/i); +}); + +test("review quality gate rejects failed status or result aliases", () => { + const failedSurfaceStatus = reviewGateForSurface("mobile Web", ["browser-automation"]); + failedSurfaceStatus.manualQa.surfaceEvidence[0].status = "failed"; + const failedSurfaceResult = reviewGateForSurface("mobile Web", ["browser-automation"]); + failedSurfaceResult.manualQa.surfaceEvidence[0].result = "failed"; + const failedAdversarialStatus = reviewGateForSurface("mobile Web", ["browser-automation"]); + failedAdversarialStatus.manualQa.adversarialCases[0].status = "failed"; + const failedAdversarialResult = reviewGateForSurface("mobile Web", ["browser-automation"]); + failedAdversarialResult.manualQa.adversarialCases[0].result = "failed"; + + for (const gate of [failedSurfaceStatus, failedSurfaceResult, failedAdversarialStatus, failedAdversarialResult]) { + assert.throws(() => validateReviewQualityGate(gate, identityArtifactPath), /status|result|passed/i); + } +}); + +test("review quality gate rejects incomplete or incoherent claim-shaped scope", () => { + const bridgeWithoutShell = reviewGateForSurface( + "Tauri desktop", + ["client-automation-transcript"], + { target: target("windows-bridge", "windows", "Windows 11 desktop"), owner: "hybrid", claims: ["interaction"], scopeReason: "The bridge journey changed." } + ); + const visualWithoutImage = reviewGateForSurface( + "Flutter Android", + ["app-automation-transcript"], + { target: target("android-phone", "android", "Android 15 phone"), owner: "renderer", claims: ["visual"], scopeReason: "Renderer-owned visual output changed." } + ); + const impossibleOwner = reviewGateForSurface( + "native desktop", + ["browser-automation"], + { target: target("windows-client", "windows", "Windows 11 desktop"), owner: "browser", claims: ["interaction"], scopeReason: "Incorrectly attributes a native-only surface to a browser owner." } + ); + const missingOwner = reviewGateForSurface( + "native desktop", + ["app-automation-transcript"], + { claims: ["interaction"] } + ); + const nativeVisualWithoutTarget = reviewGateForSurface( + "native desktop", + ["screenshot"], + { target: target("windows-visual", "windows", "Windows 11 desktop"), owner: "native", claims: ["visual"], scopeReason: "Native visual output changed." } + ); + const accessibilityWithoutInteraction = reviewGateForSurface( + "Tauri desktop", + ["accessibility-tree"], + { target: target("windows-client", "windows", "Windows 11 desktop"), owner: "browser", claims: ["accessibility"], scopeReason: "Embedded client semantics changed." } + ); + const duplicateClaims = reviewGateForSurface( + "browser", + ["browser-automation"], + { target: target("linux-chrome", "web", "Chrome on Linux"), owner: "browser", claims: ["interaction", "interaction"], scopeReason: "Browser interaction changed." } + ); + const missingReason = reviewGateForSurface( + "browser", + ["browser-automation"], + { owner: "browser", claims: ["interaction"] } + ); + const hybridVisualWithoutBothSides = reviewGateForSurface( + "Tauri desktop", + ["screenshot", "device-report"], + { target: target("windows-bridge", "windows", "Windows 11 desktop"), owner: "hybrid", claims: ["visual"], scopeReason: "Bridge-owned visible state changed." } + ); + const hybridTargetWithoutBothSides = reviewGateForSurface( + "Tauri desktop", + ["device-report"], + { target: target("windows-bridge", "windows", "Windows 11 desktop"), owner: "hybrid", claims: ["target"], scopeReason: "Bridge target behavior changed." } + ); + const hybridPackageWithoutBothSides = reviewGateForSurface( + "Tauri desktop", + ["package-lifecycle-report", "device-report"], + { target: target("windows-bridge", "windows", "Windows 11 desktop"), owner: "hybrid", claims: ["package-lifecycle"], scopeReason: "Bridge package lifecycle changed." } + ); + + assert.throws(() => validateReviewQualityGate(bridgeWithoutShell, identityArtifactPath), /shell interaction/i); + assert.throws(() => validateReviewQualityGate(visualWithoutImage, identityArtifactPath), /visual.*screenshot|screenshot.*visual/i); + assert.throws(() => validateReviewQualityGate(impossibleOwner, identityArtifactPath), /owner.*browser.*not.*surface|browser.*owner.*surface/i); + assert.throws(() => validateReviewQualityGate(missingOwner, identityArtifactPath), /owner.*claims|claims.*owner/i); + assert.throws(() => validateReviewQualityGate(nativeVisualWithoutTarget, identityArtifactPath), /device|target/i); + assert.throws(() => validateReviewQualityGate(accessibilityWithoutInteraction, identityArtifactPath), /accessibility.*interaction|interaction.*accessibility/i); + assert.throws(() => validateReviewQualityGate(duplicateClaims, identityArtifactPath), /duplicate.*claim|claim.*duplicate/i); + assert.throws(() => validateReviewQualityGate(missingReason, identityArtifactPath), /scopeReason/i); + for (const gate of [hybridVisualWithoutBothSides, hybridTargetWithoutBothSides, hybridPackageWithoutBothSides]) { + assert.throws(() => validateReviewQualityGate(gate, identityArtifactPath), /client interaction.*shell interaction|shell interaction.*client interaction/i); + } +}); + +test("review quality gate accepts legacy browser visual proof and custom-renderer browser interaction proof", () => { + const legacyBrowser = reviewGateForSurface("browser", ["screenshot"]); + const customRenderer = reviewGateForSurface("custom-rendered Web", ["browser-automation", "screenshot", "accessibility-tree", "renderer-trace"]); + + assert.doesNotThrow(() => validateReviewQualityGate(legacyBrowser, identityArtifactPath)); + assert.doesNotThrow(() => validateReviewQualityGate(customRenderer, identityArtifactPath)); +}); + +test("review quality gate limits screenshot-only compatibility to exact legacy browser literals", () => { + for (const surface of ["PWA", "Chrome extension", "authenticated Web"]) { + const gate = reviewGateForSurface(surface, ["screenshot"]); + assert.throws( + () => validateReviewQualityGate(gate, identityArtifactPath), + /automation|interaction/i, + surface + ); + } +}); + +test("review quality gate keeps browser targets Web-only while embedded WebView remains composite", () => { + for (const surface of ["mobile Web", "desktop browser", "mobile PWA", "Chrome extension on desktop"]) { + const gate = reviewGateForSurface(surface, ["browser-automation"]); + assert.doesNotThrow(() => validateReviewQualityGate(gate, identityArtifactPath), surface); + } + const embedded = reviewGateForSurface( + "embedded mobile WebView", + ["client-automation-transcript", "app-automation-transcript", "device-report"] + ); + assert.doesNotThrow(() => validateReviewQualityGate(embedded, identityArtifactPath)); +}); + +test("review quality gate enforces every owner in composite native and hybrid renderer surfaces", () => { + const nativeWithoutTarget = reviewGateForSurface( + "custom-rendered native desktop", + ["app-automation-transcript", "accessibility-tree", "renderer-trace"] + ); + const hybridWithoutClientInteraction = reviewGateForSurface( + "custom-rendered WebView hybrid", + ["app-automation-transcript", "accessibility-tree", "device-report", "renderer-trace"] + ); + const completeHybrid = reviewGateForSurface( + "custom-rendered WebView hybrid", + ["app-automation-transcript", "client-automation-transcript", "accessibility-tree", "device-report", "renderer-trace"] + ); + + assert.throws(() => validateReviewQualityGate(nativeWithoutTarget, identityArtifactPath), /device|target/i); + assert.throws(() => validateReviewQualityGate(hybridWithoutClientInteraction, identityArtifactPath), /client interaction/i); + assert.doesNotThrow(() => validateReviewQualityGate(completeHybrid, identityArtifactPath)); +}); + +test("review quality gate does not treat qualified native GUI or shell rows as legacy browser or CLI", () => { + for (const surface of ["desktop GUI", "native shell"]) { + const gate = reviewGateForSurface(surface, ["screenshot"]); + assert.throws( + () => validateReviewQualityGate(gate, identityArtifactPath), + /native.*interaction|interaction.*native/i, + surface + ); + } +}); + +test("review quality gate preserves HTTP and data artifact boundaries", () => { + const httpWithDataDiff = reviewGateForSurface("http", ["data-diff"]); + const dataWithHttpDump = reviewGateForSurface("data", ["http-dump"]); + + assert.throws(() => validateReviewQualityGate(httpWithDataDiff, identityArtifactPath), /incompatible/); + assert.throws(() => validateReviewQualityGate(dataWithHttpDump, identityArtifactPath), /incompatible/); +}); + +test("review quality gate accepts nonvisual claim-shaped proof without a decorative screenshot", () => { + const cases = [ + ["browser", ["browser-automation"], "browser", ["interaction"]], + ["native", ["app-automation-transcript", "accessibility-tree", "device-report"], "native", ["accessibility"]], + ["hybrid", ["browser-automation", "app-automation-transcript", "device-report", "package-lifecycle-report"], "hybrid", ["interaction", "package-lifecycle"]], + ["renderer", ["app-automation-transcript", "accessibility-tree", "renderer-trace"], "renderer", ["accessibility", "renderer"]] + ]; + + for (const [surface, kinds, owner, claims] of cases) { + const scopedTarget = surface === "browser" + ? target("linux-chrome", "web", "Chrome on Linux") + : surface === "native" || surface === "hybrid" + ? target("windows-desktop", "windows", "Windows 11 desktop") + : target("linux-renderer", "linux", "Linux desktop"); + const gate = reviewGateForSurface(surface, kinds, { target: scopedTarget, owner, claims, scopeReason: "Only the named nonvisual claim changed." }); + assert.doesNotThrow(() => validateReviewQualityGate(gate, identityArtifactPath), surface); + } +}); + +test("review quality gate rejects empty adversarial proof and duplicate proof-row IDs", () => { + const emptyAdversarial = reviewGateForSurface("browser", ["browser-automation"]); + emptyAdversarial.manualQa.adversarialCases[0].artifactRefs = []; + + const duplicateSurface = reviewGateForSurface("browser", ["browser-automation"]); + duplicateSurface.manualQa.surfaceEvidence.push(structuredClone(duplicateSurface.manualQa.surfaceEvidence[0])); + + const duplicateAdversarial = reviewGateForSurface("browser", ["browser-automation"]); + duplicateAdversarial.manualQa.adversarialCases.push(structuredClone(duplicateAdversarial.manualQa.adversarialCases[0])); + const scopedAdversarial = reviewGateForSurface("browser", ["browser-automation"]); + scopedAdversarial.manualQa.adversarialCases[0].target = target("web", "web", "Chrome on Linux"); + + assert.throws(() => validateReviewQualityGate(emptyAdversarial, identityArtifactPath), /artifactRefs.*empty|must not be empty/i); + assert.throws(() => validateReviewQualityGate(duplicateSurface, identityArtifactPath), /duplicate.*surface|surface.*duplicate/i); + assert.throws(() => validateReviewQualityGate(duplicateAdversarial, identityArtifactPath), /duplicate.*adversarial|adversarial.*duplicate/i); + assert.throws(() => validateReviewQualityGate(scopedAdversarial, identityArtifactPath), /adversarial.*scope|scope.*adversarial|target.*not.*allowed/i); +}); diff --git a/test/golden-scoped-target-platforms.test.js b/test/golden-scoped-target-platforms.test.js new file mode 100644 index 0000000..99ec6ab --- /dev/null +++ b/test/golden-scoped-target-platforms.test.js @@ -0,0 +1,80 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { validateMatrixQualityGate } from "../src/matrix-gate.js"; +import { validateReviewQualityGate } from "../src/review-gate.js"; +import { cloneJson, matrixStyleQualityGate, reviewStyleQualityGate } from "./golden-helpers.js"; + +const identityArtifactPath = (value) => value; +const target = (id, platform, environment) => ({ id, platform, environment }); +const SHARED_KINDS = ["app-automation-transcript", "device-report", "app-automation-transcript", "device-report"]; +const windowsScope = () => ({ + target: target("desktop", "windows", "Windows 11 workstation"), + owner: "native", + claims: ["interaction", "target"], + scopeReason: "Windows run of the shared desktop journey." +}); +const linuxRow = (row) => ({ + ...cloneJson(row), + id: "surface-linux", + target: target("desktop", "linux", "Ubuntu 24.04 workstation"), + scopeReason: "Linux run of the shared desktop journey.", + artifactRefs: ["proof-2", "proof-3"] +}); + +function matrixGateForSurface(surface, kinds, scope = {}) { + const gate = matrixStyleQualityGate({ cliRun: ".superloopy/evidence/matrix-cli-run.txt", redTeam: ".superloopy/evidence/matrix-risk-probe.txt", auditVerdict: ".superloopy/evidence/matrix-audit-verdict.json" }); + gate.executorQa.artifactRefs = kinds.map((kind, index) => ({ id: `proof-${index}`, kind, path: `.superloopy/evidence/${kind}-${index}.txt`, description: `${kind} proof for ${surface}.` })); + gate.executorQa.surfaceEvidence[0].surface = surface; + Object.assign(gate.executorQa.surfaceEvidence[0], scope); + gate.executorQa.surfaceEvidence[0].artifactRefs = ["proof-0", "proof-1"]; + gate.executorQa.adversarialCases[0].artifactRefs = [gate.executorQa.artifactRefs[0].id]; + return gate; +} + +function reviewGateForSurface(surface, kinds, scope = {}) { + const gate = reviewStyleQualityGate({ codeReview: ".superloopy/evidence/code-review.md", gateReview: ".superloopy/evidence/gate-review.md", cliPass: ".superloopy/evidence/cli-pass.txt", malformedReject: ".superloopy/evidence/malformed-reject.txt", auditVerdict: ".superloopy/evidence/review-audit-verdict.json" }); + gate.manualQa.artifactRefs = kinds.map((kind, index) => ({ id: `proof-${index}`, kind, path: `.superloopy/evidence/${kind}-${index}.txt`, description: `${kind} proof for ${surface}.` })); + gate.manualQa.surfaceEvidence[0].surface = surface; + Object.assign(gate.manualQa.surfaceEvidence[0], scope); + gate.manualQa.surfaceEvidence[0].artifactRefs = ["proof-0", "proof-1"]; + gate.manualQa.adversarialCases[0].artifactRefs = [gate.manualQa.artifactRefs[0].id]; + return gate; +} + +test("matrix quality gate accepts one generic target id across distinct platforms", () => { + const gate = matrixGateForSurface("native desktop", SHARED_KINDS, windowsScope()); + gate.executorQa.surfaceEvidence.push(linuxRow(gate.executorQa.surfaceEvidence[0])); + gate.executorQa.contractCoverage[0].surfaceEvidenceRefs.push("surface-linux"); + + assert.doesNotThrow(() => validateMatrixQualityGate(gate, identityArtifactPath)); +}); + +test("review quality gate accepts one generic target id across distinct platforms", () => { + const gate = reviewGateForSurface("native desktop", SHARED_KINDS, windowsScope()); + gate.manualQa.surfaceEvidence.push(linuxRow(gate.manualQa.surfaceEvidence[0])); + + assert.doesNotThrow(() => validateReviewQualityGate(gate, identityArtifactPath)); +}); + +test("matrix and review gates keep resolved artifacts exclusive across platform slices of one target id", () => { + const matrix = matrixGateForSurface("native desktop", SHARED_KINDS, windowsScope()); + matrix.executorQa.surfaceEvidence.push({ ...linuxRow(matrix.executorQa.surfaceEvidence[0]), artifactRefs: ["proof-0", "proof-3"] }); + matrix.executorQa.contractCoverage[0].surfaceEvidenceRefs.push("surface-linux"); + const review = reviewGateForSurface("native desktop", SHARED_KINDS, windowsScope()); + review.manualQa.surfaceEvidence.push({ ...linuxRow(review.manualQa.surfaceEvidence[0]), artifactRefs: ["proof-0", "proof-3"] }); + + assert.throws(() => validateMatrixQualityGate(matrix, identityArtifactPath), /reused across distinct scoped target\/platform\/owner slices/i); + assert.throws(() => validateReviewQualityGate(review, identityArtifactPath), /reused across distinct scoped target\/platform\/owner slices/i); +}); + +test("matrix and review gates still reject one repeated target id on one platform and owner", () => { + const matrix = matrixGateForSurface("native desktop", SHARED_KINDS, windowsScope()); + matrix.executorQa.surfaceEvidence.push({ ...linuxRow(matrix.executorQa.surfaceEvidence[0]), target: target("desktop", "windows", "Second Windows 11 workstation") }); + matrix.executorQa.contractCoverage[0].surfaceEvidenceRefs.push("surface-linux"); + const review = reviewGateForSurface("native desktop", SHARED_KINDS, windowsScope()); + review.manualQa.surfaceEvidence.push({ ...linuxRow(review.manualQa.surfaceEvidence[0]), target: target("desktop", "windows", "Second Windows 11 workstation") }); + + assert.throws(() => validateMatrixQualityGate(matrix, identityArtifactPath), /duplicates scoped target.*platform.*owner/i); + assert.throws(() => validateReviewQualityGate(review, identityArtifactPath), /duplicates scoped target.*platform.*owner/i); +}); diff --git a/test/plugin.test.js b/test/plugin.test.js index c38f9f4..056862b 100644 --- a/test/plugin.test.js +++ b/test/plugin.test.js @@ -68,10 +68,15 @@ test("plugin interface assets resolve inside the npm package", async () => { assert.equal(packed.status, 0, packed.stderr || packed.stdout); const files = new Set(JSON.parse(packed.stdout)[0].files.map((file) => file.path)); assert.equal(files.has(assetPaths[0].replace(/^\.\//u, "")), true, "interface asset missing from npm pack"); - for (const reference of ["motion.md", "redesign.md", "system-map.md", "upstream-notice.md"]) { + for (const reference of ["desktop.md", "hybrid.md", "layout.md", "mobile.md", "motion-core.md", "motion.md", "redesign.md", "renderer.md", "system-map.md", "upstream-notice.md", "ux.md"]) { const path = `skills/superloopy-frontend/references/${reference}`; assert.equal(files.has(path), true, `frontend reference missing from npm pack: ${path}`); } + assert.equal( + files.has("skills/superloopy-frontend/scripts/evidence-root.mjs"), + true, + "portable frontend evidence helper missing from npm pack", + ); }); test("plugin audit docs describe convention discovery and current ignore scope", async () => { @@ -222,7 +227,7 @@ test("plugin packages the Superloopy frontend skill with explicit activation and assert.match(frontend.content, /VISUAL_QA\.md/); assert.match(frontend.content, /SUPERLOOPY_EVIDENCE/); assert.match(frontend.content, /\.superloopy\/evidence\/frontend/); - for (const name of ["web", "qt", "qt-widgets", "qt-quick", "qt-qa"]) { + for (const name of ["ux", "desktop", "mobile", "hybrid", "renderer", "web", "qt", "qt-widgets", "qt-quick", "qt-qa"]) { assert.match(frontend.content, new RegExp(`\\]\\(references/${name}\\.md\\)`)); } @@ -240,25 +245,54 @@ test("plugin packages the Superloopy frontend skill with explicit activation and assert.match(antiSlop, /em-dash/i); const designSystem = await readFile("skills/superloopy-frontend/references/design-system.md", "utf8"); - assert.match(designSystem, /7 sections|7-section/i); + assert.match(designSystem, /7 sections|7-section|seven-section/i); const metadata = await readFile("skills/superloopy-frontend/agents/openai.yaml", "utf8"); assert.match(metadata, /\$superloopy:superloopy-frontend/); - assert.match(metadata, /\/superloopy:superloopy-frontend/); + assert.match(metadata, /short_description: "Explicit cross-platform application UI workflow"/u); + assert.match(metadata, /Use `\$superloopy:superloopy-frontend` only after explicit invocation/iu); + assert.match(metadata, /browser-hosted or interactive content-led Web, desktop, mobile\/tablet, embedded\/hybrid, Qt, custom-rendered, or mixed application UI/iu); + assert.match(metadata, /exclude TV, wearable, XR, automotive, game UI, TUI, static media\/document artifacts, and non-UI work/iu); + assert.match(metadata, /proportional evidence per owner and target/iu); + assert.doesNotMatch(metadata, /^policy:/mu); + assert.equal(metadata.match(/\$superloopy:superloopy-frontend/g)?.length, 1); + assert.equal(metadata.match(/\/superloopy:superloopy-frontend/g)?.length ?? 0, 0); assert.doesNotMatch(metadata, /any UI\/visual work/i); const promptHook = await readFile("hooks/user-prompt-submit.json", "utf8"); assert.doesNotMatch(promptHook, /statusMessage/); }); -test("web route lazily loads system, motion, and redesign context without widening Qt routing", async () => { - const frontend = await readSkill("superloopy-frontend"); +test("frontend web evidence calls the compatibility scanner a partial token lint", async () => { const web = await readFile("skills/superloopy-frontend/references/web.md", "utf8"); + const upstream = await readFile("skills/superloopy-frontend/references/upstream-notice.md", "utf8"); + const scanner = await readFile("skills/superloopy-frontend/scripts/ds-compliance.mjs", "utf8"); + const designAudit = await readFile("docs/superloopy-design-audit.md", "utf8"); + const fileAudit = await readFile("docs/superloopy-file-audit.md", "utf8"); + const golden = await readFile("docs/superloopy-loop-golden-set.md", "utf8"); - for (const name of ["system-map", "motion", "redesign"]) { - assert.match(web, new RegExp(`references/${name}\\.md`)); - assert.doesNotMatch(frontend.content, new RegExp(`references/${name}\\.md`)); + for (const contract of [web, scanner, designAudit, fileAudit, golden]) { + assert.match(contract, /partial color\/spacing token lint/i); + assert.doesNotMatch(contract, /(?:full|complete) design-system compliance/i); } + assert.match(web, /ds-compliance\.mjs.*compatibility filename/is); + assert.match(scanner, /Compatibility filename/is); + assert.match(upstream, /optional adapted Web references/is); + assert.match(upstream, /shared UX.*platform.*composition.*Superloopy-native/is); +}); + +test("web route keeps Web specializations lazy while shared motion stays cross-platform", async () => { + const frontend = await readSkill("superloopy-frontend"); + const web = await readFile("skills/superloopy-frontend/references/web.md", "utf8"); + + assert.match(web, /references\/system-map\.md/u); + assert.doesNotMatch(frontend.content, /references\/system-map\.md/u); + assert.match(web, /references\/redesign\.md/u); + assert.match(frontend.content, /references\/redesign\.md.*Web-only.*Web route/is); + assert.match(frontend.content, /native.*redesign.*does not inherit.*browser.*SEO.*real-browser/is); + assert.match(web, /references\/motion\.md/u); + assert.match(frontend.content, /references\/motion-core\.md/u); + assert.match(frontend.content, /Web implementation.*references\/motion\.md.*native.*Qt.*do not inherit/is); assert.match(web, /dial guidance is heuristic/i); assert.match(web, /user intent.*existing interface.*visual target/is); assert.doesNotMatch(web, /marketing.*default.*MOTION.*6-8/is);