@linktr.ee/arbor-mcp 0.9.0 → 0.10.0

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 (2) hide show
  1. package/dist/index.js +99 -1
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -309,6 +309,60 @@ var require_rules = __commonJS({
309
309
  ]
310
310
  }
311
311
  },
312
+ "directional-language": {
313
+ id: "directional-language",
314
+ name: "Directional language",
315
+ description: "Copy shouldn't rely on spatial directions. Element position changes across screen sizes and surfaces, and directional words are meaningless to screen-reader users.",
316
+ severity: "info",
317
+ category: "accessibility",
318
+ check: {
319
+ type: "regex",
320
+ description: "Flags spatial/directional references that don't survive layout changes across surfaces"
321
+ },
322
+ phrases: [
323
+ {
324
+ pattern: "\\b(?:(?:the|this|that|these|those)\\s+\\w+|see|shown|listed|described|mentioned|noted|explained|outlined)\\s+above\\b",
325
+ matchType: "regex",
326
+ label: "above",
327
+ reason: `"Above" is a spatial reference; element position changes across surfaces and it's meaningless to screen-reader users.`,
328
+ suggestion: 'Name the thing instead of its position, e.g. "in Settings" or "the previous step".'
329
+ },
330
+ {
331
+ pattern: "\\b(?:(?:the|this|that|these|those)\\s+\\w+|see|shown|listed|described|mentioned|noted|explained|outlined)\\s+below\\b",
332
+ matchType: "regex",
333
+ label: "below",
334
+ reason: `"Below" is a spatial reference; element position changes across surfaces and it's meaningless to screen-reader users.`,
335
+ suggestion: 'Name the thing instead of its position, e.g. "in Settings" or "the next step".'
336
+ },
337
+ {
338
+ pattern: "\\b(?:tap|press|go|see)\\s+here\\b",
339
+ matchType: "regex",
340
+ label: "directional 'here'",
341
+ reason: `Directional "here" gives weak information scent and doesn't work for screen readers.`,
342
+ suggestion: 'Use a specific action, e.g. "View pricing" or "Open settings".'
343
+ }
344
+ ]
345
+ },
346
+ "control-reference": {
347
+ id: "control-reference",
348
+ name: "Control reference",
349
+ description: `Copy shouldn't tell people to act on a control by its type ("click the button", "tap the link"). Screen-reader users don't perceive the control the same way, and the phrasing is redundant \u2014 label or hyperlink the action itself.`,
350
+ severity: "info",
351
+ category: "accessibility",
352
+ check: {
353
+ type: "regex",
354
+ description: "Flags instructions that reference a UI control by type instead of hyperlinking or labelling the action"
355
+ },
356
+ phrases: [
357
+ {
358
+ pattern: "\\b(?:clicking|click|tapping|tap|pressing|press|hitting|hit)\\s+(?:on\\s+)?(?:(?:the|this|that)\\s+)?(?:[\\w-]+\\s+){0,2}(?:button|link)\\b",
359
+ matchType: "regex",
360
+ label: "click the button",
361
+ reason: "Referencing a control by type is meaningless to screen-reader users and redundant on screen.",
362
+ suggestion: 'Hyperlink or label the action itself, e.g. make "Reset your password" the button or link text.'
363
+ }
364
+ ]
365
+ },
312
366
  "word-count": {
313
367
  id: "word-count",
314
368
  name: "Word count limits",
@@ -1024,6 +1078,20 @@ var require_engine = __commonJS({
1024
1078
  return { ...phrase, _regex: null };
1025
1079
  }
1026
1080
  });
1081
+ var compiledDirectional = rules["directional-language"].phrases.map((phrase) => {
1082
+ try {
1083
+ return { ...phrase, _regex: new RegExp(phrase.pattern, "i") };
1084
+ } catch {
1085
+ return { ...phrase, _regex: null };
1086
+ }
1087
+ });
1088
+ var compiledControlReference = rules["control-reference"].phrases.map((phrase) => {
1089
+ try {
1090
+ return { ...phrase, _regex: new RegExp(phrase.pattern, "i") };
1091
+ } catch {
1092
+ return { ...phrase, _regex: null };
1093
+ }
1094
+ });
1027
1095
  function countSyllables(word) {
1028
1096
  const w = word.toLowerCase().replace(/[^a-z]/g, "");
1029
1097
  if (w.length <= 3) return 1;
@@ -1077,6 +1145,34 @@ var require_engine = __commonJS({
1077
1145
  });
1078
1146
  }
1079
1147
  }
1148
+ function checkDirectionalLanguage(text, issues) {
1149
+ for (const phrase of compiledDirectional) {
1150
+ if (phrase._regex !== null && phrase._regex.test(text)) {
1151
+ issues.push({
1152
+ ruleId: "directional-language",
1153
+ ruleName: "Directional language",
1154
+ severity: "info",
1155
+ message: `"${phrase.label}" \u2014 ${phrase.reason}`,
1156
+ suggestion: phrase.suggestion
1157
+ });
1158
+ }
1159
+ }
1160
+ }
1161
+ function checkControlReference(text, issues) {
1162
+ for (const phrase of compiledControlReference) {
1163
+ if (phrase._regex === null) continue;
1164
+ const match = text.match(phrase._regex);
1165
+ if (match) {
1166
+ issues.push({
1167
+ ruleId: "control-reference",
1168
+ ruleName: "Control reference",
1169
+ severity: "info",
1170
+ message: `"${match[0] || phrase.label}" \u2014 ${phrase.reason}`,
1171
+ suggestion: phrase.suggestion
1172
+ });
1173
+ }
1174
+ }
1175
+ }
1080
1176
  function checkGenericButtons(text, issues) {
1081
1177
  for (const entry of rules["generic-buttons"].bannedLabels) {
1082
1178
  if (entry.notes) continue;
@@ -1161,6 +1257,8 @@ var require_engine = __commonJS({
1161
1257
  if (!trimmed) return { score: 10, issues };
1162
1258
  checkSentenceCase(trimmed, issues);
1163
1259
  checkBannedPhrases(trimmed, contentType, issues);
1260
+ checkDirectionalLanguage(trimmed, issues);
1261
+ checkControlReference(trimmed, issues);
1164
1262
  if (contentType === "button") checkGenericButtons(trimmed, issues);
1165
1263
  checkPassiveVoice(trimmed, issues);
1166
1264
  if (contentType) checkWordCount(trimmed, contentType, issues);
@@ -5943,7 +6041,7 @@ var CONTENT_TYPES = [
5943
6041
  var MAX_TEXT_LENGTH = 5e3;
5944
6042
  var TOOL_NAME2 = "arbor_check_ux_writing";
5945
6043
  var TOOL_TITLE2 = "Check UX writing against Arbor voice rules";
5946
- var TOOL_DESCRIPTION2 = `Lint a UI string against Arbor's voice and UX-writing rules (sentence case, banned/generic phrases, passive voice, word count, error structure, readability) and return a 0\u201310 score plus structured issues with suggestions. Deterministic and offline \u2014 the same engine behind the Slack /ux-check command. Pass contentType (e.g. "button", "error-system") to enable context-specific checks. This judges copy quality; it does NOT rewrite the text.`;
6044
+ var TOOL_DESCRIPTION2 = `Lint a UI string against Arbor's voice and UX-writing rules (sentence case, banned/generic phrases, passive voice, word count, error structure, readability, accessibility checks like directional language and control references) and return a 0\u201310 score plus structured issues with suggestions. Deterministic and offline \u2014 the same engine behind the Slack /ux-check command. Pass contentType (e.g. "button", "error-system") to enable context-specific checks. This judges copy quality; it does NOT rewrite the text.`;
5947
6045
  var inputSchema2 = external_exports.object({
5948
6046
  text: external_exports.string().trim().min(1, "text is required and must be a non-empty string").max(MAX_TEXT_LENGTH, `text must be ${MAX_TEXT_LENGTH} characters or fewer`).describe("The UI string to lint"),
5949
6047
  contentType: external_exports.enum(CONTENT_TYPES).optional().describe("What kind of UI text this is \u2014 enables context-specific rules (omit for content-agnostic checks)")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/arbor-mcp",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Model Context Protocol server exposing Arbor design system tools: Playroom snippets, Figma→code sync health, UX-writing checks, decision-log lookup, and federated component/version metadata.",
5
5
  "keywords": [
6
6
  "arbor",