///|
/// Human-oriented recommendations derived from machine findings.
pub(all) enum Effort {
Small
Medium
Large
} derive(Eq, @debug.Debug)
///|
pub(all) struct Recommendation {
code : String
title : String
priority : Int
severity : Severity
effort : Effort
affected_ids : Array[String]
rationale : String
action : String
} derive(Eq, @debug.Debug)
///|
fn effort_name(effort : Effort) -> String {
match effort {
Small => "small"
Medium => "medium"
Large => "large"
}
}
///|
fn recommendation_for_finding(finding : Finding) -> Recommendation {
let effort = match finding.kind {
ExactConflict => Medium
PrefixConflict => Medium
ShadowedBinding => Small
DuplicateCommand => Small
ReservedShortcut => Small
AccessibilityRisk => Medium
InvalidKey => Small
InvalidContext => Small
InvalidRecord => Small
DisabledBinding => Medium
PlatformOverlap => Medium
}
let priority = match finding.severity {
Error => 1
Warning => 2
Info => 3
}
let title = match finding.kind {
ExactConflict => "Resolve exact shortcut conflict"
PrefixConflict => "Disambiguate chord prefix"
ShadowedBinding => "Document or remove shadowed binding"
DuplicateCommand => "Explain duplicate command mapping"
ReservedShortcut => "Move reserved shortcut"
AccessibilityRisk => "Improve keyboard accessibility"
InvalidKey => "Correct invalid key spelling"
InvalidContext => "Declare the missing context"
InvalidRecord => "Repair malformed keymap record"
DisabledBinding => "Review disabled binding"
PlatformOverlap => "Separate platform fallback"
}
{
code: "REC-" + finding.code,
title,
priority,
severity: finding.severity,
effort,
affected_ids: if finding.secondary_id.length() == 0 {
[finding.primary_id]
} else {
[finding.primary_id, finding.secondary_id]
},
rationale: finding.message,
action: finding.suggestion,
}
}
///|
fn recommendation_compare(left : Recommendation, right : Recommendation) -> Int {
if left.priority < right.priority {
-1
} else if left.priority > right.priority {
1
} else if left.code < right.code {
-1
} else if left.code > right.code {
1
} else {
0
}
}
///|
/// Build one actionable item per finding, deduplicated by finding code and ids.
pub fn recommendations(analysis : Analysis) -> Array[Recommendation] {
let result : Array[Recommendation] = []
let signatures : Array[String] = []
for finding in analysis.findings {
let signature = finding.code +
":" +
finding.primary_id +
":" +
finding.secondary_id
if !array_contains(signatures, signature) {
signatures.push(signature)
result.push(recommendation_for_finding(finding))
}
}
result.sort_by(recommendation_compare)
result
}
///|
/// Add maintainability advice even when no conflict exists.
pub fn health_recommendations(
keymap : Keymap,
analysis : Analysis,
) -> Array[Recommendation] {
let result = recommendations(analysis)
let metrics = keymap_metrics(keymap)
if metrics.documented_count < metrics.binding_count {
result.push({
code: "REC-DOCS",
title: "Document every user-visible binding",
priority: 3,
severity: Warning,
effort: Medium,
affected_ids: [],
rationale: "undocumented shortcuts are difficult to discover and review",
action: "add a concise description= value to each binding",
})
}
if metrics.duplicate_command_count > 0 {
result.push({
code: "REC-COMMANDS",
title: "Explain duplicate command mappings",
priority: 3,
severity: Info,
effort: Small,
affected_ids: [],
rationale: "duplicate mappings can be intentional across contexts but should be explicit",
action: "add context-specific notes or consolidate the command",
})
}
if metrics.modifierless_count > 0 {
result.push({
code: "REC-ACCESS",
title: "Review modifierless shortcuts",
priority: 2,
severity: Warning,
effort: Medium,
affected_ids: [],
rationale: "single keys are easy to trigger while typing",
action: "provide a modifier chord and a documented alternative",
})
}
result.sort_by(recommendation_compare)
result
}
///|
pub fn recommendations_to_json(items : Array[Recommendation]) -> String {
let rows : Array[String] = []
for item in items {
let ids : Array[String] = []
for id in item.affected_ids {
ids.push(json_string(id))
}
rows.push(
"{\"code\":" +
json_string(item.code) +
",\"title\":" +
json_string(item.title) +
",\"priority\":" +
item.priority.to_string() +
",\"severity\":" +
json_string(item.severity.name()) +
",\"effort\":" +
json_string(effort_name(item.effort)) +
",\"ids\":[" +
ids.join(",") +
"],\"rationale\":" +
json_string(item.rationale) +
",\"action\":" +
json_string(item.action) +
"}",
)
}
"[" + rows.join(",") + "]"
}
///|
pub fn recommendations_to_markdown(items : Array[Recommendation]) -> String {
let lines : Array[String] = [
"## Recommended actions", "", "| Priority | Code | Finding | Effort | Action |",
"| ---: | --- | --- | --- | --- |",
]
if items.length() == 0 {
lines.push("| - | - | No action required | - | - |")
}
for item in items {
lines.push(
"| " +
item.priority.to_string() +
" | `" +
item.code +
"` | " +
item.title +
" | " +
effort_name(item.effort) +
" | " +
item.action +
" |",
)
}
lines.join("\n")
}