mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 22:48:57 +02:00
chore(design): pin impeccable rule ids and detector JSON shape as fixtures
Real captures from a human-initiated `npx impeccable install` in a scratch directory (engine 0.1.3, linux-x64), never a runtime download: - test/fixtures/impeccable-antipatterns.json: upstream crates/live/assets/antipatterns.json at 87d8f6d6 (the state engine-v0.1.3 shipped), 61 rules, source commit recorded in `_source`. - test/fixtures/impeccable-detect-sample.json: `detect --json` over gstack's planted-slop fixture (source mode), paths normalized. - test/fixtures/review-eval-design-slop.dom.html + impeccable-detect-dom-sample.json: the same page served locally, dumped through the browse engine with the shared DOM-dump script, then scanned. Pins the load-bearing assumption that the static engine reads inline <style> in a .html file: the DOM scan yields the same id set as the source scan. - lib/dom-dump-script.ts: the one dump script both browser engines evaluate (IIFE, no single quotes). Folds CSSOM rgb() back to author hex so palette rules still fire, and removes inlined <link> nodes so the engine does not warn about an unresolvable stylesheet. Both verified against the engine. - test/fixtures/impeccable-detect-help.txt + impeccable-captures.meta.json: the flags, exit codes, finding fields, and re-capture protocol. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0530392821
commit
8ef842fc75
+380
@@ -0,0 +1,380 @@
|
||||
{
|
||||
"_source": {
|
||||
"repo": "https://github.com/pbakaus/impeccable",
|
||||
"path": "crates/live/assets/antipatterns.json",
|
||||
"commit": "87d8f6d686782561fb572758d9a9bb8596a1a0e7",
|
||||
"commitDate": "2026-09-04T19:43:53Z",
|
||||
"engineRelease": "engine-v0.1.3",
|
||||
"engineReleaseDate": "2026-09-06T22:36:18Z",
|
||||
"fetched": "2026-09-08",
|
||||
"license": "Apache-2.0 (unmodified copy; see NOTICE.md)"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"id": "side-tab",
|
||||
"name": "Side-tab accent border",
|
||||
"category": "slop",
|
||||
"description": "Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely."
|
||||
},
|
||||
{
|
||||
"id": "border-accent-on-rounded",
|
||||
"name": "Border accent on rounded element",
|
||||
"category": "slop",
|
||||
"description": "Thick accent border on a rounded card — the border clashes with the rounded corners. Remove the border or the border-radius."
|
||||
},
|
||||
{
|
||||
"id": "overused-font",
|
||||
"name": "Overused font",
|
||||
"category": "slop",
|
||||
"description": "Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality."
|
||||
},
|
||||
{
|
||||
"id": "flat-type-hierarchy",
|
||||
"name": "Flat type hierarchy",
|
||||
"category": "slop",
|
||||
"description": "Dominant heading and body roles are separated by less than 1.25× at every step, leaving the size hierarchy flat. Add at least one stronger size step."
|
||||
},
|
||||
{
|
||||
"id": "gradient-text",
|
||||
"name": "Gradient text",
|
||||
"category": "slop",
|
||||
"description": "Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text."
|
||||
},
|
||||
{
|
||||
"id": "ai-color-palette",
|
||||
"name": "AI color palette",
|
||||
"category": "slop",
|
||||
"description": "Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette."
|
||||
},
|
||||
{
|
||||
"id": "cream-palette",
|
||||
"name": "Cream / beige palette",
|
||||
"category": "slop",
|
||||
"description": "A warm cream or beige page background has become the default \"tasteful\" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white."
|
||||
},
|
||||
{
|
||||
"id": "nested-cards",
|
||||
"name": "Nested cards",
|
||||
"category": "slop",
|
||||
"description": "Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers."
|
||||
},
|
||||
{
|
||||
"id": "monotonous-spacing",
|
||||
"name": "Monotonous spacing",
|
||||
"category": "slop",
|
||||
"description": "The same spacing value used everywhere — no rhythm, no variation. Use tight groupings for related items and generous separations between sections."
|
||||
},
|
||||
{
|
||||
"id": "bounce-easing",
|
||||
"name": "Bounce or elastic easing",
|
||||
"category": "slop",
|
||||
"description": "Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead."
|
||||
},
|
||||
{
|
||||
"id": "pulsing-dot",
|
||||
"name": "Pulsing status dot",
|
||||
"category": "slop",
|
||||
"description": "Small pulsing status dots simulate liveness decoratively. Reserve pulse animation for indicators tied to genuinely live, changing data; a static indicator with clear labeling is honest and calmer."
|
||||
},
|
||||
{
|
||||
"id": "blinking-cursor",
|
||||
"name": "Decorative blinking cursor",
|
||||
"category": "slop",
|
||||
"description": "A blinking text cursor animated into a hero or landing section simulates typing where no input exists. It borrows the dev-tool aesthetic as decoration. Real editable fields draw their own caret; anywhere else, let the composition hold attention without a fake prompt."
|
||||
},
|
||||
{
|
||||
"id": "shape-assembled-illustration",
|
||||
"name": "Shape-assembled illustration",
|
||||
"category": "slop",
|
||||
"description": "A large inline SVG that builds a pictorial scene from a pile of primitive shapes reads as placeholder clip art, not illustration. Icons, logos, and data graphics are fine at their scale; a hero-sized visual deserves real artwork, a photograph, or a deliberately drawn graphic."
|
||||
},
|
||||
{
|
||||
"id": "organic-clip-path",
|
||||
"name": "Organic contour drawn as clip-path",
|
||||
"category": "quality",
|
||||
"description": "A clip-path polygon with many arbitrary vertices, or a curved clip-path path(), is CSS approximating a torn edge, blob, or silhouette. It reads as the cheap version of the effect and is usually a produced or photographic material replaced with code. Derive an alpha matte from the real image, or ship the shape as a cut-out raster; keep clip-path for geometry (cut corners, diagonals, hexagons)."
|
||||
},
|
||||
{
|
||||
"id": "buried-raster",
|
||||
"name": "Raster buried under a wash or opacity",
|
||||
"category": "quality",
|
||||
"description": "A background image under a near-opaque gradient wash, or a raster on an element at near-zero opacity, never reaches the screen: the page shows the wash, and the produced texture or photo ships as a compliance token. Let the material show (a tint under 0.9 alpha, a blend mode, an opacity you can see) or remove the file."
|
||||
},
|
||||
{
|
||||
"id": "dark-glow",
|
||||
"name": "Glowing shadow accents",
|
||||
"category": "slop",
|
||||
"description": "Colored glow shadows — a zero-offset chromatic halo (box- or text-shadow) on any background, or any colored blurred shadow on a dark background — are the default \"cool\" look of AI-generated UIs. Use neutral elevation shadows and subtle, purposeful lighting instead."
|
||||
},
|
||||
{
|
||||
"id": "radial-halo",
|
||||
"name": "Radial-gradient background halo",
|
||||
"category": "slop",
|
||||
"description": "A chromatic radial-gradient wash — saturated at the center, fading to transparent — used as a decorative background glow on a dark page. Same tell as glowing shadows, drawn with a gradient instead of a shadow. Ground the surface with a solid or subtly shifted background instead."
|
||||
},
|
||||
{
|
||||
"id": "radial-spotlight-glow",
|
||||
"name": "Decorative radial spotlight glow",
|
||||
"category": "slop",
|
||||
"description": "A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a \"spotlight.\" It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze."
|
||||
},
|
||||
{
|
||||
"id": "marquee",
|
||||
"name": "Auto-scrolling marquee",
|
||||
"category": "slop",
|
||||
"description": "Continuously auto-scrolling content demands attention it has not earned and hides half its content at any moment. Reserve motion for content that changes; let readers move at their own pace."
|
||||
},
|
||||
{
|
||||
"id": "icon-tile-stack",
|
||||
"name": "Icon tile stacked above heading",
|
||||
"category": "slop",
|
||||
"description": "A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container."
|
||||
},
|
||||
{
|
||||
"id": "italic-serif-display",
|
||||
"name": "Italic serif display headline",
|
||||
"category": "slop",
|
||||
"description": "Oversized italic serif (Fraunces, Recoleta, Playfair, Newsreader-italic) as the primary hero headline reads as taste in isolation but has become the universal AI-startup landing page hero. Set roman, or move to a non-serif display face. Editorial / magazine register may legitimately want this — judge by context."
|
||||
},
|
||||
{
|
||||
"id": "hero-eyebrow-chip",
|
||||
"name": "Hero eyebrow / pill chip",
|
||||
"category": "slop",
|
||||
"description": "A tiny uppercase letter-spaced label sitting immediately above an oversized hero headline — or the same shape rendered as a pill chip — is now the default AI SaaS hero. Drop the eyebrow, integrate the kicker into the headline, or run it as a navigation breadcrumb instead."
|
||||
},
|
||||
{
|
||||
"id": "kicker-above-heading",
|
||||
"name": "Kicker / eyebrow label above heading",
|
||||
"category": "slop",
|
||||
"description": "A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body."
|
||||
},
|
||||
{
|
||||
"id": "numbered-section-labels",
|
||||
"name": "Tiny numbered section labels",
|
||||
"category": "slop",
|
||||
"description": "Small numeric index labels riding next to section headings, repeated section after section, are AI editorial scaffolding — a page numbering its own chapters instead of earning structure. Let hierarchy, content, and rhythm carry the sequence."
|
||||
},
|
||||
{
|
||||
"id": "em-dash-overuse",
|
||||
"name": "Em-dash overuse",
|
||||
"category": "slop",
|
||||
"description": "Em-dash saturation in body copy is an AI cadence tell. Advisory only: humans use em-dashes legitimately, so this fires only on saturation — at least 8 em-dashes (— or --) at a density near one per 500 characters of body text — never on a long article that uses a few. Prefer commas, colons, periods, or parentheses."
|
||||
},
|
||||
{
|
||||
"id": "marketing-buzzword",
|
||||
"name": "Marketing buzzword",
|
||||
"category": "slop",
|
||||
"description": "Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does."
|
||||
},
|
||||
{
|
||||
"id": "aphoristic-cadence",
|
||||
"name": "Aphoristic-cadence copy",
|
||||
"category": "slop",
|
||||
"description": "Three or more sections landing on a short rebuttal sentence (\"X. No Y.\" / \"X. Just Y.\") or a manufactured-contrast aphorism (\"Not a feature. A platform.\") reads as AI cadence, not voice. Once is fine; the pattern is the tell."
|
||||
},
|
||||
{
|
||||
"id": "oversized-h1",
|
||||
"name": "Oversized hero headline",
|
||||
"category": "slop",
|
||||
"description": "A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy."
|
||||
},
|
||||
{
|
||||
"id": "extreme-negative-tracking",
|
||||
"name": "Crushed letter spacing",
|
||||
"category": "slop",
|
||||
"description": "Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively."
|
||||
},
|
||||
{
|
||||
"id": "broken-image",
|
||||
"name": "Broken or placeholder image",
|
||||
"category": "quality",
|
||||
"description": "<img> tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag."
|
||||
},
|
||||
{
|
||||
"id": "script-error",
|
||||
"name": "Uncaught script error on load",
|
||||
"category": "quality",
|
||||
"description": "A script threw an uncaught exception or failed to parse while the page loaded. Broken JavaScript silently kills reveals, interactions, and dynamic content, and can leave most of a page invisible. Fix the error before judging anything else."
|
||||
},
|
||||
{
|
||||
"id": "content-hidden-at-rest",
|
||||
"name": "Content invisible at rest",
|
||||
"category": "quality",
|
||||
"description": "A large share of the page text sits at opacity 0 or visibility hidden even after every reveal handler had a chance to run. This is the failed-reveal signature: the content shipped but never becomes visible. Make content visible by default and let JavaScript enhance its entrance instead of gating its existence."
|
||||
},
|
||||
{
|
||||
"id": "edge-flush-cards",
|
||||
"name": "Cards flush against the scroller edge",
|
||||
"category": "quality",
|
||||
"description": "Cards inside a horizontal scroller or tab panel sit flush against the container edge at rest while keeping a gutter on the other side, so their edges and rounded corners get cut off. Usually the panel is sized wider than its clip box. Keep a consistent inset on both sides."
|
||||
},
|
||||
{
|
||||
"id": "text-occlusion",
|
||||
"name": "Text occluded by an overlapping element",
|
||||
"category": "quality",
|
||||
"description": "Text is painted under an opaque element or a second text run, so part of it cannot be read. A decorative box, a stacked layer, or an inline element with leaked padding lands on the words instead of beside them. Give overlapping layers room, or move the text out from under the layer above it."
|
||||
},
|
||||
{
|
||||
"id": "first-viewport-column-overflow",
|
||||
"name": "One column stretches the first viewport",
|
||||
"category": "quality",
|
||||
"description": "A multi-column opening section lets one column run far past the fold while its sibling fits in a single viewport, so the short column floats in dead space and the fold falls deep inside one section. Balance the columns, cap the tall one, or let the long content flow below the opening row."
|
||||
},
|
||||
{
|
||||
"id": "gray-on-color",
|
||||
"name": "Gray text on colored background",
|
||||
"category": "quality",
|
||||
"description": "Gray text looks washed out on colored backgrounds. Use a darker shade of the background color instead, or white/near-white for contrast."
|
||||
},
|
||||
{
|
||||
"id": "low-contrast",
|
||||
"name": "Low contrast text",
|
||||
"category": "quality",
|
||||
"description": "Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background."
|
||||
},
|
||||
{
|
||||
"id": "layout-transition",
|
||||
"name": "Layout property animation",
|
||||
"category": "quality",
|
||||
"description": "Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations."
|
||||
},
|
||||
{
|
||||
"id": "line-length",
|
||||
"name": "Line length too long",
|
||||
"category": "quality",
|
||||
"description": "Text lines wider than ~80 characters are hard to read. The eye loses its place tracking back to the start of the next line. Add a max-width (65ch to 75ch) to text containers."
|
||||
},
|
||||
{
|
||||
"id": "cramped-padding",
|
||||
"name": "Cramped padding",
|
||||
"category": "quality",
|
||||
"description": "Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers."
|
||||
},
|
||||
{
|
||||
"id": "body-text-viewport-edge",
|
||||
"name": "Body text touching viewport edge",
|
||||
"category": "quality",
|
||||
"description": "Body paragraphs render flush against the left or right viewport edge with no container providing horizontal padding. Wrap content in a container with at least 16px (ideally 24-32px) of horizontal padding, or apply max-width with mx-auto."
|
||||
},
|
||||
{
|
||||
"id": "tight-leading",
|
||||
"name": "Tight line height",
|
||||
"category": "quality",
|
||||
"description": "Line height below 1.3x the font size makes multi-line text hard to read. Use 1.5 to 1.7 for body text so lines have room to breathe."
|
||||
},
|
||||
{
|
||||
"id": "skipped-heading",
|
||||
"name": "Skipped heading level",
|
||||
"category": "quality",
|
||||
"description": "Heading levels should not skip (e.g. h1 then h3 with no h2). Screen readers use heading hierarchy for navigation. Skipping levels breaks the document outline."
|
||||
},
|
||||
{
|
||||
"id": "heading-rhythm",
|
||||
"name": "Heading crowded against the previous block",
|
||||
"category": "quality",
|
||||
"description": "A heading binds to the content it introduces, so the rendered space above it should exceed the space below it. When headings across a page sit as close or closer to the block above than to their own content, every section reads as if it captions the previous one. Open up the space above each heading."
|
||||
},
|
||||
{
|
||||
"id": "justified-text",
|
||||
"name": "Justified text",
|
||||
"category": "quality",
|
||||
"description": "Justified text without hyphenation creates uneven word spacing (\"rivers of white\"). Use text-align: left for body text, or enable hyphens: auto if you must justify."
|
||||
},
|
||||
{
|
||||
"id": "tiny-text",
|
||||
"name": "Tiny body text",
|
||||
"category": "quality",
|
||||
"description": "Body text below 12px is hard to read, especially on high-DPI screens. Use at least 14px for body content, 16px is ideal."
|
||||
},
|
||||
{
|
||||
"id": "undersized-ui-text",
|
||||
"name": "Undersized functional text",
|
||||
"category": "quality",
|
||||
"description": "Interactive and content-bearing UI text (links, buttons, nav items, labels, table cells, meta rows, timecodes) below 11px is a legibility failure, not a style choice. WCAG sets no absolute pixel floor, but functional text under 11px is a defensible quality bar: it fails on high-DPI and small viewports and it degrades tap and read targets. The 11px floor holds even inside a footer; only non-interactive legal smallprint gets the softer 10px floor. Being ON the DESIGN.md size ramp does not exempt a value here: adding 8px to the ramp launders the token but not the legibility problem, and that is exactly the escape hatch this rule closes. Exempts sup/sub, visually-hidden (sr-only) text, and code/terminal contexts. Decorative letterspaced micro-labels are still functional and stay in scope."
|
||||
},
|
||||
{
|
||||
"id": "all-caps-body",
|
||||
"name": "All-caps body text",
|
||||
"category": "quality",
|
||||
"description": "Long passages in uppercase are hard to read. We recognize words by shape (ascenders and descenders), which all-caps removes. Reserve uppercase for short labels and headings."
|
||||
},
|
||||
{
|
||||
"id": "wide-tracking",
|
||||
"name": "Wide letter spacing on body text",
|
||||
"category": "quality",
|
||||
"description": "Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only."
|
||||
},
|
||||
{
|
||||
"id": "text-overflow",
|
||||
"name": "Content overflowing its container",
|
||||
"category": "quality",
|
||||
"description": "Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance."
|
||||
},
|
||||
{
|
||||
"id": "repeated-container-text",
|
||||
"name": "Same text repeated inside one container",
|
||||
"category": "quality",
|
||||
"description": "The same literal text rendered three or more times in structurally different spots inside a single card or panel is redundant messaging — usually a status or label wired into every slot of a template. Say it once, in the slot where it matters most."
|
||||
},
|
||||
{
|
||||
"id": "clipped-overflow-container",
|
||||
"name": "Positioned child clipped by overflow container",
|
||||
"category": "quality",
|
||||
"description": "A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip."
|
||||
},
|
||||
{
|
||||
"id": "design-system-font",
|
||||
"name": "Font outside DESIGN.md",
|
||||
"category": "quality",
|
||||
"description": "A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition."
|
||||
},
|
||||
{
|
||||
"id": "design-system-color",
|
||||
"name": "Color outside DESIGN.md",
|
||||
"category": "quality",
|
||||
"description": "A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift."
|
||||
},
|
||||
{
|
||||
"id": "design-system-radius",
|
||||
"name": "Radius outside DESIGN.md",
|
||||
"category": "quality",
|
||||
"description": "A border-radius value is outside the DESIGN.md rounded scale. Use a documented radius token or update the design system if the new shape is intentional."
|
||||
},
|
||||
{
|
||||
"id": "design-system-font-size",
|
||||
"name": "Font size outside DESIGN.md",
|
||||
"category": "quality",
|
||||
"description": "A literal font-size is off the type ramp documented in DESIGN.md typography. Use a documented size step or update the design system if the new step is intentional."
|
||||
},
|
||||
{
|
||||
"id": "gpt-thin-border-wide-shadow",
|
||||
"name": "Hairline border with wide shadow",
|
||||
"category": "slop",
|
||||
"description": "A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once."
|
||||
},
|
||||
{
|
||||
"id": "repeating-stripes-gradient",
|
||||
"name": "Repeating-gradient stripes",
|
||||
"category": "slop",
|
||||
"description": "Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain."
|
||||
},
|
||||
{
|
||||
"id": "codex-grid-background",
|
||||
"name": "Decorative grid-line background",
|
||||
"category": "slop",
|
||||
"description": "A decorative grid or line-field background drawn with hairline linear-gradient layers tiled by a fixed pixel cell is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface."
|
||||
},
|
||||
{
|
||||
"id": "theater-slop-phrase",
|
||||
"name": "Theater framing copy",
|
||||
"category": "slop",
|
||||
"description": "Dismissing something as \"theater\" is a recurring generated-copy tic. Say plainly what the thing does or does not do."
|
||||
},
|
||||
{
|
||||
"id": "image-hover-transform",
|
||||
"name": "Image hover transform",
|
||||
"category": "slop",
|
||||
"description": "Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction."
|
||||
}
|
||||
]
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"capturedAt": "2026-09-08",
|
||||
"engine": {
|
||||
"version": "0.1.3",
|
||||
"release": "engine-v0.1.3",
|
||||
"platform": "linux-x64",
|
||||
"installedBy": "npx impeccable install (human-initiated, scratch directory outside the repo)",
|
||||
"binaryPath": "<skill>/scripts/bin/linux-x64/impeccable",
|
||||
"binaryNote": "the engine is installed as a sibling of the launcher (<skill>/scripts/impeccable), not under ~/.impeccable/bin/, in a project-scoped install"
|
||||
},
|
||||
"registry": {
|
||||
"file": "impeccable-antipatterns.json",
|
||||
"upstream": "crates/live/assets/antipatterns.json",
|
||||
"commit": "87d8f6d686782561fb572758d9a9bb8596a1a0e7",
|
||||
"entries": 61,
|
||||
"fields": ["id", "name", "category", "description"],
|
||||
"note": "no severity or advisory field in the registry; advisory status (em-dash-overuse) is engine-side"
|
||||
},
|
||||
"captures": {
|
||||
"impeccable-detect-sample.json": {
|
||||
"mode": "source",
|
||||
"command": "impeccable detect --json index.html styles.css",
|
||||
"cwd": "a copy of test/fixtures/review-eval-design-slop.{html,css} named index.html + styles.css so the <link> resolves",
|
||||
"exit": 2,
|
||||
"stderrBytes": 0,
|
||||
"pathsNormalized": { "index.html": "test/fixtures/review-eval-design-slop.html", "styles.css": "test/fixtures/review-eval-design-slop.css" }
|
||||
},
|
||||
"impeccable-detect-dom-sample.json": {
|
||||
"mode": "dom",
|
||||
"dump": "review-eval-design-slop.dom.html",
|
||||
"dumpedWith": "browse goto http://127.0.0.1:<port>/index.html; browse eval dom-dump.js --out <tmp>/review-eval-design-slop.dom.html --raw (script: lib/dom-dump-script.ts DOM_DUMP_SCRIPT)",
|
||||
"command": "impeccable detect --json review-eval-design-slop.dom.html",
|
||||
"exit": 2,
|
||||
"stderrBytes": 0,
|
||||
"pathsNormalized": { "review-eval-design-slop.dom.html": "test/fixtures/review-eval-design-slop.dom.html", "http://127.0.0.1:<port>/": "http://127.0.0.1/" }
|
||||
},
|
||||
"impeccable-detect-help.txt": { "command": "impeccable detect --help" }
|
||||
},
|
||||
"findingFields": ["antipattern", "name", "description", "severity", "category", "file", "line", "snippet"],
|
||||
"notes": [
|
||||
"HTML-mode findings carry line 0; snippet is the locator",
|
||||
"the static engine reads inline <style> in a .html file: the DOM dump yields the same id set as the source scan",
|
||||
"without the rgb()->hex fold in the dump script the DOM scan loses ai-color-palette (CSSOM serializes hex as rgb)",
|
||||
"a <link rel=stylesheet> left in the dump makes the engine warn on stderr about an unreadable stylesheet; the dump script removes inlined links",
|
||||
"the planted fixture has no border-left, so side-tab never fires on it; ai-color-palette is the deterministic slop id"
|
||||
]
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
[
|
||||
{
|
||||
"antipattern": "low-contrast",
|
||||
"name": "Low contrast text",
|
||||
"description": "Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.",
|
||||
"severity": "warning",
|
||||
"category": "quality",
|
||||
"file": "test/fixtures/review-eval-design-slop.dom.html",
|
||||
"line": 0,
|
||||
"snippet": "4.2:1 (need 4.5:1) — text #ffffff on #8b5cf6"
|
||||
},
|
||||
{
|
||||
"antipattern": "low-contrast",
|
||||
"name": "Low contrast text",
|
||||
"description": "Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.",
|
||||
"severity": "warning",
|
||||
"category": "quality",
|
||||
"file": "test/fixtures/review-eval-design-slop.dom.html",
|
||||
"line": 0,
|
||||
"snippet": "4.47:1 (need 4.5:1) — text #ffffff on #6366f1"
|
||||
},
|
||||
{
|
||||
"antipattern": "low-contrast",
|
||||
"name": "Low contrast text",
|
||||
"description": "Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.",
|
||||
"severity": "warning",
|
||||
"category": "quality",
|
||||
"file": "test/fixtures/review-eval-design-slop.dom.html",
|
||||
"line": 0,
|
||||
"snippet": "4.0:1 (need 4.5:1) — text #ff0000 on #1e1b4b"
|
||||
},
|
||||
{
|
||||
"antipattern": "skipped-heading",
|
||||
"name": "Skipped heading level",
|
||||
"description": "Heading levels should not skip (e.g. h1 then h3 with no h2). Screen readers use heading hierarchy for navigation. Skipping levels breaks the document outline.",
|
||||
"severity": "warning",
|
||||
"category": "quality",
|
||||
"file": "test/fixtures/review-eval-design-slop.dom.html",
|
||||
"line": 0,
|
||||
"snippet": "<h1> \"Welcome to Our Platform\" followed by <h3> \"Feature One\" (missing h2)"
|
||||
},
|
||||
{
|
||||
"antipattern": "ai-color-palette",
|
||||
"name": "AI color palette",
|
||||
"description": "Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.",
|
||||
"severity": "warning",
|
||||
"category": "slop",
|
||||
"file": "test/fixtures/review-eval-design-slop.dom.html",
|
||||
"line": 0,
|
||||
"snippet": "Purple/violet accent colors detected"
|
||||
},
|
||||
{
|
||||
"antipattern": "marketing-buzzword",
|
||||
"name": "Marketing buzzword",
|
||||
"description": "Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.",
|
||||
"severity": "warning",
|
||||
"category": "slop",
|
||||
"file": "test/fixtures/review-eval-design-slop.dom.html",
|
||||
"line": 0,
|
||||
"snippet": "1 buzzword phrase: \"ful tool to streamline your workflow ef\""
|
||||
}
|
||||
]
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
Usage: impeccable detect [options] [file-or-dir-or-url...]
|
||||
|
||||
Scan files or URLs for UI anti-patterns and design quality issues.
|
||||
|
||||
Options:
|
||||
--json Output results as JSON
|
||||
--quiet In text mode, only print the final findings count
|
||||
--scope <name> Only report rules in the given design domain
|
||||
(type, layout). Comma-separated.
|
||||
--viewport <WxH> Browser viewport for URL scans (default 1280x800),
|
||||
e.g. --viewport 390x844 for a mobile-width pass
|
||||
--no-config Do not apply project config, detector ignores, inline
|
||||
ignore comments, or DESIGN.md
|
||||
--no-inline-ignores Do not honor in-file impeccable-disable* ignore comments
|
||||
--no-design-system Do not load local DESIGN.md / .impeccable/design.json context
|
||||
--no-advisory Suppress advisory findings entirely (e.g. em-dash overuse)
|
||||
--help Show this help message
|
||||
|
||||
Advisory findings:
|
||||
Some rules are advisory: detected and listed in a separate section, but never
|
||||
counted as failures and never changing the exit code. They stay out of the
|
||||
failure count so they never block automation. --no-advisory hides them.
|
||||
|
||||
Output streams:
|
||||
Human-readable findings go to stderr so stdout stays available for structured
|
||||
output. Use --json for JSON on stdout, or redirect text with 2> findings.txt.
|
||||
|
||||
Exit status:
|
||||
0 Scan completed with no primary findings (advisories may still be listed)
|
||||
1 At least one requested target could not be scanned
|
||||
2 Scan completed with primary findings
|
||||
Operational failure takes precedence when a multi-target scan is partial.
|
||||
|
||||
Project config:
|
||||
Respects .impeccable/config.json and .impeccable/config.local.json detector
|
||||
settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,
|
||||
and detector.designSystem.enabled.
|
||||
|
||||
Inline ignores:
|
||||
In-file comments waive a finding where it lives and travel with the file:
|
||||
<!-- impeccable-disable overused-font -- exported brand doc -->
|
||||
.brand { font-family: Inter } /* impeccable-disable-line overused-font */
|
||||
// impeccable-disable-next-line bounce-easing: intentional bounce
|
||||
impeccable-disable applies to the whole file; -line / -next-line are scoped.
|
||||
List one or more rule ids (comma-separated), or omit them / use * for all.
|
||||
|
||||
Detection modes:
|
||||
HTML files Static HTML/CSS analysis (default, catches linked CSS)
|
||||
Non-HTML files Regex pattern matching (CSS, JSX, TSX, etc.)
|
||||
URLs Puppeteer full browser rendering (auto-detected;
|
||||
http(s):// and file:// URLs; accessible linked CSS included)
|
||||
|
||||
Examples:
|
||||
impeccable detect src/
|
||||
impeccable detect index.html
|
||||
impeccable detect https://example.com
|
||||
impeccable detect --json .
|
||||
impeccable detect --no-config src/
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
[
|
||||
{
|
||||
"antipattern": "low-contrast",
|
||||
"name": "Low contrast text",
|
||||
"description": "Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.",
|
||||
"severity": "warning",
|
||||
"category": "quality",
|
||||
"file": "test/fixtures/review-eval-design-slop.html",
|
||||
"line": 0,
|
||||
"snippet": "4.2:1 (need 4.5:1) — text #ffffff on #8b5cf6"
|
||||
},
|
||||
{
|
||||
"antipattern": "low-contrast",
|
||||
"name": "Low contrast text",
|
||||
"description": "Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.",
|
||||
"severity": "warning",
|
||||
"category": "quality",
|
||||
"file": "test/fixtures/review-eval-design-slop.html",
|
||||
"line": 0,
|
||||
"snippet": "4.47:1 (need 4.5:1) — text #ffffff on #6366f1"
|
||||
},
|
||||
{
|
||||
"antipattern": "low-contrast",
|
||||
"name": "Low contrast text",
|
||||
"description": "Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.",
|
||||
"severity": "warning",
|
||||
"category": "quality",
|
||||
"file": "test/fixtures/review-eval-design-slop.html",
|
||||
"line": 0,
|
||||
"snippet": "4.0:1 (need 4.5:1) — text #ff0000 on #1e1b4b"
|
||||
},
|
||||
{
|
||||
"antipattern": "skipped-heading",
|
||||
"name": "Skipped heading level",
|
||||
"description": "Heading levels should not skip (e.g. h1 then h3 with no h2). Screen readers use heading hierarchy for navigation. Skipping levels breaks the document outline.",
|
||||
"severity": "warning",
|
||||
"category": "quality",
|
||||
"file": "test/fixtures/review-eval-design-slop.html",
|
||||
"line": 0,
|
||||
"snippet": "<h1> \"Welcome to Our Platform\" followed by <h3> \"Feature One\" (missing h2)"
|
||||
},
|
||||
{
|
||||
"antipattern": "ai-color-palette",
|
||||
"name": "AI color palette",
|
||||
"description": "Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.",
|
||||
"severity": "warning",
|
||||
"category": "slop",
|
||||
"file": "test/fixtures/review-eval-design-slop.html",
|
||||
"line": 0,
|
||||
"snippet": "Purple/violet accent colors detected"
|
||||
},
|
||||
{
|
||||
"antipattern": "marketing-buzzword",
|
||||
"name": "Marketing buzzword",
|
||||
"description": "Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.",
|
||||
"severity": "warning",
|
||||
"category": "slop",
|
||||
"file": "test/fixtures/review-eval-design-slop.html",
|
||||
"line": 0,
|
||||
"snippet": "1 buzzword phrase: \"ful tool to streamline your workflow ef\""
|
||||
}
|
||||
]
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"><head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Our Platform</title>
|
||||
<style data-gstack-dom-css="">/* gstack-dom-dump: http://127.0.0.1/styles.css */
|
||||
body { font-family: Papyrus, sans-serif; font-size: 14px; margin: 0px; padding: 0px; }
|
||||
.hero { background: linear-gradient(135deg, #6366f1, #8b5cf6); text-align: center; padding: 80px 20px; color: white; }
|
||||
.hero h1 { text-align: center; font-size: 48px; }
|
||||
.hero p { text-align: center; font-size: 20px; }
|
||||
.features { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; padding: 60px 40px; text-align: center; }
|
||||
.feature-card { border-radius: 24px; padding: 32px; text-align: center; background: #f9fafb; }
|
||||
.icon-circle { width: 60px; height: 60px; border-radius: 50%; background: #ede9fe; display: flex; align-items: center; justify-content: center; margin: 0px auto 16px; font-size: 24px; }
|
||||
button { outline: none; background: #6366f1; color: white; border-width: medium; border-style: none; border-color: currentcolor; border-image: none; padding: 12px 24px; border-radius: 24px; cursor: pointer; }
|
||||
.small-link { font-size: 11px; padding: 4px 8px; }
|
||||
.override { color: red !important; margin-left: 10px !important; }
|
||||
.footer { text-align: center; padding: 40px; background: #1e1b4b; color: white; }</style></head>
|
||||
<body>
|
||||
<!-- Issue 6: [MEDIUM] Generic hero copy ("Welcome to...", "all-in-one solution") -->
|
||||
<div class="hero">
|
||||
<h1>Welcome to Our Platform</h1>
|
||||
<p>Your all-in-one solution for everything you need</p>
|
||||
<button>Get Started</button>
|
||||
</div>
|
||||
|
||||
<!-- Issue 7: [LOW] 3-column feature grid with icon-in-circle + title + description -->
|
||||
<div class="features">
|
||||
<div class="feature-card">
|
||||
<div class="icon-circle">★</div>
|
||||
<h3>Feature One</h3>
|
||||
<p>A short description of this amazing feature that will change your life.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="icon-circle">⚡</div>
|
||||
<h3>Feature Two</h3>
|
||||
<p>Another incredible capability that sets us apart from the competition.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="icon-circle">⚙</div>
|
||||
<h3>Feature Three</h3>
|
||||
<p>Yet another powerful tool to streamline your workflow effortlessly.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p class="override">Unlock the power of our platform today</p>
|
||||
<a href="#" class="small-link">Terms of Service</a>
|
||||
</div>
|
||||
|
||||
|
||||
</body></html>
|
||||
<!-- gstack-dom-dump: shadow DOM and constructed stylesheets not captured -->
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* impeccable fixture pins (commit 1 of the design-detector interop).
|
||||
*
|
||||
* gstack never runs impeccable's engine in CI. What the detector wrapper and
|
||||
* the catalog rely on is pinned here from real captures instead:
|
||||
* - the rule registry (61 ids) at the commit the engine-v0.1.3 release shipped
|
||||
* - the `detect --json` output shape over gstack's own planted-slop fixture,
|
||||
* once as a source scan and once over the rendered-DOM dump that
|
||||
* lib/dom-dump-script.ts produces through the browse engine
|
||||
* - the dump script's own contract (IIFE, no single quotes, no `${`)
|
||||
* Re-capture protocol: test/fixtures/impeccable-captures.meta.json.
|
||||
*/
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { DOM_DUMP_SCRIPT, DOM_DUMP_STYLE_ATTR, DOM_DUMP_NOTE_PREFIX } from '../lib/dom-dump-script';
|
||||
|
||||
const FIXTURES = path.join(import.meta.dir, 'fixtures');
|
||||
const read = (name: string) => fs.readFileSync(path.join(FIXTURES, name), 'utf-8');
|
||||
const json = (name: string) => JSON.parse(read(name));
|
||||
|
||||
interface RegistryEntry { id: string; name: string; category: string; description: string }
|
||||
interface Finding {
|
||||
antipattern: string; name: string; description: string; severity: string;
|
||||
category: string; file: string; line: number; snippet: string;
|
||||
}
|
||||
|
||||
const registry = json('impeccable-antipatterns.json') as { _source: Record<string, string>; rules: RegistryEntry[] };
|
||||
const sourceSample = json('impeccable-detect-sample.json') as Finding[];
|
||||
const domSample = json('impeccable-detect-dom-sample.json') as Finding[];
|
||||
const meta = json('impeccable-captures.meta.json');
|
||||
const dump = read('review-eval-design-slop.dom.html');
|
||||
const registryIds = new Set(registry.rules.map(r => r.id));
|
||||
const categoryOf = new Map(registry.rules.map(r => [r.id, r.category]));
|
||||
|
||||
describe('impeccable rule registry fixture', () => {
|
||||
test('is the upstream file at a pinned commit', () => {
|
||||
expect(registry._source.path).toBe('crates/live/assets/antipatterns.json');
|
||||
expect(registry._source.commit).toMatch(/^[0-9a-f]{40}$/);
|
||||
expect(registry._source.engineRelease).toBe('engine-v0.1.3');
|
||||
expect(meta.registry.commit).toBe(registry._source.commit);
|
||||
});
|
||||
|
||||
test('has 61 well-formed entries with unique kebab-case ids', () => {
|
||||
expect(registry.rules.length).toBe(61);
|
||||
expect(meta.registry.entries).toBe(61);
|
||||
for (const r of registry.rules) {
|
||||
expect(r.id).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
|
||||
expect(r.name.length).toBeGreaterThan(0);
|
||||
expect(r.description.length).toBeGreaterThan(0);
|
||||
expect(['slop', 'quality']).toContain(r.category);
|
||||
}
|
||||
expect(registryIds.size).toBe(61);
|
||||
});
|
||||
|
||||
test('splits 32 slop / 29 quality', () => {
|
||||
const slop = registry.rules.filter(r => r.category === 'slop').length;
|
||||
expect(slop).toBe(32);
|
||||
expect(registry.rules.length - slop).toBe(29);
|
||||
});
|
||||
|
||||
test('carries the ids the doctrine names', () => {
|
||||
for (const id of ['side-tab', 'overused-font', 'nested-cards', 'kicker-above-heading', 'icon-tile-stack',
|
||||
'gradient-text', 'ai-color-palette', 'cream-palette', 'dark-glow', 'pulsing-dot', 'em-dash-overuse',
|
||||
'low-contrast', 'broken-image', 'design-system-font', 'design-system-color', 'design-system-radius',
|
||||
'design-system-font-size']) {
|
||||
expect(registryIds.has(id)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function checkFindings(sample: Finding[], expectedFile: string) {
|
||||
expect(Array.isArray(sample)).toBe(true);
|
||||
expect(sample.length).toBeGreaterThan(0);
|
||||
for (const f of sample) {
|
||||
expect(Object.keys(f).sort()).toEqual(meta.findingFields.slice().sort());
|
||||
expect(registryIds.has(f.antipattern)).toBe(true);
|
||||
expect(f.category).toBe(categoryOf.get(f.antipattern));
|
||||
expect(typeof f.severity).toBe('string');
|
||||
expect(typeof f.line).toBe('number');
|
||||
expect(typeof f.snippet).toBe('string');
|
||||
expect(f.file).toBe(expectedFile);
|
||||
expect(f.file.startsWith('/')).toBe(false);
|
||||
}
|
||||
}
|
||||
|
||||
describe('detect --json source-scan sample', () => {
|
||||
test('is a real capture over the planted-slop fixture, paths normalized', () => {
|
||||
checkFindings(sourceSample, 'test/fixtures/review-eval-design-slop.html');
|
||||
expect(meta.captures['impeccable-detect-sample.json'].exit).toBe(2);
|
||||
});
|
||||
|
||||
test('contains a deterministic slop id and a quality id', () => {
|
||||
const ids = new Set(sourceSample.map(f => f.antipattern));
|
||||
expect(ids.has('ai-color-palette')).toBe(true);
|
||||
expect(ids.has('low-contrast')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detect --json DOM-dump sample', () => {
|
||||
test('is a real capture over the committed dump, paths normalized', () => {
|
||||
checkFindings(domSample, 'test/fixtures/review-eval-design-slop.dom.html');
|
||||
expect(meta.captures['impeccable-detect-dom-sample.json'].exit).toBe(2);
|
||||
expect(meta.captures['impeccable-detect-dom-sample.json'].stderrBytes).toBe(0);
|
||||
});
|
||||
|
||||
test('the static engine reads inlined <style>: same id set as the source scan', () => {
|
||||
const src = [...new Set(sourceSample.map(f => f.antipattern))].sort();
|
||||
const dom = [...new Set(domSample.map(f => f.antipattern))].sort();
|
||||
expect(dom).toEqual(src);
|
||||
});
|
||||
});
|
||||
|
||||
describe('committed DOM dump', () => {
|
||||
test('came from the dump script: inlined-style marker, trailing note, no leftover stylesheet link', () => {
|
||||
expect(dump.startsWith('<!DOCTYPE html>\n')).toBe(true);
|
||||
expect(dump).toContain(`<style ${DOM_DUMP_STYLE_ATTR}=""`);
|
||||
expect(dump).toContain(`<!-- ${DOM_DUMP_NOTE_PREFIX} `);
|
||||
expect(dump).not.toMatch(/<link[^>]*rel="?stylesheet/);
|
||||
});
|
||||
|
||||
test('folds CSSOM rgb() back to the author hex so palette rules still fire', () => {
|
||||
expect(dump).toContain('#6366f1');
|
||||
expect(dump).not.toMatch(/rgb\(\d+, \d+, \d+\)/);
|
||||
});
|
||||
|
||||
test('carries no capture-time port or temp path', () => {
|
||||
expect(dump).not.toMatch(/127\.0\.0\.1:\d+/);
|
||||
expect(dump).not.toContain('/tmp/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DOM_DUMP_SCRIPT contract', () => {
|
||||
test('is an expression that fits inside a single-quoted bash string and a template literal', () => {
|
||||
expect(DOM_DUMP_SCRIPT).not.toContain("'");
|
||||
expect(DOM_DUMP_SCRIPT).not.toContain('${');
|
||||
expect(DOM_DUMP_SCRIPT).not.toContain('`');
|
||||
expect(DOM_DUMP_SCRIPT.trim().startsWith('(() => {')).toBe(true);
|
||||
expect(DOM_DUMP_SCRIPT.trim().endsWith('})()')).toBe(true);
|
||||
// Parses as a JS expression (what `$B eval` and Aside `pg.evaluate` wrap).
|
||||
expect(() => new Function('return ' + DOM_DUMP_SCRIPT)).not.toThrow();
|
||||
});
|
||||
|
||||
test('works on a clone and applies the hygiene rules', () => {
|
||||
expect(DOM_DUMP_SCRIPT).toContain('document.documentElement.cloneNode(true)');
|
||||
expect(DOM_DUMP_SCRIPT).toContain(DOM_DUMP_STYLE_ATTR);
|
||||
expect(DOM_DUMP_SCRIPT).toContain(DOM_DUMP_NOTE_PREFIX);
|
||||
for (const rule of ['querySelectorAll("script")', 'querySelectorAll("textarea")', 'value.length > 32',
|
||||
'name === "content" && el.nodeName === "META"', 'value.split("?")[0]', 'value.length > 1024',
|
||||
'gstack-stripped', 'cloneLinks[i].remove()']) {
|
||||
expect(DOM_DUMP_SCRIPT).toContain(rule);
|
||||
}
|
||||
});
|
||||
|
||||
test('lib module is pure: no I/O, no scripts/ imports', () => {
|
||||
const src = fs.readFileSync(path.join(import.meta.dir, '..', 'lib', 'dom-dump-script.ts'), 'utf-8');
|
||||
expect(src).not.toMatch(/^import /m);
|
||||
expect(src).not.toMatch(/from ['"]\.\.\/scripts/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detect --help fixture', () => {
|
||||
test('pins the flags and exit codes the wrapper relies on', () => {
|
||||
const help = read('impeccable-detect-help.txt');
|
||||
expect(help).toContain('--json');
|
||||
expect(help).toContain('--no-config');
|
||||
expect(help).toMatch(/0\s+Scan completed with no primary findings/);
|
||||
expect(help).toMatch(/1\s+At least one requested target could not be scanned/);
|
||||
expect(help).toMatch(/2\s+Scan completed with primary findings/);
|
||||
expect(help).toContain('impeccable-disable');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user