@designesy/score 0.7.0 → 0.7.2

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @designesy/score
2
2
 
3
- Standalone 44-check design-contract scoring engine. Fetches a URL, extracts CSS + `:root` tokens, runs all checks locally, with **no server required**. Zero dependencies.
3
+ Standalone 44-check design-contract scoring engine. Fetches a URL, extracts CSS + `:root` tokens, runs all checks locally, with **no server required**. No required dependencies.
4
4
 
5
5
  ## Install
6
6
 
@@ -90,12 +90,14 @@ Plus 12 anti-slop deductions (S1 to S12) and 7 originality lifts (O1 to O7).
90
90
 
91
91
  Auto-detect: `designesy.org` → contract, everything else → universal.
92
92
 
93
- ## Zero dependencies
93
+ ## Dependencies
94
94
 
95
- Uses only Node.js built-ins:
95
+ The engine uses only Node.js built-ins:
96
96
  - `node:https` for URL fetching (avoids the Windows libuv/undici crash)
97
97
  - `node:dns` for SSRF guard (DNS resolution validation)
98
- - No `ipaddr.js`, no `undici`, no `fetch()`, no external packages
98
+ - No `ipaddr.js`, no `undici`, no `fetch()`
99
+
100
+ One optional dependency: Google's [`@google/design.md`](https://github.com/google-labs-code/design.md) linter (Apache-2.0), the version the hosted engine runs. v37 uses it to lint a site's `/DESIGN.md`. npm installs it by default, `npx` included. Installed without it (`npm install --omit=optional`), v37 reports MANUAL (not run, weight 0) for a site that serves a DESIGN.md, and every other check is unchanged.
99
101
 
100
102
  ## SSRF guard
101
103
 
package/dist/engine.d.ts CHANGED
@@ -6,8 +6,9 @@
6
6
  * browser), computes a weighted score (0-100), assigns a letter grade (A-F),
7
7
  * and applies anti-slop deductions + originality lifts.
8
8
  *
9
- * Zero dependencies. Uses node:https (not fetch/undici) to avoid the Windows
10
- * libuv crash during process exit.
9
+ * No required dependencies. Uses node:https (not fetch/undici) to avoid the
10
+ * Windows libuv crash during process exit. One optional dependency,
11
+ * @google/design.md, lints /DESIGN.md for v37; without it v37 is MANUAL.
11
12
  *
12
13
  * Extracted from apps/site/app/api/score/route.ts — same engine, same checks,
13
14
  * same scoring math. The API route wraps this module; the CLI calls it directly.
@@ -158,7 +159,7 @@ export declare function scoreFromParts(input: ScorePartsInput): Promise<ScoreRes
158
159
  * the site served v0.4.3; test/engine-1-2-0.test.mjs now reads the site's
159
160
  * contract source and fails when the two differ.
160
161
  */
161
- export declare const CONTRACT_VERSION = "v0.4.3";
162
+ export declare const CONTRACT_VERSION = "v0.4.4";
162
163
  /**
163
164
  * The one verdict rule, shared by every emission format.
164
165
  *
package/dist/engine.js CHANGED
@@ -6,8 +6,9 @@
6
6
  * browser), computes a weighted score (0-100), assigns a letter grade (A-F),
7
7
  * and applies anti-slop deductions + originality lifts.
8
8
  *
9
- * Zero dependencies. Uses node:https (not fetch/undici) to avoid the Windows
10
- * libuv crash during process exit.
9
+ * No required dependencies. Uses node:https (not fetch/undici) to avoid the
10
+ * Windows libuv crash during process exit. One optional dependency,
11
+ * @google/design.md, lints /DESIGN.md for v37; without it v37 is MANUAL.
11
12
  *
12
13
  * Extracted from apps/site/app/api/score/route.ts — same engine, same checks,
13
14
  * same scoring math. The API route wraps this module; the CLI calls it directly.
@@ -499,12 +500,32 @@ function checkPaperToken(tokens) {
499
500
  return { id: 'v01', item: 'Token values match live site :root foundation', category: 'tokens', status: 'FAIL', detail: '--paper background token not declared in :root' };
500
501
  }
501
502
  function checkContrastSignal(tokens) {
502
- // v22 — REAL WCAG 2.1 contrast between --signal (button fill) and the text
503
- // on top of it. Contracts typically put --paper or --ink text on --signal.
504
- // We test both --paper-on-signal and --ink-on-signal; the better ratio wins
505
- // (designers choose the higher-contrast pairing). AA threshold: 4.5:1 for
506
- // body text, 3:1 for large/UI — buttons are large text, but we hold the 4.5:1
507
- // bar because button labels are often small (12-14px).
503
+ // v22 — REAL WCAG 2.1 contrast between the primary button's fill and its
504
+ // label. AA threshold: 4.5:1 for body text, 3:1 for large/UI — buttons are
505
+ // large text, but we hold the 4.5:1 bar because button labels are often
506
+ // small (12-14px).
507
+ //
508
+ // A contract that names its primary action pair (--action, the fill, and
509
+ // --on-action, the label) is measured on exactly that pair: it is what the
510
+ // button paints, so a better-contrast token elsewhere cannot stand in for a
511
+ // label that fails. Without both tokens the check reads the convention most
512
+ // contracts use: --paper or --ink text on --signal, the better ratio winning
513
+ // (designers choose the higher-contrast pairing).
514
+ const actionVal = tokens['--action'];
515
+ const onActionVal = tokens['--on-action'];
516
+ if (actionVal && onActionVal) {
517
+ const fillRgb = resolveColor(actionVal, tokens);
518
+ const labelRgb = resolveColor(onActionVal, tokens);
519
+ if (!fillRgb || !labelRgb)
520
+ return { id: 'v22', item: 'Primary button text passes WCAG AA contrast against --signal fill', category: 'accessibility', status: 'SKIP', detail: `--action ${actionVal} or --on-action ${onActionVal} unresolvable to RGB` };
521
+ const pairRatio = contrastRatio(labelRgb, fillRgb);
522
+ const pairStr = `${pairRatio.toFixed(2)}:1`;
523
+ if (pairRatio >= 4.5)
524
+ return { id: 'v22', item: 'Primary button text passes WCAG AA contrast against --signal fill', category: 'accessibility', status: 'PASS', detail: `--on-action on --action = ${pairStr} (≥ 4.5:1 AA)` };
525
+ if (pairRatio >= 3)
526
+ return { id: 'v22', item: 'Primary button text passes WCAG AA contrast against --signal fill', category: 'accessibility', status: 'WARN', detail: `--on-action on --action = ${pairStr} (passes 3:1 large-text, fails 4.5:1 body)` };
527
+ return { id: 'v22', item: 'Primary button text passes WCAG AA contrast against --signal fill', category: 'accessibility', status: 'FAIL', detail: `--on-action on --action = ${pairStr} (below 3:1, illegible)` };
528
+ }
508
529
  const signalVal = tokens['--signal'];
509
530
  if (!signalVal)
510
531
  return { id: 'v22', item: 'Primary button text passes WCAG AA contrast against --signal fill', category: 'accessibility', status: 'WARN', detail: '--signal accent token not declared' };
@@ -3735,7 +3756,7 @@ async function checkDesignMdSpec(targetUrl) {
3735
3756
  // package missing from THIS runtime, so the same page scored lower from the
3736
3757
  // npm CLI than from the API. Unverified-here is what MANUAL means (v02,
3737
3758
  // v04, v21): weight 0, resolved by a run that has the linter.
3738
- const msg = e instanceof Error ? e.message : 'unknown error';
3759
+ const msg = sanitizeErrorText(e instanceof Error ? e.message : 'unknown error');
3739
3760
  return { id: 'v37', item: ITEM, category: CATEGORY, status: 'MANUAL', detail: `/DESIGN.md fetched but linter unavailable: ${msg}. The @google/design.md package may not be installed in this runtime; run the full audit to resolve.` };
3740
3761
  }
3741
3762
  try {
@@ -3753,10 +3774,35 @@ async function checkDesignMdSpec(targetUrl) {
3753
3774
  // Distinct from the branch above: the linter LOADED and then threw, so the
3754
3775
  // file's format is unsupported rather than the runtime being incomplete.
3755
3776
  // Both WARN, but the messages point at different fixes.
3756
- const msg = e instanceof Error ? e.message : 'unknown error';
3777
+ const msg = sanitizeErrorText(e instanceof Error ? e.message : 'unknown error');
3757
3778
  return { id: 'v37', item: ITEM, category: CATEGORY, status: 'WARN', detail: `/DESIGN.md fetched but lint failed: ${msg}. The file may use a format version the linter doesn't support yet.` };
3758
3779
  }
3759
3780
  }
3781
+ // ── Error text in a result, shared by both engine copies ───────────────────
3782
+ // A caught error's message carries the file system of the machine that ran
3783
+ // the engine: Node's "Cannot find package '@google/design.md' imported from
3784
+ // <path>" gives the full path of the npm cache the CLI ran from, under the
3785
+ // Users folder of the account that ran it, and a result is read by people
3786
+ // other than that account. Every check detail passes through
3787
+ // sanitizeErrorText before it is returned. It replaces absolute paths (a
3788
+ // drive letter, a UNC share, a POSIX home, temp or system root), file:// URLs
3789
+ // and npm cache segments, and keeps the clause that says what failed
3790
+ // ("Cannot find package '@google/design.md'"). A path segment may hold spaces
3791
+ // (an account name often does); the last one may not, so the sentence after
3792
+ // a path survives, and trailing punctuation is kept.
3793
+ function sanitizeErrorText(text) {
3794
+ const keep = (label) => (m) => `${label}${/[.,;:!?)]+$/.exec(m)?.[0] ?? ''}`;
3795
+ return text
3796
+ .replace(/\bfile:\/\/[^\s'"<>`|]*/gi, keep('a local file'))
3797
+ .replace(/\\\\[^\\\s'"<>`|]+\\(?:[^\\\r\n'"<>`|*?]+\\)*[^\\\s'"<>`|*?]*/g, keep('a local path'))
3798
+ .replace(/(?<![\w\\/])[A-Za-z]:([\\/])(?:[^\\/\r\n'"<>`|*?]+\1)*[^\\/\s'"<>`|*?]*/g, keep('a local path'))
3799
+ .replace(/(^|[\s'"`(=,:[])\/(?:home|Users|root|tmp|var|private|opt|usr|srv|mnt|Volumes|Library|app|vercel|workspace|workspaces|github|runner|nix|snap)(?:\/[^/\r\n'"<>`|]+(?=\/))*\/[^/\s'"<>`|]+/g, (m, lead) => `${lead}${keep('a local path')(m.slice(lead.length))}`)
3800
+ .replace(/[^\s'"<>`|]*(?:npm-cache|[\\/]_npx[\\/]|[\\/]\.npm[\\/])[^\s'"<>`|]*/gi, keep('the npm cache'));
3801
+ }
3802
+ /** The checks with every detail passed through sanitizeErrorText. */
3803
+ function sanitizeCheckDetails(checks) {
3804
+ return checks.map((c) => (typeof c.detail === 'string' ? { ...c, detail: sanitizeErrorText(c.detail) } : c));
3805
+ }
3760
3806
  // ── Score arithmetic, shared by both engine copies ─────────────────────────
3761
3807
  // Everything between the check verdicts and the grade: category weights, the
3762
3808
  // weighted score, the slop deduction, the originality lift, the per-category
@@ -4082,7 +4128,7 @@ export async function scoreFromParts(input) {
4082
4128
  detail: 'not probed: offline fixture run (no network); excluded from the score',
4083
4129
  }
4084
4130
  : await checkDesignMdSpec(targetUrl));
4085
- checks = applyScopeFilter(checks, effectiveScope);
4131
+ checks = sanitizeCheckDetails(applyScopeFilter(checks, effectiveScope));
4086
4132
  const pass = checks.filter((c) => c.status === 'PASS').length;
4087
4133
  const fail = checks.filter((c) => c.status === 'FAIL').length;
4088
4134
  const warn = checks.filter((c) => c.status === 'WARN').length;
@@ -4560,7 +4606,7 @@ export async function scoreFromParts(input) {
4560
4606
  * the site served v0.4.3; test/engine-1-2-0.test.mjs now reads the site's
4561
4607
  * contract source and fails when the two differ.
4562
4608
  */
4563
- export const CONTRACT_VERSION = 'v0.4.3';
4609
+ export const CONTRACT_VERSION = 'v0.4.4';
4564
4610
  /**
4565
4611
  * The one verdict rule, shared by every emission format.
4566
4612
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@designesy/score",
3
- "version": "0.7.0",
4
- "description": "Standalone 44-check design-contract scoring engine: fetches a URL, extracts CSS + tokens, and runs all checks locally. Zero dependencies.",
3
+ "version": "0.7.2",
4
+ "description": "Standalone 44-check design-contract scoring engine: fetches a URL, extracts CSS + tokens, and runs all checks locally. No required dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "Designesy <hello@designesy.org>",
7
7
  "homepage": "https://www.designesy.org/score",
@@ -43,6 +43,9 @@
43
43
  "engines": {
44
44
  "node": ">=18.0.0"
45
45
  },
46
+ "optionalDependencies": {
47
+ "@google/design.md": "0.4.0"
48
+ },
46
49
  "devDependencies": {
47
50
  "@types/node": "^26.2.0",
48
51
  "typescript": "^5.7.0"