screenhand 0.3.9 → 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.
- package/README.md +5 -2
- package/dist/mcp-desktop.js +82 -0
- package/dist/src/ingestion/coverage-auditor.js +23 -3
- package/dist/src/ingestion/feature-extractor.js +366 -0
- package/dist/src/ingestion/reference-merger.js +17 -0
- package/dist/src/memory/playbook-seeds.js +1 -1
- package/dist/src/playbook/engine.js +15 -0
- package/dist/src/playbook/mcp-recorder.js +21 -1
- package/dist/src/state/app-map.js +56 -51
- package/dist/src/state/ladder-generator.js +84 -25
- package/dist-references/finder.json +735 -7
- package/package.json +1 -1
|
@@ -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":
|
|
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
|
-
// ──
|
|
15
|
-
{ id: "browse_channels", description: "Join servers and browse channels", level: "
|
|
16
|
-
{ id: "send_message", description: "Send messages, replies, emojis, and reactions", level: "
|
|
17
|
-
{ id: "direct_messages", description: "Direct messages and group chats", level: "
|
|
18
|
-
{ id: "voice_video", description: "Voice channels, video calls, and screen share", level: "
|
|
19
|
-
// ──
|
|
20
|
-
{ id: "threads_forums", description: "Create and manage threads and forum channels", level: "
|
|
21
|
-
{ id: "roles_permissions", description: "Configure roles, overrides, inheritance, hidden channels", level: "
|
|
22
|
-
{ id: "events_stage", description: "Schedule events, run Stage channels, manage speakers", level: "
|
|
23
|
-
{ id: "onboarding_funnel", description: "Build join flows: rules screening, role assignment, starter channels", level: "
|
|
24
|
-
{ id: "notification_control", description: "Channel overrides, mention control, suppression settings", level: "
|
|
25
|
-
// ──
|
|
26
|
-
{ id: "moderation_system", description: "Configure AutoMod, mod bots, alert flows, ban appeals, raid defense", level: "
|
|
27
|
-
{ id: "bot_ecosystem", description: "Combine bots, slash commands, webhooks into coherent server OS", level: "
|
|
28
|
-
{ id: "server_architecture", description: "Design categories, channel taxonomy, permissions, escalation paths", level: "
|
|
29
|
-
{ id: "community_growth", description: "Events, role rewards, content loops, announcements, retention mechanics", level: "
|
|
30
|
-
{ id: "analytics_health", description: "Track activity patterns, onboarding drop-off, channel usage, retention", level: "
|
|
31
|
-
// ──
|
|
32
|
-
{ id: "monetization_membership", description: "Premium roles, gated channels, supporter tiers, creator monetization", level: "
|
|
33
|
-
{ id: "crisis_handling", description: "Handle raids, harassment, spam, leaks, impersonation, conflicts", level: "
|
|
34
|
-
{ id: "cross_platform", description: "Connect Discord with GitHub, Notion, Twitch, Stripe, Zapier, tools", level: "
|
|
35
|
-
{ id: "staff_system", description: "Structure mod roles, escalation, internal channels, review processes", level: "
|
|
36
|
-
{ id: "brand_culture", description: "Shape tone, rituals, norms, recognition systems, community identity", level: "
|
|
37
|
-
{ id: "governance_policy", description: "Define rules, enforcement, appeals, social boundaries that hold up", level: "
|
|
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: "
|
|
41
|
-
{ id: "tabs_windows", description: "Manage tabs and windows", level: "
|
|
42
|
-
{ id: "bookmarks", description: "Bookmarks and reading list", level: "
|
|
43
|
-
{ id: "history_search", description: "History and search", level: "
|
|
44
|
-
{ id: "tab_groups", description: "Tab groups and profiles", level: "
|
|
45
|
-
{ id: "extensions", description: "Install and use extensions", level: "
|
|
46
|
-
{ id: "dev_tools", description: "Web Inspector and developer tools", level: "
|
|
47
|
-
{ id: "privacy_settings", description: "Privacy, cookies, and content blockers", level: "
|
|
48
|
-
{ id: "web_apps", description: "Add to Dock, web apps, notifications", level: "
|
|
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: "
|
|
52
|
-
{ id: "copy_move", description: "Copy, move, rename, delete files", level: "
|
|
53
|
-
{ id: "search", description: "Spotlight and Finder search", level: "
|
|
54
|
-
{ id: "views_sort", description: "Change views, sort, and organize", level: "
|
|
55
|
-
{ id: "tags_favorites", description: "Tags, favorites, and sidebar", level: "
|
|
56
|
-
{ id: "quick_actions", description: "Quick Look, Quick Actions, and Services", level: "
|
|
57
|
-
{ id: "automator_scripts", description: "Automator, terminal, and scripting", level: "
|
|
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: "
|
|
63
|
-
{ id: "core_action", description: "Perform the app's primary action", level: "
|
|
64
|
-
{ id: "settings", description: "Configure settings and preferences", level: "
|
|
65
|
-
{ id: "advanced_features", description: "Use advanced/power-user features", level: "
|
|
66
|
-
{ id: "automation", description: "Automate or customize workflows", level: "
|
|
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 = "
|
|
292
|
+
let level = "F";
|
|
293
293
|
if (toolName === "menu_click" || toolName === "key")
|
|
294
|
-
level = "
|
|
294
|
+
level = "B";
|
|
295
295
|
if (toolName === "applescript" || toolName === "browser_js")
|
|
296
|
-
level = "
|
|
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
|
-
|
|
1439
|
-
const
|
|
1440
|
-
|
|
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
|
|
4
|
-
const
|
|
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
|
|
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
|
|
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
|
|
16
|
+
const SSS_KEYWORDS = new Set([
|
|
17
17
|
"ai", "api", "integration", "custom", "governance", "crisis", "identity",
|
|
18
18
|
"billing", "orchestrat", "pipeline",
|
|
19
19
|
]);
|
|
@@ -32,8 +32,10 @@ export function generateLadderFromReference(ref) {
|
|
|
32
32
|
const selectorGroups = ref.selectors ?? {};
|
|
33
33
|
const flows = ref.flows ?? {};
|
|
34
34
|
// Minimum threshold: need at least 2 meaningful selector groups
|
|
35
|
+
// (but website features can stand alone — they come from official sources)
|
|
35
36
|
const meaningfulGroups = Object.keys(selectorGroups).filter(k => !SKIP_GROUPS.has(k));
|
|
36
|
-
|
|
37
|
+
const hasWebsiteFeatures = (ref.websiteFeatures?.length ?? 0) > 0 || (ref.valueAddFeatures?.length ?? 0) > 0;
|
|
38
|
+
if (meaningfulGroups.length < 2 && Object.keys(flows).length < 2 && !hasWebsiteFeatures) {
|
|
37
39
|
return { ladder: [], signals: {}, hash: computeHash(ref) };
|
|
38
40
|
}
|
|
39
41
|
// Track which flow names are already covered by selector groups
|
|
@@ -76,9 +78,53 @@ export function generateLadderFromReference(ref) {
|
|
|
76
78
|
const keywords = extractKeywordsFromFlow(flowName, flow);
|
|
77
79
|
signals[featureId] = keywords;
|
|
78
80
|
}
|
|
81
|
+
// ── Step 2.5: Features from website extraction ─────────────────
|
|
82
|
+
if (ref.websiteFeatures) {
|
|
83
|
+
for (const wf of ref.websiteFeatures) {
|
|
84
|
+
const featureId = `web_${wf.id}`;
|
|
85
|
+
if (features.some((f) => f.id === featureId))
|
|
86
|
+
continue;
|
|
87
|
+
// Skip if an exact selector group or flow already covers this feature
|
|
88
|
+
// (use exact key match, not fuzzy flowNamesRelated — avoids false positives
|
|
89
|
+
// where a short website feature id like "export" matches "export_pdf_flow")
|
|
90
|
+
if (selectorGroups[wf.id] !== undefined || flows[wf.id] !== undefined)
|
|
91
|
+
continue;
|
|
92
|
+
const level = (["F", "B", "S", "SSS"].includes(wf.level)
|
|
93
|
+
? wf.level
|
|
94
|
+
: "F");
|
|
95
|
+
const weight = assignWeight(wf.id, 0, level);
|
|
96
|
+
features.push({
|
|
97
|
+
id: featureId,
|
|
98
|
+
description: wf.description || wf.name,
|
|
99
|
+
level,
|
|
100
|
+
weight,
|
|
101
|
+
critical: false,
|
|
102
|
+
});
|
|
103
|
+
signals[featureId] = extractKeywordsFromName(wf.name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// ── Step 2.6: ScreenHand value-add features ───────────────────
|
|
107
|
+
if (ref.valueAddFeatures) {
|
|
108
|
+
for (const va of ref.valueAddFeatures) {
|
|
109
|
+
const featureId = `va_${va.id}`;
|
|
110
|
+
if (features.some((f) => f.id === featureId))
|
|
111
|
+
continue;
|
|
112
|
+
const level = (["B", "S", "SSS"].includes(va.level)
|
|
113
|
+
? va.level
|
|
114
|
+
: "S");
|
|
115
|
+
features.push({
|
|
116
|
+
id: featureId,
|
|
117
|
+
description: va.description,
|
|
118
|
+
level,
|
|
119
|
+
weight: 2,
|
|
120
|
+
critical: false,
|
|
121
|
+
});
|
|
122
|
+
signals[featureId] = extractKeywordsFromName(va.name);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
79
125
|
// ── Step 3: Sort by level progression ──────────────────────────
|
|
80
126
|
const levelOrder = {
|
|
81
|
-
|
|
127
|
+
F: 0, B: 1, S: 2, SSS: 3,
|
|
82
128
|
};
|
|
83
129
|
features.sort((a, b) => levelOrder[a.level] - levelOrder[b.level]);
|
|
84
130
|
return { ladder: features, signals, hash: computeHash(ref) };
|
|
@@ -87,37 +133,37 @@ export function generateLadderFromReference(ref) {
|
|
|
87
133
|
function assignLevel(name, selectorCount, isFlow, complexity) {
|
|
88
134
|
const nameLower = name.toLowerCase();
|
|
89
135
|
const parts = nameLower.split(/[_\s-]+/);
|
|
90
|
-
// Check
|
|
91
|
-
if (parts.some(p =>
|
|
92
|
-
return "
|
|
93
|
-
if (parts.some(p =>
|
|
94
|
-
return "
|
|
95
|
-
if (parts.some(p =>
|
|
96
|
-
return "
|
|
97
|
-
if (parts.some(p =>
|
|
98
|
-
return "
|
|
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";
|
|
99
145
|
// Fallback based on complexity
|
|
100
146
|
if (isFlow) {
|
|
101
147
|
if (complexity >= 8)
|
|
102
|
-
return "
|
|
148
|
+
return "S";
|
|
103
149
|
if (complexity >= 5)
|
|
104
|
-
return "
|
|
105
|
-
return "
|
|
150
|
+
return "B";
|
|
151
|
+
return "F";
|
|
106
152
|
}
|
|
107
153
|
// Selector group: more selectors = more complex
|
|
108
154
|
if (selectorCount >= 8)
|
|
109
|
-
return "
|
|
155
|
+
return "S";
|
|
110
156
|
if (selectorCount >= 4)
|
|
111
|
-
return "
|
|
112
|
-
return "
|
|
157
|
+
return "B";
|
|
158
|
+
return "F";
|
|
113
159
|
}
|
|
114
160
|
// ── Weight assignment ────────────────────────────────────────────
|
|
115
161
|
function assignWeight(name, complexity, level) {
|
|
116
|
-
if (level === "
|
|
162
|
+
if (level === "SSS")
|
|
117
163
|
return 3;
|
|
118
|
-
if (level === "
|
|
164
|
+
if (level === "S" && complexity >= 6)
|
|
119
165
|
return 3;
|
|
120
|
-
if (level === "
|
|
166
|
+
if (level === "S" || level === "B")
|
|
121
167
|
return 2;
|
|
122
168
|
return 1;
|
|
123
169
|
}
|
|
@@ -204,6 +250,17 @@ function extractKeywordsFromFlow(flowName, flow) {
|
|
|
204
250
|
}
|
|
205
251
|
return deduplicateArray(keywords);
|
|
206
252
|
}
|
|
253
|
+
// ── Keyword extraction from feature name ──────────────────────
|
|
254
|
+
function extractKeywordsFromName(name) {
|
|
255
|
+
const keywords = [];
|
|
256
|
+
for (const part of name.split(/[\s_-]+/)) {
|
|
257
|
+
const lower = part.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
258
|
+
if (lower.length > 2 && !STOP_WORDS.has(lower)) {
|
|
259
|
+
keywords.push(lower);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return deduplicateArray(keywords);
|
|
263
|
+
}
|
|
207
264
|
// ── Flow-to-selector group name matching ─────────────────────────
|
|
208
265
|
function flowNamesRelated(groupName, flowName) {
|
|
209
266
|
const gParts = new Set(groupName.split("_"));
|
|
@@ -222,6 +279,8 @@ function computeHash(ref) {
|
|
|
222
279
|
const keys = [
|
|
223
280
|
...Object.keys(ref.selectors ?? {}).sort(),
|
|
224
281
|
...Object.keys(ref.flows ?? {}).sort(),
|
|
282
|
+
...(ref.websiteFeatures ?? []).map((f) => f.id).sort(),
|
|
283
|
+
...(ref.valueAddFeatures ?? []).map((f) => f.id).sort(),
|
|
225
284
|
].join("|");
|
|
226
285
|
// Simple string hash (djb2)
|
|
227
286
|
let hash = 5381;
|