Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit 273bc6b

Browse files
z23ccclaude
andcommitted
fix(flowctl): use vector_distance_cos for skill matching
Replace vector_top_k (requires ANN index, fails in embedded libSQL) with vector_distance_cos (exact search, no index needed). Works unconditionally in embedded mode. Performance is <1ms for 30 skills. Also lower default threshold from 0.75 to 0.70 based on testing — "deprecate old module" scores 0.73 against flow-code-deprecation. Tested: all 5 new skills match correctly with relevant queries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 75984a0 commit 273bc6b

3 files changed

Lines changed: 26 additions & 13 deletions

File tree

flowctl/crates/flowctl-cli/src/commands/skill.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub enum SkillCmd {
3333
#[arg(long, default_value = "5")]
3434
limit: usize,
3535
/// Minimum cosine similarity threshold.
36-
#[arg(long, default_value = "0.75")]
36+
#[arg(long, default_value = "0.70")]
3737
threshold: f64,
3838
},
3939
}

flowctl/crates/flowctl-db/src/skill.rs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -112,27 +112,33 @@ impl SkillRepo {
112112
};
113113
let lit = vec_to_literal(&vec);
114114

115+
// Use vector_distance_cos() instead of vector_top_k() — works without
116+
// a vector index (no ANN index required in embedded mode). Exact search
117+
// via full table scan; perfectly fast for <10,000 rows (~30 skills).
115118
let rows_result = self
116119
.conn
117120
.query(
118-
"SELECT s.name, s.description, top.distance
119-
FROM vector_top_k('skills_emb_idx', vector32(?1), ?2) AS top
120-
JOIN skills s ON s.rowid = top.id",
121+
"SELECT s.name, s.description,
122+
vector_distance_cos(s.embedding, vector32(?1)) AS distance
123+
FROM skills s
124+
WHERE s.embedding IS NOT NULL
125+
ORDER BY distance ASC
126+
LIMIT ?2",
121127
params![lit, limit as i64],
122128
)
123129
.await;
124130

125131
let mut rows = match rows_result {
126132
Ok(r) => r,
127-
Err(_) => return Ok(vec![]), // vector index unavailable
133+
Err(_) => return Ok(vec![]), // vector functions unavailable
128134
};
129135

130136
let mut out = Vec::new();
131137
while let Some(row) = rows.next().await? {
132138
let dist: f64 = row.get(2)?;
133-
// Convert L2 distance to cosine similarity.
134-
// For unit-norm vectors: cos_sim = 1 - (L2^2 / 2).
135-
let score = 1.0 - (dist * dist / 2.0);
139+
// Cosine distance cosine similarity: sim = 1 - dist
140+
// (0 = identical, 1 = orthogonal, 2 = opposite)
141+
let score = 1.0 - dist;
136142
if score >= threshold {
137143
out.push(SkillMatch {
138144
name: row.get::<String>(0)?,
@@ -231,8 +237,9 @@ mod tests {
231237
assert!(matches.len() <= 5);
232238
}
233239

234-
/// Semantic match end-to-end. Gated behind `#[ignore]` because the
235-
/// first run downloads the BGE-small model (~130MB).
240+
/// Semantic match end-to-end using vector_distance_cos (no index needed).
241+
/// Gated behind `#[ignore]` because the first run downloads the
242+
/// BGE-small model (~130MB).
236243
#[tokio::test]
237244
#[ignore = "requires fastembed model (~130MB); run with --ignored"]
238245
async fn test_match_skills_semantic() {
@@ -248,13 +255,19 @@ mod tests {
248255
.expect("upsert");
249256

250257
let matches = repo
251-
.match_skills("architecture design", 2, 0.3)
258+
.match_skills("architecture design", 3, 0.3)
252259
.await
253260
.expect("match_skills");
254261
assert!(!matches.is_empty(), "expected at least one match");
262+
// "plan" (Design and architect...) should be the best match
255263
assert_eq!(
256264
matches[0].name, "plan",
257-
"expected 'plan' as best match for architecture query"
265+
"expected 'plan' as best match for architecture query, got '{}'",
266+
matches[0].name
258267
);
268+
// Scores should be between 0 and 1
269+
for m in &matches {
270+
assert!(m.score > 0.0 && m.score <= 1.0, "score out of range: {}", m.score);
271+
}
259272
}
260273
}

skills/flow-code-plan/steps.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ After clarity check, match the request against registered engineering discipline
6666
1. Translate the request to English keywords (if not already English). This costs zero tokens — you're already processing the request.
6767
2. Run:
6868
```bash
69-
$FLOWCTL skill match "<english keywords>" --threshold 0.75 --limit 3 --json
69+
$FLOWCTL skill match "<english keywords>" --threshold 0.70 --limit 3 --json
7070
```
7171
3. If matches found (non-empty JSON array): save them for Step 5 (task spec writing). Each matched skill will be referenced in the task's Approach section.
7272
4. If empty result, error, or embedder unavailable: skip silently. Skill routing is advisory, never blocking.

0 commit comments

Comments
 (0)