Skip to content

SysML v2 Specification Compliance

Purpose: Document implementation coverage of SysML v2 / KerML behavioral semantics. UML 2.5.1 is cited only as reference semantics for an OpenSysML extension the SysML v2 notation has no production for and the bundled KerML semantic library (internal/core/libs/stdlib/) no performance for.

Related: TESTING.md (test contracts), ARCHITECTURE.md (runtime architecture), grammar-coverage.md (which OMG grammar productions our test inputs exercise — input-presence evidence, which implies nothing about compliance either way)


Current Implementation Status

✅ Fully Implemented & Tested (~98% of Targeted Features)

Calculations (14/14 features): - Invocation with typed parameters - Return expression evaluation (both return <expr>; and a bound return parameter return : T = <expr>;) - Parameter binding (positional + named arguments) - Parameter defaults (own and inherited) - Inherited parameters and result through a typed calc usage, including redeclaration - Nested calc invocation, and invocation from a constraint - Statement bodies: local declarations, assignment, if/else, while, loop … until, for, and early return - Purity and termination: a side effect or an outside assignment is rejected, and every loop iteration spends a step of the budget - Control flow (if/else), including the conditional expression if c ? a else b evaluated lazily at runtime - Unary operators (not, -, +) - Type coercion (Integer→Real) - Qualified names (A::B::C) - Deterministic evaluation trace (parameter binding, sub-expression order, results) - Error handling (unbound/unknown parameters, arity, missing return, recursion and step budgets) - Calc usages as multi-output consumers: a usage's out features resolve, typecheck and evaluate as features (attribute z = c.b;), from one run of the body per usage per object - Composition of multi-output calcs: a usage nested among a calc def's members binds its inputs from the enclosing evaluation's parameters and locals, one run per usage per object per bound input tuple

Constraints (7/7 features): - Assert evaluation (boolean satisfaction) - Assume evaluation (trusted preconditions) - Bare expression as invariant - Negated constraints (assert not) - Unresolved feature detection - Conditions of a nested constraint (assert constraint [name] { <expr> }) - Parameters a typed usage binds (constraint limit : MassLimit { in m = mass; })

Requirements (8/8 features): - Require expression evaluation, in a requirement definition body as well as a usage - Conditions stated through an anonymous nested constraint (require constraint { <expr> }) - The requirement's own attributes, inherited or rebound, in its conditions - Subject binding evaluation - Actor binding evaluation
- Assume expression evaluation, in both spellings - Nested requirements - A violated condition names the condition that failed

Actions (18/18 features): - Initial/final node token placement - Fork node (1→N parallelism) - Join node (N→1 synchronization) - Merge node (N→1 non-blocking) - Decision node (guarded branching) - Action execution nodes - Nested action invocation - Assignment statements in an action node's body - Conditional statement (if <cond> { … } else { … }), nestable in either direction with a loop - Pre-condition loop (while <cond> { … }) and post-condition loop (loop { … } until <cond>;) - Iteration over a collection (for <x> in <collection> { … }, over every collection the expression layer produces; a non-collection input is reported) - Send statement (⚠️ typed messages addressed to an object's port or receiving node, or routed through a connected port) - Accept action (⚠️ takes the oldest message of its type, parking its token until one arrives; suspension is bounded by the executor — see the Action map) - Object flow (pin-to-pin data) - Succession edges - Deadlock detection - Token-flow tracing (infrastructure ready) - Step budget enforcement

State Machines (core: faithful; advanced: partial): - Initial/final state identification - State entry/exit actions - State do behavior (runs while the state is active; concurrently active states interleave) - Transition firing - Transition guard evaluation - Transition effect actions - AcceptEvent triggers (when signal) - Sourceless transitions (accept...then, nested form) - ChangeEvent triggers (when expression) - TimeEvent triggers (after duration, at instant) - Signal discrimination (name matching) - Unmatched signal dropped - Signals sent from entry/do/exit/effect actions reaching the machine - CallEvent triggers (accept op(arg) notation, operation and argument matching) - Completion transitions (nil trigger with guard evaluation) - Hierarchical substates - Orthogonal regions (concurrent states) - Choice pseudostates (dynamic branching) - Junction pseudostates (static branching) - Fork pseudostates (one branch per orthogonal region) - Join pseudostates (waits for every branch) - Entry/exit point pseudostates (entry point <name>; / exit point <name>;) - Choice/junction/entry/exit reached from inside an orthogonal region - Nested action invocation in entry/do/exit/effect behaviors - Run-to-completion semantics - Event queue management - Dangling transition detection (a transition names one source and one target vertex of its own machine; a routing pseudostate with no transition out of it reports) - State visits tracking - Multi-region event broadcasting - History pseudostates: shallow and deep restoration (history / shallow history / deep history <name>;) - Deferred events: retention and recall across hierarchy and orthogonal regions (defer <event>[, <event>]*;)

Expression Evaluation: - Binary operators (+, -, *, /, <, >, ==, and, or) - Exponentiation (**, ^) over Integer and Real operands, folded and evaluated by one implementation - Unary operators (-, not) - Literal values (Integer, Real, Boolean, String) - Feature reference resolution - Qualified name resolution (A::B::C) - Type coercion (Integer→Real) - Unresolved reference error handling - KerML function library: the numeric functions of RealFunctions, RationalFunctions, NumericalFunctions, IntegerFunctions, NaturalFunctions, TrigFunctions, VectorFunctions and ComplexFunctions, all of StringFunctions, plus the library feature values TrigFunctions::pi and ComplexFunctions::i (see the Function Library row below)

Name Resolution: - Inherited feature resolution (follows specialization chains) - Named argument parameter binding - Redefinition target resolution (:>> featureName) - Control flow node scope registration

Test Coverage: - 338 conformance cases (all passing: calc×79, state×71, action×62, requirement×12, instance×10, binding×9, send×8, constraint×7, multiplicity×7, unit×7, nested×6, object×6, satisfy×6, string×6, redefinition×5, variation×5, accept×4, port×4, and three each of attribute, ballandchain, enum, feature, filter, variant, two of viewpoint, and one each of connector, cubesat, two and view — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage) - 194 runtime robustness cases (deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a select predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a terminate inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic perform references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, a time-triggered accept with no clock, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a variant declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back) - 524 runtime test functions (grep -c '^func Test' internal/core/runtime/*_test.go), the conformance, trace and robustness gates above among them - 90 golden AST fixtures (including the implicitly typed connector forms and the standard behavioral notation — a named flow with from, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with until, then done, a decision else branch, a bodied exhibit state, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies and n-ary connector-end parsing tests) - 106 golden execution traces (action×37, calc×30, state×28, constraint×4, string×4, accept×1, object×1, two×1 — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop until with then done, a decision's guarded and else branches, a named flow carrying a value between action nodes, an accept with a when trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation) - 135 negative parser subtests - 14 gRPC conformance cases and 8 gRPC robustness cases (internal/grpc/testdata/conformance/, internal/grpc/robustness_test.go) - 5,477 tests and subtests, of which 5,468 pass and 9 skip (go test -race -count=1 -v ./...; 2,922 top-level Test functions). The skips are TestSubsettingTargetIsTheInheritedFeature, TestEval_CollectExpr, TestEval_SelectExpr, TestEval_BuiltinInvocation, TestRequirementEvaluation_SubjectNotFound, TestRequirementEvaluation_Complete, TestMultiplicityOfALibraryFeatureIsTheSameParsedAndRestored, TestPortabilityGateIsRequired, and TestHelperSolverProcess; each skips itself.


Detailed Semantic Compliance Map

How to Read This Map

Each row documents one behavioral semantic feature:

  • Semantic Rule: the governing reference — the SysML v2 metamodel or the bundled KerML semantic library, and UML 2.5.1 only where neither has the concept
  • Implementation: File:function implementing the semantics
  • Test Case: Conformance/robustness test(s) exercising the feature
  • Status:
  • Faithful: Implements spec semantics with test coverage
  • ⚠️ Approximate: Partial implementation or known deviations
  • Not Yet Implemented: Parsed but not executable
  • 🚧 Known Failure: Test exists but fails

Calculation (Calc)

Semantic Rule Implementation Test Case Status
Calc invocation with typed parameters invoke_calc.go InvokeCalc/invokeCalcShape calc_simple_add.sysml ✅ Faithful
Return expression evaluation (return <expr>;) invoke_calc.go calcResult/resultExpression + eval.go Eval calc_simple_add.sysml ✅ Faithful
Result as a bound return parameter (return : T = <expr>;) invoke_calc.go resultExpression calc_return_parameter.sysml ✅ Faithful
Parameter binding (positional) invoke_calc.go bindCalcParameter calc_simple_add.sysml ✅ Faithful
Parameter binding (named arguments) eval.go evalInvocation + invoke_calc.go InvokeCalcNamed calc_named_arguments.sysml ✅ Faithful
Parameter default when no argument is passed invoke_calc.go bindCalcParameter calc_parameter_defaults.sysml ✅ Faithful
Parameters and result inherited through a typed calc usage invoke_calc.go calcShapeOf/calcChain/calcParameters calc_inherited_parameters.sysml, calc_return_parameter.sysml ✅ Faithful
Redeclared parameter keeps its inherited position and default, which stays the expression the supertype wrote and is evaluated where that calc wrote it invoke_calc.go calcParameters (the owner carried down with the inherited default) calc_return_parameter.sysml, calc_usage_nested_shadowed_input.sysml, calc_usage_nested_test.go:TestNestedCalcUsageInheritedDefaultOfRedeclaredInput ✅ Faithful
Nested calc invocation eval.go evalInvocationinvoke_calc.go invokeCalc calc_nested_invocation.sysml ✅ Faithful
Calc invoked from a constraint context.go EvaluateConstrainteval.go evalInvocation calc_from_constraint.sysml ✅ Faithful
Deterministic evaluation trace (binding, sub-expression order, result) trace.go RecordCalcEnter/RecordCalcBind/EndEval, eval.go Eval *.trace.golden via TestExecutionTrace, trace_calc_test.go:TestCalcTraceIsStableAcrossRuns ✅ Faithful
Canonical rendering of unordered values in traces trace.go FormatTraceValue trace_calc_test.go:TestFormatTraceValueCanonicalizesSets ✅ Faithful
Unbound parameter detection invoke_calc.go bindCalcParameter (ErrUnboundParameter) robustness_test.go:testCalcUnboundParameter ✅ Faithful
Surplus positional arguments invoke_calc.go checkArgs (ErrCalcArity) robustness_test.go:testCalcTooManyArguments ✅ Faithful
Named argument that names no parameter invoke_calc.go checkArgs (ErrUnknownParameter) robustness_test.go:testCalcUnknownNamedArgument ✅ Faithful
Invoked symbol is not a calc invoke_calc.go calcShapeOf (ErrNotACalc) robustness_test.go:testCalcSymbolIsNotACalc ✅ Faithful
Recursive calc (direct or mutual) evaluates to its result, bounded by the run's calc depth budget invoke_calc.go Context.enterCalc (ErrCalcRecursionLimit), budget from budget.go BudgetsFromEnv (SYSML_MAX_CALC_DEPTH, default 10000, ceiling 25000) calc_recursion_factorial.sysml, calc_recursion_fibonacci.sysml, calc_recursion_mutual.sysml, calc_recursion_descends_sequence.sysml, calc_recursion_beyond_nesting_bound.sysml, invoke_calc_recursion_test.go:TestRecursiveCalcSpendsTheDepthBudget, :TestRecursiveCalcDepthIsNotAFixedBound, :TestMutuallyRecursiveCalcsEvaluate ✅ Faithful
A recursion that does not terminate spends a budget and is reported, never hanging or exhausting the stack invoke_calc.go Context.enterCalc (ErrCalcRecursionLimit) and context.go step counter (ErrStepLimitExceeded); the ceiling on the depth budget keeps the report ahead of the goroutine stack limit robustness_test.go:testCalcDirectRecursion, :testCalcMutualRecursion, :testCalcRecursionSpendsStepBudget, :testCalcRecursionAtDepthCeiling, budget_test.go:TestBudgetFromValue (above the ceiling) ✅ Faithful
Step budget bounds calc evaluation context.go step counter (ErrStepLimitExceeded), budget from budget.go BudgetsFromEnv (SYSML_MAX_STEPS, default 10000000) robustness_test.go:testStepBudgetExceeded, budget_test.go:TestBudgetFromValue ✅ Faithful
Statement body (SysML v2 7.19, CalculationBodyItem carries the items of an action body): local declarations, assignment, if/else, while, loop … until, for, return parser/behavior.go parseCalcBody/atCalcStatementlower/calc_body.go CalcBodyruntime/statements.go stmtEngine driven by invoke_calc.go runCalcBody calc_statement_body.sysml (golden AST), calc_iterative_factorial.sysml, calc_conditional_branch.sysml, calc_for_over_sequence.sysml, calc_loop_until_body.sysml ✅ Faithful
Early return out of a branch or a loop unwinds the blocks entered lower/action_graph.go Return + runtime/statements.go flowReturn calc_early_return_from_loop.sysml ✅ Faithful
A body-local declaration of a branch or loop body is that block's own and does not leak runtime/statements.go stmtEnv passes/typecheck_calc_body_test.go:TestCalcBodyLoopLocalIsNotVisibleOutside ✅ Faithful
An inherited body evaluates in the scope of the calculation declaring it invoke_calc.go calcBody (specialization chain, as calcResult/calcParameters) + statement Scope carried by the lowered IR invoke_calc_body_test.go:TestInheritedCalcBodyRunsInDeclaringScope ✅ Faithful
A calculation is pure: send, perform, accept, terminate and an assignment to a feature it does not declare are rejected runtime/calc_statements.go (ErrCalcSideEffect, ErrCalcExternalAssignment) robustness_test.go:testCalcSendIsRejected, :testCalcTerminateIsRejected, :testCalcAssignmentOutsideTheCalc ✅ Faithful
A loop in a calculation terminates or fails: every iteration spends a step of the budget runtime/statements.go loop/forLoopcontext.go incrementStep; InvokeCalc/InvokeCalcNamed beginRun robustness_test.go:testCalcNonTerminatingLoop, budget_test.go:TestStepBudgetIsPerRunForInstancesAndCalcs ✅ Faithful
A body running to its end without returning is an error, not a null result invoke_calc.go runCalcBody (ErrCalcNoReturn) robustness_test.go:testCalcBodyNeverReturns ✅ Faithful
Control flow (if/else) in calc, including the conditional expression if c ? a else b evaluated lazily runtime/statements.go ifStatement; eval.go evalConditional calc_conditional_branch.sysml, calc_conditional_operator_base_case.sysml ✅ Faithful
The type tier reaches the expressions of a body whose result is the value of its last expression, as it reaches an explicit return — including the dimensional warning passes/typecheck.go checkBehaviorMember (an expression body member inferred as the value it computes) typecheck_calc_implicit_result_test.go:TestCalcBodyImplicitResultIsChecked, :TestCalcBodyImplicitResultWarnsOnDimensions, :TestCalcUsageImplicitResultIsChecked, :TestCalcBodyImplicitResultSkippedAfterNameError, calc_recursion_implicit_result.sysml ✅ Faithful
A non-Boolean if/loop condition in a calc is a type error, and a diagnostic at runtime passes/typecheck.go checkBehaviorMember; runtime/statements.go condition typecheck_calc_body_test.go:TestCalcBodyNonBooleanWhileCondition, robustness_test.go:testCalcNonBooleanCondition ✅ Faithful
Statements and loop iterations in the evaluation trace trace.go RecordStatement/RecordLoopIteration calc_iterative_factorial.trace.golden, calc_early_return_from_loop.trace.golden ✅ Faithful
Missing return expression invoke_calc.go calcShapeOf (ErrNoResultExpression: no result expression, no returning body, no output features and no output its body assigns) robustness_test.go:testCalcWithoutResult ✅ Faithful
An assignment in a body to an out the calculation declares binds that output for the activation, so a calc usage reads it (KerML 7.4.9: an invocation's outputs are features of that one evaluation). The parser gives a = expr and a := expr in statement position one AssignmentActionNode, so both spellings write the output alike runtime/statements.go stmtEngine.assign/stmtEnv.assignLocalruntime/calc_statements.go calcStmtHost.declaredOutput/assignOuter; runtime/calc_usage.go calcRun.output, assignedOutputs; invoke_calc.go calcShape.BodyOutputs parser/testdata/parse/calc_output_assignment.golden, calc_output_assigned_in_body.sysml conformance (both spellings, a loop and a branch computing outputs) ✅ Faithful
A body that both assigns an output and returns a value keeps the two apart: the return is the invocation's value, the assignment the output's, so reading the output answers what the body assigned runtime/calc_usage.go runCalcUsage (memoizing a returned value under an output's name only for calcOutput.IsResult, the rule the row on a valued output and a return states) calc_output_assigned_in_body.sysml conformance (AssignAndReturn: cr.a is 6, AssignAndReturn(5) is 500) ✅ Faithful
A calculation stating its own body — a result expression, or a body assigning an output it declares — is evaluated from it, so a library function of the same name never answers in its place runtime/invoke_calc.go hasCalcBody consulted by runtime/library_functions.go libraryFunctionFor library_functions_test.go:TestLibraryFunctionDoesNotHijackADeclaredBody, :TestLibraryFunctionDoesNotHijackAnOutputAssignedInABody ✅ Faithful
An assignment target naming more than one segment (assign other::c := 1) reaches outside the body no host binds, so it is lowered as unsupported and reported rather than writing the last segment lower/action_graph.go lowerStatement (Unsupported) robustness_test.go:testQualifiedAssignmentTargetInAStateEffect ✅ Faithful (an assignment through a feature chain, a.b := 1, is reported the same way; neither form is bound)
An inout is bound by the invocation and rebound by an assignment in the body, the read answering what the body left runtime/calc_usage.go calcOutput.IsInOut/calcRun.output; runtime/calc_statements.go calcStmtHost.declaredOutput calc_output_assigned_in_body.sysml conformance (Bump) ✅ Faithful
An output given a value by its declaration and assigned in the body is a typed error rather than a silent pick (the precedent of a feature valued two ways). Assignments to an output within the body are imperative like a body local's: the last one to run is the activation's binding, so an output may be initialized and then accumulated into, including once per loop iteration runtime/calc_statements.go calcStmtHost.assignOuter (ErrConflictingOutput) robustness_test.go:testCalcOutputValuedAndAssigned, :testCalcOutputAssignedTwice, calc_output_assigned_in_body.sysml conformance (Accum) ✅ Faithful
Reading a declared output the activation never assigned reports that output, not a missing result expression; an output only a branch that did not run would assign is unassigned for that activation runtime/calc_usage.go calcRun.output (ErrOutputNotAssigned, a kind of ErrNoValue) robustness_test.go:testCalcOutputNeverAssignedByTheBody, :testCalcOutputAssignedInABranchNotTaken ✅ Faithful
A calc usage's members are the parameters and outputs of the calc it is typed by, reachable through a feature chain (SysML 7.6.6, 7.17) resolve/target.go ResolveTarget/memberChain + resolve/document.go resolveMemberChainsemantics Model.LookupMember passes/typecheck_calc_usage_test.go:TestCalcUsageOutputTypesAsTheOutputItNames, :TestCalcUsageOutputInsideAPartDefinition ✅ Faithful
An output read through a chain types as that output declares, or as its default computes when it declares no type passes/typecheck_expr.go inferFeatureChain/featurePrimType passes/typecheck_calc_usage_test.go:TestCalcUsageOutputTypedByItsDefaultIsChecked, :TestCalcUsageDeclaredOutputTypeIsChecked ✅ Faithful
Reading a name the calc declares no output for is unresolved, reported once by the name-resolution tier resolve/document.go resolveMemberChain; runtime/calc_usage.go calcRun.output (ErrUnknownOutput) passes/typecheck_calc_usage_test.go:TestCalcUsageUnknownOutputIsUnresolved, robustness_test.go:testCalcUsageUnknownOutput ✅ Faithful
A calc usage evaluates its body once and every out feature it declares is readable from that run (SysML 7.17) runtime/calc_usage.go CalcUsageOutput/CalcUsageOutputs/calcUsageRuninvoke_calc.go calcShapeOf/bindCalcParametersruntime/statements.go stmtEngine calc_usage_multiple_outputs.sysml, calc_usage_statement_body.sysml, calc_usage_inherited_parameters.sysml, calc_usage_instance_slots.sysml ✅ Faithful
Reading several outputs of one usage runs the body once, per usage and per object, reset with the run runtime/calc_usage.go calcUsageRun (calcUsageKey) + context.go beginRun/beginExecutorRun calc_usage_multiple_outputs.trace.golden, calc_usage_statement_body.trace.golden, calc_usage_inherited_parameters.trace.golden ✅ Faithful
A usage's inputs bind from its own member values, falling back to the defaults declared along its specialization chain, and may name a sibling feature of the object carrying the usage runtime/calc_usage.go calcUsageRuninvoke_calc.go bindCalcParameters/calcParameters calc_usage_inherited_parameters.sysml, calc_usage_instance_slots.sysml ✅ Faithful
A usage declared in a behavior's body — a calc's or an action's — binds its inputs in the environment of the evaluation reading it: the enclosing parameters and locals as the running body holds them, then the enclosing lexical scope, so an input naming an attribute the body assigned reads the assigned value rather than the declared one (SysML 7.17, a usage is a feature of the body declaring it) runtime/calc_usage.go bindCalcUsage/enclosedByBehaviorBody, invoke_calc.go isActionSymbol, eval.go EvalContext.nestedEnv, invoke_calc.go bindCalcParameters calc_usage_nested_in_calc.sysml, action_body_local_calc_usage.sysml, calc_usage_nested_test.go:TestNestedCalcUsageBindsFromEnclosingParameters, :TestNestedCalcUsageBindsFromEnclosingLocals, :TestNestedCalcUsageChain, :TestNestedCalcUsageReadsEnclosingObject, action_body_local_usage_test.go:TestActionBodyLocalUsageBindsCurrentValues, :TestActionBodyLocalUsageBindsPerIteration ✅ Faithful
A nested input bound from a name of its own (in vx = vx) reads the enclosing binding: the inputs being bound are not in the environment their own values are evaluated in, so every one of them resolves names in the enclosing environment alike — in n = m; in m = n; swaps the two values rather than the second reading the first's fresh binding runtime/calc_usage.go bindCalcUsage, eval.go EvalContext.nestedEnv/Lookup calc_usage_nested_shadowed_input.sysml (shadowed and swapped names), calc_usage_nested_test.go:TestNestedCalcUsageShadowsEnclosingName, :TestNestedCalcUsageInputsDoNotSeeSiblings, robustness_test.go:testNestedCalcUsageSelfCycle (a default with nothing outside to name stays ErrCyclicFeatureValue) ✅ Faithful
One run per usage, object and activation: two reads from one enclosing invocation run the body once, two invocations do not share it, and an iteration of a loop is an activation of its own. The activation replaces the input-value hash the memo key used, so two invocations whose inputs coincide still get their own run and no read can be answered from a run bound to other values runtime/calc_usage.go calcUsageKey/calcUsageRun, context.go newActivation/endActivation calc_usage_nested_in_calc.trace.golden, calc_usage_nested_shadowed_input.trace.golden, calc_usage_nested_test.go:TestNestedCalcUsageRunsPerInputs, :TestNestedCalcUsageOwnOutputPerInvocation, calc_usage_snapshot_test.go:TestCalcUsageMemoDistinguishesEnclosingArguments ✅ Faithful
Every output read from one evaluation of a usage sees one binding of its inputs: the inputs are bound when the evaluation starts and a later assignment to a feature they named does not rebind them for a later output read, so two outputs of one usage can never come from different input bindings (KerML 7.4.9, SysML 7.17 — a usage's outputs are features of one evaluation) runtime/calc_usage.go calcUsageRun/calcUsageKey/forgetCalcUsage, context.go newActivation/endActivation calc_usage_outputs_one_binding.sysml + .trace.golden, calc_rk4_lunar_descent.sysml, calc_usage_snapshot_test.go:TestCalcUsageOutputsShareOneInputBinding, :TestCalcUsageOutputsInAssignmentLoop, :TestCalcUsageMemoDistinguishesEnclosingArguments, :TestCyclicCalcUsageOutputStillDiagnosed ✅ Faithful
A calc usage declared in a loop or conditional body is executable, in the scope it is written in and with the lifetime of the block: an iteration binds it from that iteration's state and reading it again in the same iteration reuses that evaluation. This holds in an action's body as well as a calc's, both being run by the statement engine lower/calc_body.go usageStatementlower.DeclareUsage, runtime/statements.go declareUsage, runtime/calc_usage.go bodyUsageSymbol/enclosedByBehaviorBody calc_body_local_usage_and_range.sysml (golden AST), calc_rk4_lunar_descent.sysml, action_body_local_calc_usage.sysml, calc_usage_body_local_test.go:TestBodyLocalCalcUsageInLoopBindsPerIteration, :TestBodyLocalCalcUsageInBranch, :TestBodyLocalCalcUsageInNestedBodies, action_body_local_usage_test.go:TestActionBodyLocalUsageBindsCurrentValues, :TestActionBodyLocalUsageBindsPerIteration, robustness_test.go:testBodyLocalUsageOfANonCalc, :testBodyLocalDeclarationNotExecutable ✅ Faithful
A feature chain through a part reads what the part's features carry, the part being materialized as the occurrence it denotes; an output of a calc usage the part declares evaluates in that part's context. The object being evaluated answers first: a feature value it carries for that part is the object read, and only a part no object in hand carries is materialized. A usage of several occurrences is a collection, not one object, so a chain through it is reported runtime/eval.go evalFeatureChain/chainBase, runtime/calc_usage.go occurrenceOperand, runtime/instance.go occurrenceOf/occursOnce part_feature_chain_test.go:TestCalcUsageReadThroughPartChain, :TestPartChainReadsTheObjectInHand, :TestPartChainRejectsSeveralOccurrences, :TestCalcUsageChainDiagnostics, robustness_test.go:testUsageReadThroughAPartWithoutAnOutput ✅ Faithful
A chain through a multi-valued feature has, for its last feature, that feature's values over every object the features before it name (KerML 1.0 §7.3.4.6): subsystem.volume is the volumes of every object subsystem holds, in the collection's order, flattened one level per step as ->collect flattens its mapping. An empty collection yields no values; a chain reaching an object with no value for the feature reports the unset feature value runtime/eval.go chainMemberValue/chainOverElements conformance feature_chain_rollup_over_subsets, feature_chain_nested_multivalued, feature_chain_empty_collection, cubesat_mass_rollup; robustness_test.go:feature_chain_through_an_unset_slot, :feature_chain_spends_the_element_budget, chain_trace_test.go:TestChainOverCollectionTraceOrder ✅ Faithful
A chain that stops at a calc usage rather than at one of its outputs names the outputs to read instead of reporting no value runtime/eval.go evalCalcUsageMembers (ErrNoValue) part_feature_chain_test.go:TestCalcUsageChainDiagnostics, robustness_test.go:testUsageReadThroughAPartWithoutAnOutput ✅ Faithful
A nested usage chain is bounded: the depth counted while an output binding is evaluated, the budget spent by the bodies it runs. One evaluation counts one level however its answer is reached, so an invocation whose result is a designated output and outputs of one calc naming each other spend nothing extra invoke_calc.go enterCalc (ErrCalcRecursionLimit)/runCalcBody, runtime/calc_usage.go calcRun.enter/calcRun.value robustness_test.go:testNestedCalcUsageRecursionDepth, :testNestedCalcUsageStepBudget, :testNestedCalcUsageUnboundInput, :testNestedCalcUsageUnknownOutput, calc_usage_nested_test.go:TestNestedCalcUsageDepthCountedOnce, :TestCalcOutputChainIsNotNesting ✅ Faithful
An out default evaluates in the calc's own scope and may name inputs, body locals and other outputs runtime/calc_usage.go calcRun.output/lookupOutput + eval.go EvalContext.calcRun calc_usage_statement_body.sysml, calc_usage_inherited_parameters.sysml ✅ Faithful
An output feature fed into a feature's default value (the parametric-budget pattern) eval.go evalFeatureChain/evalCalcUsageMembers calc_usage_multiple_outputs.sysml, calc_usage_instance_slots.sysml ✅ Faithful
Outputs valued from each other are a cyclic dependency, not a hang or a spent step budget runtime/calc_usage.go calcRun.output (ErrCyclicOutput) robustness_test.go:testCalcUsageCyclicOutputs ✅ Faithful
A usage leaving an input unbound, or reading an output with no value, is reported runtime/calc_usage.go calcUsageRun (ErrUnboundParameter), calcRun.output (ErrNoValue) robustness_test.go:testCalcUsageUnboundInput, :testCalcUsageOutputWithoutAValue ✅ Faithful
A calc usage typed by something that is not a calc is reported invoke_calc.go calcShapeOf (ErrNotACalc) robustness_test.go:testCalcUsageSpecializesANonCalc ✅ Faithful
The step budget bounds a usage's body the way it bounds an invocation runtime/calc_usage.go CalcUsageOutput beginRuncontext.go incrementStep (ErrStepLimitExceeded) robustness_test.go:testCalcUsageStepBudget ✅ Faithful
An invocation yields exactly one result (KerML 7.4.9), so invoking a calc that computes several outputs and designates no result is rejected rather than answered with the first of them — and the diagnostic writes out the calc usage to declare instead, with the inputs to bind runtime/calc_usage.go calcShape.designatedOutput/usageSpelling (ErrAmbiguousResult) robustness_test.go:testMultipleOutputsInvokedAsAnExpression, repl/runtime_commands_test.go:TestCalcWithSeveralOutputsIsNotInvocable ✅ Faithful
A calc with exactly one output and no return is invocable, that output being its result runtime/calc_usage.go calcShape.designatedOutput calc_usage_single_output.sysml ✅ Faithful
A return in a calc that also states an output supplies the invocation's value only: an output keeps the value its own binding computes, so a body returning something else does not change what reading that output answers. Only the result parameter (return : Real = …) takes the returned value directly runtime/calc_usage.go runCalcUsage (memoizing a returned value under an output's name only for calcOutput.IsResult) calc_valued_output_with_return.sysml conformance (Apart: ca.a is 6 while Apart(5) is 500; Together, Both) ✅ Faithful
%calc on a calc usage lists the outputs of one evaluation; a chain into a usage evaluates at the prompt repl/meta.go doCalc/calcUsageOutputs repl/runtime_commands_test.go:TestCalcUsageOutputsAtThePrompt ✅ Faithful
A calc usage's outputs are evaluation results, not feature values of an object runtime/calc_usage.go (no instance materialization) calc_usage_instance_slots.sysml (the features fed by the outputs are feature values; the usage itself is not) ⚠️ Approximate (%instances and export show the features valued from outputs, not the usage's outputs themselves)
Boolean operators evaluated at runtime (and, or, xor, implies, short-circuiting where they can) eval.go evalLogical calc_boolean_operators.sysml ✅ Faithful
Identity (===, !==), null coalescing (??, lazy) and remainder (%) evaluated at runtime eval.go evalIdentity/evalNullCoalesce/evalArithmetic calc_identity_operators.sysml, calc_null_coalesce.sysml, calc_modulo_operator.sysml ✅ Faithful
An operator with no runtime evaluation (hastype/istype classification, cast, all, bitwise complement) reports why eval.go unimplementedOperators (ErrUnsupportedOperator) eval_operator_test.go:TestUnimplementedOperatorReportsWhy ❌ Not implemented (rejected with a typed diagnostic naming what it would need; type classification needs a runtime type on values. Metadata classification @/@@ is evaluated — see the row below)
Metadata classification evaluated at runtime (p @ Safety, p @@ SysML::PartUsage, in a constraint, a calc body or an %eval; SysML v2 7.9.4 metadata, 8.4.4.2 ClassificationExpression) eval.go EvalContext.evalClassification over semantics/filter.go Model.EvalClassification (the same compiled predicate and Model.AllSupertypes conformance an element filter is decided by, so the two paths cannot disagree); eval.go classifiedElement settles the element the subject denotes — the object being evaluated for an implicit subject or self, the element a name names, or an object's classifier, a selected variant or an enumeration literal runtime/eval_classification_test.go (TestEvalClassificationOverNamedSubjects, TestEvalClassificationMetaVersusAnnotation, TestEvalClassificationInACalcBody, TestEvalClassificationOfTheObjectBeingEvaluated, TestEvalClassificationAgreesWithAnElementFilter — the verdicts of the two paths pinned equal); conformance/filter_classification_annotation_forms.sysml, filter_classification_meta_versus_annotation.sysml, filter_classification_implicit_subject.sysml, view_exposed_element_classification.sysml; robustness_test.go classification_outside_the_evaluable_subset; semantics/classification_test.go (the shared predicate itself) ✅ Faithful (@T holds for metadata annotating the subject in every form the parser accepts, inherited conformance included, and for the subject's own metaclass; @@T holds for the metaclass alone. A subject that denotes no element, or a metadata type that does not resolve, is semantics.ErrFilterUnevaluable — reported, never answered false)
lower..upper is the ordered sequence of integers the library declares it to be (IntegerFunctions::'..' returns Integer[0..*] ordered, and SequenceFunctions::subsequence maps over it), so every sequence operation, index and for applies to it unchanged. A descending range is empty, a bound that is not an Integer is a type error, and each element generated costs a step and an element of the budgets, so a range wider than either — including one whose width overflows — is ErrStepLimitExceeded or ErrElementLimitExceeded rather than an allocation runtime/range.go evalRange/rangeSequence/rangeBound/builtinIntegerRange (ErrTypeMismatch), registered in builtins.go calc_integer_range.sysml, range_test.go:TestIntegerRange, :TestIntegerRangeSequenceOperations, :TestIntegerRangeNonIntegerBound, :TestIntegerRangeSpendsTheStepBudget, :TestIntegerRangeExtremeBounds, :TestForOverIntegerRange, robustness_test.go:testRangeBoundIsNotAnInteger, :testRangeSpendsTheStepBudget ✅ Faithful
Unary operators (not, -, +) eval.go evalUnary calc_unary_operators.sysml ✅ Faithful
Type coercion (Integer→Real) eval.go:344 toReal calc_type_coercion.sysml ✅ Faithful
Qualified names (A::B::C) eval.go + resolve/ calc_qualified_names.sysml ✅ Faithful

A calc usage with several out features, a usage nested in a part or in a calc def's body, and a chain into one (c.a) are all existing productions — a directed feature member of a usage body and FeatureChainExpr — so multi-output calc usages added no grammar and no golden AST fixture of their own. calc_defaults_and_invocation.sysml (a typed calc usage binding an inherited parameter in its body) and calc_statement_body.sysml lock the parse structure they reuse.

Constraint

Semantic Rule Implementation Test Case Status
Assert evaluation (boolean satisfaction) context.go:81 EvaluateConstraint constraint_literal.sysml ✅ Faithful
Assume evaluation (trusted precondition) context.go:81 (same path) constraint_assume.sysml ✅ Faithful
Bare expression as invariant context.go:81 constraint_literal.sysml ✅ Faithful
Unresolved feature reference resolve package + eval.go robustness_test.go:testConstraintMissingFeature ✅ Faithful
Negated constraint (assert not) eval.go:483 evalNeg constraint_negation.sysml ✅ Faithful
A constraint a type carries is evaluated against an instance of it, so it reads that object's feature values rather than declared defaults context.go EvaluateConstraintOn, eval.go NewEvalContextIn/selfFeatureValue instance_constraint_binding.sysml, repl/instance_test.go:TestConstraintBindsToInstance ✅ Faithful
A condition on a type reached only through a nested redefinition (part top : Top { part :>> mid { part :>> leaf { attribute :>> value = 99.0; } } }) is checked against that nested object, at any depth and across part boundaries, so it agrees with %features instead of answering from the declaration; a sibling the object does not redefine keeps its declared value condition.go conditionSubject, carriersUnder, nestedObjects conformance nested_redefinition_two_levels, nested_redefinition_inherited_leaf, runtime/subject_value_test.go:TestNestedRedefinitionIsTheConditionSubject, TestNestedSubjectUnderSuppliedObject ✅ Faithful
With no object instantiated, such a condition still answers about the declaration condition.go conditionSubject conformance nested_redefinition_no_object, runtime/subject_value_test.go:TestNestedRedefinitionIsTheConditionSubject ✅ Faithful
Two objects redefining the same nested feature differently make the subject a question, reported as ErrAmbiguousSubject naming the carriers rather than answered from either; the same declaration materialized twice is one object, the latest condition.go ErrAmbiguousSubject, conditionSubject, rootInstances runtime/subject_value_test.go:TestNestedSubjectAmbiguous, robustness_test.go:testNestedConditionSubjectIsAmbiguous, testDuplicateObjectsOfOneDeclaration ✅ Faithful
An object a caller instantiated directly is a subject in its own right — a definition nested in another (part def Outer { part def Big :> Inner { ... } }) and a nested usage (%instantiate Top::leaf) alike — while an object a feature value holds, or one materialized only to read a nested declaration through, is reached through its holder condition.go rootInstances over heldObjectIDs and readThrough runtime/subject_value_test.go:TestSubjectOfANestedDefinition, TestSubjectOfADirectlyInstantiatedNestedUsage ✅ Faithful
A check reports the object it was evaluated against, so a verdict is labelled with that object rather than with the one supplied when the two differ context.go CheckResult, CheckConstraintOn, CheckRequirementOn, satisfy.go CheckSatisfactionOn, grpc/verify.go subjectOf runtime/subject_report_test.go:TestCheckReportsTheChosenSubject, TestCheckWithNoObjectReportsNoSubject, grpc/verify_subject_test.go:TestVerifyConstraintNamesTheNestedSubject ✅ Faithful
A satisfaction assertion (assert satisfy Inner::lim by big;) chooses the object its requirement's conditions read by the same rule the requirement does, so by naming an object carrying the requirement nested answers about that nested object, and an ambiguous one is reported rather than picked. An assertion stating its own conditions (satisfy requirement fits { require … }) resolves against the usage itself, so it too answers about the object carrying it satisfy.go CheckSatisfactionOn, condition.go checkSubject runtime/subject_report_test.go:TestSatisfactionRoutesThroughTheSubjectRule, TestSatisfactionOfItsOwnConditionsRoutesThroughTheSubjectRule, satisfy_nested_subject.sysml, robustness_test.go:testSatisfactionSubjectIsAmbiguous ✅ Faithful
A verdict about a nested object is labelled with that object as a reader can type it back — the object the search started from plus the features walked to it (A::o::b), ending in the declaration it materializes as an ambiguity's labels do — rather than with the object the command named context.go CheckResult (SubjectRoot, SubjectPath), condition.go carriersUnder, carrierFeatures, repl/query.go reportedSubject repl/subject_label_test.go:TestVerdictNamesTheNestedSubject, runtime/subject_report_test.go:TestReportedFeaturesEndInTheDeclaration ✅ Faithful
An ambiguity names each carrier by the features walked to it, ending in the declaration it materializes (Bolt #3 (front::bolt) vs Bolt #5 (rear::bolt); Component #2 (small) vs #3 (large) where one collection gathers both), so carriers are told apart however they were reached condition.go carrierLabels, carrierPath, carriersUnder runtime/subject_value_test.go:TestAmbiguousCarriersAreNamedByTheirPath, robustness_test.go:testPartsSubsettingOneCollection ✅ Faithful
Over gRPC an ambiguous subject is a typed reason (FAILURE_REASON_AMBIGUOUS_SUBJECT), so a client tells it apart from a violated condition without reading the message grpc/verify.go failureReason, api/proto/sysml.proto FailureReason grpc/verify_subject_test.go:TestVerifyConstraintReportsAnAmbiguousSubject ✅ Faithful
A requirement's subject/actor bindings are evaluated against the same object its conditions read context.go CheckRequirementOn over memberBindings runtime/subject_report_test.go:TestSatisfactionRoutesThroughTheSubjectRule ✅ Faithful
One object stands for each declaration reached in that search: the objects of a part held with a multiplicity (part wheels : Wheel[4]), those of a part nested inside it, and an object left behind by an earlier materialization of its holder are each one candidate, while two declarations feeding one collection (part small : Component :> subsystem) are two condition.go carriersUnder over occurrenceOf, rootInstances over heldObjectIDs robustness_test.go:testNestedPartHeldWithAMultiplicity, testPartNestedInsideARepeatedPart, testPartsSubsettingOneCollection, testDuplicateObjectsHoldingAPlainPart, testDuplicateObjectsOfOneDeclaration ✅ Faithful
The search for that object descends a declaration once per path, so composition naming its own kind (part def Node { part next : Node; }) terminates instead of materializing objects until the step budget is spent condition.go carriersUnder robustness_test.go:testRecursiveCompositionSubjectSearch ✅ Faithful
A false assertion is a verdict, not a malfunction (ErrViolated), and is distinguishable from an evaluation failure errors.go ErrViolated, context.go EvaluateConstraintOn repl/instance_test.go:TestConstraintEvaluationErrorIsNotAViolation ✅ Faithful
A constraint usage inherits its conditions from the definition it is typed by (constraint limit : MassLimit;) context.go chainMembers over semantics.Model.AllSupertypes instance_inherited_constraint.sysml ✅ Faithful
A parameter a typed usage binds (constraint limit : MassLimit { in m = mass; }) is visible to the conditions it inherits, and masks both the declaration it redefines and a same-named member of the object carrying the usage — including the usage's own name condition.go conditionFeatures over Model.MembersOf, eval.go evalFeatureReference instance_constraint_bound_parameter.sysml, instance_constraint_parameter_name_collision.sysml, runtime/condition_test.go:TestConstraintUsageBindsInheritedParameter ✅ Faithful
The conditions of a nested constraint (assert constraint [name] { <expr> }) are the conditions of the member stating it parser/behavior.go tryParseNestedConstraint, condition.go appendConditions parser/behavior_require_member_test.go:TestConstraintMemberNestedBody ✅ Faithful
A constraint body that declares parameters (constraint c { in x : Real; assert x >= 0; }) states conditions like any other, including conditions that start with a keyword (assert true, assert not false, assume null != x, assert if …); a condition missing its expression is a diagnostic parser/behavior.go atConstraintCondition, parser/expr.go exprStartKeywords parse/constraint_parameterised_conditions.golden, parser/constraint_condition_test.go, parser/negative_test.go:constraint_params_assert_no_condition ✅ Faithful
A constraint carrying no condition yields no verdict (ErrNoConditions) rather than a vacuous pass errors.go ErrNoConditions, context.go EvaluateConstraintOn/EvaluateRequirementOn runtime/constraint_test.go:TestConstraintWithoutConditionsIsNotAVerdict ✅ Faithful

Instantiation and Feature Values (SysML v2 §7.6 Feature Values, KerML §8.3)

Semantic Rule Implementation Test Case Status
A literal default is folded at instantiation instance.go Instantiate instance_derived_slots.sysml (folded) ✅ Faithful
A default that reads sibling features is derived per instance, evaluated against that object's feature values on demand instance.go GetFeatureValue/evalFeatureValueDefault, eval.go selfFeatureValue instance_derived_slots.sysml (doubled) ✅ Faithful
A default reaching through a nested part reads that part's own derived values eval.go evalFeatureChain (via GetFeatureValue) instance_derived_slots.sysml (total) ✅ Faithful
A default expression resolves declarations in the scope that declared the feature, while instance feature values take precedence shape.go EffectiveFeature.DeclScope, eval.go EvalContext.self instance_derived_slots.sysml ✅ Faithful
Mutually dependent defaults report a cycle rather than recursing to the step budget context.go derivingSlots, errors.go ErrCyclicFeatureValue robustness_test.go:cyclic_derived_slot ✅ Faithful
A default over an undeclared feature fails naming the feature value instance.go evalFeatureValueDefault robustness_test.go:derived_slot_over_missing_feature ✅ Faithful
A multi-valued feature holds its default's contents; a single value written on it is the collection's one element instance.go GetFeatureValue runtime/instance_test.go:TestMultiValuedDefaultMaterializes, repl/instance_test.go:TestCollectionFeatureValuesShowTheirContents ✅ Faithful
A nested part usage with a body of its own is instantiated as that usage, so what its body declares wins over what its type declares, and an untyped nested part (part engine { ... }) still materializes instance.go compositeType, GetFeatureValue instance_nested_usage_body.sysml, instance_unnamed_redefinition.sysml, runtime/instance_test.go:TestNestedUsageBodyOverridesItsType, TestUntypedNestedPartMaterializes ✅ Faithful (both the named form and an unnamed :>> power = 250.0;, which takes the name of what it redefines — see the KerML 7.3.4.5 row above)
A single-bound multiplicity is both bounds, unless it is unbounded: [*] is 0..*, so a [*] feature materializes empty like [0..*] (KerML 1.0 §8.2.5.11, confirmed by OMG issue KERML11-204) semantics/multiplicity.go multiplicityRange semantics/multiplicity_test.go:TestMultiplicitySingleBoundStar, conformance multiplicity_unbounded_single_bound, parse/multiplicity_unbounded_and_subsetting.golden, passes/constraint_test.go:TestConstraint_RedefinitionUnboundedMultiplicity (a [*] redefinition keeps an inherited 0..* and loosens an inherited 1..*) ✅ Faithful
A required lower bound is materialized eagerly, so an unbounded one ([*..*]) or one past the materialization bound reports a multiplicity violation instead of allocating instance.go GetFeatureValue (maxMaterializedLowerBound, ErrMultiplicityViolation) robustness_test.go:multiplicity_infinite_lower_bound, :multiplicity_lower_bound_too_large ⚠️ Approximate (a lower bound above 1000 is refused rather than materialized lazily)
The values of a subsetting feature are values of the feature it subsets, so a nested part declared part a : Sub :> subsystem is one of the objects subsystem holds and a roll-up over subsystem sums over it (KerML 1.0 §7.3.4.4) subsetting.go subsettingContributions/relatedFeatureNames, instance.go GetFeatureValue conformance feature_chain_rollup_over_subsets, cubesat_mass_rollup, robustness_test.go:mutually_subsetting_features ⚠️ Approximate (contributions are the subsetting features declared on the same object; the subsetted collection is read-only with respect to them, and a subsetting feature declared elsewhere for the same object contributes nothing)
A bracket multiplicity is the population, not a factor: part cell : Component[4] holds four objects, each carrying its own feature values, so a roll-up such as sum(cell.basicMass) counts each object once and a per-object value multiplied by the same 4 double counts. The subsetting features are among the population and anonymous objects make up the rest of the required lower bound instance.go GetFeatureValue (subsettingContributions, then mult.Lower.Value - len(contributed) anonymous objects), collections.go sum conformance multiplicity_rollup_counts_each_instance_once (a [4] collection, and a [3] one whose named subsetting part is joined by two anonymous objects), feature_chain_rollup_over_subsets, cubesat_mass_rollup ✅ Faithful
A redefining feature is the feature it redefines, so part subsystems : Component[*] :>> Subsystems makes both names read one collection, and a chain of redefinitions (dry :>> own :>> mass) reads one feature value under every name even when a usage restates the redefinition (part sat : Sys { attribute :>> own = 10.0; }) (KerML 1.0 §7.3.4.5) subsetting.go aliasRedefinedFeatureValues, redefinitionGroups, sharedRedefinitionName, redefinedNames, isFeatureOf conformance cubesat_mass_rollup, redefinition_restated_in_a_usage, redefinition_multilevel_base_name, redefinition_value_under_either_name, redefinition_valued_under_two_names, robustness_test.go:one_feature_valued_under_two_names ✅ Faithful (the shared feature value is the one the most specific declaration writing a value created, whichever name it wrote — the redefining name, the base name, or a name in between, including where the value is written in an abstract part usage a configuration specializes; one declaration valuing two names of the feature is ErrConflictingRedefinition rather than a silent pick)
A usage of any kind whose body restates or adds features is instantiated as that usage, so a struct-typed attribute takes its value from a nested body (attribute :>> material { attribute :>> v = 3.0; }) at any depth, and a renaming redefinition that restates no type is typed by the feature it redefines (KerML 1.0 §7.4.7) instance.go CompositeTypeOf, declaresFeatures, shape.go extractType, declaredType conformance attribute_nested_value_body, parse/nested_value_body.golden, shape_test.go:TestFeaturesOf_TypeInheritedThroughRedefinition ✅ Faithful
A feature bound to a value takes its own features from that value, so a body of the same declaration valuing one of them (attribute :>> ringCost = 400.0 { attribute :>> v = 9.0; }) states two values for it and is reported; a body that only re-declares features (the stdlib's item :>> edges : Ellipse = shape { attribute :>> Shell::edges::innerSpaceDimension, Ellipse::innerSpaceDimension; }) states no second value and reads the bound one instance.go restatedInValuedBody, restatedValueInBody, GetFeatureValue (ErrValuedFeatureRestated) robustness_test.go:valued_feature_restated_in_a_body, conformance attribute_nested_value_body (valuedWithReDeclaredFeatures) ✅ Faithful (the two values are equally specific here, neither one governing, so the model is reported rather than a value picked; a body over a value the redefined declaration wrote is the more specific declaration and governs instead — see the row below)
A body on a redefining declaration valuing features the value it inherits would supply governs over that value, so part def Ring { attribute cost : Cost = template; } re-opened as part r : Ring { attribute :>> cost { attribute :>> v = 11.0; } } reads r.cost.v as 11.0: the more specific declaration of a feature is the one that holds (KerML 1.0 §7.3.4.5, FeatureValue in KerML.kerml) instance.go valueBinds, bodyGovernsInheritedValue, restatedValueInBody, valuesAFeature (a value stated anywhere in the body, at any depth, is what makes it govern), read by CompositeTypeOf, Instantiate, materializeFeatureValue and adopt.go derivedFeatureValue conformance attribute_body_over_inherited_value (incl. deepReDeclarationKeepsInheritedValue, valueStatedAtDepthGoverns), attribute_body_over_inherited_value_chain, runtime/instance_test.go:TestBodyGovernsAnInheritedValue ⚠️ Approximate (the body supersedes the inherited value rather than merging with it — a feature the body does not value takes its type's own default, not the bound value's, since a FeatureValue binds a feature as a whole and KerML states no per-nested-feature composition of one)
A check made without materializing an object reads the same values one built from the declaration holds, so a condition naming a feature a body governs over reports it uninitialized rather than judging the superseded value, and an edit confined to a governing body changes the type's shape a carried-over object is admitted by condition.go conditionFeatures, adopt.go writeShape, ShapeDigest, read by context.go memberBindings runtime/instance_test.go:TestConditionsDoNotReadAGovernedOverValue, runtime/adopt_test.go:TestShapeFollowsAGoverningValueBody ✅ Faithful (the two readings agree; a feature the body governs is read from the object, so a check without one names it uninitialized rather than answering from a value the model replaced)
A redefining feature that declares no value holds the value the redefined declaration wrote, evaluated in the scope that wrote it, so attribute grossMass :>> mass reads the inherited default under either name (KerML 1.0 §7.3.4.5) shape.go redefinedDefault, EffectiveFeature.DefaultScope, instance.go evalFeatureValueDefault conformance instance_redefined_attribute_default, subsetting_test.go:TestRedefiningFeatureHoldsTheRedefinedDefault ✅ Faithful
A multi-valued feature given a default holds that default's contents whether or not it is also typed, so attribute volumes : Real[0..*] = subsystem.volume is the chain's values rather than an empty typed collection instance.go GetFeatureValue, CompositeTypeOf conformance feature_chain_rollup_over_subsets, feature_chain_nested_multivalued, instance_test.go:TestTypedMultiValuedDefaultHoldsItsContents ✅ Faithful
A value type classifies values, not objects (Base::DataValue is abstract datatype DataValue specializes Anything, "entities that are values"), and a Real declares no features, so a valueless feature of one holds no value: every surface that reports a value reports it as <unset>-instantiate/-e, %features, the JSON report, and pb.Value.unset / opensysml.UNSET on the wire — rather than as the empty object materialization creates for it runtime/instance.go Context.HoldsNoValue, UnsetText, isValueTypeSymbol; repl/meta.go formatValue/formatSlot/nestedInstances, repl/run.go namedValues/renderResults (which cmd/sysml/report.go renders as JSON); grpc/convert.go ValueToProtoIn, ProtoToValueIn (ErrUnsetNotAccepted); python/opensysml/values.py UnsetType/value_to_python conformance instance_valueless_value_typed_attribute, robustness_test.go:expression_over_a_slot_holding_no_value, repl/unset_feature_value_test.go, cmd/sysml/unset_feature_value_test.go:TestUnsetFeatureValueReadsTheSameOnEverySurface, gRPC conformance instantiate_unset_slot, grpc/unset_feature_value_test.go, python/tests/test_unset.py ⚠️ Approximate (the spec settles neither what materialization creates for such a feature nor a rendering for it, so the empty object stays and only the reporting is unset; a valueless 1..1 feature draws no diagnostic, since the multiplicity check bounds values a model binds and this one binds none)
Only a conforming multi-valued default is honoured: a default whose element count is within the declared multiplicity becomes the feature's value — a collection literal, one value, (), an expression's result, the objects a composite part[n] default names, quantities with their units — while one outside it is a multiplicity violation rather than a value broadcast to the lower bound, truncated to the upper one or dropped instance.go GetFeatureValue, checkDefaultCount (ErrMultiplicityViolation), semantics/multiplicity.go Range.CountViolation conformance multiplicity_default_merged, multiplicity_default_composite, multiplicity_default_nonconforming, multiplicity_default_redefinition, instance_test.go:TestCompositeMultiValuedDefaultHoldsTheNamedObjects, instance_test.go:TestNullDefaultHoldsNoElements, robustness_test.go:default_not_conforming_to_multiplicity, semantics/multiplicity_test.go:TestMultiplicityWithANonEvaluableBound (a bound that is not constant leaves that side unknown — the known side still bounds the count), :TestMultiplicityInfiniteLowerWithFiniteUpper ✅ Faithful
A feature that declares no multiplicity holds exactly one value, so a default is bound by the assumed 1..1attribute x : Real = (1.0, 2.0) is the multiplicity violation Real[1] = (1.0, 2.0) already is (KerML 1.0 §7.4.5) semantics/multiplicity.go AssumedRange, Model.EffectiveMultiplicityOf, used by runtime/shape.go extractMultiplicity and runtime/instance.go checkDefaultCount semantics/multiplicity_test.go:TestEffectiveMultiplicityAssumesOne, :TestMultiplicityOfANonUsage (a definition and a nil symbol declare none and take the assumption), conformance multiplicity_default_assumed, robustness_test.go:default_against_an_undeclared_multiplicity, cmd/sysml/materialize_test.go:TestCheckReportsMaterializationDiagnostics ⚠️ Approximate (the assumption bounds a default where its value count is known — when the feature value materializes; the static tier still bounds only a declared multiplicity, so a multi-valued default on an undeclared one is reported at materialization rather than by passes.checkValueCount)
Materializing an object is part of a run, so -instantiate reports every feature value that did not materialize and -validate reports no errors only for a run that found none: an invalid model exits 2 rather than 0 cmd/sysml/check.go runChecks, cmd/sysml/report.go reporter.finding, clean, status, over runtime/materialize.go Context.MaterializationErrors (read through repl/instantiate_report.go Session.InstantiateReport) cmd/sysml/materialize_test.go:TestCheckReportsMaterializationDiagnostics (exit code plus the diagnostic, for a clean model, a scalar default against [3], and a multi-valued default on an undeclared multiplicity), :TestCheckReportsMaterializationDiagnosticsAsJSON ✅ Faithful for -instantiate/-validate, documented in docs/reference/cli.md § Exit status
The prompt surface keeps the same rule: a meta-command that rendered a feature value it could not materialize — a %features listing carrying <error: …>, or an %eval of such a feature value, pinned to a context or not — answered nothing about it, so the failure is carried as the runtime's typed error in the session and a non-interactive (piped / non-TTY) run exits 2, while at a terminal the failure is reported at the prompt and the session goes on repl/run.go Session.HasErrors, MaterializationFailures, noteMaterializationFailure, noteIfMaterializationFailure, hasAnalysisErrors, recorded by repl/meta.go doFeatures/slotWalk, doEval and doEvalLine, read by cmd/sysml/main.go sessionStatus repl/materialize_status_test.go:TestFeatureValuesCarriesMaterializationFailureIntoStatus, :TestEvalCarriesMaterializationFailureIntoStatus, :TestPinnedEvalCarriesMaterializationFailureIntoStatus, :TestFeatureValuesOfAConformingModelLeaveNoFailure, :TestPromptContinuesAfterAMaterializationFailure, cmd/sysml/materialize_test.go:TestPipedSessionExitsOnAMaterializationFailure, :TestSessionStatusAtATerminal ✅ Faithful
A feature value that could not be materialized is marked as such by the runtime that read it, so a surface tells it from any other failure to evaluate without matching rendered text, whatever expression it surfaced through; naming no feature value of the object is not such a failure runtime/errors.go FeatureValueError, ErrFeatureValueMaterialization, marked at runtime/instance.go GetFeatureValue over materializeFeatureValue, tested by repl/run.go noteIfMaterializationFailure repl/materialize_status_test.go:TestPinnedEvalCarriesMaterializationFailureIntoStatus, :TestEvalOfAnUnknownFeatureValueIsNoMaterializationFailure, :TestFeatureValuesCarriesMaterializationFailureIntoStatus (the marked error still unwraps to ErrMultiplicityViolation) ✅ Faithful
The check reads what it can: a walk that spent its budget, met a part deeper than it descends, or met a kind already being expanded above it reports what it did not read rather than that there were no errors, and being no model error it stays 0 runtime/materialize.go materializeWalk.walk (bounded), cmd/sysml/check.go runChecks, cmd/sysml/report.go reporter.warn (runtime.materialize.bounded) runtime/materialize_test.go:TestMaterializationErrorsBoundsAWideModel, :TestMaterializationErrorsOfAConformingObject, cmd/sysml/materialize_test.go:TestCheckDoesNotReportCleanWhenNestingWasElided ✅ Faithful
The multiplicity a redefining feature does not restate is the one it redefines, so a default it adds is bound by the redefined declaration's multiplicity (KerML 1.0 §7.3.4.5) shape.go buildFeatures, redefinedMultiplicity, passes/typecheck_value.go effectiveRange conformance multiplicity_default_redefinition, passes/typecheck_value_test.go:TestValueCountAgainstRedefinedMultiplicity ✅ Faithful
A relationship target that resolves outside the object names no feature of it, so attribute totalmass :> ISQ::mass specializes the library feature and contributes nothing to a same-named feature of the object; a target the object carries under its name — including one a restating declaration masks — is a feature of it, and an unqualified target the declaring scope cannot see is looked up among the object's members subsetting.go relatedFeatures/isFeatureOf subsetting_test.go:TestSubsettingIgnoresALibraryFeatureOfTheSameName, conformance cubesat_mass_rollup ✅ Faithful

Redefinition in a Specialization (KerML §7.4.7 Redefinition, SysML v2 §7.6)

Semantic Rule Implementation Test Case Status
A usage that redefines an inherited usage (part derived :> base { part :>> inner { … } }) specializes what it redefines, so it keeps every nested member the redefined usage declared and overrides only what it restates semantics/model.go NewModel (attaches the model to resolve.Resolver, so a redefinition target reachable only through inheritance resolves and the redefining usage gains it as a supertype), consumed by runtime/shape.go FeaturesOf over Model.MembersOf redefinition_inherited_nested_values.sysml, ballandchain_variant_configuration.sysml, robustness_test.go:deep_specialization_chain_of_redefinitions, conflicting_redefinitions_at_several_levels ✅ Faithful (multi-level chains, a redefinition of a redefinition, and conflicting restatements where the innermost wins; the merge is the inherited-member view, not a feature value-level merge in the instantiator)
A union's instances are exactly those of its unioning types (KerML §8.3.3), so a type declared classifier MyWheel unions MyWheel1, MyWheel2 conforms to every type all of its unioning types conform to, and feature redefines rollsOn : MyWheel redefining rollsOn : Wheel is well-formed. Unioning is not a generalization edge — a union inherits nothing from its members — so it is resolved separately from DirectSupertypes semantics/model.go Model.ConformsunionConforms, UnioningTypes passes/constraint_unions_test.go:TestConstraintRedefinitionConformsThroughUnion, :TestConstraintRedefinitionUnionMemberDoesNotConform, :TestConstraintRedefinitionUnionCycleTerminates ✅ Faithful (conformance only: a union's members are not computed, and intersects/differences are not read)
The type a redefinition must inherit the redefined feature from is the feature's featuring type where it declares one (member feature CC1_snapshots :>> Occurrences::Occurrence::snapshots featured by CC1; is featured by CC1, not by the feature it is written inside — KerML §7.4.5, §8.3.4.3), and a bare feature owned by a package has no featuring type, so nothing can inherit it and the rule does not apply passes/constraint.go checkRedefinition over featuringOwners (the declared featured by targets, else the lexical owner), isInheritedMember and isPackageLevelFeature passes/constraint_test.go:TestConstraint_RedefinitionUsesFeaturingType, :TestConstraint_PackageLevelRedefinitionHasNoInheritedOwner, :TestConstraint_PackageLevelUnfeaturedRedefinitionExemptsNoInheritedRule ⚠️ Approximate (only a declared featured by is read — the featuring a nested feature implies is not computed — and a member whose owner is declared within the redefined feature's own scope is taken as inheriting it, which is the TimeVaryingFeatures.kerml shape rather than a general rule. The package-level exemption is decided by the absence of a featured by relationship, so a package-level feature that declares one is still checked)

Variation and Variant (SysML v2 §7.20 Variant Modelling)

Semantic Rule Implementation Test Case Status
variation and variant are recorded on the declaration they modify, in every position they may appear (variation attribute/part/interface, nested or top-level) parser/defusage.go applyFeatureMods, atKindPrefix, ast/defusage.go Usage.IsVariation/IsVariant, round-tripped by export/rdf_out.go/rdf_in.go parser/testdata/parse/variation_and_variant.golden, parser/negative_test.go (variation_no_declaration, variation_attribute_no_name, variant_unclosed_body, variant_selection_no_variant_name) ✅ Faithful
A variation is an abstract classifier of its variants: a variant specializes its variation and so carries the variation's type and features semantics/model.go DirectSupertypes via semantics/variation.go VariationOwning semantics/variation_test.go:TestVariationAndVariantModifiers, TestVariantsOfInheritedThroughRedefinition ✅ Faithful
A variant is reachable through the variation feature's name (cut::cutIdeal), including through a feature chain (engagementRing.nesting::nestingTrue) and through a feature that redefines or specializes the variation semantics/variation.go Model.IsVariationFeature, Model.VariantsOf, Model.VariantOf, runtime/variation.go EvalContext.variantSegment, runtime/eval.go evalFeatureChain variation_attribute_selection.sysml, variation_part_selection.sysml, semantics/variation_test.go:TestIsVariationFeatureThroughSpecialization ✅ Faithful
Binding a variation usage to one of its variants selects that variant, with its nested values and its own nested features, whether the variation is read through an object's feature value or through its declaration (EvaluateConstraint, EvalWithScope, REPL %eval) runtime/variation.go Context.bindVariation, variantValue, EvalContext.bindVariationOf, runtime/instance.go GetFeatureValue (variation feature values resolve before ordinary defaults), runtime/value.go ValVariant variation_attribute_selection.sysml, variation_part_selection.sysml, variation_interface_selection.sysml, robustness_test.go:variation_read_through_its_declaration ✅ Faithful
A variation feature compares equal to the variant it is bound to (x == x::variantName), and unequal to any other variant — the form an asserted configuration constraint uses runtime/eval.go equality over ValVariant, runtime/value_equality.go variation_attribute_selection.sysml, variation_interface_selection.sysml, variation_interface_mismatch.sysml, ballandchain_variant_configuration.sysml ✅ Faithful
A variation with no variant selected is a typed error, never a silently wrong value, and a variation is no occurrence of itself, so a chain through an unselected variation part reports the same rather than reading an object of the variation runtime/errors.go ErrVariationUnselected, runtime/instance.go GetFeatureValue, runtime/eval.go evalFeatureReference, runtime/calc_usage.go occurrenceOperand variation_unselected.sysml, robustness_test.go:variation_without_a_selected_variant, chain_through_an_unselected_variation_part ✅ Faithful
Selecting what is not a variant of the variation, or selecting two variants at once, are typed errors naming the variants available runtime/errors.go ErrNotAVariant/ErrMultipleVariants, runtime/variation.go bindOneVariant, variantSummary, runtime/eval.go (a missing member under a variation feature) robustness_test.go:variation_bound_to_what_is_not_a_variant, variation_bound_to_two_variants, semantics/variation_test.go:TestSelectsVariantOfRejectsForeignVariant ✅ Faithful
A variant whose owner is not a variation offers no choice, so it stays an ordinary feature of its owner and the idle variant keyword is reported; an owner that is a variation point by specialization still offers its variants as choices passes/constraint.go checkVariantOutsideVariation (warning variant-outside-variation), runtime/shape.go buildFeatures and runtime/eval.go (only a variant of a variation point is a choice rather than a feature value, via semantics/variation.go Model.VariationPointOwning over IsVariationFeature, which semantics/model.go DirectSupertypes also uses so a variant specializes such a point and inherits its type, and VariantsOf uses so the choices offered are the choices a selection accepts) variant_outside_a_variation.sysml, variant_under_an_inherited_variation.sysml, robustness_test.go:variant_outside_a_variation, variant_under_a_redefined_variation, passes/constraint_test.go:TestConstraintVariantOutsideVariation, TestConstraintVariantInsideVariationOK, TestConstraintVariantUnderInheritedVariationOK, semantics/variation_test.go:TestVariantsOfExcludesAMisplacedInheritedVariant ✅ Faithful
The object a selected variant stands for belongs to the selection that made it: two owners, or two variation points read through their declarations, each get their own object runtime/variation.go variantObject (keyed by owning object, variation point and variant), variantValue robustness_test.go:two_owners_selecting_one_variant, two_ownerless_selections_of_one_variant, repeated_reads_of_a_variant_object ✅ Faithful
variation interface and its variant interface … connect … members parser/defusage.go (interface usages take the same modifiers), selection as above; the selected variant's connection is realized by runtime/variation.go variantInstance over runtime/connector.go materializeConnector, and routing follows it through runtime/routing.go routableConnections/realizedConnections/selectedVariant over lower/connection.go Connection.Variation/Variant/Owner and ToObjectConnections, with the object performing the behavior carried by runtime/context.go ExecuteActionPerformedBy/ExecuteStatePerformedBy variation_interface_selection.sysml, variation_interface_mismatch.sysml, ballandchain_interface_connected.sysml, ballandchain_interface_disconnected.sysml, ballandchain_variant_configuration.sysml, signal_test.go:TestRoutingHonorsTheSelectedVariantConnection, lower/connection_test.go:TestLowerVariantConnectionsCarryTheirVariation, :TestLowerObjectConnectionsAreOwnedByTheObject, variant_connection_per_owner.sysml, signal_test.go:TestRoutingIsPerOwnerVariantSelection ✅ Faithful (the variant is selected and compares equal, so a configuration constraint over interface variants evaluates, and the connection that variant declares is a real runtime connector whose ends are the connected features, so port communication follows the selected variant and not the variants left unselected; a connection an object declares routes for that object, so two objects of one type selecting different variants each route their own)

⚠️ Variant selection is not ordering-sensitive — a variation feature value resolves to one variant per instance — so no golden execution trace accompanies these rows.

Requirement

Semantic Rule Implementation Test Case Status
Require expression evaluation, in a requirement definition body as well as a usage parser/behavior.go parseRequirementBody (both parseDefinition and parseUsage paths), condition.go conditionsOf requirement_literal.sysml, requirement_def_body_require.sysml, parser/behavior_require_member_test.go:TestRequirementConditionForms ✅ Faithful
A condition stated through an anonymous nested constraint (require constraint { <expr> }, the form the OMG Domain Libraries use) is evaluated, and every condition of that body is kept parser/behavior.go parseNestedConstraintConditions, condition.go appendConditions requirement_nested_constraint.sysml, parser/behavior_require_member_test.go:TestRequireMemberRetainsConditions ✅ Faithful
A requirement's conditions see the requirement's own features — declared, inherited, or rebound by a typed usage (attribute :>> maxVerticalSpeed = 1.5;) condition.go conditionFeatures, eval.go evalFeatureReference requirement_own_attribute.sysml, requirement_nested_constraint.sysml, runtime/condition_test.go:TestRequirementConditionSeesOwnAttributes ✅ Faithful
A feature a condition names but which carries no value reports that (ErrNoValue) rather than being unresolved errors.go ErrNoValue, eval.go evalFeatureReference runtime/condition_test.go:TestRequirementConditionWithoutValueIsNotUnresolved ✅ Faithful
A violated condition names the condition that failed, not only the element stating it errors.go ViolationError, condition.go conditionText requirement_violated.sysml, runtime/condition_test.go:TestRequirementConditionSeesOwnAttributes ✅ Faithful
Subject binding evaluation context.go:148 EvaluateRequirement (Pass 1) requirement_subject.sysml ✅ Faithful
Actor binding evaluation context.go:148 EvaluateRequirement (Pass 1) requirement_actor.sysml ✅ Faithful
Assume expression evaluation context.go:148 EvaluateRequirement (Pass 2, doesn't fail) requirement_assume.sysml ✅ Faithful
A false required condition is a verdict, not a malfunction (ErrViolated), like a false assertion context.go EvaluateRequirementOn, errors.go ErrViolated repl/instance_test.go:TestRequirementViolationIsAVerdictNotAnError ✅ Faithful
A requirement usage inherits assume/require conditions from the definition it is typed by, and the values it rebinds are the ones those conditions see context.go chainMembers, condition.go conditionFeatures requirement_nested_constraint.sysml, requirement_violated.sysml ✅ Faithful
A subject may redeclare the one it inherits (subject subj : View[1] :>> RequirementCheck::subj;) parser/behavior.go parseSubjectMember, resolve/document.go, passes/typecheck.go checkSubjectMember parser/behavior_require_member_test.go:TestRequirementConditionForms, libs/stdlib_conformance_test.go (Systems Library/Views.sysml) ✅ Faithful
Nested requirements context.go:148 EvaluateRequirement (recursive) requirement_nested.sysml ✅ Faithful
satisfy <name> is an OwnedReferenceSubsetting of an existing usage, not a typing (SysML v2 §8.3.21.10 SatisfyRequirementUsage) parser/defusage.go parseDefUsage (ast.RelSubsets) parser/testdata/parse/satisfy_reference.golden ✅ Faithful
referencedFeatureTarget().oclIsKindOf(RequirementUsage) — satisfy/verify may only reference a requirement usage (incl. viewpoint/concern usages) passes/typecheck.go compatMessage, isRequirementUsageKind passes/typecheck_test.go TestTypeCheckSatisfyRequirementUsageOK, TestTypeCheckSatisfyViewpointUsageOK, TestTypeCheckSatisfyNonRequirementUsageError ✅ Faithful
assert satisfy <requirement> by <part>; is a verdict of its own: the assertion is evaluated as the requirement usage it is (SatisfyRequirementUsage, SysML v2 §8.3.21.10), with the requirement's subject parameter bound to the object the by feature supplies, so the conditions read that object's values runtime/satisfy.go SatisfyAssertionsIn, EvaluateSatisfactionOn, repl/meta.go doSatisfy satisfy_subject_binding.sysml, satisfy_inherited_conditions.sysml, runtime/satisfy_test.go, repl/satisfy_test.go:TestSatisfyVerdicts ✅ Faithful
An assertion may be negated (assert not constraint { … }, assert not satisfy … by …; Invariant::isNegated, SysML v2 §8.3.21.10), and holds exactly when the conditions it denies do not ast/defusage.go Usage.IsNegated, parser/defusage.go applyFeatureMods, runtime/condition.go evaluateConditions parser/testdata/parse/assert_negated.golden, satisfy_negated.sysml, runtime/negation_test.go ✅ Faithful
A negated element states no condition it can deny when its body holds only assumptions, which are trusted rather than checked, so it reports no condition to evaluate rather than a violation naming none runtime/condition.go evaluateConditions runtime/condition_test.go TestNegatedConstraintWithOnlyAssumptionsIsNotAVerdict ✅ Faithful
A negation denies the conditions of the constraint it is written on togethernot (a and b), not not a and not b — so it holds as soon as one of them fails runtime/condition.go appendConditions, conditionHolds runtime/condition_test.go TestNegatedNestedConstraintNegatesTheConjunction, constraint_negated_group.sysml ✅ Faithful
An ObjectiveMembership's ownedObjectiveRequirement is a RequirementUsage (SysML v2 §8.3.22.4), so an objective is typed by a requirement definition or a specialization of one, never by a structural definition passes/typecheck.go compatibleTyping, isRequirementDefKind passes/typecheck_kinds_test.go TestTypeCheckObjectiveTypedByRequirementDefOK, TestTypeCheckObjectiveTypedByConcernDefOK, TestTypeCheckObjectiveTypedByPartDefError, TestTypeCheckObjectiveTypedByActionDefError ✅ Faithful
A SubjectMembership's ownedSubjectParameter is an unconstrained Usage (SysML v2 §8.3.21), so a definition of any kind types a subject — including the port def and action def the OMG training models use — and the rule applies however the requirement body is written, not only when the subject happens to parse as a usage passes/typecheck.go checkSubjectMember, compatibleTyping passes/typecheck_subject_test.go TestTypeCheckSubjectIsCheckedWhateverPrecedesIt, TestTypeCheckRequirementUsageSubjectIsChecked, TestTypeCheckSubjectWithoutResolvableTypeIsNotATypeError; typecheck_kinds_test.go TestTypeCheckSubjectTypedByAnyDefKindOK, TestTypeCheckSubjectTypedByUsageError ✅ Faithful
A definition specializes a definition of a comparable kind: a PartDefinition is an ItemDefinition (SysML v2 §8.3.9.2), so individual item def Alice :> Person names a part definition legally, while disjoint kinds — an occurrence and a data value (§8.4.5.1) — are refused passes/typecheck.go defKindParents, defKindSpecializes, defKindsComparable passes/typecheck_kinds_test.go TestTypeCheckSpecializeComparableKindOK, TestTypeCheckNewKindSpecializeCrossKindError; model/training_examples_test.go (28. Individuals/Individuals and Time Slices) ✅ Faithful
Every definition implicitly specializes the library definition of its kind, so the features that base declares resolve in its body — Items::Item declares start and done, which Parts::Part redefines semantics/implicit.go implicitDefinitionBases, implicitBase model/implicit_typing_test.go, model/training_examples_test.go (27. Occurrences/Time Slice and Snapshot Example) ✅ Faithful
A quantity expression (1.5 [m/s]) evaluates to a magnitude and the measurement reference it is written in (Quantities::ScalarQuantityValue is num + mRef), so a condition comparing values written with units reaches a verdict runtime/quantity.go evalIndexExpr, value.go ValQuantity requirement_quantity_same_unit.sysml, runtime/quantity_test.go:TestQuantityEvaluation, parser/testdata/parse/quantity_expression.golden ✅ Faithful
Name in the unit position of a quantity expression. x [u] invokes Quantities::'['(num, mRef), so u is an ordinary operand expression and its name is resolved by ordinary name resolution: resolution returns the nearest declaration the name reaches (KerML 8.2.3.5.3 Local and Visible Resolution, 8.2.3.5.4 Full Resolution), and the position's expected type (ScalarMeasurementReference) only decides whether what resolved conforms (KerML 8.2.3.5.1). A sibling named m therefore shadows an imported SI::m — resolution does not continue outward looking for a unit — and the quantity is rejected with a diagnostic naming the declaration, the namespace declaring it, and the qualified spelling of the unit it hid. One routine implements this for every evaluator (part feature value default, action/state attribute default, calc return, condition) semantics/units.go unitTermOfName, ShadowedUnitError, unitOutside; runtime/context.go chainMembers (a condition evaluates in its own body scope, as the other paths do) unit_shadowed_by_sibling_slot.sysml, unit_shadowed_by_sibling_action.sysml, unit_shadowed_by_sibling_calc.sysml, unit_shadowed_by_sibling_constraint.sysml, unit_shadowed_by_local_unit.sysml, unit_undeclared.sysml, robustness_test.go:quantity_unit_shadowed_by_sibling ✅ Faithful
A violated assertion renders a quantity operand as it was written (1.0 [m] > 500.0 [m]), since the bracket form is a quantity and not a sequence index runtime/condition.go conditionText (ast.IndexExpr) runtime/condition_test.go:TestViolationRendersQuantityOperands ✅ Faithful
Commensurable units convert before a comparison or a sum, through MeasurementUnit::unitConversion and unit-defining expressions reduced to base units — 1.5 [m/s] <= 5.4 [km/h] is true, exactly, at its boundary (a conversion factor is kept as a ratio, not evaluated) semantics/units.go UnitTermOf, Scale, ConvertMagnitude requirement_quantity_converted_unit.sysml, constraint_quantity_sum.sysml, semantics/units_test.go:TestScaleStaysExact ✅ Faithful
A unit is composed by the operation over quantities (10 [m] / 2 [s] is 5 [m/s]), and a ratio of like quantities is a number of no unit. A composed operand is parenthesized in the composed unit's text, so (m/s) * (kg/s) names m/s*(kg/s) rather than a unit that re-reads as m/(s*kg)/s runtime/quantity.go scaleQuantities, composedUnitText, groupUnitText, semantics/units.go UnitTerm.DividedBy constraint_quantity_quotient.sysml, calc_quantity_ratio.sysml, runtime/quantity_test.go:TestComposedUnitText ✅ Faithful
A quantity raised to a constant exponent raises its unit with it, and its magnitude comes from the one ** implementation the folder and the runtime share — so (0.0 [m]) ** -1.0 and an overflowing magnitude are the same typed errors as for a bare number rather than an infinity carried in a unit, and (2 [m]) ** 3 keeps an Integer magnitude runtime/quantity.go powQuantity, semantics/eval.go Pow, semantics/units.go UnitTerm.Pow runtime/quantity_test.go:TestQuantityExponentiation, TestQuantityExponentiationReports ✅ Faithful
An execution trace of a unit-carrying value renders the magnitude and the unit (5.0 [m/s]), as the REPL prints a quantity runtime/trace.go FormatTraceValue action_quantity_assign.trace.golden, runtime/quantity_test.go:TestFormatTraceValueQuantity ✅ Faithful
Incommensurable units are a typed error (ErrIncommensurableUnits), never a comparison of bare magnitudes that would equate 1.5 [m/s] with 1.5 [km/h] runtime/errors.go ErrIncommensurableUnits, quantity.go convertTo runtime/robustness_test.go quantity_incommensurable_comparison, runtime/quantity_test.go:TestQuantityIncommensurable ✅ Faithful
Statically detectable incommensurability is diagnosed before evaluation. A comparison or a sum whose operands' quantity dimensions are both statically determined and incommensurable (mass < 1000.0 [m]) is reported as a warning at the type tier, naming both units and both dimensions; the dimension comes from the stdlib QuantityDimension power factors (ISQ's L, M, T, …) and commensurability from the same UnitTerm.Commensurable the runtime applies, so the two cannot drift. Evaluation remains the hard error, and a warning changes no exit code semantics/dimension.go DimensionOfExpr, dimensionOf; passes/typecheck_dimension.go checkDimensions model/dimension_check_test.go, model/typecheck_expr_corpus_test.go (stdlib, examples and OMG corpus stay silent) ⚠️ Approximate (static only — see below)

⚠️ The static dimensional check reports only what a declaration determines, and is silent — by design, not by omission — wherever it does not. A feature's dimension is taken from the quantity value type it declares (attribute mass : ISQ::MassValue), never from the value it happens to be bound to, since an assign may replace that value with one of another dimension. So an operand whose dimension comes from an untyped attribute or parameter, a calculation result, an unresolved name, or a redefinition not yet bound is undetermined and no warning is raised (a parameter that does declare a quantity type, in actual : ISQ::MassValue, is determined and is checked); the mistake then surfaces at evaluation as it did before. Products and quotients are not checked, having no commensurability requirement, and a dimension the check derives through unit arithmetic (m/s) is compared by dimension alone.

⚠️ The spec's own QuantityCalculations::ConvertQuantity(x, targetMRef) is not an invocable function, and a quantity is not yet an instantiated ScalarQuantityValue object whose num/mRef features can be read by name: the unit is carried on the runtime value, not modelled as a library object. The gRPC value schema does carry a quantity: Value.quantity holds the magnitude as written, the unit as written, and the reduced unit term (scale and base-unit factors by FQN), so a Python caller reads a quantity feature value as opensysml.values.Quantity, sends one as an action input or a calc argument (opensysml/values.py Quantity.to_pb, connection.py _python_to_value), and a round trip preserves both magnitude and unit (internal/grpc/convert.go QuantityToProto/ProtoToQuantity/ProtoToValueIn). Sequence indexing (speeds#(3)), which the parser represents with the same node, is evaluated as the index it is (see Sequence Indexing and Collection Operations below): the two forms are told apart at the node (ast.IndexExpr.Bracket), so an index is never read as a magnitude in a unit, nor a quantity as an index.

⚠️ A requirement feature that carries no value of its own is read from the satisfying object's feature of that name, which is how a requirement stated over the values it checks (attribute verticalSpeed; compared against a limit) reaches a verdict from by. The spec supplies a subject's values to a requirement through the subject parameter (subject lander : Lander; then lander.verticalSpeed) or an explicit binding, not by matching names, so this fallback — the same one %requirement applies on an instance — is an approximation, and a requirement whose unbound feature happens to share a name with an unrelated feature of the subject would be checked against it. A requirement whose value comes from neither its own binding nor the subject (the lunar lander model's actualVerticalSpeed, produced by an analysis) still has no value to check and reports ErrNoValue.

Action (SysML v2 Actions — Systems Library/Actions.sysml, over KerML Performances)

Semantic Rule Implementation Test Case Status
Initial node token placement action_executor.go:425 initialize action_control_flow.sysml ✅ Faithful
Final node termination action_executor.go:512 stepFinalNode action_control_flow.sysml ✅ Faithful
One feature space per performance: a succession is a HappensBefore link (Kernel Semantic Library Occurrences.kerml) that orders occurrences and carries no values, so the steps of an action — concurrent ones included — read and write the features of the one action they belong to action_executor.go:29 ActionExecutor.data, :519 retireToken, :1028 Results conformance/action_fork_branches_share_features.sysml + trace golden, action_executor_test.go:TestActionExecutor_ForkNode_SharedFeatureSpace ✅ Faithful
Fork node (1→N parallelism): duplicates control only — ForkAction is a ControlAction, which "has no inherent behavior", and "fork behavior results from requiring that the target multiplicity of all outgoing succession connectors be 1..1" (Systems Library Actions.sysml) action_executor.go:529 stepForkNode action_control_flow.sysml, conformance/action_fork_branches_share_features.sysml ✅ Faithful
Join node (N→1 synchronization): "join behavior results from requiring that the source multiplicity of all incoming succession connectors be 1..1" (Systems Library Actions.sysml), so it synchronizes control and merges no values action_executor.go:558 stepJoinNode action_control_flow.sysml, conformance/action_fork_branches_share_features.sysml ✅ Faithful
Concurrent branches assigning the same feature action_executor.go:529 stepForkNode (branches write the shared space in step order) robustness_test.go:fork_branches_assigning_the_same_feature ⚠️ Approximate (branches out of a fork are unordered by the spec, so a same-feature write from each leaves the value unspecified; the runtime steps tokens in a fixed order and the last write in that order stands — step order, not the order the branches are written in — deterministic per model, but not a spec-mandated outcome, and no conflict is reported)
Merge node (N→1 non-blocking) action_executor.go:626 stepMergeNode action_control_flow.sysml ✅ Faithful
Decision node (guarded branching) action_executor.go:656 stepDecisionNode action_control_flow.sysml ✅ Faithful
Action execution nodes action_executor.go:723 stepActionExecutionNode action_control_flow.sysml ✅ Faithful
Nested action invocation action_executor.go:782 stepNestedAction, invoke_action.go invokeAction invoke_action_test.go:TestInvokeActionPassesParametersBothWays ✅ Faithful
Assignment statement in a body (assign x := <expr>) lower/action_graph.go lowerStatement Assign; runtime/action_statements.go execStatement action_send_accept.sysml, lower/action_body_test.go:TestActionBodyLowering ✅ Faithful
Conditional statement (if <cond> { … } else { … }) lower/action_graph.go lowerStatement/lowerBlock (If); runtime/action_statements.go execIf, execBlock action_if_else_then_branch.sysml, action_if_else_else_branch.sysml, action_if_no_else.sysml, action_nested_loop_if.sysml + trace golden, lower/action_body_test.go:TestActionBodyLoopAndConditionalLowering, passes/typecheck_test.go:TestTypeCheckNonBooleanControlFlowConditions ✅ Faithful (the condition is evaluated outside both branches; each branch body is a namespace of its own, so the names it declares do not reach the enclosing behavior)
Pre-condition loop (while <cond> { … }) lower/action_graph.go lowerStatement (Loop, ast.LoopWhile); runtime/action_statements.go execLoop action_while_loop.sysml + trace golden, action_while_loop_zero_iterations.sysml, parse/action_loop_forms.golden ✅ Faithful (tested before every iteration, so the body may run no times)
Post-condition loop (loop { … } until <cond>;) parser/behavior.go parseLoopAction; lower/action_graph.go (ast.LoopUntil); runtime/action_statements.go execLoop action_loop_until.sysml, action_loop_until_repeats.sysml + trace golden ✅ Faithful (tested after every iteration, so the body runs at least once)
Iteration over a collection (for <x> in <collection> { … }) ast/behavior.go WhileLoopActionNode.Variable/Collection; symbols/builder.go (the variable is a member of the loop's own scope); runtime/statements.go forLoop, forElements action_for_loop.sysml, action_for_over_produced_collections.sysml + trace golden, parse/action_loop_forms.golden, action_for_over_a_part_collection.sysml + trace golden, statements_test.go:TestForElementsOrder, robustness_test.go:for_over_a_value_no_expression_makes_iterable ✅ Faithful (the collection is evaluated once, before the loop is entered. Every collection the expression layer produces is iterated: a sequence in the order the expression built it — a literal sequence as written, a range ascending and a descending range empty (range.go rangeSequence), a filter in the order of the collection it filtered, a collection-valued function's result as returned — a set in the order its canonical rendering sorts in, since a set has no order of its own, and null, which holds no element, not at all. A for input that is not a collection reports ErrTypeMismatch naming what it was given)
A for input that is not a collection is reported, where a general collection reader coerces one runtime/statements.go forElements (ErrTypeMismatch), deliberately stricter than runtime/collections.go elementsOf, which keeps reading a scalar as the one-element collection KerML makes of it — a for over a scalar is a modelling error, and a single silent iteration hides it, while the general readers (a collection operator's argument, a multiplicity check) legitimately coerce action_for_over_a_scalar.sysml (typed error), action_for_over_a_part_collection.sysml (a valid collection input, nested in a part) + trace golden, statements_test.go:TestForElementsRejectsANonCollection, :TestElementsOfStillCoercesAScalar, robustness_test.go:for_over_a_scalar ✅ Faithful (ruling: for requires a collection; elementsOf is unchanged for its other callers)
A non-terminating loop ends the execution rather than hanging it runtime/action_statements.go execLoop (a step per iteration), context.go incrementStep action_loop_step_budget.sysml, robustness_test.go:non_terminating_loop_exhausts_step_budget ✅ Faithful (reports ErrStepLimitExceeded, the same failure as any other runaway evaluation)
A legitimately long loop runs under a raised budget budget.go BudgetsFromEnv (SYSML_MAX_STEPS) resolved at the REPL/CLI and gRPC entry points budget_test.go:TestRaisedBudgetRunsLongerLoop ✅ Faithful (a 10 000-iteration loop that exhausts a 100 000-step budget completes under the default)
The budget bounds one run, not a session context.go beginRun/beginExecutorRun (the step counter is reset when a run begins; a nested run, and every call into a run a caller drives step by step, shares the outer one's budget) budget_test.go:TestStepBudgetIsPerRun, :TestStepBudgetHoldsAcrossExecutorDrivenRun, :TestStepBudgetIsPerRunForInstancesAndCalcs ✅ Faithful
A legitimately long action or simulation runs under raised sibling budgets budget.go Budgets (SYSML_MAX_ACTION_STEPS, SYSML_MAX_EVENTS, SYSML_MAX_DO_STEPS), read by action_executor.go and state_executor.go from the context budget_test.go:TestActionStepBudgetIsConfigurable, budget_test.go:TestStateBudgetsAreConfigurable ✅ Faithful (each bound counts its own unit and its error names the variable that raises it)
A member-attached then sequences the members either side of it (action a; then action b;) parser/succession.go (desugared at parse time to the *ast.SuccessionEdge the then a b; notation builds), lowered by lower/action_graph.go and lower/state_graph.go like any other edge conformance/action_member_then_order.sysml + trace golden (declaration order is the reverse of the execution order), conformance/state_member_then_order.sysml (the same for a state's completion transitions), parser/succession_test.go:TestMemberAttachedThenDesugars, parse/succession_member_then.golden ✅ Faithful (an end with no name to give is bound by position: SuccessionEdge.SourceMember/TargetMember refer to the member itself, so then send Show(x) to screen;, a then after an anonymous member and then loop action { … } all sequence what they are written beside. A then before a member the notation does not admit one in front of, such as an attribute or a definition, is a syntax error)
A succession end with no name is carried by identity, not by name ast/behavior.go SuccessionEdge.SourceMember/TargetMember, ControlFlowEdge.SourceMember/TargetMember; parser/succession.go bindPositionalSource/bodyBuilder.add; lower/action_graph.go (the member is the graph node the edge reaches) parser/succession_test.go:TestSuccessionBindsUnnamedEndsByPosition, :TestPositionalSuccessionEndIsTheMemberItself, conformance/action_standard_loop_until_then_done.sysml ⚠️ Approximate (the RDF mapping names an edge's ends by qualified name, so exporting a model whose succession has a positional end is reported as unsupported rather than written back — see docs/reference/rdf-mapping.md)
Named flow with explicit ends (flow f from a.out to b.in;, SysML.xtext FlowUsagePayloadFeatureSpecializationPart + FlowEndMember) parser/defusage.go parseFlowEnds/parseFlowTo (the name, from and feature-chain ends); ast/defusage.go FlowEnds; lower/action_graph.go lowerFlow/flowEnd (ObjectFlow, the flow's name included); runtime/action_executor.go applyDataFlows parse/behavior_flow_named_from.golden, parse/flow_payload_declaration.golden, negative_test.go:flow_from_without_to, :flow_named_from_no_source, conformance/action_flow_named_from.sysml, robustness_test.go:flow_end_naming_no_node, :flow_from_a_node_that_produced_nothing ✅ Faithful (both ends, feature chains included, name a node and its pin, and the value at the source's pin is what the target reads; flow of x from a to b names the pin at both ends. An end naming something that is not a node of the action, and a source pin the node left empty, are reported rather than dropped)
Accept with a trigger expression in an action body (accept when <cond>, accept at <instant>, accept after <duration>; SysML.xtext TriggerValuePart) parser/behavior.go parsePayloadParameter/parseTriggerExpression; lower/action_graph.go Accept.Trigger; runtime/action_executor.go triggerHolds parse/behavior_accept_trigger.golden, conformance/action_accept_when_trigger.sysml, negative_test.go:accept_when_no_condition, :accept_at_no_instant, robustness_test.go:action_accept_time_trigger, :action_accept_non_boolean_change_trigger ⚠️ Approximate (a change trigger is tested each step and suspends the token until it holds; an action body has no clock, so accept at/accept after there reports ErrNoClock naming the state machine that does wait on time, rather than firing)
Accept subsetting a declared event (action interrupt accept :> shutDown;, SysML.xtext PayloadParameterPayloadFeatureSpecializationPart) parser/behavior.go parsePayloadParameter; lower/action_graph.go subsettingTargetAccept.SubsetsEvent; runtime/action_executor.go (the subsetted event names the message the accept takes) parse/behavior_accept_subsets.golden, conformance/action_accept_subsets_event.sysml, negative_test.go:accept_subsets_no_event ✅ Faithful (a send shutDown() to interrupt is taken by the accept subsetting shutDown, as a typed accept takes a message of its type)
Send of an invoked signal or event (send Data(reading) via commPort;, then send fullyCharged() to self;) parser/behavior.go parseSendStatement; runtime/signal.go buildMessage/buildInvokedMessage/invokesCalc parse/behavior_send_via.golden, conformance/action_send_invocation_via_port.sysml, negative_test.go:send_via_no_port, :send_no_target ✅ Faithful (the invoked name types the message and its arguments are its payload — a single positional argument also as value, which a typed accept binds; an invocation of a calc is still evaluated as an expression)
Succession to a loop node, and a loop's until condition (then loop action { … } until battery >= 100;, SysML.xtext WhileLoopNode) parser/behavior.go parseLoopAction/parseWhileLoopAction; ast/behavior.go WhileLoopActionNode.Until; lower/action_graph.go (loop body and Until lowered); runtime/statements.go iteration parse/behavior_loop_until_succession.golden, conformance/action_standard_loop_until_then_done.sysml, negative_test.go:loop_until_no_condition, :loop_until_no_semicolon, passes/typecheck.go checkBehaviorMember (the until condition is checked Boolean) ✅ Faithful (loop { … } until c tests after the iteration; while c action { … } until u tests c before and u after)
A loop or branch body written as an action body parameter (loop action [<name>] { … }, for x in c action { … }, SysML.xtext ActionBodyParameter) is the body itself, named or not parser/behavior.go parseActionBodyParameter (marks ast.Usage.IsBodyParameter); lower/action_graph.go lowerStatement (lowered to the block the usage's scope owns); runtime/statements.go block lower/action_body_test.go:TestActionBodyParameterLowersToItsBlock, conformance/action_named_loop_body_parameter.sysml ✅ Faithful (a name only scopes the members it declares — loop action charging { … } until charging.done — so the body runs either way; an empty body parameter runs as an empty body)
then done; — a final node as a successor target parser/behavior.go parseFinalNode (reached by a succession); lower/action_graph.go Finals; runtime/action_executor.go stepFinalNode parse/behavior_loop_until_succession.golden, conformance/action_standard_loop_until_then_done.sysml, negative_test.go:then_done_no_semicolon ✅ Faithful (the token reaching it ends the flow, as for a declared done end;)
A flow ending at a node with no outgoing succession (action a; action b; first a then b;) lower/action_graph.go Edges (the node has no outgoing edge); runtime/action_executor.go retireToken, called from stepActionExecutionNode, stepNestedAction, stepStatementNode conformance/action_last_node_without_a_succession.sysml + trace golden, conformance/action_flow_ending_at_a_statement.sysml, action_executor_test.go:TestActionExecutor_NodeWithoutSuccessorsRetiresItsToken, robustness_test.go:action_whose_last_node_has_no_succession ✅ Faithful (Actions::Action gives every action a done snapshot it inherits, so a flow that runs out of successions ends there and the action completes with what its features hold — an explicit done end; is one way to write that end, not a requirement. A node reached by a fork retires its own branch; the action completes when the last token does)
A first end naming a node the body declares (first s1 then s2;, first s1; with then s1 s2;, SysML.xtext TargetSuccession after first) starts the flow at that node lower/action_graph.go resolveFirstNode (the named node becomes Initial and holds the succession leaving it) lower/action_first_node_test.go:TestToActionGraph_FirstNamesADeclaredNode, :TestToActionGraph_FirstNamesADeclaredNodeSplit, :TestToActionGraph_FirstDeclaresItsOwnInitialNode, conformance/action_first_names_a_declared_node.sysml and _split.sysml + trace goldens, parse/behavior_first_then_declared_node.golden, robustness_test.go:first_node_with_a_second_succession, :first_beside_an_initial_node, :first_naming_a_final_node ✅ Faithful (first marks which node the flow starts at; only a first end naming nothing declared — first start; — is an initial node of its own. A body states one start, so a second first end, or one beside an initial node of its own, is rejected; naming a final node is rejected because the flow cannot start where it ends)
A guard on a succession out of a node that is not a decision (first s1 if c then s2;, SysML.xtext GuardedSuccession) lower/action_graph.go (the guard is carried on the edge); runtime/action_executor.go enabledSuccessions (every guard out of the node is evaluated before a token leaves it) conformance/action_succession_guard_holds.sysml, action_succession_guard_fails.sysml, action_succession_guard_two_branches.sysml, action_succession_guard_fork_branch_pruned.sysml, action_succession_guard_not_boolean.sysml, action_succession_guard_two_hold.sysml, robustness_test.go succession_guard_failure_modes ✅ Faithful (a guard that does not hold prunes the succession, since TransitionPerformance::transitionLink is HappensBefore[0..1]; two consequences the notation leaves open are tool-defined and pinned in the goldens: a node whose every succession is pruned ends its flow, and two guards holding at once out of one node is reported rather than resolved — a fork prunes only the branches whose guard fails)
else branch of a decision in an action flow (if c then a; else b;, SysML.xtext DefaultTargetSuccession) parser/behavior.go (the else clause builds *ast.ControlFlowEdge{IsElse: true}); lower/action_graph.go (the else edge is the guardless alternative); runtime/action_executor.go stepDecisionNode parse/behavior_decision_else.golden, conformance/action_decision_else_branch.sysml, conformance/action_decision_guarded_branch.sysml, negative_test.go:decision_else_no_target ✅ Faithful (the else edge is taken when no guarded branch out of the decision holds)
Qualified succession at namespace level (first part1::action1 then requirement1;) parser/namespace.go (a succession is a namespace member), parser/succession.go parse/behavior_namespace_succession.golden, negative_test.go:namespace_succession_no_target ⚠️ Approximate (parsed and carried in the AST with both ends; a succession outside a behavior body has no token flow to lower into, so it is not executed)
A statement written directly among an action's own members is reported, not ignored lower/action_graph.go ToActionGraph first pass, statementKeyword robustness_test.go:statement_directly_in_an_action_body ✅ Faithful (a statement runs as part of an action node's body; written beside first/then it has no name a succession could reach, so the execution reports it instead of dropping it)
A body member that is not an executable statement is reported, not skipped lower/action_graph.go Unsupported; runtime/statements.go execute (lower.Unsupported) lower/action_body_test.go:TestActionBodyUnexecutableMemberIsLowered, lower/block_graph_test.go:TestBlockStatingItsOwnEdgeKeepsItsStatements, robustness_test.go:loop_body_of_unexecutable_statement, :block_flow_of_unexecutable_member ✅ Faithful (a member of a loop or branch body outside what a block's flow executes — a part declaration, an edge the block states itself — fails the execution instead of producing a wrong answer silently, including in a block that does state a flow)
A block has a token flow of its own: a nested action declaration or a perform in a loop or branch body is a node of it lower/block_graph.go blockNeedsFlow, lowerBlockFlow, nestedActionBlock (Block.Graph, ActionGraph.StatementRuns); runtime/statements.go runBlock, blockFlow, blockNode; runtime/action_statements.go effect (a perform invokes the action it names) action_block_flow_nested_action.sysml, action_block_flow_perform_in_loop.sysml, action_block_flow_if_branch.sysml, calc_block_flow_early_return.sysml, all + trace goldens; lower/block_graph_test.go:TestBlockDeclaringAnActionNodeIsLoweredToAFlow, :TestBlockFlowNestsTwoLevels, :TestNestedActionParametersAreLowered; robustness_test.go:non_terminating_loop_performing_an_action; calc_block_flow_assigns_output.sysml (a walker over lowered blocks reads Block.Steps(), so an output assigned in a block's flow counts); calc_block_flow_result_in_block.sysml (a result written among a block's own nodes returns from the behavior around it, as it does in a statement body) ⚠️ Approximate (the nodes of a block's flow succeed one another in declaration order — the order the block writes them is the only flow a block states — and each spends a step, so a non-terminating loop performing an action reports ErrStepLimitExceeded. A nested action declares parameters and a nested action of its own, to any depth; a perform reads the values in scope where it stands and its outputs come back to them, so one in a loop body runs once per iteration. A return reached inside such a node ends the enclosing behavior. A block cannot state successions, forks or joins between its own nodes: an edge written in a block leaves its members in statement form, reported when reached)
Send statement (message passing) lower/action_graph.go lowerBody (FeaturePath keeps the whole target path); runtime/signal.go buildMessage, post, postTo, resolveAddress, namedAddress/qualifiedAddress, featureAddress, addressOwner, portAddress/receiverAddress/objectAddress, PostMessage (deliveryOf), Message.reaches; runtime/invoke_action.go invokeAction (a performed action runs as the object performing its caller) action_send_accept.sysml, send_identity_same_named_ports.sysml, port_identity_own_port.sysml, send_identity_unroutable_target.sysml, lower/action_body_test.go:TestActionBodyLowering, lower/connection_test.go:TestLowerSendKeepsAddressedPath, signal_test.go:TestActionMessageReachesStateMachine, :TestAddressedSendStaysWithinTheSendingObject, :TestAddressedSendResolvesPortOfNamedObject, :TestAddressedSendDescendsToNestedPort, :TestAddressedSendToUnreachablePortIsTyped, send_identity_addressed_part.sysml, send_identity_performed_object.sysml, signal_test.go:TestDeliveryHoldsAConsumerToTheWholeDestination, :TestPerformedBehaviorRunsAsItsPerformer, :TestAddressedSendToQualifiedElementOfATwinObject, :TestInjectedMessageIsHeldToTheDestinationItNames, robustness_test.go:injected_message_names_a_receiver_no_accept_has ✅ Faithful (a message is typed by what was sent and delivered by object identity: a target is resolved through the instance graph to the object owning it and the port path within it, and the destination is built whole or refused with UnroutableSendError, so a consumer takes a message only by satisfying every part of it — a same-named port or receiver of another object, or a sibling of the sender, never sees it. A behavior an object performs, however deeply, presents that object's identity, so only a behavior no object performs at all has none. A qualified target takes its object from its qualifier, and a message injected from outside the model is held to the destination its fields name)
Accept action (message consumption suspends the action) action_executor.go stepNestedAction accept case (parks the token as Token.Wait), Step (StateWaiting), RunToCompletion, deadlockError; executor_common.go AcceptWait; runtime/signal.go TakeMessage action_accept_suspends_until_message.sysml + trace golden, action_accept_two_waiters.sysml + trace golden, action_send_accept.sysml, action_accept_message.sysml, signal_test.go:TestAcceptParksTokenUntilMessageArrives, :TestParkedAcceptTakesOnlyItsOwnMessage, robustness_test.go:accept_deadlock_never_satisfied, :accept_deadlock_reports_every_waiting_accept, :send_reaches_only_its_addressee, :accept_of_unsent_type, :send_via_unconnected_port ⚠️ Approximate (an accept with no message it can take suspends the action at that node and resumes when one arrives, from a parallel branch or from another executor sharing the context; a run whose every remaining token is parked reports ErrAcceptDeadlock rather than hanging. Suspension is bounded by the executor: a nested action invoked synchronously, and an action driven by RunToCompletion, cannot wait for a message posted after the call begins)
An accept node's payload is visible by simple name to the other nodes of the same action body, and a nearer declaration shadows it (KerML 8.2.3.5.3) resolve/accept_payload.go acceptPayload/acceptPayloadsIn, consulted by resolve/unqualified.go walkUnqualifiedHiding for the scope the accept node is declared in; runtime/action_executor.go binds the accepted value under the same name resolve/accept_payload_test.go:TestAcceptPayloadVisibleToSiblingNode, :TestAcceptPayloadTwoAcceptsInOneBody, :TestAcceptPayloadVisibleInNestedBody, :TestAcceptPayloadVisibleBeforeDeclaration, :TestAcceptPayloadShadowsOuterFeature, :TestAcceptPayloadUnresolvedStillReported, :TestAcceptPayloadDoesNotEscapeBody, conformance/accept_payload_nested_body.sysml, accept_payload_shadows_outer_feature.sysml, accept_payload_read_before_accept.sysml + trace golden, robustness_test.go:accept_payload_read_before_it_is_bound ⚠️ Approximate (the payload is a parameter of the accept node, which KerML scoping does not make a member of the enclosing body, so the body's shared feature space is modelled by contributing the payload to the scope the accept node is declared in: every node resolving through that scope reads it, a nearer declaration still wins, and the payload neither escapes the body nor is reachable as A::msg. It is not offered by LSP completion, which lists a scope's own members)
Send through a port (send x via p) routes over the connectors of the behavior and of the part performing it, to the ends that can receive the message: the direction of the port's flow features decides that, conjugated where a ~P types the end (SysML v2 §7.12.2, §7.15, §7.16). An end is joined as the whole path it was written with, so a nested p.q is itself and not a same-named port elsewhere. A send that reaches no receiving end is a typed error, not a message dropped lower/connection.go lowerConnections, FeaturePath (ends and the scope they resolve in); runtime/routing.go routableConnections, performerConnections, enclosingPart, receivingEnds, endReceives, portSymbol, UnroutableSendError; runtime/signal.go postVia, reaches; semantics/conjugation.go PortFeatures port_direction_conjugation.sysml, port_nested_port_path.sysml, port_interface_typed_connection.sysml, send_no_reachable_receiver.sysml, send_into_outbound_only_end.sysml, action_port_communication.sysml + trace golden, runtime/routing_test.go (conjugated end, outbound-only end, unjoined port, nested path, performing part's ports with and without an instance, part-to-behavior connector, interface-typed connection), signal_test.go:TestSendViaPortReachesConnectedAccept, robustness_test.go:send_via_unconnected_port ⚠️ Approximate (routing honors direction, conjugation and the performing part's ports; a port declaring no flow features constrains no direction and so receives in either direction, and an end whose path this run cannot resolve is treated as able to receive rather than reported here. The performer is the object performing the behavior, or the part the behavior is declared in when no object performs it — a part reached through a chain of enclosing parts contributes only its own connectors)
Accept through a port (accept msg : T via p) lower/action_graph.go acceptPort; runtime/action_executor.go stepNestedAction accept case action_port_communication.sysml, lower/connection_test.go:TestLowerAcceptRecordsViaPort, signal_test.go:TestPortRoutedMessageBypassesPortlessAccept, :TestAddressedMessageBypassesPortAccept ✅ Faithful (an accept on a port takes only messages routed to that port, and an accept on none takes only addressed messages)
An accept node is an action node (SysML.xtext ActionNode), so it stands wherever a statement does — a member of an action body, a statement of a loop or branch body, or the member a then sequences (then action engineStopped accept engineOff : EngineOff;) — and the accepting action's name stays distinct from the received payload's (engineStarted vs engineStart), both registered and resolvable parser/behavior.go parseActionMember, startsInlineSuccessionStatement, atAcceptNode, parseAcceptNode (declaration name, payload parameter, optional via port) parse/accept_action_statement.golden, runtime/testdata/conformance/accept_statement_via_port.sysml, parser/negative_test.go (accept_statement_no_payload, accept_statement_no_payload_type, accept_statement_via_no_port, then_accept_no_payload), robustness_test.go:accept_statement_deadlock_in_a_loop, lower/action_body_test.go:TestAcceptInALoopBodyIsLoweredAsUnsupported ⚠️ Approximate (parsed, resolved and executed as an action-body node; an accept written in a loop or branch body would have to suspend a flow that has no token to park, so it is lowered as Unsupported and reported when reached rather than passed over — suspending a block's flow is not implemented)
Object flow (pin-to-pin data) action_executor.go:673 applyDataFlows action_output.sysml ✅ Faithful
Succession edges lower/action_graph.go:ToActionGraph action_control_flow.sysml ✅ Faithful
Deadlock detection action_executor.go:72 Step action_executor_test.go:TestActionExecutor_Deadlock_JoinStarvation ✅ Faithful
Step budget enforcement context.go incrementStep; budget configured by SYSML_MAX_STEPS (budget.go BudgetsFromEnv) robustness_test.go:testStepBudgetExceeded, budget_test.go:TestRaisedBudgetRunsLongerLoop ✅ Faithful (the reported limit is the effective one, and names the variable that raises it)

State Machine (SysML v2 States — Systems Library/States.sysml, over KerML StatePerformances)

Semantic Rule Implementation Test Case Status
Initial state identification lower/state_graph.go:ToStateGraph; state_executor.go:686 initialize state_simple.sysml ✅ Faithful
A succession out of a state body's own entry subaction names the state it starts in (entry; then off;), the same as initial start; start then off; lower/state_graph.go collectTransitions SuccessionEdge case + isEntrySubaction lower/state_notation_test.go:TestToStateGraph_EntrySuccessionNamesInitialState, state_entry_succession_initial.sysml conformance, parser/testdata/parse/behavior_exhibit_state_body.golden ✅ Faithful
A transition out of a state body's own entry action names the state it starts in (entry action initial { } transition initial then off;), the entry action standing in for a start pseudostate (SysML v2 §7.19.3) ast/state_entry.go EntryActions/StateEntryActions/IsEntryAction; resolve/transition.go startAction; passes/state_transition.go machine.startActions; lower/state_graph.go startsAt resolve/transition_test.go, passes/state_transition_test.go:TestTransitionOutOfEntryActionIsLegal, lower/state_notation_test.go, state_entry_action_transition_initial.sysml conformance ✅ Faithful (an ordinary action named as an endpoint is still reported)
Final state termination state_executor.go:288 processNextEvent state_simple.sysml ✅ Faithful
State entry actions state_executor.go:749 enterState state_do_behavior.sysml ✅ Faithful
State exit actions state_executor.go:810 exitState state_transition_effect.sysml ✅ Faithful
An entry/do/exit action given by reference (entry warmUp;) is a performed action usage subsetting the referenced action (StateActionUsagePerformedActionUsagePerformActionUsageDeclaration) parser/behavior.go parseStateSubaction; parser/defusage.go parsePerformedActionReference parser/testdata/parse/state_subaction_reference.golden, parser/state_subaction_test.go, parser/negative_test.go:entry_reference_no_semicolon, resolve/state_subaction_test.go, runtime/state_behavior_test.go:TestStateSubactionByReferencePerformsAction ✅ Faithful
State do behavior runs while its state is active, one action per round state_executor.go startDoActivity, runDoRound state_do_behavior.sysml, state_do_activity_test.go ✅ Faithful
An entry, do or exit behavior written as an inline action body (entry action { … }, do action named { … }, exit action { … }) executes the statements it states, locals and loops among them, in any nesting of composite states; an empty body is a behavior that does nothing lower/state_behavior.go LowerBehaviors/lowerStateBehavior (the body is lowered to a Block, its locals in the block's own frame), lower/state_graph.go StateGraph.Behaviors; runtime/state_statements.go executeBehavior/stateStmtHost parser/testdata/parse/state_anonymous_action_body.golden, state_anonymous_action_body.sysml + trace golden (entry/do/exit ordering, nesting, empty bodies), robustness_test.go:testEmptyAnonymousActionBody, :testNonTerminatingAnonymousDoBody (ErrStepLimitExceeded) ✅ Faithful
An inline body is one action, so a do round runs it to its end: orthogonal regions interleave between rounds, not inside a body. The one-action-per-statement do { … } form is what interleaves statement by statement runtime/state_statements.go executeBehavior; state_executor.go runDoRound state_anonymous_do_atomic.sysml + trace golden (123456), against state_concurrent_do.sysml (124356) ✅ Faithful
A statement of an inline body may perform an action (entry action { assign c := c + 1; perform Bump; }), the performed action being lowered as an effect rather than an unsupported usage lower/action_graph.go lowerStatement (an action usage naming what it performs → Effect{EffectPerform}); runtime/state_statements.go stateStmtHost.effect state_anonymous_body_perform.sysml conformance ✅ Faithful
A behavior that both performs an action and states a body of its own is reported rather than one of the two being chosen silently lower/state_behavior.go lowerStateBehavior (Unsupported) robustness_test.go:testBehaviorPerformingAnActionAndStatingABody ⚠️ Approximate (the form is rejected at execution, not at parse: whether SysML gives it a meaning is unadjudicated)
Concurrently active states interleave their do behaviors, in region declaration order state_executor.go runDoRound, orderedActiveRegions state_concurrent_do.sysml + trace golden ✅ Faithful
Exiting a state abandons the rest of its do behavior state_executor.go exitState, stopDoActivity state_do_activity_test.go:TestDoBehaviorIsCancelledWhenItsStateIsExited ✅ Faithful
A state completes only once its do behavior has finished state_executor.go scheduleCompletionTransitions state_do_activity_test.go:TestCompletionWaitsForTheDoBehavior ✅ Faithful
Deferred events retained while a deferring state is active, delivered afterwards in arrival order parser/behavior.go parseDeferMember (defer <event>[, <event>]*;); lower/state_graph.go stateNodeFromUsage, collectDeferred; state_executor.go defersEvent, recallDeferredEvents parser/testdata/parse/state_defer.golden, parser/state_notation_test.go:TestDeferMemberParsing, lower/state_notation_test.go:TestToStateGraph_DeferNotation, state_deferred_event.sysml + state_undeferred_event.sysml conformance, state_deferred_test.go ✅ Faithful
Earliest transfer first (Occurrence::incomingTransferSort defaults to earlierFirstIncomingTransferSort): a recalled event is dispatched before the events that arrived while it was deferred, and a completion event before either executor_common.go eventHeap.Less, isCompletionEvent; state_executor.go recallDeferredEvents state_deferred_test.go:TestRecalledEventPrecedesLaterArrivals ✅ Faithful
A transition out of a composite state is enabled while any of its substates is active: matching walks outward from every active leaf through the lowered containment (StateGraph.ParentState), the innermost enabled transition wins for the same event, and a false guard does not consume it state_executor.go broadcastEvent, selectTransitions, losesToNestedTransition, nestedIn, activeLeaves, fireFrom, enabledTransition, acceptsSignal/acceptsSignalFrom (an enclosing state's accept takes a signal off the bus) conformance/state_composite_outer_transition.sysml, :state_composite_inner_priority.sysml, state_composite_transition_test.go:TestCompositeStateHandlesEventItsSubstateDoesNot, :TestFalseGuardInsideACompositeStateFallsOutward, :TestTransitionOutOfAnIntermediateCompositeStateKeepsItsOwnerActive, :TestOuterCompositeStateStillHandlesItsEventAfterASubstateMoved, robustness_test.go:signal_no_level_of_a_composite_state_accepts ✅ Faithful
Taking a transition out of a composite state exits the states being left innermost-first, then runs the effect, then enters the target; leaving a composite with orthogonal regions exits every active region first state_executor.go transitionToInto (exit to the least common ancestor), exitStates/exitedByAncestorRegion (a state whose region another state being left owns is exited by that state's recursive region teardown, so the chain walk does not exit it a second time), state_region_transition.go leaveRegion (which instead clears the region a state is active in, recording its history, before exiting that state, since a region's active state may be nested below it), moveBetweenRegions, exitRegionTo, leaveTopRegions conformance/state_composite_exit_order.sysml + trace golden (two levels of nesting), :state_composite_orthogonal_exit.sysml + trace golden, :state_composite_nested_regions_exit_once.sysml + trace golden (a region holding a composite with a region of its own: every level exits exactly once), robustness_test.go:exit_of_nested_regions_with_a_history_pseudostate ✅ Faithful
A transition out of a composite state is external even when its target is that state itself or one of its substates: the source composite is exited and re-entered around the effect, and its regions restart at their initial states state_executor.go transitionToInto and state_region_transition.go moveBetweenRegions (the exit boundary is the source's parent where the source encloses the target, so a composite self-transitioning inside an orthogonal region is exited too, while its sibling regions stay as they were), encloses, isComposite conformance/state_composite_self_transition.sysml + trace golden (exit substate, exit composite, effect, re-enter composite and its region), :state_composite_to_substate.sysml + trace golden, :state_composite_self_transition_in_region.sysml + trace golden, state_composite_transition_test.go:TestOneEventTakesACompositesTransitionOnce, robustness_test.go:composite_self_transition_with_no_substate_to_re_enter ⚠️ Approximate (a composite state stating no starting substate — no initial and no region — is re-entered with no substate active, since nothing names the substate to enter; a self-transition on a simple state does not exit and re-enter it)
An event reaches only the regions still active when it is dispatched state_executor.go broadcastEvent (each active leaf is re-checked before it is offered the event) state_deferred_test.go:TestExitedNestedRegionDoesNotReactToTheSameEvent ✅ Faithful
Run-to-completion bounds one dispatch: only the leaves active when the event was taken off the queue are offered it, each taking at most one transition, so a state the event entered does not react to it as well state_executor.go broadcastEvent, selectTransitions (the transitions are all selected against the configuration the event was dispatched for, then fired; leaves in sibling regions of one composite state select its transition once between them, and the dispatch ends once a final state completes the machine) state_composite_transition_test.go:TestOneEventTakesOneTransitionPerActiveLeaf, :TestOneEventTakesACompositesTransitionOnce ✅ Faithful
Concurrent regions react to one event in region declaration order, whatever depth their active state sits at, and a transition one region takes disables only the states it left state_executor.go broadcastEvent (candidates are ordered by the active leaves, which activeLeaves sorts by regionPath, the declaration index of every region between the machine and the leaf, rather than by depth), losesToNestedTransition conformance/state_composite_region_depth_order.sysml + trace golden (the deeper leaf's region is declared second and reacts second), :state_composite_region_deeper_first.sysml + trace golden (the mirror: it is declared first and reacts first), state_composite_transition_test.go:TestOneRegionsInnerTransitionLeavesAConcurrentRegionsOwnTransition ✅ Faithful
A time trigger on a composite state counts from entering that state and fires while a substate is active; a substate moving does not restart it, and leaving the state destroys it, so re-entering the state times the whole interval again state_executor.go scheduleTransitionEvents (the enclosing states of every active leaf are scheduled too), scheduleTimeTransitions, exitState (drops the state's timers and withdraws the time events they queued, through executor_common.go (*EventQueue).Withdraw), dispatchEvent (the expired timer is un-marked so a transition staying in its source re-arms it, and an event whose source was left is dropped) conformance/state_composite_outer_time_trigger.sysml, conformance/state_composite_region_time_trigger.sysml + trace goldens, :state_time_trigger_restarts_on_re_entry.sysml + trace golden (a state left before its timer expires fires only a full interval after it is re-entered, timed against a sibling region's clock), robustness_test.go:stale_composite_timer_in_a_region, state_composite_transition_test.go:TestTimedSelfTransitionFiresEveryPeriod (a timed self-loop fires once per period) ✅ Faithful (a composite inside an orthogonal region fires region-locally: dispatchEvent routes through fireFrom, so the sibling regions stay as they were)
A change condition on a composite state is watched while a substate is active; a poll resolves the transitions it enables exactly as the equivalent signal does — innermost wins, one transition per region, a shared transition once — and a condition whose guard blocks it consumes nothing state_change_trigger.go pollChangeEvents (selects through the shared selectCandidates and resolves conflicts with losesToNestedTransition, the same two steps broadcastEvent uses), observeChangeConditions, risenChangeTransition (the guard is evaluated before the transition is selected, so a blocked one leaves the condition watched by the states outward of it and by the other leaves), state_executor.go fireFrom state_composite_transition_test.go:TestChangeConditionOnACompositeStateFiresWhileASubstateIsActive, :TestGuardBlockedChangeConditionDoesNotSilenceTheOtherRegions, :TestChangeConditionTakesTheInnermostTransitionOnly, state_change_trigger_test.go:TestChangeTriggerConsumesTheRiseForALosingTransition ✅ Faithful (RunToCompletion polls, so a when transition on a composite state fires without an external caller; one rise is one occurrence, so a transition that lost the conflict waits for the next rise rather than firing on the next poll)
Deferral by an ancestor state and across orthogonal regions state_executor.go defersEvent state_deferred_test.go:TestCompositeStateDefersForItsSubstates, TestDeferralSpansOrthogonalRegions ✅ Faithful
Deferring a non-dispatchable trigger reports lower/state_graph.go collectDeferred robustness_test.go:defer_of_non_deferrable_trigger ✅ Faithful
A transition's source and target name vertices of the machine they are written in, resolved by the name-resolution tier so a misspelled endpoint reports with the other name diagnostics rather than when the machine is lowered resolve/transition.go (*Resolver).ResolveEndpoint (reported from resolve/document.go TransitionMember), consumed by lower/state_graph.go (*StateGraph).vertex via lower.Endpoints resolve/transition_test.go:TestResolveEndpointsThatNameVertices (sibling, nested, sibling region, entry/exit point, sourceless accept ... then), :TestResolveEndpointMisspelledIsReportedWithASuggestion, :TestResolveEndpointNotAVertexIsReported, :TestEndpointLookupForLoweringReportsNothing, state_transition_endpoint_qualified.sysml conformance, robustness_test.go:state_transition_endpoint_misspelled, :state_transition_endpoint_never_resolved ⚠️ Approximate (a qualified endpoint resolves like any other name; an unqualified one, and one whose owners name only some of the scopes between, fall back to the first vertex of the machine whose name path ends in it, in declaration order, which is what the notation's leniency allows and what two vertices of the same name in sibling regions are told apart by only when the endpoint qualifies them. An endpoint naming no vertex leaves its edge out of the graph rather than failing the lowering, so a machine whose model was never resolved — the REPL and the gRPC handlers build a fresh resolver — still runs; an endpoint that resolves but names no vertex of this machine is reported by the state transition check, see the dangling-transition rows)
Transition firing state_executor.go:535 fireTransition state_transition_effect.sysml ✅ Faithful
Transition guard evaluation state_executor.go:218 scheduleTransitionsForState state_choice_pseudostate.sysml ✅ Faithful
Transition effect actions, whether written as a statement (do assign x := 1) or as a performed action (do perform Bump) lower/state_behavior.go LowerBehaviors (the membership a performed action is contributed through is unwrapped and each behavior is lowered to statements); state_executor.go:535 fireTransition → state_statements.go executeBehavior state_transition_effect.sysml, state_transition_effect_perform.sysml conformance ✅ Faithful
AcceptEvent triggers (when signal) state_executor.go matchesEvent state_signal_discriminate.sysml ✅ Faithful
Signals sent from state behaviors reaching the machine state_statements.go stateStmtHost.send, state_executor.go deliverPendingSignal state_send_self_signal.sysml + trace golden, signal_test.go:TestSendOfNamedTypeReachesStateMachine ✅ Faithful
A signal in flight on the context bus is dispatched by a single step as well as by a run to completion, so the REPL debugger and RunToCompletion agree state_executor.go ProcessNextEvent, acceptableMessage, HasPendingSignal, HasPendingWork; repl/meta.go %advance repl/runtime_commands_test.go:TestAdvanceDeliversPendingPortSignal, state_transition_accept_via_port.sysml ✅ Faithful
CallEvent triggers (accept op(param) notation, operation and argument matching, arguments bound for guard/effect) parser/behavior.go parseTriggerEvent/parseCallEvent; symbols/bodyscopes.go newTriggerScope (parameters visible to the transition's own guard/effect); state_executor.go matchesEvent EventCall case, bindTriggerArguments, InvokeOperation parser/testdata/parse/state_call_trigger.golden, lower/trigger_test.go:TestTriggerClassification_CallTrigger, model/behavior_body_resolve_test.go call-trigger parameter cases, state_call_trigger{,_guard,_nested,_regions}.sysml conformance, signal_test.go:TestCallEventMatchesOperationName, :TestRejectedCallLeavesNoArgumentsBehind, robustness_test.go:call_of_unhandled_operation, :call_argument_of_wrong_type ✅ Faithful (a call trigger on an enclosing composite state sees the invocation while a substate is active)
Sourceless transitions (accept...then) lower/state_graph.go:487 collectTransitions Usage case, :302 resolve container accept_then_transition.sysml ✅ Faithful (nested form only; flat form errors intentionally)
ChangeEvent triggers (when expr) state_executor.go matchesEvent, RunToCompletion (polls after each micro-step and again at quiescence); state_change_trigger.go pollChangeEvents, SuspendReason state_executor_test.go:TestStateChangeEvent, state_change_trigger_test.go:TestChangeTriggerRunsWithoutAnExternalPoll, :TestChangeTriggerFiresOnRiseFromDoBehavior, :TestChangeTriggerDoesNotRefireUnchangedCondition, :TestChangeTriggerFalseConditionIsReported, conformance/state_change_trigger_autonomous.sysml, :state_change_trigger_rising_edge.sysml, :state_change_trigger_event_order.sysml + trace golden ⚠️ Approximate (driven by the run itself and fired on the condition rising; KerML has no clock, so re-testing once per micro-step is a tool-defined cadence — see the known limitation)
TimeEvent triggers (accept after <duration> relative, accept at <time> absolute) parser/behavior.go parseAcceptTransition; state_executor.go scheduleTransitionEvents, scheduleTimeTransitions, matchesEvent; state_time_trigger.go timeMagnitude (a duration carrying a unit is converted to the clock's SI::s) state_timed_triggers.sysml golden, state_timed_transitions.sysml conformance, state_executor_test.go:TestStateExecutor_AbsoluteTimeEvent, state_time_trigger_test.go:TestTimeTriggerUnitIsConverted, :TestTimeTriggerSubSecondUnit, :TestTimeTriggerAbsoluteInstantWithUnit, :TestTimeTriggerRejectsNonTimeDimension, conformance/state_time_quantity_seconds.sysml, :state_time_quantity_instant.sysml, :state_time_quantity_unit_ordering.sysml + trace golden, robustness_test.go:non_numeric_time_trigger ✅ Faithful
Signal discrimination state_executor.go:401 matchesEvent signal name state_signal_discriminate.sysml ✅ Faithful
Unmatched signal dropped state_executor.go matchesEvent state_signal_unmatched.sysml ✅ Faithful (an injected event no transition matches is dropped; a message on the bus no active transition accepts is left in flight for another consumer, signal_test.go:TestStateMachineLeavesForeignSignalPending)
Hierarchical substates state_executor.go:131 getParentChain, :147 getLCA state_orthogonal_regions.sysml ✅ Faithful
Orthogonal regions state_executor.go broadcastEvent, state_region_transition.go fireTransitionInRegion; region order from lower.StateGraph.TopRegions and CompositeStates state_orthogonal_regions.sysml, region_pseudostate_test.go:TestRegionPseudostateExitOrderIsDeterministic ✅ Faithful
Choice pseudostates state_region_transition.go pseudostateBranch (guards in declaration order) state_choice_pseudostate.sysml, state_region_choice.sysml ✅ Faithful
A succession (a then b;, initial s; s then a;) names its endpoints the way a transition does, so it reaches a nested, region-local or qualified vertex, and a pseudostate as well as a state lower/state_graph.go collectTransitions (UsageSuccession, SuccessionEdge and InitialNode cases) → (*StateGraph).endpointVertex/endpointState, over the same lower.Endpoints a transition's endpoints resolve through lower/state_graph_nested_test.go:TestSuccessionReachesAPseudostate, :TestSuccessionQualifiedTargetNamesTheVertexItQualifies, :TestToStateGraph_EntrySuccessionNamesInitialState ✅ Faithful (previously matched the endpoint's last name segment against a flat state list, which reached no pseudostate and could bind a same-named vertex of another state)
Two regions may declare same-named pseudostates, and each is a vertex of its own lower/state_graph.go StateGraph.Pseudostates (declaration-ordered slice, not keyed by name), addPseudostate lower/state_graph_nested_test.go:TestSameNamedPseudostatesInSiblingRegionsAreBothCollected ✅ Faithful
Junction pseudostates state_region_transition.go pseudostateBranch state_junction_pseudostate.sysml ✅ Faithful (evaluated when entered, like a choice, rather than statically before the incoming transition)
Fork pseudostates (bypass targeted regions' initial states) state_executor.go:706 fireForkTransition, :1028 enterStateInto state_fork_join.sysml golden, state_fork_join_pseudostate.trace.golden, fork_join_test.go:TestForkBypassesTargetedRegionInitials ✅ Faithful
Join pseudostates state_executor.go:782 fireJoinTransition, :827 joinSources (declaration order) pseudostate_test.go:TestJoinWaitsForEveryBranch, fork_join_test.go:TestForkJoinVisitOrderIsDeterministic ✅ Faithful
Entry/exit point pseudostates parser/behavior.go parseStateMember (entry point <name>; / exit point <name>;, point matched contextually); state_region_transition.go pseudostateTarget (routed like a junction) parser/testdata/parse/state_history_entry_exit.golden, parser/state_notation_test.go:TestHistoryAndPointPseudostateParsing, :TestPointIsNotReserved, state_entry_exit_points.sysml conformance, pseudostate_test.go:TestEntryAndExitPointPseudostates, region_pseudostate_test.go:TestRegionPseudostateExitRecordsHistory ✅ Faithful
History pseudostates (shallow and deep). A region is recorded per region, in the state it was left in, so a region left by a transition that started inside its own composite state's region is restored to that composite state and its inner configuration rather than to the region's initial state; a region left with no active state at all has nothing to restore parser/behavior.go parseStateMember (history <name>;, shallow history <name>;, deep history <name>;); state_executor.go fireHistoryTransition, :deepestRecorded, exitState (records the configuration left), :recordRegionHistory, :forgetRegionHistory, state_region_transition.go leaveRegion, exitRegionTo, lower/state_graph.go PseudostateOwner parser/testdata/parse/state_history_entry_exit.golden, parser/state_notation_test.go:TestHistoryAndPointPseudostateParsing, lower/state_notation_test.go:TestToStateGraph_HistoryAndPointNotation, state_shallow_history.sysml, state_deep_history.sysml, state_history_revisit.sysml + trace golden, state_deep_history_region_composite.sysml, history_test.go:TestShallowHistoryRestoresLastSubstate, :TestDeepHistoryRestoresInnermostSubstate, :TestHistoryRestoresOrthogonalRegions, :TestDeepHistoryRestoresBelowRegion, :TestDeepHistoryRestoresARegionLeftFromInsideItsCompositeState, :TestHistoryTakesDefaultTransitionWhenUnvisited, robustness_test.go:history_outside_composite_state, :history_without_record_or_default ✅ Faithful
Composite state with regions entered by a plain transition state_executor.go transitionToInto (keeps the region configuration entering it just built) history_test.go:TestHistoryRestoresOrthogonalRegions ✅ Faithful
Leaving a composite state exits only its own regions state_executor.go exitState (scoped to CompositeStates[state]) history_test.go:TestExitingNestedRegionsKeepsSiblingRegions ✅ Faithful
A transition between two regions of one composite state exits its source only: KerML StatePerformances::StateTransitionPerformance orders private succession [*] guard then [1] transitionLinkSource.exit, so the composite state is neither exited nor re-entered, the source's region is left without an active state, and the regions holding neither endpoint keep theirs. A target nested inside the target region's own composite state moves that inner region, exiting the state it was running. A region whose target is nested inside a composite state it is not running records that composite state as its active one. A source active in a region nested deeper than its target's region leaves its region set up to the level the two share, exiting the composite state holding its own region. The level is found by walking outward through the region declaring the region owner's nearest region-declared ancestor, so a region owned by a plain substate is not missed. A target outside the composite state still exits it and its regions — each state on the way out exactly once, the region a state is active in being cleared before that state is exited, since a region's active state may be nested below it — and a nested non-orthogonal transition still exits up to the endpoints' least common ancestor state_region_transition.go fireTransitionInRegion, concurrentRegionsFor, enclosingRegion, siblingRegionContaining, moveBetweenRegions, exitRegionTo, leaveRegion, isBelowOrEqual; state_executor.go getLCA (nested and outward transitions only) state_transition_cross_region.sysml + trace golden, state_transition_cross_region_third_region.sysml, state_transition_cross_region_nested_target.sysml, state_transition_cross_region_inactive_wrapper.sysml, state_transition_cross_region_deep_source.sysml, state_transition_cross_region_substate_owner.sysml, state_transition_leave_composite_substate_region.sysml, state_transition_sibling_region.sysml, state_region_cross_pseudostate.sysml + trace golden, cross_region_transition_test.go:TestCrossRegionTransitionExitsSourceOnly, :TestCrossRegionTransitionIntoNestedTargetExitsTheAbandonedState, :TestCrossRegionTransitionIntoInactiveCompositeRecordsTheEnteredState, :TestCrossRegionTransitionFromDeeperRegionExitsUpToTheSharedLevel, :TestCrossRegionTransitionFromARegionOwnedByASubstate, :TestNestedTransitionExitsUpToTheLCA, :TestTransitionOutOfCompositeStateExitsEveryRegion, :TestTransitionOutOfCompositeStateExitsNestedStatesOnce, region_pseudostate_test.go:TestRegionPseudostateIntoSiblingRegionExitsSourceOnly, robustness_test.go:state_cross_region_transitions_ping_pong ✅ Faithful
Nested substates of a composite state declared textually lower/state_graph.go stateNodeFromUsage (carries substates and nested pseudostates into the graph) lower/state_graph_nested_test.go:TestToStateGraph_NestedPseudostateOwner ✅ Faithful
Choice/junction/entry/exit reached from inside an orthogonal region state_region_transition.go fireTransitionInRegion, moveBetweenRegions, leaveRegion, pseudostateTarget state_region_choice.sysml, state_region_exit_pseudostate.sysml, state_region_cross_pseudostate.sysml + their trace goldens, region_pseudostate_test.go, robustness_test.go:region_pseudostate_without_satisfied_guard, :region_pseudostate_cycle ✅ Faithful (a branch staying in the source region moves only that region; one into a sibling region of the same composite state exits the source only; one leaving the composite state exits the region set in declaration order, recording history on the way out)
Pseudostate chains (a pseudostate routing into another) state_region_transition.go pseudostateTarget (cycle detected) region_pseudostate_test.go:TestRegionLocalJunctionChainIsFollowed, robustness_test.go:region_pseudostate_cycle ✅ Faithful
Nested action invocation in entry/do/exit/effect state_executor.go:1075 executeAction, invoke_action.go invokeAction state_behavior_test.go:TestStateDoExitAndTransitionEffectPerformAction ✅ Faithful
Run-to-completion semantics state_executor.go:288 processNextEvent state_executor_test.go:TestStateRunToCompletion ✅ Faithful
Event queue management state_executor.go:1127 EventQueue state_executor_test.go ✅ Faithful
Deterministic dispatch order executor_common.go eventHeap.Less (time, then arrival), state_executor.go orderedActiveRegions (region declaration order) state_call_trigger_regions.sysml ✅ Faithful
A transition names exactly one source and one target vertex, both of the machine it is written in (TransitionUsage::source: ActionUsage[1..1], ::target: ActionUsage[1..1] in the SysML v2 metamodel bundled as stdlib/Systems Library/SysML.sysml; KerML TransitionPerformances::TransitionPerformance takes one transitionLinkSource: Performance[1] and one transitionLink: HappensBefore[0..1]) passes/state_transition.go StateTransitionPass.Run(*transitionChecker).checkEndpoint, over the vertices lower/vertices.go VertexDecls collects with the lowering's own collectVertices; lower/state_graph.go (*StateGraph).vertex keeps the typed construction error as the backstop passes/state_transition_test.go:TestTransitionTargetInSiblingRegionIsLegal, :TestTransitionTargetInSiblingRegionKeywordIsLegal, :TestTransitionTargetInUnrelatedMachineIsIllegal, :TestSuccessionTargetInUnrelatedMachineIsIllegal, :TestTransitionToEntryPointIsLegal, :TestTransitionTargetResolvingToNonVertexIsIllegal, :TestSourcelessAcceptTransitionIsLegal, :TestTransitionToFirstMarkerIsIllegal, :TestTransitionToFinalStateIsLegal, :TestStateUsageMachineIsChecked, conformance/state_transition_sibling_region.sysml, robustness_test.go:state_transition_endpoint_in_another_machine, :state_transition_endpoint_naming_a_first_marker ✅ Faithful (a vertex of a sibling orthogonal region, an entry/exit point of a composite state and the sourceless accept … then form are legal; a vertex of another machine, a first/then marker named as a target and an endpoint resolving to a non-vertex are reported — the last of them by endpoint resolution, which owns it. A marker named as a source is left to lowering: see the row below)
A routing pseudostate has a transition out of it, so a transition reaching it does not terminate nowhere (SysML v2 has no pseudostate notation or semantics; choice/junction/fork/join are the documented OpenSysML extension of docs/reference/grammar/README.md, whose reference semantics is UML 2.5.1 §15.7.18) passes/state_transition.go (*transitionChecker).checkMachine, routingPseudostate passes/state_transition_test.go:TestJunctionChainTerminatingNowhereIsIllegal, :TestJunctionWithOutgoingTransitionIsLegal, :TestJunctionLeftBySuccessionIsLegal, robustness_test.go:state_junction_without_an_outgoing_transition ✅ Faithful (choice, junction, fork and join only; a history, entry or exit point is excluded, since what it resumes or delegates to need not be written as a transition out of it. The chain reaching such a pseudostate is acyclic, so state_region_transition.go cycle detection does not find it)
A first/then marker named as a transition's source not checked; lower/state_graph.go (*StateGraph).vertex reports it as a construction error model/transition_first_test.go:TestTransitionFirstStart (pins the marker source as clean at check time) ⚠️ Approximate (whether first start then off; beside transition t1 first start … then off; declares a second transition out of one initial pseudostate — illegal under UML 2.5.1 §15.7.18 — or names the same one twice is a reading of SysML v2 §7.19.3 this PR does not settle; the existing test pins it clean, so only lowering reports it)
Completion transitions state_executor.go:218 scheduleTransitionsForState nil trigger state_simple.sysml ✅ Faithful
A transition written in the standard first/accept/then form, with the trigger on a line of its own, and with a name of its own (SysML.xtext TransitionUsage) parser/behavior.go parseTransitionMember/parseTransitionTail; lower/state_graph.go Transition.Name; runtime/state_executor.go transitionDescription (the name is what a diagnostic about the transition reports) parse/behavior_exhibit_state_body.golden, parse/state_transition_variants.golden, conformance/state_transition_accept_via_port.sysml, negative_test.go:transition_trigger_no_target, :transition_two_triggers, :transition_two_targets, :transition_do_without_action ✅ Faithful
accept … via <port> on a transition (SysML.xtext AcceptParameterPart) parser/behavior.go parseTransitionTail; lower/state_graph.go Transition.Via; runtime/state_executor.go matchesEvent/acceptsSignal/deliverPendingSignal conformance/state_transition_accept_via_port.sysml ✅ Faithful (a transition naming a port fires only for an occurrence routed to that port; one naming none takes an addressed message, as before)
A transition's accept payload is bound for its guard and effect (accept w : Warning do assign level := w) lower/state_graph.go classifyTrigger (AcceptEvent.Payload); runtime/state_executor.go bindAcceptPayload conformance/state_transition_accept_payload.sysml ✅ Faithful (bound while the transition is taken and unbound if it does not fire, as a call trigger's arguments are)
A transition triggered by a subsetted event (accept :> shutDown) lower/state_graph.go classifyTrigger (AcceptEvent.Subsets); runtime/state_executor.go triggerMatches/acceptsSignal parse/behavior_accept_subsets.golden, conformance/action_accept_subsets_event.sysml (the same matching rule in an action body) ✅ Faithful
A transition's do effect written as a statement is terminated by the transition's own ; (SysML.xtext TransitionUsage ends with ActionBody, while EffectBehaviorUsage carries no ;), in the standard first … then spelling and in OpenSysML's compact <source> to <target> spelling alike parser/behavior.go expectStatementEnd/atTransitionEffectStatement/atEffectEnd; parser/defusage.go parseUsage/parseReferenceMemberUsage parse/state_transition_effect_statement.golden, conformance/state_transition_effect_assign.sysml, conformance/state_transition_effect_assign_first_then.sysml (with their .trace.golden), negative_test.go:transition_effect_perform_two_semicolons, :transition_effect_assign_two_semicolons, :transition_effect_no_semicolon, :body_assignment_no_semicolon ✅ Faithful (a second ; is an error, as ActionBody takes one terminator; a statement outside a transition effect still needs its own ;)
Bodied exhibit state (exhibit state spacecraftModes { … }, SysML.xtext ExhibitStateUsage) parser/behavior.go (the exhibited state's body is parsed as a state body); ast/dump.go (the exhibit state keyword is recorded) parse/behavior_exhibit_state_body.golden, parse/classifier_behaviors.golden, negative_test.go:exhibit_state_unclosed_body ✅ Faithful (the body parses, resolves and lowers as a state machine, and an object of the type runs it — see the Classifier Behaviors map)

Classifier Behaviors (KerML §8.4.4.3 Behaviors / performances; SysML v2 §7.16 exhibit/perform)

Semantic Rule Implementation Test Case Status
A behavior a type exhibits or performs is bound to every object of the type: materializing the object gives it an execution of its own, and two objects of one type run independently lower/classifier_behavior.go ClassifierBehaviorOf/BehaviorMembers; runtime/classifier_behavior.go startClassifierBehaviors/attachClassifierBehavior; runtime/instance.go instantiateOwnedBy conformance/object_exhibits_state_machine.sysml, conformance/two_objects_exhibit_independently.sysml (with their .trace.golden), runtime/classifier_behavior_test.go, repl/classifier_behavior_test.go:TestStateDebugsTheMachineAnObjectExhibits ✅ Faithful
A body of the behavior reads and writes the performing object's own feature values, and a message addressed to one object reaches that object's machine and not a sibling's runtime/classifier_behavior.go assignPerformerFeature; runtime/state_statements.go, runtime/action_statements.go (assignment through the performer); runtime/signal.go addressOwner (the owner chain an address is resolved against) conformance/object_machine_writes_own_features.sysml, conformance/object_addressed_send_one_sibling.sysml ✅ Faithful
Startup and quiescence are tool-defined (KerML gives no clock): feature values and constant defaults come first, then the behavior is initialized and run until no event is due at the current time, no do action is runnable and no message is in flight, bounded by the event and do-step budgets runtime/classifier_behavior.go startClassifierBehaviors/drainObjectBehaviors; runtime/state_executor.go RunToQuiescence robustness_test.go:object_exhibited_machine_never_settles, :object_exhibited_machine_without_an_initial_state, conformance/object_exhibits_state_machine.trace.golden ⚠️ Approximate (tool-defined: an exhausted budget is ErrBehaviorBudget, and a machine waiting on a timer is quiescent — advancing time drives it)
A performed action parked at an accept is quiescent rather than deadlocked, and a message a sibling object sends later wakes it runtime/action_executor.go RunToQuiescence/HasPendingSignal; runtime/classifier_behavior.go hasPendingWork runtime/classifier_behavior_test.go:TestPerformedActionAwaitingAMessageIsWokenByASibling, :TestPerformedActionWithoutAFlowStillMaterializes ⚠️ Approximate (tool-defined: a standalone %action run still reports ErrAcceptDeadlock, where nothing can post the awaited message)
An object's parameter space is its own: an action's out parameter answers the caller even where the performing object declares a feature of that name runtime/action_statements.go assignOuter; runtime/action_executor.go declaresParameter runtime/classifier_behavior_test.go:TestOperationOutputNamedLikeAFeatureAnswersTheCaller ✅ Faithful
A failed materialization leaves no behavior of the object attached or queued, and an edited model drops an object whose behavior body changed rather than resuming it on the values the old body wrote runtime/classifier_behavior.go startClassifierBehaviors/forgetBehaviorsFrom; runtime/adopt.go writeBoundBehaviors runtime/classifier_behavior_test.go:TestFailedMaterializationLeavesNoBehaviorBehind, repl/classifier_behavior_test.go:TestRewritingTheExhibitedMachineDropsTheObject, :TestObjectMachineSurvivesAnUnrelatedDeclaration ⚠️ Approximate (tool-defined: the spec describes one fixed model, so what a live execution does when the model is edited is a REPL policy)
A second materialization of one name is a second object, with its own identity and its own behaviors; occurrenceOf remains the reuse path for a named occurrence runtime/instance.go instantiateOwnedBy; repl/query.go instantiateNamed (which object the name now denotes) robustness_test.go:second_instantiation_of_one_type, repl/classifier_behavior_test.go:TestSecondInstantiateIsAnotherObject ⚠️ Approximate (tool-defined; the spec leaves object creation semantics open)
Invoking an operation of an object's type runs it with that object as performer, binding named arguments as the call machinery binds them runtime/invoke_operation.go InvokeOperation; repl/meta.go %invoke runtime/classifier_behavior_test.go:TestInvokeOperationPerformedByTheObject, :TestInvokeOperationFailureModes, robustness_test.go:operation_invoked_with_unbound_parameters, repl/classifier_behavior_test.go:TestInvokeRunsAnOperationOnTheObject, :TestInvokeReportsItsFailureModes ⚠️ Approximate (actions only, with named arguments: an operation given as a calc or constraint, and positional arguments, return ErrUnsupportedClassifierBehavior — see Known Limitations)

Expression Evaluation

Semantic Rule Implementation Test Case Status
Binary operators (+, -, , /, %, *, <, >, ==, ===) eval.go evalOperator → evalArithmetic/evalComparison/evalEquality/evalIdentity calc_simple_add.sysml, calc_modulo_operator.sysml, calc_identity_operators.sysml ✅ Faithful
Boolean operators (and, or, xor, implies), short-circuiting where they can eval.go evalLogical constraint_literal.sysml, calc_boolean_operators.sysml ✅ Faithful
Unary operators (-, +, not) eval.go evalUnary calc_unary_operators.sysml ✅ Faithful
Conditional (if c ? a else b) and null coalescing (??), both lazy eval.go evalConditional/evalNullCoalesce calc_conditional_branch.sysml, calc_null_coalesce.sysml ✅ Faithful
Literal values (Integer, Real, Boolean, String) eval.go:109 evalLiteral* calc_simple_add.sysml ✅ Faithful
Feature reference resolution eval.go:141 evalFeatureReference constraint_literal.sysml ✅ Faithful
Qualified name resolution (A::B::C) eval.go:53 Eval + resolve/qualified.go calc_qualified_names.sysml ✅ Faithful
Type coercion (Integer→Real) eval.go:344 toReal calc_type_coercion.sysml ✅ Faithful
Exponentiation (**, ^) — Integer operands with a non-negative exponent give an Integer (IntegerFunctions::'**'), any other numeric pair a Real (RealFunctions::'**') semantics/eval.go Pow, shared by the folder's evalArithmetic and runtime/eval.go evalArithmetic calc_library_functions.sysml, exponentiation_test.go ✅ Faithful
An enumeration literal is a value of its enumeration (SysML v2 §7.6.4, §8.3.7 EnumerationUsage): a literal declaring no value evaluates to itself, and identity is the declaration it names — Color::red == Color::red is true, Color::red == Color::green false, and a literal of another enumeration false, since the two are unrelated values (the mismatch is a type-tier diagnostic, not a runtime one). A set keys a literal on that declaration, so the same literal twice is one element runtime/value.go ValEnumLiteral/Value.LiteralText, eval.go EvalContext.enumLiteralValue/valueEqual, value_equality.go valueKeyFunc, semantics/enumeration.go EnumerationOwning/LiteralsOf enum_literal_default_slot.sysml, eval_test.go:TestEval_EnumerationLiteralIsAValue, TestEval_EnumerationLiteralEquality, TestEval_EnumerationLiteralInASet, robustness_test.go:enumeration_name_that_is_not_a_literal ✅ Faithful
A literal of an enumeration specializing a scalar type (enum def GradePoints :> Real { A = 4.0; }) is a value of that type, so it evaluates to the value it declares and computes as one runtime/eval.go EvalContext.enumLiteralValue enum_literal_scalar_valued.sysml, eval_test.go:TestEval_EnumerationLiteralWithAValue ✅ Faithful
A literal is an occurrence of its enumeration, so the features it declares are readable (Level::high.n) and every read of that literal answers about one object runtime/eval.go chainMemberValue (ValEnumLiteral arm), Context.enumLiteralObject enum_literal_own_attributes.sysml, eval_test.go:TestEval_EnumerationLiteralOwnAttributes, TestEval_EnumerationLiteralReadTwiceIsOneObject, robustness_test.go:chain_through_a_literal_without_that_attribute ✅ Faithful
An enum-typed feature's default materializes as a feature value, and a literal renders as the enumeration writing it qualifies it (c = Color::red) in %features, in a trace and in a diagnostic repl/meta.go formatFeatureValue, runtime/trace.go FormatTraceValue, runtime/describe.go describeOperand enum_literal_default_slot.sysml, repl/meta_test.go:TestFeatureValuesEnumerationLiteral, describe_test.go:TestDescribeOperandEnumerationLiteral ✅ Faithful
A literal crossing the API boundary keeps its identity: it travels as Value.enum_literal carrying the declaration FQN, the enumeration's FQN and the qualified rendering, and an incoming literal is resolved against the model it names rather than reconstructed, so a literal no declaration of that model matches is an error and never a null grpc/convert.go enumLiteralToProto/enumLiteralFromProto, api/proto/sysml.proto EnumLiteral; Python opensysml/enumeration.py EnumLiteral, values.py, connection.py _python_to_value grpc/convert_enum_test.go:TestEnumLiteralToProto, TestEnumLiteralRoundTrip, TestEnumLiteralRoundTripInSequence, TestEnumLiteralUnresolvedIsAnError, TestInstantiate_EnumTypedFeatureValueCarriesLiteral; python/tests/test_enumeration.py, test_wire_compat.py:test_enum_literal_is_an_added_value_arm ✅ Faithful (advertised as the enum_values capability; a literal of an enumeration specializing a scalar crosses as that scalar, as it evaluates to one)
A quantity crosses the API boundary in both directions: Value.quantity carries the magnitude with the kind it was written in (Integer or Real), the unit as written and the reduced unit term, so a quantity read from the service can be sent back as an input and evaluates against the unit it names — commensurability is decided over the reduction, so a unit named without one is refused client-side rather than compared by bare magnitude opensysml/values.py Quantity.to_pb, Unit.to_pb/Unit.reduced, connection.py _python_to_value; service side grpc/convert.go ProtoToQuantity, ProtoToValueIn python/tests/test_quantity.py: test_a_quantity_encodes_as_the_message_the_service_decodes, test_an_unreduced_unit_is_refused_before_it_is_sent, and against a live service TestQuantityAgainstTheService::test_a_quantity_sent_as_a_calc_argument_round_trips, ::test_a_quantity_input_binds_into_an_action, ::test_a_sent_quantity_is_commensurable_with_the_models_own_units ✅ Faithful
An unqualified name resolves as a written reference does — the enclosing scope chain, inherited members, imports, then the global index — and the declaration it finds is evaluated in its own declaring scope, so the imports in force where a value was written answer the names that value uses runtime/eval.go evalFeatureReference (scope arm) via resolve/unqualified.go Resolver.LookupName, EvalContext.evalIn action_body_package_member.sysml, action_body_declarer_scope.sysml, body_scope_test.go:TestBodyScopeImportSpellings, robustness_test.go:action_body_unresolved_feature ✅ Faithful

Scope of an expression in a behavior body

An expression written inside an action or state machine body resolves its names in the scope it was declared in, and the values live above that scope: a frame binding (the action's feature space, a block-local declaration, a call trigger's argument) shadows a same-named declaration the scope reaches, and the innermost frame wins. The scope travels with the IR — internal/core/lower records it on the graph, on each lowered statement and block, on each state and on each transition when it lowers them — so the executors read a scope rather than re-deriving one from symbol.Decl (AGENTS.md §4).

Semantic Rule Implementation Test Case Status
An attribute default in a behavior body is evaluated in that body's scope, so a unit an import brought in resolves (attribute h : LengthValue = 500.0 [m];) lower/action_graph.go lowerAttributes + ActionGraph.Scope; runtime/action_executor.go initializeAttributes; lower/state_graph.go + runtime/state_executor.go initializeAttributes action_body_quantity_descent.sysml, state_body_quantity_scope.sysml, parser/testdata/parse/action_body_quantity_statements.golden ✅ Faithful
A statement in a nested action node resolves in that node's scope; a statement in a loop body or an if branch in the block's own scope lower/action_graph.go lowerStatement, lowerBlock, childScope (lower/scope.go); runtime/action_statements.go evalIn action_body_quantity_descent.sysml, action_body_shadows_enclosing_scope.sysml ✅ Faithful
A decision guard and an inline expression resolve in the action's own scope, its feature values shadowing it runtime/action_executor.go stepDecisionNode, stepActionExecutionNode action_body_shadows_enclosing_scope.sysml, action_control_flow.sysml ✅ Faithful
A transition's guard, effect, time-event duration and change-event condition resolve in the scope the transition was written in; a call trigger's parameters and an accept trigger's payload are visible to its guard and effect and nowhere else lower/state_graph.go Transition.Scope/BodyScope (via symbols.TriggerScope); symbols/bodyscopes.go newTriggerScope, payloadParameterDefiner; resolve/document.go; runtime/state_executor.go passesGuard, scheduleTransitionsForState, pollChangeEvents, executeAction; runtime/state_region_transition.go runEffect state_body_quantity_scope.sysml, state_call_trigger_guard.sysml, state_call_trigger_regions.sysml, state_transition_accept_payload.sysml, model/behavior_body_resolve_test.go accept-payload case ✅ Faithful
A state's entry, do and exit behaviors resolve in that state's scope, nested states and orthogonal regions included lower/state_graph.go StateGraph.StateScopes, collectStates, collectRegionStates; runtime/state_executor.go stateScope state_body_quantity_scope.sysml, state_concurrent_do.sysml, state_region_cross_pseudostate.sysml ✅ Faithful
A body member of an inherited or performed behavior is evaluated in the declarer's scope, not in the scope performing it runtime/invoke_action.go invokeAction, runtime/context.go chainMembers + EvalContext.evalIn action_body_declarer_scope.sysml ✅ Faithful
A frame binding shadows the enclosing scope, and an inner block shadows an outer one runtime/action_statements.go evalIn (frames pushed over the scope), runtime/eval.go evalFeatureReference (frames consulted first) action_body_shadows_enclosing_scope.sysml, robustness_test.go:loop_body_declaration_does_not_leak ✅ Faithful
A name or unit the declaring scope does not reach is reported, not evaluated as a bare magnitude runtime/eval.go (ErrUnresolvedReference), semantics/units.go (ErrNotAUnit) robustness_test.go:action_body_unresolved_unit, :action_body_unresolved_feature, :state_body_unresolved_unit ✅ Faithful
A %constraint/%requirement verdict is evaluated in the element's declaring scope, with or without an instance repl/meta.go declaringScope repl/runtime_commands_test.go:TestConstraintResolvesUnitsOfItsOwnPackage ✅ Faithful
An expression typed at the prompt is evaluated in the namespace the session is working in — the namespace a member typed there would be written in — so that namespace's members and the units its imports bring in resolve unqualified (1.0 [m/s], mass * 2) repl/meta.go promptScope, doEval; repl/lookup.go lookupSymbol (a name the session declares nowhere is resolved there) repl/runtime_commands_test.go:TestEvalResolvesImportedUnitsUnqualified, TestPromptScopeIsTheLastNamespaceDeclared ⚠️ Approximate (the notation says nothing about a prompt, so "the namespace the session works in" is the last one it declared: declaring a second package moves it, and the first package's members and imports are then reached by qualified name only — a context named explicitly, %eval in <qualified-name> : <expression>, pins it instead)
Arguments of a %calc command are a list of expressions parsed by the expression parser, so an argument containing spaces — a quantity, a parenthesized expression, a nested call — is one argument; successive arguments are separated by a comma or by whitespace, and the invocation form Fall(a, b) is accepted repl/meta.go doCalc, parseExprList, splitCalcArgs; parser/parser.go Parser.Offset repl/runtime_commands_test.go:TestCalcParsesExpressionArguments, TestCalcSeparatesSignedArguments ⚠️ Approximate (whitespace separates two arguments only where the first is a complete expression and the second is one — 5 -3 is two arguments, 5 - 3 one — since whitespace is no terminator in the notation; named arguments, Fall(v0 = …), are reported as unsupported rather than bound: the notation writes them in an invocation's parentheses, a production the prompt's argument list is not)
A quantity's magnitude is rendered in a result table by the same convention as a bare Real, the stored value keeping its full precision repl/meta.go formatValue, formatConst; runtime/quantity.go Quantity.TextWithMagnitude repl/runtime_commands_test.go:TestFormatValueQuantityUsesRealFormatting ✅ Faithful
A qualified name given to a command is the name the notation writes (KerML §7.2.5 unrestricted name): a segment needing quotes — a space, a keyword as a name, punctuation — is quoted, anywhere in the chain, and is one argument rather than being split on its space; the quoting is notation, so the name is normalized to the one the index records before lookup, and a name reported back is spelled so it can be typed into the next command repl/meta.go parseArgs, indexOutsideName; repl/qualname.go plainName, notationName; repl/lookup.go lookupSymbol repl/qualname_test.go:TestQuotedNamesAcceptedByNameTakingCommands, :TestQuotedNameNamesTheSameObjectThroughout, :TestParseArgsKeepsQuotedNamesWhole, :TestQuotedNameFailuresAreReportedNotPanics, :TestNotationNameRoundTrips ✅ Faithful
The same spelling rule holds over gRPC: a symbol ID is resolved whether it is written quoted ('My Pkg'::Car) or in the unquoted spelling the index records (My Pkg::Car), on every RPC taking a symbol ID grpc/service.go lookupNamed, unquotedName grpc/spelling_test.go:TestInstantiateAcceptsBothSpellings, :TestSymbolIDSpellingHoldsOnEveryRPC, :TestSymbolIDSpellingDoesNotInventSymbols ✅ Faithful
An expression is evaluated in a context named explicitly — %eval in <qualified-name> : <expression> — either the named element's namespace or, where an object was materialized under that name, that object's feature values; :: inside the name is not the separator repl/meta.go doEvalLine, evalIn, splitPinnedContext, contextSeparator; repl/lookup.go objectNamed; runtime/eval.go Context.EvalWithScopeOn (one run, so the step budget bounds a pinned evaluation as it bounds an unpinned one) repl/evalin_test.go:TestEvalInNamespaceReadsItsMembers, :TestEvalInInstanceReadsItsFeatureValues, :TestEvalQualifiedNameIsNotAContextSeparator, :TestEvalInFailuresAreTypedNotPanics, :TestEvalWithoutContextIsUnchanged, :TestEvalInInstanceIsBoundedByTheStepBudget; runtime/robustness_test.go:eval_on_an_instance_spends_the_step_budget ✅ Faithful
A session that loses an object or a debugger session says so and why: a reload carries an object over where the reloaded declaration still resolves to the shape it was materialized against, and a reset, which can prove nothing, reports the loss so the next %instances/%features/%step explains it repl/session.go clear, resetLoss; repl/carryover.go carryOverObjects; repl/notices.go lossOnReset, lossAtSubmission, lossOnBudgets; runtime/context.go Adopt repl/reset_test.go:TestClearReportsWhatItTook, :TestCommandsAfterClearExplainTheLoss, :TestClearEndsDebuggerWithAReason, :TestReloadKeepsObjectsItStillResolves, :TestLoadThatChangesDeclarationsReportsTheLoss ✅ Faithful
A submission whose text the parser cannot close (an unterminated body, quoted name or block comment, typed or loaded) is reported and kept out of the text the session analyses, so the next submission is parsed against what was there before it and drops nothing repl/enclosure.go closesItsOwnText, maskedText; repl/session.go acceptFrom, joined, openDiagnostics, diagnostics repl/openinput_test.go:TestLoadedOpenFileDoesNotPoisonTheNextSubmission, :TestOpenTypedSubmissionKeepsTheBuffer, :TestOpenSubmissionKeepsReportingTheRestOfTheBuffer, :TestOpenSubmissionKeepsWarningSeverity, :TestOpenSubmissionEchoesItsOwnLine, :TestRetypingANamespaceDoesNotMergeAMaskedSubmission, :TestMaskedSubmissionIsNotReportedAsBlockingTheChecks, :TestOpenSubmissionSurfacesStayTyped, repl/session_test.go:TestUnparseableRedeclarationDropsNothing ✅ Faithful (the buffer keeps the text for %save, so nothing typed is lost; a submission that does not close is not merged with the one after it, and its own findings keep the severity and code the parser gave them while the rest of the buffer is still analysed and reported)
Load-time diagnostics are reported the way interactive ones are, against the file and its own line numbering, and the non-interactive path's status reflects them repl/run.go LoadFile, LoadFileSummary, Diagnostics, HasErrors; repl/render.go renderSyntax repl/openinput_test.go:TestLoadReportsSyntaxDiagnostics, :TestLoadFileSummaryReportsSyntaxDiagnostics, :TestLoadReportsUnresolvedReference, :TestReloadingAFixedFileClearsItsSyntaxError ✅ Faithful (a syntax error in a loaded file is an error for HasErrors, which is what sysml -validate/%features in a script exits on; the exit status itself is cmd/sysml's)
An expression whose subject is reached through a declaration is evaluated on the object in effect for it, so a nested redefinition is honored by %eval, %constraint and %requirement alike repl/lookup.go subjectFor, carrierInstances, nestedObjects, featureChainSymbol repl/subject_test.go:TestEvalThroughDeclarationHonorsNestedRedefinition, :TestEvalThroughDeclarationWithoutObjectUsesDeclaredValue, :TestEvalThroughDeclarationWithTwoNestedCarriersIsAmbiguous, :TestCheckHonorsNestedRedefinitionThroughDeclaration ✅ Faithful (one shared subject seam: the runtime graph is walked to the object carrying the declaration, two carriers are an AmbiguousSubjectError, and no object at all still reads the declared value)
Two loaded files that open one package are two declarations of that name, and the load says so repl/merge.go reopenedNamespaces; repl/notices.go dropReport.notice repl/reopen_test.go:TestLoadingTwoFilesThatOpenOnePackageSaysSo, :TestLoadingDistinctPackagesSaysNothingAboutReopening, :TestReloadingOneFileSaysNothingAboutReopening, :TestRetypingAPackageStillMergesInteractively ⚠️ Approximate (maintainer decision, recorded here: a loaded namespace keeps its file's identity so re-loading that file replaces only its own contribution, which merging the two openings would make impossible without one file's edit deleting the other's members. Both openings' members are declared and reachable qualified; an unqualified reference across the two does not resolve, and the note says to qualify it. Re-typing a package at the prompt still folds into the one in the session)
%view <name> reports a view's exposed elements, its nested views and its conformance to the viewpoints it satisfies repl/view.go Session.View, doView, viewElementLine, conformanceLines, concernEvaluator (the runtime requirement engine, through runtime.Context.CheckSatisfactionOn); over semantics/expose.go Model.ExposedElements, Model.NestedViews and semantics/conformance.go Model.ViewConformance repl/view_test.go:TestViewListsWhatItExposes, :TestViewListsNestedViews, :TestViewExposingNothingIsNoError, :TestViewOfANonViewIsTyped, :TestViewOfAnUnknownNameReports, :TestViewIsInHelpAndCompletion, :TestViewReportsViewpointConformance, :TestViewReportsAViolatedConcernPerElement, :TestViewWithoutASatisfyReportsNoConformance, :TestViewReportsASatisfyThatIsNoViewpoint, :TestViewConformanceOutputIsDeterministic, :TestViewCreatesNoObjectOfItsOwn, :TestViewLeavesNoAmbiguityForALaterCheck, :TestViewEvaluatesTheSessionObject, :TestViewSharesTheObjectOfAQuotedName ✅ Faithful (a view exposing nothing says so; a non-view is semantics.ErrNotAView; a nested view is asked for its own exposed set; the report changes no model and is deterministic in ordering. A concern is evaluated against the object the session holds for an exposed element, and otherwise against one materialized in a runtime of the report's own, so a report registers no object of its own)
%print [name] writes the session's model back as notation at the prompt — the whole buffer, or one element and its body — through the writer a .sysml save writes with, so comments and text survive and the print can be submitted again to rebuild the same model repl/print.go doPrint, printSession, printElement, declarationSpan; export/convert.go SysMLElement, ConvertTolerantformat.Source; repl/lookup.go lookupSymbol (the quoted/qualified spellings) repl/print_test.go:TestPrintWholeSession, :TestPrintElement, :TestPrintQuotedAndQualifiedNames, :TestPrintKeepsComments, :TestPrintRoundTripsThroughSubmit, :TestPrintEmptySession, :TestPrintUnresolvableName, :TestPrintSymbolWithoutNotation, :TestPrintSaysNothingAboutRDF, :TestPrintLeavesInstancesAndBufferUntouched, :TestPrintLeavesActionDebuggerRunning, :TestPrintLeavesStateDebuggerRunning, :TestPrintCompletion, :TestPrintOfUnparsableSessionWarns ✅ Faithful (a read: nothing is materialized, the buffer is unchanged and a debugging session keeps running. An empty session, a name nothing declares and a symbol this session holds no source of each answer in one line; notation only, so no RDF notice follows a print)
A name the session cannot find is offered the qualified names it is known under, nearest scope first and bounded repl/qualsuggest.go qualifiedSuggestions, nestedInNonNamespace, writableName; repl/discover.go notFoundError, suggestSymbol repl/qualsuggest_test.go:TestQualifiedSuggestionsPreferTheSession, :TestQualifiedSuggestionsPreferPackageMembers, :TestQualifiedSuggestionsAreCapped, :TestUnresolvedMessageOffersRankedNames, :TestQualifiedSuggestionsSkipUnwritableNames ✅ Faithful (what the session declares outranks the library, a package's member outranks a name nested inside another element — a library function's parameter is no suggestion for a type — and at most suggest.Limit are offered)
A satisfaction verdict names the assertion in the notation's own spelling, quoting each inner name that needs quotes repl/satisfy.go satisfyText; repl/qualname.go notationName repl/satisfy_test.go:TestSatisfyQuotesInnerNames ⚠️ Approximate (the verdict line and the Verdict.Subject a caller reads are quoted; the condition text a runtime.ViolationError carries is rendered in runtime/condition.go, which this slice does not own, so a quoted name inside the condition of a failed assertion is still printed unquoted)

KerML Function Library (KerML §9.3 Function Library)

The library declares these functions abstractly — a signature and no body — so the runtime supplies the implementation. Dispatch is by the declaration's qualified name, and a declaration that carries a body is evaluated from that body — a result expression or a body assigning an output it declares — so a model's own calc sqrt is never hijacked. An unqualified call that resolves to no declaration dispatches by local name, which is what makes sysml -e "sqrt(2.0)" evaluable in a model that imports no part of the library.

Arguments follow the vendored signatures: a Real parameter accepts an Integer (ScalarValues declares Integer :> Rational :> Real), an Integer parameter rejects a Real rather than truncating it, and a Natural parameter rejects a negative value. A result that is not a finite value of the declared type — the square root of a negative, an inverse sine outside [-1.0, 1.0], a floor beyond the Integer range — is reported at evaluation rather than returned as a NaN, an infinity or a wrapped integer.

Beyond the scalars, a vector is the sequence of its elements — which is what VectorValues declares a NumericalVectorValue to be, elements with a dimension equal to their number — and a Complex is the two-element sequence (re, im) that ComplexFunctions::rect constructs, a Real being a Complex with a zero imaginary part (ScalarValues declares Real :> Complex). The runtime has no vector and no complex value kind of its own, so an operation whose argument is a collection of vectors or of Complex values cannot tell it from one flat vector or one Complex and is reported by name rather than answered (below).

A library feature — a named constant a library declares and gives no evaluable value — takes its value from a seam of its own: libraryFeatures maps the feature's qualified name to a Go thunk, consulted only for a symbol a library document declared, so ordinary name resolution decides and a model's own pi keeps its own value. The value is recomputed per read rather than cached, which keeps a vector-valued feature from sharing one sequence between readers and keeps the value independent of whether the library index cache was warm (a warm cache restores library symbols without their AST).

Semantic Rule Implementation Test Case Status
RealFunctions: sqrt, abs, floor, round, max, min runtime/library_functions.go TestLibraryFunctionValues ✅ Faithful
RationalFunctions/NumericalFunctions: abs, max, min (kind-preserving), isZero, isUnit runtime/library_functions.go TestLibraryFunctionValues ✅ Faithful
IntegerFunctions: abs, max, min; NaturalFunctions: max, min runtime/library_functions.go TestLibraryFunctionValues ✅ Faithful
TrigFunctions: sin, cos, tan, cot, arcsin, arccos, arctan runtime/library_functions.go TestLibraryFunctionValues ✅ Faithful
Domain, arity and argument-type failures reported at evaluation runtime/library_functions.go bindAndApply TestLibraryFunctionErrors ✅ Faithful
A declaration with a body is evaluated from that body runtime/library_functions.go libraryFunctionFor TestLibraryFunctionDoesNotHijackADeclaredBody ✅ Faithful
Named argument binds to the parameter the signature declares (sin(theta = 0.0)); a name no parameter carries is reported (ErrUnknownParameter) rather than absorbed by an omitted [0..1] parameter runtime/library_functions.go bindAndApply, checkNamedArguments TestLibraryFunctionNamedArguments, TestVectorAndComplexNamedArguments, TestVectorAndComplexUnknownNamedArgument ✅ Faithful
TrigFunctions::deg/rad, whose library bodies (theta * 180 / pi, theta * pi / 180) had no evaluable pi runtime/library_functions.go (degreesFromRadians/radiansFromDegrees, over libraryPi) TestTrigDegreesAndRadians; conformance calc_library_trig_degrees (deg(rad(180.0))), calc_library_trig_degrees.trace.golden ✅ Faithful
VectorFunctions: VectorOf/CartesianVectorOf/CartesianThreeVectorOf, isZeroVector, '+', '-' (binary and the one-argument negation the signature's [0..1] second parameter allows), inner, norm, angle, scalarVectorMult/'*', vectorScalarMult, vectorScalarDiv, each with the cartesian* specialization the library declares of it runtime/library_functions.go registerVectorFunctions TestVectorFunctionValues, TestVectorFunctionScalarValues, TestVectorAndComplexNamedArguments, TestVectorAndComplexFunctionErrors; conformance calc_library_vector_functions, calc_library_vector_norm (+ golden traces) ⚠️ Approximate: a vector is the sequence of its elements, so a mismatched dimension, a non-numeric element, an element or inner product outside the range of its kind, Real or Integer (elementArith, intArith, checkedNumeric) and a zero vector where an angle or a direction is asked for are typed errors, while a collection of vectors has no representation (below)
ComplexFunctions: rect, polar, re, im, isZero, isUnit, abs, arg, '+', '-' (both arities), '*', '/', '**'/'^', '==' runtime/library_functions.go registerComplexFunctions TestComplexFunctionValues, TestComplexFunctionScalarValues, TestLibraryFunctionOptionalOperand, TestLibraryFunctionEmptyOptionalOperand (an empty collection written for a [0..1] operand is the same no value as null, argumentOmitted); conformance calc_library_complex_functions (i * i is -1.0) ⚠️ Approximate: a Complex is the (re, im) pair, so a Real binds to a Complex parameter, division by zero is reported, and a collection of Complex values has no representation (below)
A library feature with a library-supplied value: TrigFunctions::pi (which the library fixes by an invariant, not a value), ComplexFunctions::i (rect(0.0, 1.0)), VectorFunctions::cartesian3DZeroVector runtime/library_functions.go libraryFeatures, registerLibraryFeature, Context.libraryFeatureValue, libraryFeatureByName TestLibraryFeatureValue, TestLibraryFeatureValueFromACachedSymbol, TestLibraryFeatureValueLeavesAModelsOwnFeatureAlone, TestLibraryFeatureValueUnrepresentable, TestLibraryFeatureByName, TestLibraryFeatureImaginaryUnit ✅ Faithful
A library feature is read as a name, qualified (TrigFunctions::pi) or imported (i under import ComplexFunctions::*), and computes like any other value (2 * TrigFunctions::pi); a registered feature with no computable value reports itself (ErrUnevaluableLibraryFunction) rather than answering nothing runtime/eval.go evalName (both the resolved-scope and the qualified-member paths, consulting Context.libraryFeatureValue) TestLibraryFeatureNameReadFromACachedSymbol; conformance calc_library_feature_pi, calc_library_feature_imaginary_unit, calc_library_feature_zero_vector, calc_library_feature_unevaluable ✅ Faithful
The seam is consulted after the ordinary lookups — frame binding, calc output, evaluated feature, instance feature value, enumeration literal — and answers only for a library-declared symbol, so a model feature shadowing a library name keeps its own value and name resolution decides which is read runtime/eval.go evalName (seam after Lookup/lookupOutput/selfFeatureValue, before the declaration's body and the cannot-evaluate failure) conformance calc_library_feature_shadowed_by_model; TestLibraryFeatureValueLeavesAModelsOwnFeatureAlone ✅ Faithful
A library declaration is answered by its built-in even where the vendored declaration carries a body, since a warm library index cache restores library symbols without their AST and the result must not depend on the cache runtime/library_functions.go libraryFunctionFor, Context.libraryDeclared TestLibraryFunctionAnswersALibraryDeclarationWithABody, TestLibraryFunctionDoesNotHijackADeclaredBody (a model's own declaration keeps its body) ✅ Faithful
StringFunctions: '+', Length, Substring, '<', '>', '<=', '>=', '==', ToString — the whole vendored package. Length counts characters (one per Unicode code point, so Length("héllo") is 5 over 6 bytes); Substring takes 1-based inclusive character positions and reports a position outside 1..Length(x) naming the character it indexed (ErrIndexOutOfRange, one identity for a sequence index and a string position, worded for both), an upper below lower selecting no character, as SequenceFunctions::subsequence answers for such a range; '==' declares String[0..1] operands, so two omitted operands are equal and an omitted one is not equal to a string runtime/library_functions.go registerStringFunctions, stringLength, stringSubstring, stringOrdering, stringEquals, stringToString, concatStrings, compareStrings TestStringFunctionValues, TestStringFunctionErrors, TestStringFunctionNamedArguments, TestVendoredFunctionsAreAllDispatchable (gating StringFunctions); conformance string_functions, string_empty, string_substring_out_of_range (+ golden traces) ✅ Faithful
The operators StringFunctions declares evaluate over two String operands: '+' concatenates, and '<', '>', '<=', '>=' order strings by character (UTF-8 orders bytes as it orders code points, so a byte comparison is code-point order). An operand of another type is reported, naming the operator and both operand types, and neither operand is coerced runtime/eval.go evalArithmetic (ast.OpAdd over two ValString), evalComparison (OperandTypeError) TestStringOperators, TestStringOperatorErrors, robustness_test.go:testStringOperandOfTheWrongKind; conformance string_operators, string_comparison, string_compared_with_a_number (+ golden traces) ✅ Faithful
StringFunctions::'==' specializes DataFunctions::'==', which is equality over any two values, so the == operator answers false for a String and an Integer rather than reporting; the explicit call StringFunctions::'=='(s, 3) reports the non-String argument, as every String-declared signature does. Two strings are equal by their characters, which is also what a collection membership test asks runtime/eval.go evalEqualityruntime/value_equality.go valueEqual, valueKey (ValString); runtime/library_functions.go stringEquals TestStringOperators (s == three is false, ("a", "b")->includes("b")), TestStringFunctionErrors ✅ Faithful
A declaration this runtime has no representation for the values of reports itself by name (ErrUnevaluableLibraryFunction), never a wrong result runtime/library_functions.go registerUnevaluable TestUnevaluableLibraryFunctionsNameThemselves, TestVendoredFunctionsAreAllDispatchable (every vendored declaration of these packages either computes or names itself); conformance calc_library_unevaluable_function ✅ Faithful

Found, not fixed — numeric library declarations that remain unevaluable. Each reports itself by name rather than answering:

Not implemented Why
MatrixFunctions The vendored Kernel Function Library declares no such package — only docs/ mention it — so there is nothing to dispatch. Not implemented rather than invented.
VectorFunctions::sum/sum0, ComplexFunctions::sum/product An aggregation over a collection of vectors or of Complex values: a sequence of them flattens, so the grouping the aggregation sums over is already lost in the argument. Needs a vector and a complex value kind in runtime/value.go.
VectorFunctions::cartesianZeroVector Declared as the 1-, 2- and 3-dimensional zero vectors as one feature of three vectors, which a flat sequence cannot hold. cartesian3DZeroVector has a value.
ComplexFunctions::ToString/ToComplex No string notation for a Complex value is defined; inventing a rendering would make ToComplex(ToString(x)) a value nothing else in the library agrees on.
Reading a library feature as a bare name at the REPL/CLI surface (%eval TrigFunctions::pi, sysml <model> -eval 'ComplexFunctions::i') A read inside a model or an expression consults the seam (above), but repl/meta.go resolves a lone name to a symbol and reads its usage.Value without the seam, so the bare form still reports has no value to evaluate, and cartesianZeroVector shows that generic message rather than its typed unevaluable reason. The hook belongs next to the enumeration-literal case in repl/meta.go (owned elsewhere).
Library functions in the checker's own name resolution An unqualified call to a library function the model does not import evaluates, but the unresolved-reference diagnostic still reports the name; importing RealFunctions::* clears it.

Sequence Indexing and Collection Operations (KerML §9.3 SequenceFunctions, CollectionFunctions, ControlFunctions)

A KerML sequence is not a value of its own kind: every value is a sequence — of one element where it is a scalar, of none where it is null — which is how the library's own isEmpty is seq == null and how 1->size() is 1. The runtime takes that view of a value in runtime/collections.go elementsOf, so the operations agree with the library's definitions for a scalar and for null as well as for a sequence or a set.

The index is 1-based, verified against the vendored declaration rather than assumed: SequenceFunctions::'#' declares in index: Positive[1] and SequenceFunctions::head is defined as seq#(1), last as seq#(size(seq)) and subsequence as (startIndex..endIndex)->collect {in i; seq#(i)}. An index of 0 is therefore not a position, and is reported rather than read as the first element.

The library declares each operation with a body — size recursively as if isEmpty(seq)? 0 else size(tail(seq)) + 1 — but that body is the specification of the operation, not the way to compute it, so a name denoting the library declaration dispatches to the implementation while a model's own declaration of that name is still evaluated from its own body.

The three notations a model can write an operation in — the collect/select notation (xs.{in x; …}, xs.?{in x; …}), the receiver form (xs->collect {…}, xs->size()) and the plain call (size(xs), SequenceFunctions::size(xs)) — all reach one implementation per operation, so they cannot drift apart.

Semantic Rule Implementation Test Case Status
seq#(i) is the i-th element counting from 1 (SequenceFunctions::'#', in index: Positive[1]), and an index of 0, a negative index or one past the end is a typed error rather than an empty or zero value runtime/collections.go evalSequenceIndex, elementAt, indexOf; runtime/quantity.go evalIndexExpr (non-bracket arm) runtime/collections_test.go TestSequenceIndexing, TestSequenceIndexingErrors; conformance calc_sequence_index, calc_sequence_index_out_of_range, calc_sequence_index_zero, calc_sequence_index_non_integer; robustness_test.go:sequence_index_names_no_position ✅ Faithful
The index and the quantity expression share one AST node and are told apart by the notation that produced them (ast.IndexExpr.Bracket), so 5 [m] is a quantity and xs#(1) is an element parser/expr.go (Hash and LBracket arms), runtime/quantity.go evalIndexExpr parser/testdata/parse/quantity_expression.golden, parse/collection_operations.golden; runtime/collections_test.go TestSequenceIndexKeepsQuantityForm; conformance calc_sequence_index_and_quantity_form; parser/negative_test.go (index_no_paren, index_bracket_empty) ✅ Faithful
An index that is not one whole number (a Real, a Boolean, a string, a collection) is a typed error, statically where it is written as a literal and at evaluation otherwise passes/typecheck_expr.go inferIndex (the index conforms to Integer, a literal 0 and a literal past a written sequence's length; a whole number a model counts with is a position or not depending on its value, so an Integer-typed index is checked at evaluation rather than reported for not being declared Natural); runtime/collections.go indexOf passes/typecheck_index_test.go TestIndexNonIntegerIndexReported, TestIndexZeroReported, TestIndexPastWrittenSequenceReported, TestIndexTypedIntegerNotReported, TestIndexByLoopVariableNotReported; runtime/collections_test.go TestSequenceIndexingErrors ✅ Faithful
collect answers the mapper's result for each element, in order, with the parameter the body itself declares bound to the element and the scope the body was written in still visible runtime/collections.go builtinControlCollect, applyBody; runtime/eval.go evalCollectExpr, evalCollectionNotation runtime/collections_test.go TestCollectionResults; conformance calc_collect_over_sequence, calc_collect_names_outer_variable, calc_nested_collection_operations ✅ Faithful
select/reject/selectOne/forAll/exists require the Boolean[1] result their expr parameter declares, and a selector answering anything else is a typed error rather than a dropped element runtime/collections.go filter, quantify, applyPredicate runtime/collections_test.go TestCollectionResults, TestCollectionOperationErrors; conformance calc_select_over_sequence, calc_select_predicate_not_boolean; robustness_test.go:select_predicate_is_not_a_condition ✅ Faithful
A body called with a number of arguments it declares no parameters for is a typed error (ErrBodyArity), never a call with a parameter left unbound runtime/collections.go bodyOf runtime/collections_test.go TestCollectionOperationErrors; conformance calc_collection_body_wrong_arity; robustness_test.go:collection_body_of_the_wrong_arity; parser/negative_test.go (body_param_no_name) ✅ Faithful
SequenceFunctions: #, size, isEmpty, notEmpty, includes, includesOnly, excludes, equals, same, union, intersection, including, includingAt, excluding, subsequence, excludingAt, head, tail, last — each computing what the vendored body specifies, except includingAt (below), equals by value and same by identity runtime/collections.go, registered in runtime/builtins.go runtime/collections_test.go TestCollectionResults, TestCollectionScalarResults; conformance calc_collection_aggregators ✅ Faithful, except the out-of-range endpoints of subsequence/excludingAt (see below)
includingAt inserts the values before the 1-based index, shifting the tail right, so the result is longer than the input by the values inserted; index == size + 1 appends, and any other index outside 1..size + 1 is a typed error (ErrIndexOutOfRange) runtime/collections.go builtinSequenceIncludingAt, registered in runtime/builtins.go runtime/collections_test.go TestCollectionResults, TestCollectionOperationErrors, robustness_test.go:testNumericLibraryCallThatHasNoValue; conformance calc_sequence_including_at, calc_sequence_including_at_appends, calc_sequence_including_at_out_of_range ⚠️ Approximate: the vendored body drops the element at index instead of shifting it right, which would leave addAt removing and the library with no insertion. Insertion is implemented on the maintainer's ruling and the vendored body is recorded as an OMG source bug (omg-issues.md)
An endpoint of subsequence or excludingAt that is outside the sequence is a typed error (ErrIndexOutOfRange), while an empty range inside it is the empty sequence, which is how tail is subsequence(seq, 2) of a one-element sequence runtime/collections.go builtinSequenceSubsequence, builtinSequenceExcludingAt runtime/collections_test.go TestCollectionOperationErrors, TestCollectionResults ⚠️ Approximate: the vendored bodies compose #, which returns Anything[0..1], so they answer the sequence unchanged for (1,2,3)->excludingAt(4) and truncate subsequence(1, 4). A position the sequence does not have is reported here rather than answered as if the model had asked for one it has
CollectionFunctions: size, isEmpty, notEmpty, contains, containsAll, head, tail, last, # over a collection's elements, a set included runtime/collections.go, runtime/builtins.go runtime/collections_test.go TestCollectionScalarResults, TestCollectionOperationsOverSets ✅ Faithful
An operation over an empty collection answers the empty collection and never calls its body, since there is no element to call it with runtime/collections.go elementsOf (an empty collection yields no elements) conformance calc_collection_ops_over_empty; runtime/collections_test.go TestCollectionResults ✅ Faithful
ControlFunctions: collect, select, selectOne, reject, reduce, forAll, exists, allTrue, anyTrue, minimize, maximize runtime/collections.go, runtime/builtins.go runtime/collections_test.go TestCollectionResults, TestCollectionScalarResults, TestCollectionOperationErrors ✅ Faithful
NumericalFunctions::sum/product and the specializations that fix the identity of an empty aggregation (sum0(collection, 0), product1(collection, 1)), keeping the elements' kind: Integers sum to an Integer, a Real anywhere makes the result a Real, and an overflowing sum or product is reported rather than wrapped runtime/collections.go aggregate, foldNumeric runtime/collections_test.go TestCollectionScalarResults; conformance calc_empty_collection_aggregation ✅ Faithful
A collection of quantities aggregates to a quantity, in the unit of its first element and converting the rest, as the binary operator does; mixing a bare number with a measured value reports incommensurable units runtime/collections.go aggregate/aggregateQuantities, quantity.go addQuantities/scaleQuantities conformance cubesat_mass_rollup; runtime/collections_test.go:TestAggregateQuantities ⚠️ Approximate (an empty collection has no unit to answer in, so it aggregates to the numeric identity 0/1)
The unqualified, qualified and receiver (->) forms of an operation are one implementation, so (1,2,3)->size(), size((1,2,3)) and SequenceFunctions::size((1,2,3)) cannot disagree; a name the model itself declares still resolves to that declaration runtime/builtins.go builtinsByLocalName, Context.builtinFor; runtime/eval.go evalInvocation (receiver prepended as the first argument) runtime/collections_test.go TestCollectionScalarResults; conformance calc_collection_receiver_form ✅ Faithful
A sequence is flat: an element of a sequence expression that is itself a collection contributes its elements, so (xs, ys) is xs->union(ys) — which is how the library defines union, as the sequence expression (seq1, seq2) — and a mapper answering several values contributes them all runtime/eval.go evalSequenceExpr; runtime/collections.go builtinControlCollect; passes/typecheck_expr.go writtenLength (a written length is knowable only where every element is a literal, so a literal index past a sequence of names is left to evaluation) runtime/collections_test.go TestSequenceExpressionsAreFlat; conformance calc_sequence_expression_is_flat ✅ Faithful
A receiver binds by position, so a call written with both a receiver and named arguments (x->f(a = 1)) states no parameter for the receiver and is a typed error (ErrReceiverWithNamedArgs) rather than a call the receiver is dropped from runtime/eval.go evalInvocation; passes/typecheck_expr.go checkArguments runtime/collections_test.go TestCollectionOperationErrors, TestReceiverWithNamedArgumentsIsReported; passes/typecheck_expr_test.go TestExprInvocationReceiverWithNamedArguments ✅ Faithful
A collection operation is an expression wherever an expression is allowed, including inside a calc body's while and for loops runtime/collections.go, runtime/action_statements.go conformance calc_collection_ops_in_for_loop, calc_collection_ops_in_while_loop ✅ Faithful
Every operation is bounded by the evaluation step budget, since each call of its body spends steps runtime/eval.go Context.step robustness_test.go:collection_operation_step_budget ✅ Faithful
A materialized element is bounded on its own, since it is memory the collection keeps rather than work a step does: every path that adds one to a sequence — a range, a sequence literal, ->collect, union/intersection/including/excluding/subsequence/tail/select — charges the element budget (SYSML_MAX_ELEMENTS, default 1000000, ~104MB of Values) and reports ErrElementLimitExceeded, not the step limit. The count is what an evaluation holds, not what a run produced: a statement, and an evaluation outside a body alike (beginStep), releases the elements it materialized, so a loop or a long run building a small collection each step is bounded by its peak rather than its total, while a collection kept across statements had to be materialized in one of them and so was charged in full runtime/context.go chargeElements/elementScope/beginStep, runtime/statements.go statement, runtime/collections.go newSequence, runtime/range.go rangeSequence, runtime/eval.go evalSequenceExpr; budget from budget.go element_budget_test.go:TestElementBudgetBoundsEveryMaterialization, :TestElementBudgetIsNotTheStepBudget, :TestElementBudgetCountsElementsHeldNotProduced, :TestElementBudgetIsReleasedByEveryStep, :TestElementBudgetIsPerRun, robustness_test.go:collection_spends_the_element_budget, grpc/budget_test.go:TestNewServiceResolvesBudgets ✅ Faithful
An activation ends with the body execution it belongs to, so what the calc usages read in a body computed is discarded when that execution ends rather than held for the whole run runtime/statements.go finish/enterActivation, runtime/action_statements.go executeBody, runtime/invoke_calc.go runCalcBody action_activation_test.go:TestActionBodyActivationEndsWithTheBody, calc_usage_body_local_test.go ✅ Faithful
A calc usage declared among a state machine's members binds its inputs from the values the machine has reached, as one in a calc's or an action's body does, so a guard reading it is answered over the running attribute rather than over what it was declared with runtime/calc_usage.go enclosedByBehaviorBody, runtime/invoke_calc.go isStateSymbol conformance state_guard_reads_calc_usage ✅ Faithful
An evaluation outside a body — a decision guard, an inline node expression, a transition guard, change condition or duration, an attribute default, a feature value default, an action argument, a constraint or requirement check — is a scope of its own, so what a calc usage answers it, and the elements a collection it evaluates materializes, live no longer than that step: the next guard reads the usage again over the values the step before it assigned. A read through a part's feature chain belongs to the evaluation making it and shares its activation runtime/eval.go beginStep, runtime/state_executor.go evalStep, runtime/action_executor.go stepDecisionNode/stepActionExecutionNode/initializeAttributes, runtime/condition.go evaluateConditions, runtime/calc_usage.go calcUsageMemberValue conformance action_guard_reads_calc_usage; calc_usage_step_test.go:TestDecisionGuardReadsCalcUsagePerStep, :TestDecisionGuardsShareOneCalcUsageEvaluation, :TestPartChainReadBelongsToTheReadingActivation ✅ Faithful
A failing expression of literals alone is answered at the prompt with the failure itself, so sysml -e "(1,2,3)#(0)" reports the index rather than "no declarations loaded" repl/meta.go tryEvalLiteral, isLiteralAnswerError repl/runtime_commands_test.go TestEvalReportsTheAnswerOfALiteralExpressionThatFails ✅ Faithful
A name the session declares is answered by that declaration, so the prompt's literal pass declines an expression using one rather than letting a library operation of the same unqualified name stand in for it repl/meta.go tryEvalLiteral, declaresANameIn repl/runtime_commands_test.go TestEvalPrefersASessionDeclarationOverALibraryOperation ✅ Faithful

⚠️ A body parameter takes its type from the element type of whatever the operand turns out to hold, which the expression checker does not track: an expression over the parameter (xs.?{in e; e + 1}) therefore has no static type, and its selector is checked at evaluation rather than where it is written. Statically the checker reports what it can know — a selector whose result type is known and is not Boolean, an index that is no whole number, a literal index of 0 or past a sequence written out (passes/typecheck_expr.go inferIndex, inferSelect) — and the runtime checks the rest, so no wrong answer results from what is left unchecked.

Found, not implemented — declared collection operations this runtime does not evaluate. Each is a typed unresolved reference or unsupported error, never a wrong answer:

Not implemented Why
CollectionFunctions::'array#' and the Array/Matrix collections Needs a multi-dimensional array value; the runtime's collection values are a sequence and a set.
ComplexFunctions::sum/product, VectorFunctions::sum/sum0 The other operations of both packages are implemented (see the numeric library table above); an aggregation over a collection of vectors or of Complex values is not, since a sequence of them flattens and loses the grouping it sums over.
A reducer named rather than written (->reduce min, as the library's own minimize is defined) A function-valued name is not a runtime value: reduce takes the body expression form (->reduce {in a; in b; …}), and minimize/maximize are implemented directly rather than through reduce min. A named reducer is reported as a type error, not read as a body.
SequenceFunctions::add/addAt/remove/removeAt, CollectionFunctions mutators These are behaviors, not functions: they declare an inout sequence, so they need mutable accumulation the language layer does not have. Deliberately out of scope.
Set coverage in the conformance corpus No expression of the language produces a set today — a ValSet arises only through the embedding API — so the operations over a set are pinned at the unit level (TestCollectionOperationsOverSets, elementsOf) rather than by a .sysml fixture. A set-valued expression is separate work.
at, first, reverse Not declared by the Kernel Function Library at all (head, #(1) and last are the declared spellings). Not implemented rather than invented.

OpenSysML Extension Library (non-normative)

The OMG Kernel Function Library declares no exponential, no logarithm and no two-argument arctangent: RealFunctions has sqrt/floor/round/abs/max/min/'**'/'^', TrigFunctions has sin/cos/tan/cot/arcsin/arccos/arctan, and that is all. The vendored OMG files stay byte-identical, so the missing signatures are declared in a clearly non-normative OpenSysML extension instead: internal/core/libs/stdlib/OpenSysML Libraries/OpenSysMLMathFunctions.kerml. It is bundled by the same embed.FS as the vendored tree and enters the same gates — TestStdlibConformance now reports 95/95 clean. It is OpenSysML code under Apache 2.0, not OMG code under EPL-2.0; internal/core/libs/stdlib/NOTICE carves the subdirectory out of the OMG notice.

Reachability. A model writes import OpenSysMLMathFunctions::*; (or calls OpenSysMLMathFunctions::exp(x) qualified); both resolve like any other library package, with no diagnostic. A bare exp(x) with no import is reported unresolved reference: exp and does not evaluate: the name is legal only under that import, since no OMG library declares it, so the call fails with ErrUnimportedExtensionFunction naming the import rather than being answered by a declaration the model never made visible. A bare sqrt(x) still evaluates — the OMG function libraries are in force whatever a model imports.

Semantic Rule Implementation Test Case Status
exp(x) — e raised to the power x runtime/library_functions.go (math.Exp) TestLibraryFunctionValues ✅ Faithful
ln(x) — natural logarithm, defined for x > 0.0 runtime/library_functions.go naturalLog TestLibraryFunctionValues, TestLibraryFunctionErrors ✅ Faithful
log(x, base) — logarithm to an explicit base, so base 10 and base e are never confused; base 10 and base 2 use math.Log10/math.Log2, which are exact where the ratio of logarithms is not runtime/library_functions.go logToBase TestLibraryFunctionValues, TestLibraryFunctionErrors ✅ Faithful
atan2(y, x) — full-quadrant angle, parameters ordered as in IEEE 754 and math.Atan2 runtime/library_functions.go atan2Real TestLibraryFunctionValues, TestLibraryFunctionAtan2NamedArguments ✅ Faithful
ln(0.0), ln(-1.0), log(x, 1.0), log(-1.0, 10.0), atan2(0.0, 0.0) report a domain error; exp beyond the Real range reports an overflow runtime/library_functions.go TestLibraryFunctionErrors, TestRuntimeRobustness/extension_library_function_outside_its_domain ✅ Faithful
The shipped declarations and the registered implementations cannot drift (names, parameter names, parameter order) runtime/library_functions.go registry TestOpenSysMLMathFunctionsMatchTheShippedDeclarations ✅ Faithful
Evaluable from a calc def body under import OpenSysMLMathFunctions::*; runtime/invoke_calc.go calc_opensysml_math_functions.sysml + golden trace ✅ Faithful
An unqualified call the model imports no declaration of fails with a typed error naming the function and the import that makes it legal, so the diagnostic and the behavior agree instead of contradicting each other runtime/library_functions.go extensionLocalNames, unresolvedLibraryFunction, ErrUnimportedExtensionFunction, read by runtime/eval.go (unresolved-call dispatch) runtime/library_functions_test.go:TestUnimportedExtensionFunctionCallIsATypedError, TestLibraryFunctionUnqualifiedNames, TestRuntimeRobustness/calc_calls_an_unimported_extension_function ✅ Faithful
The function listing covers every function the build implements, an extension one marked with the import its unqualified name needs, so a working function is advertised neither as unsupported nor as callable bare runtime/builtin_names.go Builtin.RequiresImport, Builtins, read by repl/discover.go doBuiltins and repl/complete.go runtime/library_functions_test.go:TestBuiltinsListExtensionFunctionsWithTheirImport, repl/discover_test.go:TestBuiltinsListsAnExtensionFunctionWithItsImport ✅ Faithful

Static Expression Type Checking (KerML §7.4 Expressions, §8.3 Feature Values)

Checked before execution, at the type validation tier. Every rule is one-sided: a diagnostic is reported only when both the expected and the actual type are known, so unmodelled types never produce a false positive.

Semantic Rule Implementation Test Case Status
Scalar type lattice over ScalarValues semantics/exprtype.go PrimTypeOf/PrimConforms typecheck_expr_test.go ✅ Faithful
Feature value conforms to declared type passes/typecheck_expr.go checkUsageValue TestExprBindStringToIntegerAttribute ✅ Faithful
Arithmetic operand types (+ - * / % **) passes/typecheck_expr.go checkAddition/checkArithmetic TestExprAddIntegerAndStringRejected ✅ Faithful (+ over two Strings is concatenation, per StringFunctions::'+'; a String with a number is rejected at this tier, and semantics.PrimConforms widens numerics only, so neither is coerced to the other)
Boolean operand types (and or xor implies & \|, not) passes/typecheck_expr.go checkBinaryBoolean/checkUnaryBoolean TestExprAndOnIntegerRejected ✅ Faithful
Comparison operand types (< > <= >=) passes/typecheck_expr.go checkComparison TestExprComparisonOfBooleanRejected ✅ Faithful
Disjoint ==/!= operands (warning; '==' is declared over Anything) passes/typecheck_expr.go checkEquality TestExprEqualityAcrossDisjointTypesWarns ✅ Faithful
Boolean-valued contexts (constraint/assume/require, if/while/until, guards) passes/typecheck.go checkBehaviorMember (recursing into loop and branch bodies, so a nested condition is checked too) TestExprTransitionGuardMustBeBoolean, TestTypeCheckNonBooleanControlFlowConditions ⚠️ Approximate (a condition whose type the expression checker can infer is checked here, before execution; a bare feature reference infers Unknown — see What We Don't (Yet) Support — and is caught by the executor instead)
Change-event conditions (accept when <expr>, transition ... when <expr>) passes/typecheck.go checkTrigger TestExprAcceptWhenConditionMustBeBoolean ⚠️ Approximate (accept when is always a condition; after transition ... when a bare name is a signal, so only expressions are checked there)
Division/exponentiation result types (Natural/Natural -> Natural, Integer/Integer -> Rational) passes/typecheck_expr.go divisionResult TestExprWholeNumberDivisionAndPowerOK ✅ Faithful
Calc/action invocation arity, incl. inherited, partially redefined (:>>), and arrow-form receiver passes/typecheck_expr.go effectiveInParameters/checkArguments TestExprPartiallyRedefinedParametersKeepInheritedSignature ✅ Faithful
Invocation argument types and named-argument names passes/typecheck_expr.go checkArguments TestExprInvocationArgumentTypeMismatch ✅ Faithful
No false positives on the shipped library and examples corpus guard model/typecheck_expr_corpus_test.go ✅ Faithful
Non-scalar conformance of bound values (specialization hierarchy, enumeration literals) passes/typecheck_value.go checkValueConformance TestValueUnrelatedInstanceDoesNot, TestValueEnumerationLiteralOfOtherEnum ⚠️ Approximate (a value is typed only when it is a name or a literal; expressions producing an instance are not judged)
Multiplicity conformance of bound values passes/typecheck_value.go checkValueCount, effectiveRange, wording shared with the runtime through semantics/multiplicity.go Range.CountViolation TestValueTooManyValuesForUpperBound, TestValueTooFewValuesForLowerBound, TestValueEmptyCollectionForLowerBound, TestValueCountAgainstRedefinedMultiplicity, TestValueNestedCollectionCountsItsElements, TestValueCollectionOfReferencesIsNotCountedStatically ⚠️ Approximate (only a literal, or a collection literal of them, has a statically known element count — flattened as binding flattens it; a reference may itself be multi-valued, so it and any collection holding one are left to the runtime check when the feature value materializes)
Collection element types (each element against the feature's type) passes/typecheck_expr.go checkUsageValue TestValueCollectionElementTypes ✅ Faithful

View and Viewpoint Members (SysML v2 §8.3.20 Views, §8.3.26 Viewpoints; SysML.xtext ViewRenderingMember, FramedConcernMember, StakeholderMember, ActorMember)

Each of these keywords owns a usage through a dedicated membership, and each usage is written either as a reference to an existing element or as a declaration introduced by the kind keyword the notation spells out. A reference declares no name of its own: the name it answers to is its reference's, derived by ast.EffectiveName (KerML §7.3.4.5), which is why a reference to an inherited element is not a name conflict.

Semantic Rule Implementation Test Case Status
render owns a RenderingUsage through a ViewRenderingMembership: render asTree; references the rendering the view uses, render rendering r : AsTree; declares one. No ValuePart, and no definition form. The declaration's UsageDeclaration is optional, so it may be anonymous (render rendering : AsTree;) ast/defusage.go UsageViewRendering; parser/defusage.go parseDefUsage (render/frame dispatch) and parseReferenceMemberUsage; symbols/builder.go (SymbolRenderingUsage), passes/typecheck.go (typed by a rendering def), semantics/implicit.go (Views::Rendering), export/kinds.go (ViewRenderingMembership), export/rdf_in.go usageHead (which form to write back is decided by the reference, not the name, so an anonymous declaration keeps its kind keyword) parse/view_members.golden, export/testdata/convert/kind_keyword_synonyms.golden.sysml, parser/negative_test.go (render_definition, render_reference_value), passes/nameres_test.go TestRenderReferenceToInheritedRenderingIsNoConflict, TestRenderDeclarationOfInheritedNameConflicts, corpus 42. Views/* ✅ Faithful (parse and naming; what a view renders is read from this member by view/view.go Renderer.KindOf — see the rendering rows)
frame owns a ConcernUsage through a FramedConcernMembership: frame 'system breakdown'; references the concern framed, frame concern c : SafetyConcern; declares one, possibly anonymously (frame concern : SafetyConcern;). Its body is a requirement body ast/defusage.go UsageFramedConcern; parser/defusage.go (same dispatch, body parseRequirementBody); symbols/builder.go (SymbolConcernUsage), passes/typecheck.go (typed by a concern def), semantics/implicit.go (Requirements::ConcernCheck), resolve/document.go parameterizedByName, export/kinds.go (FramedConcernMembership) parse/view_members.golden, parser/negative_test.go (frame_definition), corpus 42. Views/Viewpoint Example.sysml ⚠️ Approximate (frame concern SafetyConcern; follows the grammar's ConstraintUsageDeclaration and declares a concern usage named SafetyConcern; the reference to a concern of that name is frame SafetyConcern;. Framing is checked against the viewpoint's concerns by semantics/conformance.go Model.ViewConformance — see the viewpoint conformance row)
stakeholder and actor own a PartUsage through a StakeholderMembership / ActorMembership; both are declarations (stakeholder se : Engineer;, actor driver : Person;) and neither has a definition form ast/defusage.go UsageStakeholder, UsageActor (the former ast.ActorMember node is gone); parser/defusage.go parseDefUsage; symbols/builder.go (SymbolPartUsage), passes/typecheck.go (typed by a part def), semantics/implicit.go (Parts::Part), runtime/context.go memberBindings (actor binding, name via ast.EffectiveName), export/kinds.go parse/view_members.golden, parse/requirement_members.golden, parser/behavior_test.go TestParseRequirementBody_Actor, parser/negative_test.go (stakeholder_definition, actor_definition, stakeholder_no_declaration, actor_no_declaration), runtime requirement_actor.sysml, corpus 41. Use Cases/*, 42. Views/* ✅ Faithful (a stakeholder or actor definition is rejected: the notation has only the usage, typed by the party's definition)
satisfy names the requirement satisfied (satisfy vehicleSpecification by vehicle;) or declares the satisfaction (satisfy requirement r : Req1 by v;); a view body's satisfy viewpoint; is the same form parser/defusage.go parseUsage UsageSatisfy branch (RequirementUsageKeyword UsageDeclaration?, then ValuePart?, then by) parse/satisfy_reference.golden (incl. the anonymous forms satisfy requirement by vehicle; and satisfy requirement : VehicleSpecification by vehicle;), parse/view_members.golden, parser/satisfy_subject_test.go ⚠️ Approximate (the reference is recorded as a Subsetting rather than a ReferenceSubsetting, so passes/typecheck.go can require the target to be a requirement usage; a satisfy reference therefore takes no effective name, so semantics/conformance.go Model.SatisfyTarget reads the target from the Subsetting relationship instead)
frame and render are also legal names (KerML has neither keyword; the Kernel Semantic Library writes in frame : SpatialFrame[1]). Only a name or the member's own kind keyword can follow the member keyword, so anything else — a multiplicity, a specialization, a type, a value, a body, ; — declares a feature named after the keyword parser/defusage.go atMemberKeywordUsedAsKeyword parse/view_members.golden (frame[0..1] : Engineer;, render :> frame;), libs/reserved_keyword_name_test.go, TestStdlibConformance ⚠️ Approximate (the parser does not track the enclosing body kind, so the reading is decided by the following token alone: frame; inside a viewpoint declares a feature named frame instead of being diagnosed as a framing with no concern)
expose in a view body is an Import see the Name Resolution section's expose rows parser/expose_test.go, resolve/expose_test.go, parse/view_expose.golden ✅ Faithful (validateExposeOwningNamespace reports an expose outside a view usage — see the Name Resolution section's expose rows)

Structural, Interface and Analysis Notation (SysML v2 §7.12 Ports, §8.2.2.14 Interfaces, §8.2.2.19 Analysis Cases, §8.3.9.11 Occurrences)

Notation exercised by the Open-MBEE corpus models (starkit, Dragon, DesertKite/OOSEM, the spacecraft example notebooks). Conjugation is a semantic relationship, not parser sugar: the ~ is kept on the typing relationship (ast.Relationship.Conjugated) and the reversal of in/out is computed in the semantics layer over the conjugation parity of the typing/specialization chain.

Semantic Rule Implementation Test Case Status
port p : ~P types a port by the conjugated port definition P::'~P' (§7.12.3); its features are P's with in/out reversed and inout unchanged, and conjugation composes, so a conjugate of a conjugate has P's directions ast/defusage.go Relationship.Conjugated; parser/defusage.go parseRelationshipClauseTarget; semantics/conjugation.go ConjugateDirection, superEdges, typeEdge, featureEdges, conjugatedSupertypes, PortFeatures, IsConjugated (a declaration's feature typing carries the conjugation, so it is read ahead of a redefinition clause written before it) parse/conjugated_port_type.golden, semantics/conjugation_test.go TestConjugationReversesDirections, TestDoubleConjugationRestoresDirections, TestConjugationOnRedefiningPort, parser/negative_test.go (conjugated_no_type, conjugated_no_type_after_name) ✅ Faithful
A port usage conforms to the definition it conjugates, and two ports match when each named feature of one has a feature of the other with a conforming type and the conjugate direction (§7.12.2) semantics/conjugation.go PortsConform, featuresMatchConjugate, featureTypesConform semantics/conjugation_test.go TestConjugatedPortConformance ✅ Faithful
The ports at the two ends of an interface must have conjugate directed features: what one end sends the other receives semantics/conjugation.go InterfaceEndPortMismatch, endPortFeatures; passes/constraint.go checkInterfaceEndConjugation (code port-conjugation) passes/constraint_test.go TestConstraintInterfaceEndConjugation, semantics/conjugation_test.go TestInterfaceEndConjugation ⚠️ Approximate (reported as a warning, and only for an interface whose two ends both declare a resolvable port type; undirected features carry no flow, so they are not required to match; the ends of a connect/flow clause take their types by implicit redefinition of the interface's ends, which is checked as end identity, not as direction)
Only a port usage or a connector end may be typed by a conjugated port definition, and ~ must name a port definition passes/typecheck.go checkConjugatedTyping passes/typecheck_test.go TestTypeCheckConjugatedTyping ✅ Faithful
An interface body may declare a default end with no declaration at all: end; is an anonymous port usage (§8.2.2.14.1 DefaultInterfaceEnd: isEnd ?= 'end' Usage) parser/parser.go bodyContext/pushBodyContext; parser/defusage.go parseAnonymousEnd, parseAnonymousEndUsage, anonymousUsageKind parse/end_usages.golden, resolve/analysis_test.go TestResolveDragonStructures ✅ Faithful
Anywhere else a bare end; is not standard notation: only DefaultInterfaceEnd makes the usage declaration optional, and every other end form (ReferenceUsage, EndUsagePrefix + a kind keyword) requires an Identification or a specialization part parser/defusage.go parseAnonymousEndUsage (typed diagnostic naming the fix, end ref;) parser/negative_test.go (end_outside_connector, end_outside_connector_package) ✅ Faithful (see the known limitation on Dragon.sysml below)
require Q::r { … } / assume Q::r { … } subset a requirement by a qualified reference, and the body may redefine a feature of it by its qualified (:>> R::f = expr) or its plain name (:>> f = expr), which the member inherits through the reference subsetting ast/behavior.go RequireMember.Reference, AssumeMember.Reference (a full *ast.QualifiedName, not a final segment); parser/behavior.go parseRequireMember, parseAssumeMember; resolve/document.go resolveConstraintReference, walkConstraintBody, lookupConstraintRefFeature parse/require_qualified_requirement.golden, resolve/analysis_test.go TestResolveQualifiedRequirement, TestResolveRequiredRequirementFeatureByPlainName, TestResolveRequiredRequirementUnknownPlainName, TestResolveQualifiedRequirementUnresolved, TestResolveQualifiedRedefinitionUnresolved, parser/negative_test.go (require_qualified_malformed_body, require_qualified_trailing_colons) ⚠️ Approximate (only the braced spelling is read as a reference — see the known limitation below)
A require/assume body is a namespace of its own, resolved to whatever depth it is written to: a declaration nested in it has its own body walked recursively, what the body declares is visible inward but does not leak outward, and the referenced requirement's features are offered to the body's direct members, which is what the reference subsetting inherits them to symbols/builder.go buildConstraintBodyScope, ConstraintBodyScope (a body-local child scope keyed by the member), read by symbols/bodyscopes.go, resolve/document.go walkConstraintBody and resolve/references.go resolve/constraint_body_test.go TestResolveTypoAtDepthTwoInARequireBody, TestResolveTypoAtDepthThreeInAnAssumeBody, TestResolveDeepRequireBodyResolves, TestResolveRequireBodyNamesDoNotLeakOutward, resolve/analysis_test.go TestResolveNestedRedefinitionPrefersOwnType ✅ Faithful
Every tier reads a require/assume body in that same body scope, so name resolution, type checking and condition evaluation agree on which declaration a name in the body denotes (a body-local name shadows one of the same name outside it) passes/typecheck.go checkBehaviorMember (AssumeMember/RequireMember route through symbols.ConstraintBodyScope), runtime/condition.go appendConditions, Condition.Owner (nearest owning scope, since a body-local scope owns no symbol) passes/typecheck_constraint_body_test.go TestTypecheckReadsARequireBodyLocalName, TestTypecheckReportsAMismatchNestedInARequireBody, TestTypecheckRequireBodyLocalNameShadowsTheEnclosingOne, TestTypecheckRequireBodyLocalValuesStayClean, runtime/condition_test.go TestRequireBodyConditionReadsABodyLocalName, runtime/conditions_of_test.go TestConditionsOfCarryScopeAndOwner ✅ Faithful
snapshot and timeslice are the two portion kinds of an occurrence usage (PortionKind, §8.3.9.11 OccurrenceUsage::portionKind); either prefix makes the declaration an occurrence usage whatever kind keyword follows. PortionUsage ends in Usage, whose declaration is optional, so an anonymous portion (timeslice;) is standard notation and is accepted without a diagnostic. Both keywords are read by the same prefix, so they agree on every declaration spelling, including a quoted name (snapshot 'launch event';) ast/defusage.go PortionKind, Usage.Portion; parser/defusage.go (portion prefix), parser/behavior.go (portion-prefixed behavior parameters); ast/dump.go; passes/typecheck.go declKind.portion, isOccurrenceUsage; export/rdf_out.go/rdf_in.go parse/occurrence_portions.golden, parser/occurrence_modifier_test.go, resolve/analysis_test.go TestResolveDragonStructures, parser/negative_test.go (timeslice_no_subject, timeslice_usage_no_type, timeslice_unterminated) ✅ Faithful (parse, naming and resolution; a portion is not related to its whole occurrence at runtime — see below)
The standard view/diagram library is part of the vendored stdlib, so view v : StandardViewDefinitions::gv; resolves libs/stdlib/Systems Library/StandardViewDefinitions.sysml (already vendored: the eight standard view definitions with their short names) model/standard_views_test.go TestStandardViewDefinitionsBundled, TestStdlibConformance ✅ Faithful
A qualified reference whose first segment names no loaded namespace is reported as such, naming the declarations that do carry the trailing name resolve/qualified.go (unresolved-namespace diagnostic), symbols/index.go FQNsEndingIn model/standard_views_test.go TestVendorViewNamespaceDiagnostic, resolve/analysis_test.go TestResolveMissingStandardViewNamespace ✅ Faithful
Every usage element subsets the most general base usage Base::things, whose that feature is therefore visible in a usage body (§7.6, [KerML 8.4.2]) semantics/implicit.go implicitBaseUsage, semantics/reference.go contributors semantics/implicit_test.go TestImplicitBaseUsageContributesThat, model/that_constraint_test.go TestThatResolvesInAssertedConstraint ✅ Faithful (a member-contribution edge only: it is deliberately not a direct supertype, so conformance and DirectSupertypes are unchanged)
The notation the Open-MBEE corpus models write stays clean at every validation tier: a conjugated interface and connection end, connect/flow between two such ends, and a portion prefixed onto a kind keyword and named as its whole occurrence is (item item1 { timeslice item item1; }) the rules above (parser/defusage.go end and portion prefixes, semantics/conjugation.go, passes/typecheck.go checkConjugatedTyping) passes/integration_test.go TestPassesGoldenCorpusNotation over testdata/passes/corpus_notation.golden ((no diagnostics)), parser/negative_test.go (conjugated_end_no_type) ✅ Faithful
A succession may be written with no keyword at the start of a namespace member: first a::b then c; (SuccessionAsUsage) parser/namespace.go parseMember, parseSuccessionAsUsage parse/succession_as_usage.golden ✅ Faithful
Each end of a binding connector is a ConnectorEnd, so it names a feature by a QualifiedName or by a feature chain each of whose chaining features is itself a qualified name (§8.2.2.9.2 BindingConnectorAsUsage/ConnectorEndMember, [KerML 8.3.3.2] OwnedReferenceSubsetting, OwnedFeatureChaining): bind A::b.C::d.e = F::g;. The chain is recorded segment by segment (nested ast.FeatureChainExpr whose Member is the full qualified name), and the end is a ReferenceSubsetting, so it resolves outside the connector rather than as an inherited redefinition parser/defusage.go parseUsage UsageBinding branch, bindingEnd, parseRelationshipTarget; resolve/document.go resolveFeatureChain (a qualified chaining feature resolves outward when the previous element has no such member) parse/binding_qualified_ends.golden, resolve/analysis_test.go TestResolveQualifiedBindingChain, parser/negative_test.go (binding_end_qualification_no_name, binding_end_chain_trailing_dot, binding_end_chain_trailing_dot_qualified, binding_end_unterminated, binding_end_no_target) ✅ Faithful
A connection usage may state its ends where its own name would go and then carry an ordinary body: connect x.p to r.p { … }, connect (a, b, c) { … } (§8.2.2.9.2 ConnectionUsageConnectorPart UsageBody). The keyword-less form is read by the same parseUsage path every declaration is, so the body's members are the usage's own members — nothing is parsed and discarded — and each end is a feature chain of any depth parser/defusage.go atDefUsageStart, parseDefUsage (connect branch), parseUsage (skipIdentification), parseTierBEnds, atConnectorShorthandEnds, atEndThenKeyword parse/connector_usage_body.golden, parser/negative_test.go (connect_body_unclosed, connect_to_no_target_before_body, connect_no_ends, connect_body_no_ends) ✅ Faithful (the keyword is the ConnectorPart, so it states at least one end: connect; and connect { … } are reported. A multiplicity written after the keyword is the first end's, connect [1] a to [1] b)
An interface usage may likewise state connector-style ends where its name would go, with or without a body: interface b1.p to b2.p { … }, interface differential.leftDiffPort to rearAxle.leftHalfAxle.axleToDiffPort; (§8.2.2.14 InterfaceUsageInterfacePart), while interface named : Coupling connect a to b keeps stating its ends after connect parser/defusage.go parseUsage (skipIdentification, skipMultiplicity), parseTierBEnds, atConnectorShorthandEnds parse/interface_usage_shorthand.golden, parser/negative_test.go (interface_ends_no_target, interface_ends_no_to, interface_ends_unclosed_body) ✅ Faithful
A flow usage may carry a body after its ends, and its ends may be feature chains with the to on a continuation line: flow s1.x to s2.x { … } (§8.2.2.10 FlowUsageFlowDeclaration UsageBody) parser/defusage.go parseUsage (skipIdentification via atFlowShorthand/from), atEndThenKeyword (the shorthand is recognized past a chain of any depth) parse/flow_usage_body.golden, parser/negative_test.go (flow_ends_no_target_before_body, flow_ends_unclosed_body) ✅ Faithful
A requirement-like body admits usage elements, so a connector, flow or message written with its kind keyword (connection connect r to x;) is a member of a requirement, constraint, concern, objective, use case or view body exactly as it is at package level (§8.2.2.19 RequirementBodyDefinitionBodyItemUsageElement) parser/behavior.go parseRequirementMember, usageIsSubstantive (a connector or flow usage declares no name, so its ends are what make it substantive) parse/requirement_body_prefixed_usages.golden ✅ Faithful

Known limitations of this notation

  • A body on a succession or on a control node is not read yet: SuccessionAsUsage ends in DefinitionBody and MergeNode/DecisionNode/JoinNode/ForkNode in ActionBody, so first start then continue { … } and merge continue { … } are standard notation, but OpenSysML requires ; there. These are the outstanding false positives on the pilot validation corpus's 3a-Function-based Behavior-1 and 5-State-based Behavior files (a transition body is the same gap). Reading them needs body members on ast.InitialNode, the four control nodes and ast.SuccessionEdge/ControlFlowEdge, and lowering that executes them, so it is a feature of its own rather than a parser tweak, and no member is dropped in the meantime — the notation is reported, not silently accepted.
  • Dragon.sysml declares bare end; members inside connection def, flow def and nested connection def bodies (6 sites). This is not standard notation: a connection def/flow def body is an ordinary DefinitionBody, whose members are NonOccurrenceUsageElement/OccurrenceUsageElement — neither includes DefaultInterfaceEnd (only InterfaceBodyItem does), and the only other keyword-less end, DefaultReferenceUsage, requires a UsageDeclaration. OpenSysML reports these with a typed diagnostic naming the conforming form (end ref;, which ReferenceUsage does allow) rather than inventing grammar.
  • Dragon.sysml, OOSEM.sysml and DesertKite.sysml type their views by 'SysML Standard Diagrams'::gv (7, 3 and 10 sites). No such namespace exists in the OMG release library or the pilot implementation — it is a tool-specific package, not part of the standard library — so it is not vendored under that name and no alias to StandardViewDefinitions is fabricated. The diagnostic says the namespace is not loaded and points at StandardViewDefinitions::gv.
  • The two notations that had been suspected of being our false positives are adjudicated as legal and are accepted: a conjugated end (end spacePort : ~CommunicationPort, spacecraft-example-model.sysml) and a portion prefixed onto a kind keyword (timeslice item item1, Dragon.sysml). ConjugatedPortTyping specializes FeatureTyping (Systems Library/SysML.sysml:100), so any feature typing — a connection or interface end among them — may name a conjugated port definition; and PortionKind is an enumeration of timeslice/snapshot (Systems Library/SysML.sysml:291) held by OccurrenceUsage::portionKind (:262), which an ItemUsage is. Both parse, resolve and check clean over every tier, pinned by testdata/passes/corpus_notation.golden, so neither notation is outstanding. What the models do still report is other notation: OOSEM.sysml (Open-MBEE/DesertKite.sysml, default branch) reports the three 'SysML Standard Diagrams'::gv errors above and nothing else, and DesertKite.sysml — which lives only on that repository's InitialDesign branch — reports 7 errors that are ours, not the model's: a qualified name refused as a bind end (3 sites, 6 errors) and connection connect … ; refused inside a requirement body (1 site). Both are parser defects owned by a separate session; they are not adjudicated here and no verdict above depends on them.
  • Only the braced spelling of a requirement-constraint reference sets RequireMember.Reference/AssumeMember.Reference. CalculationBody also allows ;, so standard require Q::r; is a reference too, but OpenSysML reads a body-less require/assume member as a condition expression, which the runtime evaluates as Boolean. Distinguishing the two spellings needs the name's resolution, not its syntax, and no spelling requires the referenced requirement's own conditions at runtime yet (runtime/condition.go appendConditions walks only the member's own body), so the body-less form is left on the expression path rather than made a silent no-op.
  • Conjugation is not a runtime concept here: nothing is executed differently for a conjugated port, because ports carry no transfer semantics in the runtime yet (see "Major Features Not Implemented").
  • A snapshot/timeslice portion is recorded on the usage and resolves like any occurrence usage, but the runtime does not relate a portion to the occurrence it is a portion of, and no time ordering between portions is derived.

Usage Prefixes and KerML Classifier Declarations (SysML v2 §7.6 Usages, §8.3.9.11 Occurrences; KerML §8.2 Classifiers; SysML.xtext RefPrefix, IndividualUsage, PortionUsage, ForVariableDeclaration)

Semantic Rule Implementation Test Case Status
A lone occurrence modifier declares the kind of the usage it prefixes: individual i : V is an individual usage, snapshot s/timeslice ts/event e occurrence usages, and the modifier reaches the symbol kind rather than being recorded and ignored parser/defusage.go modifierImpliedKind (the kindless fallback of parseDefUsage), parser/behavior.go parseDirectionParameter; symbols/builder.go classifyUsageusageSymbolKind parse/occurrence_modifier_anonymous.golden, parse/occurrence_individual_snapshot.golden, parse/occurrence_portions.golden, parser/occurrence_modifier_test.go:TestParseUsageOccurrenceModifiers, symbols/occurrence_modifier_kind_test.go:TestOccurrenceModifierDecidesSymbolKind ✅ Faithful
A modifier followed by a kind keyword and no name (individual part : Vehicle, ref item : Integer) declares an anonymous usage of that kind — the keyword is the kind, never the name, so individual part and individual item stay distinct — and the nameless form is reported so it can be named (ambiguous-modifier-kind), for the keywords the reading site actually consumes as a kind — ref frame : SpatialFrame names the usage frame, so it is not ambiguous parser/defusage.go warnAmbiguousModifierKind with declarationKindKeyword from parseDefUsage and parameterKindKeyword from parser/behavior.go parseDirectionParameter parser/modifier_kind_ambiguity_test.go:TestAmbiguousModifierKindWarns, :TestUnambiguousModifiedUsagesDoNotWarn, parse/occurrence_modifier_anonymous.golden ✅ Faithful (a warning: the declaration is well-formed, Usage: UsageDeclaration? UsageCompletion, and the AST it yields is complete — only the name is missing)
A parameter is a usage prefixed by its direction, and its declaration is optional, so in snapshot ; is an anonymous occurrence parameter that is registered as a member rather than dropped parser/behavior.go parseDirectionParameter; symbols/builder.go (anonymous member registration) parse/parameter_occurrence_modifiers.golden, symbols/occurrence_modifier_kind_test.go:TestAnonymousParameterBuildsSymbol ⚠️ Approximate (a parameter with no kind keyword at all — in x : Real — is built as a part usage, while the same kindless declaration outside a parameter list is a plain feature (ast.UsageAttribute). Aligning the two is a one-line change to the kind default, but the two readings differ observably — the RDF export of every kindless parameter turns from sysml:PartUsage into sysml:AttributeUsage, and neither is the ReferenceUsage SysML v2 §7.6 actually names, which ast.UsageKind cannot yet carry — so the choice is escalated rather than taken)
A for loop's variable is a usage declaration, so a keyword may name it (for step in c), with the reserved-keyword warning any keyword-named declaration gets, and a short name alone identifies it (for <v> in c) parser/behavior.go parseForAction (parseIdentificationStopping("in") rather than an Identifier token) parse/action_for_keyword_variable.golden, parser/for_loop_variable_test.go:TestForLoopVariableMayBeKeyword, :TestForLoopVariableMayBeShortNameOnly, :TestForLoopKeywordVariableWarns, :TestForLoopMalformedRecovers ✅ Faithful
A datatype declares a KerML DataType — a definition — so a feature can be typed by it whether or not it specializes anything (datatype D; as much as datatype Real specializes Complex;), as class, struct, assoc, behavior and interaction already were symbols/builder.go classifyUsage (the datatype keyword decides, ahead of the relationship-driven attribute classification) symbols/kerml_type_kind_test.go:TestKerMLTypeDeclarationsAreClassified ⚠️ Approximate (a datatype is classified as an attribute definition, the SysML mapping of a DataType; a function stays a calcUsage, since the runtime resolves an invocation through it, so a function definition and a calc usage are still one kind)
A classifier declares a plain KerML Classifier, and every definition is a Classifier — a DataType among them — so classifier C specializes D is well-formed whatever kind D is declared with, while the narrower class, struct and part def rows stay constrained to their own kind (KerML §8.3.2, §8.4.4.1; SysML v2 §8.4.5.1) passes/typecheck.go compatMessage (the declKind.isPlainClassifier row, told apart by the written keyword since classifier and class are both ast.DefClass) passes/typecheck_classifier_test.go:TestTypeCheckClassifierSpecializesAnyDefinitionOK, :TestTypeCheckPartDefSpecializesDataTypeStillRejected, :TestTypeCheckClassifierSpecializesUsageStillRejected ✅ Faithful
KerML has no definition/usage distinction — every declaration is a Type and a Specialization relates two Types (KerML §8.3.3) — so in a .kerml document class Person specializes Object is well-formed, and the SysML rule that only a definition may specialize does not apply. The target must still be a type: a package or an annotation is not one passes/typecheck.go compatMessage (the declKind.isKerML row of ast.RelSpecializes), declKind.lang from source.KindOf — the same file-kind mechanism the kerml-notation warning reads passes/typecheck_kerml_language_test.go:TestTypeCheckKerMLSpecializationClean, :TestTypeCheckSysMLSpecializationStillFires, :TestTypeCheckKerMLSpecializesNonTypeStillFires ✅ Faithful
A KerML FeatureTyping's type is any Type, a Feature among them (KerML §8.3.4.4), so in a .kerml document feature yy : y is typed by a feature and the SysML usage-kind taxonomy — which requires a definition — does not apply. A non-type target is still reported passes/typecheck.go compatMessage (the declKind.isKerML row of ast.RelTyping), isTypeKind passes/typecheck_kerml_language_test.go:TestTypeCheckKerMLTypingByFeatureClean, :TestTypeCheckSysMLTypingByFeatureStillFires, :TestTypeCheckKerMLTypedByNonTypeStillFires ✅ Faithful
Only a Type may be a KerML specialization or typing target, and a Type is enumerated rather than assumed: every definition and usage kind and a KerML type declaration, but not a namespace, a dependency, an annotation or an alias — a resolvable alias is resolved to its target upstream, so one reaches the check only when it is cyclic and names no type. A kind added later is rejected until it is classified passes/typecheck.go typeSymbolKinds, isTypeKind; checkTypeTarget (alias resolution) passes/typecheck_kerml_language_test.go:TestIsTypeKindIsAnAllowlist, :TestTypeCheckKerMLSpecializesAliasOfTypeClean, :TestTypeCheckKerMLSpecializesCyclicAliasStillFires ✅ Faithful
A metaclass is a Class (KerML §8.4.4), so metaclass AtomMetadata specializes Metaobject is a metaclass specializing a metaclass and is well-formed in either language; an unrelated definition kind is still a mismatch passes/typecheck.go defSymbolKind (ast.DefMetaclasssymbols.SymbolMetaclass, absent before, which made every metaclass declaration incomparable with its own kind) passes/typecheck_kerml_language_test.go:TestTypeCheckMetaclassSpecializesMetaclass, :TestTypeCheckMetaclassSpecializesPartDefStillFires ✅ Faithful

Name Resolution

Semantic Rule Implementation Test Case Status
Inherited feature resolution document.go:199 resolveRedefinition flow_payload_test.go ✅ Faithful
Declaration named with a keyword (action flow { ... }, attribute item : Integer) parser/defusage.go atKindPrefix, atSecondaryKind parser/namespace_keywords_test.go TestParseKeywordAsNameAfterKindKeyword, model/behavior_body_resolve_test.go TestKeywordNamedDeclarationIsReferenceable ✅ Faithful (the name is kept and is referenceable)
Keywords reserved in name position (only an unrestricted name may spell one) parser/namespace.go parseIdentificationParser.Warnings, surfaced as passes.SeverityWarning code reserved-keyword-name by model/workspace.go parser/namespace_keywords_test.go TestParseKeywordAsNameIsReported, model/behavior_body_resolve_test.go TestReservedKeywordNameWarning, libs/reserved_keyword_name_test.go TestStdlibReservedKeywordNames ⚠️ Approximate (reported as a warning, not an error, because the normative OMG library itself uses unquoted keyword names — step entry[1];, part done : Part;, attribute type : String[0..1]; — and must keep parsing clean; those eleven sites — including the Kernel Semantic Library's in frame : SpatialFrame[1], whose name SysML reserves for a view/viewpoint member — are pinned by the libs test so the set cannot grow silently)
A keyword left in a parameter's name position names the parameter, quoted as KerML §7.2.4 requires (in 'type': Anything;, which the Kernel Function Library itself writes) or bare with the reserved-keyword warning, so the parameter is declared and its name resolves rather than the declaration failing to parse parser/behavior.go parseDirectionParameter (parseIdentificationStopping for the name, stopping on the post-multiplicity modifiers, rather than requiring an identifier token) parse/parameter_keyword_names.golden, resolve/parameter_keyword_name_test.go:TestResolveKeywordNamedParameter, :TestResolveKeywordNamedParameterUnresolved, parser/negative_test.go (keyword_named_parameter_no_type, parameter_default_no_value, parameter_redefines_no_target), conformance/calc_keyword_named_parameters.sysml, runtime/robustness_test.go:testCalcUnboundKeywordNamedParameter ✅ Faithful (a keyword the parameter grammar reads as something else — a kind keyword, a relationship keyword, default, ordered, nonunique — still reads as that, not as the name)
An event occurrence declares a feature like any other, so its name resolves from a value expression and from the trigger of a transition or an accept that names it; event <name> without the occurrence keyword references an existing occurrence (SysML v2 §8.3.13, SysML.xtext EventOccurrenceUsage), so a name that declares nothing is unresolved there parser/defusage.go parseUsage (the reference form is an ast.RelReferences relationship), resolved by resolve/target.go ResolveTarget and resolve/unqualified.go like any feature reference resolve/event_feature_test.go:TestResolveEventOccurrenceFeature, :TestResolveEventOccurrenceReferenceUnresolved ✅ Faithful
Keyword qualifying a kind keyword (var feature x, assert constraint { ... }, item part Shape) parser/defusage.go atKindPrefix, parseDefUsage parser/namespace_keywords_test.go TestParseKeywordBeforeKindKeywordIsNotAName ✅ Faithful
Words the grammar uses in one position only are not reserved: var marks a variable feature (KerML.xtext BasicFeaturePrefix, isVariable ?= 'var') and on is a literal in none of the pilot's grammars, so both name a feature everywhere else, as point does lexer/keywords.go (neither is in keywordList), parser/defusage.go atVarPrefix, atVarDeclaration lexer/lexer_test.go:TestContextualWordsAreIdentifiers, parser/contextual_keyword_name_test.go, parse/contextual_keyword_name_on.golden, parse/contextual_keyword_name_var.golden, parser/negative_test.go (state_named_on_no_semicolon, var_prefixed_declaration_no_type, var_prefix_without_kind_keyword) ⚠️ Approximate (var with the kind keyword left out — KerML var a : Integer; — is still reported rather than parsed; the words listed under F8 of pilot-differential.md stay reserved)
Binding connector ends (binding [1] bind [0..*] a.b = [0..*] c) parser/defusage.go parseUsage UsageBinding libs/reserved_keyword_name_test.go (the library's ShapeItems.sysml sites) ✅ Faithful (bind is the ends keyword, formerly read as the connector's name)
Named argument resolution document.go:205 (no name resolution) requirement_invocation_test.go ✅ Faithful
Control flow node registration builder.go InitialNode/FinalNode transition_first_test.go ✅ Faithful
Redefinition target lookup document.go:328 searchInheritedFeatureViaIndex localclock_test.go ✅ Faithful
References in behavioral bodies (calc return, constraint/assume/require, assignment, entry/do/exit, transition guard and effect) resolve/document.go resolveDecl model/behavior_body_resolve_test.go TestBehaviorBodyReferencesAreResolved ✅ Faithful
State def bodies are state bodies whatever their first member is parser/defusage.go DefState case (always parseStateBody) parse/state_def_region_pseudostate.golden (a state def whose first member is an attribute, followed by regions) ✅ Faithful
States a region declares with a body (state x { ... }) lower/state_graph.go collectRegionStates (memberships unwrapped, region and initial recorded) state_region_choice.sysml, region_pseudostate_test.go ✅ Faithful
Substate, region, and named-pseudostate declarations symbols/builder.go StateNode/StateRegion/PseudostateNode model/behavior_body_resolve_test.go TestBehaviorDeclarationsAreVisible ✅ Faithful
Region-scoped state names (sibling regions may reuse a name) symbols/builder.go StateRegion TestBehaviorDeclarationsAreVisible/sibling_regions_reuse_state_names ✅ Faithful
Requirement actor declaration symbols/builder.go (*ast.Usage of kind UsageActor) TestBehaviorDeclarationsAreVisible/requirement_actor_binding ✅ Faithful
Inherited member through a qualified-name segment (engine::'4cylEngine') resolve/qualified.go walkQualifiedsemantics.Model.LookupMember model/inherited_scope_resolve_test.go TestInheritedMembersAreVisible ✅ Faithful
Redefinition target that the redefinition shadows (part redefines engine) semantics/model.go inheritedFeature TestInheritedMembersAreVisible/nested_redefinition, TestRedefinitionDoesNotShadowItsTarget ✅ Faithful
Loop body as a namespace (loop { action a; } until a.x, for x in c { ... }) symbols/builder.go WhileLoopActionNode (including a for loop's iteration variable), resolve/document.go, passes/typecheck.go, resolve/references.go; at execution runtime/action_statements.go stmtEnv (a frame per entered block) TestBodyLocalDeclarationsAreVisible, TestBodyLocalNamesDoNotEscape, runtime/robustness_test.go:loop_body_declaration_does_not_leak ✅ Faithful
Body-expression parameters (c->forAll { in i : Positive; f(i) }) symbols/bodyscopes.go buildBodyScopes (scope linked into the document tree), read back by symbols.BodyExprScope in resolve/document.go and resolve/references.go TestBodyLocalDeclarationsAreVisible/body_expression_parameter, lsp TestRenameLeavesBodyExpressionParameters, TestRenameBodyExpressionParameterFromDeclaration, TestDefinitionBodyExpressionParameter ✅ Faithful
Features of the stdlib base type of an untyped usage (state normal;States::StateAction::done) semantics/implicit.go implicitUsageBases, Model.implicitBase via semantics/model.go DirectSupertypes model/implicit_typing_test.go TestImplicitUsageBaseTypes, TestInheritedMembersResolveThroughUntypedUsage, semantics/implicit_test.go, lsp/implicit_typing_test.go ⚠️ Approximate (the implicit base is the stdlib base definition of the usage kind, not the base feature it subsets, since library index records carry no specialization edges; connector/succession/flow/binding/satisfy/subject/objective usages take their type from what they relate to and get no base)
Implicit redefinition of behavior/step parameters by position (out item image; in action focus : Focus redefines Focus::image), KerML 7.4.7.2/7.4.7.3, SysML v2 7.17.2 semantics/redefinition.go Model.implicitParameterRedefinitions, Model.parametersOf, reached from semantics/model.go DirectSupertypes semantics/redefinition_test.go, model/implicit_typing_test.go TestParameterRedefinitionAccompaniesTheImplicitBase, TestImplicitRedefinitionSuppliesInheritedMembers, passes/typecheck_expr_test.go TestExprRedeclaredParametersMatchByPositionNotName (the invocation signature matches by the same rule) ✅ Faithful (owned parameters in lexical order redefine the parameter at the same position of each general behavior or step, matching direction; parameters a single general behavior leaves un-redefined are inherited after the owned ones; the kind's standard library base still applies alongside the redefinition, since the redefined parameter may itself be untyped)
Implicit redefinition of a result parameter (return redefines the general calculation's result whatever its position), SysML v2 7.19.2 semantics/redefinition.go Model.implicitParameterRedefinitions (ast.Usage.IsResult, set by parser/behavior.go parseResultMember) semantics/redefinition_test.go TestImplicitResultParameterRedefinition ✅ Faithful
A nested usage that is not a parameter and shares a name with an inherited feature resolve/document.go Resolver.checkInheritedNames, conflictable, parameterizedByName, surfaced by passes/nameres.go (code name-conflict); the usage still only gets the standard library base of its kind from semantics/implicit.go passes/nameres_test.go TestNameResolutionPassReportsInheritedNameConflict, TestRedeclaredInheritedNameIsNoConflictWhenRedefined, TestInheritedNameConflictExemptsRedefiningFeatures, TestDistinctNestedNameIsNoConflict, model/implicit_typing_test.go TestLikeNamedUsageIsNotAnImplicitRedefinition, resolve/inherited_names_test.go TestRedeclaringAnInheritedNameConflictsUnlessItRedefines, TestRequirementParametersAreNotNameConflicts, TestRedefinitionTargetFoundTwoSupertypesUp (the target is searched up the whole specialization chain, not only the direct supertype) ⚠️ Approximate (SysML v2 7.6.1 and KerML 7.3.2.1 make this a name conflict, reported at the name-resolution tier where inheritance is known; a feature that redefines what it shares the name with — explicitly, or implicitly by parameter position — does not conflict. The subject, actors and stakeholders of a case or requirement redefine the inherited ones by name (SysML v2 7.18.4, 7.19.4), which is not modelled and is not distinguishable from an ordinary feature at this tier, so the rule is not applied inside a case or requirement body at all (a concern and a viewpoint are requirements too) — a genuine conflict on an ordinary feature there goes unreported too)
Effective name of an unnamed redefining feature (in item; in action shoot : Shoot is named image), KerML 7.3.4.5, SysML v2 7.6.5 symbols/builder.go effectiveIdent for a declared redefinition; for an implicit one resolve/unqualified.go Resolver.implicitlyNamedMember (with impliesNamingFeature), reached from walkUnqualifiedHiding and resolve/target.go memberChain, over symbols/scope.go Scope.AnonymousMembers and semantics/model.go DirectSupertypes passes/nameres_test.go TestImplicitlyRedefiningParameterBindsRedefinedName, TestImplicitlyRedefiningParameterDoesNotBindItsKeyword, symbols/builder_test.go TestUnnamedRedefinitionTakesRedefinedName ✅ Faithful (the name is bound in the owning scope, so a sibling resolves it by simple name; an implicit redefinition's target is known only to the semantic model, so that binding is resolved lazily rather than when scopes are built)
Implicit redefinition of connection/association ends by position (connection : PressureSeat connect bead references t.bead to ... redefines PressureSeat::bead), SysML v2 7.13.2, 7.14.2, KerML 7.4.6 semantics/connector.go Model.implicitEndRedefinitions, Model.endsOf, reached from semantics/model.go DirectSupertypes; the ends themselves are declared by symbols/builder.go buildConnectorEnds (ast.ConnectorEnd.DeclaredName) and the arity check is passes/constraint.go checkConnectorEndRedefinition (Model.UnmatchedConnectorEnds) semantics/connector_test.go (including TestImplicitEndRedefinitionOfAssociationUsage), model/connector_ends_test.go TestConnectorEndNamesResolve, TestConnectorEndArityMismatch ✅ Faithful (an end of a connect clause that reference-subsets what it attaches to declares an end of the connector, in lexical order, and redefines the end at the same position of each connector the usage specializes; positions count every end of the clause, including one that only names what it attaches to; an explicit :>> governs, and an end past the general connector's last position is reported. Connection, interface, allocation, flow, succession, association and binding declarations are matched — a general whose own ends are not enumerable, such as an unparsed library type, suppresses the arity check rather than reporting)
Reference subsetting contributes members (perform action takePhoto references takePicture;, perform providePower.generateTorque;) semantics/reference.go Model.ReferencedFeature, Model.MemberSources, consumed by semantics/members.go MembersOf/LookupMember; targets resolved by resolve/target.go ResolveTarget/ResolveReferenceTarget semantics/reference_test.go, resolve/target_test.go, model/perform_reference_test.go, parse/perform_reference.golden, runtime/testdata/conformance/action_perform_reference.sysml, runtime/robustness_test.go (perform_of_missing_action, perform_reference_cycle) ✅ Faithful (a member-contribution relation, deliberately not a generalization — see below)
Effective name of an unnamed feature that reference-subsets (perform providePower.generateTorque; declares generateTorque) or redefines (part :>> engine;, equivalently part redefines engine;, declares engine) ast/namespace.go NamingFeature, EffectiveName, TargetName, used by symbols/builder.go effectiveIdent, namingTargetNode, lower, runtime and passes; the reference that named a feature is hidden from its own resolution by symbols/symbol.go Symbol.NamingTarget and resolve/target.go refFilter symbols/perform_test.go, symbols/builder_test.go TestUnnamedRedefinitionTakesRedefinedName, TestRedefinitionDoesNotOverrideDeclaredName, TestReferenceSubsettingOutranksRedefinitionAsNamingFeature, TestTwoRedefinitionsLeaveFeatureAnonymous, TestShortNameLeavesTheNamingFeatureInPlace, resolve/document_test.go TestRedefinitionTargetSkipsTheNameItGaveAway, model/perform_reference_test.go ✅ Faithful (a reference subsetting names the feature, and otherwise its single owned redefinition does; a declared name governs, and more than one redefinition leaves the feature anonymous. A declared short name does not suppress the derived name, since KerML derives effectiveName from declaredName alone. The naming feature's own effective name is approximated by the reference's last segment, since resolution has not run when scopes are built. A value on a member left anonymous by more than one redefinition reaches none of them, which passes/constraint.go checkUnnamedRedefinitionValue reports as a warning, code redefinition-no-derived-name, tested by passes/constraint_test.go TestConstraintUnnamedRedefinitionValue)
A reference subsetting resolves outside the name it contributes, while the members its owner inherits and imports stay visible (part v : V { perform 'provide power'; }) resolve/target.go refFilter, Resolver.ResolveReferenceTarget, threaded through resolve/unqualified.go walkUnqualifiedHiding and applied in resolve/document.go resolveRelationships, resolve/references.go refCollector.relationships (via model.Workspace.ResolveReferenceInDoc) and runtime/invoke_action.go resolveActionSymbol; the inherited half is semantics/members.go Model.LookupContributedMember resolve/target_test.go TestReferenceTargetSkipsSelfBinding, semantics/reference_test.go TestPerformOfInheritedAction, lsp/definition_test.go TestDefinitionPerformChainMember (a chain member resolves through its operand, resolve.Reference.Chain), semantics/reference_test.go TestReferenceFindsSiblingDeclaredAfterIt, model/perform_reference_test.go (perform shadowing the action it performs), lsp/definition_test.go TestDefinitionPerformReference, runtime TestPerformShorthandRunsTheReferencedAction, conformance/action_perform_shorthand.sysml ✅ Faithful
The perform X; shorthand is an action node named X lower/action_graph.go getNodeName conformance/action_perform_shorthand.sysml (then start increment; names the perform statement) ✅ Faithful
N-ary connector ends (connection link connect (a, b, c)), SysML v2 7.13.2, 8.3.13 parser/defusage.go parseConnectorEnds (parenthesized end list, reached by both the named declaration and the anonymous connect …; body member); passes/constraint.go checkConnectorEnds (arity by kind); lower/connection.go lowerConnections, PeerPorts parse/connection_nary.golden, parser/connector_ends_nary_test.go TestParseNaryConnectorEndsKeepsEveryEnd, parser/negative_test.go (nary_connect_unclosed, nary_connect_trailing_comma, nary_connect_empty), passes/constraint_test.go TestConstraintConnectionNaryEndCountReachesTheChecker, lower/connection_test.go TestLowerNaryConnectionKeepsEveryEnd and TestLowerAnonymousNaryConnectionKeepsEveryEnd, parser/connector_ends_nary_test.go TestParseAnonymousInlineConnectKeepsEveryEnd, conformance/action_port_communication_nary.sysml and action_port_communication_nary_anonymous.sysml ✅ Faithful (a connection, connector, interface or allocation keeps every end of a parenthesized list end to end — parse, constraint tier, lowering and port routing — whether or not it declares a name, and an interface or allocation beyond two ends is reported)
Anonymous binary allocation (allocate torqueGenerator to powerTrain) parser/defusage.go atAllocateShorthand parse/perform_reference.golden ✅ Faithful (both names are connector ends; formerly the first was read as the usage's name)
An object of a connector usage holds the features it connects at its ends (connection link : Link connect a.p to b.q makes link.source be a.p), KerML 7.4.6, SysML v2 7.13.2 runtime/connector.go materializeConnectorFeatureValue, materializeConnector, attachConnectorEnd, bindEndFeatureValue, bindParticipants, reached from runtime/instance.go GetFeatureValue; end features synthesized by runtime/shape.go connectorEndFeatures; attachments and effective end names by semantics/connector.go Model.ConnectorEndAttachments, Model.IsConnectorUsage; inherited ends aliased by runtime/subsetting.go over Model.ImplicitEndRedefinitions connector_test.go (TestConnectorEndsAreTheConnectedFeatures, TestWritingAConnectedPortIsReadThroughTheEnd, TestConnectorEndFollowsAFeatureChain, TestConnectorEndAttachesToAPart, TestNaryConnectorKeepsEveryEnd, TestRedefinedEndSharesTheInheritedFeatureValue, TestEveryConnectorKindAttachesItsEnds), conformance/connector_end_identity.sysml (identity assertions), semantics/connector_test.go:TestConnectorEndAttachments, robustness_test.go:unattachable_connector_end, multiplicity_on_a_connector, connector_attached_to_itself, mutually_attached_connectors ✅ Faithful (an end holds the very object the connector attaches to, so writing the connected port is read through the end and two connectors on different ports are distinguishable; ends are attached in declaration order, including n-ary and nested feature chains and an end attached to a part; an end that names no reachable feature is a typed ErrConnectorEnd with a source location rather than a fresh object or <unknown>, an end naming the connector it belongs to — directly or through another connector — is ErrCyclicFeatureValue, and a connector usage holding more than one connector is reported with where it was written)
An untyped or anonymous connector usage materializes on the standard library base of its kind (interface iface connect a.p to b.q;, connect a.p to b.q;), SysML v2 7.13.2, 8.3.13 semantics/implicit.go implicitUsageBases (Connections::Connection, Interfaces::Interface, Allocations::Allocation); runtime/connector.go connectorBaseOf, anonymousConnectors; symbols/builder.go usageSymbolKind (a KerML connector is a connection usage) and runtime/shape.go isFeature (an allocation usage is a feature) conformance/connector_end_identity.sysml, ballandchain_interface_connected.sysml, connector_test.go (TestUntypedConnectorUsageMaterializes, TestAnonymousConnectorJoinsItsEnds, TestAnonymousConnectorIsMaterializedOnce, TestAnonymousSuccessionIsNoConnector, TestEveryConnectorKindAttachesItsEnds), parse/connection_implicit_type.golden ✅ Faithful (a connection, interface, allocation or connector usage that names no definition is an object of its kind's library base with its ends attached, named form and anonymous form alike, and an anonymous one materializes once per object; a flow or binding states its ends by other syntax and is not a connect connector — its ends reach routing through lowering, not through connector-end feature values)
A flow usage (flow f from a.out to b.in) and a binding usage (binding b bind a.p = b.p) are connectors of the kernel layer, but state their ends in their own syntax — Usage.FlowEnds, and a binding's source/target relationships — rather than in a connect clause parser/defusage.go parseFlowEnds and the UsageBinding clause; resolve/document.go isImplicitCalcResult; lower/connection.go (flow ends reach routing through lowering); lower/binding.go ToBindings/lowerBinding; runtime/binding.go objectBindings/resolveBindingValue/resolveBindingSet/attemptBinding/resolveBindingLocation; runtime/instance.go materializeFeatureValue; runtime/invoke_calc.go resultBindingExpr; semantics/connector.go Model.IsConnectorUsage deliberately covers only the connect forms parse/connection_implicit_type.golden (flow f from a.p to b.p;, binding bnd bind a.p = b.p;), lower/connection_test.go, lower/binding_test.go (TestToBindingsKeepsMultipleContributors), runtime/testdata/conformance/binding_value_forward.sysml, binding_value_reverse.sysml, binding_nested_end.sysml, binding_nested_end_reverse.sysml, binding_expression_end.sysml, binding_multivalued.sysml, binding_calc_result.sysml, binding_calc_result_reverse.sysml, binding_object_end.sysml, runtime/robustness_test.go binding cases (including exact collection-conflict, multiple-contributor, element-budget and distinct-object cases) ⚠️ Approximate (a flow between action nodes carries its value through lowering; a binding declared in a materialized type/usage body is lowered to a bidirectional runtime value identity, including inherited and nested ends, expression-valued endpoints, exact sequence/set equality, multiplicity handling, element-budget charging during propagation, lazy adoption of an unmaterialized composite endpoint by the read-side object, conflict/cycle errors, and calc result binding. A named binding that states only one end — including binding bnd = x;, bind x;, and binding bnd of x; — lowers to no runtime binding, and bindings owned directly by packages/namespaces are not applied until namespace objects are materialized. Several bindings supplying unequal scalar values report a typed conflict; multiple bindings contributing to a multi-valued end are not yet element-wise merged and report a typed ErrBindingEnd instead of silently selecting one.)
The of clause of a binding (binding b of full = level) names the feature the binding binds, so it is a reference subsetting rather than a typing (KerML 8.3.3.3.9, SysML v2 8.3.13) parser/defusage.go (the UsageBinding of clause records ast.RelReferences) parser/binding_of_test.go:TestBindingOfTargetIsAReference, parse/constraint_parameterised_conditions.golden ✅ Faithful (formerly recorded as a typing, which reported the bound feature as "type must be a definition")
A declared name wins over an effective one in the same namespace (part v { perform p; action p; }) symbols/scope.go PreferDeclared, used by LookupLocal and resolve/qualified.go's segment walk; symbols/builder.go (Symbol.EffectiveName) semantics/reference_test.go TestReferenceFindsSiblingDeclaredAfterIt, TestQualifiedNameThroughEffectiveNameIsNotAmbiguous, TestRepeatedPerformResolvesToTheAction ✅ Faithful
individual def X :> PartDef, x : IndividualDef kind compatibility, SysML v2 7.9.4 passes/typecheck.go occurrenceDefSymbolKinds/isOccurrenceDefKind (specialization) and isCompatibleTyping (typing) passes/typecheck_individuals_test.go, corpus gate (Verification Case Usage Example now clean) ✅ Faithful (an individual def is an occurrence definition, so it may specialize an occurrence definition of any kind and may type a usage wherever an occurrence definition may; specializing a data type — an attribute or enumeration definition — stays an error per 8.4.5.1, and a usage kind that rejects an occurrence definition, such as a port usage, still rejects an individual definition)
individual / snapshot usage modifiers (individual testSystem : TestSystem, snapshot occurrence takeoff : Flight), SysML v2 7.9.4, abstract syntax 8.3.9.11 (OccurrenceUsage::isIndividual, OccurrenceUsage::portionKind) ast/defusage.go Usage.IsIndividual/Usage.IsSnapshot, stored by parser/defusage.go parseUsage and parser/behavior.go parseDirectionParameter; consulted by passes/typecheck.go declKind.isOccurrenceUsage, compatMessage and isCompatibleTyping parser/occurrence_modifier_test.go, parse/occurrence_individual_snapshot.golden, parser/negative_test.go (individual_modifier_no_member, individual_usage_no_type, individual_usage_no_body, snapshot_usage_no_type, individual_parameter_no_type), passes/typecheck_individuals_test.go TestTypeCheckOccurrenceModifierWidensTypingOK, TestTypeCheckOccurrenceModifierRejectsDataType, TestTypeCheckDataTypeTypingWithoutModifierOK ⚠️ Approximate (the modifier is orthogonal to the keyword that declares the usage, so individual part p is a part usage that is an individual, and either modifier makes the usage an occurrence usage: it may be typed by an occurrence definition of any kind and may not be typed by a data type — an attribute or enumeration definition — per 8.4.5.1. An individual occurrence takes Occurrences::Life as its implicit base (semantics/implicit.go implicitBase). The modifier is not yet reflected in the usage's symbol kind, so individual testSystem is still indexed as an attribute usage — the typing widening compensates)
if/else branch bodies as namespaces ast/behavior.go IfBranchNode (parsed by parser/behavior.go parseIfBranch), symbols/builder.go IfActionNode/IfBranchNode, resolve/document.go, symbols/bodyscopes.go, resolve/references.go TestBodyLocalDeclarationsAreVisible/if_branch_body_reads_its_own_declaration, /else_branch_reuses_the_then_branch's_name, TestBodyLocalNamesDoNotEscape/if_branch_member_from_outside, /else_branch_member_from_the_then_branch, parse/action_if_branch_body.golden, lsp/if_branch_test.go, resolve TestImportRecursiveSkipsBodyLocalNames, repl TestLookupInScopeTreeSkipsBodyLocalNames ✅ Faithful (each branch owns a body-local scope: names declared in a branch resolve inside it, do not escape to the enclosing behavior or to the sibling branch, and — like loop bodies — are excluded from recursive imports and the REPL scope-tree search; the condition is evaluated before either branch is entered, so it resolves in the enclosing scope only)
Transition source/target names resolve/transition.go (*Resolver).ResolveEndpoint, walked from resolve/document.go (TransitionMember) resolve/transition_test.go ⚠️ Approximate (resolved as references at the name-resolution tier, so a misspelled endpoint reports there with a suggestion and lowering consumes the resolved declaration; the leniencies are listed with the state machine row above — an unqualified endpoint falls back to the first vertex of the machine whose name path ends in it, and an endpoint naming no vertex leaves its edge out of the graph rather than failing the lowering)
Signal trigger names (when sigX) TestBehaviorDeclarationsAreVisible/signal_trigger ⚠️ Approximate (a bare trigger name is an injected event, not a declared element, so it is deliberately not resolved)
Payload feature a flow/message declares in its of clause (message m of fuelCommand : FuelCommand) parser/defusage.go parseFlowEnds (declaration recorded as FlowEnds.PayloadDecl and kept as a member of the flow), resolve/document.go (the of name resolves in the flow's own scope) parse/flow_payload_declaration.golden, model/flow_payload_resolve_test.go TestDeclaredFlowPayloadIsAMember, TestFlowPayloadReferenceStillResolvesOutward ✅ Faithful (the declared payload is a member of the message, so the of name and m.payload both resolve; the reference form of Type still resolves in the enclosing scope)
Accept-parameter visibility to sibling action nodes runtime/action_executor.go the action's shared feature space action_accept_message.sysml ⚠️ Approximate (the executor binds the payload into the action's shared feature space, which scoping does not model: a sibling node reading the parameter by simple name is reported unresolved)
Unqualified library names in files that do not import their library (Boolean, Real) — only the public top-level members of a root namespace are globally visible, and a library member is not one ([SysML, 7.2] over [KerML, 8.2.3.2, 8.2.3.5]) resolve/suggest.go unresolvedMessage (the diagnostic names the qualified spelling) and resolve/fixes.go importFix (the offered fix writes private import X::*;, an import that serves the importing namespace without re-exporting onward) model/suggestion_test.go, model/examples_test.go, resolve/fixes_test.go TestTheImportFixWritesAPrivateImport, lsp/codeaction_test.go TestCodeActionImportsResolvableFQN ✅ Faithful (such a name is genuinely unresolved: a model imports the library or qualifies the name, as OMG's own training files do; every surface says which qualified name was meant and offers both spellings)
A member chain from that reads the object featuring the value being written, not the library declaration's own members ([SysML, 7.2] over [KerML, 8.4.2]) resolve/document.go featuringOf (a chain whose operand is Base::things::that resolves its members against the usage enclosing the expression, so both the usage's own members and those it inherits are reached through lookupMember), runtime/eval.go evalName (that evaluates to the object being evaluated) model/that_test.go TestThatChainsThroughTheFeaturingType, runtime/that_test.go TestThatReadsTheFeaturingObject, TestThatReadsTheInnermostFeaturingObject ✅ Faithful (that.a resolves and evaluates through the featuring object, the innermost enclosing usage wins, an unowned member is unresolved member, and a that written where no usage encloses it stays unresolved rather than resolving to the library feature)
A namespace re-exports what it imports with import X::*, transitively and wherever the name X resolves (KerML::Element, where KerML imports Kernel::*, which imports Core::*, which imports Root::*; KerML 7.2.5, 8.2.3.5) symbols/index.go ExpandWildcardImports (repeats expandRound to a fixpoint over the importers in name order, deriving the re-exports of the ones a change reached and dropping those its imports no longer support) and resolveWildcardTarget (searches the importing package's enclosing namespaces before the global one) symbols/index_test.go TestExpandWildcardImportsChainsAndIsOrderIndependent, TestExpandWildcardImportsPrefersTheEnclosingTarget, TestExpandWildcardImportsFollowsAReexportedTarget, TestExpandWildcardImportsIgnoresAnAmbiguousTarget, libs/loader_cache_test.go TestParsedAndRestoredIndexesAreEquivalent, model/training_examples_test.go TestTrainingExamplesCacheStateIndependent ✅ Faithful (a chain of imports is followed to its end, and the result does not depend on iteration order or on whether the library was parsed or restored from the on-disk index cache; a target name resolves against the importing namespace's own imported memberships — wildcardTargetAt follows a name an earlier import re-exported to the FQN it was declared under — before the global namespace)
The names a document's own root-level import X::* surfaces are visible in the scope tree that document builds for the editor, whether or not it declares anything else — a bare import at the REPL prompt included symbols/members.go SetDocName / DocNameOf (the tree carries the document name itself, no member being needed to hold it), read by resolve/filter.go documentOf resolve/imports_test.go TestRootImportInDocumentDeclaringNothingElse ✅ Faithful
A membership import surfaces the imported member under both of its names — the declared name and the short name, so import SI::kilogram makes kilogram and kg resolve — and under no other member's short name: what the import adds is the target's Membership, which carries a memberName and a memberShortName (KerML 8.2.4) resolve/unqualified.go matchImport (the target is found by member lookup, which knows both names, instead of by comparing the written last name part with the name being resolved) with importPrefixAvailable, which keeps that wider match from re-entering an import whose own prefix has no binding to resolve through resolve/imports_test.go TestMembershipImportAcceptsDeclaredAndShortNames (kg resolves; the sibling <g> gram the import does not name stays unresolvable), TestImportMembership, TestImportDoesNotLeakNonImported ✅ Faithful
An import carries a visibility indicator: the pinned OMG pilot grammars make it mandatory (fragment ImportPrefix : visibility = VisibilityIndicator 'import' ..., KerML.xtext:169-172, SysML.xtext:241-244, with no ? unlike the sibling MemberPrefix), and all 574 imports in the 254 OMG-authored corpus files carry one passes/import_visibility.go ImportVisibilityPass at LevelSyntax, code syntax/import-visibility, span on the import keyword; no parser or AST change — ast.Import.Visibility already records VisibilityDefault for the bare form passes/import_visibility_test.go, testdata/passes/import_no_visibility.sysml + golden, lsp/diagnostics_test.go TestPublishDiagnosticsReportsBareImportAsWarning ⚠️ Approximate (reported as a warning, not an error: the bare form is unambiguous to parse, so hard-failing would reject existing models over notation. It does not gate the name-resolution, type or constraint tiers, and an expose is exempt — the pilot grammar gives it implicit protected visibility, SysML.xtext:2366-2372)
A namespace declaration is KerML notation: namespace is a literal in KerML.xtext only (:125, NamespaceDeclaration), and a SysML file's root is RootNamespace : PackageBodyElement* (SysML.xtext:38), which admits no namespace declaration passes/nonstandard_notation.go NonstandardNotationPass at LevelSyntax, code kerml-notation, span on the namespace keyword; source/kind.go Kind/KindOf tells a .sysml file from a .kerml one passes/nonstandard_notation_test.go TestNamespaceInSysMLIsKerMLNotation, TestNamespaceInKerMLIsSilent, TestNamespaceInAnUnnamedDocumentIsKerMLNotation, repl/notation_test.go ⚠️ Approximate (reported as a warning, not an error, and not in .kerml: both namespace N; and namespace N { … } are legal KerML, so the construct stays parsed and silent there. The REPL and CLI buffer is one document of no file kind, so it takes the SysML reading its prompt takes, and repl/session.go dropKerMLNotationOfKerMLFiles drops the finding for a snippet loaded from a .kerml file)
Notation OpenSysML accepts that no pinned production admits — the state pseudostates (choice, junction, the history forms, entry/exit point), region, defer, the initial/final state markers, the initial/final/decision action-node spellings, and transition <src> to <tgt> — is diagnosed rather than silently accepted; the audit with citations is reference/grammar/conformance-audit.md passes/nonstandard_notation.go NonstandardNotationPass at LevelSyntax, code nonstandard-notation, span on the word; ast.InitialNode/FinalNode/DecisionNode/PseudostateNode Keyword and ast.TransitionMember.ToSpan record which spelling was written passes/nonstandard_notation_test.go TestStateExtensionsAreReported, TestActionNodeExtensionSpellingsAreReported, TestTransitionToSpellingIsReported, TestStandardNotationIsSilent, libs/nonstandard_notation_test.go TestStdlibHasNoNonstandardNotation ⚠️ Approximate (a warning, never an error: the notation is a documented OpenSysML extension existing models use, so it keeps parsing and does not gate a higher tier. done, fork and join are silent — see the audit's judgment calls)
A private import X::* is not re-exported by its namespace, and the names it brings in are visible only within that namespace (KerML 8.2.3.3) symbols/index.go applyReexportMarks / exportedChildren (a re-export is hidden while every document that surfaced it did so with a private import, and left out when that namespace is itself wildcard-imported) and LookupQualifiedFrom (a hidden name answers a lookup only when the referring namespace is the one that hid it, or is nested in it); resolve/qualified.go referringNamespaceFQN supplies that context for a qualified reference and HiddenFrom stops its member-lookup fallback, which reaches a cached symbol's children without consulting the marks, from resurfacing a hidden name; resolve/alias.go resolveCachedAliasTarget supplies the context for the target of a cached alias; resolve/unqualified.go matchImport enumerates a wildcard import's target through symbols/index.go LookupDirectChildrenFrom, which reads the same marks from the referring namespace unless the import is import all symbols/index_test.go TestExpandWildcardImportsDoesNotCarryOnAPrivateImport, TestLookupQualifiedFromSeesAPrivateImportOnlyFromWithin, TestHiddenFromReportsOnlyPrivatelySurfacedNames, TestLookupQualifiedReachesAPubliclyImportedName, TestLookupQualifiedAcrossAChainedPrivateImport, TestLookupDirectChildrenFromDropsPrivatelyImportedNames; resolve/qualified_test.go TestResolveQualifiedRejectsAPrivatelyImportedName, TestResolveQualifiedRejectsAPrivatelyImportedNameThroughMemberLookup, TestResolveQualifiedFromInsideAnUnnamedElement, TestResolveQualifiedReachesAPubliclyImportedName; resolve/visibility_test.go TestNamespaceImportSkipsAPrivatelyImportedName, TestNamespaceImportSkipsAPrivatelyImportedCachedName; resolve/alias_test.go TestAliasResolvesAPrivatelyImportedTargetFromCache, TestAliasResolvesAPrivatelyImportedTargetWhenParsed; model/visibility_reach_test.go TestPrivateWildcardImportIsNotReExportedAcrossDocuments ✅ Faithful (neither a qualified nor an unqualified reference reaches a privately imported name from outside the importing namespace, whether the index was parsed or restored from cache; an import all still takes the target's private memberships, and a reference inside the importing namespace still sees them)
A root-level import X::* surfaces its names in the importing document's own root namespace, so they are not names of another document's root (KerML 8.2.3.3, 8.2.4) symbols/index.go ReexportVisible (a root-level re-export answers a lookup only in a document holding a claim on it; a name under a namespace is visible wherever that namespace is), read by resolve/filter.go Resolver.admitsUnderName on the whole-index routes for a top-level name symbols/index_test.go TestARootReexportIsVisibleOnlyInItsOwnDocument; resolve/filter_test.go TestARootImportSurfacesNamesInItsOwnDocumentOnly; model/that_test.go TestRootImportServesItsOwnDocumentOnly ✅ Faithful (a filter on a root-level import therefore hides what it rejects from every document, and the importing document keeps what it admits; the importing document keeps them at every visibility, private included — LookupQualifiedFrom leaves the private-import hiding of a root-level name to ReexportVisible, which decides it per document, rather than hiding it from the document that wrote the import)
Visibility of the members a recursive import surfaces (import X::**, KerML 7.2.5) resolve/unqualified.go matchImport (both the membership and namespace branches filter through resolve/visibility.go visibleThroughImport) resolve/visibility_test.go TestRecursiveMembershipImportSkipsPrivate, TestNamespaceImportSkipsPrivate, TestImportAllReExportsPrivate ✅ Faithful (a recursive membership import hides private members of the subtree it walks unless it is import all)
expose in a view body is an Import (SysML v2 8.3.26.2 Expose, 8.3.26.3 MembershipExpose, 8.3.26.4 NamespaceExpose) parser/defusage.go (expose shares parser/namespace.go parseImportTail, so ::* yields a NamespaceExpose and ::** a recursive MembershipExpose; ast.Import.IsExpose, IsAll, protected Visibility) parser/expose_test.go TestParseExposeImportKind, TestParseExposeIsImportAllAndProtected, resolve/expose_test.go ⚠️ Approximate (an Expose always imports all elements regardless of visibility — validateExposeIsImportAll — so its exposed elements resolve inside the view body, in views that specialize it, and not outside; validateExposeOwningNamespace is implemented — see the row below)
Protected import visible in specializations of the importing definition or usage (SysML v2 7.5.3) resolve/visibility.go inheritedThroughSpecialization (a protected or public import reaches specializations, a private one does not — KerML 8.2.3.3) and lookupInheritedImports (walks semantics.Model.DirectSupertypes upward from the referring scope's owner, breadth-first and cycle-guarded, matching each supertype's inherited imports through the same matchImport); resolve/unqualified.go walkUnqualifiedHiding consults it after the imports declared in the scope itself resolve/protected_test.go TestProtectedImportReachesADirectSpecialization, TestProtectedImportReachesATransitiveSpecialization, TestProtectedImportReachesAUsageTypedByTheImporter, TestProtectedImportDoesNotReachAnUnrelatedNamespace, TestPrivateImportDoesNotReachASpecialization, TestProtectedImportAllReachesASpecializationWithPrivateMembers, TestExposeReachesASpecializingView, TestInheritedImportWalkTerminatesOnASpecializationCycle; model/visibility_reach_test.go TestProtectedImportReachesSpecializationsAcrossDocuments, TestProtectedImportDoesNotReachAnUnrelatedDocument, TestExposeReachesASpecializingViewAcrossDocuments ✅ Faithful (an expose is protected, so it reaches a specializing view the same way; a feature typing is a generalization edge — KerML 8.3.4.6 — so an import declared in a definition is also reached from a usage typed by it, and an unrelated namespace sees nothing)
A protected import is re-exported only to what specializes the importing definition or usage: under part def Base { protected import Lib::Pub; }, part def Sub :> Base { public import Base::*; } reaches Pub and an unrelated part def Other { public import Base::*; } does not (SysML v2 7.5.3, KerML 8.2.3.3) resolve/unqualified.go importVisibleFrom (a protected import answers a lookup through another namespace's re-export only when the referring scope's owner specializes the namespace that declared it) over resolve/visibility.go specializes / specializationChain, one breadth-first cycle-guarded walk of semantics.Model.DirectSupertypes shared with lookupInheritedImports resolve/protected_test.go TestProtectedImportReexportFollowsSpecialization, TestProtectedImportDoesNotReachAnUnrelatedNamespace, TestProtectedImportReachesATransitiveSpecialization ✅ Faithful (a re-export widens who may reach a protected import, never what it imports: the visibility test is applied at the referring scope, so the same name is answered for a specialization and refused for a sibling)
validateExposeOwningNamespace — the importOwningNamespace of an Expose must be a ViewUsage (SysML v2 8.3.26.2) passes/expose.go checkExposeOwners / exposeOwnerDiagnostic, run by passes/constraint.go ConstraintPass at LevelConstraint; code expose-owning-namespace passes/expose_test.go TestExposeOwningNamespace, model/expose_owner_test.go TestExposeOwnerAcrossDocuments ✅ Faithful (usage-only reading, per maintainer decision: an expose owned by a view usage is legal, one in a view def body is a warning since OpenSysML resolves it — resolve/expose_test.go TestExposeInViewDefinitionBody — and any other owner is an error; a package or namespace body rejects expose in the parser)
A namespace's filter restricts the imported memberships it re-exports (package P { public import Q::*; filter @Safety; }, KerML 8.2.4, SysML v2 7.4.4) symbols/filter.go NamespaceFiltersIn and symbols/index.go reexportGated (each way a name is re-exported records the conditions along it as one route; a name with no route is ungated) — the index records the gate and never evaluates it; resolve/filter.go Resolver.admitsUnderName evaluates a candidate against the routes through semantics.Model.SatisfiesElementFilter, and resolve/unqualified.go matchImport, resolve/qualified.go and symbols/index.go LookupDirectChildrenFrom share that one admission test; a filter member of a definition or usage body — a view narrowing what its expose lines surface — reaches the same test through resolve/filter.go Resolver.importAdmits, which composes symbols.NamespaceFiltersIn(scope) with the import's own clause; a filter at a document's root gates that document's root-level imports alone, since each document owns its root namespace (symbols/filter.go namespaceFiltersGating, keyed per document by symbols/index.go gateKeyOf) symbols/index_test.go TestExpandWildcardImportsGatesAFilteredReexport, TestExpandWildcardImportsKeepsAnUnfilteredRoute, TestExpandWildcardImportsRecordsAGateOnce; resolve/filter_test.go; model/element_filter_test.go (including TestFilterMemberOfADefinitionBodyRestrictsItsImports, TestFilterConditionResolvesThroughTheImportsItFilters, TestARootFilterRestrictsOnlyItsOwnDocument), symbols/index_test.go TestRootNamespaceFiltersGateOnlyTheirOwnDocument; libs/loader_cache_test.go TestFilteredImportsSurviveCacheRestore ✅ Faithful (a filter restricts only what the namespace re-exports, per maintainer decision: its own directly declared members are never hidden, only the condition's own names resolve unfiltered — otherwise a condition could not name a metadata type the namespace itself imports, since the condition's own names would be filtered by the condition (resolve.Resolver.InCondition) — and a name reached by an unfiltered route as well as a filtered one stays visible: the routes are alternatives, and the conditions along one route are a conjunction)
An import or expose filter restricts what that import brings in (import P::*[@Safety];, expose vehicle::**[@Safety];, SysML v2 7.4.4, 8.3.26) ast.Import.FilterExpr reaches the index through symbols/index.go wildcardImport.filter, gating the memberships that import surfaces; the same admitsUnderName decides them, so the qualified and the unqualified route agree, including the whole-index fallback for a top-level name (resolve/qualified.go lookupGlobalTop, which a root-level filtered import would otherwise bypass); the condition's own names are resolved as references like a filter member's, so a typo in the clause is an unresolved reference and editor navigation over it works (resolve/document.go, resolve/references.go) resolve/filter_test.go, model/element_filter_test.go (a filtered expose in a view surfaces a strict subset, an element the condition rejects is unresolvable by either route, TestAFileLevelFilteredImportHidesWhatItRejects, TestAnUnresolvedNameInAnImportFilterIsReported), parse/element_filters.golden ✅ Faithful (an import's condition and the filters of the namespace it imports compose: the intersection is what the importer sees)
A view's exposed elements are queryable (SysML v2 7.24 Views and Viewpoints, 8.3.26 Expose) semantics/expose.go Model.ExposedElements (a view's own expose relationships, then those of the views it specializes since an Expose is protected, in declaration order and once each) and Model.NestedViews (the views in its body, to walk a view tree), enumerated by resolve/filter.go Resolver.ImportedElements — the same admission, visibility and filter gating a lookup through that import makes, so the exposed set is what the view body actually resolves semantics/expose_test.go (TestExposedElementsNamespaceWildcard, TestExposedElementsRecursiveExpose, TestExposedElementsWithAnElementFilter, TestExposedElementsWithAViewBodyFilter, TestExposedElementsOfNestedViews, TestExposedElementsExposingAnotherView, TestExposedElementsInheritedFromAViewDefinition, TestExposedElementsOfAViewExposingNothing, TestExposedElementsOfANonView) ✅ Faithful (an empty exposed set is no error; asking a non-view is semantics.ErrNotAView. The REPL surface is %viewrepl/view.go doView)
A view's exposed set is rendered as the rendering its render member states, and as a containment tree where it states none (SysML v2 7.24 Views and Viewpoints, §10.2 — the rendering is tool-defined) semantics/rendering.go Model.ViewRenderings (the render members of the view and of the views it specializes) and Model.RenderingTarget (the rendering the member references or declares); view/view.go Renderer.KindOf, Renderer.Render (the kind, then the exposed set from Model.ExposedElements), view/tree.go, view/interconnection.go (the model's own connector and flow ends, not source text), view/behavior.go (the lowered lower.StateGraph/lower.ActionGraph, never a re-parse of symbol.Decl), view/table.go (the exposed elements, the elements declared in them and the nested views, as rows), view/text.go, view/mermaid.go and view/markdown.go (the forms, chosen per kind by Kind.MachineForm in view/form.go) view/render_test.go TestGoldenRenderings (text and machine-readable goldens for tree, interconnection, state, action, a filtered view and a table, from .sysml fixtures), TestTreeRenderingShowsNestedViewsAndDefaults, TestInterconnectionRenderingDrawsConnections, TestStateRenderingComesFromTheLoweredGraph, TestActionRenderingComesFromTheLoweredGraph, TestRenderingUsesFilteredAndInheritedExposure, TestTableRenderingRows, TestTableFormsAreMarkdownNotMermaid, TestMermaidLabelsAreEscaped ✅ Faithful to the notation, tool-defined in output (the kinds produced are a tree, an interconnection diagram, a state machine, an action flow and a table; state and action renderings read the graphs the runtime executes, so a rendering cannot drift from what runs. Mermaid is the machine-readable form of the graph-shaped kinds and Markdown that of a table; SysML §10.2 specifies no artifact)
A rendering this build does not produce, a name that is no view, a view exposing nothing, and an exposed element a rendering cannot represent are each explicit view/view.go UnsupportedKindError (wrapping view.ErrUnsupportedKind, naming the kind, the view and the rendering it stated), Renderer.Render (semantics.ErrNotAView for a non-view, as %view answers), Rendering.Empty and view/text.go (an empty artifact saying whether the view exposes nothing or nothing exposed was representable), Rendering.Notices (what a kind could not draw) view/render_test.go TestUnsupportedRenderingKinds, TestRenderingSomethingThatIsNoView, TestRenderingAViewExposingNothing, TestRenderingReportsWhatItCannotRepresent; repl/view_render_test.go TestRenderOfAnUnsupportedKindNamesIt, TestRenderOfANonViewIsTyped, TestRenderOfAViewExposingNothingSaysSo; cmd/sysml/render_test.go TestRenderReportsWhatItCouldNotDo ✅ Faithful (a stated kind that is not produced — sequence, geometry, textual — is a typed error naming it, never a substituted rendering; a form the kind is not written in is a WrongFormError naming the one it is; an element that cannot be drawn is reported, not dropped)
%render <name> [text\|mermaid\|markdown] renders a view at the prompt, and sysml -render <view> [-render-form <form>] [-o file] outside it repl/view.go Session.ViewRendering, renderLines, viewRenderer (a semantic model and resolver of its own over the session's symbol index, so no runtime is built); repl/meta.go (the %render arm, help entry), repl/complete.go (the form completion); cmd/sysml/render.go runRender, reportRenderNotices, writeArtifact (artifact on stdout or -o, every notice on stderr), cmd/sysml/main.go (mutually exclusive with -convert and with the check flags) repl/view_render_test.go TestRenderDefaultsToATree, TestRenderWritesMermaidWhenAskedFor, TestRenderOfATabularView, TestRenderOfAnUnknownNameReports, TestRenderMisuseShowsUsage, TestRenderIsInHelpAndCompletion, TestRenderBetweenStepsDisturbsNothing; cmd/sysml/render_test.go TestRenderWritesTheArtifactOnStdout, TestRenderTextFormAndOutputFile, TestRenderOfATabularView ✅ Faithful (%render is a read: it materializes nothing, adds no object to the session, leaves the submission buffer alone and leaves an %action/%state debugging session stepping the same graph and objects — pinned by TestRenderBetweenStepsDisturbsNothing. %view's report is unchanged)
A view's conformance to the viewpoints it satisfies is evaluated (SysML v2 7.24 Views and Viewpoints; a concern is a requirement, so its conditions are evaluated by the requirement engine) semantics/conformance.go Model.ViewConformance (per satisfy claiming conformance — IsViewpointSatisfy, since a satisfy stating a subject asserts its requirement of that subject instead, as the stdlib View does: Model.SatisfyTarget, then every concern Model.FramedConcernsOf reports for the viewpoint, matched against the view's own, inherited and nested framings, and evaluated against the exposed elements the concern's subject admits through the ConcernEvaluator the caller supplies — implemented in repl/view.go concernEvaluator over runtime.Context.CheckSatisfactionOn, so condition evaluation is not reimplemented); passes/constraint.go checkViewSatisfyTarget (code view-satisfy-viewpoint) semantics/conformance_test.go (framing direct, inherited, nested and inherited-from-a-viewpoint; a missing concern; a false condition; an unevaluable one; no admissible subject; an unresolved stakeholder; a satisfy that is no viewpoint; determinism), passes/constraint_test.go TestConstraintViewSatisfyNonViewpointRequirement, TestConstraintViewSatisfyViewpointOK, TestConstraintSatisfyOutsideAViewIsNotChecked, TestConstraintViewSatisfyRequirementBySubjectOK, conformance/viewpoint_framed_concern_conditions.sysml, viewpoint_concern_without_condition.sysml ⚠️ Approximate (the structural question — is every framed concern framed by the view — and the conditions are evaluated faithfully; the verdict rules are tool-defined, since SysML v2 leaves verification verdicts non-normative: a concern must hold of every exposed element its subject admits, a concern framed by a nested view counts for its container, and what cannot be evaluated — no condition, no admissible subject, an unresolved party, a framing whose concern reference does not resolve, a viewpoint framing no concern at all — is reported as unevaluable with a reason, never as a pass)
A filter condition is a model-level predicate over one candidate element, with the candidate as the implicit self (@Safety, @@Safety, and/or/xor/not/implies, and a comparison of an annotation's feature) semantics/filter.go Model.CompileElementFilter / EvalElementFilter / SatisfiesElementFilter (compiled once per condition, memoized per candidate) over the annotations semantics/annotations.go collects — prefix metadata (#Safety part def P;), a metadata member of the element's body, and metadata m about X; — with conformance through Model.AllSupertypes, so @Safety matches a metadata type specializing Safety, and a metaclass classification (@SysML::PartUsage) reads the candidate's own kind semantics/filter_test.go, model/element_filter_test.go, semantics/cached_library_test.go TestNamespaceFilterOverALibraryIsTheSameParsedAndRestored (the condition as parsed and the same condition compiled into an index-cache record classify alike), TestAnnotationFactsOfALibraryElementSurviveTheCache ⚠️ Approximate (evaluated against a symbol, not a value: there is no instance at name-resolution time. The subset above is what the corpus writes; a condition outside it is reported, not guessed — see the row below. @/@@ in the runtime value evaluator remains unimplemented, line 214)
A filter condition that is not boolean-valued, or that OpenSysML cannot evaluate passes/filter.go ElementFilterPass at LevelType, over semantics/filter.go Model.CheckElementFilter; codes filter-not-boolean (error — a condition that is not a predicate can select nothing) and filter-not-evaluable (warning) passes/filter_test.go TestFilterNotBooleanIsReported, TestFilterNotEvaluableIsReported, TestFilterConditionsInTheSupportedSubsetAreClean ✅ Faithful (an unevaluable condition is reported and not applied, so the elements it would have selected from all stay visible: a verdict OpenSysML could not reach never silently hides model content — maintainer decision, recorded here because the spec does not say). A feature bound to nothing is not that case: reading it yields the empty sequence, which an ordering propagates and ==/!= decide against any value (KerML DataFunctions, where an ordering takes DataValue[1] and equality [0..1]), so @Safety and Safety::isMandatory == true does not hold of an element annotated @Safety alone and does not surface it — a verdict, not a reported failure. An annotation inherits the values its metadata type declares, so a default the type gives the feature is what the comparison reads (semantics/annotations.go addTypeDefaults; semantics/filter_test.go TestFilterUnsetAnnotationFeature, TestFilterAnnotationFeatureDefault, model/element_filter_test.go TestFilteredImportRejectsAnUnboundAnnotationFeature)
A KerML declaration specializes the library type its keyword implies (classOccurrences::Occurrence, structObjects::Object, assoc/associationLinks::Link, behaviorPerformances::Performance, function/predicate → the matching evaluation, interactionTransfers::Transfer, metaclassMetaobjects::Metaobject, datatypeBase::DataValue, classifier/typeBase::Anything), so the library members it implies are inherited (KerML 1.0 §8.4.2) semantics/implicit.go implicitKerMLBases, read by implicitBase when source.KindOf says the document is KerML (the F3/F8 file-kind mechanism, no second notion) — a bare feature is left with no base rather than being given the SysML attribute one; libs/record.go (cache format 17) stores the semantic supertype edges so a restored library symbol inherits alike semantics/implicit_test.go:TestKerMLImplicitDefinitionBases, :TestSysMLImplicitDefinitionBasesRemainKindBased, semantics/cached_library_test.go:TestInheritedImplicitBaseIsTheSameParsedAndRestored, libs/loader_cache_test.go:TestLoaderCachePreservesRedefinitionMemberEdges ⚠️ Approximate (the keyword table above is what the KerML root exercises; a keyword outside it gets no implicit base, and the implied feature bases KerML also defines are not modelled)
A declared generalization suppresses the implicit one only when it already reaches that base, directly or indirectly (KerML 1.0 §8.4.2), so struct MyWheel specializes Wheel still specializes Objects::Object when classifier Wheel; reaches only Base::Anything semantics/implicit.go declaredGeneralizationReaches (declared generalizations only, and cycle-guarded, so it cannot re-enter the closure the implicit base feeds), consulted for KerML documents; a SysML declaration keeps taking its supertypes from its own declaration semantics/implicit_test.go:TestImplicitBaseOfExplicitSupertypeIsTransitive, :TestSysMLImplicitBaseOfExplicitSupertypeIsTransitive ⚠️ Approximate (KerML only: the SysML side is unchanged, and moving it needs its own adjudication)
A public membership import is re-exported to importers of the importing namespace, a root-level import is visible from a nested package of the same document, and an imported name may prefix a qualified name (KerML 1.0 §8.2.4, SysML v2 §7.4.4) resolve/unqualified.go lookupImportedMember, matchImport (recursion-guarded), resolve/qualified.go (imported members of the current namespace before the global fallback), resolve/filter.go (a namespace's children include what its public imports re-export), symbols/builder.go (the root scope keeps its document node, so root-level imports are found from a nested scope) resolve/visibility_test.go:TestPublicMembershipImportIsReexported, :TestPrivateMembershipImportIsNotReexported, :TestCyclicMembershipImportsTerminate, resolve/imports_test.go:TestRootImportVisibleInNestedPackage, :TestImportPrefixResolvesThroughSiblingImport ✅ Faithful (private stays unexported, and the traversal is lazy and cycle-safe)
A name written in a declaration's header (featured by, crosses, a subsetting) resolves against the members and imports of that declaration's own body before the enclosing scope, and those members stay reachable from outside by qualified name and by feature chain resolve/document.go resolveHeaderRelationships, headerHasName, findFeaturedByTargets (a featuring type is traversed for inherited members like a supertype); resolve/resolver.go lookupMember (contributed members, then the scope's own imports) resolve/document_test.go:TestResolveDeclarationBodyMembersFromHeaders, :TestResolveFeatureChainThroughImportedBodyMember, resolve/inherited_names_test.go:TestInheritedMembersThroughGeneralFeature ⚠️ Approximate (the body is preferred only for a name it declares or imports itself, not for one it inherits, and a typing in the header is excluded — TestValueTypeNameNotShadowedByOwnMembers fixes that a value type is not shadowed by a member of the body it types)

Design note: references is a member-contribution edge, not a generalization

A perform action usage relates the action it performs through a ReferenceSubsetting, written references or ::> (SysML v2 §7.17.6; the derived PerformActionUsage::performedAction comes from that owned reference subsetting, §8.3.17.14). KerML makes ReferenceSubsetting a syntactically distinguished kind of Subsetting (§8.3.3.3.9), which is why the referenced feature's members are visible on the referencing one.

It is nevertheless kept out of semantics.Model.DirectSupertypes. Subsetting in this implementation drives conformance and implicit typing, and a perform statement is not a subtype of the action it performs for those purposes: making it one would give perform action takePhoto references takePicture; the type of takePicture and silently change conformance results elsewhere. Instead Model.MemberSources — the union of the generalization edges and the reference subsetting, breadth-first and cycle-guarded — is what member lookup consumes, so takePhoto.focus resolves while AllSupertypes(takePhoto) stays free of takePicture.

Two consequences of the spec's naming rules fall out of this and are implemented alongside it: an unnamed feature takes the effective name of the feature it references (KerML Feature::effectiveName), so perform providePower.generateTorque; declares generateTorque; and because that name is bound in the same scope the reference resolves in, the reference is resolved outside its own binding: a refFilter hides just that borrowed binding for the duration of the lookup, leaving each scope's declarations, inherited members and imports intact, so a perform of an action the owner inherits from its type still resolves.


Pilot Constraint Rules We Do Not Implement (P6 of the pilot differential)

Four constraint-tier rules the pinned reference implementation (pilot 2026-05) enforces and we do not. They were adjudicated diagnostic by diagnostic in pilot-differential.md (§P6, follow-ups F20–F23) against minimal reproducers run through that reference; the rule text is its validator (org.omg.kerml.xtext/.../KerMLValidator.xtend) plus the constraint text on the generated metamodel. Nothing here is implemented, so the "Implementation" column names where the rule would belong rather than where it lives.

Semantic Rule Implementation Test Case Status
validateSubsettingFeaturingTypessubsettingFeature.canAccess(subsettedFeature) (KerML Subsetting): a feature of a type is not reachable by :: from outside it, since the subsetting feature's featuring types must feature the subsetted one (transitively, through featuring types that are themselves features) would be passes/constraint.go at LevelConstraint, over a canAccess predicate semantics does not compute today passes/pilot_p6_gaps_test.go TestConstraintSubsettingFeaturingTypesNotImplemented (skipped, F20) ❌ Not implemented (all five corpus occurrences are downstream of the pilot failing to parse our namespace over-acceptance; the rule itself is real — reproducer in the P6 adjudication)
validateFlowEndSubsetting — a FlowEnd must subset a feature that is not merely redefined: each end names the feature the payload leaves from or arrives at, so it can redefine Transfer::source::sourceOutput / Transfer::target::targetInput (SysML v2 FlowEnd) would be passes/constraint.go beside checkConnectorEndRedefinition, over the connector ends semantics/lower already build passes/pilot_p6_gaps_test.go TestConstraintFlowEndSubsettingNotImplemented (skipped, F21), with the accepted counterpart TestConstraintDottedFlowEndsAreAccepted ❌ Not implemented (examples/views-demo.sysml:44 flow of Fuel from tank to thruster; violates it and we accept it)
validateElementFilterMembershipIsModelLevelEvaluablecondition.isModelLevelEvaluable: an invocation is evaluable when its function is a model-level-evaluable library function and every argument is; a feature reference when its referent is a self-reference, or owned by a Metaclass/MetadataFeature, or has no featuring type and an evaluable value expression ours exists but diverges: passes/filter.go ElementFilterPass / semantics.Model.CheckElementFilter (filter-not-evaluable, warning, LevelType) passes/pilot_p6_gaps_test.go TestFilterModelLevelEvaluableFalsePositive, TestFilterModelLevelEvaluableFalseNegative (both skipped, F22) ⚠️ Approximate, divergent both ways (we warn on filter 1 + 2 > 0;, which the reference accepts; we are silent on a reference to a feature with a featuring type, which it rejects)
validateInvocationExpressionInstantiatedTypeinstantiatedType must be a Behavior, or a Feature typed by exactly one Behavior would be passes/typecheck_expr.go beside inferInvocation, which checks arity and argument types but never the kind of the invoked declaration passes/pilot_p6_gaps_test.go TestTypeCheckInvocationInstantiatedTypeNotImplemented (skipped, F23) ❌ Not implemented (part def Widget; part w = Widget(); draws Must invoke a behavior or a behavioral feature from the reference and nothing from us)

What We Don't (Yet) Support

Decisions to Reassess

Deliberate limitations whose current handling should be revisited once the feature they wait on lands (this repository has issues disabled, so follow-ups are tracked here):

Deferred until Reassess
A parameter of a behavior or step whose general type comes from the library index semantics/redefinition.go ownedParameters reads the declaration's members, so a general behavior with no parsed AST (a cached library symbol) contributes no positions and its parameters are not redefined. Needs parameter order in IndexRecord, like the Supers row below.
Scalar type inference for a bare feature reference (passes/typecheck_expr.go infer) A condition that is a plain name — while total { … }, total : Integer — infers Unknown, so checkBoolean passes it and the executor reports it (runtime/action_statements.go evalCondition) instead. Once a feature reference infers its declared scalar type, this becomes a typecheck error like the literal and operator cases, and the runtime check goes back to being unreachable.
Specialization edges in the library index (libs/loader.go recordEntries drops Supers) implicitUsageBases maps each usage kind to its stdlib base definition because the base feature the spec has usages subset would be a dead end for member lookup. With the edges recorded, the map should name the base feature the spec names.

Major Features Not Implemented (UML-referenced; no SysML v2 notation or KerML performance)

Activity Diagrams (Advanced): - Interruptible regions - Expansion regions (parallel/iterative) - Streaming pins - Exception handlers - Structured activities with pin connectors

State Machines (Advanced): - Protocol state machines

Object Model: - Dynamic object creation/destruction (an object is materialized once, and nothing destroys it) - Operation invocation given as a calc or constraint, or with positional arguments (an action operation with named arguments runs — see the Classifier Behaviors map) - Routing to a second object of one usage (a via send follows the connections and an addressed send resolves an object, but the object reached is the one this context holds as its target's occurrence — see Known Limitations)

Type System: - Full generic/specialization validation - Interface realization - Redefinition conformance checking - Subsetting validation

Advanced SysML v2: - Analysis cases with verification semantics - Use case execution - Allocation execution semantics beyond materializing the allocation and its ends

What Can't Be Claimed for Spec Compliance

Intentionally Unspecified (No Normative Semantics): - Verification verdict evaluation (VerdictKind/PassIf) - SysML v2 §9.3.2: "evaluation... intentionally not specified normatively" - Variability/variation selection - SysML v2 §9.4: "Selection of variants is not specified normatively" — OpenSysML selects the variant a variation usage is bound to (attribute :>> cut = cut::cutIdeal;) and errors on an unselected, unknown, or multiply-selected variation; see the Variation and Variant map - View/viewpoint rendering - SysML v2 §10.2: "rendering semantics intentionally left to tools". OpenSysML does render a view — a tree, an interconnection diagram, a state machine, an action flow or a table, as text, Mermaid or a Markdown table (%render, sysml -render; see the rendering rows) — but the artifact itself is this tool's output, not a normative form, and the kinds it does not produce are a typed view.UnsupportedKindError rather than a substituted rendering. The viewpoint conformance verdicts OpenSysML reports are likewise tool-defined (see the viewpoint conformance row) - Allocation execution - SysML v2 §9.2.4: syntax defined, execution semantics not normative

Implementable But Not Yet Done: - Binding and flow connector objects (their ends reach routing, but neither is materialized as a connector object — see the Structural map) - Interruptible regions (spec exists, needs token cancellation) - Exception handlers (spec exists, needs exception propagation)


Implementation Files

Runtime Execution (internal/core/runtime/)

File Purpose Lines
context.go Execution context, constraint/requirement evaluation ~430
invoke_calc.go Calc invocation: parameter/result resolution across specialization, binding, recursion bound ~300
action_executor.go Token-flow semantics, control flow nodes, nested actions ~729
state_executor.go Event-driven state machines, transitions, hierarchical states, pseudostates ~1149
eval.go Expression evaluation (operators, literals, features) ~758
value.go Runtime value representation (ValConst, ValString, ValInstance) ~150
trace.go Deterministic execution and calc-evaluation trace recording, canonical value rendering ~290
conformance_test.go Conformance gate ~480
robustness_test.go Failure-mode tests ~830
trace_test.go Golden trace test infrastructure ~200
trace_calc_test.go Trace determinism and canonical rendering unit tests ~180

Symbol Resolution (internal/core/resolve/)

File Purpose Lines
document.go Name resolution, inheritance chain lookup ~750
qualified.go Qualified name resolution (A::B::C) ~200

Symbol Tables (internal/core/symbols/)

File Purpose Lines
builder.go AST → symbol table, control flow node registration ~380
scope.go Scope tree, member lookup ~250

Testing Infrastructure

See TESTING.md for complete test contract details.

Test counts: stated once, in the Test Coverage list near the top of this document — the per-prefix conformance breakdown and the trace, fixture, negative and robustness figures are all there, and every other page links here rather than restating them (CONTRIBUTING.md).

Quality Gates: - Parser: 95/95 stdlib files clean (94 vendored OMG, 1 OpenSysML extension) - Execution conformance: every case passing, with known_failures.txt empty - Training examples: 100/100 clean (no files recorded in internal/core/model/testdata/training_examples_expected.txt) - No regressions: All tests pass on every commit

The training-example gate needs the corpus, which is not vendored: run ./scripts/download-training-examples.sh first. The gate skips while the corpus is absent, so run the script before claiming a change is clean locally. CI downloads it (.github/workflows/pr.yml) and sets OPENSYSML_REQUIRE_TRAINING_CORPUS=1, which turns an absent corpus into a failure, so the gate can no longer skip green there. The gate runs against an empty semantic cache (t.Setenv("XDG_CACHE_HOME", t.TempDir())), so it reports the same 100/100 on any machine.


Model Persistence and RDF Interchange

Implementation: internal/core/rdf, internal/core/export User surfaces: %save (internal/repl/meta.go), sysml -convert (cmd/sysml/main.go) Reference: the RDF mapping — the mapping, the CLI, and the limitations in full

Two representations are supported, SysML textual notation and RDF Turtle. No JSON is used as an input, an output, or an intermediate form.

Notation is stable. RDF Turtle is experimental as of 0.1.0: the statuses below report how faithful the mapping is to what it covers, not that the mapping covers a whole model or that its vocabulary is settled — see the mapping's status.

Capability Implementation Test Case Status
Save a model as notation, preserving comments and notes export.Convertformat.Source (token stream, not an AST re-print) export_test.go:TestSaveKeepsComments, repl/save_test.go:TestMetaSaveSysML ✅ Faithful
Save a model as RDF Turtle export.ToRDF + rdf.WriteTurtle repl/save_test.go:TestMetaSaveTurtle, golden .golden.ttl fixtures ✅ Faithful
Notation → RDF for every definition/usage keyword the parser accepts export/kinds.go metaclass tables, rdf_out.go encode export_test.go:TestGoldenConversions (18 fixtures) ✅ Faithful
RDF → notation for the mapped subset rdf_in.go ToSysML export_test.go:TestGoldenConversions, TestConvertedNotationParses ✅ Faithful
Round trip preserves the graph (sysml→ttl→sysml→ttl is stable) both directions export_test.go:TestRoundTripIsLossless ✅ Faithful
Deterministic, reversible element IRIs keyed by qualified name, with ids in [A-Za-z0-9_-]+ rdf/vocab.go ElementIRI, rdf/ids.go EncodeElementID/DecodeElementID rdf_test.go:TestElementIRIRoundTrip, rdf/ids_test.go, export_test.go:TestElementIRIsEncodeQualifiedNames, TestFixtureElementIDsRoundTrip ✅ Faithful
Declaration order preserved across a format with no order sysx:memberIndex TestRoundTripIsLossless ✅ Faithful
Turtle writer/parser (prefixes, a, ;/, grouping, typed and language literals, long strings, escapes, @base) rdf/turtle_write.go, rdf/turtle_parse.go rdf_test.go:TestTurtleRoundTrip, TestParseTurtleForms, TestParseTurtleEscapes ✅ Faithful
Syntax errors rejected, never partially converted export.SyntaxError, rdf.ParseError (with line) export_test.go:TestSyntaxErrorIsReported, cmd/sysml/convert_test.go:TestConvertErrors ✅ Faithful
Unsupported RDF reported, never silently dropped export.UnsupportedError rdf_test.go:TestParseTurtleRejects, export_test.go:TestUnsupportedTurtleConstructs/TestUnknownMetaclassIsUnsupported/TestForeignGraph ✅ Faithful
Expression-valued positions (values, bounds, guards, filters) carried as source text, not expression trees TestRoundTripIsLossless ⚠️ Approximate — converts back exactly, but not queryable by SPARQL
End-binding heads (connect, bind, flow, succession, transition, accept, satisfy) carried as sysx:sourceText with structural properties alongside export_test.go:TestVerbatimHeadsRoundTrip ⚠️ Approximate — exact through OpenSysML; a foreign graph without the text is reported as unsupported rather than guessed
Accept-action shorthand (action X accept p : T [via Port]) parameter encoded structurally; printer rebuilds the shorthand export_test.go fixture testdata/convert/accept.sysml, parser/testdata/parse/accept_action_shorthand.golden ✅ Faithful
then succession between members sysml:SuccessionAsUsage with sourceFeature/targetFeature, from the one edge node every then parses to, in every body that admits one (a state's regions included) export_test.go:TestSuccessionRoundTrips, :TestSuccessionRoundTripsInEveryBody, parser/succession_test.go:TestMemberAttachedThenDesugars, :TestMemberAttachedThenInRegionDesugars ⚠️ Approximate — a then beside a member with no name cannot be named by these ends: it warns (unnamed-succession-end) and no edge is recorded
Behavioral nodes of an action or state body (initial/final node, perform, send, accept, terminate, assign, fork/join/merge/decision, while/loop/for, if/else, states, substates, regions, entry/do/exit, defer, pseudostates, transitions) export/behavior.go metaclasses and sysx: properties, encoded and printed back export/behavior_test.go:TestBehavioralModelsComeBackByteIdentical, :TestActionNodeMetaclasses, :TestLoopAndConditionalMetaclasses, :TestStateMachineMetaclasses, :TestStateMembersRoundTrip, fixtures testdata/convert/action_nodes.sysml, loops_conditionals.sysml, state_machine.sysml ⚠️ Approximate — the nodes round-trip byte-identically, but the conditions and expressions they carry are source text, and a succession that does not name both of its ends is refused (export/behavior_test.go:TestUnsupportedBehavioralShapesAreReported)
Two members of one namespace sharing a name refused: the qualified name is an element's graph identity export_test.go:TestDuplicateNameIsUnsupported ❌ Rejected rather than merged
Ownership cycle in an input graph refused: no root owns the element, so printing would emit an empty document export_test.go:TestOwnershipCycleIsUnsupported ❌ Rejected rather than emitting an empty file
Lexical // and /* */ trivia across the RDF hop no element owns trivia; doc/comment are declarations and do convert export_test.go:TestCommentsThroughRDF ❌ Not carried through .ttl (a direct .sysml save keeps it)
Blank nodes, RDF collections, bare literal shorthands rejected by rdf.ParseTurtle rdf_test.go:TestParseTurtleRejects ❌ Not supported (by design; see docs/reference/rdf-mapping.md)

Vocabulary: sysml: = https://www.omg.org/spec/SysML# and elmt: = urn:sysmlv2:element: match the Flexo MMS SysML v2 service's Namespaces.kt. That is only vocabulary compatibility: the service's reader derives an element's @id from the substring after the final :, and requireValidId permits only [a-zA-Z0-9_-]+, so OpenSysML's qualified-name element IRIs are not addressable through its API. The reader also ignores predicates outside sysml: and urn:sysmlv2:annotation:json:, so sysx: triples do not survive that path. Paged listing and query require sysml:elementId, while roots filtering uses sysml:owner and sysml:owningRelatedElement; these are roadmap D3 work. Whether such a graph loads into a running Flexo triplestore has not been demonstrated. Properties the SysML metamodel does not define are confined to sysx: = urn:opensysml:sysml:: memberIndex, hasBody and sourceText carry order, body presence and verbatim heads, prefixMetadata, filter, isNamespaceImport, isRecursive and isExpose carry notation the metamodel has no property for, and the behavioral properties (guard, expression, payload, subactionKind, …) carry the parts of a behavioral node the metamodel has no predicate for.

What can't be claimed: this is not a normative SysML v2 → RDF/OWL mapping. OMG's abstract syntax has no standard RDF serialization, so the property names follow the metamodel's own attribute names and the Flexo service's conventions. A model converted here is faithful to itself on a round trip; it is not guaranteed to be interpreted identically by an unrelated SysML RDF tool, and no round trip through a third-party triplestore has been demonstrated. The vocabulary may also change without a compatibility path, so a .ttl is an artifact to regenerate rather than the copy of record.


Source-Preserving Model Editing

Implementation: internal/core/edit, internal/grpc/edit.go (ApplyEdits) User surfaces: opensysmlModel.edit(), Editor.set_value, Editor.rename, Editor.apply (python/opensysml/edit.py) Reference: the Python guide

The standard defines an API for changing a model (SysML v2 API & Services commits); this is not that API. It is a source-level edit of the notation a model was parsed from: the AST stays immutable, the edit rewrites bytes of the source guided by the spans the parse recorded, and the result is read back before it is returned. Two operations are defined, and only two.

Rule Implementation (file:function) Tests Status
A feature's value is set by replacing the expression of an existing = <expr>, or by adding one before the declaration's terminating ; when it has none edit/locate.go Model.valueSplice, Model.terminator (the ; from the token stream, not a search) edit_test.go:TestSetValueReplacesOnlyTheValueSpan, :TestSetValueAddsValueToValuelessFeature, :TestSetValueKinds (quantity, string, boolean, a feature reference, an expression, a feature nested three levels deep, a feature reached through redefines), grpc/edit_test.go:TestApplyEditsSetValuePreservesSource, python/tests/test_edit.py ✅ Faithful
A declaration is renamed by rewriting its own name token edit/locate.go Model.renameSplice, declIdent edit_test.go:TestRenameRewritesTheNameTokenOnly, grpc/edit_test.go:TestApplyEditsAddsValueAndRenames ⚠️ Approximate — the name token only. References are not rewritten (next row)
A rename whose element is referenced is refused, naming where the references are made edit/locate.go Model.referringTo (resolve.References + Resolver.PartSymbol), FailureRenameReferenced edit_test.go:TestRenameRefusedWhenReferenced, grpc/edit_test.go:TestApplyEditsRenameReferencedNamesReferrers, python/tests/test_edit.py ✅ Refused rather than half-done — the referrers are reported as the FQN of the namespace each reference is made from, not of the referring expression
Targets are named by the id a read reports (Symbol.id, an FQN), and only a declaration of the edited model's own source can be edited edit/locate.go Model.target (Index.LookupQualifiedFrom + GetFQN, DocName check) edit_test.go:TestRefusals (unknown target, target outside this source, target carries no value) ✅ Faithful — a name that resolves outside the edited source, the standard library included, is refused rather than editing a file the caller did not name
Edits apply in one pass, right-to-left by offset, and every byte outside an edited span is identical to the parsed source — an edited span covers the target's own tokens only, since a node's span runs on to the next token and so contains the whitespace and comments written after it edit/edit.go Apply, splice ordering, edit/locate.go Model.tokenSpan edit_test.go:TestSetValueKeepsWhatFollowsTheValue, edit_test.go:TestSetValuePreservesCommentsAndBlankLines, :TestApplyManyEditsInOnePass, the assertOnlySpanChanged check in every case (the original is rebuilt from the result by undoing each applied edit), python/tests/test_edit.py (byte comparison around each AppliedEdit) ✅ Faithful — nothing is reformatted, so format.Source is not run over the result
Two operations that would edit overlapping bytes are refused edit/edit.go Apply (FailureOverlappingEdits) edit_test.go:TestRefusals (overlapping edits), grpc/edit_test.go:TestApplyEditsRefusalIsAResponse ✅ Faithful
A new value that does not lex/parse as one expression, or a new name that is not an identifier, is refused before anything is spliced edit/validate.go Model.checkValue, checkName edit_test.go:TestRefusals (value does not parse, value is not one expression, value is empty, new name is not an identifier, new name is a keyword, new name is empty) ✅ Faithful
A rename to a name that already means something where the element is declared is refused edit/locate.go Model.nameTaken (Resolver.LookupNameExcluding from the element's own scope, with the element's own binding hidden), FailureInvalidName edit_test.go:TestRefusals (new name is a sibling's name), :TestRenameShadowingAnOuterNameIsRefused ✅ Faithful — a sibling of that name makes the qualified name ambiguous, and a name reached through an enclosing namespace, an import or inheritance would be shadowed, so every expression reading it would silently read the renamed element instead. Re-analysis cannot catch either, since the name still resolves. The refusal is conservative: a rename onto any name visible at the element's position is refused, even where shadowing would have been intended
The edited source is re-lexed, re-parsed and re-analysed, and content is not returned when the edit introduced an error — a value that parses but names nothing included edit/validate.go validateResult (parser + passes.Analyze over the edited document in an index built for it), reported as FailureResultInvalid with the diagnostics edit_test.go:TestResultInvalidCarriesDiagnostics, :TestPreExistingErrorsDoNotRefuseAnEdit (only errors the edit introduces refuse it), :TestSemanticValidationSkippedWithoutIndexSource, grpc/edit_test.go:TestApplyEditsRefusalIsAResponse (empty content on every refusal) ✅ Faithful — the service never returns notation its own parser cannot read back
Every refusal is a typed failure kind, never a silent no-op, and a refused request applies nothing edit/errors.go Error/Failure, api/proto/sysml.proto EditFailure, python/opensysml/errors.py (EditError and its subclasses) edit_test.go:TestRefusedEditLeavesNothingApplied, grpc/edit_test.go:TestApplyEditsRefusalIsAResponse, python/tests/test_edit.py, test_wire_compat.py:test_edit_failure_kinds_keep_their_values ✅ Faithful
A model evicted from the service's cache is NOT_FOUND, as Convert reports it internal/grpc/edit.go Service.ApplyEdits grpc/edit_test.go:TestApplyEditsUncachedModelIsNotFound, python/tests/test_edit.py (ModelNotFoundError) ✅ Faithful
Creating or deleting an element, and building a model from the client ❌ Not supported (by design): an edit changes the source of a model that already declares what it declares. There is no AST printer, so a model assembled client-side cannot be written out
The client negotiates the capability before calling python/opensysml/capabilities.py CAPABILITY_APPLY_EDITS, connection.py Connection.apply_edits python/tests/test_edit.py (MissingCapabilityError, raised before any RPC) ✅ Faithful

gRPC Service Layer

Implementation: internal/grpc/service.go
Status: ✅ Functional, ✅ §5.2 test contract satisfied for the wrapper

Runtime RPC Handlers

RPC Implementation Status Tests
GetServerInfo service.go Service.GetServerInfo, capability names in Capabilities() ✅ Faithful — reports the build version (informational; a source build reports dev) and the capabilities this build supports by name, currently type_facts, convert, verification, query, enum_values, evaluate_subject, symbol_attributes, unset_value, feature_values and apply_edits. A capability is added, never renamed or dropped with its behaviour intact, so a client requires one instead of comparing versions; a service predating this RPC answers UNIMPLEMENTED, which the client reads as supporting no capability service_test.go:TestGetServerInfo, TestGetServerInfoTypeFactsCapabilityIsHonest, python/tests/test_capabilities.py
ParseFile service.go Service.ParseFile (parser + passes.Analyze + stdlib load) ✅ Faithful — the cache is keyed by the file name and content the service read, so repeated parses of an unchanged source hit it whatever the request's (ignored) content_hash says, a hash disagreeing with its content cannot serve another model, and identical content read under two names keeps a record each, since their diagnostics name different files runtime_test.go:TestParseFile_*, service_test.go:TestParseFileCachesByContentRead, service_test.go:TestParseFileCachesPerFileName, every conformance case
ParseFile (standard library) grpc/libindex.go indexPool, buildLibraryIndex, indexPoolSizeFromEnv (SYSML_GRPC_INDEX_POOL, default 4, 0 disables), grpc/service.go Service.Prewarm/Close/ParseFile, wired at startup in cmd/sysml-grpc/main.go ✅ Faithful — the library does not depend on the model, so a cache miss adds its document to an index built ahead of it rather than loading the library again, and keeps that index with its cached model. What a model resolves against is unchanged (same source list, same expansion, same persist step); an index is handed out once, so no two cached models share one, and an empty pool builds one on the request path, so a result never depends on prewarming. A cold ParseFile on examples/combined-behavioral-demo.sysml measures ~0.5–0.9 ms against a warm pool where it measured ~100–128 ms building the library per model, with the same zero diagnostics grpc/libindex_test.go:TestPooledIndexMatchesFreshlyBuiltIndex (identical diagnostics and identical qualified lookups over the whole index, pooled vs built inline), :TestParseFileServesModelsFromThePrewarmedPool, :TestParseFileTakesNoIndexOnACacheHit, :TestCachedModelsOwnTheirIndex, :TestIndexPoolFallsBackToBuildingInline, :TestIndexPoolCloseStopsPrewarmingAndStillServes, :TestIndexPoolSizeFromEnv, :TestIndexPoolStopsAtItsSize, :TestIndexPoolBuildsOneIndexAtATime, libs/cache_test.go:TestCacheStoreConcurrentWritersOfOneKey, :TestCachePruneRemovesStaleTempFiles, robustness_test.go:parse_with_unavailable_standard_library, BenchmarkParseFileColdPooled/ColdInline
Convert export.go Service.Convert, conversion in internal/core/export ✅ Faithful for what OpenSysML writes — SysML/KerML notation and RDF Turtle, from a loaded model named by its model_hash, a path the service opens, or inline content, with the format names sysml -convert takes and canonical names reported back. A model_hash converts the source that parse read, so a file edited since then does not change what is written, and a model evicted from the cache is NOT_FOUND rather than converted as something else; a file_path is read afresh, for a caller who does want the file as it stands. Notation to notation is source-preserving (comments and layout survive); a graph direction returns an equivalent model, not identical bytes, and drops comments, per docs/reference/rdf-mapping.md. A conversion that cannot be written faithfully returns error plus the diagnostics rather than partial output, and tolerate_syntax_errors is honored for notation to notation only, since a graph built from an unparsed declaration would lose it silently export_test.go:TestConvert*, TestConvertModelHashConvertsWhatWasParsed, TestConvertUncachedModelHashIsNotFound, python/tests/test_conversion.py
Query query.go Service.Query (SysML v2 API & Services Query) ✅ Faithful to the standard's query model, ⚠️ approximate on three @type names — see the construct map below query_test.go:TestQuery*, python/tests/test_query.py
GetSymbol service.go:126-145; static type facts in typefacts.go (SymbolInfo.type_info, .multiplicity, .specializations) computed by a per-model resolver + semantics context cached on the model and locked for the duration of a conversion ✅ Faithful — reports the declared and resolved type, the library scalar it reduces to, quantity/unit, the declared multiplicity, and every generalization edge (specializes, subsets, redefines, typing) in declaration order; an unresolved name is reported unresolved rather than guessed. SymbolInfo.attributes carries the attributes the element actually has — own and inherited, in that order, a redefinition masking what it redefines — each with the resolved type, unit and constant default the service resolves (following specializes/subsets/redefines for what a declaration leaves out); a default that is not constant is reported as absent rather than guessed. Advertised as the symbol_attributes capability, since an older service reports an empty set, which cannot be told from an element with no attributes service_test.go:TestGetSymbol_, typefacts_test.go:TestTypeInfo, TestSpecializations*, TestMultiplicity*, TestSymbolContextConcurrentConversion, attributes_test.go, conformance symbol_attributes, python/tests/test_symbol.py
GetDiagnostics service.go:148-169 (parser + semantic) ✅ Faithful runtime_test.go (implicit)
Evaluate service.go Service.Evaluate ✅ Faithful — evaluates in a lexical scope (context_symbol_id) and, with subject_symbol_id, against an instantiated object, the way %eval does after %instantiate: a feature then reads that object's feature value rather than the declared default, and the subject's own scope resolves inherited features when no context is named. A subject that is not a symbol, or that cannot be instantiated, is reported in-band rather than evaluated as something else; no subject leaves the existing behaviour unchanged. Advertised as the evaluate_subject capability, which a client requires before sending a subject, since a service predating it drops the field and answers with the declared default runtime_test.go:TestEvaluate_*, TestEvaluateWithSubject*, conformance evaluate_arithmetic, evaluate_subject_slot, evaluate_no_subject_default, evaluate_subject_not_found, python/tests/test_model_surface_integration.py
Instantiate service.go (feature values read through Instance.GetFeatureValue, so a derived default is evaluated against the instance; InstanceGraphToProto in convert.go returns every instance reachable from the root in InstantiateResponse.instances) ✅ Faithful — a composite feature value still marshals as the child's id, and that child is carried in the same response, so a nested object is reachable over gRPC without a follow-up RPC; expansion is bounded at depth 8 and stops at a type already on the path, as %features bounds it, so a self-referential part cannot instantiate forever runtime_test.go:TestInstantiate_*, instance_graph_test.go:TestInstantiate_ReturnsNestedInstances, _ReturnsDeepNestedInstances, _CollectionOfInstances, _FeatureValueErrorReported, _SelfReferentialPartTerminates, _MutuallyRecursivePartsTerminate, conformance instantiate_part, instantiate_derived_slot
ExecuteAction service.go:265-312 ✅ Faithful runtime_test.go:TestExecuteAction_*, conformance execute_action_inputs, execute_action_no_initial
ExecuteState service.go:315-355 ✅ Faithful runtime_test.go:TestExecuteState_*, conformance execute_state_transitions
ApplyEdits internal/grpc/edit.go Service.ApplyEdits, engine in internal/core/edit ✅ Faithful for the two operations it defines — sets a feature's value and renames a declaration, on the source the named model was parsed from, splicing the spans the operations reach and leaving every other byte identical. The edited source is re-parsed and re-analysed before it is returned, so a refusal carries diagnostics and no content; a model evicted from the cache is NOT_FOUND rather than edited as something else. Element creation and deletion are not offered, and a rename does not rewrite references — see Source-Preserving Model Editing below. Advertised as the apply_edits capability internal/grpc/edit_test.go, internal/core/edit/edit_test.go, python/tests/test_edit.py, python/tests/test_wire_compat.py:test_apply_edits_is_an_added_rpc

Server Process Lifecycle (cmd/sysml-grpc)

Rule Implementation (file:function) Tests Status
The published binary starts on the port it is given (-port 0 taking an ephemeral one), logs the address it listens on, serves RPCs, and stops on SIGINT/SIGTERM with a graceful stop and exit status 0 cmd/sysml-grpc/main.go main (net.Listen, then grpc.Server.GracefulStop on the signal, bounded by a 30s forced Stop) cmd/sysml-grpc/lifecycle_test.go:TestServiceServesRPCsAndShutsDownCleanly (built binary as a process, readiness from its own listening line, GetServerInfoParseFileInstantiate), :TestShutdownWithAnOpenConnection, :TestHealthEndpointReportsTheBuild ✅ Faithful — the process ends on its own; a client connection left open does not hold it
A start that cannot proceed is a reported error and a non-zero exit, never a hang: an unknown flag, a port already in use, a cache size that is not a positive count cmd/sysml-grpc/main.go main (flag.Parse, sysmlgrpc.NewService, net.Listen) cmd/sysml-grpc/lifecycle_test.go:TestUnknownFlagExitsNonzero, :TestOccupiedPortExitsNonzero, :TestInvalidCacheSizeExitsNonzero ✅ Faithful — each names what failed on stderr and exits non-zero
A request naming something the service does not have — a file that does not exist, a model hash it never issued — is NotFound on that call, and the service keeps serving internal/grpc/service.go Service.ParseFile, Service.GetSymbol cmd/sysml-grpc/lifecycle_test.go:TestMissingModelIsATypedError (a later GetServerInfo is still answered and the process still exits 0) ✅ Faithful
/health answers on the -health-port while the server runs, reporting the build, and no other path is served cmd/sysml-grpc/main.go healthHandler cmd/sysml-grpc/server_test.go:TestHealthHandlerAnswersOnlyHealth, lifecycle_test.go:TestHealthEndpointReportsTheBuild ✅ Faithful

SysML v2 API & Services Query Conformance

Standard: the Query, Constraint, PrimitiveConstraint and CompositeConstraint components of the SysML v2 API & Services OpenAPI schema (api/openapi.yaml in Systems-Modeling/SysML-v2-API-Java-Client). This is the only query surface the standard defines; the language has none.

Reference: docs/reference/api.md § "SysML v2 API & Services Query" documents the property table, the @type mapping and the comparison choices.

Construct Implementation (file:function) Tests Status
Query.scope — elements considered, empty being the whole model query.go:queryEval.candidates, elementWalk TestQueryScopeRestrictsToAnElementAndItsNested, TestQueryWithoutWhereSelectsWholeScope, TestQueryScopeMayNameALibraryElement ✅ Faithful — a scope entry is a qualified name (or the standard's {"@id": …} reference, translated client-side) and covers that element and everything nested in it, parents first in declaration order. A library element may be named, so the loaded stdlib is queryable
Query.select — properties projected query.go:projectedProperties, queryEval.project TestQuerySelectProjectsOnlyThoseProperties, TestQuerySelectReportsEveryPropertyByDefault, TestQueryOmitsPropertiesAnElementDoesNotHave ✅ Faithful — an empty selection reports every queryable property; a property an element does not have is absent rather than empty
Query.where absent query.go:queryEval.matches TestQueryWithoutWhereSelectsWholeScope ✅ Faithful — selects the whole scope
PrimitiveConstraint with = query.go:queryEval.matchesPrimitive, equalsAny TestQueryByTypeSelectsThatMetamodelType, TestQueryEqualMatchesAnyOfAListedValue, TestQueryIsAbstractSelectsAbstractDefinitions, TestQueryTypePropertyReportsResolvedType ✅ Faithful — textual equality, matching any listed value, which is how the standard's clients write a @type filter
PrimitiveConstraint with > / < query.go:validateOrdered, compareOrdered, parseOrdered TestQueryOrderedComparisonOnMultiplicity, TestQueryOrderedComparisonOnUnorderedPropertyFails, TestQueryOrderedComparisonAgainstNonNumberFails, TestQueryOrderedComparisonNeedsOneOperand ✅ Faithful, with documented choices — numeric, one operand, ordered properties only (multiplicityLower/Upper); * is infinity. A non-ordered property, an unparsable operand or more than one operand is INVALID_ARGUMENT, never a false verdict
PrimitiveConstraint.inverse query.go:queryEval.matchesPrimitive TestQueryInverseNegatesTheVerdict ✅ Faithful — negates its own constraint's verdict, so a constraint and its inverse partition the scope
CompositeConstraint with and / or, nested query.go:queryEval.matchesComposite, validateComposite TestQueryCompositeNesting, TestQueryFaultUnderADecisiveConstraintIsReported ✅ Faithful — nests arbitrarily and short-circuits when evaluating, but every nested constraint is validated first, so a malformed one under an already-decisive sibling is still reported
Property names — @id, @type, name, declaredName, qualifiedName, owner, isAbstract, type, multiplicityLower, multiplicityUpper query.go:queryProperties (single source of truth), QueryPropertyNames TestQuerySelectReportsEveryPropertyByDefault, TestQueryUnknownPropertyFailsRatherThanMatchingNothing, TestQuerySelectUnknownPropertyFails ✅ Faithful for the set implemented — the set is closed and an unknown property is a typed QueryError (INVALID_ARGUMENT) listing the ones that exist, never an empty answer. Other metamodel properties (documentation, isComposite, …) are not queryable: known limitation
@type — symbol kind → metamodel type name query.go:metamodelTypeNames, MetamodelTypeName TestMetamodelTypeNameCoversEveryKind, TestQueryByTypeSelectsThatMetamodelType ⚠️ Approximate for three kinds — total over every kind a parsed declaration can have, but an individual definition/usage reports OccurrenceDefinition/OccurrenceUsage (an individual is an occurrence with isIndividual set), a connector end reports Feature (a Feature with isEnd set), and an alias reports Membership. Everything else is the metamodel's own name
Malformed query — no constraint form, no operator, no operand, empty composite query.go:QueryError, validateConstraint, Service.Query TestQueryMalformedConstraintsFail, TestQueryUnsetQueryFails, TestQueryUnknownScopeFails, TestQueryFaultIsReportedWithNoElementsToConsider ✅ Faithful — the where tree is validated before any element is read, so every malformed shape fails with INVALID_ARGUMENT naming what is wrong however few elements the scope holds; an unknown scope is an error, not an empty answer
Empty result query.go:Service.Query TestQueryMatchingNothingIsEmptyNotAnError, python/tests/test_query.py ✅ Faithful — a well-formed query selecting nothing answers with no elements
Capability negotiation service.go:CapabilityQuery, python/opensysml/capabilities.py:CAPABILITY_QUERY TestQueryCapabilityIsReported, python/tests/test_query.py:test_query_requires_the_capability ✅ Faithful — reported from GetServerInfo as query; the Python client refuses to send to a service that does not report it
Standard JSON payload accepted verbatim python/opensysml/query.py:build_query, Model.query python/tests/test_query.py (test_cookbook_payload_translates_verbatim and the malformed-payload table) ✅ Faithful — a cookbook payload is sent unchanged; @type tags, symbolic operators and scalar-or-list value are translated to the RPC's oneof and enums, and a payload the standard does not describe raises QueryError before anything is sent

Known limitations (query):

  • No graph traversal and no transitive closure, by design of the standard: there are no path expressions, no joins, no "all elements under X" and no "everything specializing Y" constraint — containment is expressible only as a scope, and specialization not at all, even though semantics.Model can answer it. The Query RPC is an interop surface for the standard's clients, not OpenSysML's expressive query story.
  • Query.owningProject and Query.@id are accepted in a payload and ignored: this service holds parsed models, not projects or commits.
  • Results are unordered by the standard and are returned in declaration order here; there is no paging.
  • Only the properties tabulated above are queryable; documentation, metadata annotations and derived metamodel properties are not.
  • An element carries the value of a property it declares; an inherited value is not reported (type reports the resolved type of the feature itself).
  • An element with no qualified identity — an unnamed doc, an anonymous usage or connect — is not answered at all: its qualified name has an empty segment, so it is neither a unique @id nor a name a scope could use (TestQueryOmitsElementsWithNoQualifiedIdentity). Nor is one declared inside an action body — an if branch, a loop body — whose owner-less scope names it only locally, so the name resolves to no element (TestQueryOmitsBodyLocalDeclarations).
  • A standard-library element restored from cache carries no declaration and may carry no symbol kind, so it reports no isAbstract and, for such a kind, no @type — it is answered, but never matches a @type = comparison.

Test Coverage (AGENTS.md §5.2 Four-Layer Contract)

Current: - ✅ Layer 1 (Golden AST): Covered via parser tests (fixtures in internal/core/parser/testdata/) - ✅ Layer 2 (Execution conformance): internal/grpc/conformance_test.go drives Evaluate, Instantiate, ExecuteAction, ExecuteState and GetSymbol from .sysml + .expected.json pairs in internal/grpc/testdata/conformance/ (count in the Test Coverage list near the top; three of them failure modes), each parsed through the ParseFile RPC so the whole wrapper is exercised. Schema: that directory's README.md. - ✅ Layer 3 (Golden traces): N/A — the wrapper adds no ordering behavior of its own; traces are pinned at the runtime tier. - ✅ Layer 4 (Robustness): internal/grpc/robustness_test.go covers the wrapper's failure modes (unknown model hash, unknown symbol, malformed expression, a standard library that did not load); execution-level failure modes stay pinned in internal/core/runtime/robustness_test.go.

Rationale: the gRPC layer is a protocol wrapper over internal/core/runtime, which carries full §5.2 compliance for execution semantics. Its own conformance cases assert what the wrapper is responsible for: symbol lookup by FQN, input binding, value marshalling in both directions (including which Value oneof arm is set), the state-visit trace, and in-band error reporting.

Known Limitations (Non-blocking)

Runtime: - an inline entry/do/exit body is one action, so an outgoing transition interrupts a do body only between rounds, never between its statements; the one-action-per-statement do { … } form is the interruptible spelling - entering a composite state runs its own entry body before the region's initial transition reaches the substate, so a parent's do body can run before a substate's entry body (state_anonymous_action_body.trace.golden). Pre-existing region-entry scheduling, not specific to inline bodies - an entry/do/exit behavior that both performs an action and states a body of its own is reported at execution rather than at parse time: which of the two SysML means is unadjudicated, so neither is chosen - a calc output only a branch that did not run would assign is unassigned for that activation; the body is not statically required to bind every output it declares - a calc whose only computation is rebinding an inout in its body, with no out and no return, is still reported as having no result expression: an inout is bound by the invocation, so it does not count as an output the body computes - an object carried over an unrelated declaration keeps its identity but not its execution: an execution belongs to the analysis it started in, so its behaviors are started again from their initial states in the rebuilt analysis, what the discarded run wrote is dropped, and the restart is reported. Re-declaring what the object runs drops the object itself with a reported reason instead (runtime/adopt.go Adopt/restartBehaviors, writeBoundBehaviors; repl/session.go rebindRestartedMachine; runtime/adopt_test.go:TestAdoptRestartsACarriedObjectsBehavior, :TestAdoptRefusesAnObjectWhoseBehaviorCannotRestart, repl/classifier_behavior_test.go:TestObjectMachineRestartsOverAnUnrelatedDeclaration, :TestRestartedMachineRunsInTheNewContext, :TestRewritingTheExhibitedMachineDropsTheObject). Tool-defined: the spec has no notion of re-analysing an edited model - a nested body over a value the redefined declaration wrote governs over that value, but supersedes it whole: a feature the body does not value takes its type's own default rather than the bound value's, since a FeatureValue binds a feature as a whole

Python bindings: - generated typed classes (opensysml.generate) cover structural usages only: behavioral and connector usages are not instance feature values, so no property is emitted for them. specializes, subsets and redefines all become Python base classes, in declaration order, and a redefining feature takes the type and multiplicity it does not restate from what it redefines; a base another declared base already specializes is left implicit, and a base order that linearizes no way at all keeps the bases it can and records what it left out as a comment, rather than emitting a module that fails to import. Redefinition narrowing is still not checked - TypedObject.from_instance rejects an instance whose type another generated class describes, and accepts a generated subclass of the expected one; it accepts a type no generated class describes, because Instantiate on a usage reports the usage's own FQN (Demo::myCar), which the client cannot relate to the definition typing it. A wrong-typed instance is therefore caught only when its type has a generated class of its own; unchecked(instance) bypasses the check deliberately - a service opensysml did not spawn is never stopped by it: attaching takes no ownership reference and writes no state, so only the spawning process stops the service it recorded, when its last connection is released. The record authenticates the pid it names by the process start time written with it, so a reused pid is cleaned up rather than signalled (connection.py:_write_ownership_record, _authenticate_record, python/tests/test_lifecycle.py, test_stale_service.py) - an instance_id outside an Instantiate response (an Evaluate result, say) is still a bare int64: those responses carry no instance graph to resolve it - init.py:11-16 - Shadows builtins (RuntimeError, eval) - a downloaded binary is verified against the digest binary.py:PINNED_SHA256 pins for its release, independent of the origin that served it; a version with no pin fails rather than falling back to the served .sha256, unless $OPENSYSML_ALLOW_UNPINNED_DOWNLOAD names that repository (or is 1) and accepts same-origin trust. A opensysml release pins only service releases published before it, so a newer service needs a newer opensysml or that opt-in (scripts/pin_release_checksums.py, python/tests/test_binary.py)

Standard behavioral notation: - a succession written at namespace level (first part1::action1 then requirement1;) is parsed and carried with both ends, but there is no enclosing behavior to lower it into, so it does not execute - accept at/accept after inside an action body reports ErrNoClock: the action executor has no clock. The same trigger on a state transition waits on time and fires - a succession end with no name is carried by identity, so a model whose succession has a positional end is reported as unsupported by the RDF export rather than written back - a state machine's change conditions are re-tested once per micro-step and again at quiescence, and fire on the condition rising, one rise being consumed by the poll that observed it. KerML has no clock, so the cadence is a tool-defined choice, not a spec requirement: the coarser per-RTC-step alternative was considered and may be revisited. A machine that cannot progress reports which condition it waits on (StateExecutor.SuspendReason) rather than completing silently - an addressed send reaches the object this context holds as the occurrence of the usage its target names; a second object instantiated of that same usage is a different object, which a send addressed to the usage does not reach - a flow whose ends name no feature to carry (flow a to b; between two action nodes) and a flow whose end names something that is not a node of the action are reported when the graph is built: the notation needs a payload or a pin at each end - a nested action node written inside a loop or branch body is lowered as unsupported and reported when reached; only statements, and the body parameter the body itself is written as, execute in those bodies - the Open-MBEE corpus models still report the two OMG-side notations adjudicated above (end ; outside an interface body, 'SysML Standard Diagrams'::gv) and unresolved library references in the notebook models (Scalarattributes::String, start, envelopingShapes, mRefs). The conjugated end and timeslice item item1 are legal and are accepted - what DesertKite.sysml (branch InitialDesign) and OOSEM.sysml report, and nothing else: the 'SysML Standard Diagrams'::gv sites above; attribute 'Animal Capture Rate' :>> OOSEM::MOE;, where MOE is the short name of a member of OOSEM::'OOSEM Measures' and so is not a member of OOSEM itself; and 3 references to the decision node __unnamed1 of 'Move with Herd', which resolve to nothing because a control node's name is not registered as a symbol (symbols/builder.go buildDecl)

Go gRPC layer: - SymbolInfo.attributes reports only defaults that fold to a model-level constant; one written as a feature reference or a call is reported absent rather than guessed (internal/grpc/attributes.go) - metadata["type"]/metadata["specializes"] still report only the first edge, kept for compatibility; specializations is the complete list - runtime instances are request-local, so an id is resolvable only against the response that carried it; there is no RPC that fetches an instance by id later - a quantity crosses in both directions: every outbound path reads one, and an ExecuteAction input or EvaluateCalc argument carrying one is decoded against the model's index (ProtoToValueIn), so a unit that does not resolve — or resolves to something that is not a measurement unit — is reported rather than bound as an unusable value. The Python client has no quantity encoder yet, so a caller cannot send one until that lands with the rest of the Python API surface

These are documented for transparency; none block production use.


Language Server (internal/lsp, cmd/sysml-lsp)

Standard: LSP 3.17 § Lifecycle Messages. Reference: docs/reference/api.md § internal/lsp.

Measured coverage: 115 tests and subtests in internal/lsp, of which 107 are top-level Test functions, plus the built-binary lifecycle tests in cmd/sysml-lsp.

Rule Implementation (file:function) Tests Status
The server is started by an editor over stdin/stdout, and a client that names the transport on the command line (--stdio, as TransportKind.stdio sends) is served rather than rejected cmd/sysml-lsp/main.go run (explicit stdio flag; Go's flag accepts -stdio and --stdio) cmd/sysml-lsp/lifecycle_test.go:TestStdioTransportServesTheLifecycle (both spellings, built binary over pipes), cmd/sysml-lsp/main_test.go:TestCommandLine ✅ Faithful — the flag is a documented no-op because stdio is the only transport; every other unknown flag still exits 2 with usage, so a typo is not swallowed
shutdown is answered, and afterwards every request but exit is answered InvalidRequest (-32600); a non-exit notification is dropped internal/lsp/lifecycle.go Server.Shutdown, Server.lifecycleHandler (wraps the handler chain on the read loop, ahead of async dispatch) internal/lsp/lifecycle_test.go:TestAfterShutdownOnlyExitIsServed, :TestNotificationAfterShutdownIsDropped, cmd/sysml-lsp/lifecycle_test.go:TestRequestAfterShutdownIsInvalidRequest ✅ Faithful
exit ends the process: status 0 after a preceding shutdown, 1 otherwise internal/lsp/lifecycle.go Server.Exit, Server.ExitCode; internal/lsp/server.go Server.Run (returns on the exit signal and closes the connection itself); cmd/sysml-lsp/main.go serve internal/lsp/lifecycle_test.go:TestExitEndsTheSessionWithTheStatusLSPRequires, cmd/sysml-lsp/lifecycle_test.go:TestExitAfterShutdownEndsTheProcess, :TestExitWithoutShutdownIsNonzero, :TestClosedStreamEndsTheProcess ✅ Faithful — Run returns rather than being killed from a handler, so the process leaves no server behind per editor window

Constraint Solving (internal/core/solve) — advertised extension, not a conformance claim

Standard: none. SysML v2 defines evaluation of Invariant, RequirementUsage and assert satisfy against concrete values; it defines no solving semantics. This package is therefore an optional, additive extension that OpenSysML advertises, not a compliance item.

The runtime evaluator (internal/core/runtime) remains the normative semantics: nothing here changes a verdict it reaches, and no verdict is ever derived from a translation. The package answers the questions the evaluator cannot — satisfiability, conflicting subsets, value synthesis — by translating conditions into a solver-independent term IR, writing that IR as an SMT-LIB2 script, and optionally running an external solver over it. No solver is bundled and no module dependency or cgo is added: a solver is an external process, so an absent one is a typed error and the feature is opt-in.

Conditions come from the evaluator's own collection (runtime.Context.ConditionsOf, the accessor over conditionsOf/appendConditions), so a translation encodes exactly what the evaluator checks, in the same order, with the same distinctions: require versus assume, negation, and a body meaning the conjunction of its conditions (a negated body denies that conjunction).

Rule Implementation (file:function) Tests Status
The conditions translated are the conditions the evaluator checks — inherited first, require versus assume kept, negation kept, a negated body denied as one conjunction internal/core/runtime/condition.go Context.ConditionsOf, conditionsOf, appendConditions (one collection, shared) internal/core/runtime/conditions_of_test.go, internal/core/solve/translate_test.go ✅ Faithful to the evaluator
A constraint, requirement or assert satisfy translates to a Query: declared variables, finite datatype sorts, asserted terms, and the provenance (condition text, element, declaring symbol, file and span) of each assertion internal/core/solve/translate.go Constraint, Requirement, Satisfaction, Translate; query.go internal/core/solve/translate_test.go, satisfy_test.go
Sorts come from the semantic type facts, never from the literals written: Boolean→Bool, Natural/Integer→Int (a Natural also declared non-negative), Rational/Real/Number→Real, String→String, an enumeration definition or a variation point→a finite datatype sort internal/core/solve/reference.go sortOf, datatype internal/core/solve/translate_test.go, goldens ring_variants.smt2
A quantity is normalized to the base units its unit reduces to, through the existing unit model, as an exact rational — so no rounding enters a script — and an incommensurable comparison or sum refuses at translation time internal/core/solve/translate.go (quantity handling over semantics units/dimensions) internal/core/solve/translate_test.go, golden touchdown.smt2
Anything outside the subset refuses with a typed ErrNotTranslatable naming the construct and where it was written; one refused conjunct fails the whole query, so no partial script exists to answer sat/unsat about conditions it does not contain internal/core/solve/errors.go NotTranslatableError; translate.go internal/core/solve/translate_test.go (one case per unsupported construct, plus all-or-nothing)
The script is deterministic byte for byte: declarations ordered by name, assertions in evaluation order, generated names stable, set-logic chosen from the sorts and operators actually used internal/core/solve/smtlib.go Script; logic.go Query.Logic internal/core/solve/golden_test.go (.smt2 goldens, -update, translate-twice), smtlib_unit_test.go

Translatable subset: not, and/&, or/|, xor, implies, if c ? a else b; ==, !=; <, <=, >, >=; +, -, *, unary -/+, real and integer division / and remainder % (truncating toward zero as the evaluator does, with a non-zero divisor asserted); boolean, integer, real and string literals; quantity expressions (450.0 [km/h]); references to scalar-valued features and feature chains that ground in one; enumeration literals and variation-point variants.

Deliberately out of subset (each refuses, none is silently dropped): - collections and quantifiers — sequences, sets, ->select, ->collect, ->forAll, ->exists, ->size, indexing #(i), ranges, collection-valued features (bounded expansion is not implemented) - invocations of any kind, calc included: a body may iterate or read state, and folding one is the evaluator's job - SMT-LIB's own Euclidean div/mod, and real %, which the evaluator answers by floating-point remainder - exponentiation (**, ^) - classification and metadata operators (hastype, istype, @, @@, as, meta, all, ===, !==, ??, ~, null), complex numbers, and string operations other than equality - comparing or adding magnitudes of different dimensions, features whose type determines no scalar sort, unresolved names, and feature chains that ground in nothing

Known limitation: a variable stands for the values a feature may take, constrained only by its sort; a query with no partial assignment asserts no value a model declares, and an assert satisfy … by x translates the requirement's conditions read through the requirement's own parameters rather than substituting x. Such a query asks what the conditions permit, not what one object holds; a query with a partial assignment fixes the values an object holds or the model declares (see the synthesis rows below). Optimization is a later step.

Solving a query — experimental, opt-in

A solver is run as a process speaking SMT-LIB2 on standard input: OPENSYSML_SMT names one explicitly, else z3 and then cvc5 are looked for on PATH. OPENSYSML_SMT_TIMEOUT overrides the 10s budget one query is given.

Rule Implementation (file:function) Tests Status
A solver is discovered, never assumed: the override first, then z3 and cvc5 on PATH; an absent solver is a typed NoSolverError naming what to install, never a silent skip and never a fabricated verdict internal/core/solve/solver.go Discover, newSolver; errors.go NoSolverError solve/solver_test.go:TestDiscovery, repl/check_test.go:TestCheckReportsAnAbsentSolver
The three verdicts stay distinct to the user: sat, unsat, unknown. A timeout, or arithmetic the solver gave up on, is unknown with the reason it gave — never reported as either of the others solve/solver.go Solver.Solve, session.verdict, session.reasonUnknown; repl/check.go SolveStatus solve/solver_test.go:TestSolverVerdicts, :TestSolverTimeout, repl/check_test.go:TestCheckReportsAnUndecidedAnswer
A solver process failure — crash, non-zero exit, malformed or missing reply, a model naming an undeclared variable — is a typed SolverProcessError, distinguished from unknown solve/solver.go session.read, session.finish, Solver.processError; errors.go SolverProcessError solve/solver_test.go:TestSolverProcessFailures, :TestSolverBadModel, repl/check_test.go:TestCheckReportsASolverProcessFailure
On sat the model is read back and rendered in OpenSysML's own terms: the feature's qualified name, a quantity's magnitude in the base unit the query normalized to, an enumeration literal or variant by name — never raw SMT-LIB. A value the notation has no literal for is marked as the solver's own rather than mistaken for one solve/model.go assign, renderValue; solve/sort.go smtName solve/model_test.go, solve/solver_test.go:TestSolverVerdicts
Integer / and % mean what the evaluator means: for a spread of sign combinations, the solved quotient and remainder equal what internal/core/runtime computes for the same expression solve/term.go TruncDiv, TruncRem; translate.go multiplicative, remainder solve/agreement_test.go:TestSolvedIntegerDivisionAgreesWithEvaluator, :TestSolvedDivisionRejectsEuclideanAnswer
Division by zero, which the evaluator refuses and SMT-LIB leaves total: a literal zero divisor refuses translation, and any other divisor — integer or real — carries a non-zero side condition, so no model is found by choosing zero solve/translate.go divisor, guard solve/translate_test.go:TestRefusals, solve/agreement_test.go:TestDivisorGuardRulesOutDivisionByZero
%check <name> reports the verdict for a constraint, requirement or satisfaction assertion, and on sat the assignment. It is a read: nothing is materialized, and an action or state debugging session keeps running repl/check.go CheckSolve, doCheck; repl/meta.go (command table, dispatch) repl/check_test.go ✅ Faithful — an experimental surface, kept apart from %constraint/%satisfy, whose VerdictStatus it never collapses into
An unsat verdict is explained by an unsat core: the script names each assertion, cores are turned on, and the core is asked for only once the verdict is unsat. Labels are assertion positions, so each core member is the Assertion — and Provenance — it came from, rather than a table beside the query solve/smtlib.go CoreScript, CoreLabel, coreLabelIndex; solve/core.go session.explain, session.unsatCore solve/core_test.go:TestCoreScriptShape, :TestCoreLabelsRoundTrip, :TestCoreGolden, :TestExplainedTwoConditionConflict
A reported core is minimal, or says it is not: reduction drops one member at a time, each round a fresh solver process, and Core.Minimal means dropping any one member left the rest satisfiable. A core past DefaultMaxCoreMembers, out of the OPENSYSML_SMT_CORE_BUDGET budget, or a round the solver did not decide, is reported as it stands with Core.Note saying why solve/core.go Solver.Explain, Solver.reduce, coreBudgetFromEnv solve/core_test.go:TestExplainReducesANonMinimalCore, :TestExplainReportsAnUnreducedCoreHonestly, :TestCoreBudgetFromEnv
A solver that refuses cores, names an assertion the query did not assert, repeats one, answers unreadably or reports an empty core is a typed CoreError (both an ErrNoCore and an ErrSolverProcess), and one that fails mid-reduction stays the SolverProcessError it is — never an empty or invented core solve/core.go session.unsatCore, Solver.coreError, Solver.reduce; errors.go CoreError solve/core_test.go:TestExplainCoreFailures, :TestExplainReportsAFailureWhileShrinking
%explain <name> prints the conflicting conditions of an unsatisfiable constraint, requirement or satisfaction assertion — role, condition as written, declaring element, file:line:col — in the query's assertion order, including RoleDomain bounds and RoleDefined guards, and names the supertype an inherited condition was declared by. sat points at %check, unknown explains nothing, and it is a read that leaves a debugging session running repl/explain.go ExplainSolve, explainQuery, conflictLines, doExplain; repl/meta.go (command table, dispatch) repl/explain_test.go ✅ Faithful — the same experimental surface as %check, and no verdict is fabricated when no solver is installed

Not a conformance claim: satisfiability is not evaluation. %check answers sat about conditions %constraint cannot evaluate at all (an unbound parameter has no value), and sat never means a condition holds of any object. A core says which conditions cannot hold together, not that any object violates them. Solving and conflict explanation are an experimental OpenSysML extension: SysML v2 defines no solving semantics, and verdicts about a model remain the evaluator's.

Known limitations: a variable divisor sets Query.Nonlinear, so unknown is an expected verdict there, and OPENSYSML_SMT names an executable, not a command line with arguments.

SMT-LIB portability — which backends the extension supports

OPENSYSML_SMT may name any executable speaking SMT-LIB2 on standard input, so what a backend must support is stated, probed and reported rather than assumed of z3.

Rule Implementation (file:function) Tests Status
The logic set is the narrowest logic of the SMT-LIB 2.6 logic list that covers the sorts and operators the query actually uses: QF_UF, QF_LIA/QF_NIA, QF_LRA/QF_NRA, and AUFLIRA/AUFNIRA for a query over both Int and Real, the list defining no quantifier-free mixed logic. Truncating integer division by a literal divisor keeps the linear logic (div/mod are the Ints theory's), a variable divisor selects the nonlinear one, and no logic is widened to avoid a hard case internal/core/solve/logic.go Query.Features, Query.LogicChoice, Query.Logic solve/smtlib_unit_test.go:TestLogicSelection (a case per feature), :TestNonStandardLogicIsExplained, goldens mission_budget.smt2, ring_variants.smt2, touchdown.smt2
A non-standard logic is emitted only where the list defines none for the feature — datatypes and strings — and says so: LogicChoice.Standard is false, LogicChoice.Why names the features, and the script carries a comment above (set-logic ALL) solve/logic.go NonStandardLogic, unstandardisedWhy; smtlib.go Script, CoreScript solve/smtlib_unit_test.go:TestNonStandardLogicIsExplained, golden ring_variants.smt2 ✅ Non-standard by necessity, declared as such
What the layer requires of a backend is enumerated as capabilities — model output, unsat cores, incremental checks, declare-datatypes, strings, div/mod, nonlinear and mixed arithmetic, the non-standard logic, (maximize)/(minimize) and :opt.priority — and a query says which of them it needs solve/capability.go Capability, AllCapabilities, Capability.Feature; logic.go Query.Requires solve/capability_test.go:TestQueryRequires, :TestCapabilitiesProbeCapableBackend
A backend is probed rather than believed: one small script per capability, run at most once per executable per process (cached), and only for what the query and operation need — never a subprocess per query. A caller who knows its backend declares them instead (DeclaredCapabilities) and nothing is probed solve/capability.go Solver.Capabilities, Solver.require, runProbe, capabilityCache, DeclaredCapabilities solve/capability_test.go:TestCapabilitiesProbeCapableBackend (probe count), :TestDeclaredCapabilitiesSkipProbing
A capability the backend rejects makes the request a typed UnsupportedCapabilityError (an ErrUnsupportedCapability) naming the backend, the missing feature, the operation and what the backend said, refused before the query runs — never a silent degrade, a widened logic or a fabricated verdict solve/capability.go Solver.require; errors.go UnsupportedCapabilityError, Unsupported; solver.go Solver.Solve; core.go Solver.Explain; configure.go Solver.Configurations solve/capability_test.go:TestCapabilitiesProbeIncapableBackend, :TestUnsupportedCapabilityIsRefused
Not supporting a feature, not being runnable, and not deciding stay three different reports: a probe the backend neither answered nor rejected settles nothing, so the query runs and its own unknown verdict or SolverProcessError is what is reported; an absent executable is a SolverProcessError rather than a claim about features; and a probe reply SMT-LIB does not define at all (maybe) is a SolverProcessError too, refusal being kept for (error …), unsupported or a defined reply that contradicts the check solve/capability.go capState, smtlibResponse, Capabilities.Undetermined, Capabilities.Missing solve/capability_test.go:TestUndeterminedCapabilityDoesNotRefuse, :TestCapabilitiesMissingBackend, :TestUnreadableReplyIsAProcessFailure, :TestUnsupportedReplyIsARefusal, solver_test.go:TestSolveReportsAnUndecidedAnswer
Portability is measured, not asserted: a harness runs one query per feature against whatever OPENSYSML_SMT names and reports each as pass, refuse (the backend rejected a capability it needs) or fail (anything else, including a script the backend would not parse — ours to fix in the writer) solve/portability_test.go portabilityCases, TestPortability, runPortabilityCase solve/portability_test.go:TestPortability (run with z3 4.8.12 and cvc5 1.3.4), :TestPortabilityGateIsRequired; CI pr.yml / config.yml portability jobs

Verified backends (probed on the machine this was written on, not read off documentation): z3 4.8.12 supports every capability above. cvc5 1.3.4 supports all but the two z3 optimization extensions — it rejects (maximize …) as a parse error and answers unsupported to :opt.priority — so every command works on it except %optimize, which refuses on it by naming the missing extension. Per-platform install instructions and the same matrix for users are 1. Install: solver compatibility.

Differential agreement with the evaluator — the evidence for the translation

The translation is checked against the normative evaluator rather than asserted to be faithful. For an element whose conditions translate, and a concrete assignment of the features they read, the property gated is: the query conjoined with that assignment is sat exactly when the evaluator says the conditions hold, and unsat exactly when it says they do not.

Rule Implementation (file:function) Tests Status
An assignment is pinned by equality assertions on a derived query — the AST and the translated query are never mutated — and every variable the query declares is pinned, so the solver decides the question the evaluator answered and not a weaker one internal/core/solve/differential_test.go pinnedQuery, pinsOf, pinTerm solve/differential_corpus_test.go, differential_random_test.go
A disagreement is reported with what debugging it needs: element, condition text, per-variable values, the evaluator's verdict or typed error, the solver status, the exact SMT-LIB script, and locations solve/differential_test.go diffGate.check, diffGate.compare, diffSummary.report as above
A solver unknown is recorded, not counted as disagreement; an evaluator typed error is no verdict; and ErrDivisionByZero is required to correspond to the guarded query being unsat for that assignment rather than being skipped solve/differential_test.go compare, status solve/differential_corpus_test.go, differential_random_test.go
Coverage is counted, never invisible: translated, skipped-by-refusal, agreed, disagreed, unknown, evaluator-refused, assumption-unmet and without-values are summarized per corpus, with each refusal's reason solve/differential_test.go diffSummary TestDifferentialConformanceCorpus, TestDifferentialStandardLibrary, TestDifferentialTrainingCorpus
The gate runs over the runtime conformance corpus on the values those fixtures declare, the bundled standard library, and the OMG training corpus solve/differential_corpus_test.go conditionElements, hostOf, hostsOf (an element is checked on the object carrying the values it reads: the usage stating it, or the usages specializing an abstract family) the three tests above
Randomized assignments over the translatable subset — boundaries, negatives, zero divisors, mixed signs, enumerations, variants and quantities in non-base units — are deterministic: a fixed default seed, OPENSYSML_DIFF_SEED to reproduce one, OPENSYSML_DIFF_SWEEP for a longer run solve/differential_random_test.go TestDifferentialRandomizedAssignments as named
No solver installed means the gate skips loudly and checks nothing, and CI sets OPENSYSML_REQUIRE_SMT=1 so that skipping is a failure there solve/agreement_test.go requireSolver; solve/differential_test.go newGate .github/workflows/pr.yml, .circleci/config.yml (differential agreement gate steps)

Two real disagreements the gate found, both fixed at their root:

  • A real quotient by zero answered an infinity. internal/core/runtime/eval.go returned +Inf for a / 0.0, so a / b > 0.000001 "held" with b = 0.0, while integer division, real remainder, quantity division and the constant folder all report ErrDivisionByZero. Real division now reports it too (runtime/eval_operator_test.go:TestRealDivisionByZeroIsReported).
  • A variation point had two sorts. A usage redefining a variation attribute was given a finite sort of its own, distinct from the one the variation declaring the variants was given, so nesting == nesting::nestingTrue refused as mismatched operands. Both now read the variation that declares the variants (solve/reference.go variationDeclaring, solve/translate_test.go:TestVariationSortIsShared).

What agreement proves, and what it does not: the gate is evidence that the translation is faithful to the evaluator for the cases it covered — the concrete assignments of the corpora and of the generated models, within the translatable subset. It is not a conformance claim, it says nothing about the elements that refuse translation, and where the two ever differ the evaluator is right by definition.

Value synthesis and variant configuration — experimental, opt-in

Rule Implementation (file:function) Tests Status
A query takes a partial assignment: pins fix some features to the values the model already fixes and leave the rest free, so the solver synthesises them. Pinning nothing translates exactly the script it always did solve/pin.go Pin, PinSource, translator.fix; solve/translate.go TranslateWith, ConstraintWith, RequirementWith, SatisfactionWith, translator.pinnedAssertions solve/pin_test.go:TestNoPinsTranslateAsBefore, :TestPinnedValueIsAssertedAndReported, :TestGoldenWithFixedValues, :TestFixedValuesAreOrderedByTheirVariable
A pinned value is read where the evaluator reads it — the object's feature values, else the declared default, through the runtime — and carries its provenance: held by an object, declared by the model, or chosen by the user solve/pin.go Fixed, FixedFor, fixedValue; repl/synth.go Session.declaredPins, owningElement solve/pin_test.go:TestFixedReadsTheValuesTheModelDeclares, repl/synth_test.go:TestSolveKeepsWhatAnObjectHolds, :TestSolveSynthesisesWhatIsFree
A pinned quantity is normalized through the same unit machinery the translator uses, exactly: 5.4 [km/h] is fixed as the rational 1.5 in base units, and a value whose dimension does not match its feature is refused solve/pin.go translator.pinQuantity, translator.commensurable, ratOfConst, ratOfFloat; semantics/dimension.go Model.DimensionOfFeature, Model.DimensionOfUnit solve/pin_test.go:TestPinnedQuantityIsScaledExactly, :TestPinRefusesAnIncommensurableQuantity, :TestPinnedBareNumberRefusesAMeasuredFeature
A pinned enumeration literal or variant is fixed as the datatype constructor the writer declares, and a value the subset cannot represent — or one of the wrong type — is a typed PinError wrapping ErrNotPinnable, never a silent drop solve/pin.go translator.pinTerm, translator.pinDatatype, translator.pinRefusal, PinError, ErrNotPinnable; solve/reference.go (datatype construction, Sort.Variation) solve/pin_test.go:TestPinnedEnumerationNamesItsConstructor, :TestPinRefusesAValueWithNoLiteral, :TestPinRefusesAValueOfTheWrongType, :TestPinnedStringIsAsserted
unsat under a partial assignment means no values exist consistent with what is already fixed, reported as exactly that and distinct from the unpinned unsat; pinned assertions carry roles and indices, so an unsat core names the fixed values that conflict solve/query.go RolePinned, Query.Fixes, Query.Free; repl/synth.go Session.noValuesLines, Session.conflictingFixed solve/synthesis_test.go:TestSynthesisIsUnsatWhenTheFixedValuesForbidIt, :TestFixedValuesInAConflictAreNamed, repl/synth_test.go:TestSolveReportsNoValuesConsistentWithWhatIsFixed, :TestSolveReportsConditionsThatConflictOnTheirOwn
A synthesised model is one witness, not a canonical answer, and is reported as such; values are rendered in OpenSysML's terms through the same model rendering %check uses solve/solver.go Result.Model (documented non-canonical); repl/synth.go Session.synthesise, fixedLines, synthesisedLines solve/synthesis_test.go:TestSynthesisFillsWhatIsNotFixed, :TestWitnessIsReportedInOpenSysMLTerms, repl/synth_test.go:TestSolveReportsAQuantityAsDeclared
Variant configuration: a chosen selection is checked, a consistent one synthesised, and consistent selections enumerated — one fresh check-sat per solution, each excluding the whole previous assignment, built from the solver's own model solve/configure.go Solver.Configurations, session.enumerate, session.deny, session.blocking, Query.Variations, Query.FixValue; repl/synth.go Session.checkSelection, Session.synthesiseConfiguration solve/configure_test.go:TestConfigurationsEnumeratesEveryConsistentSelection, :TestVariationsAreTheVariationPointsRead, :TestFixValueChecksWhatItIsGiven, :TestChosenSelectionCanConflict
The enumeration is bounded by DefaultMaxConfigurations (OPENSYSML_SMT_MAX_CONFIGURATIONS overrides it): reaching the bound sets Result.Truncated with Result.AtBound, a solver that stops deciding or runs out of time sets Result.Undecided (with Result.TimedOut for a deadline) and keeps the selections already found rather than discarding them, and results are called exhaustive only after a final check-sat answered unsat solve/configure.go MaxConfigurationsFromEnv, session.enumerate, partialResult, foundBeforeDeadline; solve/solver.go Solver.solve, Result.AtBound, Result.Undecided; repl/synth.go Session.enumerateConfigurations, truncation solve/configure_test.go:TestMaxConfigurationsFromEnv, :TestConfigurationsStopAtTheirBound, :TestConfigurationsKeepWhatTheyFoundWhenTimeRunsOut, repl/synth_test.go:TestConfigureStopsAtItsBound, :TestConfigureEnumeratesEverySelection
Nested variation points, variants under constraints of their own, assume versus require roles and a denied (assert not) element are configured as the variables and assertions they already are; a query reading no variation point is a typed NoVariationsError, not an empty enumeration solve/configure.go Query.Variations, ErrNoVariations, NoVariationsError; solve/translate.go (roles, negation) solve/configure_test.go:TestConfigurationsOfAConstrainedVariant, :TestConfiguringWithoutVariationsIsTyped, :TestFixValueRefusesAFeatureWithNoValuesToName, solve/synthesis_test.go:TestSynthesisRespectsAssumedConditions, :TestSynthesisForADeniedElement
%solve <name> synthesises values for a constraint, requirement or satisfaction assertion, printing what was already fixed and what the solver chose, and saying the answer is one witness. It is a read: nothing is materialized and a debugging session keeps running repl/synth.go Session.SolveValues, Session.doSolve; repl/check.go pinner; repl/meta.go (command table, dispatch) repl/synth_test.go:TestSolveSynthesisesWhatIsFree, :TestSolveAndConfigureKeepADebuggingSession, :TestSolveUnderANaturalDomainAndADivisorGuard, solve/synthesis_test.go:TestSynthesisUnderANaturalDomainAndADivisorGuard ✅ Faithful — the same experimental surface as %check, reusing SolveStatus/SolveReport and never collapsing into VerdictStatus
%configure <name> [<variation>=<variant>…] [all [<count>]] checks a chosen selection, synthesises one, or enumerates them up to a bound. An unknown variation point, a name that is not a variant of it, a variation chosen twice, a malformed count and a mixed request are each a message saying what to write instead repl/synth.go Session.ConfigureVariants, parseConfigure, Session.configureQuery, chooseVariants, matchVariation, matchVariant, Session.doConfigure; repl/meta.go (command table, dispatch) repl/synth_test.go:TestConfigureSynthesisesASelection, :TestConfigureChecksAChosenSelection, :TestConfigureRejectsWhatItCannotAnswer, :TestConfigureOnAnElementReadingNoVariation, :TestConfigureWithoutAName ✅ Faithful — distinct messages for no solver, an untranslatable element, a solver failure and unknown, never a silent skip

Known limitations of synthesis and configuration: a synthesised model is one of possibly many, and its choice is the solver's, as is the order selections are enumerated in; the enumeration is bounded and says when it was truncated rather than implying exhaustiveness; a variant is configured as the value of a variation point, not as an object, so nothing is materialized and features a variant would only have once bound are not constrained; a feature the conditions read whose value cannot be read as a scalar stays free and is reported as such rather than fixed; and a variation point outside the translatable subset refuses with ErrNotTranslatable like any other untranslatable condition.

Objective optimization — experimental, opt-in

SysML v2 states no direction, no value and no solving semantics for objective, so the contract this layer reads is OpenSysML's own, stated here and in solve/doc.go.

Rule Implementation (file:function) Tests Status
An objective's direction is the trade-study definition typing it — TradeStudies::MinimizeObjective or MaximizeObjective, specializations included, matched by symbol identity so a type merely named alike is not one. An objective typed by neither is a typed ObjectiveError wrapping ErrNotOptimizable, never a guessed direction runtime/analysis.go Context.objectiveDirection, Context.specializesLibraryType; solve/objective.go translator.direction runtime/analysis_test.go:TestObjectivesOfDirectionValueAndOrder, :TestObjectivesOfDirectionThroughSpecialization, :TestObjectivesOfWithoutDirection, solve/objective_test.go:TestObjectiveRefusals, repl/optimize_test.go:TestOptimizeRefusesAnObjectiveWithoutADirection
The value improved is the expression the objective states for the library's best feature (objective o : MinimizeObjective { attribute :>> best = expression; }), since an ObjectiveMembership owns a requirement usage (§8.3.22.4) which carries no scalar value of its own; a value bound directly is read too, where a model can write one. An objective stating no value is a typed refusal runtime/analysis.go Context.objectiveOf, Context.bestValueOf, Context.statesObjectiveBest, restatesFeatureNamed runtime/analysis_test.go:TestObjectivesOfValueScope, :TestObjectivesOfWithoutValue, :TestObjectivesOfRedeclared, solve/objective_test.go:TestObjectiveDirectionAndValue, :TestObjectiveRefusals
What is feasible is the case's own conditions — require/assume/assert/inv, inherited first, in the evaluator's order — together with the conditions each objective states in its own body; the trade-study conditions an objective inherits are about choosing among alternatives, not about feasible values, so they are left out runtime/analysis.go Context.CaseConditionsOf, Context.appendCheckedConstraint, Context.ownConditionsOf; solve/objective.go AnalysisWith runtime/analysis_test.go:TestCaseConditionsOf, :TestCaseConditionsOfInherited, :TestObjectivesOfOwnConditions, solve/objective_test.go:TestObjectiveOwnConditions, repl/optimize_test.go:TestOptimizeReportsTheGreatestValue
Objectives, values and conditions are read through the runtime's own surfaces, so what is optimized is what the evaluator would evaluate: no declaration is re-parsed in the solver layer and no AST is mutated runtime/analysis.go Context.ObjectivesOf, RequireAnalysis; solve/objective.go Analysis runtime/analysis_test.go:TestObjectivesOfAnalysisUsage, :TestRequireAnalysis, solve/objective_test.go:TestObjectivesInDeclarationOrder, :TestAnalysisRefusesANonAnalysis
An objective term joins the query as a first-class part of it: it declares variables, decides the logic, contributes datatype sorts, declared domains and divisor guards, and normalizes quantities exactly as an asserted condition does. A query with no objective writes the byte-identical script it always did solve/query.go Objective, Direction, Query.Objectives, Query.Optimizes, Query.Logic; solve/objective.go translator.optimize solve/objective_test.go:TestObjectiveVariablesAreDeclared, :TestObjectiveDecidesLogic, :TestQuantityObjectiveIsNormalized, :TestObjectiveOverVariantSelection, :TestObjectiveWithGuardedDivision, :TestObjectiveFreeScriptsAreUnchanged
An objective outside the translatable subset refuses with a typed ObjectiveError naming the objective, why it was refused, what to write instead and where it was written — a nonlinear term (an optimizer improves a linear objective) included — never a silent skip solve/errors.go ObjectiveError, NoObjectiveError, ErrNotOptimizable, ErrNoObjective; solve/objective.go translator.refuseObjective solve/objective_test.go:TestObjectiveRefusals, solve/optimize_test.go:TestObjectiveErrorMessage, :TestNoObjectiveErrorMessage, repl/optimize_test.go:TestOptimizeRefusesANonlinearObjective, :TestOptimizeReportsAnAnalysisStatingNoObjective
Several objectives are optimized lexicographically in declaration order, and the script says so itself with (set-option :opt.priority lex) rather than relying on a backend default — z3's box mode returns a model attaining only one of the optima it reports, which would make "the assignment attaining the optimum" untrue solve/smtlib.go writeScript, objectiveComment (priority, ordered (minimize e)/(maximize e), provenance comments) solve/objective_test.go:TestObjectiveScriptIsExplicitlyLexicographic, :TestSingleObjectiveScriptStatesPriority, goldens objective_lexicographic.smt2, objective_mass.smt2, objective_variants.smt2, objective_guarded.smt2, solve/optimum_test.go:TestLexicographicOptima
(minimize …)/(maximize …), (get-objectives) and :opt.priority are solver extensions, not SMT-LIB2, and cvc5 implements none of them: the backend's capabilities are settled by the shared capability model before a query is sent (probed once and cached, or declared by the caller), and a backend without them is a typed NoOptimizationError wrapping both ErrNoOptimization and the UnsupportedCapabilityError that settled it. Nothing is degraded to a plain check-sat and presented as an optimum solve/optimize.go Solver.requireOptimization; solve/capability.go CapOptimization, CapOptimizationPriority, Solver.require solve/optimize_test.go:TestOptimizeRefusesABackendWithoutOptimization, :TestOptimizeRefusesABackendWithoutTheOptimizationCapability, :TestOptimumClassification, repl/optimize_test.go:TestOptimizeReportsABackendWithoutOptimization
Every optimum is verified rather than trusted: the objective's value is read back from the reported model, and a further check asks whether any assignment does lexicographically better — unsat is what makes the answer an optimum. z3 4.8.12 (what apt and CI install) reports 9.5 as the maximum of x under x < 10.5, which this refutes instead of reporting solve/optimize.go session.optimize, session.attained, session.verifyOptimal, better, classifyOptima solve/optimum_test.go:TestOptimumIsVerifiedIndependently, :TestBoundThatIsNotAttained, solve/optimize_test.go:TestOptimumClassification (refuted by verification, verification undecided)
The answers optimization adds stay apart, and none of them fabricates a number: OptimumAttained (verified), OptimumUnbounded (oo), OptimumBounded (an infinitesimal or interval bound, reported as a bound), OptimumUnverified, OptimumUndecided. unsat and unknown remain the verdicts they are, with no optima invented, and a solver answering unreadably is an OptimumError wrapping ErrNoOptimum and ErrSolverProcess solve/optimize.go OptimumStatus, Optimum, parseOptimum, classifyOptimum; solve/solver.go Result.Optima solve/optimize_test.go:TestOptimumClassification, :TestOptimizeRejectsUnreadableOptima, :TestOptimizeKeepsVerdictsApart, :TestOptimizeProcessFailures, :TestOptimizeRefusesAQueryWithoutObjectives, :TestOptimizeCarriesTheObjectiveThrough, solve/optimum_test.go:TestUnboundedOptimum, :TestUnsatisfiableAnalysis
%optimize <name> reports each objective's optimum for an analysis definition or usage, with its declared unit and the assignment attaining it, through the same model rendering %check uses. It is a read: nothing is materialized and a debugging session keeps running repl/optimize.go Session.OptimizeSolve, optimizeQuery, optimumLines, doOptimize; repl/check.go SolveUnbounded, SolveNoOptimum; repl/meta.go (command table, dispatch) repl/optimize_test.go:TestOptimizeReportsTheLeastValue, :TestOptimizeReportsObjectivesInDeclarationOrder, :TestOptimizeKeepsADebuggingSessionAndMaterializesNothing, :TestOptimizeIsListedInHelpAndCompletion ✅ Faithful — the same experimental surface as %check, reusing SolveStatus/SolveReport and never collapsing into VerdictStatus

objective execution status changes with this PR: an objective was parsed, typechecked and otherwise inert; it is now executed as an optimization query by %optimize. This is not SysML v2 conformance — the spec defines no solving — and the runtime evaluator remains normative for every verdict about a model.

Known limitations of optimization: it needs z3, since optimization is a z3 extension; the objective term must be numeric and linear, so a nonlinear objective refuses (a computed divisor makes a term nonlinear, which is why divisor guards are exercised through the case's conditions rather than the objective's); an old z3 can report an optimum verification refutes, and that is reported as no optimum rather than as a value; boxed and Pareto multi-objective semantics are not offered, only lexicographic declaration order; and an analysis case whose conditions bound nothing is legitimately unbounded rather than an error.