@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 +6 -4
- package/dist/engine.d.ts +4 -3
- package/dist/engine.js +58 -12
- package/package.json +5 -2
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**.
|
|
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
|
-
##
|
|
93
|
+
## Dependencies
|
|
94
94
|
|
|
95
|
-
|
|
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()
|
|
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
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
// (
|
|
506
|
-
//
|
|
507
|
-
//
|
|
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.
|
|
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.
|
|
4
|
-
"description": "Standalone 44-check design-contract scoring engine: fetches a URL, extracts CSS + tokens, and runs all checks locally.
|
|
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"
|