@stacknyu/pi-glitch-lab 0.0.0-stage → 0.5.1

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 (37) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -2
  4. package/examples/glitchy-app/README.md +14 -0
  5. package/examples/glitchy-app/THIRD_PARTY_NOTICES.md +27 -0
  6. package/examples/glitchy-app/app.jsx +62 -0
  7. package/examples/glitchy-app/public/api/credits.json +1 -0
  8. package/examples/glitchy-app/public/api/me.json +1 -0
  9. package/examples/glitchy-app/public/api/messages.json +1 -0
  10. package/examples/glitchy-app/public/app.js +14749 -0
  11. package/examples/glitchy-app/public/index.html +2 -0
  12. package/examples/glitchy-app/serve.mjs +12 -0
  13. package/extensions/index.ts +157 -0
  14. package/package.json +53 -3
  15. package/scripts/affordance_scan.mjs +235 -0
  16. package/scripts/doctor.mjs +14 -0
  17. package/scripts/glitch/analyze.mjs +274 -0
  18. package/scripts/glitch_lab.mjs +519 -0
  19. package/scripts/interactions.mjs +172 -0
  20. package/scripts/journey_runner.mjs +260 -0
  21. package/scripts/lib.mjs +260 -0
  22. package/scripts/report.mjs +144 -0
  23. package/scripts/sensors/dom_observer.mjs +96 -0
  24. package/scripts/sensors/react_sensor.mjs +167 -0
  25. package/skills/ai-agentic-usability-testing/SKILL.md +68 -0
  26. package/skills/ai-agentic-usability-testing/assets/default-personas.md +96 -0
  27. package/skills/ai-agentic-usability-testing/assets/findings.example.json +56 -0
  28. package/skills/ai-agentic-usability-testing/assets/matrix.default.json +746 -0
  29. package/skills/ai-agentic-usability-testing/assets/matrix.partners.example.json +73 -0
  30. package/skills/ai-agentic-usability-testing/assets/matrix.web.example.json +32 -0
  31. package/skills/ai-agentic-usability-testing/assets/personas-template.md +45 -0
  32. package/skills/ai-agentic-usability-testing/references/latency-reliability.md +41 -0
  33. package/skills/ai-agentic-usability-testing/references/regression-pipeline.md +41 -0
  34. package/skills/ai-agentic-usability-testing/references/tenant-persona-matrix.md +61 -0
  35. package/skills/ai-agentic-usability-testing/references/visual-affordance-guide.md +97 -0
  36. package/skills/ai-agentic-usability-testing/references/visual-glitches.md +83 -0
  37. package/skills/glitch-hunt/SKILL.md +59 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * React render sensor — runs inside the page, installed before any app script.
3
+ *
4
+ * It registers itself as (or wraps) window.__REACT_DEVTOOLS_GLOBAL_HOOK__, the same channel React DevTools
5
+ * uses, and on every commit records which components MOUNTED, RE-RENDERED or were REMOVED, where they sit on
6
+ * screen, and which props changed. From that it derives REMOUNTS: a component that is removed and a new
7
+ * instance of the same component appears in the same place (e.g. a sidebar rebuilt on every navigation,
8
+ * a chat box whose component type changes on every render).
9
+ *
10
+ * Works on production builds too, but component names are minified there (e.g. "a", "Xe"); a staging
11
+ * build with readable names gives much better reports.
12
+ *
13
+ * Data: window.__glReact = { available, commits, events: [...], counts }
14
+ * event: { type: "mount"|"render"|"unmount"|"remount", t, name, rect, changed?, gapMs? }
15
+ */
16
+ export function REACT_SENSOR() {
17
+ if (window.__glReact) return;
18
+ const S = (window.__glReact = { available: false, renderers: 0, commits: 0, events: [], dropped: 0, lastRect: new WeakMap() });
19
+ const MAX_EVENTS = 20000, MAX_VISIT = 5000, MAX_RECTS = 120;
20
+ const COMPOSITE = new Set([0, 1, 2, 11, 14, 15]); // Function, Class, Indeterminate, ForwardRef, Memo, SimpleMemo
21
+ const HOST = new Set([5, 26, 27]); // HostComponent, HostHoistable, HostSingleton
22
+ const push = (e) => { if (S.events.length < MAX_EVENTS) S.events.push(e); else S.dropped++; };
23
+
24
+ const nameOf = (type) => {
25
+ if (!type || typeof type === "string") return null;
26
+ if (typeof type === "function") return type.displayName || type.name || null;
27
+ if (type.displayName) return type.displayName;
28
+ if (type.render) return type.render.displayName || type.render.name || null;
29
+ if (type.type) return nameOf(type.type);
30
+ return null;
31
+ };
32
+ let lastHops = 0;
33
+ const hostOf = (fiber) => {
34
+ let f = fiber, depth = 0;
35
+ while (f && depth++ < 60) {
36
+ if (HOST.has(f.tag) && f.stateNode && f.stateNode.getBoundingClientRect) { lastHops = depth; return f.stateNode; }
37
+ f = f.child;
38
+ }
39
+ return null;
40
+ };
41
+ let rectBudget = 0;
42
+ const rectOf = (fiber) => {
43
+ if (rectBudget-- <= 0) return null;
44
+ const el = hostOf(fiber);
45
+ if (!el || !el.isConnected) return null;
46
+ const r = el.getBoundingClientRect();
47
+ if (!r.width || !r.height) return null;
48
+ // d = hops from the component to its first DOM element: wrappers that share the same element have a larger d
49
+ return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height), d: lastHops };
50
+ };
51
+ const changedProps = (prev, next) => {
52
+ if (!prev || !next || prev === next) return [];
53
+ const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
54
+ const out = [];
55
+ for (const k of keys) if (k !== "children" && prev[k] !== next[k]) out.push(k);
56
+ if (prev.children !== next.children && out.length === 0) out.push("children");
57
+ return out.slice(0, 6);
58
+ };
59
+ // did any hook VALUE change? (hook objects are new on every render, so compare their values;
60
+ // effect hooks hold new effect objects each render and are ignored)
61
+ const isEffect = (v) => v && typeof v === "object" && "create" in v && "deps" in v;
62
+ const stateChanged = (fiber) => {
63
+ const a = fiber.alternate.memoizedState, b = fiber.memoizedState;
64
+ if (fiber.tag === 1) return a !== b; // class component: new state object on setState
65
+ let x = a, y = b, n = 0;
66
+ while (x && y && n++ < 80) {
67
+ if (typeof x !== "object" || typeof y !== "object" || !("next" in x)) return x !== y;
68
+ if (x.memoizedState !== y.memoizedState && !(isEffect(x.memoizedState) && isEffect(y.memoizedState))) return true;
69
+ x = x.next; y = y.next;
70
+ }
71
+ return false;
72
+ };
73
+ const collectDeleted = (fiber, into, now) => {
74
+ // walk a deleted subtree and record composite components that went away
75
+ const stack = [fiber]; let n = 0;
76
+ while (stack.length && n++ < 800) {
77
+ const f = stack.pop(); if (!f) continue;
78
+ if (COMPOSITE.has(f.tag) && !reported.has(f) && !(f.alternate && reported.has(f.alternate))) {
79
+ reported.add(f);
80
+ const name = nameOf(f.type);
81
+ if (name) into.push({ name, rect: S.lastRect.get(f) || (f.alternate && S.lastRect.get(f.alternate)) || null, t: now });
82
+ }
83
+ if (f.child) stack.push(f.child);
84
+ if (f.sibling && f !== fiber) stack.push(f.sibling);
85
+ }
86
+ };
87
+
88
+ const recent = []; // recently removed composites: {name, rect, t}
89
+ const reported = new WeakSet(); // fibers already reported as removed (React may report via two channels)
90
+ const overlap = (a, b) => a && b && a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
91
+
92
+ function onCommit(root) {
93
+ const now = Math.round(performance.now());
94
+ S.commits++;
95
+ rectBudget = MAX_RECTS;
96
+ const current = root && root.current;
97
+ if (!current) return;
98
+ const removed = [];
99
+ const mounts = [];
100
+ let visited = 0;
101
+ const walk = (fiber, mounting) => {
102
+ const stack = [[fiber, mounting]];
103
+ while (stack.length) {
104
+ const [f, isMount] = stack.pop();
105
+ if (!f || visited++ > MAX_VISIT) continue;
106
+ if (f.deletions && f.deletions.length) for (const d of f.deletions) collectDeleted(d, removed, now);
107
+ const composite = COMPOSITE.has(f.tag);
108
+ const name = composite ? nameOf(f.type) : null;
109
+ let descend = true;
110
+ if (isMount || !f.alternate) {
111
+ if (name) { const rect = rectOf(f); if (rect) S.lastRect.set(f, rect); mounts.push({ name, rect }); push({ type: "mount", t: now, name, rect }); }
112
+ if (f.child) stack.push([f.child, true]);
113
+ } else {
114
+ if (name && ((f.flags ?? f.effectTag ?? 0) & 1) === 1) { // PerformedWork => this component rendered in this commit
115
+ const rect = rectOf(f); if (rect) S.lastRect.set(f, rect);
116
+ const ch = changedProps(f.alternate.memoizedProps, f.memoizedProps);
117
+ const why = ch.length ? ch : stateChanged(f) ? ["(own state)"] : ["(parent re-rendered)"];
118
+ push({ type: "render", t: now, name, rect, changed: why });
119
+ }
120
+ // only descend where React actually re-processed children
121
+ descend = f.child !== f.alternate.child || !!(f.deletions && f.deletions.length);
122
+ // (flags on skipped subtrees are stale, so never descend into them)
123
+ if (f.child && descend) stack.push([f.child, false]);
124
+ }
125
+ if (f.sibling && f !== fiber) stack.push([f.sibling, isMount]);
126
+ }
127
+ };
128
+ try { walk(current, false); } catch (e) { S.error = String(e && e.message); }
129
+ for (const r of removed) { push({ type: "unmount", t: now, name: r.name, rect: r.rect }); recent.push(r); }
130
+ // remount = same component name removed and mounted again in (about) the same place within 2s
131
+ while (recent.length && now - recent[0].t > 2000) recent.shift();
132
+ for (const m of mounts) {
133
+ const idx = recent.findIndex((r) => r.name === m.name && (!r.rect || !m.rect || overlap(r.rect, m.rect)));
134
+ if (idx >= 0) { const r = recent.splice(idx, 1)[0]; push({ type: "remount", t: now, name: m.name, rect: m.rect || r.rect, gapMs: now - r.t }); }
135
+ }
136
+ }
137
+ function onUnmount(fiber) {
138
+ if (!COMPOSITE.has(fiber.tag) || reported.has(fiber) || (fiber.alternate && reported.has(fiber.alternate))) return;
139
+ reported.add(fiber);
140
+ const name = nameOf(fiber.type); if (!name) return;
141
+ const now = Math.round(performance.now());
142
+ const rect = S.lastRect.get(fiber) || (fiber.alternate && S.lastRect.get(fiber.alternate)) || null;
143
+ recent.push({ name, rect, t: now }); push({ type: "unmount", t: now, name, rect });
144
+ }
145
+
146
+ const existing = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
147
+ if (existing && typeof existing === "object") {
148
+ const oc = existing.onCommitFiberRoot, ou = existing.onCommitFiberUnmount;
149
+ existing.onCommitFiberRoot = function (id, root, ...rest) { try { S.available = true; onCommit(root); } catch {} return oc && oc.call(this, id, root, ...rest); };
150
+ existing.onCommitFiberUnmount = function (id, fiber, ...rest) { try { onUnmount(fiber); } catch {} return ou && ou.call(this, id, fiber, ...rest); };
151
+ return;
152
+ }
153
+ let nextId = 0;
154
+ const hook = {
155
+ renderers: new Map(),
156
+ supportsFiber: true,
157
+ isDisabled: false,
158
+ inject(renderer) { const id = ++nextId; this.renderers.set(id, renderer); S.renderers++; S.available = true; return id; },
159
+ onCommitFiberRoot(_id, root) { try { onCommit(root); } catch (e) { S.error = String(e && e.message); } },
160
+ onCommitFiberUnmount(_id, fiber) { try { onUnmount(fiber); } catch {} },
161
+ onPostCommitFiberRoot() {},
162
+ onScheduleFiberRoot() {},
163
+ setStrictMode() {},
164
+ checkDCE() {},
165
+ };
166
+ Object.defineProperty(window, "__REACT_DEVTOOLS_GLOBAL_HOOK__", { value: hook, configurable: true, writable: true });
167
+ }
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: ai-agentic-usability-testing
3
+ description: "Browser UI/UX and deterministic UAT testing with Glitch Lab. Inventory rendered controls, explore authorized menu/tab/dialog/navigation states, test scripted acceptance criteria and report evidence/coverage. For dashboards and web apps. No AI calls in the execution scripts. Not exhaustive accessibility or native-device certification."
4
+ ---
5
+
6
+ # UI/UX and UAT with Glitch Lab
7
+
8
+ ## Authorization first
9
+
10
+ Use approved disposable environments and test accounts. Loading a page can itself change data. Never claim this is a read-only sandbox or that reloading rolls back server state.
11
+
12
+ - Start `ux_affordance_scan` without `probeClicks`: observation only.
13
+ - Before any click exploration or navigation glitch pass, obtain explicit user permission and set `interact=true`.
14
+ - Login, typing, selects/checkboxes, submit/Enter and writes require separate permission and `allowWrites=true` as well.
15
+ - Never infer authorization from tool availability or from a request for a report. In headless mode the caller supplies permission flags only after operator approval.
16
+ - Run `glitch_lab_doctor` to check the browser. Install Chromium only when the user authorizes the download.
17
+
18
+ ## Configure
19
+
20
+ Copy `assets/matrix.web.example.json` into `.ux/matrix.json`. Replace URL, target selectors and expected outcomes with the actual approved contract. Ask for missing business criteria; do not invent them.
21
+
22
+ Use exact role/name or label locators; explicit selectors can identify controls with duplicate names. Missing/ambiguous targets fail. For login, use environment-variable credentials plus `login.authenticated` (an authenticated-only visible selector) and/or an exact non-root `login.waitFor` pathname. No login state is saved to disk. Keep `.ux/` ignored/private, configure masks, and review evidence for sensitive data before sharing.
23
+
24
+ Optional education/finance personas in `assets/default-personas.md` and `matrix.default.json` are examples for those domains, not defaults to impose on unrelated apps. Their placeholders require calibration and updated positive authentication checks.
25
+
26
+ ## Pass 1 — inventory
27
+
28
+ Call `ux_affordance_scan` with URL or config/cell. Read annotated screenshots and saved items:
29
+ - labels, focusability, disabled/expanded/selected/checked state;
30
+ - small, unlabeled or covered targets and horizontal overflow;
31
+ - fake/hidden-button visual candidates, not proven failures;
32
+ - scroll bounds and inaccessible/unexplored content.
33
+
34
+ ## Pass 2 — bounded interaction coverage
35
+
36
+ After approval, call `ux_affordance_scan` with `probeClicks=true, interact=true`. Set page/state/action/depth/time bounds appropriate to the approved area.
37
+
38
+ The explorer resets/replays parent states, discovers revealed menus/tabs/dialogs and records before/after evidence. It skips automatic input values and obvious submit/destructive controls. HTTP write blocking without allowWrites is defense in depth, not proof of no side effects. Repeated interactions can repeat writes if authorized.
39
+
40
+ Distinguish responded, no-observable-response, error, blocked and skipped. A network request is not acceptance. No visible response may be valid for clipboard/hover controls or may need a longer observation window. Report limit hits and unvisited states; never claim every control works.
41
+
42
+ ## Pass 3 — scripted UAT
43
+
44
+ Run `ux_journeys` for explicit business acceptance:
45
+ 1. Assert the expected loaded screen or authenticated user.
46
+ 2. Execute approved actions.
47
+ 3. Assert text/URL/visibility/value/checked/enabled/count/attribute outcomes.
48
+ 4. For an authorized persistence test, reload and assert the saved value.
49
+
50
+ Use a keyboard `press` step for Tab/arrow/Enter tests. Fill/select/check/save require writes permission. Optional steps must not hide a required outcome. Negative persona/content checks belong after a positive loaded-state assertion; a login page is not proof that tenant gating works.
51
+
52
+ Required failures, missing/empty journeys and missing baselines fail. Do not convert a tool error into a pass. A new baseline needs a new directory; never overwrite historical evidence. `measureResponse` must observe output and stable completion before its timeout.
53
+
54
+ ## Glitch pass
55
+
56
+ Use `glitch_hunt` for flicker/remount/layout-shift concerns; read the glitch-hunt skill. Prefer production-like builds and repeated evidence. React sensor availability, sampling and minification limit attribution.
57
+
58
+ ## Report
59
+
60
+ Use `ux_report` with run directory and optional written findings JSON. Include:
61
+ - approved target, persona/device, scope and actions;
62
+ - discovered/exercised control-state pairs, visited states, bounds and skipped/blocked reasons;
63
+ - UAT criteria passed/failed and baseline provenance;
64
+ - evidence paths and observed timing;
65
+ - UX findings in plain language with recommendations;
66
+ - gaps: iframe/shadow/virtualized/native/keyboard-only or other untested states.
67
+
68
+ Keep execution evidence separate from interpretation. Simulated personas are not real-user usability research. No application code changes or extra journeys without authorization.
@@ -0,0 +1,96 @@
1
+ # Default personas
2
+
3
+ These are the built-in personas. Use them unless the user provides others. Each one is tested as a **direct user of the platform (no partner)** first; partner cells come later.
4
+
5
+ When simulating a persona, stay in character: use their vocabulary, their device, their patience level, and their worries. Their "questions to ask the AI" are the exact words to type into chat.
6
+
7
+ ---
8
+
9
+ ## 1. `indian-germany-student` — Arjun, 22, wants a Master's in Germany
10
+
11
+ - **Situation:** Final-year B.Tech (Computer Science) student at a private engineering college in Pune. CGPA 7.8/10. Wants an MS in Computer Science or Data Science at a public German university (TU Munich, RWTH Aachen, TU Berlin) for a winter intake. Middle-class family; cost matters a lot.
12
+ - **Device:** Mid-range Android phone (360×800), Chrome; sometimes a college lab PC. Mobile data or hostel Wi-Fi (fast 3G-ish, drops often).
13
+ - **Language:** Fluent but Indian English; may type in Hinglish ("how much blocked account amount needed bro"). Reads quickly, skims long paragraphs, screenshots things to send to family.
14
+ - **Tech comfort:** High for apps (Instagram, UPI, WhatsApp), low patience for web forms.
15
+ - **What he needs to figure out:** APS certificate, IELTS/TOEFL score (and whether German A1/B1 helps), GRE for some programs, uni-assist or direct application, SOP and LORs, blocked account (Sperrkonto) and health insurance, deadlines, DAAD scholarships, student visa, part-time work rules, total cost in ₹.
16
+ - **Top tasks (his words):**
17
+ 1. "What all documents and exams do I need for MS in Germany?" → wants a checklist he can save.
18
+ 2. "Is my 7.8 CGPA enough for TU Munich? Which universities should I target?"
19
+ 3. "How much money do I need in total, in rupees?" → wants a clear breakdown, maybe a widget/table.
20
+ 4. Save everything as notes and come back tomorrow to continue.
21
+ - **Success looks like:** A saved checklist with deadlines, and knowing exactly what to do this month.
22
+ - **Frustrations:** Long generic answers; not knowing if something saved; buttons he can't identify on a small screen; losing a long chat when the network drops.
23
+ - **Must NOT see:** tax, stock market, trading, portfolio, or finance-analyst agents/widgets; US-only admissions content presented as if it applied to him.
24
+ - **Formats he expects:** ₹ with Indian grouping (₹12,50,000 or "12.5 lakh"), € conversions, dates as DD/MM/YYYY or "15 July".
25
+
26
+ ## 2. `us-ivy-applicant` — Emily, 17, aiming for Harvard or MIT
27
+
28
+ - **Situation:** High-school junior in Austin, Texas. Strong in math and robotics club; unsure how to stand out. Parents are involved but busy. Applying next fall (Early Action / Restrictive Early Action is on her mind).
29
+ - **Device:** iPhone (390×844) most of the time; school Chromebook (1366×768) for longer work.
30
+ - **Language:** US English, casual, types fast, uses abbreviations ("EA vs REA?", "do APs matter"). Short attention span for walls of text.
31
+ - **What she needs to figure out:** SAT/ACT (test-required again at both), AP/course rigor, extracurriculars and awards, Common App essay and supplements, recommendation letters, interviews, timeline month by month, MIT vs Harvard differences, financial aid basics.
32
+ - **Top tasks (her words):**
33
+ 1. "What do I need to get into MIT? Like what should I be doing junior year?"
34
+ 2. "Make me a timeline until application deadlines" → wants a plan she can save and check off.
35
+ 3. "Help me brainstorm my Common App essay" → iterative chat, saves drafts as notes.
36
+ 4. "Compare Harvard and MIT for someone into robotics."
37
+ - **Success looks like:** A month-by-month plan and an essay idea she's excited about, saved where she can find them.
38
+ - **Frustrations:** Anything that looks "old" or corporate; unclear icons; not knowing which agent to use; slow AI responses on her phone.
39
+ - **Must NOT see:** Indian/German admissions specifics as defaults (APS, uni-assist, blocked account), tax or stock content.
40
+ - **Formats she expects:** US dates (MM/DD), US grading (GPA /4.0, SAT /1600).
41
+
42
+ ## 3. `us-finance-analyst` — Mark, 40, financial analyst checking out the platform
43
+
44
+ - **Situation:** Corporate financial analyst living in suburban Columbus, Ohio. Married, two kids, W-2 income plus some freelance consulting (1099) and a rental property. Received an IRS notice last year and wants to be ready for a possible audit. Evaluating whether this platform is worth using.
45
+ - **Device:** Desktop, 1440×900 (sometimes a 1920 monitor), Chrome, fast network.
46
+ - **Behavior:** **Skims.** Clicks around quickly to "see what it can do" before committing: opens menus, hovers icons, opens widgets, tries a couple of chats. Judges the product in the first few minutes. Expects keyboard shortcuts, exports, and precise numbers.
47
+ - **Language:** Professional US English, uses tax jargon correctly (Schedule C, Schedule E, 1099-NEC, quarterly estimates, CP2000, substantiation).
48
+ - **Top tasks (his words):**
49
+ 1. Explore the dashboard and features: "What does this thing actually do?"
50
+ 2. "I got a CP2000 notice last year. What documentation should I prepare in case of an audit?"
51
+ 3. "Help me plan quarterly estimated taxes for my consulting income this year." → wants a table/widget he can export.
52
+ 4. Save a checklist and a plan as notes; export or copy them.
53
+ - **Success looks like:** Understanding the product's value in under 5 minutes; a precise, exportable plan.
54
+ - **Frustrations:** Fluff, unclear feature names, slow widgets, numbers without units or dates, uncertainty about data privacy when uploading tax documents, no clear "not tax advice / consult a CPA" framing where appropriate.
55
+ - **Must NOT see:** student/admissions agents or widgets, Indian market content, INR formatting.
56
+ - **Formats he expects:** $ with US grouping ($12,500.00), US dates, US tax-year terminology.
57
+
58
+ ## 4. `indian-market-analyst` — Indian stock market analyst, age 30–65
59
+
60
+ One persona with two variants at the ends of the age range. Test both; many findings differ by age.
61
+
62
+ ### 4a. `indian-market-analyst-young` — Rahul, 32
63
+ - **Situation:** Works at a Mumbai brokerage research desk; also trades personally (equity and some F&O). Checks markets constantly.
64
+ - **Device:** Android phone (360×800) during market hours, fast 3G/4G in transit.
65
+ - **Behavior:** Fast, impatient, glances at numbers, wants market overview immediately, jumps between chat and data.
66
+ - **Top tasks (his words):**
67
+ 1. "NIFTY and Bank NIFTY outlook for the coming week?"
68
+ 2. "Should I book profits in IT stocks or hold? What are the key levels?"
69
+ 3. Check market widgets (indices, sectors, watchlist) and see how fresh the data is.
70
+ 4. Save a research note on a sector.
71
+
72
+ ### 4b. `indian-market-analyst-senior` — Suresh, 61
73
+ - **Situation:** Semi-retired equity analyst in Ahmedabad; manages family portfolio, long-term investor (large-caps, mutual funds, dividends). Careful, reads everything.
74
+ - **Device:** Laptop 1366×768 at **125% browser zoom** (reading glasses); sometimes a tablet. Moderate network.
75
+ - **Behavior:** Slow and deliberate; reads labels fully; won't click something he doesn't understand; worried about pressing the wrong thing; distrusts anything that looks like a "tip".
76
+ - **Top tasks (his words):**
77
+ 1. "Give me an overview of the market today and what to watch this month."
78
+ 2. "Is it a good time to add HDFC Bank and ITC for the long term?"
79
+ 3. Read a research summary with sources and save it.
80
+ 4. Find where previous notes and chats are.
81
+
82
+ ### Shared for 4a/4b
83
+ - **Language:** Indian English; comfortable with NSE/BSE, NIFTY 50, SENSEX, F&O, SEBI, FII/DII, results season.
84
+ - **Success looks like:** Clear, sourced views with the data's timestamp; clear framing that it isn't SEBI-registered investment advice; notes saved and easy to find.
85
+ - **Frustrations:** Stale data without a timestamp, numbers without units, US-centric defaults, confident "buy/sell" tips without caveats, small text (4b), slow widgets (4a).
86
+ - **Must NOT see:** student/admissions content, US tax content, $ or US formatting on Indian market data.
87
+ - **Formats expected:** ₹ with lakh/crore (₹1,25,000; ₹3.2 crore), IST times, market-hours awareness (09:15–15:30 IST).
88
+
89
+ ---
90
+
91
+ ## Cross-persona checks to always include
92
+
93
+ - Each persona sees only their vertical's agents, dashboard, widgets and suggestions (persona tagging works). A leak is S1.
94
+ - Number, currency, date and time formats match the persona.
95
+ - AI answers in sensitive areas (finance, tax, market advice) show appropriate, visible, non-intrusive disclaimers.
96
+ - A first-time user of each persona can find: chat, notes, research, and their saved work, without help.
@@ -0,0 +1,56 @@
1
+ {
2
+ "title": "Usability, glitch & reliability audit — direct users, 5 personas",
3
+ "subtitle": "Staging build · Arjun, Emily, Mark, Rahul, Suresh · no partner",
4
+ "summary": "Most confusion comes from three patterns: status chips that look like buttons, icon-only chat actions with no labels, and AI replies that show nothing for 2–4 seconds. On the health partner, a Tax agent from the finance vertical is visible to patients.",
5
+ "scorecard": [
6
+ {
7
+ "cell": "indian-germany-student@direct",
8
+ "journey": "Get the Germany MS checklist and save it",
9
+ "result": "friction",
10
+ "issues": [
11
+ "UX-002",
12
+ "UX-014"
13
+ ]
14
+ },
15
+ {
16
+ "cell": "us-finance-analyst@direct",
17
+ "journey": "First look: explore features",
18
+ "result": "friction",
19
+ "issues": [
20
+ "UX-005"
21
+ ]
22
+ }
23
+ ],
24
+ "patterns": [
25
+ {
26
+ "title": "Status chips look like buttons",
27
+ "findings": [
28
+ "UX-002",
29
+ "UX-007",
30
+ "UX-011"
31
+ ],
32
+ "description": "Filled, rounded chips (Scholarships, Pending, Verified) have the same look as primary buttons. Teens on mobile tap them repeatedly.",
33
+ "recommendation": "Give non-interactive chips a flat, outline-free, low-saturation style and never the primary brand color. Reserve filled brand color for actions only, in the design system."
34
+ }
35
+ ],
36
+ "findings": [
37
+ {
38
+ "id": "UX-014",
39
+ "title": "Sidebar blinks every time a page opens",
40
+ "category": "Visual glitch",
41
+ "severity": "S2",
42
+ "cells": [
43
+ "indian-germany-student@direct",
44
+ "indian-market-analyst-senior@direct"
45
+ ],
46
+ "screen": "/notes, /research, /dashboard",
47
+ "personaVoice": "Every time I open something the menu on the left disappears for a moment. Is it reloading? Did I lose something?",
48
+ "observation": "The left sidebar disappears for ~120 ms and reappears on every navigation (remount).",
49
+ "evidence": "glitch images at ~1.2 s after load on 3 screens; DOM remount of nav#sidebar",
50
+ "recommendation": "Keep the sidebar persistent across page changes; only the main content area should update.",
51
+ "screenshots": [
52
+ "indian-germany-student__direct/glitches/load_notes_glitch_01.png"
53
+ ]
54
+ }
55
+ ]
56
+ }