use std::fs;
use std::path::Path;
use crate::test_support::SCHEMA_KNOWLEDGE;
#[allow(dead_code)]
pub(crate) fn write(root: &Path, rel: &str, body: &str) {
let path = root.join(rel);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, body).unwrap();
}
#[allow(dead_code)]
pub(crate) fn tmp() -> tempfile::TempDir {
tempfile::tempdir().unwrap()
}
#[allow(dead_code)]
pub(crate) fn relation_rows(edges: &[(&str, &[&str])]) -> String {
let mut parts: Vec<String> = Vec::new();
for (label, targets) in edges {
let (label, role) = match label
.strip_prefix("references(")
.and_then(|s| s.strip_suffix(')'))
{
Some(role) => ("references", Some(role)),
None => (*label, None),
};
let role_line = role
.map(|r| format!("role = \"{r}\"\n"))
.unwrap_or_default();
for t in *targets {
parts.push(format!(
"[[relation]]\nlabel = \"{label}\"\n{role_line}target = \"{t}\"\n"
));
}
}
parts.concat()
}
#[allow(dead_code)]
pub(crate) fn seed_slice(root: &Path, id: u32, edges: &[(&str, &[&str])]) {
write(
root,
&format!(".doctrine/slice/{id:03}/slice-{id:03}.toml"),
&format!(
"id = {id}\nslug = \"s{id}\"\ntitle = \"S{id}\"\nstatus = \"proposed\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n{}",
relation_rows(edges)
),
);
write(
root,
&format!(".doctrine/slice/{id:03}/slice-{id:03}.md"),
"scope\n",
);
}
#[allow(dead_code)]
pub(crate) fn seed_adr(root: &Path, id: u32, edges: &[(&str, &[&str])]) {
write(
root,
&format!(".doctrine/adr/{id:03}/adr-{id:03}.toml"),
&format!(
"id = {id}\nslug = \"a{id}\"\ntitle = \"A{id}\"\nstatus = \"accepted\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n{}",
relation_rows(edges)
),
);
write(
root,
&format!(".doctrine/adr/{id:03}/adr-{id:03}.md"),
"body\n",
);
}
#[allow(dead_code)]
pub(crate) fn seed_requirement(root: &Path, id: u32) {
write(
root,
&format!(".doctrine/requirement/{id:03}/requirement-{id:03}.toml"),
&format!(
"id = {id}\nslug = \"r{id}\"\ntitle = \"R{id}\"\n\
status = \"active\"\nkind = \"functional\"\n"
),
);
write(
root,
&format!(".doctrine/requirement/{id:03}/requirement-{id:03}.md"),
"r\n",
);
}
#[allow(dead_code)]
pub(crate) fn seed_knowledge(root: &Path, prefix: &str, id: u32, title: &str, status: &str) {
let record_kind = crate::knowledge::RecordKind::from_prefix(prefix)
.unwrap_or_else(|| panic!("unknown knowledge prefix: {prefix}"));
let kind_dir = record_kind.as_str();
write(
root,
&format!(".doctrine/knowledge/{kind_dir}/{id:03}/record-{id:03}.toml"),
&format!(
"schema = \"{SCHEMA_KNOWLEDGE}\"\nversion = 1\nid = {id}\nslug = \"k{id}\"\ntitle = \"{title}\"\nstatus = \"{status}\"\nrecord_kind = \"{kind_dir}\"\ncreated = \"2026-01-01\"\nupdated = \"2026-01-01\"\ntags = []\n"
),
);
write(
root,
&format!(".doctrine/knowledge/{kind_dir}/{id:03}/record-{id:03}.md"),
"body\n",
);
}
#[allow(dead_code)]
pub(crate) fn seed_spec(
root: &Path,
subtype: crate::spec::SpecSubtype,
id: u32,
members: &[&str],
interactions: &[&str],
lineage: &[(&str, &str)],
) {
use crate::spec::SpecSubtype;
let (dir, schema, kind) = match subtype {
SpecSubtype::Product => (
crate::spec::PRODUCT_SPEC_KIND.dir,
"doctrine.spec.product",
"product",
),
SpecSubtype::Tech => (
crate::spec::TECH_SPEC_KIND.dir,
"doctrine.spec.tech",
"tech",
),
};
let mut head: Vec<String> = vec![format!(
"schema = \"{schema}\"\nversion = 1\nid = {id}\nslug = \"s{id}\"\n\
title = \"S{id}\"\nstatus = \"draft\"\nkind = \"{kind}\"\ntags = []\n"
)];
for (field, target) in lineage {
head.push(format!("{field} = \"{target}\"\n"));
}
write(
root,
&format!("{dir}/{id:03}/spec-{id:03}.toml"),
&head.concat(),
);
write(root, &format!("{dir}/{id:03}/spec-{id:03}.md"), "scope\n");
let mut member_doc: Vec<String> = Vec::new();
for (i, req) in members.iter().enumerate() {
let order = i + 1;
member_doc.push(format!(
"[[member]]\nrequirement = \"{req}\"\nlabel = \"FR-{order:03}\"\norder = {order}\n"
));
}
write(
root,
&format!("{dir}/{id:03}/members.toml"),
&member_doc.concat(),
);
if subtype == SpecSubtype::Tech {
let mut edge_doc: Vec<String> = Vec::new();
for target in interactions {
edge_doc.push(format!(
"[[edge]]\ntarget = \"{target}\"\ntype = \"uses\"\n"
));
}
write(
root,
&format!("{dir}/{id:03}/interactions.toml"),
&edge_doc.concat(),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::relation::RelationLabel;
use crate::spec::SpecSubtype;
#[test]
fn seeded_fixture_round_trips_through_the_real_loaders() {
let dir = tmp();
let root = dir.path();
seed_requirement(root, 5);
seed_spec(root, SpecSubtype::Product, 1, &["REQ-005"], &[], &[]);
seed_spec(
root,
SpecSubtype::Tech,
2,
&["REQ-005"],
&["SPEC-003"],
&[("descends_from", "PRD-001"), ("parent", "SPEC-004")],
);
seed_adr(root, 1, &[("related", &["ADR-002"])]);
crate::backlog::test_support::write_fixture(
root,
crate::backlog::test_support::Fixture {
kind: crate::backlog::ItemKind::Issue,
id: 7,
slug: "round-trip",
title: "Round trip",
status: "open",
resolution: "",
tags: &[],
facet: None,
rels: None,
},
);
let prd = crate::spec::relation_edges(SpecSubtype::Product, root, 1).unwrap();
assert!(
prd.iter()
.any(|e| e.label == RelationLabel::Members && e.target == "REQ-005"),
"product spec member read back: {prd:?}"
);
let spec = crate::spec::relation_edges(SpecSubtype::Tech, root, 2).unwrap();
let has = |label: RelationLabel, target: &str| {
spec.iter().any(|e| e.label == label && e.target == target)
};
assert!(
has(RelationLabel::Members, "REQ-005"),
"tech member: {spec:?}"
);
assert!(
has(RelationLabel::Interactions, "SPEC-003"),
"tech interaction: {spec:?}"
);
assert!(
has(RelationLabel::DescendsFrom, "PRD-001"),
"tech descends_from: {spec:?}"
);
assert!(
has(RelationLabel::Parent, "SPEC-004"),
"tech parent: {spec:?}"
);
let adr = crate::governance::relation_edges(&crate::adr::ADR_KIND, root, 1).unwrap();
assert!(
adr.iter()
.any(|e| e.label == RelationLabel::Related && e.target == "ADR-002"),
"ADR non-supersedes (related) edge read back: {adr:?}"
);
let items = crate::backlog::read_all(root).unwrap();
let item = items
.iter()
.find(|i| i.kind.canonical_id(i.id) == "ISS-007")
.expect("seeded backlog item read back");
assert_eq!(item.title, "Round trip");
}
}