Skip to content

feat(persona): fuzzy --theme/--persona resolution + 'persona list <theme>'#59

Merged
arcaven merged 2 commits intodevelopfrom
feat/fuzzy-persona-theme-resolution
Apr 19, 2026
Merged

feat(persona): fuzzy --theme/--persona resolution + 'persona list <theme>'#59
arcaven merged 2 commits intodevelopfrom
feat/fuzzy-persona-theme-resolution

Conversation

@arcaven
Copy link
Copy Markdown
Collaborator

@arcaven arcaven commented Apr 19, 2026

Summary

Replaces exact-slug-only lookup with a three-tier match: exact → unique case-insensitive prefix → fuzzy subsequence (via nucleo-matcher, Helix editor's matcher). Makes the ~100 themes + ~1050 characters accessible without memorising canonical slugs.

Depends on #58 (legacy role retired — merged).

Design (session-032)

Resolution order (match_slug):

  1. Exact slug (case-insensitive) — always wins.
  2. Unique prefix — kubectl-style partial ID (discdiscworld).
  3. Fuzzy subsequence with two thresholds:
    • >= MIN_FUZZY_SCORE AND gap >= FUZZY_AMBIGUITY_GAP vs rank-2 → FuzzyUnique
    • >= MIN_FUZZY_SCORE but tight with rank-2 → FuzzyAmbiguous (proceed with top, warn on stderr with candidates)
    • below MIN_FUZZY_SCORENotFound (caller errors, shows candidates)

Two-phase resolver (resolve_theme_and_persona):

  • Resolve --theme first. When it succeeds, narrow --persona matching to that theme's ~10 characters. ~100× fewer candidates drops ambiguity to near-zero.
  • If --theme is missing or unresolvable and --persona is given, search characters globally and back-propagate the matched character's home theme. stderr warning names the override.

What users can now type

Input Resolves to
--theme disc --persona grny discworld + granny-weatherwax
--theme discworld --persona lhv discworld + lord-havelock-vetinari (initials)
--persona granny-weatherwax (no theme) discworld + granny-weatherwax (back-prop)
--persona mvl discworld + moist-von-lipwig
persona list disc discworld's 11-character roster
persona show disc --agent ponder ponder-stibbons card

New

  • src/resolve.rsMatchResult<T> enum, match_slug, match_theme, match_character_in_theme, match_character_globally, resolve_theme_and_persona. 15 unit tests.
  • persona list <theme> subcommand — lists characters in a theme with slug + name + one-line style. Without arg, lists themes (unchanged).
  • nucleo-matcher v0.3.1 — ~200KB, pure Rust, no C deps.

Wired into

  • Top-level CLI flags (--theme, --persona): resolved before cli_overrides build, so config merge sees canonical slugs only.
  • persona show <theme> — theme slug fuzzy-resolved.
  • persona show <theme> --agent <slug> — character slug fuzzy-resolved within theme roster.
  • persona list <theme> — theme slug fuzzy-resolved.

Test plan

  • cargo test --release --lib — 150 pass (132 pre-existing + 3 from PR 58 + 15 new)
  • cargo clippy --release --all-targets -- -D warnings — clean
  • cargo build --release — clean
  • Manual: persona list disc → shows discworld roster
  • Manual: persona show disc --agent grny → Granny's card
  • Manual: persona show disc --agent lhv → Vetinari (initials)
  • Manual: --theme disc --persona grny configtheme = "discworld", character = "granny-weatherwax"
  • Manual: --persona al --theme 1984 (no match) → stderr warning + hard error with candidates
  • Manual: persona show xxx → hard error with hint (theme subcommand doesn't back-prop — design choice, only top-level CLI does)

Known behavior worth naming

  • Short queries that don't score above MIN_FUZZY_SCORE (e.g. dw against a 5-theme pool) return NotFound rather than a weak match. Users can be more specific or use persona list to browse.
  • Cross-theme name collisions surface via the ambiguity warning. Example: --persona starbuck resolves to Moby Dick's Starbuck (exact slug match) rather than Battlestar Galactica's Kara Thrace (whose slug is kara-starbuck-thrace). This is the B14 "persona = character slug" model doing its job.

Refs

  • bd: aae-orc-jwqz (P2 feature)
  • Follows PR 57 + PR 58 from session-032.

arcaven added 2 commits April 18, 2026 23:54
Replaces exact-slug-only lookup with a three-tier match: exact →
unique case-insensitive prefix → fuzzy subsequence (nucleo-matcher,
Helix editor's matcher). Makes the ~100 themes + ~1050 characters
accessible without memorising canonical slugs.

Per session-032 design (aae-orc-jwqz):

Resolution order (match_slug):
- Exact slug (case-insensitive) — always wins.
- Unique prefix — kubectl-style partial ID.
- Fuzzy subsequence with score thresholds:
    >= MIN_FUZZY_SCORE and gap >= FUZZY_AMBIGUITY_GAP vs rank-2
      → FuzzyUnique
    >= MIN_FUZZY_SCORE but tight with rank-2
      → FuzzyAmbiguous (proceed with top, warn on stderr)
    below MIN_FUZZY_SCORE → NotFound (caller errors with candidates)

Two-phase resolver (resolve_theme_and_persona):
- Resolve --theme first. When it succeeds, narrow --persona matching
  to that theme's ~10 characters — ~100× fewer candidates → near-zero
  ambiguity.
- If --theme is missing or unresolvable and --persona is given,
  search characters globally and back-propagate the matched
  character's home theme. stderr warning explains the override.

New:
- src/resolve.rs — MatchResult<T>, match_slug/match_theme/
  match_character_in_theme/match_character_globally, and the two-phase
  resolve_theme_and_persona. 15 unit tests covering exact/prefix/fuzzy
  hits, empty queries, garbage input, theme-narrowed + global paths,
  theme-only / persona-only / back-prop variants.
- persona list <theme> subcommand — lists characters in a theme with
  slug + character name + one-line style truncation. Without arg,
  lists themes (unchanged).
- nucleo-matcher v0.3.1 dependency (Helix editor's matcher, ~200KB,
  pure Rust).

Wired into:
- Top-level CLI: resolve_theme_and_persona called before cli_overrides
  build, so --theme/--persona values reaching the config merge are
  already canonical.
- persona show <theme>: theme slug fuzzy-resolved.
- persona show <theme> --agent <slug>: character slug fuzzy-resolved
  within the theme's roster.
- persona list <theme>: theme slug fuzzy-resolved.

All 150 lib tests pass (132 pre-existing + 3 from PR 58 + 15 new);
clippy clean under -D warnings.

Refs: aae-orc-jwqz
Two CI failures surfaced on PR #59:

1. cargo-deny license check — nucleo-matcher is MPL-2.0, which is
   weak copyleft and not in forestage's deny.toml allow list (policy
   is permissive-licenses-only, 'Adding a new license requires
   explicit review'). This is by design.

2. clippy::unnecessary_sort_by on scored.sort_by(|a, b| b.1.cmp(&a.1)).

Fix: swap to fuzzy-matcher v0.3.7 (skim's own matcher, by lotabout,
MIT licensed). Same subsequence-matching behavior; simpler API
(no Utf32Str buffer plumbing). Scoring range shifts so thresholds
retune: MIN_FUZZY_SCORE 60→40, FUZZY_AMBIGUITY_GAP 20→15, score type
u16→i64 (fuzzy-matcher's native). All 17 resolve tests still pass,
and the empirical probe confirms realistic queries still work:
  --persona mvl        → moist-von-lipwig
  --theme disc --persona lhv → lord-havelock-vetinari
  --persona grny       → granny-weatherwax

sort_by replaced with sort_by_key(|(_,s)| Reverse(*s)) — resolves the
clippy lint.

All 150 lib tests pass; clippy clean; cargo-deny license check clean.

Refs: aae-orc-jwqz
@arcaven arcaven merged commit 07c1b59 into develop Apr 19, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant