✅ Allowed
Force → Float (parent)
Float → Force (permissive descendant)
Force → Numeric
Integer → Float
Workflows chain function outputs into other function inputs. Without a shared vocabulary for “what flows on this edge”, the platform would have to choose between:
Force matches only Force, no flexibility, no
unit hint, no UI guidance.MecaPy takes a third path: a canonical catalog of types shared across packages, combined with a sous-typage rule (a.k.a. Liskov-style substitution) that makes typical engineering connections work with zero friction while still catching unit mismatches.
Types are grouped in three categories:
| Type | Kind | Description |
|---|---|---|
Numeric | scalar | Root of the numeric hierarchy. |
Integer | scalar | Whole numbers (extends Numeric). |
Float | scalar | Real numbers (extends Numeric). |
Boolean | scalar | true / false. |
String | scalar | Text. |
All physical scalars extend Float.
| Type | Unit | Type | Unit |
|---|---|---|---|
Length | m | Force | N |
Area | m² | Moment | N·m |
Volume | m³ | Pressure | Pa |
Mass | kg | Stress | Pa |
Density | kg/m³ | Temperature | K |
Time | s | Energy | J |
Frequency | Hz | Power | W |
Angle | rad | Velocity | m/s |
Acceleration | m/s² |
| Type | Shape | Typical use |
|---|---|---|
Vector3 | [x, y, z] three Float | Positions, displacements. |
Force3 | Three Force | Load at a node. |
Moment3 | Three Moment | Bending moments. |
Torsor | {force: Force3, moment: Moment3} | Reactions, loads. |
Matrix | 2D array of Float | Stiffness, transforms. |
A connection from port A.out (type S) to port B.in (type T) is
allowed iff S and T are on the same ancestor / descendant chain.
Numeric ├── Integer └── Float ├── Length ├── Force ← siblings: incompatible └── Stress✅ Allowed
Force → Float (parent)
Float → Force (permissive descendant)
Force → Numeric
Integer → Float
❌ Refused
Force → Length (siblings)
Length → Pressure (siblings)
String → Integer (different roots)
List and struct types apply the rule element-wise:
list[Force] → list[Numeric] ✅ (elements compatible)list[Force] → list[Length] ❌ (siblings)Torsor → Torsor ✅ (same composite)Types are declared explicitly in mecapy.yml — auto-introspection
is deliberately not used for workflow-eligible functions.
name: bolt-sizingfunctions: sizing: handler: bolt:size inputs: force: type: Force description: Axial load required: true diameter: type: Length description: Nominal diameter outputs: margin: type: Float description: Safety marginWithout an io_spec block, the function is still deployable and
executable standalone — but it cannot be used as a workflow node.
The platform returns 400 at deploy time if a workflow references an
untyped function.
The editor calls POST /types/check-connection on every drag attempt
before the edge is materialised:
curl -X POST https://api.mecapy.com/types/check-connection \ -H "Content-Type: application/json" \ -d '{"source": "Force", "target": "Length"}'# → { "compatibility": "REFUSED", "reason": "Force and Length are siblings under Float" }The endpoint is public (no authentication) — newcomers can explore the catalog and test compatibility before signing up.
When the precise type is unknown, authors may fall back to:
Numeric — any number works.Float — any real number (no integer guarantee).Both keep the connection valid against downstream typed ports thanks to the sous-typage rule, at the cost of letting unit errors through.
A workflow InputNode (see the
visual editor) has a single
output port named value. Its input_spec.type_expr accepts any
TypeSpec the catalog parser understands, including:
Force, Length, Stress…list[Force], list[Vector3]dict[str, Length]{"d": "Length", "p": "Length", "As": "Area"}The struct form is the most powerful: one input node captures a whole business object (a bolt, an assembly, a load case) and can be wired to every function port that declares the same struct shape. Subtyping applies field-wise:
InputNode type_expr Function port type {d: Length, p: Length} → {d: Length, p: Length} ✅ {d: Length, p: Length} → {d: Length, p: Area} ❌ p mismatch {d: Length, p: Length, As: Area} → {d: Length, p: Length} ❌ extra fieldAt run time the value is read verbatim from run.inputs[node_key] — no
field-level indexation, no projection. Downstream functions receive the
whole struct as declared.
Invalid type expressions (malformed, referencing an unknown canonical
name) surface as invalid_input_node_types in the validation report.