use std::fs;
use crate::session::layout;
pub const STARTER_TEMPLATE: &str = r#"# Research Schema
## Goal
<!-- one sentence describing what this session is for -->
## Wiki conventions
- Entity pages: `<lowercase-slug>.md` — one per significant named thing.
- Concept pages: `concept-<slug>.md` — for recurring abstractions.
- Source summaries: `source-<domain>-<slug>.md`.
- Comparisons: `cmp-<a>-vs-<b>.md`.
## What to emphasize
<!-- guidance the agent should lean on: "focus on memory model",
"cite benchmark numbers", "name the author when possible" -->
## What to deprioritize
<!-- guidance the agent should skip: "skip boilerplate code walks",
"don't restate project README marketing copy" -->
## House style
<!-- tone knobs: terseness, paragraph length, citation format,
whether to hedge claims, etc. -->
"#;
pub fn read(slug: &str) -> Option<String> {
let path = layout::session_schema_md(slug);
fs::read_to_string(&path).ok()
}
pub fn exists(slug: &str) -> bool {
layout::session_schema_md(slug).exists()
}
pub fn write(slug: &str, body: &str) -> std::io::Result<std::path::PathBuf> {
let path = layout::session_schema_md(slug);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&path, body)?;
Ok(path)
}
pub fn write_starter_if_absent(slug: &str) -> std::io::Result<bool> {
if exists(slug) {
return Ok(false);
}
write(slug, STARTER_TEMPLATE)?;
Ok(true)
}
pub fn prompt_body(slug: &str) -> Option<String> {
let body = read(slug)?;
let cleaned = strip_html_comments(&body);
let trimmed = cleaned.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
fn strip_html_comments(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut i = 0;
let bytes = s.as_bytes();
while i < bytes.len() {
if bytes[i..].starts_with(b"<!--") {
if let Some(end) = s[i + 4..].find("-->") {
i += 4 + end + 3;
continue;
}
break;
}
let ch_len = match std::str::from_utf8(&bytes[i..]).ok() {
Some(rest) => rest.chars().next().map(char::len_utf8).unwrap_or(1),
None => 1,
};
out.push_str(&s[i..i + ch_len]);
i += ch_len;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starter_template_has_expected_sections() {
for section in [
"## Goal",
"## Wiki conventions",
"## What to emphasize",
"## What to deprioritize",
"## House style",
] {
assert!(STARTER_TEMPLATE.contains(section), "missing {section}");
}
}
#[test]
fn strip_html_comments_drops_single_comment() {
let s = "before <!-- gone --> after";
assert_eq!(strip_html_comments(s), "before after");
}
#[test]
fn strip_html_comments_drops_multiline_comment() {
let s = "alpha\n<!--\n hidden\n block\n-->\nbeta";
let out = strip_html_comments(s);
assert!(out.contains("alpha"));
assert!(out.contains("beta"));
assert!(!out.contains("hidden"));
}
#[test]
fn strip_html_comments_handles_unclosed_gracefully() {
let s = "visible <!-- ...never closed";
let out = strip_html_comments(s);
assert!(out.contains("visible"));
assert!(!out.contains("never"));
}
#[test]
fn prompt_body_returns_none_when_only_placeholders() {
let stripped = strip_html_comments(STARTER_TEMPLATE);
assert!(!stripped.contains("one sentence"));
assert!(stripped.contains("## Goal"));
}
}