dembrandt 0.19.5 → 0.19.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +23 -2
  2. package/dist/lib/exit-codes.d.ts +1 -1
  3. package/dist/lib/exit-codes.js.map +1 -1
  4. package/dist/lib/extractors/context-config.d.ts +70 -0
  5. package/dist/lib/extractors/context-config.js +96 -0
  6. package/dist/lib/extractors/context-config.js.map +1 -0
  7. package/dist/lib/extractors/guard.d.ts +9 -0
  8. package/dist/lib/extractors/guard.js +10 -0
  9. package/dist/lib/extractors/guard.js.map +1 -0
  10. package/dist/lib/extractors/index.d.ts +2 -1
  11. package/dist/lib/extractors/index.js +38 -55
  12. package/dist/lib/extractors/index.js.map +1 -1
  13. package/dist/lib/extractors/typography.d.ts +19 -1
  14. package/dist/lib/extractors/typography.js +61 -1
  15. package/dist/lib/extractors/typography.js.map +1 -1
  16. package/dist/lib/formatters/dtcg.js.map +1 -1
  17. package/dist/lib/formatters/terminal.js +13 -0
  18. package/dist/lib/formatters/terminal.js.map +1 -1
  19. package/dist/lib/merger.js +22 -1
  20. package/dist/lib/merger.js.map +1 -1
  21. package/dist/lib/types.d.ts +80 -6
  22. package/dist/package.json +1 -1
  23. package/dist/test/context-config.test.d.ts +1 -0
  24. package/dist/test/context-config.test.js +114 -0
  25. package/dist/test/context-config.test.js.map +1 -0
  26. package/dist/test/guard.test.d.ts +1 -0
  27. package/dist/test/guard.test.js +46 -0
  28. package/dist/test/guard.test.js.map +1 -0
  29. package/dist/test/merger.test.js +18 -0
  30. package/dist/test/merger.test.js.map +1 -1
  31. package/dist/test/typography.test.d.ts +1 -0
  32. package/dist/test/typography.test.js +35 -0
  33. package/dist/test/typography.test.js.map +1 -0
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -42,7 +42,7 @@ Or add to your project's `.mcp.json`:
42
42
  }
43
43
  ```
44
44
 
45
- 7 tools available: `get_design_tokens`, `get_color_palette`, `get_typography`, `get_component_styles`, `get_surfaces`, `get_spacing`, `get_brand_identity`.
45
+ Available tools include `get_design_tokens`, `get_color_palette`, `get_typography`, `get_component_styles`, `get_surfaces`, `get_spacing`, and `get_brand_identity`, plus drift, report, and job-control tools.
46
46
 
47
47
  Pair with **[dembrandt-skills](https://github.com/dembrandt/dembrandt-skills)** to give your agent UX intelligence on top of extracted tokens — hierarchy, accessibility, interaction states, and a full 6-stage design pipeline orchestrator.
48
48
 
@@ -247,7 +247,28 @@ dembrandt https://app.example.com --json-only > baseline.json
247
247
  dembrandt https://app.example.com --compare baseline.json --html report.html
248
248
  ```
249
249
 
250
- A ready-to-use GitHub Actions workflow (preview vs production, run summary, report artifact, host-auth bypass) is in [`examples/drift-gate.yml`](examples/drift-gate.yml).
250
+ When the change is intended, accept it as the new baseline `--approve` overwrites the local baseline file and passes instead of failing:
251
+
252
+ ```bash
253
+ dembrandt https://app.example.com --compare baseline.json --approve
254
+ ```
255
+
256
+ Add `--json-only` to a `--compare` run to get the drift report as machine-readable JSON under a `drift` key — `score`, `status`, `summary`, and per-token `changes[]` (each with `category`, `kind`, `before`, `after`, `delta`). A CI gate can render exactly which tokens moved (e.g. in a PR comment) from this instead of parsing the HTML report:
257
+
258
+ ```bash
259
+ dembrandt https://app.example.com --compare baseline.json --json-only
260
+ ```
261
+
262
+ **Any CI.** The gate is platform-neutral — it is just the exit code plus the drift JSON, so it drops into any runner:
263
+
264
+ ```bash
265
+ dembrandt "$PREVIEW/checkout" --compare base.json --json-only > drift.json
266
+ # exit 1 = drift. Read drift.json (.drift.changes) and surface it however your
267
+ # platform does: a GitLab MR note, an Azure DevOps PR thread, a Jenkins status,
268
+ # a Slack message, or an auto-filed Jira/Linear ticket.
269
+ ```
270
+
271
+ A ready-to-use **GitHub Actions** workflow (preview vs production, per-page PR comment with the exact tokens that changed, run summary, report artifact, host-auth bypass) is in [`examples/drift-gate.yml`](examples/drift-gate.yml) as one full reference. The result-surfacing step (annotations, PR comment) is the only platform-specific part; the extract → compare → branch-on-exit-code core is identical on GitLab CI, Jenkins, and Azure DevOps.
251
272
 
252
273
  ### Exit codes
253
274
 
@@ -23,7 +23,7 @@ export type ErrorCode = "NAVIGATION_TIMEOUT" | "EXTRACTION_FAILED";
23
23
  * is a generic RUNTIME failure. Reads only `err.message`, so a bare string or a
24
24
  * malformed error never throws.
25
25
  */
26
- export declare function classifyError(err: any): {
26
+ export declare function classifyError(err: unknown): {
27
27
  code: ErrorCode;
28
28
  exit: number;
29
29
  };
@@ -1 +1 @@
1
- {"version":3,"file":"exit-codes.js","sourceRoot":"","sources":["../../lib/exit-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAW,CAAC;AAK1E;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,GAAQ;IACpC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC;IACrC,IAAI,2EAA2E,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACxF,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IAC5D,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;AAC3D,CAAC"}
1
+ {"version":3,"file":"exit-codes.js","sourceRoot":"","sources":["../../lib/exit-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAW,CAAC;AAK1E;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,MAAM,CAAC,GAAG,MAAM,CAAE,GAA6B,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC;IAChE,IAAI,2EAA2E,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACxF,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IAC5D,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Pure parsing of CLI-supplied browser-context inputs (cookies, headers, screen
3
+ * size, locale) into Playwright context configuration. Split out of the
4
+ * extraction orchestrator so the untrusted-input surface is unit-testable and
5
+ * malformed input degrades to safe defaults instead of producing broken values.
6
+ *
7
+ * Written strict-clean (no implicit any, no null leaks) per the per-module
8
+ * strict ratchet, even though the global tsconfig is strict:false.
9
+ */
10
+ import type { ExtractOptions } from '../types.js';
11
+ export interface ParsedCookie {
12
+ readonly name: string;
13
+ readonly value: string;
14
+ readonly url: string;
15
+ }
16
+ export interface ScreenSize {
17
+ readonly width: number;
18
+ readonly height: number;
19
+ }
20
+ export type ColorScheme = 'light' | 'dark' | 'no-preference';
21
+ /**
22
+ * The subset of Playwright's BrowserContextOptions that we set. Declared
23
+ * explicitly (rather than `Record<string, any>`) so the config the extraction
24
+ * engine depends on is type-checked at the one call site where `browser` is an
25
+ * untyped handle.
26
+ */
27
+ export interface ContextOptions {
28
+ viewport: ScreenSize;
29
+ screen: ScreenSize;
30
+ userAgent: string;
31
+ locale: string;
32
+ timezoneId: string;
33
+ extraHTTPHeaders: Record<string, string>;
34
+ colorScheme: ColorScheme;
35
+ permissions?: string[];
36
+ }
37
+ export declare const DEFAULT_SCREEN: ScreenSize;
38
+ export declare const DEFAULT_LOCALE = "en-US";
39
+ export declare const DEFAULT_TIMEZONE = "America/New_York";
40
+ export declare const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36";
41
+ /**
42
+ * Parse a "Name=value; Name2=value2" cookie string. Pairs without an "=" are
43
+ * skipped rather than emitted with a truncated name and the whole token as the
44
+ * value (the previous behavior). Empty names are also dropped.
45
+ */
46
+ export declare function parseCookies(cookie: string | undefined, url: string): ParsedCookie[];
47
+ /**
48
+ * Parse a single "Name: value" header. Returns {} when absent or when no colon
49
+ * is present (an invalid header is ignored rather than guessed at).
50
+ */
51
+ export declare function parseHeader(header: string | undefined): Record<string, string>;
52
+ /**
53
+ * Parse a "WIDTHxHEIGHT" screen size. Falls back to the default when either
54
+ * dimension is missing, non-numeric, or non-positive, so a typo never produces
55
+ * a NaN viewport that silently breaks layout-dependent extraction. Returns a
56
+ * fresh object so callers can never mutate the shared default.
57
+ */
58
+ export declare function parseScreenSize(screenSize: string | undefined): ScreenSize;
59
+ /**
60
+ * Derive the Accept-Language header. An explicit value wins; otherwise it is
61
+ * built from the locale, weighting the locale and its base language ahead of
62
+ * English.
63
+ */
64
+ export declare function deriveAcceptLanguage(locale: string, explicit?: string): string;
65
+ /**
66
+ * Build the Playwright context options from CLI options. Pure: no browser
67
+ * access, no mutation of the input. Cookies and init scripts are applied
68
+ * separately by the caller because they are side effects on a live context.
69
+ */
70
+ export declare function buildContextOptions(options: ExtractOptions, browserName: string): ContextOptions;
@@ -0,0 +1,96 @@
1
+ export const DEFAULT_SCREEN = Object.freeze({ width: 1920, height: 1080 });
2
+ export const DEFAULT_LOCALE = 'en-US';
3
+ export const DEFAULT_TIMEZONE = 'America/New_York';
4
+ export const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36';
5
+ /**
6
+ * Parse a "Name=value; Name2=value2" cookie string. Pairs without an "=" are
7
+ * skipped rather than emitted with a truncated name and the whole token as the
8
+ * value (the previous behavior). Empty names are also dropped.
9
+ */
10
+ export function parseCookies(cookie, url) {
11
+ if (!cookie)
12
+ return [];
13
+ const out = [];
14
+ for (const raw of cookie.split(';')) {
15
+ const c = raw.trim();
16
+ if (!c)
17
+ continue;
18
+ const eq = c.indexOf('=');
19
+ if (eq < 1)
20
+ continue; // no "=", or "=value" with an empty name
21
+ out.push({ name: c.slice(0, eq).trim(), value: c.slice(eq + 1).trim(), url });
22
+ }
23
+ return out;
24
+ }
25
+ /**
26
+ * Parse a single "Name: value" header. Returns {} when absent or when no colon
27
+ * is present (an invalid header is ignored rather than guessed at).
28
+ */
29
+ export function parseHeader(header) {
30
+ if (!header)
31
+ return {};
32
+ const colon = header.indexOf(':');
33
+ if (colon < 1)
34
+ return {};
35
+ const name = header.slice(0, colon).trim();
36
+ if (!name)
37
+ return {};
38
+ return { [name]: header.slice(colon + 1).trim() };
39
+ }
40
+ /**
41
+ * Parse a "WIDTHxHEIGHT" screen size. Falls back to the default when either
42
+ * dimension is missing, non-numeric, or non-positive, so a typo never produces
43
+ * a NaN viewport that silently breaks layout-dependent extraction. Returns a
44
+ * fresh object so callers can never mutate the shared default.
45
+ */
46
+ export function parseScreenSize(screenSize) {
47
+ if (!screenSize)
48
+ return { width: DEFAULT_SCREEN.width, height: DEFAULT_SCREEN.height };
49
+ const parts = screenSize.split('x');
50
+ if (parts.length !== 2)
51
+ return { width: DEFAULT_SCREEN.width, height: DEFAULT_SCREEN.height };
52
+ const width = Number(parts[0]);
53
+ const height = Number(parts[1]);
54
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
55
+ return { width: DEFAULT_SCREEN.width, height: DEFAULT_SCREEN.height };
56
+ }
57
+ return { width: Math.round(width), height: Math.round(height) };
58
+ }
59
+ /**
60
+ * Derive the Accept-Language header. An explicit value wins; otherwise it is
61
+ * built from the locale, weighting the locale and its base language ahead of
62
+ * English.
63
+ */
64
+ export function deriveAcceptLanguage(locale, explicit) {
65
+ if (explicit)
66
+ return explicit;
67
+ const base = locale.split('-')[0];
68
+ return `${locale},${base};q=0.9,en;q=0.8`;
69
+ }
70
+ /**
71
+ * Build the Playwright context options from CLI options. Pure: no browser
72
+ * access, no mutation of the input. Cookies and init scripts are applied
73
+ * separately by the caller because they are side effects on a live context.
74
+ */
75
+ export function buildContextOptions(options, browserName) {
76
+ const locale = options.locale || DEFAULT_LOCALE;
77
+ const size = parseScreenSize(options.screenSize);
78
+ const extraHTTPHeaders = {
79
+ 'Accept-Language': deriveAcceptLanguage(locale, options.acceptLanguage),
80
+ ...parseHeader(options.header),
81
+ };
82
+ const contextOptions = {
83
+ viewport: { width: size.width, height: size.height },
84
+ screen: { width: size.width, height: size.height },
85
+ userAgent: options.userAgent || DEFAULT_USER_AGENT,
86
+ locale,
87
+ timezoneId: options.timezoneId || DEFAULT_TIMEZONE,
88
+ extraHTTPHeaders,
89
+ colorScheme: 'light',
90
+ };
91
+ if (browserName === 'chromium') {
92
+ contextOptions.permissions = ['clipboard-read', 'clipboard-write'];
93
+ }
94
+ return contextOptions;
95
+ }
96
+ //# sourceMappingURL=context-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-config.js","sourceRoot":"","sources":["../../../lib/extractors/context-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Fault isolation for the parallel extractors. Each extractor runs inside
3
+ * guardExtractor so a single throw degrades only its own category — recording
4
+ * { stage, reason } and returning a fallback — instead of rejecting Promise.all
5
+ * and aborting the entire run. Kept in its own module so the isolation contract
6
+ * is unit-testable without a browser.
7
+ */
8
+ import type { ExtractorError } from '../types.js';
9
+ export declare function guardExtractor<T>(stage: string, run: Promise<T>, fallback: T, sink: ExtractorError[]): Promise<T>;
@@ -0,0 +1,10 @@
1
+ export async function guardExtractor(stage, run, fallback, sink) {
2
+ try {
3
+ return await run;
4
+ }
5
+ catch (err) {
6
+ sink.push({ stage, reason: err instanceof Error ? err.message : String(err) });
7
+ return fallback;
8
+ }
9
+ }
10
+ //# sourceMappingURL=guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard.js","sourceRoot":"","sources":["../../../lib/extractors/guard.ts"],"names":[],"mappings":"AASA,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,KAAa,EACb,GAAe,EACf,QAAW,EACX,IAAsB;IAEtB,IAAI,CAAC;QACH,OAAO,MAAM,GAAG,CAAC;IACnB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC/E,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC"}
@@ -1,3 +1,4 @@
1
+ import type { Browser } from 'playwright';
1
2
  import type { ExtractOptions, BrandingResult, Spinner } from '../types.js';
2
3
  /**
3
4
  * @param {string} url
@@ -6,4 +7,4 @@ import type { ExtractOptions, BrandingResult, Spinner } from '../types.js';
6
7
  * @param {{ slow?: boolean, darkMode?: boolean, mobile?: boolean, wcag?: boolean, screenshotPath?: string, discoverLinks?: number|null, navigationTimeout?: number, stealth?: boolean, userAgent?: string, locale?: string, timezoneId?: string, acceptLanguage?: string, screenSize?: string }} [options]
7
8
  * @returns {Promise<BrandingResult>}
8
9
  */
9
- export declare function extractBranding(url: string, spinner: Spinner, browser: any, options?: ExtractOptions): Promise<BrandingResult>;
10
+ export declare function extractBranding(url: string, spinner: Spinner, browser: Browser, options?: ExtractOptions): Promise<BrandingResult>;
@@ -10,6 +10,8 @@ import { extractBreakpoints, detectIconSystem, detectFrameworks, extractGradient
10
10
  import { extractTeach } from './teach.js';
11
11
  import { extractWcagPairs } from './colors.js';
12
12
  import { SCHEMA_VERSION } from '../version.js';
13
+ import { buildContextOptions, parseCookies, parseScreenSize, DEFAULT_LOCALE } from './context-config.js';
14
+ import { guardExtractor } from './guard.js';
13
15
  // Gaussian noise via Box-Muller
14
16
  function gaussian(mean = 0, std = 1) {
15
17
  let u, v;
@@ -45,7 +47,7 @@ function velocityProfile(t, overshootProb = 0.3) {
45
47
  return 0.15 + Math.random() * 0.1; // corrective micro-movements
46
48
  }
47
49
  // Sleep helper
48
- const sleep = ms => new Promise(r => setTimeout(r, ms));
50
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
49
51
  /**
50
52
  * Adaptive readiness: resolve as soon as the page is actually settled — network
51
53
  * quiet, web fonts loaded, and the DOM done mutating — instead of always waiting
@@ -252,49 +254,21 @@ export async function extractBranding(url, spinner, browser, options = {}) {
252
254
  const timeoutMultiplier = options.slow ? 3 : 1;
253
255
  const timeouts = [];
254
256
  const degraded = []; // post-extraction stages that failed but did not abort the run
257
+ const extractorErrors = []; // scoped failures of the parallel extractors
255
258
  // Progress lines print only in verbose mode (the main `dembrandt <url>`
256
259
  // command). Report commands (drift/init/conformance) pass no verbose flag and
257
260
  // stay clean. Warnings are NOT routed through this — they always print.
258
261
  const log = (...args) => { if (options.verbose)
259
262
  console.log(...args); };
260
263
  spinner.text = "Creating browser context...";
261
- const locale = options.locale || "en-US";
262
- const timezoneId = options.timezoneId || "America/New_York";
263
- const acceptLanguage = options.acceptLanguage || `${locale},${locale.split('-')[0]};q=0.9,en;q=0.8`;
264
- const [screenW, screenH] = options.screenSize
265
- ? options.screenSize.split('x').map(Number)
266
- : [1920, 1080];
267
- // Parse "Name=value; Name2=value2" cookie string into Playwright format
268
- const parsedCookies = options.cookie
269
- ? options.cookie.split(";").map((c) => c.trim()).filter(Boolean).map((c) => {
270
- const eq = c.indexOf("=");
271
- return {
272
- name: c.slice(0, eq).trim(),
273
- value: c.slice(eq + 1).trim(),
274
- url,
275
- };
276
- })
277
- : [];
278
- const extraHeaders = { "Accept-Language": acceptLanguage };
279
- if (options.header) {
280
- const colon = options.header.indexOf(":");
281
- if (colon > -1) {
282
- extraHeaders[options.header.slice(0, colon).trim()] = options.header.slice(colon + 1).trim();
283
- }
284
- }
285
- const contextOptions = {
286
- viewport: { width: screenW, height: screenH },
287
- screen: { width: screenW, height: screenH },
288
- userAgent: options.userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
289
- locale,
290
- timezoneId,
291
- extraHTTPHeaders: extraHeaders,
292
- colorScheme: "light",
293
- };
294
- if (browser.browserType().name() === 'chromium') {
295
- contextOptions.permissions = ["clipboard-read", "clipboard-write"];
296
- }
264
+ // locale, screenW and screenH are still needed below for the stealth init
265
+ // script; the rest of the context configuration is built purely in
266
+ // context-config.ts and unit-tested there.
267
+ const locale = options.locale || DEFAULT_LOCALE;
268
+ const { width: screenW, height: screenH } = parseScreenSize(options.screenSize);
269
+ const contextOptions = buildContextOptions(options, browser.browserType().name());
297
270
  const context = await browser.newContext(contextOptions);
271
+ const parsedCookies = parseCookies(options.cookie, url);
298
272
  if (parsedCookies.length > 0) {
299
273
  await context.addCookies(parsedCookies);
300
274
  }
@@ -694,27 +668,33 @@ export async function extractBranding(url, spinner, browser, options = {}) {
694
668
  log(color.info("\n Extracting design tokens...\n"));
695
669
  spinner.start("Analyzing design system (17 parallel tasks)...");
696
670
  const [logoResult, colors, typography, spacing, borderRadius, borders, shadows, buttons, inputs, links, badges, breakpoints, iconSystem, frameworks, siteNameRaw, gradients, motion,] = await Promise.all([
697
- extractLogo(page, url).catch(() => ({ logo: null, instances: [], favicons: [], manifest: null })),
698
- extractColors(page).catch(() => ({ semantic: {}, palette: [], cssVariables: [], _raw: [] })),
699
- extractTypography(page).catch(() => ({ styles: [], sources: {} })),
700
- extractSpacing(page).catch(() => ({ scaleType: 'unknown', commonValues: [] })),
701
- extractBorderRadius(page).catch(() => ({ values: [] })),
702
- extractBorders(page).catch(() => ({ combinations: [] })),
703
- extractShadows(page).catch(() => []),
704
- extractButtonStyles(page).catch(() => []),
705
- extractInputStyles(page).catch(() => []),
706
- extractLinkStyles(page).catch(() => []),
707
- extractBadgeStyles(page).catch(() => ({ all: [], byVariant: {} })),
708
- extractBreakpoints(page).catch(() => []),
709
- detectIconSystem(page).catch(() => []),
710
- detectFrameworks(page).catch(() => []),
711
- extractSiteName(page).catch(() => null),
712
- extractGradients(page).catch(() => []),
713
- extractMotion(page).catch(() => ({ durations: [], easings: [], byContext: {} })),
671
+ guardExtractor('logo', extractLogo(page, url), { logo: null, instances: [], favicons: [], manifest: null }, extractorErrors),
672
+ guardExtractor('colors', extractColors(page), { semantic: {}, palette: [], cssVariables: [], _raw: [] }, extractorErrors),
673
+ guardExtractor('typography', extractTypography(page), { styles: [], sources: {} }, extractorErrors),
674
+ guardExtractor('spacing', extractSpacing(page), { scaleType: 'unknown', commonValues: [] }, extractorErrors),
675
+ guardExtractor('borderRadius', extractBorderRadius(page), { values: [] }, extractorErrors),
676
+ guardExtractor('borders', extractBorders(page), { combinations: [] }, extractorErrors),
677
+ guardExtractor('shadows', extractShadows(page), [], extractorErrors),
678
+ guardExtractor('buttons', extractButtonStyles(page), [], extractorErrors),
679
+ guardExtractor('inputs', extractInputStyles(page), [], extractorErrors),
680
+ guardExtractor('links', extractLinkStyles(page), [], extractorErrors),
681
+ guardExtractor('badges', extractBadgeStyles(page), { all: [], byVariant: {} }, extractorErrors),
682
+ guardExtractor('breakpoints', extractBreakpoints(page), [], extractorErrors),
683
+ guardExtractor('iconSystem', detectIconSystem(page), [], extractorErrors),
684
+ guardExtractor('frameworks', detectFrameworks(page), [], extractorErrors),
685
+ guardExtractor('siteName', extractSiteName(page), null, extractorErrors),
686
+ guardExtractor('gradients', extractGradients(page), [], extractorErrors),
687
+ guardExtractor('motion', extractMotion(page), { durations: [], easings: [], byContext: {} }, extractorErrors),
714
688
  ]);
715
689
  const { logo, instances: logoInstances, favicons, manifest } = logoResult;
716
690
  let siteName = siteNameRaw;
717
691
  spinner.stop();
692
+ // Per-extractor failures are fault-isolated above and surface in meta.errors
693
+ // during result assembly. Print them once now, after the spinner stops, so a
694
+ // broken extractor is visible during a verbose run without clobbering it.
695
+ for (const e of extractorErrors) {
696
+ log(color.warning(` ! ${e.stage}: extraction failed (continuing) — ${e.reason}`));
697
+ }
718
698
  // Inject manifest theme_color / background_color as high-confidence palette entries
719
699
  try {
720
700
  if (manifest) {
@@ -894,7 +874,8 @@ export async function extractBranding(url, spinner, browser, options = {}) {
894
874
  }
895
875
  if (['input', 'textarea', 'select', 'button', 'a'].includes(beforeState.tag)) {
896
876
  try {
897
- await element.focus({ timeout: 500 * timeoutMultiplier });
877
+ // ElementHandle.focus() takes no options; the focus is synchronous.
878
+ await element.focus();
898
879
  await page.waitForTimeout(100 * timeoutMultiplier);
899
880
  const afterFocus = await element.evaluate(el => {
900
881
  function findBg(node) {
@@ -1244,6 +1225,8 @@ export async function extractBranding(url, spinner, browser, options = {}) {
1244
1225
  }
1245
1226
  if (degraded.length)
1246
1227
  result.meta.degraded = degraded;
1228
+ if (extractorErrors.length)
1229
+ result.meta.errors = extractorErrors;
1247
1230
  return result;
1248
1231
  }
1249
1232
  catch (error) {