KeiSeiKit-1.0/_primitives/_rust/kei-task/src/atoms/search.rs
Parfii-bot ae82bc6242 feat(stream-b): kei-task pilot — 3 atoms (create/search/add-dependency)
Pilot refactor per locked substrate schema. kei-task migrated to atom
layout:

- atoms/<verb>.md — YAML frontmatter + human body for 3 verbs
- atoms/schemas/<verb>-{input,output}.json — JSON Schema draft-07
- src/atoms/<verb>.rs — typed Input/Output/Error + pub fn run()
- src/atoms/mod.rs — module registry
- Cargo.toml [package.metadata.keisei] — crate-level substrate data
- src/main.rs — dispatcher for 3 pilot commands via atoms::

Zero behaviour change: 7/7 integration tests pass before and after
(create_and_get, update_persists, cycle_detected, milestone_linking,
dependency_chain_traversal, task_graph_edges, search_finds_task).

main.rs still has 5 non-migrated subcommands (update, graph,
dependency-chain, milestone, link-milestone) — scope discipline, they
migrate in later passes. main.rs 120 → 132 LOC.

Stream B pilot reference — other crates follow this pattern in v0.24+.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:09:55 +08:00

79 lines
2.1 KiB
Rust

//! kei-task::search atom — see atoms/search.md for contract.
use crate::search as search_impl;
use crate::store::Store;
use crate::types::Task;
use serde::{Deserialize, Serialize};
use std::fmt;
const DEFAULT_LIMIT: i64 = 20;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Input {
pub query: String,
#[serde(default)]
pub limit: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchHit {
pub id: i64,
pub title: String,
pub description: String,
pub status: String,
pub priority: String,
pub task_type: String,
pub parent_id: i64,
pub assigned_to: String,
pub due_date: i64,
pub completed_at: i64,
pub created_at: i64,
pub updated_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Output {
pub results: Vec<SearchHit>,
}
#[derive(Debug)]
pub enum Error {
InvalidQuery,
StoreError(anyhow::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InvalidQuery => write!(f, "InvalidQuery: query must be non-empty"),
Error::StoreError(e) => write!(f, "StoreError: {e:#}"),
}
}
}
impl std::error::Error for Error {}
pub fn run(store: &Store, input: Input) -> Result<Output, Error> {
if input.query.trim().is_empty() {
return Err(Error::InvalidQuery);
}
let limit = normalize_limit(input.limit);
let hits = search_impl::search(store, &input.query, limit).map_err(Error::StoreError)?;
Ok(Output { results: hits.into_iter().map(task_to_hit).collect() })
}
fn normalize_limit(raw: Option<i64>) -> i64 {
match raw {
Some(n) if n > 0 => n,
_ => DEFAULT_LIMIT,
}
}
fn task_to_hit(t: Task) -> SearchHit {
SearchHit {
id: t.id, title: t.title, description: t.description, status: t.status,
priority: t.priority, task_type: t.task_type, parent_id: t.parent_id,
assigned_to: t.assigned_to, due_date: t.due_date, completed_at: t.completed_at,
created_at: t.created_at, updated_at: t.updated_at,
}
}