; Search.ahk — Recherche floue et classement (architecture §14 et Annexe B). ; Fonction pure : score déterministe, insensible aux accents, pondéré par l'usage. class Search { ; usage : Map id -> Map(count, last_used) (peut être vide) static Filter(prompts, query, usage) { q := Util.Normalize(Trim(query)) scored := [] if (q = "") { for p in prompts scored.Push(Map("prompt", p, "score", Search.UsageWeight(p, usage))) } else { words := StrSplit(q, " ") for p in prompts { total := 0 ok := true hayTitle := Util.Normalize(p.Title) hayTags := Util.Normalize(Join(p.Tags, " ")) hayCat := Util.Normalize(p.Category) hayBody := Util.Normalize(SubStr(p.Body, 1, 2000)) for w in words { if (w = "") continue s := Search.WordScore(w, hayTitle, hayTags, hayCat, hayBody) if (s = 0) { ok := false break } total += s } if ok scored.Push(Map("prompt", p, "score", total * Search.UsageWeight(p, usage))) } } ; Tri décroissant par score, égalité : dernier usage puis titre (déterminisme, Annexe B). SortScored(scored, usage) out := [] for entry in scored out.Push(entry["prompt"]) return out } static WordScore(w, title, tags, cat, body) { best := 0 best := Max(best, Search.FieldScore(w, title) * 4) best := Max(best, Search.FieldScore(w, tags) * 2) best := Max(best, Search.FieldScore(w, cat) * 2) best := Max(best, Search.FieldScore(w, body)) return best } ; Score d'un champ : sous-séquence floue ; bonus début de mot et mot entier. static FieldScore(w, field) { if (field = "") return 0 if InStr(field, w) { score := 50 ; Mot entier ou début de champ / de mot. if (SubStr(field, 1, StrLen(w)) = w) score += 50 if InStr(field, " " w) score += 40 if InStr(field, w " ") || InStr(field, " " w " ") score += 30 return score } ; Sous-séquence : toutes les lettres de w, dans l'ordre. pos := 1 loop parse w { found := InStr(field, A_LoopField, , pos) if (!found) return 0 pos := found + 1 } return 15 } static UsageWeight(p, usage) { wgt := 1.0 if (p.Pinned > 0) wgt *= 1.5 if (p.Favorite) wgt *= 1.3 if (usage is Map && usage.Has(p.Id)) { u := usage[p.Id] count := u.Has("count") ? Integer(u["count"]) : 0 if (count > 0) wgt *= 1 + (Log(1 + count) / Log(10)) * 0.2 if u.Has("last_used") { age := DateDiff(A_Now, u["last_used"], "Days") if (age <= 7) wgt *= 1.2 } } return wgt } } ; Tri simple par insertion (les listes sont petites ; la stabilité prime sur la vitesse). SortScored(scored, usage) { n := scored.Length loop n - 1 { i := A_Index loop n - i { j := A_Index a := scored[j], b := scored[j + 1] swap := false if (b["score"] > a["score"]) swap := true else if (b["score"] = a["score"]) { la := LastUsedOf(a["prompt"], usage), lb := LastUsedOf(b["prompt"], usage) if (lb > la) swap := true else if (lb = la && StrCompare(a["prompt"].Title, b["prompt"].Title) > 0) swap := true } if swap { scored[j] := b scored[j + 1] := a } } } } LastUsedOf(p, usage) { if (usage is Map && usage.Has(p.Id) && usage[p.Id].Has("last_used")) return usage[p.Id]["last_used"] return "" }