1use crate::graph::{ExtendedRelationKind, GraphRelationKind};
4use crate::symbols::RelationKind;
5use blake3::Hasher;
6use serde::Serialize;
7use std::collections::BTreeSet;
8use std::fmt::{self, Write as _};
9
10pub const ACCEPTED_RELATION_FAMILY_INVENTORY_VERSION: u32 = 1;
12pub const ACCEPTED_RELATION_FAMILY_INVENTORY_V1_DIGEST: &str =
14 "aae2984ba57c42a387081c0f601025f127a32104c2662cb8fa9855b4cfd61bcc";
15
16#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
18#[serde(rename_all = "snake_case")]
19pub enum RelationFamilyId {
20 StructuralType,
22 PackageManifest,
24 Test,
26 RouteProtocol,
28 ConfigurationEnvironment,
30 DeploymentInfrastructure,
32 StaticDataAccess,
34 InferredSimilarity,
36 InferredCoChange,
38}
39
40impl RelationFamilyId {
41 #[must_use]
43 pub const fn as_str(self) -> &'static str {
44 match self {
45 Self::StructuralType => "structural_type",
46 Self::PackageManifest => "package_manifest",
47 Self::Test => "test",
48 Self::RouteProtocol => "route_protocol",
49 Self::ConfigurationEnvironment => "configuration_environment",
50 Self::DeploymentInfrastructure => "deployment_infrastructure",
51 Self::StaticDataAccess => "static_data_access",
52 Self::InferredSimilarity => "inferred_similarity",
53 Self::InferredCoChange => "inferred_co_change",
54 }
55 }
56}
57
58#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
60#[serde(rename_all = "snake_case")]
61pub enum RelationFamilyState {
62 Active,
64 OptionalDisabled,
66}
67
68impl RelationFamilyState {
69 const fn as_str(self) -> &'static str {
71 match self {
72 Self::Active => "active",
73 Self::OptionalDisabled => "optional_disabled",
74 }
75 }
76}
77
78#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
80pub struct RelationFamilyCapability {
81 pub id: RelationFamilyId,
83 pub state: RelationFamilyState,
85 pub graph_relations: &'static [GraphRelationKind],
87 pub producer: &'static str,
89 pub persistence: &'static str,
91 pub invalidation: &'static str,
93 pub query_consumer: &'static str,
95 pub positive_fixture: &'static str,
97 pub negative_fixture: &'static str,
99 pub coverage: &'static str,
101}
102
103const STRUCTURAL_RELATIONS: &[GraphRelationKind] = &[
105 GraphRelationKind::Legacy(RelationKind::Contains),
106 GraphRelationKind::Legacy(RelationKind::Imports),
107 GraphRelationKind::Legacy(RelationKind::Calls),
108];
109const PACKAGE_RELATIONS: &[GraphRelationKind] =
111 &[GraphRelationKind::Legacy(RelationKind::DependsOn)];
112const TEST_RELATIONS: &[GraphRelationKind] =
114 &[GraphRelationKind::Extended(ExtendedRelationKind::Tests)];
115const ROUTE_RELATIONS: &[GraphRelationKind] =
117 &[GraphRelationKind::Extended(ExtendedRelationKind::RoutesTo)];
118const CONFIGURATION_RELATIONS: &[GraphRelationKind] = &[GraphRelationKind::Extended(
120 ExtendedRelationKind::Configures,
121)];
122const DEPLOYMENT_RELATIONS: &[GraphRelationKind] =
124 &[GraphRelationKind::Extended(ExtendedRelationKind::Deploys)];
125const DATA_ACCESS_RELATIONS: &[GraphRelationKind] = &[
127 GraphRelationKind::Extended(ExtendedRelationKind::Reads),
128 GraphRelationKind::Extended(ExtendedRelationKind::Writes),
129];
130const NO_RELATIONS: &[GraphRelationKind] = &[];
132
133pub static RELATION_FAMILY_CAPABILITIES: &[RelationFamilyCapability] = &[
135 RelationFamilyCapability {
136 id: RelationFamilyId::StructuralType,
137 state: RelationFamilyState::Active,
138 graph_relations: STRUCTURAL_RELATIONS,
139 producer: "projectatlas-symbols",
140 persistence: "projectatlas-db/repository-graph",
141 invalidation: "projectatlas-cli/graph-projection",
142 query_consumer: "projectatlas-service/relations",
143 positive_fixture: "graph_projection::accepted_relation_families_publish_and_reopen",
144 negative_fixture: "graph_projection::accepted_relation_families_abstain_without_static_evidence",
145 coverage: "provider-owned structural facts with parser-strength coverage",
146 },
147 RelationFamilyCapability {
148 id: RelationFamilyId::PackageManifest,
149 state: RelationFamilyState::Active,
150 graph_relations: PACKAGE_RELATIONS,
151 producer: "projectatlas-symbols/manifest",
152 persistence: "projectatlas-db/repository-graph",
153 invalidation: "projectatlas-cli/graph-projection",
154 query_consumer: "projectatlas-service/relations",
155 positive_fixture: "graph_projection::accepted_relation_families_publish_and_reopen",
156 negative_fixture: "graph_projection::accepted_relation_families_abstain_without_static_evidence",
157 coverage: "accepted manifest dependencies only",
158 },
159 RelationFamilyCapability {
160 id: RelationFamilyId::Test,
161 state: RelationFamilyState::Active,
162 graph_relations: TEST_RELATIONS,
163 producer: "projectatlas-cli/graph-projection",
164 persistence: "projectatlas-db/repository-graph",
165 invalidation: "projectatlas-cli/graph-projection",
166 query_consumer: "projectatlas-service/relations",
167 positive_fixture: "graph_projection::accepted_relation_families_publish_and_reopen",
168 negative_fixture: "graph_projection::accepted_relation_families_abstain_without_static_evidence",
169 coverage: "statically resolved calls and imports from recognized test paths",
170 },
171 RelationFamilyCapability {
172 id: RelationFamilyId::RouteProtocol,
173 state: RelationFamilyState::Active,
174 graph_relations: ROUTE_RELATIONS,
175 producer: "projectatlas-cli/graph-projection",
176 persistence: "projectatlas-db/repository-graph",
177 invalidation: "projectatlas-cli/graph-projection",
178 query_consumer: "projectatlas-service/relations",
179 positive_fixture: "graph_projection::accepted_relation_families_publish_and_reopen",
180 negative_fixture: "graph_projection::accepted_relation_families_abstain_without_static_evidence",
181 coverage: "recognized static route registrations only; dynamic registrations abstain",
182 },
183 RelationFamilyCapability {
184 id: RelationFamilyId::ConfigurationEnvironment,
185 state: RelationFamilyState::Active,
186 graph_relations: CONFIGURATION_RELATIONS,
187 producer: "projectatlas-cli/graph-projection",
188 persistence: "projectatlas-db/repository-graph",
189 invalidation: "projectatlas-cli/graph-projection",
190 query_consumer: "projectatlas-service/relations",
191 positive_fixture: "graph_projection::accepted_relation_families_publish_and_reopen",
192 negative_fixture: "graph_projection::accepted_relation_families_abstain_without_static_evidence",
193 coverage: "recognized configuration files and static environment keys; values are excluded",
194 },
195 RelationFamilyCapability {
196 id: RelationFamilyId::DeploymentInfrastructure,
197 state: RelationFamilyState::Active,
198 graph_relations: DEPLOYMENT_RELATIONS,
199 producer: "projectatlas-cli/graph-projection",
200 persistence: "projectatlas-db/repository-graph",
201 invalidation: "projectatlas-cli/graph-projection",
202 query_consumer: "projectatlas-service/relations",
203 positive_fixture: "graph_projection::accepted_relation_families_publish_and_reopen",
204 negative_fixture: "graph_projection::accepted_relation_families_abstain_without_static_evidence",
205 coverage: "recognized infrastructure configuration files; resource detail remains partial",
206 },
207 RelationFamilyCapability {
208 id: RelationFamilyId::StaticDataAccess,
209 state: RelationFamilyState::Active,
210 graph_relations: DATA_ACCESS_RELATIONS,
211 producer: "projectatlas-cli/graph-projection",
212 persistence: "projectatlas-db/repository-graph",
213 invalidation: "projectatlas-cli/graph-projection",
214 query_consumer: "projectatlas-service/relations",
215 positive_fixture: "graph_projection::accepted_relation_families_publish_and_reopen",
216 negative_fixture: "graph_projection::accepted_relation_families_abstain_without_static_evidence",
217 coverage: "recognized static read and write calls only; dynamic paths abstain",
218 },
219 RelationFamilyCapability {
220 id: RelationFamilyId::InferredSimilarity,
221 state: RelationFamilyState::OptionalDisabled,
222 graph_relations: NO_RELATIONS,
223 producer: "optional-semantic-lifecycle",
224 persistence: "none",
225 invalidation: "optional-semantic-lifecycle",
226 query_consumer: "explicit-semantic-mode",
227 positive_fixture: "deferred-until-quality-gates-pass",
228 negative_fixture: "default-core-rejects-unavailable-semantic-mode",
229 coverage: "not advertised while optional quality and resource gates are unmet",
230 },
231 RelationFamilyCapability {
232 id: RelationFamilyId::InferredCoChange,
233 state: RelationFamilyState::OptionalDisabled,
234 graph_relations: NO_RELATIONS,
235 producer: "optional-vcs-analysis",
236 persistence: "none",
237 invalidation: "call-scoped-vcs-identity",
238 query_consumer: "explicit-vcs-analysis-mode",
239 positive_fixture: "deferred-until-quality-gates-pass",
240 negative_fixture: "default-analysis-does-not-infer-co-change",
241 coverage: "not advertised while optional quality and freshness gates are unmet",
242 },
243];
244
245#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
247pub struct RelationFamilyInventoryReport {
248 pub version: u32,
250 pub digest: String,
252 pub active_families: u32,
254 pub optional_disabled_families: u32,
256}
257
258pub fn validate_relation_family_inventory() -> Result<String, &'static str> {
265 let mut ids = BTreeSet::new();
266 let mut relation_kinds = BTreeSet::new();
267 for row in RELATION_FAMILY_CAPABILITIES {
268 if !ids.insert(row.id)
269 || row.producer.is_empty()
270 || row.persistence.is_empty()
271 || row.invalidation.is_empty()
272 || row.query_consumer.is_empty()
273 || row.positive_fixture.is_empty()
274 || row.negative_fixture.is_empty()
275 || row.coverage.is_empty()
276 {
277 return Err("relation-family inventory contains an incomplete or duplicate row");
278 }
279 match row.state {
280 RelationFamilyState::Active if row.graph_relations.is_empty() => {
281 return Err("active relation-family row has no persisted relation kind");
282 }
283 RelationFamilyState::OptionalDisabled if !row.graph_relations.is_empty() => {
284 return Err("disabled inferred relation-family row advertises persisted facts");
285 }
286 RelationFamilyState::Active | RelationFamilyState::OptionalDisabled => {}
287 }
288 for relation in row.graph_relations {
289 if !relation_kinds.insert(relation.as_str()) {
290 return Err("persisted relation kind is owned by more than one family");
291 }
292 }
293 }
294 Ok(relation_family_inventory_digest())
295}
296
297#[must_use]
299pub fn relation_family_inventory_digest() -> String {
300 let mut hasher = Hasher::new();
301 hasher.update(b"projectatlas-relation-family-inventory-v1\0");
302 for row in RELATION_FAMILY_CAPABILITIES {
303 for value in [
304 row.id.as_str(),
305 row.state.as_str(),
306 row.producer,
307 row.persistence,
308 row.invalidation,
309 row.query_consumer,
310 row.positive_fixture,
311 row.negative_fixture,
312 row.coverage,
313 ] {
314 hasher.update(value.as_bytes());
315 hasher.update(&[0]);
316 }
317 for relation in row.graph_relations {
318 hasher.update(relation.as_str().as_bytes());
319 hasher.update(&[0]);
320 }
321 hasher.update(&[0xff]);
322 }
323 hasher.finalize().to_hex().to_string()
324}
325
326#[must_use]
329pub fn relation_family_inventory_report() -> RelationFamilyInventoryReport {
330 let active_families = RELATION_FAMILY_CAPABILITIES
331 .iter()
332 .filter(|row| row.state == RelationFamilyState::Active)
333 .count();
334 let optional_disabled_families = RELATION_FAMILY_CAPABILITIES
335 .iter()
336 .filter(|row| row.state == RelationFamilyState::OptionalDisabled)
337 .count();
338 RelationFamilyInventoryReport {
339 version: ACCEPTED_RELATION_FAMILY_INVENTORY_VERSION,
340 digest: relation_family_inventory_digest(),
341 active_families: u32::try_from(active_families).unwrap_or(u32::MAX),
342 optional_disabled_families: u32::try_from(optional_disabled_families).unwrap_or(u32::MAX),
343 }
344}
345
346pub fn render_relation_support_markdown() -> Result<String, fmt::Error> {
352 let report = relation_family_inventory_report();
353 let mut output = String::new();
354 writeln!(output, "# Relation Family Support\n")?;
355 writeln!(
356 output,
357 "Generated from accepted relation-family inventory v{} (`{}`).\n",
358 report.version, report.digest
359 )?;
360 writeln!(
361 output,
362 "| Family | State | Persisted graph relations | Coverage |"
363 )?;
364 writeln!(output, "| --- | --- | --- | --- |")?;
365 for row in RELATION_FAMILY_CAPABILITIES {
366 let relations = if row.graph_relations.is_empty() {
367 "—".to_owned()
368 } else {
369 row.graph_relations
370 .iter()
371 .map(|relation| format!("`{}`", relation.as_str()))
372 .collect::<Vec<_>>()
373 .join(", ")
374 };
375 writeln!(
376 output,
377 "| `{}` | `{}` | {} | {} |",
378 row.id.as_str(),
379 row.state.as_str(),
380 relations,
381 row.coverage
382 )?;
383 }
384 writeln!(
385 output,
386 "\nActive rows are persisted through the normalized SQLite graph and consumed by the existing bounded relation and analysis calls. Optional inferred rows remain unavailable until their independent quality, determinism, freshness, package, memory, and platform gates pass."
387 )?;
388 Ok(output)
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn accepted_relation_family_inventory_is_complete_and_frozen()
397 -> Result<(), Box<dyn std::error::Error>> {
398 let digest = validate_relation_family_inventory()?;
399 if digest != ACCEPTED_RELATION_FAMILY_INVENTORY_V1_DIGEST {
400 return Err("accepted relation-family digest changed".into());
401 }
402 Ok(())
403 }
404
405 #[test]
406 fn generated_relation_support_document_is_current() -> Result<(), Box<dyn std::error::Error>> {
407 if render_relation_support_markdown()? != include_str!("../../../docs/relation-support.md")
408 {
409 return Err("generated relation support document is stale".into());
410 }
411 Ok(())
412 }
413}