Files
kebab/crates/kebab-app/src/external.rs
altair823 685007789a style: cargo fmt --all (round 4 ingest log feature follow-up)
Phase C4 executor 의 마지막 `fix(test): clippy + fmt fixes` commit 이
test file 부분만 fmt 적용. workspace 전체 fmt 누락 발견 → cargo fmt --all
적용. 모든 import alphabetical reorder + line wrapping 정합.

추가 untracked artifact 동시 commit:
- docs/superpowers/specs/2026-05-28-v0.20-ingest-log-spec.md (491 line, ACCEPT)
- docs/superpowers/plans/2026-05-28-v0.20-ingest-log-plan.md (616 line, ACCEPT)

workspace test: 1370 passed / 0 failed / 50 ignored, ingest_log_smoke green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 04:18:40 +00:00

244 lines
9.3 KiB
Rust

//! Helpers for the `_external/` workspace subdirectory used by
//! `ingest_file_with_config` and `ingest_stdin_with_config` (p9-fb-31).
//!
//! - `ensure_external_dir`: create `<workspace.root>/_external/` if absent.
//! - `ensure_kebabignore_entry`: append `_external/` to `<workspace.root>/.kebabignore`
//! if missing — prevents subsequent `kebab ingest` workspace walks from
//! re-walking files that were imported via single-file ingest.
//! - `copy_to_external`: write bytes to `_external/<blake3-12>.<ext>`, idempotent.
//! - `inject_frontmatter`: prepend a YAML frontmatter block to a markdown body
//! string (used by `ingest_stdin_with_config`).
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
pub const EXTERNAL_DIR: &str = "_external";
const KEBABIGNORE_LINE: &str = "_external/";
/// Ensure `<workspace_root>/_external/` exists. Returns the directory path.
pub fn ensure_external_dir(workspace_root: &Path) -> Result<PathBuf> {
let dir = workspace_root.join(EXTERNAL_DIR);
fs::create_dir_all(&dir)
.with_context(|| format!("create _external dir at {}", dir.display()))?;
Ok(dir)
}
/// Append `_external/` line to `<workspace_root>/.kebabignore` if not already
/// present. Idempotent — checks for the exact line before appending.
pub fn ensure_kebabignore_entry(workspace_root: &Path) -> Result<()> {
let path = workspace_root.join(".kebabignore");
let existing = if path.exists() {
fs::read_to_string(&path)
.with_context(|| format!("read existing .kebabignore at {}", path.display()))?
} else {
String::new()
};
let already = existing.lines().any(|line| line.trim() == KEBABIGNORE_LINE);
if already {
return Ok(());
}
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.with_context(|| format!("open .kebabignore for append at {}", path.display()))?;
if !existing.is_empty() && !existing.ends_with('\n') {
file.write_all(b"\n")?;
}
writeln!(file, "{KEBABIGNORE_LINE}")?;
Ok(())
}
/// Copy bytes to `<external_dir>/<blake3-12>.<ext>`. Idempotent — if the
/// destination file already exists with the expected hash, the existing
/// file is reused (no second write). Returns the destination path.
pub fn copy_to_external(external_dir: &Path, bytes: &[u8], ext: &str) -> Result<PathBuf> {
let hash = blake3::hash(bytes);
let hex = hash.to_hex();
let prefix = &hex.as_str()[..12];
let filename = format!("{prefix}.{ext}");
let dest = external_dir.join(&filename);
if !dest.exists() {
fs::write(&dest, bytes)
.with_context(|| format!("write external file at {}", dest.display()))?;
}
Ok(dest)
}
/// Prepend a YAML frontmatter block to a markdown body. Returns the wrapped
/// markdown string. Errors if `body` already starts with `---` (the user
/// should use `ingest_file_with_config` for files that already carry
/// frontmatter).
///
/// Internal `yaml_quote` always uses double-quoted YAML form with backslash
/// escapes for `"` / `\` / control chars — agent-supplied titles with
/// special characters are safe.
pub fn inject_frontmatter(body: &str, title: &str, source_uri: Option<&str>) -> Result<String> {
let head = body.trim_start();
if head.starts_with("---\n") || head.starts_with("---\r\n") || head.starts_with("---\r") {
anyhow::bail!(
"stdin already has frontmatter; use `kebab ingest-file` for files with metadata"
);
}
let title_yaml = yaml_quote(title);
let mut header = String::new();
header.push_str("---\n");
header.push_str(&format!("title: {title_yaml}\n"));
if let Some(uri) = source_uri {
let uri_yaml = yaml_quote(uri);
header.push_str(&format!("source_uri: {uri_yaml}\n"));
}
header.push_str("---\n\n");
header.push_str(body);
Ok(header)
}
/// YAML-quote a string. Always uses double-quoted form with backslash-escape
/// for `"` and `\`. Defensive against agent-supplied titles that contain
/// quotes / control chars.
fn yaml_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn ensure_external_dir_creates_dir() {
let dir = tempdir().unwrap();
let result = ensure_external_dir(dir.path()).unwrap();
assert_eq!(result, dir.path().join("_external"));
assert!(result.is_dir());
}
#[test]
fn ensure_external_dir_is_idempotent() {
let dir = tempdir().unwrap();
let _ = ensure_external_dir(dir.path()).unwrap();
let result = ensure_external_dir(dir.path()).unwrap();
assert!(result.is_dir());
}
#[test]
fn ensure_kebabignore_entry_creates_file_with_line() {
let dir = tempdir().unwrap();
ensure_kebabignore_entry(dir.path()).unwrap();
let content = fs::read_to_string(dir.path().join(".kebabignore")).unwrap();
assert!(content.lines().any(|l| l.trim() == "_external/"));
}
#[test]
fn ensure_kebabignore_entry_appends_to_existing() {
let dir = tempdir().unwrap();
fs::write(dir.path().join(".kebabignore"), "*.tmp\n").unwrap();
ensure_kebabignore_entry(dir.path()).unwrap();
let content = fs::read_to_string(dir.path().join(".kebabignore")).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert!(lines.contains(&"*.tmp"));
assert!(lines.contains(&"_external/"));
}
#[test]
fn ensure_kebabignore_entry_idempotent() {
let dir = tempdir().unwrap();
ensure_kebabignore_entry(dir.path()).unwrap();
ensure_kebabignore_entry(dir.path()).unwrap();
let content = fs::read_to_string(dir.path().join(".kebabignore")).unwrap();
let count = content.lines().filter(|l| l.trim() == "_external/").count();
assert_eq!(count, 1, "should not duplicate");
}
#[test]
fn ensure_kebabignore_entry_handles_missing_trailing_newline() {
let dir = tempdir().unwrap();
fs::write(dir.path().join(".kebabignore"), "*.tmp").unwrap(); // no \n
ensure_kebabignore_entry(dir.path()).unwrap();
let content = fs::read_to_string(dir.path().join(".kebabignore")).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert!(lines.contains(&"*.tmp"));
assert!(lines.contains(&"_external/"));
}
#[test]
fn copy_to_external_writes_with_hash_prefix_filename() {
let dir = tempdir().unwrap();
let ext_dir = ensure_external_dir(dir.path()).unwrap();
let path = copy_to_external(&ext_dir, b"hello", "md").unwrap();
assert!(path.exists());
assert!(path.file_name().unwrap().to_string_lossy().ends_with(".md"));
let stem = path.file_stem().unwrap().to_string_lossy();
assert_eq!(stem.len(), 12);
}
#[test]
fn copy_to_external_is_idempotent_for_same_bytes() {
let dir = tempdir().unwrap();
let ext_dir = ensure_external_dir(dir.path()).unwrap();
let p1 = copy_to_external(&ext_dir, b"hello", "md").unwrap();
let p2 = copy_to_external(&ext_dir, b"hello", "md").unwrap();
assert_eq!(p1, p2);
}
#[test]
fn copy_to_external_different_bytes_produce_different_filenames() {
let dir = tempdir().unwrap();
let ext_dir = ensure_external_dir(dir.path()).unwrap();
let p1 = copy_to_external(&ext_dir, b"hello", "md").unwrap();
let p2 = copy_to_external(&ext_dir, b"world", "md").unwrap();
assert_ne!(p1, p2);
}
#[test]
fn inject_frontmatter_basic() {
let out = inject_frontmatter("## Body", "Article X", None).unwrap();
assert!(out.starts_with("---\ntitle: \"Article X\"\n---\n\n## Body"));
}
#[test]
fn inject_frontmatter_with_source_uri() {
let out = inject_frontmatter("## Body", "X", Some("https://example.com/x")).unwrap();
assert!(out.contains("title: \"X\""));
assert!(out.contains("source_uri: \"https://example.com/x\""));
assert!(out.contains("\n## Body"));
}
#[test]
fn inject_frontmatter_errors_on_existing_frontmatter() {
let body = "---\ntitle: Existing\n---\n\n## Body";
let err = inject_frontmatter(body, "New", None).unwrap_err();
assert!(err.to_string().contains("already has frontmatter"));
}
#[test]
fn inject_frontmatter_errors_on_existing_frontmatter_crlf() {
let body = "---\r\ntitle: Existing\r\n---\r\n\r\n## Body";
let err = inject_frontmatter(body, "New", None).unwrap_err();
assert!(err.to_string().contains("already has frontmatter"));
}
#[test]
fn yaml_quote_escapes_quotes_and_backslashes() {
assert_eq!(yaml_quote("hello \"world\""), "\"hello \\\"world\\\"\"");
assert_eq!(yaml_quote("path\\to"), "\"path\\\\to\"");
assert_eq!(yaml_quote("line\nbreak"), "\"line\\nbreak\"");
}
}