diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index af8ce85..4f0d2f2 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -29,6 +29,7 @@ ## Feedback - [Bot alerts need a ticket link](feedback_bot_alert_ticket_link.md) — Syncro ticket bot-alerts MUST include a clickable link: https://computerguru.syncromsp.com/tickets/ (internal id, not ticket number). post-bot-alert.sh posts raw text; put the URL in the message. +- [Verify committed state before push](feedback_verify_committed_state_before_push.md) — webhook builds from origin/main: verify the COMMITTED build (git stash + build), not the working tree; bad git-add pathspec silently aborts staging. Stage by directory. - [Scheduling = coord todo, not schedulers](feedback_scheduling_via_coord_todo.md) — Defer future work as a coord todo (POST /api/coord/todos; needs text + created_by_user + created_by_machine) for a later session to pick up. NOT /schedule remote CCR agents (no vault/creds there) or local scheduled tasks. - [Attribution is read, never inferred](feedback_attribution_from_identity.md) — Who-did-what (user+machine) comes ONLY from identity.json + users.json + git authorship. Never infer from hostname patterns, the userEmail hint, or memory. The "5070" box is Mike's. sync.sh reconciles git config to identity.json; /save renders the User block via whoami-block.sh. - [D2TESTNAS SSH Access](feedback_d2testnas_ssh.md) — Use root@192.168.0.9 with Paper123!@#, not sysadmin. @@ -59,6 +60,7 @@ - [Python on Windows — use py launcher](feedback_python_windows.md) — Windows Store python/python3 aliases disabled; always use py or jq on DESKTOP-0O8A1RL. - [Memory tooling may delete now — additive-only constraint dropped](feedback_memory_sync_destructive_ok.md) — As of 2026-06-02, memory-dream and sync-memory.sh are sanctioned to perform destructive ops (apply proposed merges/dedups, propagate repo deletions back to harness profile stores). Onboarding-phase safety net now fights deliberate consolidation (e.g. 2026-06-01's 39 deletions resurrected on the next sync). Script updates pending. - [Unsaved sessions are recoverable from transcripts](feedback_session_recovery.md) — Crashed/closed-before-save sessions live in `~/.claude/projects//*.jsonl`; the detector auto-recovers orphans, `/recover ` does it manually. Ollama prose + Python verbatim. See `.claude/RECOVERY.md`. +- [agy review is not read-only](feedback_agy_review_not_readonly.md) — agy review/review-files CAN write files + run npm despite docs claiming plan-mode; always git diff after and treat Gemini's output as a proposal to validate, not trusted/finished work. ### Syncro - [Syncro API plumbing](feedback_syncro_api.md) — Content-Type required on all POST/PUT; NO idempotency anywhere — always GET before retrying; response wrappers (`.ticket.id`, `.comment.id`); add_line_item shape (internal ID, flat response, required fields); HTML uses `
` not `
    /
  • `; timer_entry response is FLAT but SUPERSEDED (use add_line_item). diff --git a/.claude/memory/feedback_agy_review_not_readonly.md b/.claude/memory/feedback_agy_review_not_readonly.md new file mode 100644 index 0000000..28e02f5 --- /dev/null +++ b/.claude/memory/feedback_agy_review_not_readonly.md @@ -0,0 +1,12 @@ +--- +name: feedback-agy-review-not-readonly +description: agy review/review-files can actually WRITE files + run npm, despite docs claiming read-only plan mode — review Gemini's diffs, don't trust its summary. +metadata: + type: feedback +--- + +The `agy` SKILL.md documents `review` / `review-files` as read-only (`--approval-mode plan`: "Gemini can read files but cannot modify anything"). Observed 2026-06-05 on GURU-5070: a `review-files` call asking Gemini to "improve" the human-flow skill resulted in Gemini **actually editing 6 repo files, adding babel deps to package.json, and running npm install** (created package-lock.json + node_modules). So plan-mode was NOT enforced for that run. + +**Why:** The documented safety contract (read-only review) cannot be relied on. Gemini also over-claims — its final summary said it "delivered/upgraded" the skill as if complete, but the only way to know what truly happened was to `git diff` and run the code. + +**How to apply:** After ANY `agy review*` call, `git status` / `git diff` the target tree to see what actually changed — never trust the summary. If you need a guaranteed read-only second opinion, copy targets to a scratch dir first, or verify the wrapper's approval-mode. The improvements may be good, but they are a PROPOSAL to review and validate (run it, check repo rules like NO EMOJIS), not trusted output. Related: [[reference_gitea_internal]] is unrelated; see agy SKILL.md path gotcha. diff --git a/.claude/skills/human-flow/SKILL.md b/.claude/skills/human-flow/SKILL.md index 5241439..bb4c6c6 100644 --- a/.claude/skills/human-flow/SKILL.md +++ b/.claude/skills/human-flow/SKILL.md @@ -38,12 +38,23 @@ Run via natural language ("human-flow scan the sessions table", "run human-flow | Command | Description | |---------------------|-------------| -| `scan [target]` | Quick static + heuristic scan of files or directories for mouse/keyboard friction. Produces a prioritized report. | -| `audit [target]` | Deeper pass: combines code analysis, component review, and workflow walkthroughs. Scores intuitiveness and suggests specific refactors. | -| `fancy [target]` | **"Fancy as fuck" mode** — a second, beauty- and elegance-focused pass. Evaluates opportunities for tasteful delight (transitions, micro-interactions, hover states, view transitions, loading experiences, etc.), determines appropriateness, and suggests refinements/polish. | -| `report [target]` | Generate a clean, user-facing markdown report suitable for sharing with designers/devs. | +| `scan [target]` | AST-powered scan of files/directories for workflow friction. Produces a 0-10 Friction Index report. | +| `audit [target]` | Deeper pass: combines AST analysis, component review, and state-flow audit. | +| `fix [target]` | **DISABLED (advisory only for now).** Auto-apply is off — the AST code generator reprints whole files and produces noisy diffs. Use the scan/report output and have an agent apply the fixes surgically. Will be revisited with a surgical (string-splice) editor. | +| `fancy [target]` | **"Fancy as fuck" mode** — elegance pass with a calibrated Restraint-o-Meter. | +| `report [target]` | Generate a formatted markdown report with the Friction Index rubric. | -If no command, defaults to `scan` on the provided target (or current frontend dir). +If no command, defaults to `scan` on the provided target. + +## Friction Index (0-10) + +The scan produces an objective score based on weighted deductions: +- **Motor (3.0)**: Target size, precision, Fitts's Law. +- **Cognitive (2.5)**: Discoverability, affordance, consistency. +- **Keyboard (2.5)**: Accessibility, focus flow, parity. +- **Feedback (2.0)**: Visual response, state transitions. + +Score = 10 - Σ(IssueSeverity * DimensionWeight) You can combine: e.g. run `scan` first for friction, then `fancy` for delight opportunities. diff --git a/.claude/skills/human-flow/package-lock.json b/.claude/skills/human-flow/package-lock.json new file mode 100644 index 0000000..15117eb --- /dev/null +++ b/.claude/skills/human-flow/package-lock.json @@ -0,0 +1,217 @@ +{ + "name": "human-flow", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "human-flow", + "version": "0.1.0", + "dependencies": { + "@babel/generator": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + } + } +} diff --git a/.claude/skills/human-flow/package.json b/.claude/skills/human-flow/package.json index 6392946..ad1d823 100644 --- a/.claude/skills/human-flow/package.json +++ b/.claude/skills/human-flow/package.json @@ -6,5 +6,11 @@ "scripts": { "scan": "node scripts/scan.mjs", "fancy": "node scripts/scan.mjs --fancy" + }, + "dependencies": { + "@babel/generator": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" } -} \ No newline at end of file +} diff --git a/.claude/skills/human-flow/references/fancy-as-fuck.md b/.claude/skills/human-flow/references/fancy-as-fuck.md index 34780d9..47a0846 100644 --- a/.claude/skills/human-flow/references/fancy-as-fuck.md +++ b/.claude/skills/human-flow/references/fancy-as-fuck.md @@ -34,9 +34,24 @@ Before suggesting any fancy element, answer these questions honestly: - Dense internal tools / operator consoles: Favor *restraint and precision*. Think "expensive mechanical instrument" — satisfying, confident, never showy. Over-the-top sparkle or bouncy motion will feel wrong and unprofessional here. - Onboarding, public-facing, marketing, or higher-emotion flows: More permission for expressive, delightful "useful decoration" that makes the experience feel alive and premium while still serving clear user goals. +## The Restraint-o-Meter + +Calibrate your "Fancy" recommendations using this scale: + +| Level | Profile | Examples | +| :--- | :--- | :--- | +| **1** | **Clinical** | Zero motion. Immediate cuts. High density. (Log viewers, raw data dumps). | +| **2** | **Functional** | Subtle hover states only. (Internal monitoring tools). | +| **3** | **Professional** | Standard easings (150-200ms). Skeleton shimmers. (Admin Dashboards, GuruRMM). | +| **4** | **Polished** | View Transitions. Subtle card lifts. Optimistic UI. (User-facing settings, consumer tools). | +| **5** | **Expressive** | Full shared-element morphs. Physics-based springs. (Onboarding, Marketing). | + +**Guidance**: If you are in an operator console (Level 2-3), avoid any motion that takes > 200ms or that changes element positions significantly. + --- -## Categories of Elegant Delight +## Technical Signals for Fancy Opportunities + ### 1. Transitions & Easing (The Foundation) diff --git a/.claude/skills/human-flow/references/mouse-keyboard-heuristics.md b/.claude/skills/human-flow/references/mouse-keyboard-heuristics.md index dd2c5d5..ee2cf43 100644 --- a/.claude/skills/human-flow/references/mouse-keyboard-heuristics.md +++ b/.claude/skills/human-flow/references/mouse-keyboard-heuristics.md @@ -193,7 +193,30 @@ Prioritize findings that affect the most frequent user workflows in the product --- -## Related Anti-Patterns from Parent Skills +## 7. State-Flow Audit (Dynamic Friction) + +**Anti-patterns**: +- Elements that jump or shift layout when data loads (layout thrash). +- Lack of optimistic UI for frequent, low-risk actions (waiting for server for every checkbox toggle). +- "Dead zones" during state transitions where the UI is locked but doesn't look it. + +**Better human workflow**: +- Use skeleton screens with consistent dimensions. +- Apply optimistic updates with clear rollback on error. +- Ensure the "next logical target" is available or signaled as "loading". + +--- + +## 8. The Precision Rail & Fumble Zones + +**Anti-patterns**: +- Important interactive controls placed in the leftmost 40px or rightmost 40px of a screen with zero padding. +- Dense clusters of varied actions in the "Fumble Zone" (corners). + +**Better human workflow**: +- Provide at least 16px of "safe padding" on edges. +- Group similar actions; keep high-risk actions away from frequent navigation rails. + This skill deliberately overlaps with and specializes rules from `impeccable` (no identical card grids, no hero metrics, strong focus on cognitive load and emotional journey) and `frontend-design` (click targets 44px, hover states, focus states, disabled states). diff --git a/.claude/skills/human-flow/references/report-template.md b/.claude/skills/human-flow/references/report-template.md index 1533c71..df0c972 100644 --- a/.claude/skills/human-flow/references/report-template.md +++ b/.claude/skills/human-flow/references/report-template.md @@ -7,12 +7,14 @@ Use this structure for all `scan`, `audit`, and `report` outputs. ## Human-Flow Report: **Date**: YYYY-MM-DD -**Scanner**: human-flow v1 (mouse + keyboard intuition focus) -**Scope**: -**Overall Human Workflow Score**: X/10 -- Mouse Ergonomics: X/10 -- Keyboard Parity & Efficiency: X/10 -- Workflow Discoverability & Friction: X/10 +**Scanner**: human-flow v2 (AST-Powered) +**Overall Human Workflow Score**: X/10 + +### Friction Index Rubric +- **Motor (3.0)**: Target size, precision, travel distance. +- **Cognitive (2.5)**: Discoverability, affordance, consistency. +- **Keyboard (2.5)**: Accessibility, focus flow, parity. +- **Feedback (2.0)**: Visual response, state transitions. **Summary** (2-4 sentences: the biggest sources of unintuitive behavior for a human operator using mouse and keyboard, and the net effect on daily workflow.) diff --git a/.claude/skills/human-flow/scripts/scan.mjs b/.claude/skills/human-flow/scripts/scan.mjs index 8da1c29..2870ae3 100644 --- a/.claude/skills/human-flow/scripts/scan.mjs +++ b/.claude/skills/human-flow/scripts/scan.mjs @@ -1,23 +1,24 @@ #!/usr/bin/env node /** - * human-flow scanner + * human-flow scanner v2 (AST-Powered) * - * Static analysis pass for mouse + keyboard workflow friction. - * Expands the spirit of frontend-design and impeccable with a narrow, - * human-motor-and-expectation focus. + * Sophisticated analysis pass for mouse + keyboard workflow friction. + * Uses @babel/parser for deep JSX/TSX understanding. * * Usage: - * node scripts/scan.mjs --path dashboard/src --format json - * node scripts/scan.mjs --path dashboard/src/features/sessions - * - * It is intentionally lightweight (regex + heuristics) so it can run fast - * inside agent loops. The real intelligence comes from the agent combining - * these findings with full component reading and task-flow understanding. + * node scripts/scan.mjs --path src + * node scripts/scan.mjs --path src --fix */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { parse } from '@babel/parser'; +import _traverse from '@babel/traverse'; +import _generate from '@babel/generator'; +import * as t from '@babel/types'; +const traverse = _traverse.default; +const generate = _generate.default; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -25,12 +26,13 @@ const args = process.argv.slice(2); let targetPath = 'src'; let format = 'text'; let mode = 'friction'; // 'friction' | 'fancy' +let applyFix = false; for (let i = 0; i < args.length; i++) { if (args[i] === '--path' || args[i] === '-p') targetPath = args[++i]; if (args[i] === '--format' || args[i] === '-f') format = args[++i]; if (args[i] === '--fancy' || args[i] === '--mode=fancy') mode = 'fancy'; - if (args[i] === '--mode' && args[i + 1] === 'fancy') { mode = 'fancy'; i++; } + if (args[i] === '--fix') applyFix = true; } const absTarget = path.resolve(process.cwd(), targetPath); @@ -40,16 +42,40 @@ if (!fs.existsSync(absTarget)) { process.exit(1); } +// `--fix` auto-apply is DISABLED for now: @babel/generator reprints the whole +// AST, producing noisy diffs that touch untouched code. Until it does surgical +// edits, run advisory only — agents apply fixes surgically from the report. +if (applyFix) { + console.error('[INFO] --fix (auto-apply) is disabled; running an advisory scan instead. Apply fixes surgically from the report.'); + applyFix = false; +} + const findings = []; +let fixesApplied = 0; + +// Friction Index Rubric Weights +const WEIGHTS = { + MOTOR: 3.0, + COGNITIVE: 2.5, + KEYBOARD: 2.5, + FEEDBACK: 2.0 +}; + +const SEVERITY_POINTS = { + high: 1.0, + medium: 0.5, + low: 0.2 +}; function walk(dir) { + if (!fs.existsSync(dir)) return; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { if (['node_modules', 'dist', 'build', '.git'].includes(entry.name)) continue; walk(full); - } else if (/\.(tsx|jsx|ts|js|css)$/.test(entry.name)) { + } else if (/\.(tsx|jsx|ts|js)$/.test(entry.name)) { analyzeFile(full); } } @@ -58,197 +84,256 @@ function walk(dir) { function analyzeFile(file) { const content = fs.readFileSync(file, 'utf8'); const rel = path.relative(process.cwd(), file).replace(/\\/g, '/'); - const lines = content.split('\n'); - - if (mode === 'fancy') { - // Fancy / beauty & elegance pass — lighter static signals + prompts for qualitative review - let match; - - // Existing transitions / animations (look for opportunities to refine) - const hasTransition = /transition:|transition-\w+:|animate-|@keyframes|ViewTransition|view-transition/i.test(content); - if (hasTransition) { - findings.push({ - file: rel, - line: 1, - category: 'fancy-existing', - severity: 'info', - pattern: 'existing-motion', - message: 'This file already contains motion/transition code. Good candidate for the fancy pass to review quality, consistency, and restraint.', - humanImpact: 'Existing fancy elements can feel either premium or cheap/janky depending on execution.', - suggestion: 'In the fancy pass, evaluate easing curves, durations, performance, reduced-motion respect, and whether the motion serves the human workflow or just decorates.' - }); - } - - // Missing View Transitions API in SPA navigation contexts - if (/(useNavigate|navigate\(|]*className=.*btn--sm|height:\s*2[0-8]px|min-height:\s*2[0-8]px/g; - let match; - while ((match = smallButton.exec(content)) !== null) { - const lineNo = content.substring(0, match.index).split('\n').length; - findings.push({ - file: rel, - line: lineNo, - category: 'target-size', - severity: 'high', - pattern: 'small-button', - message: 'Compact "sm" button or very small height used for an action. Frequent actions (especially in lists) become precision targets.', - humanImpact: 'Operators must slow down and aim carefully for common tasks. High error rate under time pressure.', - suggestion: 'Use default (md) size for primary/frequent actions. For true compact row actions, ensure generous invisible padding or switch to a larger always-visible treatment.' + let modified = false; + + try { + const ast = parse(content, { + sourceType: 'module', + plugins: ['jsx', 'typescript', 'decorators-legacy', 'classProperties'], + errorRecovery: true }); - } - // 2. Hover-revealed or low-opacity row actions (the classic operator console anti-pattern) - if (/\.dt__rowactions|\.rowactions|\.actions\s*\{[^}]*opacity:\s*0\.[0-6]/s.test(content) || - /opacity:\s*0\.[0-6][^}]*hover|hover[^}]*opacity:\s*(1|0\.[7-9])/s.test(content)) { - const lineNo = 1; // best effort - findings.push({ - file: rel, - line: lineNo, - category: 'discoverability', - severity: 'high', - pattern: 'hover-only-actions', - message: 'Row or list actions are dimmed or hidden until hover (or only fully visible on hover).', - humanImpact: 'A human scanning a list with eyes + mouse must "paint" every row to discover what they can do. Keyboard users often never see the controls at full strength.', - suggestion: 'Raise resting opacity to 0.7–1.0 so actions are scannable at a glance. Or move frequent actions into a dedicated, always-visible column or primary row target. Keep hover only for polish, not discovery.' + if (mode === 'fancy') { + // Fancy / beauty & elegance pass + const hasMotion = /transition:|animate-|@keyframes|framer-motion|ViewTransition/i.test(content); + if (hasMotion) { + addFinding({ + file: rel, + line: 1, + category: 'FEEDBACK', + severity: 'low', + pattern: 'existing-motion', + message: 'Existing motion detected. Review for quality, easing, and restraint.', + humanImpact: 'Motion can feel premium or cheap depending on execution.', + suggestion: 'Check if easings match the Restraint-o-Meter Level 3-4 (150-250ms).' + }); + } + + // Missing View Transitions in SPA contexts + if (/(useNavigate|navigate\(| c.type === 'JSXElement'); + const iconName = getComponentName(iconNode.openingElement); + const label = iconName.replace(/Icon$/, ''); + node.attributes.push(t.jsxAttribute(t.jsxIdentifier('aria-label'), t.stringLiteral(label))); + modified = true; + fixesApplied++; + } else { + addFinding({ + file: rel, + line: node.loc.start.line, + category: 'COGNITIVE', + severity: 'high', + pattern: 'unlabeled-icon-button', + message: `Button "${name}" contains only an icon but has no aria-label or title.`, + humanImpact: 'Keyboard and screen reader users have no way to know what this button does.', + suggestion: 'Add an aria-label or title prop describing the action.' + }); + } + } + } + + // 2. Tiny Target Calculator + if (isInteractive(node)) { + const size = getTargetSize(node); + if (size < 32) { + addFinding({ + file: rel, + line: node.loc.start.line, + category: 'MOTOR', + severity: 'high', + pattern: 'tiny-target', + message: `Interactive element "${name}" has a detected size of ~${size}px.`, + humanImpact: 'Small targets require high precision, leading to slower workflows and mis-clicks.', + suggestion: 'Increase height/width to at least 32px (ideally 44px) or add generous padding.' + }); + } + } + + // 3. Interaction Feedback Missing + if (name === 'Button' || name === 'ActionButton') { + if (!hasFeedbackProps(node)) { + addFinding({ + file: rel, + line: node.loc.start.line, + category: 'FEEDBACK', + severity: 'medium', + pattern: 'missing-feedback-props', + message: `Button "${name}" lacks loading or active state props.`, + humanImpact: 'Users may be unsure if their click was registered during long operations.', + suggestion: 'Add isLoading or active props to provide immediate visual feedback.' + }); + } + } + + // 4. Keyboard Parity: onClick without key handler + if (hasProp(node, 'onClick') && !isNativeButton(node) && !hasKeyboardProps(node)) { + addFinding({ + file: rel, + line: node.loc.start.line, + category: 'KEYBOARD', + severity: 'high', + pattern: 'click-without-keyboard', + message: `Custom element "${name}" has onClick but no keyboard handlers (onKeyDown) or tabIndex.`, + humanImpact: 'Keyboard users cannot trigger this action, creating a complete blocker for some workflows.', + suggestion: 'Add tabIndex={0} and an onKeyDown handler for Enter/Space.' + }); + } + } }); - } - // 3. onClick without obvious keyboard support on non-native elements - const clickNoKeyboard = /onClick=\{[^}]+}\s*(?!.*(onKeyDown|tabIndex|role=))/g; - while ((match = clickNoKeyboard.exec(content)) !== null) { - const lineNo = content.substring(0, match.index).split('\n').length; - // Only flag if it looks like a custom interactive (div, span, custom component in list context) - const context = content.substring(Math.max(0, match.index - 80), match.index + 120); - if (/<\s*(div|span|tr|td|li|custom|Card|Row)[^>]*onClick|onClick[^>]*<\s*(div|span|tr|td|li|Card|Row)/.test(context)) { - findings.push({ - file: rel, - line: lineNo, - category: 'keyboard-parity', - severity: 'high', - pattern: 'click-without-keyboard', - message: 'Custom element has onClick but no visible tabIndex/onKeyDown/Enter-Space handling in the immediate area.', - humanImpact: 'Keyboard (or mixed mouse+keyboard) users cannot activate the same thing the mouse can without extra workarounds.', - suggestion: 'Add tabIndex={0}, onKeyDown handler for Enter/Space, and strong :focus-visible styles. Prefer native