#![cfg_attr(
not(test),
expect(
dead_code,
reason = "SL-048 PHASE-02 — RELATION_RULES table + GovernedBy/Consumes built ahead of their PHASE-03/04 consumers; self-clears when wired"
)
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum RelationLabel {
Specs,
Requirements,
Supersedes,
DescendsFrom,
Parent,
Members,
Interactions,
GovernedBy,
Consumes,
Slices,
Related,
Reviews,
OwningSlice,
Drift,
DecisionRef,
}
impl RelationLabel {
pub(crate) const fn name(self) -> &'static str {
match self {
RelationLabel::Specs => "specs",
RelationLabel::Requirements => "requirements",
RelationLabel::Supersedes => "supersedes",
RelationLabel::DescendsFrom => "descends_from",
RelationLabel::Parent => "parent",
RelationLabel::Members => "members",
RelationLabel::Interactions => "interactions",
RelationLabel::GovernedBy => "governed_by",
RelationLabel::Consumes => "consumes",
RelationLabel::Slices => "slices",
RelationLabel::Related => "related",
RelationLabel::Reviews => "reviews",
RelationLabel::OwningSlice => "owning_slice",
RelationLabel::Drift => "drift",
RelationLabel::DecisionRef => "decision_ref",
}
}
pub(crate) fn from_name(name: &str) -> Option<RelationLabel> {
let label = match name {
"specs" => RelationLabel::Specs,
"requirements" => RelationLabel::Requirements,
"supersedes" => RelationLabel::Supersedes,
"descends_from" => RelationLabel::DescendsFrom,
"parent" => RelationLabel::Parent,
"members" => RelationLabel::Members,
"interactions" => RelationLabel::Interactions,
"governed_by" => RelationLabel::GovernedBy,
"consumes" => RelationLabel::Consumes,
"slices" => RelationLabel::Slices,
"related" => RelationLabel::Related,
"reviews" => RelationLabel::Reviews,
"owning_slice" => RelationLabel::OwningSlice,
"drift" => RelationLabel::Drift,
"decision_ref" => RelationLabel::DecisionRef,
_ => return None,
};
debug_assert_eq!(label.name(), name);
Some(label)
}
}
use crate::entity::Kind;
#[derive(Clone, Copy)]
pub(crate) enum TargetSpec {
Kinds(&'static [&'static Kind]),
SameKind,
AnyNumbered,
Unvalidated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Tier {
One,
Typed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LinkPolicy {
Writable,
LifecycleOnly,
TypedVerbOnly,
}
#[derive(Clone, Copy)]
pub(crate) struct RelationRule {
pub(crate) sources: &'static [&'static Kind],
pub(crate) label: RelationLabel,
pub(crate) inbound_name: &'static str,
pub(crate) target: TargetSpec,
pub(crate) tier: Tier,
pub(crate) link: LinkPolicy,
}
const SLICE: &Kind = &crate::slice::SLICE_KIND;
const PRD: &Kind = &crate::spec::PRODUCT_SPEC_KIND;
const SPEC: &Kind = &crate::spec::TECH_SPEC_KIND;
const REQ: &Kind = &crate::requirement::REQUIREMENT_KIND;
const ADR: &Kind = &crate::adr::ADR_KIND.kind;
const POL: &Kind = &crate::policy::POLICY_KIND.kind;
const STD: &Kind = &crate::standard::STANDARD_KIND.kind;
const RV: &Kind = &crate::review::REVIEW_KIND;
const REC: &Kind = &crate::rec::REC_KIND;
const ISS: &Kind = &crate::backlog::ISSUE_KIND;
const IMP: &Kind = &crate::backlog::IMPROVEMENT_KIND;
const CHR: &Kind = &crate::backlog::CHORE_KIND;
const RSK: &Kind = &crate::backlog::RISK_KIND;
const IDE: &Kind = &crate::backlog::IDEA_KIND;
const GOV: &[&Kind] = &[ADR, POL, STD];
const BACKLOG: &[&Kind] = &[ISS, IMP, CHR, RSK, IDE];
pub(crate) const RELATION_RULES: &[RelationRule] = &[
RelationRule {
sources: &[SLICE, ISS, IMP, CHR, RSK, IDE],
label: RelationLabel::Specs,
inbound_name: "specs",
target: TargetSpec::Kinds(&[PRD, SPEC]),
tier: Tier::One,
link: LinkPolicy::Writable,
},
RelationRule {
sources: &[SLICE],
label: RelationLabel::Requirements,
inbound_name: "requirements",
target: TargetSpec::Kinds(&[REQ]),
tier: Tier::One,
link: LinkPolicy::Writable,
},
RelationRule {
sources: &[SLICE],
label: RelationLabel::Supersedes,
inbound_name: "superseded by",
target: TargetSpec::Kinds(&[SLICE]),
tier: Tier::One,
link: LinkPolicy::Writable,
},
RelationRule {
sources: GOV,
label: RelationLabel::Supersedes,
inbound_name: "superseded by",
target: TargetSpec::SameKind,
tier: Tier::One,
link: LinkPolicy::LifecycleOnly,
},
RelationRule {
sources: &[SPEC],
label: RelationLabel::DescendsFrom,
inbound_name: "descends_from",
target: TargetSpec::Kinds(&[PRD]),
tier: Tier::Typed,
link: LinkPolicy::TypedVerbOnly,
},
RelationRule {
sources: &[SPEC],
label: RelationLabel::Parent,
inbound_name: "parent",
target: TargetSpec::Kinds(&[SPEC]),
tier: Tier::Typed,
link: LinkPolicy::TypedVerbOnly,
},
RelationRule {
sources: &[PRD, SPEC],
label: RelationLabel::Members,
inbound_name: "members",
target: TargetSpec::Kinds(&[REQ]),
tier: Tier::Typed,
link: LinkPolicy::TypedVerbOnly,
},
RelationRule {
sources: &[SPEC],
label: RelationLabel::Interactions,
inbound_name: "interactions",
target: TargetSpec::Kinds(&[SPEC]),
tier: Tier::Typed,
link: LinkPolicy::TypedVerbOnly,
},
RelationRule {
sources: &[SLICE, PRD, SPEC],
label: RelationLabel::GovernedBy,
inbound_name: "governs",
target: TargetSpec::Kinds(GOV),
tier: Tier::One,
link: LinkPolicy::Writable,
},
RelationRule {
sources: &[PRD],
label: RelationLabel::Consumes,
inbound_name: "consumed_by",
target: TargetSpec::Kinds(&[PRD]),
tier: Tier::One,
link: LinkPolicy::Writable,
},
RelationRule {
sources: BACKLOG,
label: RelationLabel::Slices,
inbound_name: "slices",
target: TargetSpec::Kinds(&[SLICE]),
tier: Tier::One,
link: LinkPolicy::Writable,
},
RelationRule {
sources: GOV,
label: RelationLabel::Related,
inbound_name: "related",
target: TargetSpec::SameKind,
tier: Tier::One,
link: LinkPolicy::Writable,
},
RelationRule {
sources: &[RV],
label: RelationLabel::Reviews,
inbound_name: "reviews",
target: TargetSpec::AnyNumbered,
tier: Tier::Typed,
link: LinkPolicy::TypedVerbOnly,
},
RelationRule {
sources: &[REC],
label: RelationLabel::OwningSlice,
inbound_name: "owning_slice",
target: TargetSpec::Kinds(&[SLICE]),
tier: Tier::Typed,
link: LinkPolicy::TypedVerbOnly,
},
RelationRule {
sources: BACKLOG,
label: RelationLabel::Drift,
inbound_name: "drift",
target: TargetSpec::Unvalidated,
tier: Tier::One,
link: LinkPolicy::Writable,
},
RelationRule {
sources: &[REC],
label: RelationLabel::DecisionRef,
inbound_name: "decision_ref",
target: TargetSpec::Unvalidated,
tier: Tier::Typed,
link: LinkPolicy::TypedVerbOnly,
},
];
pub(crate) fn lookup(source: &Kind, label: RelationLabel) -> Option<&'static RelationRule> {
RELATION_RULES
.iter()
.find(|r| r.label == label && r.sources.iter().any(|k| k.prefix == source.prefix))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RelationEdge {
pub(crate) label: RelationLabel,
pub(crate) target: String,
}
impl RelationEdge {
pub(crate) fn new(label: RelationLabel, target: String) -> Self {
Self { label, target }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum IllegalReason {
UnknownLabel,
IllegalForSource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct IllegalRow {
pub(crate) label: String,
pub(crate) target: String,
pub(crate) reason: IllegalReason,
}
#[derive(Debug, Clone, serde::Deserialize)]
struct RelationRow {
label: String,
target: String,
}
#[derive(Debug, Default, serde::Deserialize)]
pub(crate) struct RelationDoc {
#[serde(default)]
relation: Vec<RelationRow>,
}
impl RelationDoc {
pub(crate) fn parse(text: &str) -> anyhow::Result<RelationDoc> {
toml::from_str(text).map_err(|e| anyhow::anyhow!("parse [[relation]] block: {e}"))
}
}
pub(crate) fn read_block(
source_kind: &Kind,
doc: &RelationDoc,
) -> (Vec<RelationEdge>, Vec<IllegalRow>) {
let mut illegal: Vec<IllegalRow> = Vec::new();
let mut legal: Vec<(usize, RelationEdge)> = Vec::new();
for row in &doc.relation {
match RelationLabel::from_name(&row.label) {
None => illegal.push(IllegalRow {
label: row.label.clone(),
target: row.target.clone(),
reason: IllegalReason::UnknownLabel,
}),
Some(label) => match canonical_position(source_kind, label) {
Some(pos) => {
legal.push((pos, RelationEdge::new(label, row.target.clone())));
}
None => illegal.push(IllegalRow {
label: row.label.clone(),
target: row.target.clone(),
reason: IllegalReason::IllegalForSource,
}),
},
}
}
legal.sort_by_key(|(pos, _)| *pos);
let edges = legal.into_iter().map(|(_, e)| e).collect();
(edges, illegal)
}
fn canonical_position(source: &Kind, label: RelationLabel) -> Option<usize> {
RELATION_RULES
.iter()
.position(|r| r.label == label && r.sources.iter().any(|k| k.prefix == source.prefix))
}
pub(crate) fn tier1_edges(source_kind: &Kind, text: &str) -> anyhow::Result<Vec<RelationEdge>> {
let doc = RelationDoc::parse(text)?;
let (edges, _illegal) = read_block(source_kind, &doc);
Ok(edges)
}
pub(crate) fn targets_for(edges: &[RelationEdge], label: RelationLabel) -> Vec<String> {
edges
.iter()
.filter(|e| e.label == label)
.map(|e| e.target.clone())
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AppendOutcome {
Wrote,
Noop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RemoveOutcome {
Removed,
Absent,
}
fn trailing_typed_table_after_relation(doc: &toml_edit::DocumentMut) -> Option<String> {
let mut seen_relation = false;
for (key, item) in doc.as_table() {
if key == "relation" && item.is_array_of_tables() {
seen_relation = true;
} else if seen_relation {
return Some(key.to_string());
}
}
None
}
fn append_relation_row(
text: &str,
label: RelationLabel,
target: &str,
) -> anyhow::Result<(String, AppendOutcome)> {
let mut doc = text
.parse::<toml_edit::DocumentMut>()
.map_err(|e| anyhow::anyhow!("parse TOML for relation append: {e}"))?;
if relation_row_present(&doc, label, target) {
return Ok((text.to_string(), AppendOutcome::Noop));
}
if let Some(offending) = trailing_typed_table_after_relation(&doc) {
anyhow::bail!(
"refusing to append [[relation]]: typed table `[{offending}]` is authored AFTER \
the [[relation]] array (F1 — appending would corrupt it by tail-inserting into \
the last array element). Re-home `[{offending}]` above the [[relation]] block."
);
}
let array = doc
.as_table_mut()
.entry("relation")
.or_insert_with(|| toml_edit::Item::ArrayOfTables(toml_edit::ArrayOfTables::new()))
.as_array_of_tables_mut()
.ok_or_else(|| {
anyhow::anyhow!("`relation` is present but is not an array-of-tables (corrupt file)")
})?;
let mut row = toml_edit::Table::new();
row.insert("label", toml_edit::value(label.name()));
row.insert("target", toml_edit::value(target));
array.push(row);
Ok((doc.to_string(), AppendOutcome::Wrote))
}
fn remove_relation_row(
text: &str,
label: RelationLabel,
target: &str,
) -> anyhow::Result<(String, RemoveOutcome)> {
let mut doc = text
.parse::<toml_edit::DocumentMut>()
.map_err(|e| anyhow::anyhow!("parse TOML for relation remove: {e}"))?;
let Some(array) = doc
.as_table_mut()
.get_mut("relation")
.and_then(toml_edit::Item::as_array_of_tables_mut)
else {
return Ok((text.to_string(), RemoveOutcome::Absent));
};
let before = array.len();
array.retain(|row| !row_matches(row, label, target));
if array.len() == before {
return Ok((text.to_string(), RemoveOutcome::Absent));
}
Ok((doc.to_string(), RemoveOutcome::Removed))
}
fn relation_row_present(doc: &toml_edit::DocumentMut, label: RelationLabel, target: &str) -> bool {
doc.as_table()
.get("relation")
.and_then(toml_edit::Item::as_array_of_tables)
.is_some_and(|array| array.iter().any(|row| row_matches(row, label, target)))
}
fn row_matches(row: &toml_edit::Table, label: RelationLabel, target: &str) -> bool {
row.get("label").and_then(toml_edit::Item::as_str) == Some(label.name())
&& row.get("target").and_then(toml_edit::Item::as_str) == Some(target)
}
pub(crate) fn append_edge(
toml_path: &std::path::Path,
label: RelationLabel,
target: &str,
) -> anyhow::Result<AppendOutcome> {
let text = std::fs::read_to_string(toml_path)
.map_err(|e| anyhow::anyhow!("read {} for relation append: {e}", toml_path.display()))?;
let (next, outcome) = append_relation_row(&text, label, target)?;
if outcome == AppendOutcome::Wrote {
std::fs::write(toml_path, next).map_err(|e| {
anyhow::anyhow!("write {} after relation append: {e}", toml_path.display())
})?;
}
Ok(outcome)
}
pub(crate) fn remove_edge(
toml_path: &std::path::Path,
label: RelationLabel,
target: &str,
) -> anyhow::Result<RemoveOutcome> {
let text = std::fs::read_to_string(toml_path)
.map_err(|e| anyhow::anyhow!("read {} for relation remove: {e}", toml_path.display()))?;
let (next, outcome) = remove_relation_row(&text, label, target)?;
if outcome == RemoveOutcome::Removed {
std::fs::write(toml_path, next).map_err(|e| {
anyhow::anyhow!("write {} after relation remove: {e}", toml_path.display())
})?;
}
Ok(outcome)
}
pub(crate) fn inbound_name(label: RelationLabel) -> &'static str {
RELATION_RULES
.iter()
.find(|r| r.label == label)
.map_or(label.name(), |r| r.inbound_name)
}
fn writable_labels_for(source: &Kind) -> Vec<&'static str> {
RELATION_RULES
.iter()
.filter(|r| {
r.link == LinkPolicy::Writable && r.sources.iter().any(|k| k.prefix == source.prefix)
})
.map(|r| r.label.name())
.collect()
}
fn owning_verb_for(rule: &RelationRule) -> &'static str {
match rule.link {
LinkPolicy::Writable => "link",
LinkPolicy::LifecycleOnly => "the transactional supersede verb (IMP-006)",
LinkPolicy::TypedVerbOnly => "the kind's typed verb (e.g. `spec req add`, `review …`)",
}
}
pub(crate) fn validate_link(
source_kind: &Kind,
label_str: &str,
) -> anyhow::Result<&'static RelationRule> {
let legal = || writable_labels_for(source_kind).join(", ");
let label = RelationLabel::from_name(label_str).ok_or_else(|| {
anyhow::anyhow!(
"`{label_str}` is not a relation label authorable by {} via `link`. Legal labels: {}",
source_kind.prefix,
legal()
)
})?;
let rule = lookup(source_kind, label).ok_or_else(|| {
anyhow::anyhow!(
"{} may not author `{label_str}` (illegal for this source). Legal `link` labels: {}",
source_kind.prefix,
legal()
)
})?;
anyhow::ensure!(
rule.link == LinkPolicy::Writable,
"`{label_str}` is not `link`-writable — author it through {}, not generic `link`",
owning_verb_for(rule)
);
Ok(rule)
}
pub(crate) fn check_target_kind(
rule: &RelationRule,
source_kind: &Kind,
target_prefix: &str,
) -> anyhow::Result<()> {
match rule.target {
TargetSpec::Kinds(set) => anyhow::ensure!(
set.iter().any(|k| k.prefix == target_prefix),
"`{}` target must be one of [{}], got a {target_prefix}",
rule.label.name(),
set.iter().map(|k| k.prefix).collect::<Vec<_>>().join(", ")
),
TargetSpec::SameKind => anyhow::ensure!(
target_prefix == source_kind.prefix,
"`{}` target must be the same kind as the source ({}), got a {target_prefix}",
rule.label.name(),
source_kind.prefix
),
TargetSpec::AnyNumbered | TargetSpec::Unvalidated => {}
}
Ok(())
}
#[cfg(test)]
pub(crate) fn rels_block(source: &Kind, axes: &[(&str, &[&str])]) -> String {
let migrated = |label: RelationLabel| -> bool {
lookup(source, label)
.map(|r| r.tier == Tier::One && r.link != LinkPolicy::LifecycleOnly)
.unwrap_or(false)
};
let mut typed = String::new();
let mut rows = String::new();
for (label, targets) in axes {
let is_migrated = RelationLabel::from_name(label)
.map(migrated)
.unwrap_or(false);
if is_migrated {
for t in *targets {
rows.push_str(&format!(
"[[relation]]\nlabel = \"{label}\"\ntarget = \"{t}\"\n"
));
}
} else {
let list = targets
.iter()
.map(|t| format!("\"{t}\""))
.collect::<Vec<_>>()
.join(", ");
typed.push_str(&format!("{label} = [{list}]\n"));
}
}
let typed_table = if typed.is_empty() {
String::new()
} else {
format!("[relationships]\n{typed}")
};
format!("{typed_table}{rows}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn label_name_is_stable() {
assert_eq!(RelationLabel::Supersedes.name(), "supersedes");
assert_eq!(RelationLabel::OwningSlice.name(), "owning_slice");
assert_eq!(RelationLabel::Drift.name(), "drift");
assert_eq!(RelationLabel::DecisionRef.name(), "decision_ref");
}
#[test]
fn edge_carries_label_and_target() {
let e = RelationEdge::new(RelationLabel::Specs, "PRD-010".to_string());
assert_eq!(e.label, RelationLabel::Specs);
assert_eq!(e.target, "PRD-010");
}
use crate::adr::ADR_KIND;
use crate::backlog::{CHORE_KIND, IDEA_KIND, IMPROVEMENT_KIND, ISSUE_KIND, RISK_KIND};
use crate::entity::Kind;
use crate::policy::POLICY_KIND;
use crate::rec::REC_KIND;
use crate::requirement::REQUIREMENT_KIND;
use crate::review::REVIEW_KIND;
use crate::slice::SLICE_KIND;
use crate::spec::{PRODUCT_SPEC_KIND, TECH_SPEC_KIND};
use crate::standard::STANDARD_KIND;
fn distinct_labels_in_decl_order() -> Vec<RelationLabel> {
let mut seen: Vec<RelationLabel> = Vec::new();
for r in RELATION_RULES {
if !seen.contains(&r.label) {
seen.push(r.label);
}
}
seen
}
#[test]
fn enum_ord_matches_relation_rules_label_order() {
let from_table = distinct_labels_in_decl_order();
let mut sorted = from_table.clone();
sorted.sort();
assert_eq!(
from_table, sorted,
"RELATION_RULES distinct-label declaration order diverged from RelationLabel enum Ord"
);
}
#[test]
fn sources_match_shipped_accessors() {
let expected: &[(RelationLabel, &[&str])] = &[
(
RelationLabel::Specs,
&["SL", "ISS", "IMP", "CHR", "RSK", "IDE"],
),
(RelationLabel::Requirements, &["SL"]),
(RelationLabel::Supersedes, &["SL", "ADR", "POL", "STD"]),
(RelationLabel::DescendsFrom, &["SPEC"]),
(RelationLabel::Parent, &["SPEC"]),
(RelationLabel::Members, &["PRD", "SPEC"]),
(RelationLabel::Interactions, &["SPEC"]),
(RelationLabel::Slices, &["ISS", "IMP", "CHR", "RSK", "IDE"]),
(RelationLabel::Related, &["ADR", "POL", "STD"]),
(RelationLabel::Reviews, &["RV"]),
(RelationLabel::OwningSlice, &["REC"]),
(RelationLabel::Drift, &["ISS", "IMP", "CHR", "RSK", "IDE"]),
(RelationLabel::DecisionRef, &["REC"]),
];
for (label, want_prefixes) in expected {
let mut got: Vec<&str> = RELATION_RULES
.iter()
.filter(|r| r.label == *label)
.flat_map(|r| r.sources.iter().map(|k| k.prefix))
.collect();
got.sort_unstable();
got.dedup();
let mut want: Vec<&str> = want_prefixes.to_vec();
want.sort_unstable();
want.dedup();
assert_eq!(
got, want,
"RELATION_RULES source set for {label:?} diverged from the shipped accessor"
);
}
}
#[test]
fn inbound_name_equals_name_except_the_three_inverted() {
for r in RELATION_RULES {
let differs = r.inbound_name != r.label.name();
let allowed_to_differ = matches!(
r.label,
RelationLabel::Supersedes | RelationLabel::GovernedBy | RelationLabel::Consumes
);
if differs {
assert!(
allowed_to_differ,
"{:?} inbound_name {:?} differs from name() {:?} but is not an allowed inverted label",
r.label,
r.inbound_name,
r.label.name()
);
}
}
assert_eq!(
lookup(&SLICE_KIND, RelationLabel::Supersedes)
.unwrap()
.inbound_name,
"superseded by"
);
assert_eq!(
lookup(&SLICE_KIND, RelationLabel::GovernedBy)
.unwrap()
.inbound_name,
"governs"
);
assert_eq!(
lookup(&PRODUCT_SPEC_KIND, RelationLabel::Consumes)
.unwrap()
.inbound_name,
"consumed_by"
);
}
#[test]
fn no_rule_label_is_an_inverse_spelling() {
const INVERSE_SPELLINGS: &[&str] = &["superseded_by", "governs", "consumed_by"];
for r in RELATION_RULES {
assert!(
!INVERSE_SPELLINGS.contains(&r.label.name()),
"{:?} round-trips to an inverse outbound spelling {:?} — inverses are derived, not authorable",
r.label,
r.label.name()
);
}
assert!(
RELATION_RULES
.iter()
.any(|r| r.inbound_name == "superseded by"),
"expected the supersedes rule to carry the inverted inbound text"
);
}
#[test]
fn every_variant_appears_in_the_table() {
const ALL: &[RelationLabel] = &[
RelationLabel::Specs,
RelationLabel::Requirements,
RelationLabel::Supersedes,
RelationLabel::DescendsFrom,
RelationLabel::Parent,
RelationLabel::Members,
RelationLabel::Interactions,
RelationLabel::GovernedBy,
RelationLabel::Consumes,
RelationLabel::Slices,
RelationLabel::Related,
RelationLabel::Reviews,
RelationLabel::OwningSlice,
RelationLabel::Drift,
RelationLabel::DecisionRef,
];
let mut sorted = ALL.to_vec();
sorted.sort();
assert_eq!(ALL, sorted.as_slice(), "ALL is not in enum Ord order");
assert_eq!(
distinct_labels_in_decl_order(),
ALL.to_vec(),
"RELATION_RULES does not cover exactly the RelationLabel variants in order"
);
}
#[test]
fn tier_partition_matches_design() {
let tier_one = [
RelationLabel::Specs,
RelationLabel::Requirements,
RelationLabel::Supersedes,
RelationLabel::GovernedBy,
RelationLabel::Consumes,
RelationLabel::Slices,
RelationLabel::Related,
RelationLabel::Drift,
];
for r in RELATION_RULES {
let want = if tier_one.contains(&r.label) {
Tier::One
} else {
Tier::Typed
};
assert_eq!(
r.tier, want,
"{:?} tier diverged from the design storage-shape column",
r.label
);
}
}
#[test]
fn target_spec_matches_design() {
for r in RELATION_RULES {
match (r.label, r.sources) {
(RelationLabel::Related, _) => {
assert!(
matches!(r.target, TargetSpec::SameKind),
"related → SameKind"
);
}
(RelationLabel::Supersedes, s) if !s.iter().any(|k| k.prefix == "SL") => {
assert!(
matches!(r.target, TargetSpec::SameKind),
"gov supersedes → SameKind"
);
}
(RelationLabel::Drift | RelationLabel::DecisionRef, _) => {
assert!(
matches!(r.target, TargetSpec::Unvalidated),
"{:?} → Unvalidated",
r.label
);
}
(RelationLabel::Reviews, _) => {
assert!(
matches!(r.target, TargetSpec::AnyNumbered),
"reviews → AnyNumbered"
);
}
(_, _) => match r.target {
TargetSpec::Kinds(ks) => {
assert!(!ks.is_empty(), "{:?} → non-empty Kinds set", r.label)
}
other => panic!(
"{:?} expected an explicit Kinds target, got {}",
r.label,
match other {
TargetSpec::SameKind => "SameKind",
TargetSpec::AnyNumbered => "AnyNumbered",
TargetSpec::Unvalidated => "Unvalidated",
TargetSpec::Kinds(_) => unreachable!(),
}
),
},
}
}
if let TargetSpec::Kinds(ks) = lookup(&SLICE_KIND, RelationLabel::GovernedBy)
.unwrap()
.target
{
let mut got: Vec<&str> = ks.iter().map(|k| k.prefix).collect();
got.sort_unstable();
assert_eq!(got, ["ADR", "POL", "STD"]);
} else {
panic!("governed_by → Kinds([ADR,POL,STD])");
}
}
fn edge_pairs(edges: &[RelationEdge]) -> Vec<(RelationLabel, &str)> {
edges.iter().map(|e| (e.label, e.target.as_str())).collect()
}
#[test]
fn read_block_rejects_illegal_source_label_pairs() {
let slice_doc = RelationDoc::parse(
"[[relation]]\nlabel = \"related\"\ntarget = \"SL-002\"\n\
[[relation]]\nlabel = \"specs\"\ntarget = \"PRD-010\"\n",
)
.unwrap();
let (edges, illegal) = read_block(&SLICE_KIND, &slice_doc);
assert_eq!(
edge_pairs(&edges),
vec![(RelationLabel::Specs, "PRD-010")],
"the legal specs row emits an edge"
);
assert_eq!(
illegal,
vec![IllegalRow {
label: "related".to_string(),
target: "SL-002".to_string(),
reason: IllegalReason::IllegalForSource,
}],
"related is illegal for a slice source — a finding, not a live edge"
);
let backlog_doc = RelationDoc::parse(
"[[relation]]\nlabel = \"governed_by\"\ntarget = \"ADR-010\"\n\
[[relation]]\nlabel = \"slices\"\ntarget = \"SL-020\"\n",
)
.unwrap();
let (edges, illegal) = read_block(&ISSUE_KIND, &backlog_doc);
assert_eq!(edge_pairs(&edges), vec![(RelationLabel::Slices, "SL-020")]);
assert_eq!(
illegal,
vec![IllegalRow {
label: "governed_by".to_string(),
target: "ADR-010".to_string(),
reason: IllegalReason::IllegalForSource,
}],
"governed_by is illegal for a backlog source"
);
let bad_doc = RelationDoc::parse(
"[[relation]]\nlabel = \"superseded_by\"\ntarget = \"SL-001\"\n\
[[relation]]\nlabel = \"nonsense\"\ntarget = \"X\"\n",
)
.unwrap();
let (edges, illegal) = read_block(&SLICE_KIND, &bad_doc);
assert!(edges.is_empty(), "no legal edges from unknown labels");
assert_eq!(
illegal,
vec![
IllegalRow {
label: "superseded_by".to_string(),
target: "SL-001".to_string(),
reason: IllegalReason::UnknownLabel,
},
IllegalRow {
label: "nonsense".to_string(),
target: "X".to_string(),
reason: IllegalReason::UnknownLabel,
},
],
"an inverse spelling and a typo are both UnknownLabel findings, verbatim"
);
}
#[test]
fn read_block_emits_in_canonical_order_stable_within_label() {
let doc = RelationDoc::parse(
"[[relation]]\nlabel = \"supersedes\"\ntarget = \"SL-000\"\n\
[[relation]]\nlabel = \"requirements\"\ntarget = \"REQ-002\"\n\
[[relation]]\nlabel = \"requirements\"\ntarget = \"REQ-001\"\n\
[[relation]]\nlabel = \"specs\"\ntarget = \"PRD-010\"\n",
)
.unwrap();
let (edges, illegal) = read_block(&SLICE_KIND, &doc);
assert!(illegal.is_empty(), "all rows are legal for a slice");
assert_eq!(
edge_pairs(&edges),
vec![
(RelationLabel::Specs, "PRD-010"),
(RelationLabel::Requirements, "REQ-002"),
(RelationLabel::Requirements, "REQ-001"),
(RelationLabel::Supersedes, "SL-000"),
],
"edges land in canonical table order; same-label rows keep authored order"
);
}
#[test]
fn read_block_empty_block_is_no_edges_no_findings() {
let doc = RelationDoc::parse("id = 1\ntitle = \"x\"\n").unwrap();
let (edges, illegal) = read_block(&SLICE_KIND, &doc);
assert!(edges.is_empty());
assert!(illegal.is_empty());
}
#[test]
fn append_relation_row_appends_and_preserves() {
let text = "# a comment\nid = 1\ntitle = \"x\"\n";
let (next, outcome) =
append_relation_row(text, RelationLabel::GovernedBy, "ADR-010").unwrap();
assert_eq!(outcome, AppendOutcome::Wrote);
assert!(next.contains("# a comment"), "comment preserved");
assert!(next.contains("[[relation]]"));
assert!(next.contains("label = \"governed_by\""));
assert!(next.contains("target = \"ADR-010\""));
let edges = tier1_edges(&SLICE_KIND, &next).unwrap();
assert_eq!(
edge_pairs(&edges),
vec![(RelationLabel::GovernedBy, "ADR-010")]
);
}
#[test]
fn append_relation_row_is_idempotent() {
let text = "id = 1\n";
let (once, o1) = append_relation_row(text, RelationLabel::GovernedBy, "ADR-010").unwrap();
assert_eq!(o1, AppendOutcome::Wrote);
let (twice, o2) = append_relation_row(&once, RelationLabel::GovernedBy, "ADR-010").unwrap();
assert_eq!(o2, AppendOutcome::Noop);
assert_eq!(once, twice, "a no-op append leaves the text byte-identical");
}
#[test]
fn append_relation_row_refuses_trailing_typed_table() {
let trap = "id = 1\n\
[[relation]]\nlabel = \"specs\"\ntarget = \"PRD-010\"\n\
[relationships]\ntags = [\"x\"]\n";
let err = append_relation_row(trap, RelationLabel::GovernedBy, "ADR-010").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("relationships") && msg.contains("AFTER"),
"refusal must name the offending trailing table: {msg}"
);
let (out, outcome) = append_relation_row(trap, RelationLabel::Specs, "PRD-010").unwrap();
assert_eq!(outcome, AppendOutcome::Noop);
assert_eq!(out, trap);
}
#[test]
fn append_relation_row_escapes_target() {
let text = "id = 1\n";
let (next, _) = append_relation_row(text, RelationLabel::Drift, "a\"b").unwrap();
let doc = RelationDoc::parse(&next).unwrap();
let (edges, _illegal) = read_block(&ISSUE_KIND, &doc);
assert_eq!(edge_pairs(&edges), vec![(RelationLabel::Drift, "a\"b")]);
}
#[test]
fn remove_relation_row_round_trips_and_is_idempotent() {
let (with, _) =
append_relation_row("id = 1\n", RelationLabel::GovernedBy, "ADR-010").unwrap();
let (without, o1) =
remove_relation_row(&with, RelationLabel::GovernedBy, "ADR-010").unwrap();
assert_eq!(o1, RemoveOutcome::Removed);
assert!(
tier1_edges(&SLICE_KIND, &without).unwrap().is_empty(),
"the edge is gone after remove"
);
let (again, o2) =
remove_relation_row(&without, RelationLabel::GovernedBy, "ADR-010").unwrap();
assert_eq!(o2, RemoveOutcome::Absent);
assert_eq!(without, again, "a second remove is a byte-identical no-op");
}
#[test]
fn inbound_name_is_table_driven() {
assert_eq!(inbound_name(RelationLabel::GovernedBy), "governs");
assert_eq!(inbound_name(RelationLabel::Consumes), "consumed_by");
assert_eq!(inbound_name(RelationLabel::Supersedes), "superseded by");
for label in distinct_labels_in_decl_order() {
let inverted = matches!(
label,
RelationLabel::GovernedBy | RelationLabel::Consumes | RelationLabel::Supersedes
);
if !inverted {
assert_eq!(
inbound_name(label),
label.name(),
"{label:?} inbound render must equal its name()"
);
}
}
}
#[test]
fn validate_link_gates_source_label_and_policy() {
match validate_link(&SLICE_KIND, "governed_by") {
Ok(rule) => assert_eq!(rule.label, RelationLabel::GovernedBy),
Err(e) => panic!("governed_by should be writable for a slice: {e}"),
}
let refusal = |src: &Kind, label: &str| -> String {
match validate_link(src, label) {
Ok(_) => panic!("expected `{label}` to be refused for {}", src.prefix),
Err(e) => e.to_string(),
}
};
let e = refusal(&SLICE_KIND, "nonsense");
assert!(e.contains("governed_by"), "lists legal labels: {e}");
let e = refusal(&SLICE_KIND, "related");
assert!(e.contains("illegal for this source"), "{e}");
let e = refusal(&ADR_KIND.kind, "supersedes");
assert!(e.contains("supersede verb"), "names the owning verb: {e}");
let e = refusal(&PRODUCT_SPEC_KIND, "members");
assert!(e.contains("typed verb"), "names the typed verb: {e}");
}
#[test]
fn check_target_kind_enforces_target_kind() {
let unwrap_rule = |r: anyhow::Result<&'static RelationRule>| -> &'static RelationRule {
match r {
Ok(rule) => rule,
Err(e) => panic!("expected a writable rule: {e}"),
}
};
let gov_by = unwrap_rule(validate_link(&SLICE_KIND, "governed_by"));
assert!(check_target_kind(gov_by, &SLICE_KIND, "SL").is_err());
for p in ["ADR", "POL", "STD"] {
assert!(check_target_kind(gov_by, &SLICE_KIND, p).is_ok());
}
let related = unwrap_rule(validate_link(&ADR_KIND.kind, "related"));
assert!(check_target_kind(related, &ADR_KIND.kind, "ADR").is_ok());
assert!(
check_target_kind(related, &ADR_KIND.kind, "POL").is_err(),
"SameKind refuses a cross-gov target"
);
}
#[test]
fn lookup_keys_on_source_and_label() {
assert!(lookup(&ISSUE_KIND, RelationLabel::GovernedBy).is_none());
assert!(lookup(&SLICE_KIND, RelationLabel::Related).is_none());
for k in [&SLICE_KIND, &PRODUCT_SPEC_KIND, &TECH_SPEC_KIND] {
assert!(lookup(k, RelationLabel::GovernedBy).is_some());
}
assert!(lookup(&PRODUCT_SPEC_KIND, RelationLabel::Consumes).is_some());
assert!(lookup(&TECH_SPEC_KIND, RelationLabel::Consumes).is_none());
let sl_sup = lookup(&SLICE_KIND, RelationLabel::Supersedes).unwrap();
assert_eq!(sl_sup.link, LinkPolicy::Writable);
let adr_sup = lookup(&ADR_KIND.kind, RelationLabel::Supersedes).unwrap();
assert_eq!(adr_sup.link, LinkPolicy::LifecycleOnly);
let _ = (
&REQUIREMENT_KIND,
&REVIEW_KIND,
&REC_KIND,
&STANDARD_KIND,
&POLICY_KIND,
&IMPROVEMENT_KIND,
&CHORE_KIND,
&RISK_KIND,
&IDEA_KIND,
);
let _: &Kind = &SLICE_KIND;
}
}