About calm
Common Architecture Language Model (CALM) Specification - LinkML Schema
Solution Design
Overview
This package provides a LinkML representation of the FINOS CALM 1.2 specification, enabling CALM architecture documents to participate in the broader Linked Data ecosystem. The design follows a three-stage pipeline:
CALM 1.2 JSON-Schema meta files
│
▼
calm_to_linkml.py ← Stage 1: schema generation
│
▼
src/calm/schema/calm.yaml ← authoritative LinkML schema (941 lines)
│
├──▶ apply_sssom_overlay.py ← Stage 2: semantic mapping
│ │
│ ▼
│ src/calm/mappings/*.sssom.tsv
│ (12 SSSOM mapping sets, 68 links)
│
└──▶ gen-project / gen-json-schema / gen-python … ← Stage 3: artefacts
Stage 1 - Schema Generation (scripts/calm_to_linkml.py)
The CALM specification is authored as a set of JSON-Schema meta files (one per vocabulary domain). calm_to_linkml.py reads those files at build time and emits a single consolidated LinkML YAML schema.
Key design decisions:
| Decision | Rationale |
|---|---|
| Autogenerated schema, not hand-authored | CALM's JSON-Schema meta is the single source of truth; a generator keeps the LinkML schema in sync across CALM releases without manual editing. |
SLOT_ENRICHMENTS dict |
JSON-Schema properties carry no semantic metadata. Hand-curated overlays add slot_uri, aliases, range overrides, identifier, and minimum_value constraints on top of what can be inferred automatically. |
CLASS_ENRICHMENTS dict |
Adds tree_root, class_uri, in_subset, close_mappings, and per-class slot_usage (e.g. inlined_as_list: true) that cannot be derived from JSON-Schema alone. |
Per-class slot_usage for inlining |
CALM reuses slots (e.g. nodes) in two distinct semantic roles: embedded objects in Architecture/Decision, and string ID references in relationship subtypes. Global inlined_as_list on the slot breaks relationship subtypes; per-class slot_usage applies inlining only where required. |
None-sentinel in overlay |
LinkML's gen-json-schema emits a broken $ref + anyOf[string, string] when a slot has both any_of (from a JSON-Schema anyOf) and inlined_as_list: true. Setting any_of: None in SLOT_ENRICHMENTS deletes the generated any_of key, pinning a single concrete range and producing a valid JSON Schema. See upstream-releases/ISSUE.md for the upstream bug report. |
DEFINITION_CLASSES map |
Many CALM JSON-Schema $defs are anonymous helper types. This map resolves (filename, def-name) pairs to canonical LinkML class names, keeping class naming stable across CALM releases. |
Generated schema statistics:
- 32 classes (8 enums / types, 24 data classes) across 7 subsets:
core,controls_framework,flow_modeling,interface_defs,decorators,timeline,units - 56 slots, enriched with slot URIs, aliases, identifier flags, and range constraints
- Schema file:
src/calm/schema/calm.yaml(~941 lines)
Stage 2 - Semantic Mapping (scripts/apply_sssom_overlay.py)
CALM concepts are mapped to well-known ontologies and standards using SSSOM (Simple Standard for Sharing Ontological Mappings). Twelve mapping sets are maintained as TSV files under src/calm/mappings/.
Mapping targets (12 files, 68 total mappings):
| File | Standard |
|---|---|
calm-to-attack.sssom.tsv |
MITRE ATT&CK |
calm-to-bpmn.sssom.tsv |
BPMN (FluxNova model) |
calm-to-capec.sssom.tsv |
MITRE CAPEC |
calm-to-cis-controls.sssom.tsv |
CIS Critical Security Controls |
calm-to-dpv.sssom.tsv |
W3C Data Privacy Vocabulary |
calm-to-gist.sssom.tsv |
Semantic Arts gist ontology |
calm-to-iso27001.sssom.tsv |
ISO/IEC 27001 |
calm-to-nist-csf-v2.sssom.tsv |
NIST Cybersecurity Framework v2 |
calm-to-nist-sp-800-53.sssom.tsv |
NIST SP 800-53 |
calm-to-ocsf.sssom.tsv |
Open Cybersecurity Schema Framework |
calm-to-oscal.sssom.tsv |
NIST OSCAL |
calm-to-stix.sssom.tsv |
STIX 2.1 |
apply_sssom_overlay.py reads all mapping files and writes exact_mappings, close_mappings, and related_mappings predicates directly into calm.yaml. The overlay is idempotent - it overwrites only mapping keys and leaves all other schema content unchanged.
Stage 3 - Artefact Generation
Standard LinkML generators consume calm.yaml to produce downstream artefacts via just gen-project:
| Generator | Output |
|---|---|
gen-json-schema |
JSON Schema for validating CALM instance documents |
gen-python |
Python dataclasses with runtime validation |
gen-docs |
MkDocs-ready Markdown (published to docs/elements/) |
gen-owl |
OWL ontology for semantic reasoning |
gen-shacl |
SHACL shapes for RDF graph validation |
gen-prefixmap |
Canonical prefix registry |
Data Validation
Test fixtures in tests/data/ validate that the generated schema correctly accepts and rejects CALM instance documents:
- 23 valid fixtures (
tests/data/valid/) - one per major class, exercising required fields and embedded object inlining - 25 invalid fixtures (
tests/data/invalid/) - one violation per fixture (missing required field, value belowminimum_value, or disallowed additional property) - 10 valid vendor fixtures (
tests/data/chandralanka/valid/) - real-world "in-the-wild" data derived from the CALM examples repository (e-commerce platform architecture with actors, services, databases, and all relationship types) - 3 invalid vendor fixtures (
tests/data/chandralanka/invalid/) - vendor data with required fields omitted
Tests call linkml-validate --target-class <ClassName> <file.yaml> via subprocess. The target class is derived from the filename stem (ClassName-description.yaml → ClassName). All 48 parameterised tests run in tests/test_data.py.
The just test recipe runs both the core fixtures and vendor fixtures via linkml-run-examples.
Build Commands
# Regenerate schema from CALM 1.2 JSON-Schema sources
just gen-linkml # runs calm_to_linkml.py then apply_sssom_overlay.py
# Generate all downstream artefacts (docs, JSON Schema, Python, OWL, ...)
just gen-project
# Run all tests
uv run pytest tests/
# Run only data-validation tests
uv run pytest tests/test_data.py
# Lint the schema
uv run linkml-lint src/calm/schema/calm.yaml
Known Limitations
controlsslot - CALM'scontrolsproperty uses a JSON-SchemapatternPropertiesmap keyed by control ID. LinkML has no nativepatternPropertiesequivalent; the slot is emitted withrange: Control(a single nullable object) and the map structure is lost. Instance documents that embed an array of controls will fail schema validation.InterfaceType- Theany_of: [InterfaceDefinition, InterfaceType]union on theinterfacesslot is collapsed torange: InterfaceDefinitionto work around a LinkML upstream bug (seeupstream-releases/ISSUE.md).InterfaceTypeobjects are not currently validatable vialinkml-validate.additionalProperties- JSON SchemaadditionalProperties: falseconstraints are generated for most classes, butlinkml-validateenforces them inconsistently for empty/null documents.