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>
79 lines
2.1 KiB
Rust
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,
|
|
}
|
|
}
|