#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Suggest,
Warn,
Block,
}
impl Severity {
pub fn as_str(&self) -> &'static str {
match self {
Self::Info => "info",
Self::Suggest => "suggest",
Self::Warn => "warn",
Self::Block => "block",
}
}
}
impl Default for Severity {
fn default() -> Self {
Self::Info
}
}
impl FromStr for Severity {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"info" => Ok(Self::Info),
"suggest" => Ok(Self::Suggest),
"warn" | "warning" => Ok(Self::Warn),
"block" | "blocker" => Ok(Self::Block),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Priority {
Info,
Preference,
Strong,
Constraint,
}
impl Priority {
pub fn as_str(&self) -> &'static str {
match self {
Self::Info => "info",
Self::Preference => "preference",
Self::Strong => "strong",
Self::Constraint => "constraint",
}
}
}
impl Default for Priority {
fn default() -> Self {
Self::Info
}
}
impl FromStr for Priority {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"info" => Ok(Self::Info),
"preference" => Ok(Self::Preference),
"strong" => Ok(Self::Strong),
"constraint" => Ok(Self::Constraint),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Scope {
User,
Project,
Tech,
}
impl Scope {
pub fn as_str(&self) -> &'static str {
match self {
Self::User => "user",
Self::Project => "project",
Self::Tech => "tech",
}
}
}
impl FromStr for Scope {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"user" => Ok(Self::User),
"project" => Ok(Self::Project),
"tech" => Ok(Self::Tech),
_ => Err(()),
}
}
}
pub fn sanitize_severity(claimed: Severity, advice_text: &str) -> Severity {
let text = advice_text.to_lowercase();
match claimed {
Severity::Block => {
if has_block_evidence(&text) {
Severity::Block
} else {
Severity::Warn
}
}
Severity::Warn => {
if has_warn_evidence(&text) {
Severity::Warn
} else {
Severity::Suggest
}
}
other => other,
}
}
fn has_block_evidence(text_lower: &str) -> bool {
const SIGNALS: &[&str] = &[
"build fail", "compile error", "compilation error", "syntax error",
"type error", "import error", "module not found", "undefined",
"cannot find", "panic", "crash",
"빌드 실패", "컴파일 오류", "컴파일 에러", "타입 오류", "타입 에러",
"구문 오류", "구문 에러", "동작 안", "작동 안", "실행 안",
];
SIGNALS.iter().any(|s| text_lower.contains(s))
}
fn has_warn_evidence(text_lower: &str) -> bool {
const SIGNALS: &[&str] = &[
"missing", "leak", "undefined behavior", "race", "deadlock",
"incorrect", "wrong", "bug", "vulnerability", "security",
"deprecated", "warning",
"누락", "유출", "버그", "잘못", "오류", "에러", "위험",
"취약", "보안", "문제", "주의",
];
SIGNALS.iter().any(|s| text_lower.contains(s))
}
pub fn default_priority_for_source(source: &str) -> Priority {
match source {
"user" => Priority::Preference, _ => Priority::Info, }
}
pub fn infer_scope(text: &str, current_project: Option<&str>) -> Scope {
let lower = text.to_lowercase();
if let Some(p) = current_project {
if lower.contains(&p.to_lowercase()) {
return Scope::Project;
}
}
let has_tech_keyword = TECH_KEYWORDS.iter().any(|k| lower.contains(k));
let has_personal_or_project_signal = PERSONAL_SIGNALS.iter().any(|k| lower.contains(k))
|| current_project
.map(|p| lower.contains(&p.to_lowercase()))
.unwrap_or(false);
if has_tech_keyword && !has_personal_or_project_signal {
return Scope::Tech;
}
Scope::User
}
const TECH_KEYWORDS: &[&str] = &[
"react", "rust", "typescript", "python", "supabase", "postgres",
"node", "vite", "tailwind", "useeffect", "usestate", "trait",
"async", "tokio", "axum", "javascript", "go ", "java ",
];
const PERSONAL_SIGNALS: &[&str] = &[
"alice", "bob", "user", "사용자", "본인", "i ", " me ", " my ",
"내가", "나는", "저는", "제가",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ClassifiedField {
Severity,
Priority,
Scope,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OverrideRecord {
pub memory_or_event_id: String,
pub field: ClassifiedField,
pub original: String,
pub corrected_to: String,
pub at: chrono::DateTime<chrono::Utc>,
pub by_user_id: String,
}
impl OverrideRecord {
pub fn new(
id: impl Into<String>,
field: ClassifiedField,
original: impl Into<String>,
corrected_to: impl Into<String>,
by_user_id: impl Into<String>,
) -> Self {
Self {
memory_or_event_id: id.into(),
field,
original: original.into(),
corrected_to: corrected_to.into(),
at: chrono::Utc::now(),
by_user_id: by_user_id.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn block_with_evidence_stays() {
assert_eq!(
sanitize_severity(Severity::Block, "build fails because of missing import"),
Severity::Block
);
}
#[test]
fn block_without_evidence_downgrades() {
assert_eq!(
sanitize_severity(Severity::Block, "이 함수 더 짧게 쓸 수 있어"),
Severity::Warn
);
}
#[test]
fn block_korean_evidence_stays() {
assert_eq!(
sanitize_severity(Severity::Block, "빌드 실패 — 타입 오류 있음"),
Severity::Block
);
}
#[test]
fn warn_with_evidence_stays() {
assert_eq!(
sanitize_severity(Severity::Warn, "여기 cleanup 누락 가능"),
Severity::Warn
);
}
#[test]
fn warn_without_evidence_downgrades() {
assert_eq!(
sanitize_severity(Severity::Warn, "이 함수 이름 좀 바꿔도 괜찮을 듯"),
Severity::Suggest
);
}
#[test]
fn suggest_unchanged() {
assert_eq!(
sanitize_severity(Severity::Suggest, "anything here"),
Severity::Suggest
);
}
#[test]
fn user_source_starts_at_preference() {
assert_eq!(default_priority_for_source("user"), Priority::Preference);
}
#[test]
fn asurada_source_starts_at_info() {
assert_eq!(default_priority_for_source("asurada"), Priority::Info);
}
#[test]
fn project_name_in_text_means_project_scope() {
assert_eq!(
infer_scope("Devist 는 Rust 만 쓴다", Some("Devist")),
Scope::Project
);
}
#[test]
fn tech_keyword_only_means_tech_scope() {
assert_eq!(
infer_scope("useEffect cleanup 함수가 필요하다", None),
Scope::Tech
);
}
#[test]
fn personal_signal_means_user_scope() {
assert_eq!(
infer_scope("alice는 짧은 함수를 선호한다", None),
Scope::User
);
}
#[test]
fn ambiguous_defaults_to_user() {
assert_eq!(infer_scope("뭔가 일반적인 메모", None), Scope::User);
}
#[test]
fn enum_string_roundtrip() {
for s in &[Severity::Info, Severity::Suggest, Severity::Warn, Severity::Block] {
assert_eq!(s.as_str().parse::<Severity>().unwrap(), *s);
}
for p in &[Priority::Info, Priority::Preference, Priority::Strong, Priority::Constraint] {
assert_eq!(p.as_str().parse::<Priority>().unwrap(), *p);
}
for sc in &[Scope::User, Scope::Project, Scope::Tech] {
assert_eq!(sc.as_str().parse::<Scope>().unwrap(), *sc);
}
}
#[test]
fn override_record_constructs() {
let rec = OverrideRecord::new(
"mem-123",
ClassifiedField::Priority,
"info",
"strong",
"alice",
);
assert_eq!(rec.field, ClassifiedField::Priority);
assert_eq!(rec.corrected_to, "strong");
}
}