@nodegx/mcp 0.2.5 → 0.2.7
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 +14 -7
- package/bin/noodl-mcp.js +14 -0
- package/dist/backend/LICENSE +21 -0
- package/dist/backend/cli.js +6534 -2836
- package/dist/backend/folder/start.cmd +4 -0
- package/dist/backend/folder/start.js +466 -0
- package/dist/backend/folder/start.sh +4 -0
- package/dist/backend/vm/provision-app.sh +235 -0
- package/dist/cloud-bundle.cjs +5051 -2381
- package/dist/kit-extract.cjs +5264 -2595
- package/dist/kit-extract.inputs.json +143 -113
- package/dist/node-catalog.json +44 -7
- package/dist/noodl-mcp.cjs +19819 -6657
- package/dist/package.json +1 -1
- package/dist/render-harness/data/node-catalog-enriched.json +1611 -257
- package/dist/render-harness/data/node-catalog.json +44 -7
- package/dist/render-harness/drive-steps.js +196 -21
- package/dist/render-harness/measure-from-disk.js +174 -22
- package/dist/render-harness/render-from-disk.js +136 -136
- package/dist/src/external/viewer/noodl.viewer.js +1 -1
- package/package.json +2 -3
- package/src/index.ts +0 -14
|
@@ -654,6 +654,80 @@ var require_src = __commonJS({
|
|
|
654
654
|
}
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
+
// P112-OWN019-LOOKBLIND / OWN-026 \u2014 text a person cannot read against what is painted behind it. Large 4's number pad
|
|
658
|
+
// set white digits on a #FAF5FF button (1.07:1) and it survived five changes: the model tapped the keys, read
|
|
659
|
+
// "Correct!" and never saw that nobody could read them, because nothing here measured contrast. The ground is the
|
|
660
|
+
// text's ancestors' background colours composited from the first opaque one (white under them all, as a browser
|
|
661
|
+
// paints), the ink is the text's colour faded by every ancestor's opacity, and the ratio is WCAG's. It abstains,
|
|
662
|
+
// counted, wherever ancestors are not what is painted underneath: a background image or gradient, a text shadow, an
|
|
663
|
+
// SVG text. A disabled or aria-hidden control is exempt (WCAG 1.4.3's inactive components). Known limit: a sibling
|
|
664
|
+
// laid over the text by position is not an ancestor, so it is not seen. Raw pairs here; the judgement is summarise's.
|
|
665
|
+
layout.lowContrast = [];
|
|
666
|
+
layout.contrastChecked = 0;
|
|
667
|
+
layout.contrastAbstained = 0;
|
|
668
|
+
const CONTRAST_FLOOR = 3;
|
|
669
|
+
{
|
|
670
|
+
const parseColor = (c) => {
|
|
671
|
+
const m = String(c || '').match(/rgba?\\(([^)]+)\\)/);
|
|
672
|
+
if (!m) return null;
|
|
673
|
+
const p = m[1].split(/[ ,\\/]+/).filter(Boolean).map(Number);
|
|
674
|
+
if (p.length < 3 || p.some((x) => isNaN(x))) return null;
|
|
675
|
+
return { r: p[0], g: p[1], b: p[2], a: p.length > 3 ? p[3] : 1 };
|
|
676
|
+
};
|
|
677
|
+
const over = (top, under) => ({ r: top.r * top.a + under.r * (1 - top.a), g: top.g * top.a + under.g * (1 - top.a), b: top.b * top.a + under.b * (1 - top.a), a: 1 });
|
|
678
|
+
const lum = (c) => {
|
|
679
|
+
const f = (x) => { const v = x / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
|
680
|
+
return 0.2126 * f(c.r) + 0.7152 * f(c.g) + 0.0722 * f(c.b);
|
|
681
|
+
};
|
|
682
|
+
const contrastOf = (a, b) => { const x = lum(a), y = lum(b); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); };
|
|
683
|
+
const hexOf = (c) => '#' + [c.r, c.g, c.b].map((v) => Math.round(v).toString(16).padStart(2, '0')).join('');
|
|
684
|
+
const inactive = (e) => e.disabled === true || (typeof e.getAttribute === 'function' && (e.getAttribute('aria-disabled') === 'true' || e.getAttribute('aria-hidden') === 'true'));
|
|
685
|
+
// What is behind the text, or null to abstain; 'exempt' for an inactive control.
|
|
686
|
+
const groundOf = (el) => {
|
|
687
|
+
const stack = [];
|
|
688
|
+
let opacity = 1;
|
|
689
|
+
for (let e = el; e; e = e.parentElement) {
|
|
690
|
+
if (inactive(e)) return 'exempt';
|
|
691
|
+
const cs = styleOf(e);
|
|
692
|
+
if (!cs) break;
|
|
693
|
+
const image = cs.backgroundImage;
|
|
694
|
+
if (image && image !== 'none' && !/^url\\("data:image\\/svg/.test(image)) return null;
|
|
695
|
+
const bg = parseColor(cs.backgroundColor);
|
|
696
|
+
const o = Number(cs.opacity);
|
|
697
|
+
if (o >= 0 && o <= 1) opacity *= o;
|
|
698
|
+
if (bg && bg.a > 0) {
|
|
699
|
+
stack.push(bg);
|
|
700
|
+
if (bg.a >= 0.999) break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
let g = { r: 255, g: 255, b: 255, a: 1 };
|
|
704
|
+
while (stack.length) g = over(stack.pop(), g);
|
|
705
|
+
return { ground: g, opacity };
|
|
706
|
+
};
|
|
707
|
+
const pairs = new Map();
|
|
708
|
+
for (const el of textEls) {
|
|
709
|
+
if (el.namespaceURI && el.namespaceURI !== 'http://www.w3.org/1999/xhtml') { layout.contrastAbstained += 1; continue; }
|
|
710
|
+
const cs = styleOf(el);
|
|
711
|
+
const fill = parseColor(cs.webkitTextFillColor);
|
|
712
|
+
const fg = fill || parseColor(cs.color);
|
|
713
|
+
if (!fg || fg.a === 0) continue;
|
|
714
|
+
if (cs.textShadow && cs.textShadow !== 'none') { layout.contrastAbstained += 1; continue; }
|
|
715
|
+
const at = groundOf(el);
|
|
716
|
+
if (at === 'exempt') continue;
|
|
717
|
+
if (at === null) { layout.contrastAbstained += 1; continue; }
|
|
718
|
+
if (at.opacity < 0.05) continue;
|
|
719
|
+
const ink = over({ r: fg.r, g: fg.g, b: fg.b, a: fg.a * at.opacity }, at.ground);
|
|
720
|
+
const ratio = Math.round(contrastOf(ink, at.ground) * 100) / 100;
|
|
721
|
+
layout.contrastChecked += 1;
|
|
722
|
+
if (ratio >= CONTRAST_FLOOR) continue;
|
|
723
|
+
const key = hexOf(ink) + '|' + hexOf(at.ground);
|
|
724
|
+
const seen = pairs.get(key);
|
|
725
|
+
if (seen) seen.count += 1;
|
|
726
|
+
else pairs.set(key, { text: el.textContent.trim().slice(0, 40), fg: hexOf(ink), bg: hexOf(at.ground), ratio, size: round(parseFloat(cs.fontSize) || 0), weight: String(cs.fontWeight || ''), cls: cls(el), count: 1 });
|
|
727
|
+
}
|
|
728
|
+
layout.lowContrast = [...pairs.values()].sort((a, b) => a.ratio - b.ratio).slice(0, 10);
|
|
729
|
+
}
|
|
730
|
+
|
|
657
731
|
const contentBottom = visible.reduce((lowest, el) => {
|
|
658
732
|
const r = el.getBoundingClientRect();
|
|
659
733
|
return r.width > 0 && r.height > 0 && r.bottom > lowest ? r.bottom : lowest;
|
|
@@ -821,7 +895,12 @@ var require_src = __commonJS({
|
|
|
821
895
|
* P112-OWN019-LETTEREDGE (Richard 2026-10-15, TL2's hero). Words within 3 px of the inner left or right edge of the
|
|
822
896
|
* box that frames them (a border or a fill) — a letter in an outlined chip set left because its Text stretched.
|
|
823
897
|
*/
|
|
824
|
-
TextAgainstEdge: "text-against-edge"
|
|
898
|
+
TextAgainstEdge: "text-against-edge",
|
|
899
|
+
/**
|
|
900
|
+
* P112-OWN019-LOOKBLIND (OWN-026's number pad: white digits on #FAF5FF, 1.07:1, through five changes). Text under 3:1
|
|
901
|
+
* against the background composited behind it — unreadable to anyone, and invisible to an agent that only reads text.
|
|
902
|
+
*/
|
|
903
|
+
LowContrastText: "low-contrast-text"
|
|
825
904
|
};
|
|
826
905
|
var SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
|
|
827
906
|
var ROW_GAP_MIN_TALLEST = 48;
|
|
@@ -867,6 +946,7 @@ var require_src = __commonJS({
|
|
|
867
946
|
const findings = [];
|
|
868
947
|
const add = (f) => findings.push(f);
|
|
869
948
|
let controlsJudged = false;
|
|
949
|
+
const contrastPairs = /* @__PURE__ */ new Map();
|
|
870
950
|
for (const [name, v] of Object.entries(viewports)) {
|
|
871
951
|
if (!v || v.error) continue;
|
|
872
952
|
const isDesktop = v.requested.width >= DESKTOP_WIDTH;
|
|
@@ -1112,6 +1192,12 @@ var require_src = __commonJS({
|
|
|
1112
1192
|
evidence: edges.slice(0, 6)
|
|
1113
1193
|
});
|
|
1114
1194
|
}
|
|
1195
|
+
for (const c of v.layout && v.layout.lowContrast || []) {
|
|
1196
|
+
const key = c.fg + "|" + c.bg;
|
|
1197
|
+
const prev = contrastPairs.get(key);
|
|
1198
|
+
if (!prev) contrastPairs.set(key, { ...c, viewport: name });
|
|
1199
|
+
else if (c.count > prev.count) prev.count = c.count;
|
|
1200
|
+
}
|
|
1115
1201
|
if (v.consoleErrors && v.consoleErrors.length) {
|
|
1116
1202
|
add({
|
|
1117
1203
|
code: RenderFinding.ConsoleError,
|
|
@@ -1122,6 +1208,17 @@ var require_src = __commonJS({
|
|
|
1122
1208
|
});
|
|
1123
1209
|
}
|
|
1124
1210
|
}
|
|
1211
|
+
if (contrastPairs.size) {
|
|
1212
|
+
const worst = [...contrastPairs.values()].sort((a, b) => a.ratio - b.ratio);
|
|
1213
|
+
const named = worst.slice(0, 3).map((c) => `"${c.text}" ${c.fg} on ${c.bg}, ${c.ratio}:1${c.count > 1 ? ` (and ${c.count - 1} more like it)` : ""}`);
|
|
1214
|
+
add({
|
|
1215
|
+
code: RenderFinding.LowContrastText,
|
|
1216
|
+
severity: "warning",
|
|
1217
|
+
viewport: worst[0].viewport,
|
|
1218
|
+
message: `${plural(worst.length, "colour pair", "colour pairs")} of text ${worst.length === 1 ? "reads" : "read"} under 3:1 against what is behind ${worst.length === 1 ? "it" : "them"}, so a person cannot read ${worst.length === 1 ? "it" : "them"}: ${named.join("; ")}. Pressing it still works, so a drive will not notice: set the text's colour or its background from the style vocabulary so they differ in lightness, not only in hue. Look at the ${worst[0].viewport} screenshot.`,
|
|
1219
|
+
evidence: worst.slice(0, 6)
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1125
1222
|
const seen = /* @__PURE__ */ new Set();
|
|
1126
1223
|
const unique = findings.filter((f) => {
|
|
1127
1224
|
const key = `${f.code}|${f.viewport}|${f.message}`;
|
|
@@ -2087,6 +2184,32 @@ var require_render_report = __commonJS({
|
|
|
2087
2184
|
}
|
|
2088
2185
|
};
|
|
2089
2186
|
}
|
|
2187
|
+
var PAGE_ANSWER_MS = Number(process.env.NODEGX_RENDER_PAGE_ANSWER_MS) || 3e4;
|
|
2188
|
+
async function pageAnswers(attaching, chromePath) {
|
|
2189
|
+
const neverAnswered = (refused) => {
|
|
2190
|
+
const message = "Chrome started (its debugging port opened) but its page " + (refused ? `refused the connection (${refused.message || refused}). ` : `never answered within ${Math.round(PAGE_ANSWER_MS / 1e3)} s. `) + `That is what Chrome does inside another sandbox (an agent harness that runs commands under sandbox-exec, a container): its own sandbox cannot start and its pages crash. Run the render outside that sandbox, or set CHROME_PATH to a script that starts ${chromePath} with --no-sandbox.`;
|
|
2191
|
+
const error = new Error(message);
|
|
2192
|
+
error.problems = [message];
|
|
2193
|
+
error.actionable = true;
|
|
2194
|
+
return error;
|
|
2195
|
+
};
|
|
2196
|
+
if (!attaching) throw neverAnswered();
|
|
2197
|
+
let timer;
|
|
2198
|
+
const deadline = new Promise((_, reject) => {
|
|
2199
|
+
timer = setTimeout(() => reject(neverAnswered()), PAGE_ANSWER_MS);
|
|
2200
|
+
});
|
|
2201
|
+
try {
|
|
2202
|
+
const answered = attaching.then(
|
|
2203
|
+
async (tab) => (await evaluate(tab.client, "1"), tab),
|
|
2204
|
+
(refused) => {
|
|
2205
|
+
throw neverAnswered(refused);
|
|
2206
|
+
}
|
|
2207
|
+
);
|
|
2208
|
+
return await Promise.race([answered, deadline]);
|
|
2209
|
+
} finally {
|
|
2210
|
+
clearTimeout(timer);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2090
2213
|
async function withRenderedPage(options, fn) {
|
|
2091
2214
|
const { projectDir: projectDir2, backendPort, editorTokens = false, chromeArgs = [], beforeBoot } = options;
|
|
2092
2215
|
reapOrphanedRenderProcesses();
|
|
@@ -2186,7 +2309,10 @@ var require_render_report = __commonJS({
|
|
|
2186
2309
|
if (!targets) throw new Error(`Chrome never opened a debugging port (${pre.chrome}).`);
|
|
2187
2310
|
const target = targets.find((t) => t.type === "page");
|
|
2188
2311
|
const settles = [];
|
|
2189
|
-
const main2 = await
|
|
2312
|
+
const main2 = await pageAnswers(
|
|
2313
|
+
target ? attachTab({ wsUrl: target.webSocketDebuggerUrl, servePort, settles }) : void 0,
|
|
2314
|
+
pre.chrome
|
|
2315
|
+
);
|
|
2190
2316
|
client = main2.client;
|
|
2191
2317
|
const openTab = async (label) => {
|
|
2192
2318
|
if (!browserClient) {
|
|
@@ -2251,9 +2377,11 @@ var require_render_report = __commonJS({
|
|
|
2251
2377
|
editorTokens = false,
|
|
2252
2378
|
renderRoutedPages = true,
|
|
2253
2379
|
page: requestedPage,
|
|
2254
|
-
concurrency = PAGE_TABS
|
|
2380
|
+
concurrency = PAGE_TABS,
|
|
2381
|
+
imageBudget
|
|
2255
2382
|
} = options;
|
|
2256
2383
|
const started = Date.now();
|
|
2384
|
+
const imageBytesEach = imageBudget ? Math.floor(imageBudget / Math.max(1, viewports.length)) : 0;
|
|
2257
2385
|
const routes = routedPages(projectDir2);
|
|
2258
2386
|
let focus;
|
|
2259
2387
|
if (requestedPage !== void 0) {
|
|
@@ -2285,24 +2413,45 @@ var require_render_report = __commonJS({
|
|
|
2285
2413
|
if (focusedOffStart) await page.navigate(focus.url);
|
|
2286
2414
|
const measured = {};
|
|
2287
2415
|
const screenshots = [];
|
|
2416
|
+
const take = (vp, client, format, height, quality) => client.send("Page.captureScreenshot", {
|
|
2417
|
+
format,
|
|
2418
|
+
...format === "jpeg" ? { quality } : {},
|
|
2419
|
+
captureBeyondViewport: screenshot === "full",
|
|
2420
|
+
optimizeForSpeed: true,
|
|
2421
|
+
...screenshot === "full" ? { clip: { x: 0, y: 0, width: vp.width, height, scale: deviceScaleFactor } } : {}
|
|
2422
|
+
});
|
|
2288
2423
|
const capture = async (vp, raw, component, subject2, client = page.client) => {
|
|
2289
|
-
const
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2424
|
+
const fullHeight = Math.min(raw.pageHeight, 16384);
|
|
2425
|
+
const shot = await take(vp, client, "png", fullHeight);
|
|
2426
|
+
const base = { name: vp.name, page: component, subject: subject2 };
|
|
2427
|
+
if (!imageBytesEach || shot.data.length <= imageBytesEach) return { ...base, mimeType: "image/png", base64: shot.data };
|
|
2428
|
+
return { ...base, ...await fitted(vp, client, fullHeight) };
|
|
2429
|
+
};
|
|
2430
|
+
const fitted = async (vp, client, fullHeight) => {
|
|
2431
|
+
const screenHeight = Math.max(1, vp.height);
|
|
2432
|
+
const screens = Math.max(1, Math.ceil(fullHeight / screenHeight));
|
|
2433
|
+
let quality = 70;
|
|
2434
|
+
let height = fullHeight;
|
|
2435
|
+
let shot = await take(vp, client, "jpeg", height, quality);
|
|
2436
|
+
if (screenshot === "full" && shot.data.length > imageBytesEach && screens > 1) {
|
|
2437
|
+
let keep = Math.min(screens - 1, Math.max(1, Math.floor(screens * imageBytesEach / shot.data.length)));
|
|
2438
|
+
for (; ; ) {
|
|
2439
|
+
height = Math.min(fullHeight, keep * screenHeight);
|
|
2440
|
+
shot = await take(vp, client, "jpeg", height, quality);
|
|
2441
|
+
if (shot.data.length <= imageBytesEach || keep === 1) break;
|
|
2442
|
+
keep -= 1;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
if (shot.data.length > imageBytesEach) {
|
|
2446
|
+
quality = 40;
|
|
2447
|
+
shot = await take(vp, client, "jpeg", height, quality);
|
|
2448
|
+
}
|
|
2449
|
+
const shown = screenshot === "full" ? Math.min(screens, Math.ceil(height / screenHeight)) : 1;
|
|
2450
|
+
return {
|
|
2451
|
+
mimeType: "image/jpeg",
|
|
2452
|
+
base64: shot.data,
|
|
2453
|
+
fit: { format: "jpeg", quality, screens: shown, of: screenshot === "full" ? screens : 1, overBudget: shot.data.length > imageBytesEach }
|
|
2454
|
+
};
|
|
2306
2455
|
};
|
|
2307
2456
|
const expression = measureExpression(placeholders, probesFor(subjectComponent));
|
|
2308
2457
|
let loggedThrough = 0;
|
|
@@ -2449,6 +2598,7 @@ var require_render_report = __commonJS({
|
|
|
2449
2598
|
// Exported so a session-teardown hook, a CI step or a spec can reap without
|
|
2450
2599
|
// starting a drive — the leak outlives the run that caused it.
|
|
2451
2600
|
reapOrphanedRenderProcesses,
|
|
2601
|
+
pageAnswers,
|
|
2452
2602
|
routedPages,
|
|
2453
2603
|
resolvePageRequest,
|
|
2454
2604
|
reachableComponents,
|
|
@@ -2509,7 +2659,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
2509
2659
|
"--scale",
|
|
2510
2660
|
"--backend-port",
|
|
2511
2661
|
"--page",
|
|
2512
|
-
"--concurrency"
|
|
2662
|
+
"--concurrency",
|
|
2663
|
+
"--image-budget"
|
|
2513
2664
|
]);
|
|
2514
2665
|
var flag = (name, fallback) => {
|
|
2515
2666
|
const i = argv.indexOf(name);
|
|
@@ -2573,7 +2724,8 @@ async function main() {
|
|
|
2573
2724
|
// FLD-011 — `--concurrency 1` is the serial sweep, and it is here so the arms
|
|
2574
2725
|
// that grade the parallel one are two runs of the SAME build differing by a
|
|
2575
2726
|
// number, rather than a run of this build against a `git show HEAD:` copy.
|
|
2576
|
-
...flag("--concurrency") ? { concurrency: Number(flag("--concurrency")) } : {}
|
|
2727
|
+
...flag("--concurrency") ? { concurrency: Number(flag("--concurrency")) } : {},
|
|
2728
|
+
...flag("--image-budget") ? { imageBudget: Number(flag("--image-budget")) } : {}
|
|
2577
2729
|
});
|
|
2578
2730
|
const out = flag("--out");
|
|
2579
2731
|
const files = [];
|