screenhand 0.4.0 → 0.4.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.
@@ -5289,6 +5289,16 @@ function getJobRunner() {
5289
5289
  const client = await CDPClient({ port });
5290
5290
  return { Runtime: client.Runtime, Input: client.Input, close: () => client.close() };
5291
5291
  });
5292
+ // Wire AppleScript runner into playbook engine for applescript steps
5293
+ playbookEngine.setAppleScriptRunner(async (script) => {
5294
+ if (process.platform === "win32")
5295
+ throw new Error("AppleScript is not supported on Windows");
5296
+ const { execSync } = await import("node:child_process");
5297
+ return execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, {
5298
+ encoding: "utf-8",
5299
+ timeout: 15000,
5300
+ }).trim();
5301
+ });
5292
5302
  activeJobRunner = new JobRunner(bridge, jobManager, leaseManager, supervisor, (() => {
5293
5303
  const cfg = {
5294
5304
  hasCDP: cdpPort !== null,
@@ -39,7 +39,7 @@ export class CoverageAuditor {
39
39
  */
40
40
  audit(bundleId, appName, menuScan) {
41
41
  const refs = this.loadReferences(bundleId);
42
- const playbooks = this.loadPlaybooks(bundleId);
42
+ const playbooks = this.loadPlaybooks(bundleId, appName);
43
43
  // Count what we know
44
44
  let shortcutsKnown = 0;
45
45
  let selectorsKnown = 0;
@@ -208,8 +208,12 @@ export class CoverageAuditor {
208
208
  catch { /* dir not found */ }
209
209
  return refs;
210
210
  }
211
- loadPlaybooks(bundleId) {
211
+ loadPlaybooks(bundleId, appName) {
212
212
  const playbooks = [];
213
+ // Derive short platform name from bundleId: "com.apple.Notes" → "notes"
214
+ const bundleParts = bundleId.split(".");
215
+ const shortName = (bundleParts[bundleParts.length - 1] ?? "").toLowerCase();
216
+ const appNameLower = appName.toLowerCase();
213
217
  try {
214
218
  const files = fs.readdirSync(this.playbooksDir);
215
219
  for (const file of files) {
@@ -218,7 +222,12 @@ export class CoverageAuditor {
218
222
  try {
219
223
  const raw = fs.readFileSync(path.join(this.playbooksDir, file), "utf-8");
220
224
  const pb = JSON.parse(raw);
221
- if (pb.bundleId === bundleId || pb.platform === bundleId) {
225
+ // Match by bundleId (exact), platform name (case-insensitive), or app name
226
+ const platformLower = (pb.platform ?? "").toLowerCase();
227
+ if (pb.bundleId === bundleId ||
228
+ platformLower === bundleId ||
229
+ platformLower === shortName ||
230
+ platformLower === appNameLower) {
222
231
  playbooks.push(pb);
223
232
  }
224
233
  }
@@ -1,27 +1,27 @@
1
1
  // Copyright (C) 2025 Clazro Technology Private Limited
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
- // ── Level assignment keywords ─────────────────────────────────────
3
+ // ── Tier assignment keywords (F=entry, B=proficient, S=expert, SSS=grandmaster) ─
4
4
  /** Single-word keywords checked individually */
5
- const BEGINNER_WORDS = new Set([
5
+ const F_WORDS = new Set([
6
6
  "basic", "create", "view", "share", "read",
7
7
  "browse", "search", "home", "start", "launch", "write",
8
8
  ]);
9
- const PRO_WORDS = new Set([
9
+ const B_WORDS = new Set([
10
10
  "organize", "format", "customize", "template", "tag", "folder",
11
11
  "sort", "filter", "pin", "archive", "move", "rename", "duplicate",
12
12
  "favorites", "bookmark", "list", "table", "style", "font",
13
13
  ]);
14
- const EXPERT_WORDS = new Set([
14
+ const S_WORDS = new Set([
15
15
  "automate", "shortcut", "export", "import", "collaborate", "sync",
16
16
  "scan", "link", "mention", "embed", "attachment", "password",
17
17
  "encrypt", "lock", "version", "history", "recover", "backup",
18
18
  ]);
19
- const GRANDMASTER_WORDS = new Set([
19
+ const SSS_WORDS = new Set([
20
20
  "api", "integrate", "plugin", "advanced", "workflow", "script",
21
21
  "extension", "developer", "sdk", "automation", "pipeline", "webhook",
22
22
  ]);
23
23
  /** Multi-word phrases checked via substring match */
24
- const GRANDMASTER_PHRASES = [
24
+ const SSS_PHRASES = [
25
25
  "custom action", "get started",
26
26
  ];
27
27
  // ── HTML entity decoding ──────────────────────────────────────────
@@ -97,32 +97,32 @@ function nameToId(name) {
97
97
  function assignLevel(name, description) {
98
98
  const text = `${name} ${description}`.toLowerCase();
99
99
  const words = text.split(/\s+/);
100
- // Check multi-word grandmaster phrases first
101
- for (const phrase of GRANDMASTER_PHRASES) {
100
+ // Check multi-word SSS phrases first
101
+ for (const phrase of SSS_PHRASES) {
102
102
  if (text.includes(phrase))
103
- return "grandmaster";
103
+ return "SSS";
104
104
  }
105
105
  // Check single-word keywords
106
106
  for (const w of words) {
107
- if (GRANDMASTER_WORDS.has(w))
108
- return "grandmaster";
107
+ if (SSS_WORDS.has(w))
108
+ return "SSS";
109
109
  }
110
110
  for (const w of words) {
111
- if (EXPERT_WORDS.has(w))
112
- return "expert";
111
+ if (S_WORDS.has(w))
112
+ return "S";
113
113
  }
114
114
  for (const w of words) {
115
- if (PRO_WORDS.has(w))
116
- return "pro";
115
+ if (B_WORDS.has(w))
116
+ return "B";
117
117
  }
118
118
  for (const w of words) {
119
- if (BEGINNER_WORDS.has(w))
120
- return "beginner";
119
+ if (F_WORDS.has(w))
120
+ return "F";
121
121
  }
122
122
  // Fallback: longer descriptions suggest complexity
123
123
  if (description.length > 80)
124
- return "pro";
125
- return "beginner";
124
+ return "B";
125
+ return "F";
126
126
  }
127
127
  // ── Main: Extract features from HTML ──────────────────────────────
128
128
  export function extractFeaturesFromHTML(html, appName, url) {
@@ -252,7 +252,7 @@ const VALUE_ADD_RULES = [
252
252
  name: "Bulk Create",
253
253
  description: `Create multiple ${app} items from a list or template`,
254
254
  category: "bulk",
255
- level: "pro",
255
+ level: "B",
256
256
  }),
257
257
  },
258
258
  {
@@ -263,7 +263,7 @@ const VALUE_ADD_RULES = [
263
263
  name: "Bulk Delete",
264
264
  description: `Delete multiple ${app} items matching criteria`,
265
265
  category: "bulk",
266
- level: "pro",
266
+ level: "B",
267
267
  }),
268
268
  },
269
269
  {
@@ -274,7 +274,7 @@ const VALUE_ADD_RULES = [
274
274
  name: "Bulk Export",
275
275
  description: `Export all ${app} items at once`,
276
276
  category: "bulk",
277
- level: "pro",
277
+ level: "B",
278
278
  }),
279
279
  },
280
280
  {
@@ -285,7 +285,7 @@ const VALUE_ADD_RULES = [
285
285
  name: "Auto-Organize",
286
286
  description: `Sort and organize ${app} items by content, date, or type`,
287
287
  category: "organization",
288
- level: "expert",
288
+ level: "S",
289
289
  }),
290
290
  },
291
291
  {
@@ -296,7 +296,7 @@ const VALUE_ADD_RULES = [
296
296
  name: "Smart Search",
297
297
  description: `Search across all ${app} content with pattern matching`,
298
298
  category: "organization",
299
- level: "pro",
299
+ level: "B",
300
300
  }),
301
301
  },
302
302
  {
@@ -307,7 +307,7 @@ const VALUE_ADD_RULES = [
307
307
  name: "Summarize",
308
308
  description: `Read and summarize all ${app} content`,
309
309
  category: "intelligence",
310
- level: "expert",
310
+ level: "S",
311
311
  }),
312
312
  },
313
313
  {
@@ -318,7 +318,7 @@ const VALUE_ADD_RULES = [
318
318
  name: "Find Duplicates",
319
319
  description: `Identify duplicate or near-duplicate ${app} items`,
320
320
  category: "intelligence",
321
- level: "expert",
321
+ level: "S",
322
322
  }),
323
323
  },
324
324
  {
@@ -329,7 +329,7 @@ const VALUE_ADD_RULES = [
329
329
  name: "Cross-App Export",
330
330
  description: `Export ${app} content to other apps automatically`,
331
331
  category: "cross_app",
332
- level: "expert",
332
+ level: "S",
333
333
  }),
334
334
  },
335
335
  {
@@ -340,7 +340,7 @@ const VALUE_ADD_RULES = [
340
340
  name: "Cross-App Import",
341
341
  description: `Import content from other apps into ${app}`,
342
342
  category: "cross_app",
343
- level: "expert",
343
+ level: "S",
344
344
  }),
345
345
  },
346
346
  {
@@ -351,7 +351,7 @@ const VALUE_ADD_RULES = [
351
351
  name: "Change Monitor",
352
352
  description: `Monitor ${app} for changes and notify`,
353
353
  category: "monitoring",
354
- level: "grandmaster",
354
+ level: "SSS",
355
355
  }),
356
356
  },
357
357
  ];
@@ -50,7 +50,7 @@ export function seedErrorsFromPlaybooks(playbooksDir) {
50
50
  for (const pb of playbooks) {
51
51
  const platform = pb.platform ?? pb.id ?? "unknown";
52
52
  // Extract from errors[]
53
- if (pb.errors) {
53
+ if (pb.errors && Array.isArray(pb.errors)) {
54
54
  for (const err of pb.errors) {
55
55
  const key = `${platform}::${err.error}`;
56
56
  if (seen.has(key))
@@ -20,6 +20,7 @@ const STEP_DELAY_MS = 300;
20
20
  export class PlaybookEngine {
21
21
  runtime;
22
22
  cdpConnect;
23
+ appleScriptRunner;
23
24
  /** Enable observer-based popup checks before each step */
24
25
  popupCheckEnabled = false;
25
26
  constructor(runtime) {
@@ -33,6 +34,10 @@ export class PlaybookEngine {
33
34
  setCDPConnect(factory) {
34
35
  this.cdpConnect = factory;
35
36
  }
37
+ /** Set AppleScript runner for applescript steps. Runner should execute the script and return stdout. */
38
+ setAppleScriptRunner(runner) {
39
+ this.appleScriptRunner = runner;
40
+ }
36
41
  /**
37
42
  * Execute a playbook against a live session.
38
43
  * Returns result with success/failure and which step broke.
@@ -284,6 +289,14 @@ export class PlaybookEngine {
284
289
  await client.close();
285
290
  }
286
291
  }
292
+ case "applescript": {
293
+ if (!step.script)
294
+ throw new Error("applescript step missing script");
295
+ if (!this.appleScriptRunner)
296
+ throw new Error("applescript requires runner — call setAppleScriptRunner() first");
297
+ const result = await this.appleScriptRunner(step.script);
298
+ return `applescript: ${result.substring(0, 200)}`;
299
+ }
287
300
  default:
288
301
  throw new Error(`Unknown action: ${step.action}`);
289
302
  }
@@ -312,6 +325,8 @@ export class PlaybookEngine {
312
325
  result.verify = sub(result.verify);
313
326
  if (result.menuPath)
314
327
  result.menuPath = result.menuPath.map(sub);
328
+ if (result.script)
329
+ result.script = sub(result.script);
315
330
  return result;
316
331
  }
317
332
  /**
@@ -28,18 +28,30 @@ const SKIP_TOOLS = new Set([
28
28
  "codex_monitor_start", "codex_monitor_status", "codex_monitor_add_task",
29
29
  "codex_monitor_tasks", "codex_monitor_assign_now", "codex_monitor_stop",
30
30
  "platform_learn", "platform_explore",
31
+ // Observation tools — not steps
32
+ "read_with_fallback", "locate_with_fallback", "execution_plan",
33
+ "browser_stealth",
34
+ // Observer/orchestrator lifecycle — not steps
35
+ "observer_start", "observer_stop", "observer_status", "observer_ocr_roi",
36
+ "orchestrator_start", "orchestrator_stop", "orchestrator_submit", "orchestrator_status",
37
+ // Ingestion/discovery — learning, not steps
38
+ "scan_menu_bar", "ingest_documentation", "ingest_tutorial",
39
+ "discover_features", "coverage_report",
31
40
  ]);
32
41
  /** Map MCP tool names to PlaybookStep actions */
33
42
  function mapToolToAction(toolName) {
34
43
  switch (toolName) {
35
- case "browser_navigate": return "navigate";
44
+ case "browser_navigate":
45
+ case "browser_open": return "navigate";
36
46
  case "click":
37
47
  case "click_text":
38
48
  case "browser_click":
49
+ case "browser_human_click":
39
50
  case "click_with_fallback":
40
51
  case "ui_press": return "press";
41
52
  case "type_text":
42
53
  case "browser_type":
54
+ case "browser_fill_form":
43
55
  case "type_with_fallback": return "type_into";
44
56
  case "key": return "key";
45
57
  case "menu_click": return "menu_click";
@@ -50,6 +62,9 @@ function mapToolToAction(toolName) {
50
62
  case "screenshot_file": return "screenshot";
51
63
  case "browser_wait":
52
64
  case "wait_for_state": return "wait";
65
+ case "applescript": return "applescript";
66
+ case "ui_set_value": return "type_into";
67
+ case "select_with_fallback": return "press";
53
68
  case "focus":
54
69
  case "launch": return null; // useful context but not a step
55
70
  case "drag": return null; // drag is complex, skip for now
@@ -108,6 +123,10 @@ function buildStep(toolName, params, success) {
108
123
  step.ms = Number(params.timeout ?? params.ms ?? params.timeoutMs ?? 1000);
109
124
  step.description = `Wait ${step.ms}ms`;
110
125
  break;
126
+ case "applescript":
127
+ step.script = String(params.script ?? "");
128
+ step.description = `AppleScript: ${step.script.substring(0, 80)}${step.script.length > 80 ? "..." : ""}`;
129
+ break;
111
130
  }
112
131
  if (!success) {
113
132
  step.optional = true;
@@ -168,6 +187,7 @@ export class McpPlaybookRecorder {
168
187
  ...(typeof s.target === "string" ? { target: redactPII(s.target) } : {}),
169
188
  ...(s.url ? { url: redactPII(s.url) } : {}),
170
189
  ...(s.code ? { code: redactPII(s.code) } : {}),
190
+ ...(s.script ? { script: redactPII(s.script) } : {}),
171
191
  ...(s.description ? { description: redactPII(s.description) } : {}),
172
192
  }));
173
193
  const playbook = {
@@ -11,59 +11,59 @@ import { generateLadderFromReference } from "./ladder-generator.js";
11
11
  // Define what real users do at each level. Used to measure honest mastery.
12
12
  const BUILTIN_LADDERS = {
13
13
  "com.hnc.Discord": [
14
- // ── Beginner: basic consumer actions (weight 1) ──
15
- { id: "browse_channels", description: "Join servers and browse channels", level: "beginner", weight: 1, critical: false },
16
- { id: "send_message", description: "Send messages, replies, emojis, and reactions", level: "beginner", weight: 1, critical: false },
17
- { id: "direct_messages", description: "Direct messages and group chats", level: "beginner", weight: 1, critical: false },
18
- { id: "voice_video", description: "Voice channels, video calls, and screen share", level: "beginner", weight: 1, critical: false },
19
- // ── Pro: operational features (weight 2) ──
20
- { id: "threads_forums", description: "Create and manage threads and forum channels", level: "pro", weight: 2, critical: false },
21
- { id: "roles_permissions", description: "Configure roles, overrides, inheritance, hidden channels", level: "pro", weight: 2, critical: true },
22
- { id: "events_stage", description: "Schedule events, run Stage channels, manage speakers", level: "pro", weight: 2, critical: false },
23
- { id: "onboarding_funnel", description: "Build join flows: rules screening, role assignment, starter channels", level: "pro", weight: 2, critical: true },
24
- { id: "notification_control", description: "Channel overrides, mention control, suppression settings", level: "pro", weight: 1, critical: false },
25
- // ── Expert: system-level features (weight 2-3) ──
26
- { id: "moderation_system", description: "Configure AutoMod, mod bots, alert flows, ban appeals, raid defense", level: "expert", weight: 3, critical: true },
27
- { id: "bot_ecosystem", description: "Combine bots, slash commands, webhooks into coherent server OS", level: "expert", weight: 3, critical: true },
28
- { id: "server_architecture", description: "Design categories, channel taxonomy, permissions, escalation paths", level: "expert", weight: 3, critical: true },
29
- { id: "community_growth", description: "Events, role rewards, content loops, announcements, retention mechanics", level: "expert", weight: 2, critical: false },
30
- { id: "analytics_health", description: "Track activity patterns, onboarding drop-off, channel usage, retention", level: "expert", weight: 2, critical: true },
31
- // ── Grandmaster: mastery-level operations (weight 3) ──
32
- { id: "monetization_membership", description: "Premium roles, gated channels, supporter tiers, creator monetization", level: "grandmaster", weight: 2, critical: false },
33
- { id: "crisis_handling", description: "Handle raids, harassment, spam, leaks, impersonation, conflicts", level: "grandmaster", weight: 3, critical: true },
34
- { id: "cross_platform", description: "Connect Discord with GitHub, Notion, Twitch, Stripe, Zapier, tools", level: "grandmaster", weight: 2, critical: false },
35
- { id: "staff_system", description: "Structure mod roles, escalation, internal channels, review processes", level: "grandmaster", weight: 3, critical: true },
36
- { id: "brand_culture", description: "Shape tone, rituals, norms, recognition systems, community identity", level: "grandmaster", weight: 2, critical: false },
37
- { id: "governance_policy", description: "Define rules, enforcement, appeals, social boundaries that hold up", level: "grandmaster", weight: 3, critical: true },
14
+ // ── F tier: basic consumer actions (weight 1) ──
15
+ { id: "browse_channels", description: "Join servers and browse channels", level: "F", weight: 1, critical: false },
16
+ { id: "send_message", description: "Send messages, replies, emojis, and reactions", level: "F", weight: 1, critical: false },
17
+ { id: "direct_messages", description: "Direct messages and group chats", level: "F", weight: 1, critical: false },
18
+ { id: "voice_video", description: "Voice channels, video calls, and screen share", level: "F", weight: 1, critical: false },
19
+ // ── B tier: operational features (weight 2) ──
20
+ { id: "threads_forums", description: "Create and manage threads and forum channels", level: "B", weight: 2, critical: false },
21
+ { id: "roles_permissions", description: "Configure roles, overrides, inheritance, hidden channels", level: "B", weight: 2, critical: true },
22
+ { id: "events_stage", description: "Schedule events, run Stage channels, manage speakers", level: "B", weight: 2, critical: false },
23
+ { id: "onboarding_funnel", description: "Build join flows: rules screening, role assignment, starter channels", level: "B", weight: 2, critical: true },
24
+ { id: "notification_control", description: "Channel overrides, mention control, suppression settings", level: "B", weight: 1, critical: false },
25
+ // ── S tier: system-level features (weight 2-3) ──
26
+ { id: "moderation_system", description: "Configure AutoMod, mod bots, alert flows, ban appeals, raid defense", level: "S", weight: 3, critical: true },
27
+ { id: "bot_ecosystem", description: "Combine bots, slash commands, webhooks into coherent server OS", level: "S", weight: 3, critical: true },
28
+ { id: "server_architecture", description: "Design categories, channel taxonomy, permissions, escalation paths", level: "S", weight: 3, critical: true },
29
+ { id: "community_growth", description: "Events, role rewards, content loops, announcements, retention mechanics", level: "S", weight: 2, critical: false },
30
+ { id: "analytics_health", description: "Track activity patterns, onboarding drop-off, channel usage, retention", level: "S", weight: 2, critical: true },
31
+ // ── SSS tier: grandmaster operations (weight 3) ──
32
+ { id: "monetization_membership", description: "Premium roles, gated channels, supporter tiers, creator monetization", level: "SSS", weight: 2, critical: false },
33
+ { id: "crisis_handling", description: "Handle raids, harassment, spam, leaks, impersonation, conflicts", level: "SSS", weight: 3, critical: true },
34
+ { id: "cross_platform", description: "Connect Discord with GitHub, Notion, Twitch, Stripe, Zapier, tools", level: "SSS", weight: 2, critical: false },
35
+ { id: "staff_system", description: "Structure mod roles, escalation, internal channels, review processes", level: "SSS", weight: 3, critical: true },
36
+ { id: "brand_culture", description: "Shape tone, rituals, norms, recognition systems, community identity", level: "SSS", weight: 2, critical: false },
37
+ { id: "governance_policy", description: "Define rules, enforcement, appeals, social boundaries that hold up", level: "SSS", weight: 3, critical: true },
38
38
  ],
39
39
  "com.apple.Safari": [
40
- { id: "browse_navigate", description: "Open URLs and navigate pages", level: "beginner", weight: 1, critical: false },
41
- { id: "tabs_windows", description: "Manage tabs and windows", level: "beginner", weight: 1, critical: false },
42
- { id: "bookmarks", description: "Bookmarks and reading list", level: "beginner", weight: 1, critical: false },
43
- { id: "history_search", description: "History and search", level: "beginner", weight: 1, critical: false },
44
- { id: "tab_groups", description: "Tab groups and profiles", level: "pro", weight: 2, critical: false },
45
- { id: "extensions", description: "Install and use extensions", level: "pro", weight: 2, critical: false },
46
- { id: "dev_tools", description: "Web Inspector and developer tools", level: "expert", weight: 2, critical: true },
47
- { id: "privacy_settings", description: "Privacy, cookies, and content blockers", level: "expert", weight: 2, critical: false },
48
- { id: "web_apps", description: "Add to Dock, web apps, notifications", level: "grandmaster", weight: 2, critical: false },
40
+ { id: "browse_navigate", description: "Open URLs and navigate pages", level: "F", weight: 1, critical: false },
41
+ { id: "tabs_windows", description: "Manage tabs and windows", level: "F", weight: 1, critical: false },
42
+ { id: "bookmarks", description: "Bookmarks and reading list", level: "F", weight: 1, critical: false },
43
+ { id: "history_search", description: "History and search", level: "F", weight: 1, critical: false },
44
+ { id: "tab_groups", description: "Tab groups and profiles", level: "B", weight: 2, critical: false },
45
+ { id: "extensions", description: "Install and use extensions", level: "B", weight: 2, critical: false },
46
+ { id: "dev_tools", description: "Web Inspector and developer tools", level: "S", weight: 2, critical: true },
47
+ { id: "privacy_settings", description: "Privacy, cookies, and content blockers", level: "S", weight: 2, critical: false },
48
+ { id: "web_apps", description: "Add to Dock, web apps, notifications", level: "SSS", weight: 2, critical: false },
49
49
  ],
50
50
  "com.apple.finder": [
51
- { id: "browse_files", description: "Browse and open files/folders", level: "beginner", weight: 1, critical: false },
52
- { id: "copy_move", description: "Copy, move, rename, delete files", level: "beginner", weight: 1, critical: false },
53
- { id: "search", description: "Spotlight and Finder search", level: "beginner", weight: 1, critical: false },
54
- { id: "views_sort", description: "Change views, sort, and organize", level: "pro", weight: 2, critical: false },
55
- { id: "tags_favorites", description: "Tags, favorites, and sidebar", level: "pro", weight: 2, critical: false },
56
- { id: "quick_actions", description: "Quick Look, Quick Actions, and Services", level: "expert", weight: 2, critical: true },
57
- { id: "automator_scripts", description: "Automator, terminal, and scripting", level: "grandmaster", weight: 2, critical: false },
51
+ { id: "browse_files", description: "Browse and open files/folders", level: "F", weight: 1, critical: false },
52
+ { id: "copy_move", description: "Copy, move, rename, delete files", level: "F", weight: 1, critical: false },
53
+ { id: "search", description: "Spotlight and Finder search", level: "F", weight: 1, critical: false },
54
+ { id: "views_sort", description: "Change views, sort, and organize", level: "B", weight: 2, critical: false },
55
+ { id: "tags_favorites", description: "Tags, favorites, and sidebar", level: "B", weight: 2, critical: false },
56
+ { id: "quick_actions", description: "Quick Look, Quick Actions, and Services", level: "S", weight: 2, critical: true },
57
+ { id: "automator_scripts", description: "Automator, terminal, and scripting", level: "SSS", weight: 2, critical: false },
58
58
  ],
59
59
  };
60
60
  /** Generic fallback ladder — used when no builtin AND no reference-generated ladder exists. */
61
61
  const GENERIC_LADDER = [
62
- { id: "basic_navigation", description: "Open, navigate, and browse the app", level: "beginner", weight: 1, critical: false },
63
- { id: "core_action", description: "Perform the app's primary action", level: "beginner", weight: 1, critical: false },
64
- { id: "settings", description: "Configure settings and preferences", level: "pro", weight: 2, critical: false },
65
- { id: "advanced_features", description: "Use advanced/power-user features", level: "expert", weight: 2, critical: true },
66
- { id: "automation", description: "Automate or customize workflows", level: "grandmaster", weight: 3, critical: true },
62
+ { id: "basic_navigation", description: "Open, navigate, and browse the app", level: "F", weight: 1, critical: false },
63
+ { id: "core_action", description: "Perform the app's primary action", level: "F", weight: 1, critical: false },
64
+ { id: "settings", description: "Configure settings and preferences", level: "B", weight: 2, critical: false },
65
+ { id: "advanced_features", description: "Use advanced/power-user features", level: "S", weight: 2, critical: true },
66
+ { id: "automation", description: "Automate or customize workflows", level: "SSS", weight: 3, critical: true },
67
67
  ];
68
68
  /** Redact an array of user-facing strings in place, returning a new array. */
69
69
  function redactStrings(strings) {
@@ -289,11 +289,11 @@ export class AppMap {
289
289
  if (discoveredCount >= 30)
290
290
  return null;
291
291
  // Assign level based on tool complexity
292
- let level = "beginner";
292
+ let level = "F";
293
293
  if (toolName === "menu_click" || toolName === "key")
294
- level = "pro";
294
+ level = "B";
295
295
  if (toolName === "applescript" || toolName === "browser_js")
296
- level = "expert";
296
+ level = "S";
297
297
  const feature = {
298
298
  id: featureId,
299
299
  description: `[auto] ${target}`,
@@ -1435,9 +1435,14 @@ export class AppMap {
1435
1435
  let totalFailures = 0;
1436
1436
  let weightedScore = 0;
1437
1437
  // Tier-scoped critical floor: only check critical features at or below the target tier
1438
- const tierOrder = ["beginner", "pro", "expert", "grandmaster"];
1439
- const scopeIdx = tierScope ? tierOrder.indexOf(tierScope) : 3; // default: all tiers
1440
- const scopedLevels = new Set(tierOrder.slice(0, scopeIdx + 1));
1438
+ // MasteryLevel (deprecated) maps to FeatureTier: beginner→F, pro→B, expert→S, grandmaster→SSS
1439
+ const masteryToFeatureTier = {
1440
+ beginner: "F", pro: "B", expert: "S", grandmaster: "SSS",
1441
+ };
1442
+ const featureTierOrder = ["F", "B", "S", "SSS"];
1443
+ const scopedFeatureTier = tierScope ? masteryToFeatureTier[tierScope] : "SSS";
1444
+ const scopeIdx = featureTierOrder.indexOf(scopedFeatureTier);
1445
+ const scopedLevels = new Set(featureTierOrder.slice(0, scopeIdx + 1));
1441
1446
  let criticalMinDepth = 999;
1442
1447
  let hasScopedCritical = false;
1443
1448
  for (const feature of ladder) {
@@ -1,19 +1,19 @@
1
1
  // Copyright (C) 2025 Clazro Technology Private Limited
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
- // ── Keyword sets for level assignment ────────────────────────────
4
- const BEGINNER_KEYWORDS = new Set([
3
+ // ── Keyword sets for tier assignment (F=entry, B=proficient, S=expert, SSS=grandmaster) ──
4
+ const F_KEYWORDS = new Set([
5
5
  "navigation", "browse", "basic", "search", "header", "page", "nav", "home",
6
6
  "page_header", "quick_find",
7
7
  ]);
8
- const PRO_KEYWORDS = new Set([
8
+ const B_KEYWORDS = new Set([
9
9
  "settings", "create", "views", "sort", "filter", "template", "new", "sidebar",
10
10
  "create_new", "slash_commands", "notification", "preferences",
11
11
  ]);
12
- const EXPERT_KEYWORDS = new Set([
12
+ const S_KEYWORDS = new Set([
13
13
  "database", "admin", "moderation", "permissions", "automation", "advanced",
14
14
  "server", "import", "export", "workflow", "security", "form",
15
15
  ]);
16
- const GRANDMASTER_KEYWORDS = new Set([
16
+ const SSS_KEYWORDS = new Set([
17
17
  "ai", "api", "integration", "custom", "governance", "crisis", "identity",
18
18
  "billing", "orchestrat", "pipeline",
19
19
  ]);
@@ -89,9 +89,9 @@ export function generateLadderFromReference(ref) {
89
89
  // where a short website feature id like "export" matches "export_pdf_flow")
90
90
  if (selectorGroups[wf.id] !== undefined || flows[wf.id] !== undefined)
91
91
  continue;
92
- const level = (["beginner", "pro", "expert", "grandmaster"].includes(wf.level)
92
+ const level = (["F", "B", "S", "SSS"].includes(wf.level)
93
93
  ? wf.level
94
- : "beginner");
94
+ : "F");
95
95
  const weight = assignWeight(wf.id, 0, level);
96
96
  features.push({
97
97
  id: featureId,
@@ -109,9 +109,9 @@ export function generateLadderFromReference(ref) {
109
109
  const featureId = `va_${va.id}`;
110
110
  if (features.some((f) => f.id === featureId))
111
111
  continue;
112
- const level = (["pro", "expert", "grandmaster"].includes(va.level)
112
+ const level = (["B", "S", "SSS"].includes(va.level)
113
113
  ? va.level
114
- : "expert");
114
+ : "S");
115
115
  features.push({
116
116
  id: featureId,
117
117
  description: va.description,
@@ -124,7 +124,7 @@ export function generateLadderFromReference(ref) {
124
124
  }
125
125
  // ── Step 3: Sort by level progression ──────────────────────────
126
126
  const levelOrder = {
127
- beginner: 0, pro: 1, expert: 2, grandmaster: 3,
127
+ F: 0, B: 1, S: 2, SSS: 3,
128
128
  };
129
129
  features.sort((a, b) => levelOrder[a.level] - levelOrder[b.level]);
130
130
  return { ladder: features, signals, hash: computeHash(ref) };
@@ -133,37 +133,37 @@ export function generateLadderFromReference(ref) {
133
133
  function assignLevel(name, selectorCount, isFlow, complexity) {
134
134
  const nameLower = name.toLowerCase();
135
135
  const parts = nameLower.split(/[_\s-]+/);
136
- // Check grandmaster first (most specific)
137
- if (parts.some(p => GRANDMASTER_KEYWORDS.has(p)))
138
- return "grandmaster";
139
- if (parts.some(p => EXPERT_KEYWORDS.has(p)))
140
- return "expert";
141
- if (parts.some(p => PRO_KEYWORDS.has(p)))
142
- return "pro";
143
- if (parts.some(p => BEGINNER_KEYWORDS.has(p)))
144
- return "beginner";
136
+ // Check SSS first (most specific)
137
+ if (parts.some(p => SSS_KEYWORDS.has(p)))
138
+ return "SSS";
139
+ if (parts.some(p => S_KEYWORDS.has(p)))
140
+ return "S";
141
+ if (parts.some(p => B_KEYWORDS.has(p)))
142
+ return "B";
143
+ if (parts.some(p => F_KEYWORDS.has(p)))
144
+ return "F";
145
145
  // Fallback based on complexity
146
146
  if (isFlow) {
147
147
  if (complexity >= 8)
148
- return "expert";
148
+ return "S";
149
149
  if (complexity >= 5)
150
- return "pro";
151
- return "beginner";
150
+ return "B";
151
+ return "F";
152
152
  }
153
153
  // Selector group: more selectors = more complex
154
154
  if (selectorCount >= 8)
155
- return "expert";
155
+ return "S";
156
156
  if (selectorCount >= 4)
157
- return "pro";
158
- return "beginner";
157
+ return "B";
158
+ return "F";
159
159
  }
160
160
  // ── Weight assignment ────────────────────────────────────────────
161
161
  function assignWeight(name, complexity, level) {
162
- if (level === "grandmaster")
162
+ if (level === "SSS")
163
163
  return 3;
164
- if (level === "expert" && complexity >= 6)
164
+ if (level === "S" && complexity >= 6)
165
165
  return 3;
166
- if (level === "expert" || level === "pro")
166
+ if (level === "S" || level === "B")
167
167
  return 2;
168
168
  return 1;
169
169
  }