use rto_graph::{Edge, EdgeKind, FactSet, Node, NodeKind, Provenance};
use crate::adr::{Section, WikiLink, resolve_target};
use crate::text::first_h1;
const MARKER: &str = "— Technical Implementation Plan";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlueprintDoc {
pub path: String,
pub title: String,
pub sections: Vec<Section>,
pub links: Vec<WikiLink>,
}
impl BlueprintDoc {
#[must_use]
pub fn key(&self) -> String {
format!("blueprint:{}", self.path)
}
#[must_use]
pub fn facts(&self) -> FactSet {
let key = self.key();
let mut node = Node::new(
key.clone(),
NodeKind::Other("blueprint".into()),
self.title.clone(),
)
.with_provenance(Provenance::Authored);
node.path = Some(self.path.clone());
let mut fs = FactSet::new().with_node(node);
for section in &self.sections {
let skey = format!("{key}#{}", section.slug);
let mut snode = Node::new(
skey.clone(),
NodeKind::Other("blueprint_section".into()),
section.title.clone(),
)
.with_provenance(Provenance::Authored);
snode.path = Some(self.path.clone());
fs = fs.with_node(snode).with_edge(Edge::authored(
key.clone(),
skey,
EdgeKind::Contains,
));
}
fs
}
}
#[must_use]
pub fn is_blueprint(rel_path: &str, text: &str) -> bool {
let lower = rel_path.to_ascii_lowercase();
lower.starts_with("docs/blueprint") || first_h1(text).is_some_and(|h| h.contains(MARKER))
}
#[must_use]
pub fn parse_blueprint(rel_path: &str, text: &str) -> BlueprintDoc {
let key = format!("blueprint:{rel_path}");
let title = first_h1(text).unwrap_or_else(|| stem_of(rel_path));
let mut sections = Vec::new();
let mut links = Vec::new();
let mut current: Option<String> = None;
let mut in_fence = false;
for line in text.lines() {
if line.trim_start().starts_with("```") {
in_fence = !in_fence;
continue;
}
if in_fence {
continue;
}
if let Some(heading) = line.strip_prefix("## ") {
let title = heading.trim().to_owned();
let slug = crate::text::slugify(&title);
current = Some(slug.clone());
sections.push(Section {
slug,
title,
text: String::new(),
});
}
for raw in crate::text::scan_wiki_links(line) {
let from = match ¤t {
Some(slug) => format!("{key}#{slug}"),
None => key.clone(),
};
if let Some(target_key) = resolve_target(&raw) {
links.push(WikiLink {
from,
raw,
target_key,
});
}
}
}
BlueprintDoc {
path: rel_path.to_owned(),
title,
sections,
links,
}
}
fn stem_of(path: &str) -> String {
let name = path.rsplit('/').next().unwrap_or(path);
name.rsplit_once('.')
.map_or(name, |(stem, _)| stem)
.to_owned()
}
#[cfg(test)]
mod tests {
use super::{is_blueprint, parse_blueprint};
use rto_graph::{EdgeKind, NodeKind};
const BP: &str = "# Token flow — Technical Implementation Plan\n\n\
Grounded in: [[docs/adr/0004-x.md]].\n\n\
> **Status.** Design → build.\n\n\
## 1. Crate placement\n\n\
Touches [[crates/rto-graph/src/store.rs#Store]].\n\n\
## 2. Design\n\n\
```\n[[not/a/real#Link]]\n```\n\nDone.\n";
#[test]
fn detects_blueprints_by_marker_or_path() {
assert!(is_blueprint("docs/plans/token.md", BP), "H1 marker");
assert!(
is_blueprint("docs/blueprint/anything.md", "# Plain\n"),
"path prefix"
);
assert!(
!is_blueprint("docs/notes/x.md", "# Just a note\n"),
"neither marker nor path"
);
assert!(
!is_blueprint(
"docs/notes/y.md",
"# Our Technical Implementation Plan overview\n"
),
"phrase without the em-dash marker is not a blueprint"
);
}
#[test]
fn detection_reads_the_h1_the_parser_sees_not_the_line() {
assert!(
is_blueprint(
"docs/plans/a.md",
"# Token flow — Technical Implementation Plan {#plan}\n"
),
"an attribute block must not hide the marker"
);
assert!(
is_blueprint(
"docs/plans/b.md",
"# Token flow — Technical *Implementation* Plan\n"
),
"emphasis is markup; the marker is still in the text a reader sees"
);
assert!(
is_blueprint(
"docs/plans/c.md",
"Token flow — Technical Implementation Plan\n===\n"
),
"a setext heading is an H1"
);
assert!(
!is_blueprint(
"docs/notes/d.md",
"# How to write one\n\n```\n# Widget — Technical Implementation Plan\n```\n"
),
"a fenced example must not classify the document that quotes it"
);
assert!(
!is_blueprint(
"docs/notes/e.md",
"```\n# Widget — Technical Implementation Plan\n```\n"
),
"a fenced `#` is not a heading at all"
);
}
#[test]
fn a_blueprint_title_falling_back_to_its_h1_carries_no_markup() {
let bp = parse_blueprint(
"docs/plans/token.md",
"# Token flow — Technical Implementation Plan {#plan}\n",
);
assert_eq!(bp.title, "Token flow — Technical Implementation Plan");
assert!(!bp.title.contains("{#"));
}
#[test]
fn parses_title_sections_and_links() {
let bp = parse_blueprint("docs/plans/token.md", BP);
assert_eq!(bp.key(), "blueprint:docs/plans/token.md");
assert_eq!(bp.title, "Token flow — Technical Implementation Plan");
let slugs: Vec<_> = bp.sections.iter().map(|s| s.slug.as_str()).collect();
assert_eq!(slugs, ["1-crate-placement", "2-design"]);
assert_eq!(bp.links.len(), 2);
assert_eq!(bp.links[0].from, "blueprint:docs/plans/token.md");
assert_eq!(bp.links[0].target_key, "file:docs/adr/0004-x.md");
assert_eq!(
bp.links[1].from,
"blueprint:docs/plans/token.md#1-crate-placement"
);
assert_eq!(
bp.links[1].target_key,
"sym:rust:crates/rto-graph/src/store.rs#Store"
);
}
#[test]
fn facts_carry_blueprint_and_section_nodes() {
let bp = parse_blueprint("docs/plans/token.md", BP);
let fs = bp.facts();
let bp_node = fs
.nodes
.iter()
.find(|n| n.key == "blueprint:docs/plans/token.md")
.expect("blueprint node");
assert_eq!(bp_node.kind, NodeKind::Other("blueprint".into()));
assert!(
fs.nodes
.iter()
.any(|n| n.kind == NodeKind::Other("blueprint_section".into())
&& n.key.ends_with("#2-design"))
);
assert_eq!(
fs.edges
.iter()
.filter(|e| e.kind == EdgeKind::Contains
&& e.src == "blueprint:docs/plans/token.md")
.count(),
2
);
}
}