explorbot 0.5.8 → 0.5.10

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 (83) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +25 -4
  3. package/boat/api-tester/src/commands/init-command.ts +2 -0
  4. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -1
  5. package/boat/prima/README.md +3 -0
  6. package/boat/prima/bin/prima-cli.ts +2 -1
  7. package/boat/prima/src/cli.ts +7 -1
  8. package/boat/prima/src/envelope.ts +18 -4
  9. package/boat/prima/src/help.ts +15 -4
  10. package/boat/prima/src/prima.ts +181 -25
  11. package/dist/bin/explorbot-cli.js +31 -4
  12. package/dist/boat/api-tester/bin/apibot-cli.js +2 -1
  13. package/dist/boat/api-tester/src/commands/init-command.js +2 -0
  14. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -1
  15. package/dist/boat/prima/bin/prima-cli.js +2 -1
  16. package/dist/boat/prima/src/cli.js +6 -1
  17. package/dist/boat/prima/src/envelope.js +16 -3
  18. package/dist/boat/prima/src/help.js +14 -4
  19. package/dist/boat/prima/src/prima.js +191 -27
  20. package/dist/package.json +1 -1
  21. package/dist/rules/navigator/verification-actions.md +5 -3
  22. package/dist/rules/planner/ignored-pages.md +6 -0
  23. package/dist/rules/quartermaster/ux/accessibility.md +8 -0
  24. package/dist/rules/quartermaster/ux/charts.md +6 -0
  25. package/dist/rules/quartermaster/ux/copy.md +9 -0
  26. package/dist/rules/quartermaster/ux/forms.md +7 -0
  27. package/dist/rules/quartermaster/ux/interaction.md +7 -0
  28. package/dist/rules/quartermaster/ux/motion.md +5 -0
  29. package/dist/rules/quartermaster/ux/navigation.md +7 -0
  30. package/dist/rules/quartermaster/ux/readability.md +6 -0
  31. package/dist/rules/quartermaster/ux/responsive.md +7 -0
  32. package/dist/src/ai/judge.d.ts +3 -1
  33. package/dist/src/ai/judge.js +13 -3
  34. package/dist/src/ai/navigator.js +3 -2
  35. package/dist/src/ai/pilot.js +10 -4
  36. package/dist/src/ai/planner.js +9 -1
  37. package/dist/src/ai/provider.d.ts +4 -0
  38. package/dist/src/ai/provider.js +27 -5
  39. package/dist/src/ai/quartermaster.d.ts +31 -3
  40. package/dist/src/ai/quartermaster.js +262 -17
  41. package/dist/src/ai/tester.js +6 -3
  42. package/dist/src/commands/init-command.js +4 -0
  43. package/dist/src/commands/options/auth-option.d.ts +6 -0
  44. package/dist/src/commands/options/auth-option.js +14 -0
  45. package/dist/src/commands/options/index.d.ts +2 -0
  46. package/dist/src/commands/options/index.js +2 -0
  47. package/dist/src/config.js +1 -0
  48. package/dist/src/explorbot.d.ts +1 -1
  49. package/dist/src/explorbot.js +8 -13
  50. package/dist/src/explorer.js +1 -0
  51. package/dist/src/global-config.js +6 -2
  52. package/dist/src/utils/ux-probe.d.ts +25 -0
  53. package/dist/src/utils/ux-probe.js +155 -0
  54. package/docs/reference/commands.md +12 -0
  55. package/docs/workflow/agentic-usage.md +7 -0
  56. package/docs/workflow/knowledge.md +6 -0
  57. package/package.json +1 -1
  58. package/rules/navigator/verification-actions.md +5 -3
  59. package/rules/planner/ignored-pages.md +6 -0
  60. package/rules/quartermaster/ux/accessibility.md +8 -0
  61. package/rules/quartermaster/ux/charts.md +6 -0
  62. package/rules/quartermaster/ux/copy.md +9 -0
  63. package/rules/quartermaster/ux/forms.md +7 -0
  64. package/rules/quartermaster/ux/interaction.md +7 -0
  65. package/rules/quartermaster/ux/motion.md +5 -0
  66. package/rules/quartermaster/ux/navigation.md +7 -0
  67. package/rules/quartermaster/ux/readability.md +6 -0
  68. package/rules/quartermaster/ux/responsive.md +7 -0
  69. package/src/ai/judge.ts +11 -3
  70. package/src/ai/navigator.ts +2 -2
  71. package/src/ai/pilot.ts +10 -4
  72. package/src/ai/planner.ts +10 -1
  73. package/src/ai/provider.ts +29 -6
  74. package/src/ai/quartermaster.ts +300 -18
  75. package/src/ai/tester.ts +5 -3
  76. package/src/commands/init-command.ts +5 -0
  77. package/src/commands/options/auth-option.ts +15 -0
  78. package/src/commands/options/index.ts +2 -0
  79. package/src/config.ts +1 -0
  80. package/src/explorbot.ts +9 -13
  81. package/src/explorer.ts +1 -0
  82. package/src/global-config.ts +5 -2
  83. package/src/utils/ux-probe.ts +163 -0
@@ -0,0 +1,163 @@
1
+ import { HTML_SELECTORS, HTML_VISIBILITY_LIMITS } from './html.ts';
2
+
3
+ export const UX_FOCUS_ATTR = 'data-explorbot-ux';
4
+
5
+ const UX_LIMITS = {
6
+ touchTarget: 44,
7
+ pointerTarget: 24,
8
+ touchWidth: 1024,
9
+ targetGap: 8,
10
+ minFontSize: 12,
11
+ minLineHeight: 1.3,
12
+ maxEntries: 8,
13
+ minOpacity: HTML_VISIBILITY_LIMITS.minOpacity,
14
+ };
15
+
16
+ export function uxProbeArgs(scope: string): UxProbeArgs {
17
+ return { scope, controlsSelector: HTML_SELECTORS.interactiveControl, limits: UX_LIMITS };
18
+ }
19
+
20
+ export function renderUxMeasurements(measurements: UxMeasurements): string {
21
+ const lines = [`viewport: ${measurements.width}px wide`];
22
+ for (const [check, entries] of Object.entries(measurements.checks)) {
23
+ if (!entries.length) continue;
24
+ lines.push(`${check}:`, ...entries.map((entry) => ` - ${entry}`));
25
+ }
26
+ if (lines.length === 1) lines.push('no measurable problems');
27
+ return lines.join('\n');
28
+ }
29
+
30
+ // Passed to page.evaluate as a function — must stay self-contained with no outer-scope references.
31
+ export function measureUx({ scope, controlsSelector, limits }: UxProbeArgs): UxMeasurements {
32
+ const root = document.querySelector(scope) || document.body;
33
+ const width = window.innerWidth;
34
+ const touch = width <= limits.touchWidth;
35
+ const visibleWidth = document.documentElement.clientWidth;
36
+ const checks: Record<string, string[]> = {
37
+ 'horizontal overflow': [],
38
+ 'zoom and viewport meta': [],
39
+ 'small targets': [],
40
+ 'crowded targets': [],
41
+ 'placeholder-only labels': [],
42
+ 'small text': [],
43
+ 'cramped line-height': [],
44
+ 'truncated text without full text': [],
45
+ 'layout-property transitions': [],
46
+ };
47
+
48
+ const add = (check: string, entry: string) => {
49
+ if (checks[check].length >= limits.maxEntries) return;
50
+ if (checks[check].includes(entry)) return;
51
+ checks[check].push(entry);
52
+ };
53
+
54
+ const describe = (el: Element) => {
55
+ const role = el.getAttribute('role') || el.tagName.toLowerCase();
56
+ const name = (el.getAttribute('aria-label') || (el as HTMLElement).innerText || el.getAttribute('placeholder') || el.getAttribute('title') || '').trim().replace(/\s+/g, ' ');
57
+ if (!name) return `${role} (unnamed)`;
58
+ return `${role} "${name.slice(0, 40)}"`;
59
+ };
60
+
61
+ const visible = (el: Element) => {
62
+ const rect = el.getBoundingClientRect();
63
+ if (!rect.width || !rect.height) return false;
64
+ const style = window.getComputedStyle(el);
65
+ return style.visibility !== 'hidden' && style.display !== 'none' && Number(style.opacity) > limits.minOpacity;
66
+ };
67
+
68
+ if (root === document.body || root.matches('main, [role="main"]')) {
69
+ const docWidth = document.documentElement.scrollWidth;
70
+ if (docWidth > visibleWidth + 1) add('horizontal overflow', `page is ${docWidth}px wide in a ${visibleWidth}px visible area`);
71
+
72
+ const meta = document.querySelector('meta[name="viewport"]')?.getAttribute('content');
73
+ if (!meta) add('zoom and viewport meta', 'no viewport meta tag');
74
+ const metaRules = (meta || '').toLowerCase().replace(/\s/g, '');
75
+ if (metaRules.includes('user-scalable=no') || metaRules.includes('user-scalable=0')) add('zoom and viewport meta', 'zoom disabled by user-scalable');
76
+ const maxScale = metaRules.split(',').find((part) => part.startsWith('maximum-scale='));
77
+ if (maxScale && Number(maxScale.split('=')[1]) <= 1) add('zoom and viewport meta', `zoom capped by ${maxScale}`);
78
+ }
79
+
80
+ const minTarget = touch ? limits.touchTarget : limits.pointerTarget;
81
+ const controls = Array.from(root.querySelectorAll(controlsSelector)).filter(visible);
82
+ const boxes = controls.map((el) => ({ el, rect: el.getBoundingClientRect() }));
83
+ for (const { el, rect } of boxes) {
84
+ if (el.tagName === 'A' && window.getComputedStyle(el).display === 'inline') continue;
85
+ if (rect.width >= minTarget && rect.height >= minTarget) continue;
86
+ add('small targets', `${describe(el)} is ${Math.round(rect.width)}×${Math.round(rect.height)}px, below ${minTarget}px`);
87
+ }
88
+
89
+ if (touch) {
90
+ for (let i = 0; i < boxes.length; i++) {
91
+ for (let j = i + 1; j < boxes.length; j++) {
92
+ const a = boxes[i];
93
+ const b = boxes[j];
94
+ if (a.el.contains(b.el) || b.el.contains(a.el)) continue;
95
+ const gapX = Math.max(b.rect.left - a.rect.right, a.rect.left - b.rect.right);
96
+ const gapY = Math.max(b.rect.top - a.rect.bottom, a.rect.top - b.rect.bottom);
97
+ const gap = Math.max(gapX, gapY);
98
+ if (gap >= limits.targetGap) continue;
99
+ if (a.rect.height >= limits.touchTarget && b.rect.height >= limits.touchTarget && a.rect.width >= limits.touchTarget && b.rect.width >= limits.touchTarget) continue;
100
+ add('crowded targets', `${describe(a.el)} and ${describe(b.el)} are ${Math.max(0, Math.round(gap))}px apart`);
101
+ }
102
+ }
103
+ }
104
+
105
+ for (const el of Array.from(root.querySelectorAll('input[placeholder], textarea[placeholder]'))) {
106
+ if (!visible(el)) continue;
107
+ if (el.getAttribute('aria-label') || el.getAttribute('aria-labelledby') || el.getAttribute('title')) continue;
108
+ if ((el as HTMLInputElement).labels?.length) continue;
109
+ add('placeholder-only labels', describe(el));
110
+ }
111
+
112
+ for (const el of Array.from(root.querySelectorAll('*'))) {
113
+ if (!visible(el)) continue;
114
+ const rect = el.getBoundingClientRect();
115
+ const style = window.getComputedStyle(el);
116
+
117
+ const overflows = rect.right > visibleWidth + 1 && rect.left < visibleWidth;
118
+ if (overflows && !(el.parentElement && el.parentElement.getBoundingClientRect().right > visibleWidth + 1)) add('horizontal overflow', `${describe(el)} ends at ${Math.round(rect.right)}px`);
119
+
120
+ const animated = style.transitionProperty.split(',').map((property) => property.trim());
121
+ const layout = animated.filter((property) => ['width', 'height', 'top', 'left', 'right', 'bottom', 'margin', 'padding'].includes(property));
122
+ const transitions = style.transitionDuration.split(',').some((duration) => Number.parseFloat(duration) > 0);
123
+ if (transitions && layout.length) add('layout-property transitions', `${describe(el)} transitions ${layout.join(', ')}`);
124
+
125
+ const ownText = Array.from(el.childNodes)
126
+ .filter((node) => node.nodeType === Node.TEXT_NODE)
127
+ .map((node) => node.textContent || '')
128
+ .join('')
129
+ .trim();
130
+ if (!ownText) continue;
131
+
132
+ const fontSize = Number.parseFloat(style.fontSize);
133
+ if (fontSize < limits.minFontSize) add('small text', `"${ownText.slice(0, 40)}" at ${fontSize}px`);
134
+
135
+ const lineHeight = Number.parseFloat(style.lineHeight);
136
+ const multiline = rect.height > fontSize * 2.5;
137
+ if (multiline && lineHeight && lineHeight / fontSize < limits.minLineHeight) add('cramped line-height', `"${ownText.slice(0, 40)}" at ${(lineHeight / fontSize).toFixed(2)}`);
138
+
139
+ const htmlEl = el as HTMLElement;
140
+ if (style.textOverflow === 'ellipsis' && htmlEl.scrollWidth > htmlEl.clientWidth && !el.getAttribute('title')) add('truncated text without full text', `"${ownText.slice(0, 40)}"`);
141
+ }
142
+
143
+ return { width, checks };
144
+ }
145
+
146
+ export function countRunningAnimations(scope: string): number {
147
+ const root = document.querySelector(scope) || document.body;
148
+ return document.getAnimations().filter((animation) => {
149
+ const target = (animation.effect as KeyframeEffect | null)?.target;
150
+ return animation.playState === 'running' && target && root.contains(target);
151
+ }).length;
152
+ }
153
+
154
+ interface UxProbeArgs {
155
+ scope: string;
156
+ controlsSelector: string;
157
+ limits: typeof UX_LIMITS;
158
+ }
159
+
160
+ export interface UxMeasurements {
161
+ width: number;
162
+ checks: Record<string, string[]>;
163
+ }