projectatlas_core/
relation_capabilities.rs

1//! Own the accepted relation-family inventory and its content-free projections.
2
3use 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
10/// Version of the accepted relation-family inventory.
11pub const ACCEPTED_RELATION_FAMILY_INVENTORY_VERSION: u32 = 1;
12/// Frozen digest of the accepted version-one inventory.
13pub const ACCEPTED_RELATION_FAMILY_INVENTORY_V1_DIGEST: &str =
14    "aae2984ba57c42a387081c0f601025f127a32104c2662cb8fa9855b4cfd61bcc";
15
16/// Stable product-level relation family.
17#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
18#[serde(rename_all = "snake_case")]
19pub enum RelationFamilyId {
20    /// Declarations, containment, imports, calls, and references.
21    StructuralType,
22    /// Package and manifest dependencies.
23    PackageManifest,
24    /// Test-to-subject relationships.
25    Test,
26    /// Route and protocol registration to handlers.
27    RouteProtocol,
28    /// Configuration or environment selection.
29    ConfigurationEnvironment,
30    /// Deployment and infrastructure provisioning.
31    DeploymentInfrastructure,
32    /// Bounded statically visible reads and writes.
33    StaticDataAccess,
34    /// Optional inferred semantic similarity.
35    InferredSimilarity,
36    /// Optional inferred version-control co-change.
37    InferredCoChange,
38}
39
40impl RelationFamilyId {
41    /// Return the stable payload and documentation label.
42    #[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/// Whether one accepted family is active or separately gated.
59#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
60#[serde(rename_all = "snake_case")]
61pub enum RelationFamilyState {
62    /// Produced, persisted, invalidated, queryable, and covered now.
63    Active,
64    /// Typed but unavailable until its optional capability passes its own gates.
65    OptionalDisabled,
66}
67
68impl RelationFamilyState {
69    /// Return the stable settings label.
70    const fn as_str(self) -> &'static str {
71        match self {
72            Self::Active => "active",
73            Self::OptionalDisabled => "optional_disabled",
74        }
75    }
76}
77
78/// One end-to-end accepted relation-family contract.
79#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
80pub struct RelationFamilyCapability {
81    /// Stable product family.
82    pub id: RelationFamilyId,
83    /// Current lifecycle state.
84    pub state: RelationFamilyState,
85    /// Persisted graph relation kinds owned by the family.
86    pub graph_relations: &'static [GraphRelationKind],
87    /// Responsibility owner that emits current facts.
88    pub producer: &'static str,
89    /// Typed persistence owner.
90    pub persistence: &'static str,
91    /// Invalidation owner.
92    pub invalidation: &'static str,
93    /// Existing bounded query consumer.
94    pub query_consumer: &'static str,
95    /// Positive fixture locator.
96    pub positive_fixture: &'static str,
97    /// Negative or ambiguity fixture locator.
98    pub negative_fixture: &'static str,
99    /// Honest coverage statement.
100    pub coverage: &'static str,
101}
102
103/// Structural and type relation kinds.
104const STRUCTURAL_RELATIONS: &[GraphRelationKind] = &[
105    GraphRelationKind::Legacy(RelationKind::Contains),
106    GraphRelationKind::Legacy(RelationKind::Imports),
107    GraphRelationKind::Legacy(RelationKind::Calls),
108];
109/// Package and manifest relation kinds.
110const PACKAGE_RELATIONS: &[GraphRelationKind] =
111    &[GraphRelationKind::Legacy(RelationKind::DependsOn)];
112/// Test relation kinds.
113const TEST_RELATIONS: &[GraphRelationKind] =
114    &[GraphRelationKind::Extended(ExtendedRelationKind::Tests)];
115/// Route and protocol relation kinds.
116const ROUTE_RELATIONS: &[GraphRelationKind] =
117    &[GraphRelationKind::Extended(ExtendedRelationKind::RoutesTo)];
118/// Configuration and environment relation kinds.
119const CONFIGURATION_RELATIONS: &[GraphRelationKind] = &[GraphRelationKind::Extended(
120    ExtendedRelationKind::Configures,
121)];
122/// Deployment and infrastructure relation kinds.
123const DEPLOYMENT_RELATIONS: &[GraphRelationKind] =
124    &[GraphRelationKind::Extended(ExtendedRelationKind::Deploys)];
125/// Static data-access relation kinds.
126const DATA_ACCESS_RELATIONS: &[GraphRelationKind] = &[
127    GraphRelationKind::Extended(ExtendedRelationKind::Reads),
128    GraphRelationKind::Extended(ExtendedRelationKind::Writes),
129];
130/// Empty persisted set for separately gated inferred families.
131const NO_RELATIONS: &[GraphRelationKind] = &[];
132
133/// Accepted direct and separately gated inferred relation families.
134pub 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/// Content-free settings projection of the accepted inventory.
246#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
247pub struct RelationFamilyInventoryReport {
248    /// Inventory schema version.
249    pub version: u32,
250    /// Digest over every accepted owner and coverage field.
251    pub digest: String,
252    /// Number of active direct families.
253    pub active_families: u32,
254    /// Number of separately gated inferred families.
255    pub optional_disabled_families: u32,
256}
257
258/// Validate the accepted inventory and return its stable digest.
259///
260/// # Errors
261///
262/// Returns an error when a row is incomplete, duplicated, or overlaps another
263/// accepted family's persisted relation kinds.
264pub 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/// Return the stable accepted-inventory digest.
298#[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/// Return the content-free settings report.
327///
328#[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
346/// Render the accepted inventory as its generated Markdown authority.
347///
348/// # Errors
349///
350/// Returns formatting errors from the in-memory Markdown writer.
351pub 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}