@bezda/rhp-mcp 0.1.6 → 0.1.8

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.
@@ -94,9 +94,21 @@ export async function interact(page, { events, capture, shoot, taps = false }) {
94
94
  window.__rhpProbe.record(keep);
95
95
  }, {keep:texts,view});
96
96
  if (!error) await attempt(act);
97
+ // a control that sets the page moving (Play) is read as it moves too: a text that jitters while the values run
98
+ // may be back in place at the end
99
+ const passing = [];
100
+ for (let k = 0; !lit && !error && k < 5; k++) {
101
+ await page.waitForTimeout(200);
102
+ passing.push(...(await page.evaluate((n) => window.__rhpProbe.moved(n, false), used).catch(() => [])).filter((m) => m.text));
103
+ }
97
104
  await page.evaluate(() => window.__rhpProbe.settle(1500)).catch(() => {});
98
105
  const seen = await page.evaluate(() => window.__rhpProbe.stop()).catch(() => null);
99
106
  const moved = await page.evaluate(([n, l]) => window.__rhpProbe.moved(n, l), [used, lit]).catch(() => []);
107
+ for (const m of passing) {
108
+ const was = moved.find((x) => x.what === m.what);
109
+ if (!was) moved.push(m);
110
+ else if (Math.abs(m.dx) + Math.abs(m.dy) > Math.abs(was.dx) + Math.abs(was.dy)) Object.assign(was, m);
111
+ }
100
112
  // a value a slat read once, and that this interaction changed, is shown stale (the guard reads them again)
101
113
  await page.evaluate(() => globalThis.__rhpCheck?.recheck?.()).catch(() => {});
102
114
  const thrown = events.slice(before).filter((e) => e.kind === "pageerror" || e.kind === "console-error");
@@ -5,7 +5,7 @@
5
5
  const PASSES = [
6
6
  [["runtime-error", "console-error", "syntax-error", "missing-import", "missing-import-map", "plain-script-tag", "interaction-error", "missing-export"], "no errors thrown"],
7
7
  [["rhp-warning"], "no rhp warnings"],
8
- [["unknown-prop", "bad-prop", "bad-scale", "dot-size", "missing-group", "plot-typo", "group-lengths", "no-component", "bare-boolean", "handler-at-render", "style-string", "class-name", "two-solids", "series-past-theme", "page-css-ignored", "block-transition", "stale-read", "static-chart"], "rhp used as documented"],
8
+ [["unknown-prop", "bad-prop", "bad-scale", "dot-size", "missing-group", "plot-typo", "group-lengths", "no-component", "bare-boolean", "handler-at-render", "style-string", "class-name", "two-solids", "series-past-theme", "page-css-ignored", "page-css-reaches", "block-transition", "stale-read", "static-chart"], "rhp used as documented"],
9
9
  [["no-chart", "empty-plot", "small-plot", "collapsed-chart", "plot-outside-chart"], null],
10
10
  [["nan-value"], "no NaN values"],
11
11
  [["value-past-scale", "points-past-scale"], "values inside the scale"],
@@ -44,6 +44,7 @@ export function report(r) {
44
44
  list.forEach((f, i) => {
45
45
  lines.push(`${i + 1}. [${f.code}] ${f.message}${widthsOf(f)}`);
46
46
  if (f.fix) lines.push(` Fix: ${f.fix}`);
47
+ if (f.see) lines.push(` See: ${f.see}`);
47
48
  });
48
49
  };
49
50
 
@@ -88,9 +89,15 @@ export function report(r) {
88
89
  if (quiet.includes(x.kind)) continue;
89
90
  lines.push(`- ${x.kind} ${x.target}${x.width !== r.interactedAt ? ` at ${x.width}px` : ""}: ${x.how}${x.error ? `, and it threw: ${x.error}` : ""}`);
90
91
  }
91
- // the sweep: one line, every width at which every slat was picked in turn
92
+ // the sweep: a line for the pointer moved along the chart, where it was, and one for every slat picked in turn, at
93
+ // every width at which it was (none when no slat could be reached: nothing was picked)
92
94
  const swept = (r.swept ?? []).filter((x) => x.slats);
93
- if (swept.length) lines.push(`- every slat picked in turn (${swept[0].slats}) at ${swept.map((x) => `${x.width}px`).join(", ")}: ${swept.some((x) => x.jumps) ? "the layout jumped (see the warnings)" : "nothing else moved"}`);
95
+ const at = (list) => list.map((x) => `${x.width}px`).join(", ");
96
+ const scrubbed = swept.filter((x) => x.scrubbed);
97
+ if (scrubbed.length) lines.push(`- pointer moved along the chart (${scrubbed[0].scrubbed.points} steps) at ${at(scrubbed)}: ${scrubbed.some((x) => x.scrubbed.moved) ? "the layout or the readout moved (see the warnings)" : "nothing else moved"}`);
98
+ const picked = swept.filter((x) => x.reached ?? x.slats);
99
+ if (picked.length) lines.push(`- every slat picked in turn (${picked[0].reached < picked[0].slats ? `${picked[0].reached} of ${picked[0].slats} reached` : picked[0].slats}) at ${at(picked)}: ${picked.some((x) => x.jumps) ? "the layout jumped (see the warnings)" : "nothing else moved"}`);
100
+ else if (swept.length) lines.push(`- no slat picked in turn: something covers the slats' marks at ${at(swept)}`);
94
101
  } else if (r.charts?.length && r.interactedAt) {
95
102
  lines.push("", "Interactions: nothing to interact with (no slats to point at, no buttons, ranges or selects).");
96
103
  }
@@ -77,6 +77,21 @@ export async function selectionBehavior(page, { events }) {
77
77
  const controlled = await controlSelection(page, { slats, off, look, away, settle });
78
78
  findings.push(...controlled.findings);
79
79
  if (controlled.tested.length) audit.tested = { controls: controlled.tested };
80
+ // The control audit leaves focus on the last control it tried. Its focus ring is not the chart's state,
81
+ // and a click beside the chart takes it away, so the reset would differ from the rest by that ring alone.
82
+ await page.evaluate(() => document.activeElement?.blur?.());
83
+ // The reset is the chart's state and its readout, not the controls' own look: a control keeps a focus ring, a hover
84
+ // or a pressed state of its own after the clicks, so the reset leaves out the controls' boxes (as the control audit
85
+ // does), with room for a ring around them
86
+ const controls = await page.evaluate((target) => {
87
+ const poster = window.__rhpTargets[target]?.closest(".poster, figure") ?? document.body;
88
+ return [...poster.querySelectorAll('button, input, select, textarea, summary, [role="button"], [role="slider"], [role="switch"], [role="tab"]')]
89
+ .filter((el) => !el.closest(".rhp-chart"))
90
+ .map((el) => el.getBoundingClientRect())
91
+ .filter((r) => r.width > 0 && r.height > 0)
92
+ .map((r) => ({ left: r.left - 8, right: r.right + 8, top: r.top - 8, bottom: r.bottom + 8 }));
93
+ }, slats[0].target);
94
+ const quiet = (a, b) => seenPixels(a, b, controls) < SAME;
80
95
  if (slats.length < 2) return audit;
81
96
  const candidates = [];
82
97
  for (const s of slats) {
@@ -128,7 +143,7 @@ export async function selectionBehavior(page, { events }) {
128
143
  await page.mouse.click(off.x, off.y);
129
144
  await away();
130
145
  reset = await look();
131
- resetBroken = !same(rest, reset);
146
+ resetBroken = !quiet(rest, reset);
132
147
  }
133
148
  const end = await page.evaluate(() => [scrollX, scrollY]);
134
149
  if (position.some((v, i) => v !== end[i]) || !same(reset, await look())) return audit;
@@ -13,10 +13,11 @@ import { closest } from "../guard/rules.js";
13
13
  const STANDALONE = "@bezda/rhp/standalone";
14
14
 
15
15
  // The props of rhp's components (and Solid's Show) that take a boolean, and what "" (false) does to each
16
- const BOOLEANS = { Chart: ["animate", "static"], Plot: ["overlap", "static", "keyboard", "animate"], Area: ["mirror", "smooth"], Line: ["fill", "smooth"], Show: ["keyed"] };
16
+ const BOOLEANS = { Chart: ["animate", "static", "noCssGuard"], Plot: ["overlap", "static", "keyboard", "animate"], Area: ["mirror", "smooth"], Line: ["fill", "smooth"], Show: ["keyed"] };
17
17
  const OFF = {
18
18
  overlap: "its slats don't share one band",
19
19
  static: "the chart is not static",
20
+ noCssGuard: "the chart keeps the guard",
20
21
  keyboard: "its slats take no focus",
21
22
  animate: "the JS version stays off",
22
23
  mirror: "the Area is not mirrored",
@@ -25,7 +25,7 @@ export const SETTING_VALUE = {
25
25
  children: (v) => typeof v === "function",
26
26
  };
27
27
 
28
- export const CHART_PROPS = ["scale", "orientation", "height", "aspect", "ticks", "grid", "format", "animate", "theme", "static", "cross", "crossTicks", "crossGrid", "crossFormat", "label", "id", "class", "role", "style", "ref", "children"];
28
+ export const CHART_PROPS = ["scale", "orientation", "height", "aspect", "ticks", "grid", "format", "animate", "theme", "static", "noCssGuard", "cross", "crossTicks", "crossGrid", "crossFormat", "label", "id", "class", "role", "style", "ref", "children"];
29
29
 
30
30
  export const THEME_KEYS = ["series", "positive", "negative", "ink", "muted", "grid", "surface", "low", "high", "font"];
31
31
 
@@ -58,7 +58,7 @@ export const RENAMES = {
58
58
  Label: { value: "at", x: "at", y: "cross", position: "side", align: "side", text: "children", label: "children" },
59
59
  Area: { data: "points", values: "points", max: "peak", fill: "color", colour: "color", curved: "smooth" },
60
60
  Line: { data: "points", values: "points", max: "peak", curved: "smooth", area: "fill", colour: "color" },
61
- Chart: { domain: "scale", range: "scale", extent: "scale", horizontal: "orientation", vertical: "orientation", direction: "orientation", colors: "theme", palette: "theme", tickFormat: "format", ariaLabel: "label", title: "label" },
61
+ Chart: { domain: "scale", range: "scale", extent: "scale", horizontal: "orientation", vertical: "orientation", direction: "orientation", colors: "theme", palette: "theme", tickFormat: "format", ariaLabel: "label", title: "label", noGuard: "noCssGuard", unguarded: "noCssGuard", noCSSGuard: "noCssGuard" },
62
62
  Poster: { headline: "title", heading: "title", subtitle: "dek", subheading: "dek", subhead: "dek", description: "dek", lede: "dek", standfirst: "dek", source: "note", footnote: "note", credit: "note", credits: "note", caption: "note", footer: "note", eyebrow: "kicker", overline: "kicker", tag: "kicker" },
63
63
  slat: { height: "thickness", width: "thickness", size: "thickness", thick: "thickness", pitch: "thickness", padding: "room", margin: "room", gutter: "room", gutters: "room", gap: "inset", style: "css", styles: "css", class: "css", className: "css" },
64
64
  theme: { colors: "series", palette: "series", accent: "series", text: "ink", foreground: "ink", color: "ink", background: "surface", bg: "surface", paper: "surface", gridline: "grid", gridlines: "grid", axis: "grid", fontFamily: "font", family: "font", typeface: "font" },
package/src/server.js CHANGED
@@ -21,7 +21,7 @@ const LIMIT = 50;
21
21
  const MAX_WIDTH = 1280;
22
22
  const MAX_HEIGHT = 2000;
23
23
 
24
- const INSTRUCTIONS = `rhp 2 (@bezda/rhp) makes charts out of HTML elements and CSS with SolidJS. It came out in September 2026, after your training data, so what you remember about rhp is wrong or belongs to the unrelated rhp 1. Before you write or change any rhp code, call rhp_guide and follow its workflow. Take each chart's technique from the closest recipe (rhp_recipe) and design the chart for its own subject, and look up every prop you are unsure of with rhp_reference instead of guessing. After every edit, call rhp_check with the chart file's absolute path, fix what it reports and look at its screenshots, until it reports no errors and no warnings.`;
24
+ const INSTRUCTIONS = `rhp 2 (@bezda/rhp) makes charts out of HTML elements and CSS with SolidJS. It came out in September 2026, after your training data, so what you remember about rhp is wrong or belongs to the unrelated rhp 1. Before you write or change any rhp code, call rhp_guide and follow its workflow. Take each chart's technique from the closest recipe (rhp_recipe) and design the chart for its own subject. Choose interaction behavior and presentation for the reader's task and the intended design; familiar patterns are welcome when they fit. Look up every prop you are unsure of with rhp_reference instead of guessing. After every edit, call rhp_check with the chart file's absolute path, fix what it reports and look at its screenshots, until it reports no errors and no warnings.`;
25
25
 
26
26
  const text = (body) => ({ content: [{ type: "text", text: body }] });
27
27
  const failure = (body) => ({ content: [{ type: "text", text: body }], isError: true });
@@ -185,7 +185,7 @@ server.registerTool("rhp_guide", {
185
185
 
186
186
  server.registerTool("rhp_reference", {
187
187
  title: "rhp reference",
188
- description: "Returns one of rhp's references by name: api (every component, prop, helper and CSS variable, exactly), design (art direction from the subject, the editorial poster, color, type, layout), looks (eight looks written out in full, as examples), interaction (which interaction for which story, with code), download (the fit-to-screen and PNG download icons for standalone poster pages), environments (plain HTML, Solid, React, Next.js, Vue, Svelte, Angular, Astro), forms (the subject's own form, which chart for which data and story), pitfalls (mistakes and their fixes). Look props up here instead of guessing. A long reference comes back as its list of sections: call again with section to get the one you need.",
188
+ description: "Returns one of rhp's references by name: api (every component, prop, helper and CSS variable, exactly), design (art direction from the subject, the editorial poster, color, type, layout), looks (eight looks written out in full, as examples), interaction (choosing behavior and presentation for the reader's task, with implementation examples), download (the fit-to-screen and PNG download icons for standalone poster pages), environments (plain HTML, Solid, React, Next.js, Vue, Svelte, Angular, Astro), forms (the subject's own form, which chart for which data and story), pitfalls (mistakes and their fixes). Look props up here instead of guessing. A long reference comes back as its list of sections: call again with section to get the one you need.",
189
189
  inputSchema: {
190
190
  name: z.string().describe("The reference: api, design, environments, forms, interaction, looks or pitfalls"),
191
191
  section: z.string().optional().describe('One "##" section of the reference, by its number or title, for example "4" or "Plot"'),
@@ -200,7 +200,7 @@ server.registerTool("rhp_reference", {
200
200
 
201
201
  server.registerTool("rhp_recipe", {
202
202
  title: "rhp recipe",
203
- description: 'Lists rhp\'s recipes (call it with no type), or returns one recipe\'s complete HTML file (type, for example "bar"). A recipe is a tested chart page that shows the technique for one kind of chart: take its technique (how its Plots, slats and blocks are composed, its interaction, its phone rules) and leave its design, which was made for its own subject.',
203
+ description: 'Lists rhp\'s recipes (call it with no type), or returns one recipe\'s complete HTML file (type, for example "bar"). A recipe is a tested chart page demonstrating composition, state and event handling, accessibility and responsive techniques. Choose the chart\'s interaction behavior and presentation for the reader\'s task and intended design. Familiar patterns are welcome when they fit; the recipe\'s controls, feedback, layout and transitions are choices to reconsider.',
204
204
  inputSchema: {
205
205
  type: z.string().optional().describe('The recipe, for example "bar", "line" or "donut"; leave it out for the list'),
206
206
  },
@@ -239,7 +239,7 @@ server.registerPrompt("chart", {
239
239
  role: "user",
240
240
  content: {
241
241
  type: "text",
242
- text: `Make this chart with rhp (@bezda/rhp): ${request}\n\nFirst call rhp_guide and follow its workflow: write the brief, take the technique from the closest recipe (rhp_recipe), design the chart for its own subject, and look up props with rhp_reference instead of guessing. After every edit, call rhp_check with the chart file's absolute path, fix what it reports and look at its screenshots, until it reports no errors and no warnings.`,
242
+ text: `Make this chart with rhp (@bezda/rhp): ${request}\n\nFirst call rhp_guide and follow its workflow: write the brief, take the technique from the closest recipe (rhp_recipe), design the chart for its own subject, and look up props with rhp_reference instead of guessing. Choose interaction behavior and presentation for the reader's task and intended design, keeping familiar patterns when they fit. After every edit, call rhp_check with the chart file's absolute path, fix what it reports and look at its screenshots, until it reports no errors and no warnings.`,
243
243
  },
244
244
  }],
245
245
  }));