Redesign an existing website to make it better. Built on top of impeccable.
71
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
The shape of stardust/current/_brand-extraction.json and the
procedure that produces it. Run once per extract invocation, after
the capture phases have finished. Aggregation may accumulate
incrementally as concurrent page captures complete
(extract/SKILL.md § Concurrency); the file itself is written once,
when every page has landed.
Some fields are sourced from the home page only (logo, voice samples, hero-specific copy). Others must be aggregated across all extracted pages to avoid the home-page bias documented in § Aggregation scope.
The brand surface is the descriptive ground truth for what the
existing site looks and feels like at the brand level. It is the
input both to direct authoring of stardust/current/PRODUCT.md and
DESIGN.md (Phase 4 of extract) and to the direct sub-command
when it later reasons about how far to move from the current brand.
Stardust does not invent brand-surface values. Every value is either captured directly from the page or aggregated from captured values, and carries a source citation.
{
"_provenance": {
"writtenBy": "stardust:extract",
"writtenAt": "2026-04-25T13:50:00Z",
"readArtifacts": [
"https://example.com/",
"stardust/current/pages/home.json"
],
"synthesizedInputs": [],
"stardustVersion": "0.10.0"
},
"site": {
"name": "Example",
"tagline": "...",
"originUrl": "https://example.com"
},
"origins": [ /* see § Origins */ ],
"logo": { /* see § Logo */ },
"palette": [ /* see § Palette */ ],
"type": { /* see § Type */ },
"spacing": { /* see § Spacing */ },
"motifs": { /* see § Motifs */ },
"componentStyle": { /* see § Component style */ },
"systemComponents": [ /* see § System components */ ],
"iconFont": null, // null | { /* see § Icon font */ }
"voice": { /* see § Voice */ },
"voiceTable": { /* see § Voice table */ },
"crossPromo": { /* see § Cross-promo */ },
"register": "brand" // "brand" | "product" | "ambiguous"
}Each field below is sourced from one of two scopes:
| scope | fields | rationale |
|---|---|---|
| home-only | logo, voice.heroHeadline, voice.heroSubcopy, voice.heroImage, voice.heroMedium, voice.primaryCTALabel, voice.firstParagraph, register | These are the landing surface; aggregating across pages would dilute, not clarify |
| cross-page | palette, type (sizes, weights, families used), spacing, motifs.borderRadius, motifs.shadows, motifs.gradients, componentStyle, voice.ctaSamples, voice.navItems, voice.footerHeadings | Home-only readings are biased by hero/CTA-heavy markup; the dominant motif on the site as a whole is what direct needs |
Cross-page aggregation rules:
stardust/current/pages/<slug>.json produced in Phase 2,
plus every brand-sources/<host>/pages/<slug>.json when brand
sources were captured (§ Origins).primary value; second mode
as secondary; pill-radius (≥9999px or ≥50% on a square element)
captured separately as pill if present.sources so the agent can verify and direct can reason about
"where this motif lives."If a field's mode-on-home and mode-cross-page disagree, prefer the
cross-page value and surface the divergence in _provenance.notes
(e.g. "home suggested borderRadius=150px (pill, buttons-only); cross-page mode is 3px (cards/inputs/chips, 122 occurrences)").
This divergence note is the single most important hint for direct
when deciding whether to keep, soften, or replace the existing motif.
Which origins contributed evidence to this file. Always present:
single-origin runs emit a one-entry array. Multi-origin entries
appear when extract runs with --brand-source
(extract/SKILL.md § Cross-site brand sources).
[
{
"origin": "https://example.com",
"role": "primary", // "primary" | "brand-source" | "design-source"
"pagesCaptured": 5,
"contributedSignals": [] // primary owns everything not attributed elsewhere
},
{
"origin": "https://example.co.uk",
"role": "brand-source",
"pagesCaptured": 3,
"contributedSignals": [
"motifs.patterns[duotone-photography]",
"palette[accent-2]",
"voice.ctaSamples[+2]"
]
}
]Merge rules:
_provenance.notes, not silently dropped.contributedSignals[] so downstream direct /
uplift can amplify it as captured evidence with an origin
citation, not as invention.primary / mode —
it lands as secondary, accent-N, or an additional pattern.role: "design-source" appears only in
stardust/canon-source/_brand-extraction.json (the donor's own
capture, per extract/SKILL.md § Cross-site brand sources); donor
evidence never aggregates into the primary file.{
"source": "inline-svg", // one of: inline-svg | img | apple-touch-icon | og-image | favicon | synthesized
"sourceSelector": "header svg", // CSS selector or URL — null only for synthesized
"localPath": "stardust/current/assets/logo.svg",
"format": "svg", // svg | png | jpg | ico
"intrinsicWidth": 180,
"intrinsicHeight": 32,
"synthesized": false,
"synthesizedBasis": null // e.g. "Brand initials EX, derived from page title"
}Locator priority chain in playwright-recipe.md § Logo locator chain.
First hit wins.
Aggregated computed colors from across all extracted pages (see § Aggregation scope). Frequency-sorted, near-duplicates clustered, role-named.
[
{
"role": "background",
"value": "#ffffff",
"occurrences": 3421, // pixels weighted by element area
"sourceSelectors": ["body", ".container", ".hero"]
},
{
"role": "text-primary",
"value": "#0f1217",
"occurrences": 894,
"sourceSelectors": ["h1", "h2", "p"]
},
{
"role": "primary",
"value": "#147aff",
"occurrences": 412,
"sourceSelectors": [".btn-primary", "a"],
"sources": ["home", "pricing", "donate"],
"usedAs": ["background", "border", "fill"]
},
{
"role": "surface",
"value": "#f7f8fa",
"occurrences": 280,
"sourceSelectors": [".card", "section.alt"]
},
{
"role": "border",
"value": "#e5e7eb",
"occurrences": 156,
"sourceSelectors": [".card", "input"]
}
]Aggregation rules:
rgba(*, 0),
rgba(0, 0, 0, 0), the literal string transparent, and any
CSS color whose alpha channel is 0 are not colors — they are
"no color." If a section's background-color resolves to one
of these, the section inherits its parent's effective background;
the transparent value must not enter the palette aggregation.
Picking up rgba(255, 255, 255, 0) as a surface role with high
occurrence count (because most elements default to transparent
backgrounds in CSS) is a real bug observed on
a nonprofit shelter site.ΔE < 5 (CIE76 in Lab space) and pick the
most frequent member as the cluster representative.#000 and #fff are kept verbatim — do not silently tint
them. Stardust's job here is descriptive, not corrective. The
divergence toolkit (in direct) will flag pure black/white when
the redesign target rejects them.background;
most-frequent text color → text-primary; most-frequent
background-color on [role="button"] and .btn-* → primary; etc.
When a role can't be assigned, use accent-N with N ascending._provenance.notes.usedAs: a deduped list drawn
from { "background", "text", "border", "fill", "stroke", "outline" }
reflecting which CSS properties this color appeared as across the
crawl. A color that appears only as text and never as
background/border/fill is the signal the Tensions detector
uses for color-imbalance flagging
(brand-review-template.md § Detectors).{
"headingFamily": {
"name": "Söhne",
"stack": "\"Söhne\", system-ui, sans-serif",
"weights": [400, 600, 700],
"sizes": ["clamp(2rem, 5vw, 3.5rem)", "2.25rem", "1.75rem"],
"lineHeights": [1.1, 1.15, 1.2],
"letterSpacing": ["-0.02em", "-0.015em", "-0.01em"],
"sourceSelectors": ["h1", "h2"]
},
"bodyFamily": {
"name": "Inter",
"stack": "\"Inter\", system-ui, sans-serif",
"weights": [400, 500],
"sizes": ["1rem", "0.9375rem", "0.8125rem"],
"lineHeights": [1.5, 1.55, 1.4],
"letterSpacing": ["normal"],
"sourceSelectors": ["p", "li", "label"]
},
"monoFamily": null,
"scaleRatio": 1.25, // see § Modular-scale audit below; null if ad-hoc
"scaleAudit": {
"kind": "modular", // "modular" | "ad-hoc"
"ratios": [1.25, 1.25, 1.20],
"matchedScale": "major-third" // null when kind == "ad-hoc"
},
"loadStrategy": "swap", // detected from font-display in @font-face rules
"files": [ // captured font files (per playwright-recipe.md § Capture list 16)
{
"url": "https://example.com/fonts/sohne-buch.woff2",
"family": "Söhne",
"weight": 400,
"style": "normal",
"unicodeRange": "U+0000-00FF",
"localPath": "stardust/current/assets/fonts/sohne-buch.woff2",
"sourceCssRule": "@font-face { font-family: 'Söhne'; src: url('/fonts/sohne-buch.woff2') format('woff2'); ... }",
"licensingFlag": "private" // null | "private" | "open"
}
]
}files[] is required when at least one captured page references
a @font-face rule. Empty array when the site uses only
system-stack fallbacks. licensingFlag defaults to null; set
to "private" when the family name does not appear in the
known-open-licence list (Google Fonts, Adobe Fonts free tier,
fontsource.org); "open" when it does. The flag is heuristic —
a "private" flag means "verify before redeploying with
prototype output", not "this font is non-free."
Identify heading vs body by which family appears in the heading
outline (pages/<slug>.json § Headings) most often. If only one
family is in use, set both headingFamily and bodyFamily to it
with disjoint weights.
Per-level heading sizes are picked by weighted score, not mode-of-frequency:
score = pixelSize × (fontWeight / 400) × √count
For each heading level present in the crawl, group captured headings
by (fontSize, fontWeight) tuple, compute the score per group, and
emit the top-scoring group's fontSize and fontWeight as that
level's representative.
Rationale: WordPress / SEO-plugin patterns ship hidden semantic H1s
on every page (e.g. position: absolute; clip: rect(0 0 0 0)).
Mode-of-size lets 20 hidden 18px H1s outweigh 2 visible 60px hero H1s
by raw count. The pixel-and-weight scoring biases toward the
visually-dominant variant — the display H1 is large and heavy and
repeated visibly across the site, so it always outranks accessibility
scaffolding under this rule.
This rule applies to per-level size selection in
type.headingFamily.sizes[]. Mode-of-frequency continues to apply
to motif aggregation (border-radius, shadow) per the rules in
§ Aggregation scope.
After heading sizes are collected, compute the ratio between every consecutive pair (largest → smallest, in px). Compare each ratio to the canonical scale set:
| name | ratio |
|---|---|
| minor-second | 1.067 |
| major-second | 1.125 |
| minor-third | 1.200 |
| major-third | 1.250 |
| perfect-fourth | 1.333 |
| augmented-fourth | 1.414 |
| perfect-fifth | 1.500 |
| golden | 1.618 |
If every observed ratio is within ±0.025 of the same canonical
ratio, set scaleAudit.kind = "modular", matchedScale = <name>, and
scaleRatio = <ratio>.
Otherwise set scaleAudit.kind = "ad-hoc", matchedScale = null,
scaleRatio = null, and list the observed ratios verbatim in
scaleAudit.ratios. Page-builder sites (Elementor, Webflow, Squarespace)
frequently end up here — large uneven jumps like 14 → 18 → 20 → 32 → 45 → 60.
direct reads scaleAudit.kind to decide whether the redesign target
should adopt a modular scale by default.
{
"baseUnit": 4, // 4 | 8 — inferred from mode of paddings/gaps
"scale": [4, 8, 12, 16, 24, 32, 48, 64, 96],
"sectionPadding": "96px",
"containerMaxWidth": "1280px",
"gridGap": "24px"
}If the existing site lacks rhythm (paddings are arbitrary, no
detectable scale), set scale: [] and baseUnit: null — the
divergence toolkit will flag this in direct.
Signature visual moves the existing site uses repeatedly.
borderRadius, shadows, and gradients are aggregated cross-page
per § Aggregation scope; patterns is observed where it occurs and
labelled with the page list in evidence.
{
"borderRadius": {
"primary": "8px", // mode of non-zero border-radii across all pages, weighted by element count
"secondary": "16px",
"pill": "9999px",
"primarySources": ["home", "about", "stories"],
"occurrences": { "8px": 122, "16px": 53, "9999px": 38, "2px": 30 }
},
"shadows": [
{ "value": "0 1px 2px rgba(0,0,0,0.06)", "uses": "buttons" },
{ "value": "0 4px 16px rgba(0,0,0,0.08)", "uses": "cards" },
{ "value": "0 24px 48px rgba(0,0,0,0.12)", "uses": "modals" }
],
"gradients": [
{ "value": "linear-gradient(135deg, #147aff 0%, #6b21ff 100%)", "uses": "hero-background" }
],
"patterns": [
{ "name": "card-grid", "evidence": "3 sections use 3-column repeat-cards" },
{ "name": "hero-with-image", "evidence": "home hero uses split-half image+copy" },
{ "name": "social-proof-strip", "evidence": "logo strip after hero" }
]
}Patterns is the open-ended one. Common values to watch for: card-grid,
hero-with-image, hero-with-illustration, feature-3up, feature-list,
stat-row, pricing-3up, social-proof-logos, testimonial-carousel,
cta-band, footer-mega, nav-mega. Add new pattern names freely;
this is descriptive.
The v1 fields, preserved so nothing is lost when DESIGN.json's
extensions block carries them forward.
{
"buttons": {
"primary": {
"background": "#147aff",
"color": "#ffffff",
"borderRadius": "8px",
"padding": "12px 24px",
"fontWeight": 600,
"shadow": "0 1px 2px rgba(0,0,0,0.06)",
"hoverDelta": "lighten 6%"
},
"secondary": { /* ... */ },
"ghost": { /* ... */ }
},
"dualCTAPattern": "primary-then-secondary-link", // observed if both appear together; null otherwise
"cards": { "background": "#f7f8fa", "borderRadius": "12px", "padding": "24px", "shadow": null, "border": "1px solid #e5e7eb" },
"inputs": { "borderRadius": "8px", "padding": "10px 12px", "border": "1px solid #d1d5db", "focusRing": "0 0 0 3px rgba(20,122,255,0.2)" }
}Cross-page repeated DOM blocks. These are almost always the most
load-bearing surfaces of the site (site header, site footer,
cross-promo strips, persistent CTAs, breadcrumbs). Missing them at
the brand-surface stage means direct cannot decide deliberately
whether to keep, move, or kill them — they silently disappear from
the redesign target.
[
{
"name": "site-header",
"kind": "header", // header | footer | cross-promo | nav-secondary | sidebar | cta-band | breadcrumb | background-motif | other
"occurrences": 12, // pages where it appears (out of pages crawled)
"headingSequence": ["About", "Stories", "Donate"],
"ctaLabels": ["Donate now"],
"domFingerprintHash": "sha256:...",
"exampleSlug": "home", // representative page; the verbatim block lives on this slug
"exampleSelector": "header > nav.primary",
"examplePages": ["home", "about", "stories"]
}
]Detection algorithm (heading-sequence diff, the cheap version that captures ~80% of the value):
(landmark tag, ordered list of immediate-child heading texts + CTA labels).kind heuristic:
header / [role="banner"] → headerfooter / [role="contentinfo"] → footercross-promonav not in header/footer → nav-secondaryaside / [role="complementary"] → sidebarcta-bandHome > Section > Page → breadcrumbotherexampleSlug at exampleSelector) so direct and prototype can
reason about the actual content, not just the structure.Beyond DOM-fingerprint repeats, also detect shared CSS background images across pages. A background image used on home AND on about-us is almost always a system motif (a recurring banner, a brand-photo treatment, a section ground) — not page-specific content.
Algorithm:
pages/<slug>.json#media.cssBackgrounds[].url across
the crawl (after playwright-recipe.md capture step 11).kind: "background-motif", occurrences = page
count, examplePages = the page slugs.exampleBlock for a background-motif entry is the
domPath + boundingClientRect + backgroundSize /
backgroundPosition of one representative occurrence — enough
for direct and prototype to reason about how the brand
deploys the image.This catches the case where a hero photo applied via
background-image is shared across multiple pages but invisible
to <img>-based capture. Without this aggregation, prototype
routinely places brand photos at the wrong section position
because it never saw them in the first place.
When pages crawled < 3, skip detection and emit
systemComponents: [] with a _provenance.notes line ("system
component detection requires ≥ 3 pages; crawl too small").
DOM-fingerprint diff (the thorough version) is out of scope for v0.2 — file an issue if the heading-sequence version misses important blocks.
When the site uses an icon font (per playwright-recipe.md
§ Capture list 17), capture the family name, the saved file, and
the iconClass → codepoint table so downstream prototype and
migrate can render the brand's actual icons rather than emoji
fallbacks.
{
"family": "panynj-icons",
"localPath": "stardust/current/assets/fonts/panynj-icons.woff2",
"sourceCss": "@font-face { font-family: 'panynj-icons'; src: url('/fonts/panynj-icons.woff2') format('woff2'); ... }",
"glyphCount": 19, // distinct icon classes captured (subset of the font's glyph table — only the ones the site actually uses)
"glyphs": [
{ "class": "icon-search", "codepoint": "\\E928", "name": "search" },
{ "class": "icon-accessible", "codepoint": "\\E852", "name": "accessible" },
{ "class": "icon-arrow-right", "codepoint": "\\E806", "name": "arrow-right" }
]
}Set iconFont: null when no icon font is detected (the common
case for SVG-icon and inline-graphic sites). When detected, every
glyph the captured pages used appears in glyphs[]; glyphs the
font defines but the site doesn't deploy are out of scope.
name is a heuristic guess from the class suffix
(icon-search → "search"); leave null when the suffix is
opaque (icon-i7, glyph-22, etc.).
Sampled copy from the home page. Used by direct to reason about
tone moves and to seed the voice.examples.do/dont arrays in
DESIGN.json.
{
"heroHeadline": "Build, ship, and own your work",
"heroSubcopy": "...",
"heroImage": {
"url": "https://example.com/img/hero-2-engineers-tablet.avif",
"alt": "Two engineers reviewing a tablet at a standing desk",
"source": "css-background", // "img" | "css-background" | "css-pseudo-background" | null
"domPath": "main > section.hero",
"localPath": "stardust/current/assets/media/hero-a3f9.avif",
"rect": { "x": 0, "y": 80, "width": 1440, "height": 720 }
},
"heroMedium": {
"kind": "video", // "video" | "canvas" | "lottie" | "animated-svg" | "scroll-motion" | null
"mechanism": "hls", // "video-file" | "hls" | "youtube/vimeo-embed" | "webgl" | "lottie" | "css-keyframes" | "scroll-driven"
"src": "https://customer-xxxx.cloudflarestream.com/<id>/manifest/video.m3u8",
"domPath": "div.home-video-wrapper > video#background-video",
"loader": "hls.js@cdn", // library the source uses to drive it, if any
"rect": { "x": 0, "y": 0, "width": 1440, "height": 900 }
},
"primaryCTALabel": "Start free trial",
"ctaSamples": ["Start free trial", "Talk to sales", "See pricing", "Read the docs"],
"navItems": ["Product", "Pricing", "Customers", "Docs", "Sign in"],
"footerHeadings": ["Product", "Company", "Resources", "Legal"],
"firstParagraph": "...",
"tone": {
"guess": "professional-warm", // descriptive guess; one of: professional-warm | professional-formal | playful-bright | playful-dry | technical-precise | aspirational | bold-direct | other
"evidence": "short sentences, second-person address, no jargon"
}
}heroImage resolutionThe hero photo is the home page's most load-bearing visual asset.
Without this field elevated, downstream prototype has to re-derive
"which captured image is the hero" from a noisy 16-image list every
render — and frequently picks the og:image (a curated thumbnail
optimised for social cards) instead of the actual visible hero. The
2026-05-04 logistics home shipped the wrong hero on three variants
this way before the user noticed.
Resolve from the home page's captured media (both
pages/home.json#media.images[] and media.cssBackgrounds[] —
the second covers background-image and ::before /
::after pseudo-element heroes per playwright-recipe.md
§ Capture list 11):
rect.top < 800 px (within the
first viewport).rect.width × rect.height ≥
100 000 px² (excludes header logos, tiny chips, decorative
columns).rect.aspectRatio in [0.3, 3.0]
— excludes thin banners and tall vertical strips.<img> over cssBackgrounds[] over
cssBackgrounds[] with pseudo selector (slight preference
for explicit DOM imagery, which downstream consumers can
manipulate more reliably).Set source to "img", "css-background", or
"css-pseudo-background" based on which capture produced the
winner. When no candidate survives the filters, set heroImage: null and surface in _provenance.notes (most non-marketing
pages — search, 404, login — legitimately have no hero).
Downstream prototype reads heroImage.url /
heroImage.localPath directly when composing the hero slot
under Mode A's image-reuse contract (per
skills/direct/SKILL.md § Mode A).
heroMedium resolution (signature hero medium)The hero is often not a still image but a moving asset — a
background video (file / HLS / Mux / Cloudflare Stream), a
canvas/WebGL scene, a Lottie/Rive animation, an animated SVG, or a
scroll-driven motion. This is the page's signature and is the most
common thing a generic redesign silently flattens to a static hero.
Elevate it here so downstream prototype preserves it under
intent-dimensions.md § 8b (Signature preservation) instead of
re-deriving it from the raw media inventory and missing it — the
exact failure that dropped a fintech home's HLS brand-animation
on the first migration pass.
Resolve from the home page's captured media
(pages/home.json#media): the <video> / <iframe> inventory
(playwright-recipe.md § Capture list — every <video>/<iframe>
src), <canvas> presence, and any motion library detected among
loaded scripts (hls.js, lottie, three.js, gsap, lenis).
<video> / <canvas> / Lottie mount whose rect
intersects the first viewport (rect.top < 800) and covers a
large area (same area/aspect filters as heroImage).kind + mechanism + src (the manifest/file/embed URL —
reuse it the way image-fidelity reuses image URLs) + loader
(the driving library, so prototype can wire it).kind: "scroll-motion" with mechanism naming the technique and
src: null.heroMedium: null
(most pages — the still heroImage is then the hero).A non-null heroMedium is a § 8b trigger: every Mode A variant must
reproduce it (graceful fallback + prefers-reduced-motion + scrim),
and doing so is exempt from the per-page surprise budget.
The tone.guess is a heuristic — never present it as ground truth in
the user report. direct will use it as one of several signals, not
as a fact.
Cross-page aggregations that fuel the brand-review's data tables. The
voice block above samples the home page; voiceTable aggregates
across all extracted pages so the review can show frequency, not
just examples.
{
"ctaFrequency": [
{ "label": "Donate", "total": 35, "pageCount": 10, "pages": ["home", "about-us", ...] },
{ "label": "Read More", "total": 56, "pageCount": 5, "pages": [...] }
],
"headingFrequency": [
{ "text": "Looking for immediate shelter?", "total": 22, "pageCount": 10, "level": 2 },
{ "text": "Get Involved", "total": 11, "pageCount": 9, "level": 3 }
],
"toneMetrics": {
"headingsTotal": 312,
"headingsUppercasePercent": 44,
"distinctHeadings": 153,
"distinctCtaLabels": 40
}
}Aggregation rules:
ctaFrequency — built from every pages/*.json#ctas[].label. To
catch styled <a> links the visual-button heuristic missed (e.g.
WordPress sites that style links as buttons via classes), also
include link texts that match the closed list below. Match is
case-insensitive after trimming whitespace; full-string equality
only (no partial / fuzzy matching).
donate, donate now, give, give now, support us, contribute,
read more, learn more, more info, see more, view more, view, more,
discover more, explore, overview,
sign up, signup, subscribe, register, join, create account,
contact, contact us, get in touch, talk to us, reach out, say hello,
get help, get involved, find help,
volunteer, share, download, submit,
here, click here, read this, thisThese mirror and slightly expand the CTA equivalence buckets in
brand-review-template.md § CTA equivalence buckets; the buckets
drive the T-cta-vocab Tensions detector, while this list drives
the voiceTable.ctaFrequency aggregation.
Sort by total descending; on ties, sort by pageCount descending.
Cap retained list at 8 entries; the rest are dropped (the
brand-review table renders the top 8).
Closed list. The list is exhaustive for v0.2; do not extend at
runtime. When a real run surfaces a missing CTA verb worth
catching, add it here in spec, not in the renderer. Fuzzy /
edit-distance matching is tracked as a v0.3 issue
(brand-review-template.md § Open issues for v0.3).
headingFrequency — built from every pages/*.json#headings[].text.
Filter to entries with pageCount >= 3. Sort by total, then
pageCount. This is the strongest signal of repeated site-wide
blocks (e.g. a "Looking for immediate shelter?" cross-promo).
toneMetrics.headingsUppercasePercent — fraction of headings whose
text equals its uppercase form. The brand-review uses
≥ 25 as the threshold to apply text-transform: uppercase to
H1/H2/H3 in the review chrome.
toneMetrics.distinctHeadings and distinctCtaLabels — set sizes
across all pages. High distinct-CTA counts (>20) often signal
vocabulary fragmentation; the Tensions detector
(brand-review-template.md § Detectors, T-cta-vocab) consumes
this.
A cluster of repeated headings + CTAs that appears on most interior
pages. Almost always the most load-bearing site-wide repeated block
(call-to-action band, footer cross-promo, "looking for X?" panel).
Detection runs after headingFrequency is built.
{
"detected": true,
"anchorHeading": "Looking for immediate shelter?",
"cluster": [
{ "text": "Looking for immediate shelter?", "total": 22, "pageCount": 10, "overlap": 10 },
{ "text": "Experiencing a housing crisis?", "total": 12, "pageCount": 10, "overlap": 9 },
{ "text": "Please call: 211", "total": 11, "pageCount": 10, "overlap": 9 }
],
"pages": ["about-us", "donate", "events", ...],
"pageCount": 10,
"totalPages": 24
}Detection algorithm:
headingFrequency as the anchor.headingFrequency, count how many
anchor-pages also contain it (overlap). Keep entries where
overlap / anchorPages.size >= 0.6.When detected, the brand-review must render a cross-promo
reproduction section (brand-review-template.md § Section ordering → Cross-promo reproduction). Set detected: false and emit null for
the rest when no cluster meets the threshold.
After Phase 2, scan each page's embedDominance field
(current-state-schema.md § Embed dominance). If any page has
dominated: true, add a section to the generated DESIGN.md (and
mention in the user report) labeled "Third-party embeds (opaque to
extraction)" that lists the affected slugs and embed sources. The
brand-surface tokens for those pages are not captured in computed
styles — the screenshot is the only artifact direct and prototype
can reason from.
Do not attempt recursive extraction of the iframe src URL in
v0.2; that's tracked as a separate feature.
"register": "brand"One of brand, product, ambiguous. Heuristic:
brand if landing-page indicators dominate: hero with marketing
copy, social proof, pricing, signup CTA above the fold, no
authentication required to see the page.product if tool indicators dominate: data tables, navigation
optimized for known features, requires auth, contains widgets like
filters and sort.ambiguous if both appear or neither dominates. Stardust will ask
the user in direct rather than guess.direct.critique belongs to
impeccable..tessl-plugin
skills
audit
reference
deploy
diff
direct
extract
migrate
prepare-migration
prototype
replica
reskin
rollout
stardust
uplift
reference