Aller au contenu

Type system

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:

  • Fully dynamic — wire any output to any input, break at run time.
  • Nominal typesForce 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:

TypeKindDescription
NumericscalarRoot of the numeric hierarchy.
IntegerscalarWhole numbers (extends Numeric).
FloatscalarReal numbers (extends Numeric).
Booleanscalartrue / false.
StringscalarText.

All physical scalars extend Float.

TypeUnitTypeUnit
LengthmForceN
AreaMomentN·m
VolumePressurePa
MasskgStressPa
Densitykg/m³TemperatureK
TimesEnergyJ
FrequencyHzPowerW
AngleradVelocitym/s
Accelerationm/s²
TypeShapeTypical use
Vector3[x, y, z] three FloatPositions, displacements.
Force3Three ForceLoad at a node.
Moment3Three MomentBending moments.
Torsor{force: Force3, moment: Moment3}Reactions, loads.
Matrix2D array of FloatStiffness, 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-sizing
functions:
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 margin

Without 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:

Fenêtre de terminal
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:

  • a canonical name — Force, Length, Stress
  • a homogeneous list — list[Force], list[Vector3]
  • a homogeneous dict keyed by string — dict[str, Length]
  • an inline struct — {"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 field

At 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.