Skip to main content

memstead_schema/
manifest.rs

1//! Schema manifest (`schema.yaml`) — the outer envelope declaring a schema
2//! package: name, version, type list, relationship vocabulary, community
3//! defaults, and LLM-facing documentation.
4
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
9#[serde(deny_unknown_fields)]
10pub struct SchemaManifest {
11    pub name: String,
12    /// Semver string — parsed into `semver::Version` by the loader.
13    pub version: String,
14    pub description: String,
15    pub when_to_use: String,
16    #[serde(default)]
17    pub system_message: Option<String>,
18    pub types: Vec<String>,
19    pub relationships: RelationshipVocabulary,
20    pub community: CommunityConfig,
21    /// Schema-generic writing guidance — `avoid` and `goal` prose that
22    /// applies to every mem pinned to this schema. The plugin layer
23    /// concatenates these with per-mem `writeGuidance.avoid_additions`
24    /// / `goal_additions` (an opaque pass-through on the engine side —
25    /// see `MemConfig::write_guidance`'s contract).
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub default_writing_guidance: Option<DefaultWritingGuidance>,
28    /// Outbound cross-mem relationship vocabulary, per target schema
29    /// domain. Each entry names a target schema (bare name — never a
30    /// version; eligibility is name-based) and lists rel-types that may
31    /// cross the boundary in that direction. Absent or `[]` means the
32    /// schema declares no outbound cross-mem edges. Source-ownership
33    /// only — third-party bridge schemas are not modelled; each
34    /// direction is owned by exactly one schema.
35    #[serde(default, skip_serializing_if = "Vec::is_empty")]
36    pub cross_mem_relationships: Vec<CrossMemRelationshipEntry>,
37    /// Schema-level pointer naming the rel-type that body wiki-links
38    /// `[[target]]` should auto-emit as engine-synthesised relations.
39    /// `None` (default) means the schema is opt-out of alias synthesis
40    /// — unbacked body wiki-links continue to refuse with
41    /// `WIKILINK_WITHOUT_RELATION`. When set, the named rel-type must
42    /// be declared in `relationships.definitions` or schema load
43    /// fails with `SchemaLoadError::AliasTargetRelTypeNotDeclared`.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub alias_target_rel_type: Option<String>,
46}
47
48/// One outbound cross-mem declaration — a target schema domain
49/// (named, never versioned) and the rel-types admitted in that
50/// direction.
51///
52/// `target_types` strings within each definition live in the target
53/// schema's namespace by construction — the source schema's loader
54/// accepts them as opaque since the target schema may not be present
55/// at source-schema load time.
56#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
57#[serde(deny_unknown_fields)]
58pub struct CrossMemRelationshipEntry {
59    /// Bare name of the target schema — the domain identity. A version
60    /// suffix (`software@1.0.0`) or range (`software@^1.0`) is rejected
61    /// at schema load: cross-mem eligibility is name-based, so the
62    /// declaration is satisfied by a target mem pinning *any* version
63    /// of the named schema.
64    pub to_schema: String,
65    pub definitions: Vec<RelationshipDef>,
66}
67
68/// Schema-level writing-guidance defaults. Both fields are optional so a
69/// schema can ship `avoid` without a `goal` (or vice versa). The engine
70/// surfaces them via `build_schema_payload` at the top level of the
71/// schema-payload JSON; resolution (concatenation with mem additions)
72/// lives in the plugin layer.
73#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
74#[serde(deny_unknown_fields)]
75pub struct DefaultWritingGuidance {
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub avoid: Option<String>,
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub goal: Option<String>,
80}
81
82#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
83#[serde(deny_unknown_fields)]
84pub struct RelationshipVocabulary {
85    pub mode: RelationshipMode,
86    pub definitions: Vec<RelationshipDef>,
87    /// Acyclicity over SETS of rel-types: a write that closes a cycle
88    /// in the union subgraph of a set refuses, with a path that may
89    /// mix the set's rel-types. Each inner list names two or more
90    /// declared rel-types; a name may appear in at most one set
91    /// (overlapping sets have no coherent refusal), and a single-name
92    /// set refuses at load (that is the per-definition `acyclic`
93    /// flag). Per-relationship `acyclic: true` keeps its exact
94    /// meaning and may coexist. Empty default keeps current
95    /// behaviour.
96    #[serde(default, skip_serializing_if = "Vec::is_empty")]
97    pub acyclic_sets: Vec<Vec<String>>,
98    /// Grounded-labelling declaration (see [`LabellingDef`]): which of
99    /// this schema's rel-types constitute the attack relation, and
100    /// optionally a support walk enabling chain-shape statistics.
101    /// Absent = no computation, byte-identical responses.
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub labelling: Option<LabellingDef>,
104}
105
106/// The schema's attack vocabulary for the grounded labelling — the
107/// one argumentation-semantics computation that is parameter-free,
108/// unique, polynomial, and explainable by construction. The engine
109/// serves the labelling as a reported observation with its evidence:
110/// never a stored value, never a write gate, never a status. The
111/// labelling is deliberately support-blind (it walks attack edges
112/// only); a defeated supporter never flips what it supports — the
113/// reader sees that defeat through the shape block's counts instead.
114#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
115#[serde(deny_unknown_fields)]
116pub struct LabellingDef {
117    /// Declared rel-types constituting attack — inline relation set,
118    /// at least one name, each declared in the vocabulary.
119    pub attack: Vec<String>,
120    /// Optional support walk enabling the chain-shape statistics
121    /// (depth, branching, terminal share, defeated/undecided counts
122    /// on the support subtree). Absent = no shape block served.
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub support: Option<SupportWalk>,
125}
126
127/// The support walk of a labelling declaration — same grammar as a
128/// `must_reach` block: an inline relation set, a walk direction, and
129/// the terminal types that count as ground.
130#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
131#[serde(deny_unknown_fields)]
132pub struct SupportWalk {
133    pub relationships: Vec<String>,
134    /// `out` follows edges pointing away from the walked entity, `in`
135    /// follows edges pointing at it — the `must_reach` vocabulary.
136    pub direction: crate::types::ReachDirection,
137    pub terminal_types: Vec<String>,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
141#[serde(rename_all = "lowercase")]
142pub enum RelationshipMode {
143    Strict,
144    Open,
145}
146
147#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
148#[serde(deny_unknown_fields)]
149pub struct RelationshipDef {
150    pub name: String,
151    pub description: String,
152    #[serde(default)]
153    pub when_to_use: Option<String>,
154    pub default_weight: f32,
155    /// Per-edge description posture for edges of this rel-type:
156    /// `forbidden` (default) rejects any trailing description text;
157    /// `optional` accepts edges with or without a description;
158    /// `required` rejects edges without a description. The schema
159    /// author opts a catch-all rel-type (e.g. `OTHER`) into
160    /// `required` to force per-edge documentation; most rel-types
161    /// keep the default `forbidden` posture so the rel-type's name
162    /// is the edge's documentation.
163    #[serde(default)]
164    pub per_edge_description: PerEdgeDescription,
165    /// When true, the engine rejects writes that would close a cycle in the
166    /// subgraph restricted to edges of this relationship type. Defaults to
167    /// false so existing user schemas stay opt-in. Semantically meaningless
168    /// on the `_default` sentinel (never a real edge's rel_type).
169    #[serde(default)]
170    pub acyclic: bool,
171    /// When true, edges of this rel-type declare that the SOURCE
172    /// derives from the TARGET (agent-trust plan 12). Exactly three
173    /// effects, warn-tier forever: (1) explicitly writing such an
174    /// edge records the target's current content hash as the edge's
175    /// baseline in the engine-owned derivations sidecar (never in the
176    /// markdown, never in `_hash`); (2) the include-gated
177    /// `stale_derivations` health axis reports every such edge whose
178    /// target's current hash differs from its baseline — and edges
179    /// with no baseline as `unbaselined`, distinctly; (3) a
180    /// duplicate-add `memstead_relate` on such an edge refreshes the
181    /// baseline as its one effect — the agent's explicit "reviewed,
182    /// still holds" — and the response says so. Never a write-block.
183    #[serde(default)]
184    pub derivation: bool,
185    /// Schema-declared types whose entities may be the source of this
186    /// edge. Empty (default) means shape-free — any source type admitted.
187    /// The loader validates every entry against the schema's declared
188    /// types list; unknown names raise `SchemaLoadError::UndeclaredType`.
189    /// At write time, `memstead_relate` rejects shape violations with
190    /// `INVALID_REL_SHAPE`.
191    #[serde(default, skip_serializing_if = "Vec::is_empty")]
192    pub source_types: Vec<String>,
193    /// Same as `source_types` but for the target side. Empty = shape-free.
194    #[serde(default, skip_serializing_if = "Vec::is_empty")]
195    pub target_types: Vec<String>,
196    /// Per-source cardinality hint, parsed and stored on the
197    /// relationship definition. Declarative only — the engine does not
198    /// currently enforce it or warn when a relate pushes the source's
199    /// outgoing count for this rel_type outside the declared range.
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub cardinality_per_source: Option<Cardinality>,
202    /// Manual-authoring posture for this rel-type. `allow` (default)
203    /// admits explicit `memstead_relate` calls. `warn` lands the relation
204    /// with a `RELATION_MANUAL_AUTHORING_NOT_RECOMMENDED` warning.
205    /// `forbidden` refuses explicit-author calls with the typed
206    /// `RELATION_MANUAL_AUTHORING_FORBIDDEN` code. The body-link →
207    /// relation alias machinery (`memstead_update` / `memstead_create`'s
208    /// wiki-link parser) is NOT gated — schema-emitted relations like
209    /// REFERENCES synthesise unchanged.
210    #[serde(default)]
211    pub manual_authoring: ManualAuthoring,
212}
213
214/// Per-edge description posture declared on a `RelationshipDef`.
215///
216/// `Forbidden` (the default) rejects any trailing description text on
217/// edges of this rel-type. `Optional` accepts both shapes. `Required`
218/// rejects edges without a description — the schema author opts a
219/// catch-all rel-type into this so every edge carries its own
220/// rationale.
221#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
222#[serde(rename_all = "lowercase")]
223pub enum PerEdgeDescription {
224    #[default]
225    Forbidden,
226    Optional,
227    Required,
228}
229
230/// Manual-authoring posture declared per `RelationshipDef`.
231///
232/// `Allow` (default) is the no-op posture for every rel-type a user
233/// or agent may author explicitly via `memstead_relate`. `Warn` lands the
234/// relation but surfaces a warning so the audit trail records the
235/// drift. `Forbidden` refuses with a typed code — used for rel-types
236/// the engine emits via the body-link → relation alias machinery
237/// (e.g. REFERENCES), where explicit authoring duplicates work and
238/// often masks the author's intent. The schema's `when_to_use` text
239/// rides on the wire as recovery guidance.
240#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
241#[serde(rename_all = "lowercase")]
242pub enum ManualAuthoring {
243    #[default]
244    Allow,
245    Warn,
246    Forbidden,
247}
248
249/// Allowed cardinality ranges for `RelationshipDef::cardinality_per_source`.
250/// Stringly-typed parsing rejected — typos surface at YAML load time via
251/// `serde`, the warning builder gets exhaustive matches, and the wire
252/// payload renders via `Display`.
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
254pub enum Cardinality {
255    #[serde(rename = "1")]
256    One,
257    #[serde(rename = "0..1")]
258    ZeroOrOne,
259    #[serde(rename = "1..N")]
260    OneOrMore,
261    #[serde(rename = "0..N")]
262    ZeroOrMore,
263}
264
265impl std::fmt::Display for Cardinality {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        let s = match self {
268            Cardinality::One => "1",
269            Cardinality::ZeroOrOne => "0..1",
270            Cardinality::OneOrMore => "1..N",
271            Cardinality::ZeroOrMore => "0..N",
272        };
273        f.write_str(s)
274    }
275}
276
277impl Cardinality {
278    /// Returns `true` iff `count` falls inside the allowed range. Used by
279    /// `memstead_relate` to predict whether a post-mutation outgoing count
280    /// would violate the schema's intent.
281    pub fn admits(&self, count: usize) -> bool {
282        match self {
283            Cardinality::One => count == 1,
284            Cardinality::ZeroOrOne => count <= 1,
285            Cardinality::OneOrMore => count >= 1,
286            Cardinality::ZeroOrMore => true,
287        }
288    }
289}
290
291/// Community-detection (Louvain) defaults — schema-level, not per-type.
292///
293/// Distinct from the legacy `schemas::CommunityConfig` which was attached to
294/// each `TypeDefinition`; the legacy variant has been removed.
295#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
296#[serde(deny_unknown_fields)]
297pub struct CommunityConfig {
298    pub resolution: f64,
299    pub seed: u32,
300}