@nodegx/mcp 0.2.0 → 0.2.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.
Files changed (74) hide show
  1. package/dist/backend/cli.js +182 -182
  2. package/dist/cloud-bundle.cjs +747 -745
  3. package/dist/kit-extract.cjs +228 -228
  4. package/dist/kit-extract.inputs.json +113 -113
  5. package/dist/noodl-mcp.cjs +6073 -1090
  6. package/dist/package.json +1 -1
  7. package/dist/render-harness/data/node-catalog-enriched.json +242 -9
  8. package/dist/render-harness/drive-steps.js +222 -18
  9. package/dist/render-harness/measure-from-disk.js +172 -2
  10. package/dist/render-harness/render-from-disk.js +136 -136
  11. package/dist/src/external/viewer/noodl.viewer.js +1 -1
  12. package/dist/starter-assets/noodl_modules/inter/Inter-Bold.woff2 +0 -0
  13. package/dist/starter-assets/noodl_modules/inter/Inter-Medium.woff2 +0 -0
  14. package/dist/starter-assets/noodl_modules/inter/Inter-Regular.woff2 +0 -0
  15. package/dist/starter-assets/noodl_modules/inter/Inter-SemiBold.woff2 +0 -0
  16. package/dist/starter-assets/noodl_modules/inter/LICENSE.txt +93 -0
  17. package/dist/starter-assets/noodl_modules/inter/manifest.json +7 -0
  18. package/dist/starter-assets/noodl_modules/inter/styles.css +49 -0
  19. package/dist/starter-assets/noodl_modules/lucide-icons/lucide.woff2 +0 -0
  20. package/dist/starter-assets/noodl_modules/lucide-icons/manifest.json +229 -0
  21. package/dist/starter-assets/noodl_modules/lucide-icons/styles.css +2007 -0
  22. package/dist/starter-assets/noodl_modules/starter-imagery/LICENCES.json +445 -0
  23. package/dist/starter-assets/noodl_modules/starter-imagery/animal-cat.webp +0 -0
  24. package/dist/starter-assets/noodl_modules/starter-imagery/animal-dog.webp +0 -0
  25. package/dist/starter-assets/noodl_modules/starter-imagery/animal-horse.webp +0 -0
  26. package/dist/starter-assets/noodl_modules/starter-imagery/animal-sheep.webp +0 -0
  27. package/dist/starter-assets/noodl_modules/starter-imagery/avatar-1.webp +0 -0
  28. package/dist/starter-assets/noodl_modules/starter-imagery/avatar-2.webp +0 -0
  29. package/dist/starter-assets/noodl_modules/starter-imagery/avatar-3.webp +0 -0
  30. package/dist/starter-assets/noodl_modules/starter-imagery/avatar-4.webp +0 -0
  31. package/dist/starter-assets/noodl_modules/starter-imagery/avatar-5.webp +0 -0
  32. package/dist/starter-assets/noodl_modules/starter-imagery/avatar-6.webp +0 -0
  33. package/dist/starter-assets/noodl_modules/starter-imagery/food-bakery.webp +0 -0
  34. package/dist/starter-assets/noodl_modules/starter-imagery/food-board.webp +0 -0
  35. package/dist/starter-assets/noodl_modules/starter-imagery/food-bread.webp +0 -0
  36. package/dist/starter-assets/noodl_modules/starter-imagery/food-carrots.webp +0 -0
  37. package/dist/starter-assets/noodl_modules/starter-imagery/food-grocer.webp +0 -0
  38. package/dist/starter-assets/noodl_modules/starter-imagery/food-market.webp +0 -0
  39. package/dist/starter-assets/noodl_modules/starter-imagery/food-plate.webp +0 -0
  40. package/dist/starter-assets/noodl_modules/starter-imagery/ground-aurora.svg +28 -0
  41. package/dist/starter-assets/noodl_modules/starter-imagery/ground-canyon.webp +0 -0
  42. package/dist/starter-assets/noodl_modules/starter-imagery/ground-city-dusk.webp +0 -0
  43. package/dist/starter-assets/noodl_modules/starter-imagery/ground-coast.webp +0 -0
  44. package/dist/starter-assets/noodl_modules/starter-imagery/ground-dune.webp +0 -0
  45. package/dist/starter-assets/noodl_modules/starter-imagery/ground-forest-fog.webp +0 -0
  46. package/dist/starter-assets/noodl_modules/starter-imagery/ground-red-rock.webp +0 -0
  47. package/dist/starter-assets/noodl_modules/starter-imagery/ground-ridge.svg +28 -0
  48. package/dist/starter-assets/noodl_modules/starter-imagery/ground-shore.webp +0 -0
  49. package/dist/starter-assets/noodl_modules/starter-imagery/ground-skyline.webp +0 -0
  50. package/dist/starter-assets/noodl_modules/starter-imagery/manifest.json +4 -0
  51. package/dist/starter-assets/noodl_modules/starter-imagery/people-cafe.webp +0 -0
  52. package/dist/starter-assets/noodl_modules/starter-imagery/people-coffee-shop.webp +0 -0
  53. package/dist/starter-assets/noodl_modules/starter-imagery/people-desk.webp +0 -0
  54. package/dist/starter-assets/noodl_modules/starter-imagery/people-market.webp +0 -0
  55. package/dist/starter-assets/noodl_modules/starter-imagery/people-meeting.webp +0 -0
  56. package/dist/starter-assets/noodl_modules/starter-imagery/portrait.svg +11 -0
  57. package/dist/starter-assets/noodl_modules/starter-imagery/texture-brick.webp +0 -0
  58. package/dist/starter-assets/noodl_modules/starter-imagery/texture-coffee.webp +0 -0
  59. package/dist/starter-assets/noodl_modules/starter-imagery/texture-earth.webp +0 -0
  60. package/dist/starter-assets/noodl_modules/starter-imagery/texture-pebbles.webp +0 -0
  61. package/dist/starter-assets/noodl_modules/starter-imagery/texture-snow.webp +0 -0
  62. package/dist/starter-assets/noodl_modules/starter-imagery/texture-soil.webp +0 -0
  63. package/dist/starter-assets/noodl_modules/starter-imagery/tile-1.svg +17 -0
  64. package/dist/starter-assets/noodl_modules/starter-imagery/tile-2.svg +17 -0
  65. package/dist/starter-assets/noodl_modules/starter-imagery/tile-3.svg +17 -0
  66. package/dist/starter-assets/noodl_modules/starter-imagery/work-baker.webp +0 -0
  67. package/dist/starter-assets/noodl_modules/starter-imagery/work-carpenter.webp +0 -0
  68. package/dist/starter-assets/noodl_modules/starter-imagery/work-chef.webp +0 -0
  69. package/dist/starter-assets/noodl_modules/starter-imagery/work-hands-clay.webp +0 -0
  70. package/dist/starter-assets/noodl_modules/starter-imagery/work-leather-bench.webp +0 -0
  71. package/dist/starter-assets/noodl_modules/starter-imagery/work-machine-shop.webp +0 -0
  72. package/dist/starter-assets/noodl_modules/starter-imagery/work-potter.webp +0 -0
  73. package/dist/starter-assets/noodl_modules/starter-imagery/work-welder.webp +0 -0
  74. package/package.json +4 -3
@@ -492,6 +492,122 @@ var require_src = __commonJS({
492
492
  // viewport height with content laid out past it is clipped; a page that is
493
493
  // simply short has nothing past it. That difference is the whole of
494
494
  // \`clipped-page\`, and neither number alone can tell them apart.
495
+ // P112-OWN019-LAPTOPGAPS (R39) \u2014 where the INK is, not where the boxes are. Richard on RL1's laptop page: a
496
+ // feature number in a Columns "1 4" row had a fifth of the page to itself, ~215 px of nothing before its title;
497
+ // L1's pricing toggle and price sat ~260 px left of their centred heading. Every box there is well-formed \u2014 the
498
+ // number's column box abuts the title's \u2014 so the measure is each child's ink: the union of its shown text runs
499
+ // (a Range, so a full-width text block counts only as far as its words reach) and its painted boxes. Desktop only:
500
+ // that is where a row has room to open a hole. Raw numbers here; the judgement is summarise's.
501
+ const layout = { rowGaps: [], drifts: [] };
502
+ // (The API guard is for the fake DOMs the unit suites hand this expression; every browser has both.)
503
+ if (vw >= 1024 && typeof document.createRange === 'function' && typeof document.createTreeWalker === 'function') {
504
+ const shown = new Set(visible);
505
+ const textInk = [];
506
+ const range = document.createRange();
507
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
508
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
509
+ if (!n.textContent.trim() || !shown.has(n.parentElement)) continue;
510
+ range.selectNodeContents(n);
511
+ const r = range.getBoundingClientRect();
512
+ if (r.width > 0 && r.height > 0) textInk.push({ node: n, r });
513
+ }
514
+ const PAINTED_TAGS = ['IMG', 'SVG', 'CANVAS', 'VIDEO', 'INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'svg'];
515
+ const paintedInk = visible
516
+ .filter((el) => {
517
+ if (PAINTED_TAGS.includes(el.tagName)) return true;
518
+ const cs = styleOf(el);
519
+ const bg = cs.backgroundColor;
520
+ return (bg && bg !== 'transparent' && !/rgba\\([^)]*,\\s*0\\)$/.test(bg)) || cs.backgroundImage !== 'none' || parseFloat(cs.borderTopWidth) > 0;
521
+ })
522
+ .map((el) => ({ el, r: el.getBoundingClientRect() }))
523
+ .filter((p) => p.r.width > 0 && p.r.height > 0 && p.r.width < vw * 0.98);
524
+ const inkOf = (el) => {
525
+ let l = Infinity, t = Infinity, rr = -Infinity, b = -Infinity, text = '';
526
+ const take = (r) => { l = Math.min(l, r.left); t = Math.min(t, r.top); rr = Math.max(rr, r.right); b = Math.max(b, r.bottom); };
527
+ for (const x of textInk) if (el.contains(x.node)) { take(x.r); if (!text) text = x.node.textContent.trim().slice(0, 50); }
528
+ for (const p of paintedInk) if (el === p.el || el.contains(p.el)) take(p.r);
529
+ return l === Infinity ? null : { left: l, top: t, right: rr, bottom: b, width: rr - l, height: b - t, text };
530
+ };
531
+ const ROW_GAP_MIN_PX = 160;
532
+ for (const parent of visible) {
533
+ const kids = [...parent.children].filter((k) => shown.has(k));
534
+ if (kids.length < 2) continue;
535
+ const pr = parent.getBoundingClientRect();
536
+ if (pr.width < 600) continue;
537
+ const inks = kids.map(inkOf).filter(Boolean);
538
+ if (inks.length < 2) continue;
539
+ // Side by side: consecutive by left edge, overlapping vertically, the second starting after the first ends.
540
+ const byLeft = [...inks].sort((a, b) => a.left - b.left);
541
+ for (let i = 1; i < byLeft.length; i++) {
542
+ const a = byLeft[i - 1], b = byLeft[i];
543
+ const overlap = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
544
+ const gap = b.left - a.right;
545
+ if (overlap <= 0 || gap < ROW_GAP_MIN_PX) continue;
546
+ layout.rowGaps.push({ cls: cls(parent), gap: round(gap), rowWidth: round(pr.width), tallest: round(Math.max(a.height, b.height)), left: a.text, right: b.text });
547
+ }
548
+ // Stacked, each starting below the one before, and the parts of ONE section: every part under 240 px tall (a
549
+ // page whose children are whole sections compared section against section \u2014 measured on L1 and GL1, s5). The
550
+ // section's axis is set by a centred part among its first two (its heading, usually); L1's pricing is just
551
+ // two parts, a centred heading block and a toggle-and-price group ~260 px left of it.
552
+ const byTop = [...inks].sort((a, b) => a.top - b.top);
553
+ if (!byTop.every((k, i) => i === 0 || k.top >= byTop[i - 1].bottom - 2)) continue;
554
+ // Two or more tall parts, not one: RL1's waitlist is eyebrow, heading, line and a 290 px form card in ONE group,
555
+ // and "any part > 240" skipped it \u2014 Richard saw the eyebrow off the centred heading (s6). A page of sections
556
+ // has many tall children; a section with one card has one.
557
+ if (byTop.filter((k) => k.height > 240).length > 1) continue;
558
+ const centre = pr.left + pr.width / 2;
559
+ const offOf = (k) => k.left + k.width / 2 - centre;
560
+ const centred = byTop.filter((k) => Math.abs(offOf(k)) <= 12 && k.width < pr.width * 0.9);
561
+ if (!centred.some((k) => byTop.indexOf(k) < 2)) continue;
562
+ for (const k of byTop) {
563
+ if (Math.abs(offOf(k)) > 40 && k.width < pr.width * 0.8) {
564
+ layout.drifts.push({ cls: cls(parent), offBy: round(offOf(k)), sectionWidth: round(pr.width), text: k.text, centredText: centred[0].text });
565
+ }
566
+ }
567
+ }
568
+ }
569
+
570
+ // P112-OWN019-LETTEREDGE \u2014 words hard against the edge of the box that frames them. Richard on TL2's hero: seven
571
+ // outlined day chips, each letter pressed to its chip's left edge. The chip Group centred its child the way CSS
572
+ // would, but a Text stretches to its Group's width and sets its letters left, so the centring moved nothing. The
573
+ // frame is the nearest of the text's first four ancestors with a border or a fill (a page-wide band is not a frame,
574
+ // nor an inline highlight); the reading is the gap between the words' own ink (a Range) and the frame's inner left
575
+ // and right edges. Every width: a chip is a chip on a phone too. Raw numbers here; the judgement is summarise's.
576
+ layout.edgeTexts = [];
577
+ const EDGE_TEXT_MAX_GAP = 3;
578
+ if (typeof document.createRange === 'function' && typeof document.createTreeWalker === 'function') {
579
+ const shown = new Set(visible);
580
+ const range = document.createRange();
581
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
582
+ const framed = (el) => {
583
+ const cs = styleOf(el);
584
+ if (cs.display === 'inline') return false;
585
+ const bg = cs.backgroundColor;
586
+ return (bg && bg !== 'transparent' && !/rgba\\([^)]*,\\s*0\\)$/.test(bg)) || cs.backgroundImage !== 'none' ||
587
+ parseFloat(cs.borderLeftWidth) > 0 || parseFloat(cs.borderRightWidth) > 0;
588
+ };
589
+ const frames = new Set();
590
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
591
+ const text = n.textContent.trim();
592
+ if (!text || !shown.has(n.parentElement)) continue;
593
+ let frame = null;
594
+ for (let e = n.parentElement, i = 0; e && e !== document.body && i < 4; e = e.parentElement, i++) {
595
+ if (framed(e)) { frame = e; break; }
596
+ }
597
+ if (!frame || frames.has(frame)) continue;
598
+ const fr = frame.getBoundingClientRect();
599
+ if (fr.width < 16 || fr.width >= vw * 0.98) continue;
600
+ range.selectNodeContents(n);
601
+ const r = range.getBoundingClientRect();
602
+ if (!(r.width > 0 && r.height > 0)) continue;
603
+ const fcs = styleOf(frame);
604
+ const left = r.left - (fr.left + (parseFloat(fcs.borderLeftWidth) || 0));
605
+ const right = fr.right - (parseFloat(fcs.borderRightWidth) || 0) - r.right;
606
+ frames.add(frame);
607
+ if (Math.min(left, right) < EDGE_TEXT_MAX_GAP) layout.edgeTexts.push({ cls: cls(frame), text: text.slice(0, 50), left: round(left), right: round(right), frameWidth: round(fr.width), frameHeight: round(fr.height) });
608
+ }
609
+ }
610
+
495
611
  const contentBottom = visible.reduce((lowest, el) => {
496
612
  const r = el.getBoundingClientRect();
497
613
  return r.width > 0 && r.height > 0 && r.bottom > lowest ? r.bottom : lowest;
@@ -499,6 +615,7 @@ var require_src = __commonJS({
499
615
 
500
616
  return {
501
617
  lists,
618
+ layout,
502
619
  // FLD-012 AC3 \u2014 the SIZE of the population every rule below is computed over, and the number of
503
620
  // elements the visibility correction took out of it. Reported directly because a rule's finding
504
621
  // count can move for several reasons at once, and 'the fix changed what is measured' is a
@@ -637,9 +754,24 @@ var require_src = __commonJS({
637
754
  */
638
755
  SingleGround: "single-ground",
639
756
  NoImagery: "no-imagery",
640
- NoDisplayType: "no-display-type"
757
+ NoDisplayType: "no-display-type",
758
+ /**
759
+ * P112-OWN019-LAPTOPGAPS (R39, Richard 2026-10-14). On a laptop, two things side by side in one row with a hole
760
+ * between their ink — RL1's feature number given a fifth of the page by a Columns "1 4" row, L1's text and card
761
+ * pushed to the row's two edges. A warning: a hole can be meant, and the screenshots are the arbiter.
762
+ */
763
+ RowGap: "row-gap",
764
+ /** P112-OWN019-LAPTOPGAPS — a section that centres its heading and leaves another part off that centre (L1's price). */
765
+ AlignmentDrift: "alignment-drift",
766
+ /**
767
+ * P112-OWN019-LETTEREDGE (Richard 2026-10-15, TL2's hero). Words within 3 px of the inner left or right edge of the
768
+ * box that frames them (a border or a fill) — a letter in an outlined chip set left because its Text stretched.
769
+ */
770
+ TextAgainstEdge: "text-against-edge"
641
771
  };
642
772
  var SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
773
+ var ROW_GAP_MIN_TALLEST = 48;
774
+ var ROW_GAP_MIN_SHARE = 0.12;
643
775
  var POVERTY_FINDINGS = [RenderFinding.SingleGround, RenderFinding.NoImagery, RenderFinding.NoDisplayType];
644
776
  function isPovertyFinding(finding) {
645
777
  return Boolean(finding) && POVERTY_FINDINGS.indexOf(finding.code) !== -1;
@@ -855,6 +987,43 @@ var require_src = __commonJS({
855
987
  evidence: v.grounds.values
856
988
  });
857
989
  }
990
+ if (isDesktop && v.layout) {
991
+ const gaps = (v.layout.rowGaps || []).filter((g) => (g.left || g.right) && g.tallest >= ROW_GAP_MIN_TALLEST && g.gap >= g.rowWidth * ROW_GAP_MIN_SHARE);
992
+ const seen2 = /* @__PURE__ */ new Set();
993
+ for (const g of gaps) {
994
+ const key = `${g.left}|${g.right}`;
995
+ if (seen2.has(key) || seen2.size >= 3) continue;
996
+ seen2.add(key);
997
+ add({
998
+ code: RenderFinding.RowGap,
999
+ severity: "warning",
1000
+ viewport: name,
1001
+ message: `"${g.left}" and "${g.right}" sit side by side with ${g.gap}px of empty space between them (${Math.round(g.gap / g.rowWidth * 100)}% of the ${g.rowWidth}px row). A label or number beside text reads as one thing at its own width with a small gap \u2014 a row Group, the label at its natural width and the text growing \u2014 not a column given a share of the page (Columns widths are proportions); two parts pushed to a wide row's edges leave the same hole: give the row a max width and a gap. Look at this width's screenshot.`,
1002
+ evidence: g
1003
+ });
1004
+ }
1005
+ const drifts = (v.layout.drifts || []).slice(0, 3);
1006
+ for (const d of drifts) {
1007
+ add({
1008
+ code: RenderFinding.AlignmentDrift,
1009
+ severity: "warning",
1010
+ viewport: name,
1011
+ message: `This section centres "${d.centredText}", but "${d.text}" sits ${Math.abs(d.offBy)}px ${d.offBy < 0 ? "left" : "right"} of that centre. One section, one alignment: centre the rest too, or align everything to the same left edge. Look at this width's screenshot.`,
1012
+ evidence: d
1013
+ });
1014
+ }
1015
+ }
1016
+ const edges = v.layout && v.layout.edgeTexts || [];
1017
+ if (edges.length) {
1018
+ const named = edges.slice(0, 3).map((e) => `"${e.text}" ${Math.max(0, Math.min(e.left, e.right))}px from the ${e.left <= e.right ? "left" : "right"} edge of its ${e.frameWidth}\xD7${e.frameHeight}px box`);
1019
+ add({
1020
+ code: RenderFinding.TextAgainstEdge,
1021
+ severity: "warning",
1022
+ viewport: name,
1023
+ message: `${plural(edges.length, "text sits", "texts sit")} hard against the edge of the outlined or filled box that frames ${edges.length === 1 ? "it" : "them"}: ${named.join("; ")}. Centring a Group does not centre a Text's letters \u2014 the Text stretches to the Group's width and sets them left \u2014 so give the Text textAlignX: center, or give the box padding. Look at this width's screenshot.`,
1024
+ evidence: edges.slice(0, 6)
1025
+ });
1026
+ }
858
1027
  if (v.consoleErrors && v.consoleErrors.length) {
859
1028
  add({
860
1029
  code: RenderFinding.ConsoleError,
@@ -964,6 +1133,7 @@ var require_render_report = __commonJS({
964
1133
  var WS_MODULE = HARNESS_PATHS.WS_MODULE.path;
965
1134
  var {
966
1135
  DEFAULT_VIEWPORTS,
1136
+ NAMED_VIEWPORTS,
967
1137
  DESKTOP_WIDTH,
968
1138
  RenderFinding,
969
1139
  SEVERITY_ORDER,
@@ -1499,7 +1669,7 @@ var require_render_report = __commonJS({
1499
1669
  }
1500
1670
  function parseViewports2(spec) {
1501
1671
  if (!spec) return DEFAULT_VIEWPORTS;
1502
- const known = new Map(DEFAULT_VIEWPORTS.map((v) => [v.name, v]));
1672
+ const known = new Map(NAMED_VIEWPORTS.map((v) => [v.name, v]));
1503
1673
  return spec.split(",").map((token) => {
1504
1674
  const trimmed = token.trim();
1505
1675
  if (known.has(trimmed)) return known.get(trimmed);