Compare commits

..
Author SHA1 Message Date
Abdul WahabandClaude Fable 5.1 dacf7d4664 Refresh the in-page bundle for the sampled-contrast core changes
The core crate changed on this branch (the sampled-contrast module and
the shared safe-tag gate in check_colors), so the tracked in-page
bundle is regenerated, as docs/ENGINE.md asks after touching
crates/core. This machine's rust-lld cannot link wasm, so the bundle is
the one CI's `bun run build:extension` produced for this branch (run
34444485574 on 634b579b6), taken from its dist artifact. Its registry
JSON is byte-identical to the tracked one: no rule was added. Under the
same CI toolchain the bundle differs from main's by 208 bytes, with no
rule or adapter behavior change.

AI assistance: Claude Code (Claude Fable 5.1), on maintainer instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 11:31:55 +05:00
Abdul WahabandClaude Fable 5.1 634b579b63 Sampled contrast: url() is a token, not a suffix of an identifier
The sheet url rewrite and its comment scan both matched `url(` inside a
longer identifier, so `myurl(a /* url(x.png) */)` read as an unquoted
url and the comment inside it as url content. Both now require a
non-identifier byte before `url(`, the way the tokenizer does; the
regex re-emits that byte unchanged. A test pins a custom function
wrapping a comment beside a real url on the same rule.

AI assistance: Claude Code (Claude Fable 5.1), on maintainer instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 11:16:24 +05:00
Abdul WahabandClaude Fable 5.1 4c5f3974b3 Sampled contrast: comment scan honors CSS strings and url tokens
The sheet url rewrite skipped comments by looking for `/*` in the raw
text, which is not how the CSS tokenizer sees it: a `/*` inside a quoted
string or an unquoted url() is content, so `content: "a/*b"` turned the
rest of the sheet into a comment and its relative urls stayed
sheet-relative, where the sampler could not find them. The scan is a
small state machine now (code, string, unquoted url) that mirrors the
tokenizer: a string ends at its quote or at a raw newline, a url at its
closing paren, and only a `/*` in code opens a comment. Tests pin a
string with a marker, a url with a marker, and a real comment after a
string.

AI assistance: Claude Code (Claude Fable 5.1), on maintainer instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 11:03:50 +05:00
Abdul WahabandClaude Fable 5.1 2422ec058f Sampled contrast: keep the sheet url rewrite inside one url token
The linked-sheet url rewrite ran one regex over the raw stylesheet text,
and its unquoted `url(` form could match across lines to a `)` in a
later rule, so an unclosed `url(` inside a comment would have replaced
the rules between them with one rewritten path, and that text feeds the
whole cascade, not only sampled contrast. Comments are now copied
through untouched, a quoted url ends at its line, and an unquoted one
stops at whitespace, quotes, parens, braces, or a semicolon, as CSS
does. Tests pin a commented `url(`, an unclosed quote, and an
unterminated comment.

AI assistance: Claude Code (Claude Fable 5.1), on maintainer instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 10:52:08 +05:00
Abdul WahabandClaude Fable 5.1 0f115e5d37 Sampled contrast: no panic on multibyte urls, any layer order, auto size
Second review round for the sampled-contrast path (#560), plus the
Windows CI fix:

- `is_data_uri` byte-sliced the url, so a local image whose name opens
  with a multibyte character panicked the static scanner. It reads the
  prefix with `get` now.
- The ground walk only accepted a layer that begins with `url(`, while a
  `background` shorthand may put color, position, size, or repeat tokens
  before the image; such layers fell out of the sampled path. The call is
  found anywhere in the layer and decoded from there. A fixture case pins
  it, so the fixture and directory goldens gain one finding.
- The decoration gate scaled the missing size axis only for a single
  `40px` token; `40px auto` and `auto 40px` now follow the image's aspect
  ratio the same way.
- The linked-sheet url rewrite did its path math through the OS-flavored
  helpers, which rendered `css/light.png` as `light.png` on Windows. It
  uses the POSIX helpers on both directories, since a CSS url is POSIX on
  every OS.

AI assistance: Claude Code (Claude Fable 5.1), on maintainer instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 10:43:07 +05:00
Abdul WahabandClaude Fable 5.1 66945a79da Sampled contrast: bound data URIs, resolve sheet urls, reset shorthand
Review fixes for the sampled-contrast path (#560):

- A base64 data URI is refused before anything is copied or decoded
  when its payload would exceed the 24 MiB budget a file gets, the
  cache keys it by length and hash instead of holding the URI, and a
  payload without whitespace is decoded in place.
- A linked stylesheet from another directory has its relative url()s
  rewritten to page-relative form when it is inlined, so the sampler
  resolves the image the winning declaration named against one base
  instead of guessing across every sheet directory.
- Every `background` shorthand resets backgroundRepeat and
  backgroundSize to what it names, the defaults when it names nothing,
  so `background: #fff` no longer leaves a stale no-repeat behind for a
  later background-image; CSS-wide keywords pass through and a bare
  var() value is left alone the way the expansion leaves it.

Tests cover the bound, the rewrite (relative, parent, quoted, query,
root-relative, remote, data, fragment, whitespace), and the reset order.
Goldens are unchanged.

AI assistance: Claude Code (Claude Fable 5.1), on maintainer instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 10:30:34 +05:00
Abdul WahabandClaude Fable 5.1 a82233736c Static engine: pixel-sample image backgrounds behind text
The analytic walk skips the contrast check when a url() image layer backs
text, so CLI scans, CI gates, and the hook had no answer for image-backed
text. Outside a browser there is no CORS and no canvas taint: the static
engine now reads the image itself, a local file next to the markup or a
base64 data URI, decoded with the pure-Rust image crate, sampled on a fixed
6x6 grid, and measured against the text. Static scans have no layout, so
the verdict is coarse by design: the finding fires only when the 90th
percentile of the grid fails. Uniform tint washes composite exactly,
varying scrims keep the skip, no-repeat icons count as decoration, and a
remote URL is never fetched.

The cascade carries backgroundRepeat / backgroundSize for the decoration
gate, stored beside the background shorthand expansion rather than inside
it, because the frozen vectors pin that expansion.

Fixture sampled-image-contrast.html with generated PNG, JPEG, WebP, alpha
and icon images; goldens recorded for the new cases, and the directory
sweeps re-recorded (only the fixture's eight findings moved), noted in
tests/oracle/DELTAS.md.

Refs #560.

AI assistance: Claude Code (Claude Fable 5.1), on maintainer instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 10:08:00 +05:00
67d018fe05 Fix: print JSON on live-poll --reply success (#800)
Successful --reply was exit 0 with empty stdout, so agents could not tell delivery from a hang. Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 08:47:39 +05:00
3bdb9ff06c Fix: drop stale carbonize diagnostic on complete (#801)
Complete and discarded snapshots no longer keep carbonize_cleanup_required after cleanup is done.

AI assistance: Cursor Grok 4.6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 08:47:16 +05:00
Paul Bakaus cd12f8660e Release skill 4.3.1
Publish the launcher setup diagnostics hotfix and refresh generated provider version metadata.

AI-assisted by Codex.
2026-09-08 19:23:50 -07:00
github-actions[bot] 43088d8fbf Sync generated provider output 2026-09-09 02:20:49 +00:00
Paul BakausandGitHub a8ce5962d3 Fix silent launcher setup failures (#788)
* Fix silent launcher setup failures

Report cache creation, cache write, and download failures with recovery guidance while preserving lazy engine downloads.

AI-assisted by Codex.

* Fix Windows staging-write failure handling

Branch directly on redirection failure and reject staging directories before cleanup. Add coverage for an existing read-only staging file.

AI-assisted by Codex.
2026-09-08 19:20:16 -07:00
7d6c5bdd92 Fix: keep UNC prefix when normalizing hook paths (#784)
Collapsing every separator run turned \\server\share into /server/share, so doctor would probe the wrong file. Leading // after a quote is left intact.

Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 05:10:10 +05:00
73552b79c5 Fix: user-scope Windows hook group duplication (#784)
JSON-quoted absolute paths doubled backslashes, so merge failed to recognize the group it had just written and appended another copy on every update.

Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 05:10:10 +05:00
Paul BakausandGitHub 73a6f51a54 Release engine 0.1.5, CLI 4.1.0 and skill 4.3.0 (#787)
* Release engine 0.1.5, CLI 4.1.0 and skill 4.3.0

Ship GPT Image 2.5 Flare and native transparent assets. Refresh generated provider distributions for the release.

Prepared with AI assistance under the maintainer's instructions.

* Fix: lock released engine platform packages

Regenerate all five 0.1.5 platform resolutions after npm publication and verify frozen installation.

Prepared with AI assistance under maintainer instructions.
2026-09-08 16:28:39 -07:00
112 changed files with 3070 additions and 249 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
license: Apache 2.0
allowed-tools:
- Bash(npx impeccable *)
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -2,7 +2,7 @@
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
metadata:
version: 4.3.0
version: 4.3.1
---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -12,7 +12,7 @@
{
"name": "impeccable",
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"version": "4.3.0",
"version": "4.3.1",
"author": {
"name": "Paul Bakaus",
"email": "paul@paulbakaus.com"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "impeccable",
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"version": "4.3.0",
"version": "4.3.1",
"author": {
"name": "Paul Bakaus",
"email": "paul@paulbakaus.com"
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
user-invocable: true
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
license: Apache 2.0
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
license: Apache 2.0
---
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
user-invocable: true
license: Apache 2.0
---
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
+35 -4
View File
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
user-invocable: true
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
license: Apache 2.0
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
user-invocable: true
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
license: Apache 2.0
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
+35 -4
View File
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
license: Apache 2.0
---
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
license: Apache 2.0
---
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
+35 -4
View File
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
user-invocable: true
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
license: Apache 2.0
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
license: Apache 2.0
allowed-tools:
- Bash(npx impeccable *)
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
+35 -4
View File
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
user-invocable: true
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
license: Apache 2.0
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
user-invocable: true
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
license: Apache 2.0
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
user-invocable: true
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
license: Apache 2.0
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
user-invocable: true
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
license: Apache 2.0
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
+35 -4
View File
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
license: Apache 2.0
---
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
+35 -4
View File
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
user-invocable: true
license: Apache 2.0
allowed-tools:
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
+35 -4
View File
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
Generated
+3
View File
@@ -509,6 +509,7 @@ dependencies = [
"image-webp",
"moxcms",
"num-traits",
"png",
"zune-core",
"zune-jpeg",
]
@@ -671,9 +672,11 @@ dependencies = [
name = "impeccable-html"
version = "0.1.5"
dependencies = [
"base64",
"cssparser",
"ego-tree",
"html5ever",
"image",
"impeccable-common",
"impeccable-core",
"impeccable-detect",
+1 -1
View File
@@ -465,7 +465,7 @@ npx impeccable ignores add-value overused-font Inter --reason "Brand font"
The detector catches 61 deterministic issues across AI slop (side-tab borders, purple gradients, bounce easing, dark glows) and general design quality (line length, cramped padding, small touch targets, skipped headings, and more).
Human-readable findings are diagnostics written to stderr, so redirect them with `2> findings.txt`. Use `--json` for machine-readable results on stdout. Exit `0` means the scan completed without primary findings, exit `2` means it completed with primary findings, and exit `1` means at least one requested target could not be scanned; operational failure takes precedence for a partial multi-target scan. URL scans inspect the rendered DOM, computed layout, and accessible linked stylesheets; browser security still prevents reading cross-origin CSS without CORS. A clean detector run is evidence, not proof of visual or accessibility quality: it does not replace inspecting the rendered experience across relevant viewports.
Human-readable findings are diagnostics written to stderr, so redirect them with `2> findings.txt`. Use `--json` for machine-readable results on stdout. Exit `0` means the scan completed without primary findings, exit `2` means it completed with primary findings, and exit `1` means at least one requested target could not be scanned; operational failure takes precedence for a partial multi-target scan. URL scans inspect the rendered DOM, computed layout, and accessible linked stylesheets; browser security still prevents reading cross-origin CSS without CORS. File scans read the pixels of local background images behind text, so white copy on a near-white photo fails a CI gate without a browser; a remote image is never fetched. A clean detector run is evidence, not proof of visual or accessibility quality: it does not replace inspecting the rendered experience across relevant viewports.
By default, `detect` respects the same `.impeccable/config.json` and `.impeccable/config.local.json` detector config as the design hook: `detector.ignoreRules`, `detector.ignoreFiles`, `detector.ignoreValues`, and `detector.designSystem.enabled`. Hook lifecycle settings such as `hook.enabled` only affect automatic hook execution.
+84 -11
View File
@@ -30,9 +30,39 @@ static LAUNCHER_HOOK_MARKER: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"skills/impeccable/scripts/impeccable(?:\.cmd|\.exe)?["']?\s+hook(?:-before-edit|-probe|-after-edit|-stop)?(?:\s|$|["'&|;)])"#).unwrap()
});
/// User-scope Windows commands embed a JSON-quoted path whose backslashes are
/// doubled in the command string (#784). #604's single `\`→`/` replace is not
/// enough: `\\` becomes `//`, which breaks `skills/impeccable` matching.
/// A leading `//` after a quote (or at the start of the string) is a UNC
/// prefix and stays two slashes, so doctor still probes `//server/share/...`.
fn normalize_hook_separators(command: &str) -> String {
let mut out = String::with_capacity(command.len());
let mut chars = command.chars().peekable();
let mut prev: Option<char> = None;
while let Some(ch) = chars.next() {
if ch != '\\' && ch != '/' {
out.push(ch);
prev = Some(ch);
continue;
}
let mut n = 1usize;
while matches!(chars.peek(), Some('\\' | '/')) {
chars.next();
n += 1;
}
out.push('/');
if n >= 2 && matches!(prev, None | Some('"' | '\'')) {
out.push('/');
}
prev = Some('/');
}
out
}
/// True when `command` invokes an Impeccable hook in either generation's spelling.
pub fn is_impeccable_hook_command(command: &str) -> bool {
LEGACY_HOOK_SCRIPT_MARKERS.iter().any(|m| command.contains(m)) || LAUNCHER_HOOK_MARKER.is_match(command)
let command = normalize_hook_separators(command);
LEGACY_HOOK_SCRIPT_MARKERS.iter().any(|m| command.contains(m)) || LAUNCHER_HOOK_MARKER.is_match(&command)
}
/// True when `command` invokes an Impeccable hook in the launcher generation
@@ -40,7 +70,7 @@ pub fn is_impeccable_hook_command(command: &str) -> bool {
/// one: install/update use this to decide which manifests still need
/// migrating to the launcher form.
pub fn is_launcher_hook_command(command: &str) -> bool {
LAUNCHER_HOOK_MARKER.is_match(command)
LAUNCHER_HOOK_MARKER.is_match(&normalize_hook_separators(command))
}
/// The launcher-era markers `context` and `doctor` treat as the design hook
@@ -54,9 +84,10 @@ static LAUNCHER_DESIGN_HOOK: Lazy<Regex> = Lazy::new(|| {
/// `hook-before-edit`) in either spelling; the JS `context.mjs` scan and
/// `staleness-deep` HOOK_SCRIPT_MARKERS both meant exactly these two.
pub fn is_design_hook_command(command: &str) -> bool {
let command = normalize_hook_separators(command);
command.contains("skills/impeccable/scripts/hook.mjs")
|| command.contains("skills/impeccable/scripts/hook-before-edit.mjs")
|| LAUNCHER_DESIGN_HOOK.is_match(command)
|| LAUNCHER_DESIGN_HOOK.is_match(&command)
}
/// True when `command` runs the design hook (`hook` / `hook-before-edit`) in
@@ -66,7 +97,7 @@ pub fn is_design_hook_command(command: &str) -> bool {
/// so the hook is dead and `MANUAL_DETECTOR_REQUIRED` must fire until an
/// install/update repairs it.
pub fn is_launcher_design_hook_command(command: &str) -> bool {
LAUNCHER_DESIGN_HOOK.is_match(command)
LAUNCHER_DESIGN_HOOK.is_match(&normalize_hook_separators(command))
}
/// The shell token that names the hook program inside `command`, for
@@ -74,7 +105,11 @@ pub fn is_launcher_design_hook_command(command: &str) -> bool {
/// path in the binary form. `None` when the command carries no marker or
/// the token cannot be isolated (a `'\''` escape sequence, for instance).
pub fn hook_program_token(command: &str) -> Option<String> {
if !is_design_hook_command(command) {
if command.contains("'\\''") {
return None;
}
let command = normalize_hook_separators(command);
if !is_design_hook_command(&command) {
return None;
}
static QUOTED: Lazy<Regex> = Lazy::new(|| {
@@ -86,16 +121,13 @@ pub fn hook_program_token(command: &str) -> Option<String> {
static BARE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"([^\s"'|&;()]*skills/impeccable/scripts/(?:hook(?:-before-edit)?\.mjs|impeccable(?:\.cmd|\.exe)?))"#).unwrap()
});
if let Some(m) = QUOTED.captures(command) {
if let Some(m) = QUOTED.captures(&command) {
return Some(m[1].to_string());
}
if command.contains("'\\''") {
return None;
}
if let Some(m) = SINGLE.captures(command) {
if let Some(m) = SINGLE.captures(&command) {
return Some(m[1].to_string());
}
BARE.captures(command).map(|m| m[1].to_string())
BARE.captures(&command).map(|m| m[1].to_string())
}
#[cfg(test)]
@@ -183,4 +215,45 @@ mod tests {
assert_eq!(hook_program_token("'/x/it'\\''s/.claude/skills/impeccable/scripts/impeccable' hook"), None);
assert_eq!(hook_program_token("echo hi"), None);
}
#[test]
fn recognizes_json_escaped_windows_launcher_path() {
let launcher = r"C:\Users\alice\.claude\skills\impeccable\scripts\impeccable";
let quoted = serde_json::to_string(launcher).unwrap();
let cmd = format!("[ ! -f {quoted} ] || {quoted} hook");
assert!(is_impeccable_hook_command(&cmd), "{cmd}");
assert!(is_launcher_hook_command(&cmd), "{cmd}");
assert!(is_design_hook_command(&cmd), "{cmd}");
assert_eq!(
hook_program_token(&cmd).as_deref(),
Some("C:/Users/alice/.claude/skills/impeccable/scripts/impeccable")
);
}
#[test]
fn recognizes_single_backslash_windows_path() {
let cmd = r#"[ ! -f "C:\Users\alice\.claude\skills\impeccable\scripts\impeccable" ] || "C:\Users\alice\.claude\skills\impeccable\scripts\impeccable" hook"#;
assert!(is_impeccable_hook_command(cmd), "{cmd}");
assert!(is_launcher_hook_command(cmd), "{cmd}");
assert!(is_design_hook_command(cmd), "{cmd}");
}
#[test]
fn preserves_unc_prefix_in_program_token() {
let launcher = r"\\server\share\.claude\skills\impeccable\scripts\impeccable";
let quoted = serde_json::to_string(launcher).unwrap();
let json_escaped = format!("[ ! -f {quoted} ] || {quoted} hook");
assert!(is_impeccable_hook_command(&json_escaped), "{json_escaped}");
assert_eq!(
hook_program_token(&json_escaped).as_deref(),
Some("//server/share/.claude/skills/impeccable/scripts/impeccable")
);
let single = r#"[ ! -f "\\server\share\.claude\skills\impeccable\scripts\impeccable" ] || "\\server\share\.claude\skills\impeccable\scripts\impeccable" hook"#;
assert!(is_impeccable_hook_command(single), "{single}");
assert_eq!(
hook_program_token(single).as_deref(),
Some("//server/share/.claude/skills/impeccable/scripts/impeccable")
);
}
}
+4
View File
@@ -29,6 +29,9 @@
//! checkNumberedSectionLabels, checkEmDashOveruse, isRepeatedTextContainer.
//! Open (selector and tag lists, thresholds, the two text parsers):
//! `impeccable_foundation::rules::text`.
//! - `sampled_contrast`: the static engine's pixel-sampled verdict for text
//! over a `url()` background (#560): grid, decoration gate, wash and
//! compositing rules, the percentile verdict. No JS ancestor.
//!
//! Element/document adapters (`checkElement*`, `*DOM`, `*FromDoc`) are NOT in
//! core: the static ones live in the `html` crate against its DOM model, the
@@ -43,6 +46,7 @@ pub mod css_scan;
pub mod html_patterns;
pub mod measures;
pub mod rules;
pub mod sampled_contrast;
pub mod text_rules;
#[cfg(feature = "vectors")]
+19 -10
View File
@@ -135,21 +135,30 @@ fn is_heading_123(tag: &str) -> bool {
matches!(tag, "h1" | "h2" | "h3")
}
/// The `SAFE_TAGS` skip at the top of `checkColors`: a link, control, or
/// quote is measured only when it paints its own surface. Shared with the
/// sampled-contrast path so both skip the same elements.
pub fn safe_tag_unstyled(opts: &ColorOpts) -> bool {
if !set_has(SAFE_TAGS, opts.tag.as_str()) {
return false;
}
let bg_image = opts.bg_image.as_deref().unwrap_or("");
let own_bg = opts
.bg_color
.map_or(false, |c| c.a.map_or(false, |a| a > 0.5));
let own_gradient = !bg_image.is_empty() && GRADIENT_CI.is_match(bg_image);
let is_styled_control =
opts.has_direct_text && (own_bg || own_gradient) && opts.font_size >= 9.0;
!is_styled_control
}
/// JS: checks.mjs#checkColors
pub fn check_colors(opts: &ColorOpts) -> Vec<RuleHit> {
let tag = opts.tag.as_str();
let bg_image = opts.bg_image.as_deref().unwrap_or("");
let bg_clip = opts.bg_clip.as_deref().unwrap_or("");
if set_has(SAFE_TAGS, tag) {
let own_bg = opts
.bg_color
.map_or(false, |c| c.a.map_or(false, |a| a > 0.5));
let own_gradient = !bg_image.is_empty() && GRADIENT_CI.is_match(bg_image);
let is_styled_control =
opts.has_direct_text && (own_bg || own_gradient) && opts.font_size >= 9.0;
if !is_styled_control {
return Vec::new();
}
if safe_tag_unstyled(opts) {
return Vec::new();
}
let mut findings = Vec::new();
+302
View File
@@ -0,0 +1,302 @@
//! Pixel-sampled contrast for text over a `url()` background image in the
//! static engine (#560). Outside a browser there is no CORS and no canvas
//! taint, so the engine reads the image itself; what it cannot know is
//! layout. The verdict is therefore coarse: a fixed grid over the whole
//! image, and a finding only when nearly all of it fails. A photo with a dark
//! third under the headline passes; white text on a near-white texture does
//! not, wherever the text sits.
//!
//! This module is the decisions: grid geometry, the decoration gate, the
//! wash and compositing rules, the percentile verdict and its label. Reading
//! and decoding the image is the html crate's job.
use crate::checks::rules::{safe_tag_unstyled, ColorOpts, RuleHit};
use crate::color::{color_to_hex, composite_color_over, contrast_ratio, Rgba};
use crate::constants::{WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX};
use crate::js::{number_to_string, parse_float, to_fixed};
/// Grid points per axis: 6x6 = 36 samples, sampling rather than scanning.
const GRID: usize = 6;
/// A `no-repeat` layer painted smaller than this on a non-repeating axis is
/// an icon, badge, or rule, not the ground the text sits on.
const DECORATION_MAX_PX: f64 = 160.0;
/// The share of the grid that has to resolve to a color before a verdict.
const MIN_SAMPLE_SHARE: f64 = 0.75;
/// The finding fires when this share of the samples fails: the text is
/// somewhere on the image, so only a near-uniform failure is a failure.
const FAIL_PERCENTILE: f64 = 0.9;
/// The color rule's own gates, so the sampler never fires where
/// `check_colors` would not have measured a resolved background.
pub fn applies(opts: &ColorOpts) -> bool {
opts.has_direct_text
&& opts.text_color.is_some()
&& !opts.is_emoji_only
&& opts.bg_clip.as_deref() != Some("text")
&& !safe_tag_unstyled(opts)
}
/// The sample positions over a `width` x `height` raster: cell centers of
/// the `GRID`, row-major.
pub fn grid_points(width: usize, height: usize) -> Vec<(usize, usize)> {
if width == 0 || height == 0 {
return Vec::new();
}
let at = |i: usize, extent: usize| -> usize {
let v = ((i as f64 + 0.5) / GRID as f64 * extent as f64).floor() as usize;
v.min(extent - 1)
};
let mut points = Vec::with_capacity(GRID * GRID);
for gy in 0..GRID {
for gx in 0..GRID {
points.push((at(gx, width), at(gy, height)));
}
}
points
}
/// Whether a layer with this `background-repeat` and `background-size`
/// (the layer's own comma-list entries) paints too little to be the ground:
/// a non-repeating axis whose painted extent is under
/// `DECORATION_MAX_PX`. Repeating, `cover`, `contain`, and percentage sizes
/// fill the box and are never decoration.
pub fn is_decorative_layer(repeat: &str, size: &str, intrinsic_w: f64, intrinsic_h: f64) -> bool {
let repeat = repeat.trim().to_ascii_lowercase();
let tokens: Vec<&str> = repeat.split_whitespace().collect();
let (repeat_x, repeat_y) = match tokens.as_slice() {
["no-repeat"] => (false, false),
["repeat-x"] => (true, false),
["repeat-y"] => (false, true),
[x, y] => (*x != "no-repeat", *y != "no-repeat"),
_ => (true, true),
};
if repeat_x && repeat_y {
return false;
}
let size = size.trim().to_ascii_lowercase();
if size == "cover" || size == "contain" || size.contains('%') {
return false;
}
let tokens: Vec<&str> = size.split_whitespace().collect();
let px = |t: Option<&&str>| t.filter(|t| t.ends_with("px")).map(|t| parse_float(t));
// An `auto` (or absent) axis follows the other at the image's aspect
// ratio, as in CSS: `40px`, `40px auto`, and `auto 40px` all scale.
let (w, h) = match (px(tokens.first()), px(tokens.get(1))) {
(Some(w), Some(h)) => (w, h),
(Some(w), None) if intrinsic_w > 0.0 => (w, intrinsic_h * (w / intrinsic_w)),
(None, Some(h)) if intrinsic_h > 0.0 => (intrinsic_w * (h / intrinsic_h), h),
(Some(w), None) => (w, intrinsic_h),
(None, Some(h)) => (intrinsic_w, h),
(None, None) => (intrinsic_w, intrinsic_h),
};
(!repeat_x && w < DECORATION_MAX_PX) || (!repeat_y && h < DECORATION_MAX_PX)
}
/// A translucent gradient whose stops are all one color is a tint over the
/// image and composites exactly; a gradient that varies is a scrim placed
/// under the text on purpose, and without layout the engine cannot say
/// which stop the text sits on. `None` for the varying case.
pub fn uniform_wash(stops: &[Rgba]) -> Option<Rgba> {
let first = *stops.first()?;
let same = |s: &Rgba| {
s.r == first.r
&& s.g == first.g
&& s.b == first.b
&& (s.alpha_or_one() - first.alpha_or_one()).abs() < 1e-9
};
stops.iter().all(same).then_some(first)
}
/// One grid sample's ground color: the pixel, composited over `under` when
/// it is translucent, then under every `overlays` layer (top to bottom, as
/// the walk collected them). `None` when a translucent pixel has nothing
/// known beneath it.
pub fn composite_sample(pixel: Rgba, under: Option<Rgba>, overlays: &[Rgba]) -> Option<Rgba> {
let mut color = if pixel.alpha_or_one() >= 0.99 {
Rgba::new(pixel.r, pixel.g, pixel.b, 1.0)
} else {
composite_color_over(&pixel, &under?)
};
for overlay in overlays.iter().rev() {
color = composite_color_over(overlay, &color);
}
Some(color)
}
/// The verdict over the resolved `samples` of a grid of `grid_total` points,
/// against the text of `opts`. `ground` names the image in the snippet (its
/// file name, or `data:image/png`).
pub fn sampled_contrast(
opts: &ColorOpts,
ground: &str,
samples: &[Rgba],
grid_total: usize,
) -> Option<RuleHit> {
if !applies(opts) {
return None;
}
let text = opts.text_color?;
let needed = ((grid_total as f64) * MIN_SAMPLE_SHARE).ceil().max(1.0) as usize;
if samples.len() < needed {
return None;
}
let mut ratios: Vec<f64> = samples
.iter()
.map(|bg| {
let fg = if text.alpha_or_one() < 1.0 {
composite_color_over(&text, bg)
} else {
text
};
contrast_ratio(&fg, bg)
})
.collect();
ratios.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = ratios.len();
let pick = |share: f64| ratios[((share * n as f64).floor() as usize).min(n - 1)];
let measured = pick(FAIL_PERCENTILE);
let median = pick(0.5);
let is_large_text = opts.font_size >= WCAG_LARGE_TEXT_PX
|| (opts.font_size >= WCAG_LARGE_BOLD_TEXT_PX && opts.font_weight >= 700.0);
let threshold = if is_large_text { 3.0 } else { 4.5 };
if measured >= threshold {
return None;
}
let ratio_label = if to_fixed(measured, 1) == to_fixed(threshold, 1) {
to_fixed(measured, 2)
} else {
to_fixed(measured, 1)
};
Some(RuleHit::new(
"low-contrast",
format!(
"sampled (coarse) {}:1 (need {}:1) — text {} on {}; p90 of {} samples, median {}:1",
ratio_label,
number_to_string(threshold),
color_to_hex(Some(&text)),
ground,
n,
to_fixed(median, 1)
),
))
}
#[cfg(test)]
mod tests {
use super::*;
fn rgba(r: f64, g: f64, b: f64, a: f64) -> Rgba {
Rgba::new(r, g, b, a)
}
fn opts(text: Rgba, font_size: f64) -> ColorOpts {
ColorOpts {
tag: "p".into(),
text_color: Some(text),
font_size,
font_weight: 400.0,
has_direct_text: true,
..Default::default()
}
}
#[test]
fn grid_covers_cell_centers_and_clamps() {
let points = grid_points(96, 64);
assert_eq!(points.len(), 36);
assert_eq!(points[0], (8, 5));
assert_eq!(points[35], (88, 58));
assert_eq!(grid_points(1, 1), vec![(0, 0); 36]);
assert!(grid_points(0, 10).is_empty());
}
#[test]
fn decoration_gate() {
assert!(is_decorative_layer("no-repeat", "auto", 24.0, 24.0));
assert!(is_decorative_layer("no-repeat", "40px", 1200.0, 800.0));
assert!(is_decorative_layer("no-repeat", "40px auto", 1200.0, 800.0));
assert!(is_decorative_layer("no-repeat", "auto 40px", 1200.0, 800.0));
assert!(!is_decorative_layer(
"no-repeat",
"auto 800px",
1200.0,
800.0
));
assert!(is_decorative_layer("repeat-y", "auto", 8.0, 400.0));
assert!(is_decorative_layer("repeat-x", "auto", 1200.0, 80.0));
assert!(!is_decorative_layer("no-repeat", "cover", 24.0, 24.0));
assert!(!is_decorative_layer("no-repeat", "100% auto", 24.0, 24.0));
assert!(!is_decorative_layer("no-repeat", "auto", 1600.0, 900.0));
assert!(!is_decorative_layer("repeat", "auto", 4.0, 4.0));
assert!(!is_decorative_layer("", "", 16.0, 16.0));
assert!(!is_decorative_layer(
"repeat no-repeat",
"auto",
1200.0,
300.0
));
assert!(is_decorative_layer(
"repeat no-repeat",
"auto",
1200.0,
30.0
));
}
#[test]
fn washes_and_compositing() {
let tint = rgba(0.0, 0.0, 0.0, 0.4);
assert_eq!(uniform_wash(&[tint, tint]), Some(tint));
assert_eq!(uniform_wash(&[tint, rgba(0.0, 0.0, 0.0, 0.0)]), None);
assert_eq!(uniform_wash(&[]), None);
let light = rgba(240.0, 240.0, 240.0, 1.0);
assert_eq!(composite_sample(light, None, &[]), Some(light));
let dimmed = composite_sample(light, None, &[tint]).unwrap();
assert_eq!((dimmed.r, dimmed.g, dimmed.b), (144.0, 144.0, 144.0));
let translucent = rgba(240.0, 240.0, 240.0, 0.25);
assert_eq!(composite_sample(translucent, None, &[]), None);
let over_dark =
composite_sample(translucent, Some(rgba(20.0, 20.0, 20.0, 1.0)), &[]).unwrap();
assert_eq!((over_dark.r, over_dark.a), (75.0, Some(1.0)));
}
#[test]
fn verdict_needs_a_near_uniform_failure() {
let white = rgba(253.0, 253.0, 253.0, 1.0);
let light = rgba(243.0, 239.0, 230.0, 1.0);
let dark = rgba(26.0, 24.0, 22.0, 1.0);
let all_light: Vec<Rgba> = vec![light; 36];
let hit = sampled_contrast(&opts(white, 16.0), "hero.png", &all_light, 36).unwrap();
assert_eq!(hit.id, "low-contrast");
assert_eq!(
hit.snippet,
"sampled (coarse) 1.1:1 (need 4.5:1) — text #fdfdfd on hero.png; p90 of 36 samples, median 1.1:1"
);
// Half the image is dark: the text may well sit there.
let split: Vec<Rgba> = (0..36)
.map(|i| if i % 2 == 0 { light } else { dark })
.collect();
assert!(sampled_contrast(&opts(white, 16.0), "hero.png", &split, 36).is_none());
// Three dark samples out of 36 are not enough to save it.
let mostly: Vec<Rgba> = (0..36).map(|i| if i < 3 { dark } else { light }).collect();
assert!(sampled_contrast(&opts(white, 16.0), "hero.png", &mostly, 36).is_some());
// Too few resolved samples: no verdict.
assert!(sampled_contrast(&opts(white, 16.0), "hero.png", &all_light[..20], 36).is_none());
// Large text lowers the bar, and the snippet says so.
let mid = rgba(150.0, 150.0, 150.0, 1.0);
let on_mid: Vec<Rgba> = vec![mid; 36];
assert!(
sampled_contrast(&opts(white, 28.0), "hero.png", &on_mid, 36)
.unwrap()
.snippet
.contains("(need 3:1)")
);
// Safe tags and gradient-clipped text never sample.
let mut anchor = opts(white, 16.0);
anchor.tag = "a".into();
assert!(sampled_contrast(&anchor, "hero.png", &all_light, 36).is_none());
let mut clipped = opts(white, 16.0);
clipped.bg_clip = Some("text".into());
assert!(sampled_contrast(&clipped, "hero.png", &all_light, 36).is_none());
}
}
+5
View File
@@ -26,6 +26,11 @@ html5ever = "0.39"
selectors = "0.38"
cssparser = "0.37"
thiserror = { workspace = true }
# Decoding the background images behind text for the sampled-contrast path
# (#560): pure-Rust decoders only (the comp crate's set plus `png`), so the
# wasm `detect` build keeps working and nothing native is linked.
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "webp"] }
base64 = "0.22"
[dev-dependencies]
impeccable-core = { workspace = true, features = ["vectors"] }
+49 -5
View File
@@ -6,11 +6,13 @@
//! and the computed style and hands plain data over.
use crate::background::{
a_ge, a_gt, read_own_background_color, resolve_background, resolve_background_info,
resolve_border_radius_px, resolve_gradient_stops, sv, sv_opt, CustomPropMap,
a_ge, a_gt, find_image_ground, read_own_background_color, resolve_background,
resolve_background_info, resolve_border_radius_px, resolve_gradient_stops, sv, sv_opt,
CustomPropMap,
};
use crate::cascade::StyleValues;
use crate::dom::{StaticDocument, StaticElement};
use crate::image_sampling::{ground_label, ImageSampler};
use crate::quality::{collapse_ws, pf0, resolve_font_size_px};
use impeccable_core::checks::measures::{
self, border_colors_from_style, border_widths_from_style, check_gpt_thin_border_wide_shadow,
@@ -24,13 +26,14 @@ use impeccable_core::checks::rules::{
GlowOpts, HeroEyebrowOpts, HoverContrastOpts, IconTileOpts, ItalicSerifOpts, KickerCandidate,
MotionOpts, RuleHit, Sides,
};
use impeccable_core::checks::sampled_contrast;
use impeccable_core::checks::text_rules::{
check_numbered_section_labels, is_kicker_candidate, is_numbered_section_label_candidate,
parse_numbered_label_text, KickerCandidateInput, NumberedLabelCandidate,
NumberedLabelCandidateInput, HEADING_TAGS, KICKER_CARD_CONTEXT_SELECTOR, KICKER_SKIP_SELECTOR,
POSITIONED_CHILD_INTERACTIVE_SELECTOR,
};
use impeccable_core::color::{composite_color_over, parse_any_color, parse_rgb};
use impeccable_core::color::{composite_color_over, parse_any_color, parse_rgb, Rgba};
use impeccable_core::js::{self, parse_float, parse_int};
use impeccable_core::js_ext_a::num_truthy;
use impeccable_core::js_ext_b::slice_utf16_prefix;
@@ -461,11 +464,15 @@ pub fn check_element_borders(
}
/// JS: checks.mjs#checkElementColors(el, style, tag, window, customPropMap, hasAnchorInheritRule)
///
/// `images` serves the one branch the JS never had: text whose ground is a
/// `url()` layer is measured against the image's pixels (#560).
pub fn check_element_colors(
el: &StaticElement<'_>,
style: &StyleValues,
tag: &str,
custom_props: CustomPropMap<'_>,
images: &ImageSampler,
) -> Vec<RuleHit> {
if sv_opt(style, "visibility") == Some("hidden") {
return Vec::new();
@@ -539,7 +546,7 @@ pub fn check_element_colors(
sv(style, "backgroundClip")
}
};
check_colors(&ColorOpts {
let opts = ColorOpts {
tag: tag.to_string(),
text_color,
bg_color: own_bg,
@@ -557,7 +564,44 @@ pub fn check_element_colors(
bg_image: Some(sv(style, "backgroundImage").to_string()),
class_list: Some(el.class_name().to_string()),
detector_is_browser: false,
})
};
let mut findings = check_colors(&opts);
if surface_unresolved {
findings.extend(sampled_image_contrast(el, &opts, images));
}
findings
}
/// The sampled-contrast path (#560): when the analytic walk gave up at a
/// `url()` layer, read that image and measure the text against a grid of
/// its pixels. Every gate that needs no IO runs first, so a container
/// without direct text never touches the file.
fn sampled_image_contrast(
el: &StaticElement<'_>,
opts: &ColorOpts,
images: &ImageSampler,
) -> Option<RuleHit> {
if !sampled_contrast::applies(opts) {
return None;
}
let ground = find_image_ground(el)?;
let raster = images.load(&ground.url)?;
if sampled_contrast::is_decorative_layer(
&ground.repeat,
&ground.size,
raster.intrinsic_width as f64,
raster.intrinsic_height as f64,
) {
return None;
}
let points = sampled_contrast::grid_points(raster.width as usize, raster.height as usize);
let samples: Vec<Rgba> = points
.iter()
.filter_map(|&(x, y)| {
sampled_contrast::composite_sample(raster.pixel(x, y), ground.under, &ground.overlays)
})
.collect();
sampled_contrast::sampled_contrast(opts, &ground_label(&ground.url), &samples, points.len())
}
/// JS: checks.mjs#checkElementHoverContrast(el, style, tag, window)
+150
View File
@@ -4,9 +4,11 @@
//! `compositeGradientStops`, `resolveBorderRadiusPx`), static-engine
//! branches only (`DETECTOR_IS_BROWSER === false`).
use crate::cascade::csstree::strings::decode_url;
use crate::cascade::StyleValues;
use crate::dom::StaticElement;
use impeccable_core::checks::measures::{parse_color_resolved, parse_radius_to_px, CustomProps};
use impeccable_core::checks::sampled_contrast::uniform_wash;
use impeccable_core::color::{
composite_color_over, is_no_paint_color_value, parse_any_color, parse_gradient_colors,
parse_rgb, split_top_level_commas, Rgba,
@@ -331,3 +333,151 @@ pub fn composite_gradient_stops(
pub fn resolve_border_radius_px(style: &StyleValues, width_px: f64) -> f64 {
parse_radius_to_px(sv_opt(style, "borderRadius"), width_px).unwrap_or(0.0)
}
// ─── the image ground (#560) ────────────────────────────────────────────────
/// The image a text element sits on when the walk ends at a `url()` layer:
/// what the sampled-contrast path reads instead of skipping.
#[derive(Debug, Clone, PartialEq)]
pub struct ImageGround {
/// The layer's `url()` argument, unescaped and unquoted.
pub url: String,
/// The layer's own `background-repeat` and `background-size` entries.
pub repeat: String,
pub size: String,
/// Translucent paint between the text and the image, top to bottom:
/// tinted surfaces up the chain and uniform gradient washes.
pub overlays: Vec<Rgba>,
/// What a translucent pixel composites over, when the walk can say.
pub under: Option<Rgba>,
}
/// One entry of a comma-separated background list, with the CSS wraparound
/// for a list shorter than the image list.
fn layer_entry(value: &str, index: usize) -> String {
let parts = split_top_level_commas(value);
if parts.is_empty() {
return String::new();
}
js::trim(&parts[index % parts.len()]).to_string()
}
/// The `url()` argument of one background layer: `url(x\ y.png)` from the
/// css-tree generator, `url("x y.png")` from a style attribute. A shorthand
/// layer may carry other tokens around the call (`#000 url(x)center/cover`),
/// so only the call itself is decoded.
fn layer_url(layer: &str) -> String {
let Some(call) = URL_CALL_RE.find(layer) else {
return String::new();
};
let open = call.end();
let mut depth = 1usize;
let mut quote: Option<char> = None;
let mut close = layer.len();
for (i, ch) in layer[open..].char_indices() {
match quote {
Some(q) if ch == q => quote = None,
Some(_) => {}
None if ch == '"' || ch == '\'' => quote = Some(ch),
None if ch == '(' => depth += 1,
None if ch == ')' => {
depth -= 1;
if depth == 0 {
close = open + i + 1;
break;
}
}
None => {}
}
}
let decoded = decode_url(&format!("url({}", &layer[open..close]));
let trimmed = js::trim(&decoded);
let unquoted = trimmed
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.or_else(|| {
trimmed
.strip_prefix('\'')
.and_then(|s| s.strip_suffix('\''))
})
.unwrap_or(trimmed);
unquoted.to_string()
}
/// The mirror of [`resolve_background_info`]'s walk for the case it reports
/// as unresolved because a `url()` layer is the ground: that layer plus
/// everything painted between the text and it. `None` whenever the analytic
/// walk would have resolved a color or given up for another reason (an
/// opaque gradient, an unparseable color, a varying scrim).
pub fn find_image_ground(el: &StaticElement<'_>) -> Option<ImageGround> {
let mut current = Some(*el);
let mut overlays: Vec<Rgba> = Vec::new();
while let Some(cur) = current {
let style = cur.style();
let mut bg = read_cascade_background_color(&cur, style, None);
if (bg.is_none() || bg.as_ref().is_some_and(|c| a_lt(c, 0.1)))
&& CURRENTCOLOR_RE.is_match(js::trim(sv(style, "backgroundColor")))
{
let color = sv_opt(style, "color");
bg = parse_rgb(color).or_else(|| parse_color_resolved(color, None));
}
if bg.as_ref().is_some_and(|c| a_ge(c, 0.99)) {
return None;
}
if bg.is_none() && !is_no_paint_color_value(sv_opt(style, "backgroundColor")) {
return None;
}
let bg_image = sv(style, "backgroundImage");
let layers: Vec<String> = if bg_image.is_empty() || bg_image == "none" {
Vec::new()
} else {
split_top_level_commas(bg_image)
};
for (index, layer) in layers.iter().enumerate() {
let layer = js::trim(layer);
// A shorthand layer may put color, position, size, or repeat
// tokens before the image call; the call is what matters.
let is_gradient = GRADIENT_CALL_RE.is_match(layer);
if !is_gradient && URL_CALL_RE.is_match(layer) {
// A layer beneath the image is unknown paint; otherwise the
// element's own color over its parent's ground, white at the
// root like the analytic walk.
let under = if index + 1 < layers.len() {
None
} else {
let parent_ground = match cur.parent_element() {
Some(p) => resolve_background(&p, None),
None => Some(Rgba::new(255.0, 255.0, 255.0, 1.0)),
};
match bg {
Some(c) if a_gt(&c, 0.1) => {
parent_ground.map(|p| composite_color_over(&c, &p))
}
_ => parent_ground,
}
};
return Some(ImageGround {
url: layer_url(layer),
repeat: layer_entry(sv(style, "backgroundRepeat"), index),
size: layer_entry(sv(style, "backgroundSize"), index),
overlays,
under,
});
}
if is_gradient {
let stops = parse_gradient_colors(Some(layer));
if stops.is_empty() || stops.iter().all(|s| s.alpha_or_one() >= 0.99) {
return None;
}
overlays.push(uniform_wash(&stops)?);
continue;
}
return None;
}
if let Some(c) = bg.filter(|c| a_gt(c, 0.1)) {
overlays.push(c);
}
current = cur.parent_element();
}
None
}
+177 -6
View File
@@ -10,10 +10,11 @@
use super::checks_shim::CustomProps;
use super::{
apply_static_declaration, collect_static_css_rules, compare_static_priority,
is_static_inherited_prop, make_default_style, normalize_static_css_value,
parse_static_style_attribute, static_default_style, CssRule, DeclMeta, SpecifiedDecl,
SpecifiedStore, StyleValues, STATIC_DEFAULT_STYLE,
apply_static_declaration, apply_static_longhand, background_longhands,
collect_static_css_rules, compare_static_priority, is_static_inherited_prop,
make_default_style, normalize_static_css_value, parse_static_style_attribute,
static_default_style, CssRule, DeclMeta, SpecifiedDecl, SpecifiedStore, StyleValues,
STATIC_DEFAULT_STYLE,
};
use crate::dom::StaticDocument;
use crate::profile::{self, Meta, ProfileSink};
@@ -39,7 +40,7 @@ static REMOTE_HREF_RE: Lazy<Regex> =
/// Cache-busting (styles.css?v=3) and root-relative (/static/app.css) hrefs
/// must not resolve as OS-absolute paths; otherwise the whole stylesheet is
/// invisible to every element-level check.
fn resolve_linked_css_path(file_dir: &str, href: &str) -> String {
pub(crate) fn resolve_linked_css_path(file_dir: &str, href: &str) -> String {
let stripped = href.split(['?', '#']).next().unwrap_or("");
let root_relative = stripped.starts_with('/') && !stripped.starts_with("//");
if !root_relative {
@@ -117,7 +118,15 @@ pub fn collect_static_css_text(
|| std::fs::read(&css_path),
);
match read {
Ok(bytes) => style_texts.push(String::from_utf8_lossy(&bytes).into_owned()),
Ok(bytes) => {
let text = String::from_utf8_lossy(&bytes);
let sheet_dir = jsp::dirname(&css_path);
style_texts.push(if sheet_dir == file_dir_str {
text.into_owned()
} else {
rewrite_sheet_urls(&text, &sheet_dir, &file_dir_str)
});
}
Err(_) => {
if warned_missing_stylesheets.insert(css_path.clone()) {
if let Some(warn) = warn {
@@ -132,6 +141,162 @@ pub fn collect_static_css_text(
style_texts.join("\n")
}
// A quoted url cannot span a line and an unquoted one cannot hold
// whitespace, quotes, parens, braces, or semicolons (CSS syntax), so a
// stray `url(` can never pair with a `)` in a later rule.
static CSS_URL_RE: Lazy<Regex> = Lazy::new(|| {
// The leading group keeps `url(` from matching inside a longer
// identifier such as `myurl(`; the closure emits it unchanged.
Regex::new(
r#"(?i)(^|[^A-Za-z0-9_-])url\(\s*(?:"((?:[^"\\\n\r]|\\.)*)"|'((?:[^'\\\n\r]|\\.)*)'|([^)"'(\s{};]*))\s*\)"#,
)
.expect("CSS_URL_RE")
});
static URL_SCHEME_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[A-Za-z][A-Za-z0-9+.-]*:").expect("URL_SCHEME_RE"));
/// A relative `url()` in a stylesheet is relative to the sheet, and the
/// cascade sees one concatenated text, so a sheet inlined from another
/// directory has its relative urls rewritten to page-relative form here.
/// This is what lets the sampled-contrast path (#560) resolve the image the
/// winning declaration named. Root-relative, remote, `data:`, fragment, and
/// escaped urls are left as they are, and comments are copied through
/// untouched, so nothing inside one can reach the rules after it.
pub fn rewrite_sheet_urls(css: &str, sheet_dir: &str, page_dir: &str) -> String {
let mut out = String::with_capacity(css.len());
let mut cursor = 0usize;
for (start, end) in comment_spans(css) {
out.push_str(&rewrite_code_urls(&css[cursor..start], sheet_dir, page_dir));
out.push_str(&css[start..end]);
cursor = end;
}
out.push_str(&rewrite_code_urls(&css[cursor..], sheet_dir, page_dir));
out
}
/// The byte spans of every `/* */` comment in `css`, found the way the CSS
/// tokenizer finds them: a `/*` inside a quoted string or an unquoted
/// `url()` is content, not a comment opener, and an unclosed comment runs to
/// the end of the sheet. Every span boundary sits on an ASCII byte, so the
/// spans are valid `str` indices.
/// A byte that can continue a CSS identifier, so `myurl(` is a custom
/// function and not the `url(` token.
fn is_ident_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b >= 0x80
}
fn comment_spans(css: &str) -> Vec<(usize, usize)> {
#[derive(Clone, Copy, PartialEq)]
enum State {
Code,
Str(u8),
Url,
}
let bytes = css.as_bytes();
let mut spans = Vec::new();
let mut state = State::Code;
let mut i = 0usize;
while i < bytes.len() {
let b = bytes[i];
match state {
State::Code => {
if b == b'/' && bytes.get(i + 1) == Some(&b'*') {
let end = css[i + 2..]
.find("*/")
.map(|at| i + 2 + at + 2)
.unwrap_or(bytes.len());
spans.push((i, end));
i = end;
continue;
}
if b == b'"' || b == b'\'' {
state = State::Str(b);
} else if b.eq_ignore_ascii_case(&b'u')
&& css[i..]
.get(..4)
.is_some_and(|s| s.eq_ignore_ascii_case("url("))
&& !(i > 0 && is_ident_byte(bytes[i - 1]))
{
i += 4;
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
}
// A quoted argument is a string like any other; an
// unquoted one runs to the closing paren.
if i < bytes.len() && bytes[i] != b'"' && bytes[i] != b'\'' {
state = State::Url;
}
continue;
}
}
State::Str(quote) => {
if b == b'\\' {
i += 2;
continue;
}
// A raw newline ends a string in CSS (a bad-string token).
if b == quote || b == b'\n' || b == b'\r' {
state = State::Code;
}
}
State::Url => {
if b == b'\\' {
i += 2;
continue;
}
if b == b')' {
state = State::Code;
}
}
}
i += 1;
}
spans
}
fn rewrite_code_urls(css: &str, sheet_dir: &str, page_dir: &str) -> String {
CSS_URL_RE
.replace_all(css, |caps: &regex::Captures| {
let whole = caps.get(0).map(|m| m.as_str()).unwrap_or("");
let before = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let (target, quote) = match (caps.get(2), caps.get(3), caps.get(4)) {
(Some(m), _, _) => (m.as_str(), "\""),
(_, Some(m), _) => (m.as_str(), "'"),
(_, _, Some(m)) => (js::trim(m.as_str()), ""),
_ => return whole.to_string(),
};
let lower = js::to_lower_case(target);
if target.is_empty()
|| target.contains('\\')
|| target.starts_with('#')
|| target.starts_with('/')
|| lower.starts_with("data:")
|| URL_SCHEME_RE.is_match(target)
{
return whole.to_string();
}
let cut = target.find(['?', '#']).unwrap_or(target.len());
let (path, suffix) = target.split_at(cut);
// Pure POSIX string math on both directories: a CSS url is
// POSIX on every OS, and the win32 helpers would render a drive
// path (`D:\a\...`) differently from the url beside it.
let sheet = jsp::to_posix(sheet_dir);
let page = jsp::to_posix(page_dir);
let absolute = jsp::posix::resolve("/", &[&sheet, path]);
let relative = jsp::posix::relative("/", &page, &absolute);
if relative.is_empty() {
return whole.to_string();
}
let needs_quotes = quote.is_empty()
&& relative
.chars()
.any(|c| c.is_whitespace() || matches!(c, '(' | ')' | '"' | '\''));
let quote = if needs_quotes { "\"" } else { quote };
format!("{before}url({quote}{relative}{suffix}{quote})")
})
.into_owned()
}
static PSEUDO_RULE_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(&format!(
r"(?i)^(.+?){ws}*::?(?:before|after)$",
@@ -308,6 +473,9 @@ pub fn build_static_style_map(
inline: false,
};
apply_static_declaration(store, node, &decl.prop, &decl.value, &meta);
for (prop, value) in background_longhands(&decl.prop, &decl.value) {
apply_static_longhand(store, node, &prop, &value, &meta);
}
}
}
}
@@ -331,6 +499,9 @@ pub fn build_static_style_map(
inline: true,
};
apply_static_declaration(&mut specified, node, &decl.prop, &decl.value, &meta);
for (prop, value) in background_longhands(&decl.prop, &decl.value) {
apply_static_longhand(&mut specified, node, &prop, &value, &meta);
}
}
inline_order += 1000;
}
+33 -9
View File
@@ -187,15 +187,39 @@ pub fn apply_static_declaration<K: Hash + Eq>(
) {
let map = specified.map.entry(node).or_default();
for (expanded_prop, expanded_value) in expand_static_declaration(prop, value) {
let existing = map.get(&expanded_prop).map(|d| &d.meta);
if compare_static_priority(existing, meta) {
let next = SpecifiedDecl {
meta: meta.clone(),
prop: expanded_prop.clone(),
value: expanded_value,
};
map.insert(expanded_prop, next);
}
apply_expanded(map, &expanded_prop, &expanded_value, meta);
}
}
/// Stores one already-expanded longhand for `node` under the cascade's
/// priority rule, the way `apply_static_declaration` stores each pair the
/// shorthand expansion yields. For the longhands that ride beside that
/// expansion rather than inside it (`background_longhands`).
pub fn apply_static_longhand<K: Hash + Eq>(
specified: &mut SpecifiedStore<K>,
node: K,
prop: &str,
value: &str,
meta: &DeclMeta,
) {
let map = specified.map.entry(node).or_default();
apply_expanded(map, prop, value, meta);
}
fn apply_expanded(
map: &mut IndexMap<String, SpecifiedDecl>,
prop: &str,
value: &str,
meta: &DeclMeta,
) {
let existing = map.get(prop).map(|d| &d.meta);
if compare_static_priority(existing, meta) {
let next = SpecifiedDecl {
meta: meta.clone(),
prop: prop.to_string(),
value: value.to_string(),
};
map.insert(prop.to_string(), next);
}
}
+95
View File
@@ -193,6 +193,101 @@ pub fn parse_static_animation(value: &str) -> StaticAnimation {
}
}
static BG_REPEAT_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)^(?:repeat|no-repeat|repeat-x|repeat-y|space|round)$").expect("BG_REPEAT_RE")
});
static BG_SIZE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)^(?:auto|cover|contain|-?[0-9.]+[a-z%]*)$").expect("BG_SIZE_RE"));
/// The per-layer `background-repeat` and `background-size` lists of a
/// `background` shorthand (`url(a) center / cover no-repeat, url(b)` ->
/// `no-repeat, repeat` and `cover, auto`). The css-tree generator may glue
/// the first keyword to the image (`url(a)center/cover`), so a token that
/// holds a function call is read up to its closing paren and the rest kept.
fn parse_static_background_layers(value: &str) -> (String, String) {
let mut repeats: Vec<String> = Vec::new();
let mut sizes: Vec<String> = Vec::new();
for layer in split_css_list(value) {
let mut repeat: Vec<String> = Vec::new();
let mut size: Vec<String> = Vec::new();
let mut in_size = false;
for raw in split_css_tokens(&layer) {
let token = match raw.rfind(')') {
Some(end) if raw.contains('(') => raw[end + 1..].to_string(),
_ => raw,
};
if token.is_empty() {
in_size = false;
continue;
}
// `center/cover`, `center / cover`, or `center/ cover`: the size
// is what follows the slash, one or two values.
if let Some((_, after)) = token.split_once('/') {
in_size = true;
size.clear();
if !after.is_empty() && BG_SIZE_RE.is_match(after) {
size.push(js::to_lower_case(after));
}
continue;
}
if in_size && size.len() < 2 && BG_SIZE_RE.is_match(&token) {
size.push(js::to_lower_case(&token));
continue;
}
in_size = false;
if repeat.len() < 2 && BG_REPEAT_RE.is_match(&token) {
repeat.push(js::to_lower_case(&token));
}
}
repeats.push(if repeat.is_empty() {
"repeat".into()
} else {
repeat.join(" ")
});
sizes.push(if size.is_empty() {
"auto".into()
} else {
size.join(" ")
});
}
(repeats.join(", "), sizes.join(", "))
}
/// The `backgroundRepeat` / `backgroundSize` pairs a declaration sets. Not
/// part of `expand_static_declaration`, whose output the recorded vectors
/// pin; the cascade stores these beside it (`apply_static_longhand`) so the
/// sampled-contrast path (#560) can tell a tiled or cover image from a
/// no-repeat icon. Every `background` shorthand resets both to what it
/// names, the defaults when it names nothing, as in CSS; a CSS-wide keyword
/// passes through, and a bare `var()` value is left alone the way the
/// expansion leaves it.
pub fn background_longhands(prop: &str, value: &str) -> Vec<Expanded> {
let v = js::trim(value);
match js::to_lower_case(prop).as_str() {
"background" if VAR_ANYWHERE_RE.is_match(v) && !BG_IMAGE_RE.is_match(v) => Vec::new(),
"background" if CSS_WIDE_KEYWORD_RE.is_match(v) => {
let keyword = js::to_lower_case(v);
vec![
("backgroundRepeat".into(), keyword.clone()),
("backgroundSize".into(), keyword),
]
}
"background" => {
let (repeat, size) = parse_static_background_layers(v);
vec![
("backgroundRepeat".into(), repeat),
("backgroundSize".into(), size),
]
}
"background-repeat" if !v.is_empty() => vec![("backgroundRepeat".into(), v.to_string())],
"background-size" if !v.is_empty() => vec![("backgroundSize".into(), v.to_string())],
_ => Vec::new(),
}
}
static CSS_WIDE_KEYWORD_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)^(?:inherit|initial|unset|revert|revert-layer)$").expect("CSS_WIDE_KEYWORD_RE")
});
static BG_IMAGE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)gradient|url\(").expect("BG_IMAGE_RE"));
static BG_IMAGE_SPLIT_RE: Lazy<Regex> = Lazy::new(|| {
+10 -3
View File
@@ -20,6 +20,7 @@ use crate::adapters::{
use crate::background::{resolve_background, resolve_border_radius_px, sv};
use crate::cascade::{build_static_style_map, collect_static_css_text};
use crate::dom::{StaticDocument, StaticElement};
use crate::image_sampling::ImageSampler;
use crate::page::{
check_cream_palette, check_page_layout, check_repeated_container_text_from_doc,
check_static_page_typography,
@@ -119,14 +120,19 @@ const STATIC_ELEMENT_RULES: &[(&str, &str)] = &[
("radial-spotlight-glow", "*"),
];
fn run_rule(rule_id: &str, el: &StaticElement<'_>, tag: &str) -> Vec<RuleHit> {
fn run_rule(
rule_id: &str,
el: &StaticElement<'_>,
tag: &str,
images: &ImageSampler,
) -> Vec<RuleHit> {
let style = el.style();
match rule_id {
"border-rules" => {
let radius = resolve_border_radius_px(style, pf0(sv(style, "width")));
check_element_borders(tag, style, radius, el)
}
"color-rules" => check_element_colors(el, style, tag, None),
"color-rules" => check_element_colors(el, style, tag, None, images),
"hover-color-rules" => check_element_hover_contrast(el, style, tag),
"dark-glow" => {
let base = el.parent_element().unwrap_or(*el);
@@ -210,6 +216,7 @@ pub fn detect_html_source(
let css_text = collect_static_css_text(&doc, &file_dir, profile, fp, options.warn);
build_static_style_map(&mut doc, css_text.as_str(), profile, fp);
let doc = doc;
let images = ImageSampler::new(&file_dir.to_string_lossy());
let mut findings: Vec<Finding> = Vec::new();
let mk = |id: &str, snippet: &str| try_finding(id, fp, snippet, 0.0);
@@ -222,7 +229,7 @@ pub fn detect_html_source(
profile,
Meta::new("element", rule_id, fp),
|h: &RuleHit| h.id.as_str(),
|| run_rule(rule_id, el, &tag),
|| run_rule(rule_id, el, &tag, &images),
);
for h in hits {
if scoped_ignore_active(el, &h.id) {
+285
View File
@@ -0,0 +1,285 @@
//! The pixels behind image-backed text for the static engine (#560). A
//! `url()` resolves to bytes from a local file relative to the page (linked
//! stylesheets have their urls rewritten to page-relative form when they are
//! inlined, see `rewrite_sheet_urls`) or from a base64 data URI, never from
//! the network; the pure-Rust decoders turn them into a raster no larger than
//! the browser overlay's 640px canvas; and the raster is cached for the rest
//! of the process, so a directory scan decodes each hero once. Anything
//! unreadable, remote, oversized, or undecodable is `None`, and the caller
//! keeps today's skip. The same byte budget bounds a file on disk and a data
//! URI's payload, checked before anything is copied or decoded.
use crate::cascade::resolve_linked_css_path;
use base64::Engine;
use impeccable_common::jsp;
use impeccable_core::color::Rgba;
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::io::Cursor;
use std::rc::Rc;
/// The browser overlay draws to a canvas no larger than this on a side.
const MAX_RASTER_SIDE: u32 = 640;
/// Files above this are not read: a hero is a few megabytes, and the hook
/// runs on every edit.
const MAX_FILE_BYTES: u64 = 24 * 1024 * 1024;
/// The base64 payload length that decodes to [`MAX_FILE_BYTES`], so a data
/// URI is refused before its payload is copied or decoded.
const MAX_DATA_URI_CHARS: usize = (MAX_FILE_BYTES as usize / 3) * 4 + 4;
const MAX_IMAGE_SIDE: u32 = 8192;
const MAX_DECODE_BYTES: u64 = 128 * 1024 * 1024;
/// Decoded rasters kept per process; the map is cleared when full.
const CACHE_ENTRIES: usize = 24;
/// A decoded image, RGBA8, at most [`MAX_RASTER_SIDE`] on a side.
pub struct Raster {
pub width: u32,
pub height: u32,
/// The size before the downscale: what `background-size: auto` paints.
pub intrinsic_width: u32,
pub intrinsic_height: u32,
rgba: Vec<u8>,
}
impl Raster {
pub fn pixel(&self, x: usize, y: usize) -> Rgba {
let i = (y.min(self.height as usize - 1) * self.width as usize
+ x.min(self.width as usize - 1))
* 4;
let p = &self.rgba[i..i + 4];
Rgba::new(p[0] as f64, p[1] as f64, p[2] as f64, p[3] as f64 / 255.0)
}
}
thread_local! {
static RASTERS: RefCell<HashMap<String, Option<Rc<Raster>>>> = RefCell::new(HashMap::new());
}
/// Resolves and decodes the `url()` grounds of one document, relative to
/// the document's directory. A url from a linked stylesheet reaches the
/// cascade already rewritten to page-relative form.
pub struct ImageSampler {
base: String,
}
impl ImageSampler {
pub fn new(html_dir: &str) -> Self {
ImageSampler {
base: html_dir.to_string(),
}
}
/// The raster behind a `url()` argument, or `None` when it cannot be
/// read here: a remote URL, a missing or oversized file, a format the
/// decoders do not cover (SVG), or an `svg`/text data URI.
pub fn load(&self, url: &str) -> Option<Rc<Raster>> {
let url = url.trim();
if url.is_empty() {
return None;
}
let key = if is_data_uri(url) {
// Refused before anything is allocated: a project file can carry
// any size of data URI, and the hook scans on every edit.
if url.len() > MAX_DATA_URI_CHARS + 256 {
return None;
}
data_uri_key(url)
} else {
self.resolve_file(url)?
};
if let Some(hit) = RASTERS.with(|c| c.borrow().get(&key).cloned()) {
return hit;
}
let bytes = if is_data_uri(url) {
data_uri_bytes(url)
} else {
read_bounded(&key)
};
let raster = bytes.and_then(|b| decode(&b)).map(Rc::new);
RASTERS.with(|c| {
let mut cache = c.borrow_mut();
if cache.len() >= CACHE_ENTRIES {
cache.clear();
}
cache.insert(key, raster.clone());
});
raster
}
fn resolve_file(&self, url: &str) -> Option<String> {
if url.starts_with("//") || url.contains("://") {
return None;
}
let path = resolve_linked_css_path(&self.base, url);
std::fs::metadata(&path)
.map(|m| m.is_file())
.unwrap_or(false)
.then_some(path)
}
}
fn is_data_uri(url: &str) -> bool {
// `get`, not a byte slice: a url may open with a multibyte character.
url.get(..5)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("data:"))
}
/// The cache key of a data URI: its length and a hash, so the cache never
/// holds a copy of the URI itself.
fn data_uri_key(url: &str) -> String {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
url.hash(&mut hasher);
format!("data:{}:{:016x}", url.len(), hasher.finish())
}
/// The name a finding gives the image: the file name, or `data:<mime>`.
pub fn ground_label(url: &str) -> String {
let url = url.trim();
if is_data_uri(url) {
let header = url[5..].split(',').next().unwrap_or("");
let mime = header
.split(';')
.next()
.unwrap_or("")
.trim()
.to_ascii_lowercase();
return if mime.is_empty() {
"data:image".into()
} else {
format!("data:{mime}")
};
}
let stripped = url.split(['?', '#']).next().unwrap_or("");
jsp::basename(stripped)
}
fn data_uri_bytes(url: &str) -> Option<Vec<u8>> {
let (header, payload) = url[5..].split_once(',')?;
if !header
.split(';')
.any(|p| p.trim().eq_ignore_ascii_case("base64"))
{
return None;
}
if payload.len() > MAX_DATA_URI_CHARS {
return None;
}
let compact: Cow<str> = if payload.chars().any(char::is_whitespace) {
Cow::Owned(payload.chars().filter(|c| !c.is_whitespace()).collect())
} else {
Cow::Borrowed(payload)
};
base64::engine::general_purpose::STANDARD
.decode(compact.as_ref())
.or_else(|_| base64::engine::general_purpose::STANDARD_NO_PAD.decode(compact.as_ref()))
.ok()
}
fn read_bounded(path: &str) -> Option<Vec<u8>> {
let meta = std::fs::metadata(path).ok()?;
if !meta.is_file() || meta.len() > MAX_FILE_BYTES {
return None;
}
std::fs::read(path).ok()
}
fn decode(bytes: &[u8]) -> Option<Raster> {
let mut reader = image::ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.ok()?;
let mut limits = image::Limits::default();
limits.max_image_width = Some(MAX_IMAGE_SIDE);
limits.max_image_height = Some(MAX_IMAGE_SIDE);
limits.max_alloc = Some(MAX_DECODE_BYTES);
reader.limits(limits);
let decoded = reader.decode().ok()?;
let (intrinsic_width, intrinsic_height) = (decoded.width(), decoded.height());
if intrinsic_width == 0 || intrinsic_height == 0 {
return None;
}
// `thumbnail` also upscales, so only images past the budget go through it.
let scaled = if intrinsic_width.max(intrinsic_height) > MAX_RASTER_SIDE {
decoded.thumbnail(MAX_RASTER_SIDE, MAX_RASTER_SIDE)
} else {
decoded
};
let rgba = scaled.to_rgba8();
Some(Raster {
width: rgba.width(),
height: rgba.height(),
intrinsic_width,
intrinsic_height,
rgba: rgba.into_raw(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn png_bytes(width: u32, height: u32, rgba: [u8; 4]) -> Vec<u8> {
let img = image::RgbaImage::from_pixel(width, height, image::Rgba(rgba));
let mut out = Cursor::new(Vec::new());
image::DynamicImage::ImageRgba8(img)
.write_to(&mut out, image::ImageFormat::Png)
.unwrap();
out.into_inner()
}
#[test]
fn data_uri_decodes_and_labels() {
let bytes = png_bytes(8, 8, [240, 236, 228, 255]);
let uri = format!(
"data:image/png;base64,{}",
base64::engine::general_purpose::STANDARD.encode(&bytes)
);
let sampler = ImageSampler::new("/nonexistent");
let raster = sampler.load(&uri).expect("png data uri decodes");
assert_eq!((raster.width, raster.height), (8, 8));
assert_eq!(raster.pixel(3, 3).r, 240.0);
assert_eq!(ground_label(&uri), "data:image/png");
assert!(sampler.load("data:image/svg+xml;utf8,<svg/>").is_none());
assert!(sampler.load("https://example.com/hero.jpg").is_none());
assert!(sampler.load("//cdn.example.com/hero.jpg").is_none());
assert_eq!(ground_label("img/hero.jpg?v=3"), "hero.jpg");
// A url that opens with a multibyte character is a local path, not
// a panic.
assert!(!is_data_uri("dat\u{20ac}"));
assert!(sampler.load("abcd\u{20ac}.png").is_none());
assert_eq!(ground_label("abcd\u{20ac}.png"), "abcd\u{20ac}.png");
// A payload past the byte budget is refused before it is decoded.
let huge = format!(
"data:image/png;base64,{}",
"A".repeat(MAX_DATA_URI_CHARS + 1)
);
assert!(sampler.load(&huge).is_none());
assert_ne!(data_uri_key(&uri), data_uri_key(&huge));
}
#[test]
fn files_resolve_against_the_page_dir_and_downscale() {
let dir = std::env::temp_dir().join(format!("impeccable-sampler-{}", std::process::id()));
std::fs::create_dir_all(dir.join("img")).unwrap();
std::fs::write(
dir.join("img").join("wide.png"),
png_bytes(1280, 320, [20, 20, 20, 255]),
)
.unwrap();
let sampler = ImageSampler::new(&dir.to_string_lossy());
assert!(sampler.load("wide.png").is_none());
let raster = sampler
.load("img/wide.png")
.expect("resolves against the page dir");
assert_eq!((raster.width, raster.height), (640, 160));
assert_eq!(
(raster.intrinsic_width, raster.intrinsic_height),
(1280, 320)
);
assert_eq!(raster.pixel(639, 159).g, 20.0);
// The second load is the cached raster.
assert!(Rc::ptr_eq(&raster, &sampler.load("img/wide.png").unwrap()));
std::fs::remove_dir_all(&dir).ok();
}
}
+1
View File
@@ -9,6 +9,7 @@ pub mod background;
pub mod cascade;
pub mod dom;
pub mod engine;
pub mod image_sampling;
pub mod page;
pub mod profile;
pub mod quality;
+206
View File
@@ -209,3 +209,209 @@ fn checks_shim_helpers() {
assert_eq!(resolve_length_px("50%", 10.0), Some(5.0));
assert_eq!(resolve_length_px("1.5", 10.0), Some(15.0));
}
#[test]
fn background_longhands_ride_beside_the_expansion() {
use impeccable_html::cascade::rules::apply_static_longhand;
use impeccable_html::cascade::shorthand::background_longhands;
// The expansion itself is pinned by the recorded vectors and stays
// image-and-color only; repeat and size come from the side channel.
assert_eq!(
background_longhands(
"background",
"url(a.png) center / cover no-repeat, url(b.png)"
),
vec![
(
"backgroundRepeat".to_string(),
"no-repeat, repeat".to_string()
),
("backgroundSize".to_string(), "cover, auto".to_string()),
]
);
// The css-tree generator glues the first keyword to the call.
assert_eq!(
background_longhands("background", "url(a.png)center/cover no-repeat"),
vec![
("backgroundRepeat".to_string(), "no-repeat".to_string()),
("backgroundSize".to_string(), "cover".to_string()),
]
);
assert_eq!(
background_longhands("background", "#fff url(a.png) repeat-x"),
vec![
("backgroundRepeat".to_string(), "repeat-x".to_string()),
("backgroundSize".to_string(), "auto".to_string()),
]
);
assert_eq!(
background_longhands("Background-Size", "100% 32px"),
vec![("backgroundSize".to_string(), "100% 32px".to_string())]
);
// A color-only shorthand still resets both longhands, as in CSS.
assert_eq!(
background_longhands("background", "#fff"),
vec![
("backgroundRepeat".to_string(), "repeat".to_string()),
("backgroundSize".to_string(), "auto".to_string()),
]
);
assert_eq!(
background_longhands("background", "Inherit"),
vec![
("backgroundRepeat".to_string(), "inherit".to_string()),
("backgroundSize".to_string(), "inherit".to_string()),
]
);
assert!(background_longhands("background", "var(--surface)").is_empty());
assert!(background_longhands("color", "red").is_empty());
// A later shorthand with an image resets an earlier longhand, and a
// later longhand overrides a shorthand, under the cascade's priority.
let mut specified: SpecifiedStore<&str> = SpecifiedStore::new();
let node = "n1";
let apply = |specified: &mut SpecifiedStore<&str>, prop: &str, value: &str, m: DeclMeta| {
apply_static_declaration(specified, node, prop, value, &m);
for (p, v) in background_longhands(prop, value) {
apply_static_longhand(specified, node, &p, &v, &m);
}
};
apply(
&mut specified,
"background-repeat",
"no-repeat",
meta(false, [0, 1, 0], 0, false),
);
apply(
&mut specified,
"background",
"url(hero.jpg) center / cover",
meta(false, [0, 1, 0], 1, false),
);
apply(
&mut specified,
"background-size",
"contain",
meta(false, [0, 1, 0], 2, false),
);
let map = specified.get(&node).expect("node entry");
assert_eq!(
map.get("backgroundRepeat").map(|d| d.value.as_str()),
Some("repeat")
);
assert_eq!(
map.get("backgroundSize").map(|d| d.value.as_str()),
Some("contain")
);
assert_eq!(
map.get("backgroundImage").map(|d| d.value.as_str()),
Some("url(hero.jpg) center / cover")
);
// `background: #fff` after a longhand resets it, so a later
// `background-image` is classified with the defaults, not a stale value.
apply(
&mut specified,
"background-repeat",
"no-repeat",
meta(false, [0, 1, 0], 3, false),
);
apply(
&mut specified,
"background",
"#fff",
meta(false, [0, 1, 0], 4, false),
);
let map = specified.get(&node).expect("node entry");
assert_eq!(
map.get("backgroundRepeat").map(|d| d.value.as_str()),
Some("repeat")
);
assert_eq!(
map.get("backgroundSize").map(|d| d.value.as_str()),
Some("auto")
);
}
#[test]
fn linked_sheet_urls_are_rewritten_page_relative() {
use impeccable_html::cascade::build::rewrite_sheet_urls;
let css = concat!(
".a { background: url(light.png) }\n",
".b { background: url(\"../img/hero.jpg?v=3\") no-repeat }\n",
".c { background-image: url('./x.webp'), url(/root.png), url(data:image/png;base64,AAAA) }\n",
".d { background: url(https://cdn.example.com/a.png) }\n",
".e { mask: url(#clip) }\n",
".f { background: URL( a b.png ) }\n",
);
let out = rewrite_sheet_urls(css, "/site/css", "/site");
assert!(
out.contains(".a { background: url(css/light.png) }"),
"{out}"
);
assert!(
out.contains(".b { background: url(\"img/hero.jpg?v=3\") no-repeat }"),
"{out}"
);
assert!(
out.contains("url('css/x.webp'), url(/root.png), url(data:image/png;base64,AAAA)"),
"{out}"
);
assert!(out.contains("url(https://cdn.example.com/a.png)"), "{out}");
assert!(out.contains("url(#clip)"), "{out}");
// An unquoted url with a space is not a url token in CSS; it stays put.
assert!(out.contains("URL( a b.png )"), "{out}");
// A sheet beside the page keeps every path where it was (the engine
// does not even call the rewrite for that case).
let same = rewrite_sheet_urls(css, "/site", "/site");
assert!(same.contains(".a { background: url(light.png) }"), "{same}");
assert!(same.contains("url(\"../img/hero.jpg?v=3\")"), "{same}");
// A sheet above the page walks back up.
let up = rewrite_sheet_urls(".a { background: url(light.png) }", "/site", "/site/pages");
assert_eq!(up, ".a { background: url(../light.png) }");
// A stray `url(` in a comment, or an unclosed quote, never swallows the
// rules after it: comments pass through untouched and a url form ends
// where CSS says it ends.
let hazards = concat!(
"/* see url( for details */ .g { color: red }\n",
".h { background: url(a.png) }\n",
".i { background: url(\"oops }\n",
".j { background: url(b.png) }\n",
"/* url(unterminated.png",
);
let out = rewrite_sheet_urls(hazards, "/site/css", "/site");
assert!(
out.contains("/* see url( for details */ .g { color: red }"),
"{out}"
);
assert!(out.contains(".h { background: url(css/a.png) }"), "{out}");
assert!(out.contains(".i { background: url(\"oops }"), "{out}");
assert!(out.contains(".j { background: url(css/b.png) }"), "{out}");
assert!(out.ends_with("/* url(unterminated.png"), "{out}");
// A `/*` inside a string or an unquoted url is content, not a comment,
// so the rules after it are still rewritten.
let strings = concat!(
".k { content: \"a/*b\"; background: url(k.png) }\n",
".l { background: url(l/*.png) }\n",
".m { content: 'c/*d'; } /* real url( */ .n { background: url(n.png) }\n",
);
let out = rewrite_sheet_urls(strings, "/site/css", "/site");
assert!(
out.contains(".k { content: \"a/*b\"; background: url(css/k.png) }"),
"{out}"
);
assert!(out.contains(".l { background: url(css/l/*.png) }"), "{out}");
assert!(
out.contains("/* real url( */ .n { background: url(css/n.png) }"),
"{out}"
);
// `myurl(` is a custom function, not the url token: its comment stays a
// comment and its argument is not rewritten.
let custom = ".q { mask: myurl(a /* url(x.png) */); background: url(q.png) }";
let out = rewrite_sheet_urls(custom, "/site/css", "/site");
assert_eq!(
out,
".q { mask: myurl(a /* url(x.png) */); background: url(css/q.png) }"
);
}
File diff suppressed because one or more lines are too long
+55 -1
View File
@@ -596,6 +596,18 @@ fn write_carbonize_banner(event: &Map<String, Value>, io: &mut Io) {
}
}
fn reply_ack_json(reply: &Reply) -> Value {
let mut m = Map::new();
m.insert("ok".into(), json!(true));
m.insert("id".into(), json!(reply.id));
m.insert("status".into(), json!(reply.ty));
if let Some(f) = &reply.file {
m.insert("file".into(), json!(f));
}
m.insert("_instructions".into(), json!("Poll again now."));
Value::Object(m)
}
/// JS: printPollEvent(event) — a wire-supplied `_instructions` must never
/// win over the locally generated one (#488).
fn print_poll_event(event: &mut Value, io: &mut Io) {
@@ -718,7 +730,13 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
}
};
return match post_reply(&base, &token, &reply) {
Ok(()) => 0,
Ok(()) => {
println(
io,
&serde_json::to_string(&reply_ack_json(&reply)).unwrap_or_default(),
);
0
}
Err(PollError::ConnRefused) => {
io.err(&format!(
"Live server not running. Start one with: {}\n",
@@ -839,4 +857,40 @@ mod tests {
}));
assert!(parsed.get("_instructions").is_none(), "{}", parsed);
}
#[test]
fn reply_ack_json_includes_file_when_present() {
let reply = Reply {
id: "ab12cd34".into(),
ty: "done".into(),
message: None,
file: Some("index.html".into()),
data: None,
source_event_type: None,
};
let parsed = reply_ack_json(&reply);
assert_eq!(parsed["ok"], json!(true));
assert_eq!(parsed["id"], json!("ab12cd34"));
assert_eq!(parsed["status"], json!("done"));
assert_eq!(parsed["file"], json!("index.html"));
assert_eq!(parsed["_instructions"], json!("Poll again now."));
}
#[test]
fn reply_ack_json_omits_file_when_absent() {
let reply = Reply {
id: "ab12cd34".into(),
ty: "steer_done".into(),
message: None,
file: None,
data: None,
source_event_type: None,
};
let parsed = reply_ack_json(&reply);
assert_eq!(parsed["ok"], json!(true));
assert_eq!(parsed["id"], json!("ab12cd34"));
assert_eq!(parsed["status"], json!("steer_done"));
assert!(parsed.get("file").is_none(), "{}", parsed);
assert_eq!(parsed["_instructions"], json!("Poll again now."));
}
}
+72
View File
@@ -418,6 +418,16 @@ fn push_diag(next: &mut Map<String, Value>, d: Value) {
next.insert("diagnostics".to_string(), Value::Array(arr));
}
fn drop_diag(next: &mut Map<String, Value>, error: &str) {
let mut arr = next
.get("diagnostics")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
arr.retain(|d| d.get("error").and_then(|e| e.as_str()) != Some(error));
next.insert("diagnostics".to_string(), Value::Array(arr));
}
/// JS: applyEvent(snapshot, entry)
pub fn apply_event(snapshot: &Map<String, Value>, entry: &Value) -> Map<String, Value> {
let event: Map<String, Value> = match entry.get("event") {
@@ -864,6 +874,7 @@ pub fn apply_event(snapshot: &Map<String, Value>, entry: &Value) -> Map<String,
set!("phase", json!("discarded"));
set!("pendingEventSeq", Value::Null);
set!("pendingEvent", Value::Null);
drop_diag(&mut next, "carbonize_cleanup_required");
}
"complete" => {
set!("phase", json!("completed"));
@@ -876,6 +887,7 @@ pub fn apply_event(snapshot: &Map<String, Value>, entry: &Value) -> Map<String,
set_if!("previewMode", ev("previewMode"));
set!("pendingEventSeq", Value::Null);
set!("pendingEvent", Value::Null);
drop_diag(&mut next, "carbonize_cleanup_required");
}
"agent_error" => {
if canceled && ev("sourceEventType").and_then(|v| v.as_str()) == Some("generate") {
@@ -925,3 +937,63 @@ fn write_snapshot(path: &str, snapshot: &Map<String, Value>, journal_bytes: i64,
pub fn get_str<'a>(m: &'a Map<String, Value>, k: &str) -> Option<&'a str> {
get(m, k).and_then(|v| v.as_str())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn journal_entry(seq: i64, event: Value) -> Value {
json!({ "seq": seq, "ts": "2026-01-01T00:00:00.000Z", "event": event })
}
fn has_diag(snapshot: &Map<String, Value>, error: &str) -> bool {
snapshot
.get("diagnostics")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.any(|d| d.get("error").and_then(|e| e.as_str()) == Some(error))
})
.unwrap_or(false)
}
fn replay(id: &str, events: &[Value]) -> Map<String, Value> {
let mut snap = base_snapshot(id);
for entry in events {
snap = apply_event(&snap, entry);
}
snap
}
fn accept_carbonize_done(id: &str, terminal: &str) -> Map<String, Value> {
replay(
id,
&[
journal_entry(
1,
json!({ "id": id, "type": "accept", "variantId": 2 }),
),
journal_entry(
2,
json!({ "id": id, "type": "agent_done", "carbonize": true, "file": "index.html" }),
),
journal_entry(3, json!({ "id": id, "type": terminal })),
],
)
}
#[test]
fn complete_drops_carbonize_cleanup_required() {
let snap = accept_carbonize_done("ab12cd34", "complete");
assert_eq!(snap.get("phase").and_then(|p| p.as_str()), Some("completed"));
assert!(!has_diag(&snap, "carbonize_cleanup_required"));
}
#[test]
fn discarded_drops_carbonize_cleanup_required() {
let snap = accept_carbonize_done("ab12cd34", "discarded");
assert_eq!(snap.get("phase").and_then(|p| p.as_str()), Some("discarded"));
assert!(!has_diag(&snap, "carbonize_cleanup_required"));
}
}
+3 -9
View File
@@ -219,13 +219,10 @@ fn rewrite_value(value: &Value, provider: &str, quoted: &QuotedPath, win32: bool
}
}
/// JS: valueHasImpeccableHookMarker(value). Command separators are
/// normalized to `/` first so a legacy Windows-path guard is still
/// recognized as ours and replaced instead of duplicated (upstream
/// 665c51b9, #604).
/// JS: valueHasImpeccableHookMarker(value).
pub fn value_has_impeccable_hook_marker(value: &Value) -> bool {
match value {
Value::String(s) => is_impeccable_hook_command(&s.replace('\\', "/")),
Value::String(s) => is_impeccable_hook_command(s),
Value::Array(a) => a.iter().any(value_has_impeccable_hook_marker),
Value::Object(o) => o.values().any(value_has_impeccable_hook_marker),
_ => false,
@@ -233,12 +230,9 @@ pub fn value_has_impeccable_hook_marker(value: &Value) -> bool {
}
/// True when `value` names an Impeccable hook in the launcher generation.
/// Separators are normalized to `/` first, matching
/// `value_has_impeccable_hook_marker`, so a legacy Windows-path launcher
/// command is still recognized.
pub fn value_has_launcher_hook_marker(value: &Value) -> bool {
match value {
Value::String(s) => is_launcher_hook_command(&s.replace('\\', "/")),
Value::String(s) => is_launcher_hook_command(s),
Value::Array(a) => a.iter().any(value_has_launcher_hook_marker),
Value::Object(o) => o.values().any(value_has_launcher_hook_marker),
_ => false,
@@ -357,3 +357,57 @@ fn hook_artifacts_map_providers_to_manifest_files() {
assert_eq!(c[0].dest, jsp::join(&["/p", ".codex", "hooks.json"]));
assert!(c[0].shared_dest.is_none());
}
fn windows_user_scope_hook_command() -> String {
let launcher = r"C:\Users\alice\.claude\skills\impeccable\scripts\impeccable";
let q = json_string(launcher);
format!("[ ! -f {q} ] || {q} hook")
}
#[test]
fn merge_json_escaped_windows_launcher_is_idempotent() {
let cmd = windows_user_scope_hook_command();
let hook_entry = |cmd: String| {
json!({ "matcher": "Edit", "hooks": [{ "type": "command", "command": cmd }] })
};
let stop_entry = |cmd: String| {
json!({ "hooks": [{ "type": "command", "command": cmd, "timeout": 30 }] })
};
let existing = json!({
"hooks": {
"PostToolUse": [hook_entry(cmd.clone())],
"Stop": [stop_entry(cmd.clone())]
}
});
let fresh = json!({
"description": "fresh",
"hooks": {
"PostToolUse": [hook_entry(cmd.clone())],
"Stop": [stop_entry(cmd.clone())]
}
});
let merged = merge_hook_manifests(&existing, &fresh);
assert_eq!(merged["hooks"]["PostToolUse"].as_array().unwrap().len(), 1);
assert_eq!(merged["hooks"]["Stop"].as_array().unwrap().len(), 1);
let merged2 = merge_hook_manifests(&merged, &fresh);
assert_eq!(merged2["hooks"]["PostToolUse"].as_array().unwrap().len(), 1);
assert_eq!(merged2["hooks"]["Stop"].as_array().unwrap().len(), 1);
}
#[test]
fn merge_heals_triplicated_stop_groups() {
let cmd = windows_user_scope_hook_command();
let stop_entry = json!({ "hooks": [{ "type": "command", "command": cmd.clone(), "timeout": 30 }] });
let existing = json!({
"hooks": {
"Stop": [stop_entry.clone(), stop_entry.clone(), stop_entry]
}
});
let fresh = json!({
"hooks": {
"Stop": [json!({ "hooks": [{ "type": "command", "command": cmd, "timeout": 30 }] })]
}
});
let merged = merge_hook_manifests(&existing, &fresh);
assert_eq!(merged["hooks"]["Stop"].as_array().unwrap().len(), 1);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "impeccable",
"version": "4.3.0",
"version": "4.3.1",
"description": "Design and refine interfaces with Impeccable skills, specialist agents, and design checks.",
"author": {
"name": "Renaissance Geek, Inc."
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
license: Apache 2.0
---
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+2 -1
View File
@@ -332,6 +332,7 @@ Optional keys added later by engines (appended after the above): `ignoreValue` (
#### Static and regex engines (only what affects the contract)
- `detectHtml`: reads file, imports `htmlparser2`, `css-select`, `css-tree`, `domutils`; on import failure prints once to stderr `impeccable detect: DEGRADED - HTML parser modules unavailable (htmlparser2, css-select, css-tree, domutils).\nFalling back to regex matching. Custom properties, selector matching and computed contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n` and falls back to `detectText`. Inlines `<link rel=stylesheet href>` that are local (not `/^(https?:)?\/\//i`), query/hash stripped. Runs element rules, design-system rules (`checkSourceDesignSystem` + `collectStaticDesignSystemFindings`, merged), then page rules only when `isFullPage(html)` (`/<!doctype\s|<html[\s>]|<head[\s>]/i` after stripping comments), plus text-content analyzers; ends with inline-ignore filtering.
- **Sampled contrast (#560, engine only, no JS ancestor)**: when `resolveBackgroundInfo` ends at a `url()` layer (the case the JS skipped), the static engine reads that image and measures the text against its pixels. The url resolves to a local file relative to the page (a linked stylesheet from another directory has its relative urls rewritten to page-relative form when it is inlined, so a url resolves against the sheet that declared it; root-relative paths walk up to the project root the way linked stylesheets do) or a base64 `data:` URI; a remote URL, a missing or unreadable file, a file over 24 MiB, a data URI whose payload decodes to more than that, an image over 8192px on a side, and SVG keep the skip. Decoders: PNG, JPEG, GIF, WebP. The image is scaled to at most 640px on a side and read on a fixed 6x6 grid. Each sample composites a translucent pixel over the element's own color and its parent's resolved ground (white at the root; unknown when another layer sits beneath the image), then under every translucent surface and every uniform gradient wash between the text and the image; a gradient whose stops differ is a scrim placed on purpose and keeps the skip, as does an opaque gradient or an unparseable color anywhere in the chain. A layer whose `background-repeat` leaves an axis unrepeated and whose painted extent on that axis (`background-size` in px, else the intrinsic size) is under 160px is decoration and keeps the skip; `cover`, `contain`, and percentage sizes always paint. The cascade carries `backgroundRepeat` / `backgroundSize` for this from the longhands and from the `background` shorthand (per layer, defaults `repeat` / `auto`). A verdict needs 27 of the 36 samples; the finding fires when the 90th-percentile ratio is under the WCAG threshold (same large-text rule as `checkColors`, text alpha blended over the sample): `{id:'low-contrast', snippet:`sampled (coarse) ${p90}:1 (need ${threshold}:1) — text ${hex} on ${file name | data:<mime>}; p90 of ${n} samples, median ${median}:1`}` (ratio label to 2 decimals when its 1-decimal form equals the threshold, as in `checkColors`). It is emitted in the `color-rules` pass right after `checkColors`'s hits for that element, obeys the same `SAFE_TAGS` gate, `data-impeccable-ignore`, and inline ignores, and never produces `gray-on-color`. Fixture: `sampled-image-contrast.html` (+ `sampled-images/`).
- `detectText`: regex line matchers (ids: side-tab, border-accent-on-rounded, overused-font, gradient-text, ai-color-palette, gray-on-color, bounce-easing, layout-transition, broken-image), inset-stripe/pseudo-stripe CSS scans, `codex-grid-background`, `<style>` blocks (Astro/Vue/Svelte), CSS-in-JS templates, design-system source checks; dedupe (same antipattern+snippet within 2 lines); page analyzers only when `isFullPage` and ext ∈ `{'.html','.htm','.astro','.vue','.svelte'}` or no ext (`<stdin>`): flat-type-hierarchy, monotonous-spacing, em-dash-overuse, marketing-buzzword, aphoristic-cadence, dark-glow (+ radial-halo, marquee); inline ignores last.
#### Profiler (`cli/engine/profile/profiler.mjs`)
@@ -1740,7 +1741,7 @@ Conventions: every script's "run directly" guard is `process.argv[1]` ending wit
#### `live-poll.mjs` -> `impeccable poll`
- Invoked from live.md poll loop; `--reply` forms quoted in `_instructions` (see instructions.mjs strings in 6.3/below).
- Args: `--stream`, `--timeout=MS` (one-shot total, default 600000), `--types=A,B`, `--ack-timeout=MS` (stream, default 600000), `--reply <id> <status> [--file PATH] [--data JSON] [message]`, `--help`. `--reply` errors (stderr, exit 1): `Usage: node "<abs>/live-poll.mjs" --reply <id> <status> [--file path] [--data '<json>'] [message]` + `Missing event id after --reply.` / `The value after --reply must be the event id, not the status "done". Use --reply EVENT_ID done.` / `Missing reply status after event id "X".`; `--data must be valid JSON: <err>`.
- Args: `--stream`, `--timeout=MS` (one-shot total, default 600000), `--types=A,B`, `--ack-timeout=MS` (stream, default 600000), `--reply <id> <status> [--file PATH] [--data JSON] [message]`, `--help`. `--reply` success (stdout, exit 0): one compact JSON line `{ok:true,id,status,file? (only when --file was passed),_instructions:'Poll again now.'}`. `--reply` errors (stderr, exit 1): `Usage: node "<abs>/live-poll.mjs" --reply <id> <status> [--file path] [--data '<json>'] [message]` + `Missing event id after --reply.` / `The value after --reply must be the event id, not the status "done". Use --reply EVENT_ID done.` / `Missing reply status after event id "X".`; `--data must be valid JSON: <err>`.
- Needs `server.json`; else stderr `No running live server found. Start one with: node "<abs>/live.mjs"` exit 1.
- One-shot: loops `GET /poll?token&timeout=<slice ≤270000>&leaseMs=600000[&types]` until an event or total deadline; prints one JSON line (`console.log(JSON.stringify(event))`) with `_instructions` added by `instructionsForEvent` (unless already present). For `accept`/`discard`: spawns `node live-accept.mjs --id ID (--discard | --variant N) [--page-url U] [--param-values JSON]` (30 s), sets `event._acceptResult` (parse failure/throw → `{handled:false, mode:'error', error}`), then POSTs completion `{id, type: completionType, sourceEventType: event.type, message: _acceptResult.error, file: _acceptResult.file, data: {carbonize:true}?}` where completionType = discard: `discarded` if handled else `error`; accept: `agent_done` if handled&carbonize, `complete` if handled, `error` if mode error or (svelte-component unhandled), else `agent_done`; sets `event._completionAck = {ok:true, type}` (+ `final:false, requiresComplete:true, nextCommand:'live-complete.mjs --id <id>', message:'Carbonize cleanup must be verified, then the session must be completed explicitly before polling again.'` for carbonize) or `{ok:false, error}`. Stderr banners: manual_edit_apply → 4-line banner starting `Manual Apply action required: edit source, then reply with \`live-poll.mjs --reply <id> done --data '<json>'\`.`; carbonize → `⚠ Carbonize cleanup REQUIRED before next poll. After cleanup, run live-complete.mjs --id <id>. See reference/live.md "Required after accept".`
- Stream: stderr `[impeccable-poll] stream mode: one JSON object per line on stdout; use --reply while this process stays running`; after each reply-needing event waits (poll `/status` every 400 ms) until the id leaves `pendingEvents` (else `Timed out waiting for --reply on event <id>` exit 1); returns on `exit`.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "impeccable",
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"version": "4.3.0",
"version": "4.3.1",
"author": {
"name": "Paul Bakaus",
"email": "paul@paulbakaus.com"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "impeccable",
"version": "4.3.0",
"version": "4.3.1",
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"author": {
"name": "Paul Bakaus",
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.3.0
version: 4.3.1
user-invocable: true
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
license: Apache 2.0
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+20 -1
View File
@@ -87,6 +87,11 @@ fi
# Last resort: fetch this version's binary for the current platform from the
# public release channel into the user cache. Needs network; sandboxes without
# egress preinstall the binary on PATH instead.
setup_help() {
echo "Engine $version setup needs network access and write permission to $cache_root/bin/$version." >&2
echo "Run this launcher ($0) with engine-probe in a terminal that has those permissions, then retry the original command." >&2
echo "Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary." >&2
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null
@@ -119,7 +124,18 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
[ "$os" = windows ] && asset="$asset.exe"
url="$base/engine-v$version/$asset"
tmp="$cache_root/bin/$version/.impeccable.part.$$"
mkdir -p "$cache_root/bin/$version" 2>/dev/null
if ! mkdir -p "$cache_root/bin/$version" 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot create cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
# Check the actual staging file, not just directory existence: a cache from
# an earlier run can be readable but no longer writable inside a sandbox.
if ! (umask 077; : > "$tmp") 2>/dev/null; then
echo "impeccable: engine $version is not installed; cannot write to cache directory: $cache_root/bin/$version" >&2
setup_help
exit 127
fi
fetched=0
if fetch_url "$url"; then
fetched=1
@@ -180,6 +196,9 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then
exec "$cached" "$@"
fi
rm -f "$tmp" 2>/dev/null
echo "impeccable: could not download engine $version from $url; check network access, the release URL, and curl or wget availability." >&2
setup_help
exit 127
fi
echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2
+35 -4
View File
@@ -68,18 +68,27 @@ rem another launcher's probe: fail fast and quiet instead.
if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127
if not defined version goto fail
where curl.exe >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto curl_missing
if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/pbakaus/impeccable/releases/download"
if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if exist "%IMPECCABLE_HOME%\bin\%version%\" goto cache_ready
mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul
if errorlevel 1 goto cache_directory_failed
:cache_ready
rem Check the staging file too: an existing directory may be read-only.
rem Redirection failures do not reliably update ERRORLEVEL in cmd.exe;
rem branch on the command's failure directly. Never treat a directory as a
rem staging file (later del cleanup would prompt to delete its contents).
if exist "%cached%.part\" goto cache_write_failed
(type nul >"%cached%.part") 2>nul || goto cache_write_failed
set "asset=impeccable-windows-%arch%.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if not errorlevel 1 goto verify
if not "%arch%"=="arm64" goto fail
if not "%arch%"=="arm64" goto download_failed
set "asset=impeccable-windows-x64.exe"
set "url=%IMPECCABLE_DOWNLOAD_BASE%/engine-v%version%/%asset%"
curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul
if errorlevel 1 goto fail
if errorlevel 1 goto download_failed
:verify
call :check_download
@@ -176,6 +185,28 @@ if not errorlevel 1 set "probe_ok=1"
del "%probe_tmp%" >nul 2>nul
exit /b 0
:cache_directory_failed
echo impeccable: engine %version% is not installed; cannot create cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:cache_write_failed
echo impeccable: engine %version% is not installed; cannot write to cache directory: "%IMPECCABLE_HOME%\bin\%version%" 1>&2
goto setup_failed
:curl_missing
echo impeccable: cannot download engine %version%; curl.exe is unavailable. 1>&2
goto setup_failed
:download_failed
del "%cached%.part" >nul 2>nul
echo impeccable: could not download engine %version% from %url%; check network access and the release URL. 1>&2
:setup_failed
echo Engine %version% setup needs network access and write permission to "%IMPECCABLE_HOME%\bin\%version%". 1>&2
echo Run this launcher ("%~f0") with engine-probe in a terminal that has those permissions, then retry the original command. 1>&2
echo Alternatively, set IMPECCABLE_HOME to a writable cache location, or IMPECCABLE_BIN to a preinstalled engine binary. 1>&2
exit /b 127
:fail
del "%cached%.part" >nul 2>nul
echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2
+116
View File
@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Fixture: pixel-sampled contrast on image-backed text (static engine)</title>
<link rel="stylesheet" href="sampled-images/site.css">
<style>
body { margin: 0; padding: 40px; background: #ffffff; font-family: Georgia, serif; color: #262421; }
/* Every panel is a url() ground the analytic walk cannot measure. The
static engine reads the image's pixels instead (#560): a fixed grid over
the whole image, and a finding only when nearly all of it fails. Each
case carries UNIQUE copy so a finding attributes to exactly one case.
Images live in sampled-images/: light.png and light.webp are a
near-white texture, dark.jpg a charcoal one, split.png half light and
half dark, cutout.png the light texture at 25% alpha, icon.png a 24px
glyph. */
h1 { font-size: 28px; margin: 0 0 24px; }
.panel { width: 420px; padding: 24px; margin: 0 0 24px; }
.panel p { margin: 0; font-size: 16px; line-height: 1.6; }
.panel h2 { margin: 0; font-size: 28px; line-height: 1.3; }
.light { background: url(sampled-images/light.png); }
.light-webp { background: url("sampled-images/light.webp") center / cover; }
.tokens-first { background: center / cover no-repeat url(sampled-images/light.png); }
.dark-jpg { background: url(sampled-images/dark.jpg) center / cover no-repeat; }
.split { background: url(sampled-images/split.png) repeat; }
.inline-data { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAaUlEQVR42k2PSQrAMAwD/f9ndt+TNl2OLmMQ5CCELUtR7CuLP+cUuPPo19F73tqY09q4nXsXC0SYA8T3mgNxgMiypMGZccLA5Kij6+eMDhKIZEZQsmkBA47UKxLkFKsHjCE6IOBQSv2bH8D48hUetch/AAAAAElFTkSuQmCC); }
/* Uniform tints composite exactly; a wash that varies is a scrim placed
under the text on purpose, and without layout the engine cannot say
which stop the text sits on. */
.wash-weak { background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url(sampled-images/light.png); }
.wash-strong { background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url(sampled-images/light.png); }
.fade-scrim { background: linear-gradient(rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0)), url(sampled-images/light.png); }
/* A translucent image composites over what is beneath it: here a dark
section, so the light texture at 25% reads as a dark ground. */
.dark-section { width: 468px; padding: 0; background: #17150f; }
.cutout { background: url(sampled-images/cutout.png); margin: 0; }
.missing { background: url(sampled-images/nope.png); }
.remote { background: url(https://example.com/hero.jpg) center / cover; }
.svg-data { background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><rect width='100' height='100' fill='%23f5f2ea'/></svg>") center / cover; }
/* A no-repeat icon painted at 24px is decoration, not the ground. */
.icon-link { background: url(sampled-images/icon.png) no-repeat left center; padding: 8px 8px 8px 32px; margin: 0 0 24px; width: 380px; font-size: 16px; line-height: 1.6; }
.white { color: #fdfdfd; }
.ink { color: #262421; }
.smoke { color: #3a3632; }
</style>
</head>
<body>
<h1>Sampled contrast fixture</h1>
<!-- ── Should flag ──────────────────────────────────────────────── -->
<div class="panel light">
<p class="white">White copy on a near-white texture fails everywhere the grid lands.</p>
</div>
<div class="panel dark-jpg">
<p class="smoke">Smoke gray copy on a charcoal photo fails wherever it sits.</p>
</div>
<div class="panel light-webp">
<p class="white">The same texture as a WebP decodes through the same path.</p>
</div>
<div class="panel inline-data">
<p class="white">A base64 PNG data URI is read without touching the disk.</p>
</div>
<div class="panel wash-weak">
<p class="white">A thirty percent tint is too weak to carry white copy over the texture.</p>
</div>
<div class="dark-section">
<div class="panel cutout">
<p class="ink">Ink on the translucent texture composites over the dark section and fails.</p>
</div>
</div>
<div class="panel from-sheet">
<p class="white">The ground of this panel is declared in the linked stylesheet.</p>
</div>
<div class="panel light">
<h2 class="white">Large white heading on the texture</h2>
</div>
<div class="panel tokens-first">
<p class="white">Shorthand tokens before the image still name the ground.</p>
</div>
<!-- ── Should pass ──────────────────────────────────────────────── -->
<div class="panel light">
<p class="ink">Ink on the near-white texture reads fine and must not flag.</p>
</div>
<div class="panel dark-jpg">
<p class="white">White copy on the charcoal photo reads fine and must not flag.</p>
</div>
<div class="panel split">
<p class="white">Half of this image is dark, so the copy may well sit there.</p>
</div>
<div class="panel wash-strong">
<p class="white">A sixty percent tint carries white copy over the texture.</p>
</div>
<div class="panel fade-scrim">
<p class="white">A scrim that fades out is placed on purpose; the engine keeps the skip.</p>
</div>
<div class="dark-section">
<div class="panel cutout">
<p class="white">White copy on the translucent texture over the dark section passes.</p>
</div>
</div>
<div class="panel missing">
<p class="white">An image that is not on disk keeps the skip.</p>
</div>
<div class="panel remote">
<p class="white">A remote image is never fetched during a file scan.</p>
</div>
<div class="panel svg-data">
<p class="white">An SVG has no raster to sample, so the skip stands.</p>
</div>
<p class="icon-link">A twenty four pixel icon beside the copy is decoration, not the ground.</p>
<div class="panel light" data-impeccable-ignore="low-contrast">
<p class="white">A waived panel keeps its sampled finding out of the report.</p>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+3
View File
@@ -0,0 +1,3 @@
/* Linked from ../sampled-image-contrast.html. A url() inside a stylesheet is
relative to the sheet, so light.png resolves here, not next to the page. */
.from-sheet { background: url(light.png); }
Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

+61 -3
View File
@@ -29,8 +29,29 @@ async function exercise(t, scenario) {
const launcher = path.join(scripts, name);
fs.copyFileSync(path.join(ROOT, 'skill/scripts', name), launcher);
const cacheDir = path.join(cache, 'bin', '0.0.0-test');
if (scenario === 'cache-directory-failure') {
// A file where the cache parent belongs makes mkdir fail on every OS,
// including privileged test runners where chmod cannot deny writes.
fs.mkdirSync(cache);
fs.writeFileSync(path.join(cache, 'bin'), 'blocked');
}
if (scenario === 'cache-write-failure') {
fs.mkdirSync(cacheDir, { recursive: true });
if (WINDOWS) fs.mkdirSync(path.join(cacheDir, 'impeccable.exe.part'));
}
if (scenario === 'cache-readonly-file') {
fs.mkdirSync(cacheDir, { recursive: true });
const staging = path.join(cacheDir, 'impeccable.exe.part');
fs.writeFileSync(staging, 'read-only');
fs.chmodSync(staging, 0o444);
}
const tools = path.join(root, 'tools');
fs.mkdirSync(tools);
if (!WINDOWS && scenario === 'cache-write-failure') {
// The POSIX staging name contains the launcher's PID, so intercept the
// preceding mkdir to place a directory at precisely that file path.
fs.writeFileSync(path.join(tools, 'mkdir'), '#!/bin/sh\n/bin/mkdir "$@" || exit $?\n/bin/mkdir "$IMPECCABLE_HOME/bin/0.0.0-test/.impeccable.part.$PPID"\n', { mode: 0o755 });
}
if (!WINDOWS && ['hash-failure', 'removed-during-hash'].includes(scenario)) {
fs.writeFileSync(path.join(tools, 'shasum'),
`#!/bin/sh\n${scenario === 'removed-during-hash' ? 'rm -f "$3"\n' : ''}printf '%s %s\\n' '${HASH}' "$3"\nexit ${scenario === 'hash-failure' ? 1 : 0}\n`,
@@ -71,6 +92,10 @@ async function exercise(t, scenario) {
const requests = [];
const server = http.createServer((req, res) => {
requests.push(req.url);
if (scenario === 'transport-failure') {
req.socket.destroy();
return;
}
if (req.url.endsWith('.sha256')) {
const part = fs.readdirSync(cacheDir).find(file => file.includes('.part'));
if (scenario === 'removed') fs.unlinkSync(path.join(cacheDir, part));
@@ -78,6 +103,7 @@ async function exercise(t, scenario) {
res.writeHead(scenario === 'no-sidecar' ? 404 : 200);
res.end(scenario === 'empty-sidecar' ? '' : `${scenario === 'mismatch' ? '0'.repeat(64) : HASH} engine\n`);
} else {
if (scenario === 'download-failure') res.writeHead(404);
res.end(scenario === 'empty-download' ? '' : PAYLOAD);
}
});
@@ -92,7 +118,7 @@ async function exercise(t, scenario) {
IMPECCABLE_DOWNLOAD_BASE: `http://127.0.0.1:${server.address().port}`,
...(WINDOWS ? { SystemRoot: process.env.SystemRoot, ComSpec: COMSPEC, PROCESSOR_ARCHITECTURE: 'AMD64' } : {}),
};
const result = await new Promise((resolve, reject) => {
const run = () => new Promise((resolve, reject) => {
const child = WINDOWS
? spawn(COMSPEC, ['/d', '/s', '/c', `""${launcher}" /d /c echo verified-engine"`], { env, cwd: root, windowsVerbatimArguments: true, timeout: 20000 })
: spawn('/bin/sh', [launcher], { env, cwd: root, timeout: 20000 });
@@ -103,9 +129,41 @@ async function exercise(t, scenario) {
child.on('error', reject);
child.on('close', (status, signal) => resolve({ status, signal, stdout, stderr }));
});
const result = await run();
if (scenario === 'valid') {
const requestCount = requests.length;
const cachedResult = await run();
assert.equal(cachedResult.status, 0, cachedResult.stderr);
assert.match(cachedResult.stdout, /verified-engine/);
assert.equal(cachedResult.stderr, '', 'cached execution stays quiet');
assert.equal(requests.length, requestCount, 'subsequent runs use the cached engine without network');
}
assert.equal(result.signal, null, JSON.stringify(result));
assert.equal(requests.filter(url => !url.endsWith('.sha256')).length, 1, 'one binary download, no verification retry loop');
return { ...result, files: fs.readdirSync(cacheDir), requests };
const cacheFailure = scenario.startsWith('cache-');
assert.equal(requests.filter(url => !url.endsWith('.sha256')).length, cacheFailure ? 0 : 1,
'cache failures do not attempt a download; other scenarios download once');
return { ...result, files: fs.existsSync(cacheDir) ? fs.readdirSync(cacheDir) : [], requests, cacheDir };
}
for (const scenario of ['cache-directory-failure', 'cache-write-failure', 'download-failure', 'transport-failure', ...(WINDOWS ? ['cache-readonly-file'] : [])]) {
test(`launcher explains ${scenario} and how to retry setup`, async t => {
const result = await exercise(t, scenario);
assert.equal(result.status, 127, JSON.stringify(result));
assert.doesNotMatch(result.stdout, /verified-engine/);
assert.match(result.stderr, /engine 0\.0\.0-test/);
assert.ok(result.stderr.includes(result.cacheDir), result.stderr);
assert.match(result.stderr, /engine-probe/);
assert.match(result.stderr, /IMPECCABLE_HOME/);
assert.match(result.stderr, /IMPECCABLE_BIN/);
if (['download-failure', 'transport-failure'].includes(scenario)) {
assert.match(result.stderr, /could not download/);
assert.match(result.stderr, /network/);
assert.deepEqual(result.files, [], 'failed downloads leave no staging files');
assert.equal(result.requests.length, 1, 'no verification without a download');
} else {
assert.match(result.stderr, scenario === 'cache-directory-failure' ? /cannot create/ : /cannot write/);
}
});
}
test('launcher downloads and runs a verified executable', async t => {
+12
View File
@@ -164,3 +164,15 @@ installed. The binary's `CLI_VERSION` moves from `3.6.0` to `4.0.0` with the
CLI 4.0.0 release; it is what the binary prints when run directly.
- `cli-version`.
## Recorded 2026-09-10: sampled contrast on image-backed text (#560)
The static engine now reads the pixels of a local `url()` background behind
text instead of skipping the contrast check (docs/CLI-CONTRACT.md, "Sampled
contrast"). The JS never had this path, so the new fixture
`sampled-image-contrast.html` (with `sampled-images/`) has no JS golden; its
cases were recorded from the engine, and the directory sweeps below moved
only by that fixture's nine `low-contrast` findings, verified entry by entry.
- `detect-fixture-json-sampled-image-contrast-html`, `detect-fixture-text-sampled-image-contrast-html`, `detect-fixture-json-sampled-images`, `detect-fixture-text-sampled-images`: new cases.
- `detect-dir-json-all-fixtures`, `detect-dir-text-all-fixtures`, `detect-dir-quiet-all-fixtures`, `detect-no-advisory-json`, `detect-no-advisory-text`: the directory sweep picks up the new fixture.
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More