Skip to main content

ckm/
types.rs

1//! CKM type definitions — Rust implementation of INTERFACE.md v2.0.0.
2//!
3//! Every type here has a 1:1 correspondence with the Single Source of Truth interface definition.
4
5use std::collections::HashMap;
6
7use serde::{Deserialize, Serialize};
8
9/// Freeform extension data. Producers can attach arbitrary key-value pairs
10/// to any CKM entity. The SDK passes these through without validation.
11pub type Extensions = HashMap<String, serde_json::Value>;
12
13// ────────────────────────────────────────────────────────────────────────────
14// Section 2: Input Types (from ckm.json v2)
15// ────────────────────────────────────────────────────────────────────────────
16
17/// The set of portable primitive types, mapped to JSON Schema primitives.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum CanonicalType {
21    /// JSON string type.
22    String,
23    /// JSON boolean type.
24    Boolean,
25    /// JSON number type (floating point).
26    Number,
27    /// JSON integer type.
28    Integer,
29    /// JSON array type.
30    Array,
31    /// JSON object type.
32    Object,
33    /// JSON null type.
34    Null,
35    /// Any type (untyped).
36    Any,
37}
38
39impl CanonicalType {
40    /// Parses a string into a CanonicalType. Defaults to Object for unknown types.
41    pub fn parse(s: &str) -> Self {
42        match s.to_lowercase().as_str() {
43            "string" => Self::String,
44            "boolean" | "bool" => Self::Boolean,
45            "number" | "float" | "f64" => Self::Number,
46            "integer" | "int" | "i64" => Self::Integer,
47            "array" => Self::Array,
48            "object" => Self::Object,
49            "null" | "void" | "undefined" => Self::Null,
50            "any" | "unknown" => Self::Any,
51            _ => Self::Object,
52        }
53    }
54}
55
56impl std::fmt::Display for CanonicalType {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        let s = match self {
59            CanonicalType::String => "string",
60            CanonicalType::Boolean => "boolean",
61            CanonicalType::Number => "number",
62            CanonicalType::Integer => "integer",
63            CanonicalType::Array => "array",
64            CanonicalType::Object => "object",
65            CanonicalType::Null => "null",
66            CanonicalType::Any => "any",
67        };
68        write!(f, "{}", s)
69    }
70}
71
72/// A portable type reference with canonical mapping.
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct CkmTypeRef {
76    /// Language-agnostic canonical type.
77    pub canonical: CanonicalType,
78
79    /// Source language type annotation (e.g., `CalVerFormat`).
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub original: Option<String>,
82
83    /// Known enum values for string types.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub r#enum: Option<Vec<String>>,
86}
87
88/// A property within a concept.
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct CkmProperty {
92    /// Property name.
93    pub name: String,
94
95    /// Type reference (canonical + original).
96    pub r#type: CkmTypeRef,
97
98    /// Description from source documentation.
99    pub description: String,
100
101    /// Whether the property is required.
102    pub required: bool,
103
104    /// Default value. Null means no default.
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub default: Option<String>,
107}
108
109/// A domain concept extracted from source code.
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct CkmConcept {
113    /// Unique identifier (e.g., "concept-calver-config").
114    pub id: String,
115
116    /// Type name (e.g., `CalVerConfig`).
117    pub name: String,
118
119    /// Topic slug (e.g., "calver") — used for topic derivation.
120    pub slug: String,
121
122    /// One-line description.
123    pub what: String,
124
125    /// Semantic tags (e.g., ["config"]).
126    pub tags: Vec<String>,
127
128    /// Properties of the type, if applicable.
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub properties: Option<Vec<CkmProperty>>,
131
132    /// Validation rules extracted from remarks or constraint tags.
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub rules: Option<Vec<String>>,
135
136    /// Related concept names from @see tags or type references.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub related_to: Option<Vec<String>>,
139
140    /// Producer-defined extension data. CKM passes this through without validation.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub extensions: Option<Extensions>,
143}
144
145/// A function parameter within an operation.
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct CkmInput {
149    /// Parameter name.
150    pub name: String,
151
152    /// Type reference.
153    pub r#type: CkmTypeRef,
154
155    /// Whether the parameter is required.
156    pub required: bool,
157
158    /// Description from source documentation.
159    pub description: String,
160}
161
162/// A return value from an operation.
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct CkmOutput {
166    /// Type reference.
167    pub r#type: CkmTypeRef,
168
169    /// Description of the return value.
170    pub description: String,
171}
172
173/// A user-facing operation extracted from source code.
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct CkmOperation {
177    /// Unique identifier (e.g., "op-validate").
178    pub id: String,
179
180    /// Function name (e.g., "validate").
181    pub name: String,
182
183    /// One-line description.
184    pub what: String,
185
186    /// Semantic tags for topic linkage.
187    pub tags: Vec<String>,
188
189    /// Preconditions that must be met before invoking this operation.
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub preconditions: Option<Vec<String>>,
192
193    /// Function parameters.
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub inputs: Option<Vec<CkmInput>>,
196
197    /// Return value.
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub outputs: Option<CkmOutput>,
200
201    /// Exit codes and their meanings.
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub exit_codes: Option<std::collections::HashMap<String, String>>,
204
205    /// Checks or validations performed by this operation.
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub checks_performed: Option<Vec<String>>,
208
209    /// Producer-defined extension data.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub extensions: Option<Extensions>,
212}
213
214/// A rule enforced by the tool.
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct CkmConstraint {
218    /// Unique identifier (e.g., "constraint-future-date").
219    pub id: String,
220
221    /// Human-readable rule description.
222    pub rule: String,
223
224    /// Function or module that enforces the constraint.
225    pub enforced_by: String,
226
227    /// Severity level.
228    pub severity: Severity,
229
230    /// Config key that controls this constraint.
231    #[serde(default, skip_serializing_if = "Option::is_none")]
232    pub config_key: Option<String>,
233
234    /// Default value for the config key.
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub default: Option<String>,
237
238    /// Whether this constraint has security implications.
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub security: Option<bool>,
241
242    /// Producer-defined extension data.
243    #[serde(default, skip_serializing_if = "Option::is_none")]
244    pub extensions: Option<Extensions>,
245}
246
247/// Severity levels for constraints.
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
249#[serde(rename_all = "lowercase")]
250pub enum Severity {
251    /// Validation error — must be fixed.
252    Error,
253    /// Validation warning — should be addressed.
254    Warning,
255    /// Informational notice.
256    Info,
257}
258
259/// A single step within a workflow.
260#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
261#[serde(rename_all = "camelCase")]
262pub struct CkmWorkflowStep {
263    /// Discriminant: CLI command or manual action.
264    pub action: StepAction,
265
266    /// The command or instruction.
267    pub value: String,
268
269    /// Optional explanatory note.
270    #[serde(default, skip_serializing_if = "Option::is_none")]
271    pub note: Option<String>,
272
273    /// Expected outcome of this step.
274    #[serde(default, skip_serializing_if = "Option::is_none")]
275    pub expect: Option<String>,
276}
277
278/// Discriminant for workflow step types.
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280#[serde(rename_all = "lowercase")]
281pub enum StepAction {
282    /// A CLI command to execute.
283    Command,
284    /// A manual action for the user to perform.
285    Manual,
286}
287
288/// A multi-step workflow for achieving a common goal.
289#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
290#[serde(rename_all = "camelCase")]
291pub struct CkmWorkflow {
292    /// Unique identifier.
293    pub id: String,
294
295    /// What the workflow achieves.
296    pub goal: String,
297
298    /// Semantic tags.
299    pub tags: Vec<String>,
300
301    /// Ordered steps (minimum 1).
302    pub steps: Vec<CkmWorkflowStep>,
303
304    /// Producer-defined extension data.
305    #[serde(default, skip_serializing_if = "Option::is_none")]
306    pub extensions: Option<Extensions>,
307}
308
309/// A configuration schema entry.
310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub struct CkmConfigEntry {
313    /// Dotted key path (e.g., "calver.format").
314    pub key: String,
315
316    /// Type reference.
317    pub r#type: CkmTypeRef,
318
319    /// Description.
320    pub description: String,
321
322    /// Default value. Null means no default.
323    #[serde(default, skip_serializing_if = "Option::is_none")]
324    pub default: Option<String>,
325
326    /// Whether the config entry is required.
327    pub required: bool,
328
329    /// Downstream effect or behavior this config entry controls.
330    #[serde(default, skip_serializing_if = "Option::is_none")]
331    pub effect: Option<String>,
332
333    /// Producer-defined extension data.
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub extensions: Option<Extensions>,
336}
337
338/// A producer-declared topic grouping for the manifest.
339///
340/// When present in the manifest, these override engine-derived topics.
341/// This gives generators full control over topic structure.
342#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct CkmDeclaredTopic {
345    /// Topic slug (e.g., "getting-started").
346    pub name: String,
347
348    /// One-line summary.
349    pub summary: String,
350
351    /// IDs of concepts belonging to this topic.
352    #[serde(default, skip_serializing_if = "Vec::is_empty")]
353    pub concept_ids: Vec<String>,
354
355    /// IDs of operations belonging to this topic.
356    #[serde(default, skip_serializing_if = "Vec::is_empty")]
357    pub operation_ids: Vec<String>,
358
359    /// IDs of constraints belonging to this topic.
360    #[serde(default, skip_serializing_if = "Vec::is_empty")]
361    pub constraint_ids: Vec<String>,
362
363    /// Config key prefixes belonging to this topic.
364    #[serde(default, skip_serializing_if = "Vec::is_empty")]
365    pub config_keys: Vec<String>,
366}
367
368/// Provenance metadata about the manifest source.
369#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
370#[serde(rename_all = "camelCase")]
371pub struct CkmMeta {
372    /// Project name (e.g., "my-tool").
373    pub project: String,
374
375    /// Source language (e.g., "typescript", "python", "rust").
376    pub language: String,
377
378    /// Tool that generated the manifest (e.g., "forge-ts@0.21.1").
379    pub generator: String,
380
381    /// ISO 8601 timestamp of generation.
382    pub generated: String,
383
384    /// Optional URL to source repository.
385    #[serde(default, skip_serializing_if = "Option::is_none")]
386    pub source_url: Option<String>,
387}
388
389/// The top-level CKM manifest object (v2).
390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
391#[serde(rename_all = "camelCase")]
392pub struct CkmManifest {
393    /// Schema URL (e.g., `https://ckm.dev/schemas/v2.json`).
394    #[serde(rename = "$schema")]
395    pub schema: String,
396
397    /// Schema version (e.g., "2.0.0").
398    pub version: String,
399
400    /// Project metadata and provenance.
401    pub meta: CkmMeta,
402
403    /// Domain concepts (interfaces, types).
404    pub concepts: Vec<CkmConcept>,
405
406    /// User-facing operations (functions).
407    pub operations: Vec<CkmOperation>,
408
409    /// Enforced rules.
410    pub constraints: Vec<CkmConstraint>,
411
412    /// Multi-step workflows.
413    pub workflows: Vec<CkmWorkflow>,
414
415    /// Configuration schema entries.
416    pub config_schema: Vec<CkmConfigEntry>,
417
418    /// Producer-declared topics. When present, these override engine-derived topics.
419    /// This gives generators full control over topic grouping.
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub topics: Option<Vec<CkmDeclaredTopic>>,
422
423    /// Producer-defined extension data at the manifest level.
424    #[serde(default, skip_serializing_if = "Option::is_none")]
425    pub extensions: Option<Extensions>,
426}
427
428// ────────────────────────────────────────────────────────────────────────────
429// Section 3: Derived Types (computed by the engine)
430// ────────────────────────────────────────────────────────────────────────────
431
432/// An auto-derived topic grouping related concepts, operations, config, and constraints.
433#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
434#[serde(rename_all = "camelCase")]
435pub struct CkmTopic {
436    /// Slug used as CLI argument (e.g., "calver").
437    pub name: String,
438
439    /// One-line description (from the primary concept).
440    pub summary: String,
441
442    /// Related concepts.
443    pub concepts: Vec<CkmConcept>,
444
445    /// Related operations.
446    pub operations: Vec<CkmOperation>,
447
448    /// Related config entries.
449    pub config_schema: Vec<CkmConfigEntry>,
450
451    /// Related constraints.
452    pub constraints: Vec<CkmConstraint>,
453}
454
455/// A summary entry for the topic index.
456#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
457#[serde(rename_all = "camelCase")]
458pub struct CkmTopicIndexEntry {
459    /// Topic slug.
460    pub name: String,
461
462    /// One-line description.
463    pub summary: String,
464
465    /// Count of related concepts.
466    pub concepts: usize,
467
468    /// Count of related operations.
469    pub operations: usize,
470
471    /// Count of related config entries.
472    pub config_fields: usize,
473
474    /// Count of related constraints.
475    pub constraints: usize,
476}
477
478/// Aggregate manifest counts.
479#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
480#[serde(rename_all = "camelCase")]
481pub struct CkmManifestCounts {
482    /// Total concepts in manifest.
483    pub concepts: usize,
484
485    /// Total operations.
486    pub operations: usize,
487
488    /// Total constraints.
489    pub constraints: usize,
490
491    /// Total workflows.
492    pub workflows: usize,
493
494    /// Total config entries.
495    pub config_schema: usize,
496}
497
498/// The full topic index returned by `topic_json()` with no argument.
499#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
500#[serde(rename_all = "camelCase")]
501pub struct CkmTopicIndex {
502    /// All topic summaries.
503    pub topics: Vec<CkmTopicIndexEntry>,
504
505    /// Aggregate manifest counts.
506    pub ckm: CkmManifestCounts,
507}
508
509/// Manifest statistics returned by `inspect()`.
510#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
511#[serde(rename_all = "camelCase")]
512pub struct CkmInspectResult {
513    /// Manifest metadata.
514    pub meta: CkmMeta,
515
516    /// Counts of each manifest section.
517    pub counts: CkmInspectCounts,
518
519    /// List of derived topic slugs.
520    pub topic_names: Vec<String>,
521}
522
523/// Counts for the inspect result.
524#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
525#[serde(rename_all = "camelCase")]
526pub struct CkmInspectCounts {
527    /// Number of concepts.
528    pub concepts: usize,
529    /// Number of operations.
530    pub operations: usize,
531    /// Number of constraints.
532    pub constraints: usize,
533    /// Number of workflows.
534    pub workflows: usize,
535    /// Number of config keys.
536    pub config_keys: usize,
537    /// Number of derived topics.
538    pub topics: usize,
539}
540
541/// A single validation error with a JSON pointer path.
542#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
543#[serde(rename_all = "camelCase")]
544pub struct CkmValidationError {
545    /// JSON pointer path (e.g., "/concepts/0/slug").
546    pub path: String,
547
548    /// Human-readable error message.
549    pub message: String,
550}
551
552/// Result of manifest validation.
553#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
554#[serde(rename_all = "camelCase")]
555pub struct CkmValidationResult {
556    /// Whether the manifest is valid.
557    pub valid: bool,
558
559    /// Validation errors (empty if valid).
560    pub errors: Vec<CkmValidationError>,
561}
562
563/// Error returned when a topic is not found.
564#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
565#[serde(rename_all = "camelCase")]
566pub struct CkmErrorResult {
567    /// Error message (e.g., "Unknown topic: foo").
568    pub error: String,
569
570    /// Available topic names for suggestion.
571    pub topics: Vec<String>,
572}
573
574/// The result of `topic_json()` — either a topic index, a single topic, or an error.
575#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
576#[serde(untagged)]
577pub enum TopicJsonResult {
578    /// Full topic index (when no topic name is given).
579    Index(CkmTopicIndex),
580
581    /// Single topic detail (when topic name matches).
582    Topic(CkmTopic),
583
584    /// Error result (when topic name does not match).
585    Error(CkmErrorResult),
586}