Interchange

longeron.export

Exporters: model -> JSON and model -> SysML v2 textual notation.

to_sysml regenerates parseable textual notation from a model (whether it was parsed from text or built programmatically); to_dict/to_json serialize the model structure, with expressions carried both as structured trees and rendered text.

longeron.export.ExportFormat

the on-disk formats save() writes (inferred from the file suffix when not given explicitly)

alias of Literal[‘sysml’, ‘kerml’, ‘json’]

longeron.export.fmt_qname(qname)[source]

Format a stored qualified-name string (::/. separated).

Return type:

str

longeron.export.doc_comment_body(text)[source]

The canonical /* ... */ body for documentation text.

Single-line text becomes /* text */; multi-line text uses the conventional *-prefixed continuation lines. This form is a textual-export fixpoint: stripping it back to text (text()) and re-rendering yields the identical body, no matter how deeply the owner is indented – which is why _Printer.emit_Documentation() re-renders multi-line bodies through it instead of echoing them verbatim (verbatim multi-line bodies accumulate indentation on every parse/print cycle). longeron.edit.set_doc() writes bodies in this same form.

Return type:

str

longeron.export.to_dict(element)[source]

Convert a model element (or expression) to JSON-able data.

Return type:

dict[str, Any] | None

longeron.export.save(element, path, fmt=None)[source]

Write a model element to disk as .sysml, .kerml, or .json.

The format is inferred from the file suffix unless given explicitly.

Return type:

None

longeron.export.workspace_plan(model, changes, indent=4)[source]

The files a workspace save would rewrite, and their new text.

model is a directory-loaded model whose top-level members carry the per-file breadcrumbs longeron.workspace.load_dir() stamps; changes is its tracked edit record (longeron.edit.Tracker.changes – any iterable of (op, qname, detail) tuples). Each edit maps to its top-level member (through the change records’ detail["tops"] breadcrumbs; a rename maps to every top-level member its cascade rewrote references in), and every mapped member’s source file is re-rendered whole with to_sysml(); files whose regenerated text already matches the disk content are dropped, so untouched files are never rewritten. Returns {path: new_text} in model member order.

Honest refusals (SysMLError, before anything is written): a top-level member with no recorded source file, or an edit that no longer names one.

Return type:

dict[Path, str]

longeron.export.save_workspace(model, changes, indent=4)[source]

Write a directory-loaded model’s tracked edits back, file by file.

The write half of workspace_plan() (same arguments, same refusals – and a refusal writes NOTHING): each file whose regenerated content differs from disk is rewritten in to_sysml() canonical form; every other file is left untouched, byte for byte. Returns the paths written, in model member order.

Return type:

list[Path]

longeron.export.indent_string(indent)[source]

Normalize an indent argument (space count or literal string).

Return type:

str

longeron.export.find_emitter(printer, element, prefix='emit_')[source]

Look up <prefix><ClassName> on printer along the element’s MRO.

Shared dispatch helper for the textual printers (_Printer here and longeron.kerml._KerMLPrinter): the most specific handler wins, and None signals “no handler” so each printer keeps its own unknown-element failure behavior.

Return type:

Callable[[Any, int], None] | None

longeron.export.to_sysml(element, indent=4)[source]

Render a model element (usually a Model or Package) to SysML v2 textual notation.

indent is a number of spaces (or, for back-compat, a literal indentation string).

Return type:

str

longeron.importer

Rebuild models from their JSON export (longeron.export.to_dict()).

The JSON produced by to_dict/to_json is lossless for the model layer: from_dict/from_json reconstruct the same element tree, so JSON is a first-class interchange format alongside the textual notation:

model = longeron.loads("package P { part def X; }")
clone = longeron.from_json(longeron.to_json(model))
assert longeron.to_dict(clone) == longeron.to_dict(model)
longeron.importer.from_dict(data)[source]

Reconstruct a model element from longeron.export.to_dict() data.

Return type:

Element

longeron.importer.from_json(text)[source]

Parse to_json output back into a model.

A non-Model root element (e.g. a serialized package) is wrapped in a fresh Model so the result is always executable.

Return type:

Model

longeron.kerml

Best-effort projection of a SysML model onto KerML textual notation.

SysML v2 is defined as an extension of KerML: every SysML definition kind maps to a kernel metatype (part def -> struct, calc def -> function, constraint -> inv/predicate, …). to_kerml renders that projection for the structural subset of a model. It is one-way and lossy: behavioral statements (assignments, control flow, transitions), connections, views, and metadata have no kernel-level textual equivalent here and are emitted as /* omitted: ... */ comments.

The output is guaranteed parseable by the bundled KerML grammar (this is enforced by the test suite).

longeron.kerml.to_kerml(element, indent=4)[source]

Render a model element as KerML textual notation (see module doc).

indent is a number of spaces (or, for back-compat, a literal indentation string).

Return type:

str

longeron.ecore

Requires the ecore extra (pip install "longeron[ecore]").

Bridge to the OMG SysML v2 specification metamodel (Stage B prototype).

The pragmatic dataclasses in longeron.model are shaped like the textual notation. The OMG abstract syntax is different: ~175 metaclasses where ownership goes through reified OwningMembership elements and every specialization/typing is itself an element. This module projects a longeron model onto that abstract syntax using the pilot implementation’s published SysML.ecore (vendored under longeron/_spec/) and pyecore.

Scope (prototype): element skeletons, names, common flags, reified memberships, and Specialization / FeatureTyping / Subsetting / Redefinition relationships for targets that resolve inside the model. View persistence projects too: expose becomes MembershipExpose / NamespaceExpose records (targets wired when they resolve in-model) and filter becomes ElementFilterMembership records whose condition Expression carries the rendered text as a TextualRepresentation. Expression trees and unresolved (standard-library) references are counted in the report, not mapped. Requires the ecore extra: pip install longeron[ecore].

class longeron.ecore.SpecReport(elements=0, memberships=0, relationships=0, skipped_elements=<factory>, unresolved_references=<factory>)[source]

Bases: object

What the projection covered (and what it had to skip).

class longeron.ecore.SpecModel(root, report, instances=None)[source]

Bases: object

A model projected onto the OMG abstract syntax.

instances

id(model element) -> EObject for projections built by to_spec() (None for models rebuilt from API records); only meaningful while the source model is alive

longeron.ecore.spec_metamodel()[source]

The vendored OMG SysML v2 EPackage (pyecore).

Return type:

Any

longeron.ecore.spec_class(name)[source]

Look up a metaclass (EClass) by name, e.g. PartDefinition.

Return type:

Any

longeron.ecore.to_spec(model)[source]

Project a model onto spec abstract-syntax instances.

Return type:

SpecModel

longeron.api

Requires the ecore extra (pip install "longeron[ecore]").

Relationship records carry the derived source/target endpoint arrays by default (to_api_records(..., derived=True)): the OMG pilot-implementation API servers serialize these derived properties, and pilot-ecosystem consumers (pymbe, for one) use their presence to recognize relationship records and navigate the model graph — an export without them loads but is unnavigable. Pass derived=False (CLI: longeron export --format api --no-derived) for minimal records restricted to stored features; round trips are lossless either way.

OMG Systems Modeling API JSON interchange (Stage E prototype).

SysML v2 tools exchange models through the “Systems Modeling API & Services” JSON: one flat record per element, @type naming the spec metaclass, @id/elementId UUIDs, and every reference expressed as {"@id": ...}. This module rides on the longeron.ecore projection (so it inherits its scope and its pyecore requirement):

records = longeron.api.to_api_json(model) # model -> API JSON clone = longeron.api.model_from_api_json(records) # API JSON -> Model (inverse) spec = longeron.api.spec_from_api_json(records) # API JSON -> spec instances

The export is a structural prototype: element skeletons, names, flags, memberships, specialization/typing relationships, and the view-persistence records (exposes and element filters) – not expression trees. See longeron.ecore for what is and is not projected. Relationship records carry the derived source/target endpoint arrays by default (derived=True), matching what the OMG pilot- implementation API servers serialize – see to_api_records().

longeron.api.to_api_records(model, *, implied=False, derived=True)[source]

Flat API-style records for a model (or an existing projection).

With derived=True (the default), every relationship record also carries the spec-derived source and target endpoint arrays ([{"@id": ...}, ...]), computed from the relationship’s role features: subclassifier/superclassifier for a Subclassification, typedFeature/type for a FeatureTyping, the owning namespace / owned member for memberships, and so on (the role-to-endpoint mapping is read from the redefines / subsets annotation chains in the vendored spec Ecore, not hard-coded). The OMG pilot-implementation API servers always serialize these derived properties, and pilot-ecosystem consumers (e.g. pymbe) rely on them both to recognize relationship records and to navigate the model graph – without them an export loads but is unnavigable. Records whose endpoints are not derivable (an import whose target never resolved, a bare Dependency) simply omit the fields. Pass derived=False for minimal records restricted to stored features. Round-trips are lossless either way: spec_from_api_records() accepts records with or without the endpoint fields, and a re-export reproduces them.

With implied=True (requires a Model), the implied standard-library specializations (Resolver.implied_generals: a plain part def specializes Parts::Part, a plain part usage subsets Parts::parts, …) are emitted too, as additional Subclassification/Subsetting records flagged "isImplied": true. Off by default: the extra records reference library elements that are not part of the export (their @id is a deterministic UUID of the library element’s qualified name), which would break lossless round-trips.

Return type:

list[dict[str, Any]]

longeron.api.spec_from_api_records(records)[source]

Rebuild spec (pyecore) instances from API records.

This is the spec-level import: the result mirrors the metamodel instances the records serialize. It is not the inverse of to_api_records() – for API records back to a longeron.model.Model, use model_from_api_records().

Return type:

SpecModel

longeron.api.spec_from_api_json(text)[source]

Parse API JSON into spec (pyecore) instances (see spec_from_api_records()). Not the inverse of to_api_json() – that is model_from_api_json().

Return type:

SpecModel

longeron.api.from_api_records(records)

back-compat aliases; prefer the explicit spec_from_api_* names (or model_from_api_* for the model-layer inverse of to_api_*)

Return type:

SpecModel

longeron.api.from_api_json(text)

Parse API JSON into spec (pyecore) instances (see spec_from_api_records()). Not the inverse of to_api_json() – that is model_from_api_json().

Return type:

SpecModel

longeron.api.model_from_api_records(records)[source]

Rebuild a Model from flat API records.

This is the reverse of to_api_records() at the same structural fidelity: element kinds, names, flags, ownership (via the reified membership records), the FeatureTyping / Subclassification / Subsetting / Redefinition relationships, and view persistence – expose (MembershipExpose / NamespaceExpose, targets rebound by qualified name) and filter (ElementFilterMembership, conditions re-parsed from their textual representation) – come back; expression trees, attribute values, multiplicities, and import/dependency targets are not part of API records and are therefore absent from the result. An expose whose target reference is missing (it never resolved at export) has no textual form and is dropped. Relationship endpoints are read from the stored role features when present and from the derived source/target arrays otherwise, so both longeron exports and pilot-server payloads import. Records are accepted in flat GET form or pilot POST identity/payload form; unknown @type values are skipped, never fatal. Unlike spec_from_api_records() this needs no pyecore.

Return type:

Model

longeron.api.model_from_api_json(text)[source]

Parse API JSON (see model_from_api_records()).

Return type:

Model

longeron.rdf

Requires the rdf extra (pip install "longeron[rdf]").

Project a model onto RDF for SPARQL querying and linked-data interchange.

The projection turns a Model into an rdflib Graph: every element becomes a subject, its spec metaclass becomes the rdf:type, and memberships, specialization/subsetting/redefinition/typing, and attribute values (typed literals) become predicates. Requires the rdf extra (pip install "longeron[rdf]"):

from longeron import rdf

graph = rdf.to_graph(model)              # rdflib.Graph
rdf.to_turtle(model, "model.ttl")        # Turtle serialization
rdf.to_jsonld(model)                     # JSON-LD text
rows = rdf.sparql(model, '''
    SELECT ?def ?mass WHERE {
        ?def a sysml:PartDefinition ; sysml:ownedMember ?attr .
        ?attr sysml:name "mass" ; sysml:value ?mass .
    }''')

Vocabulary design

Class names derive from the OMG projection, not a parallel invention. rdf:type uses the same spec metaclass per element kind that longeron.ecore / longeron.api emit as @type in Systems Modeling API records (part def -> sysml:PartDefinition, a subject usage -> sysml:ReferenceUsage, …), so the RDF view and the API JSON view of one model agree on what things are. Property names likewise follow the spec’s derived-property vocabulary (ownedMember, specializes, subsets, redefines, definedBy) rather than the reified relationship records: RDF triples are already edges, so the Subclassification/FeatureTyping reification the flat API records need is collapsed into direct predicates (the reified form stays available via longeron.api).

The namespace is this package’s own, not OMG’s. Classes and properties live under https://sanbales.github.io/longeron/rdf/sysml# (bound to the prefix sysml:), elements under .../rdf/element/ by default. OMG has not (yet) published an official RDF vocabulary for SysML v2; squatting on a plausible-looking OMG IRI would be worse than owning an honest one. When an official vocabulary lands, the local names here – taken verbatim from the spec metamodel – should map 1:1, making migration a namespace substitution (and an owl:equivalentClass/owl:equivalentProperty bridge trivial to generate). Element IRIs are minted from qualified names (DeepScout::Propulsion::HoverPower -> .../element/DeepScout/Propulsion/HoverPower, percent-encoded per segment); anonymous elements fall back to blank nodes labeled in document order, so a rebuilt graph is isomorphic to the last one.

Scope mirrors the longeron.ecore prototype: structure, names, flags, relationships, documentation, and attribute values – expression trees are carried as rendered text (sysml:valueExpression), not as RDF sub-structure. Action/state statement bodies are not projected.

longeron.rdf.VOCABULARY = 'https://sanbales.github.io/longeron/rdf/sysml#'

default namespace for classes and properties (prefix sysml)

longeron.rdf.ELEMENT_BASE = 'https://sanbales.github.io/longeron/rdf/element/'

default namespace under which element IRIs are minted

longeron.rdf.to_graph(model, *, base='https://sanbales.github.io/longeron/rdf/element/', evaluated=False)[source]

Project a model onto an rdflib.Graph.

base overrides the namespace under which element IRIs are minted. With evaluated=True, attribute values that are expressions (not plain literals) are additionally evaluated by the Interpreter per owning definition and emitted as sysml:evaluatedValue typed literals (best-effort: definitions that fail to instantiate are skipped silently).

Return type:

Graph

longeron.rdf.to_turtle(model_or_graph, path=None, **kwargs)[source]

Serialize a model (or an existing graph) as Turtle; optionally write it.

Return type:

str

longeron.rdf.to_jsonld(model_or_graph, path=None, **kwargs)[source]

Serialize a model (or an existing graph) as JSON-LD; optionally write it.

Return type:

str

longeron.rdf.sparql(model_or_graph, query, **kwargs)[source]

Run a SPARQL query against a model (or an existing graph).

The prefixes sysml:, rdf:, rdfs:, and xsd: are pre-bound, so queries can use them without PREFIX headers. Returns the rdflib result (iterable of rows).

Return type:

Any

longeron.rag

No extra required — the retrieval substrate is stdlib only.

A deterministic retrieval substrate for LLM/RAG pipelines (stdlib only).

This module carries no LLM, embedding, or vendor dependencies: it turns a model into stable, re-parseable textual chunks that an embedding index, a keyword search, or an agent’s context window can consume – and is useful without any of them via search().

  • model_chunks() – deterministic chunking: one chunk per package, definition, and package-level usage, each re-printed as valid SysML v2 text by the existing exporter (longeron.to_sysml()), with a stable qualified-name id, an ancestry breadcrumb, outgoing references, and doc text. Same model in, byte-identical chunks out – so embedding caches keyed on (id, text) stay warm across runs.

  • neighborhood() – graph-RAG helper: the chunks of an element’s semantic neighborhood (specializations, types, members, incoming and outgoing references), breadth-first by hop count.

  • search() – an embedding-free fallback: TF-IDF-style token scoring (camelCase-aware, stdlib math only) over the chunks.

The intended LLM workflow is retrieval -> cite qualified names -> resolve those names back through Interpreter for ground truth (evaluate the calc, check the constraint) instead of trusting generated text – the chunks exist to get the right element names into the conversation, not to replace execution.

class longeron.rag.Chunk[source]

Bases: TypedDict

One retrievable unit of a model.

class longeron.rag.SearchResult[source]

Bases: TypedDict

A scored search() hit.

longeron.rag.MAX_CHARS = 4000

default chunk-size budget, in characters of printed SysML text

longeron.rag.model_chunks(model, *, max_chars=4000)[source]

Deterministic retrieval chunks for a model, in document order.

Chunk units are packages, definitions, and package-level usages. Packages chunk shallow (declaration, doc, imports – their definitions are chunks of their own, so no text is duplicated). A definition whose printed text exceeds max_chars also chunks shallow, and its named nested members become chunks of their own (best-effort: a single oversized leaf is emitted whole rather than dropped). Ids, ordering, and text are stable across runs.

Return type:

list[Chunk]

longeron.rag.neighborhood(model, qname, hops=1, *, max_chars=4000)[source]

The chunks of an element’s semantic neighborhood.

Starting from the chunk containing qname, follows outgoing references (typing, specialization, subsetting, values), incoming references (who mentions me), and ownership (members and owner) for hops steps. The seed chunk comes first; the rest follow in (distance, document order).

Return type:

list[Chunk]

longeron.rag.search(model, terms, *, limit=10, max_chars=4000)[source]

Embedding-free keyword retrieval over the model’s chunks.

terms is a query string (split on whitespace) or a list of terms. Scoring is TF-IDF-style token overlap – camelCase-aware, so "station" matches stationMinutes – with name and doc tokens weighted above body text. Returns the limit best chunks with positive scores, ties broken by document order.

Return type:

list[SearchResult]