Registration Metadata Contract¶
At a glance — Functions, entry points, websocket services, Event Script plugins and graph fetch features all register declaratively: metadata attached at the definition site, discovered and resolved by the engine at boot. This page fixes the metadata model and its semantics across languages. How each language attaches the metadata — a Java annotation, a Rust attribute macro, a Python or TypeScript decorator — is an idiom; everything else on this page is the contract, and the golden vectors prove an engine honors it.
The Java engine is the reference implementation; the Annotations Reference documents its carrier surface in full. The Rust engine implements this contract today (its Macros Reference is the carrier twin of the annotations page). Python and Node.js ports implement it next.
One model, many carriers¶
The same declaration in the four languages:
// Java - runtime classpath scan
@PreLoad(route = "hello.world", instances = 10)
public class HelloWorld implements TypedLambdaFunction<Map<String, Object>, Object> { ... }
// Rust - link-time inventory
#[preload(route = "hello.world", instances = 10, typed)]
struct HelloWorld; // + impl TypedFunction<I, O>
# Python (future) - decorator + explicit package walk
@pre_load(route="hello.world", instances=10)
class HelloWorld(TypedLambdaFunction): ...
// Node/TypeScript (future) - decorator + explicit module glob
@PreLoad({ route: "hello.world", instances: 10 })
class HelloWorld implements TypedLambdaFunction<EventEnvelope, unknown> { ... }
The discovery mechanics differ by necessity — only the JVM scans a classpath at runtime — but the developer-visible style, the metadata model and the boot-time semantics must not.
The canonical metadata model¶
Every registration resolves to one canonical record. Wire form for tooling and conformance: JSON, camelCase keys, enums as strings (never ordinals).
{
"kind": "function | websocket | entrypoint-before | entrypoint-main | plugin | feature",
"routes": ["array of route names - function kind; comma-separated aliases in the carrier"],
"name": "string - websocket / plugin / feature kinds",
"namespace": "string - websocket kind, default \"ws\"",
"sequence": "integer - entrypoint kinds, default 10 (0 is framework-reserved)",
"declaredInstances": "integer, default 1",
"envInstances": "string configuration KEY, default \"\" - carried for audit",
"resolvedInstances": "integer - the boot-resolved effective value, clamped 1..1000",
"isPrivate": "boolean, default true",
"zeroTracing": "boolean marker, default false",
"eventInterceptor": "boolean marker, default false",
"optionalService": "string | null - an OR-list of key, !key, key=value conditions"
}
Capability fields (per-language applicability)¶
Some Java @PreLoad fields are capabilities of a runtime family, not universal contract:
| Field | Java | Rust | Python / Node guidance |
|---|---|---|---|
customSerializer |
runtime ObjectMapper swap per route | N/A — serde is the single compile-time serializer; per-type attributes cover the use cases | applicable — both have runtime serialization hooks; carry as a type-name string |
inputPojoClass |
defeats JVM type erasure; enables List<PoJo> |
N/A — TypedFunction<I, O> is monomorphized; body_as::<Vec<T>>() carries the element type |
Python: type hints suffice; Node: applicable as a class reference |
inputStrategy / outputStrategy |
SNAKE / CAMEL / DEFAULT, flippable at runtime (snake.case.serialization) |
N/A at runtime — #[serde(rename_all)] fixes case at compile time (the one genuine capability difference) |
applicable — runtime case mapping is natural in both |
executionHint (reserved) |
@KernelThreadRunner → kernel thread pool |
reserved, unimplemented — every function is a tokio task; revisit if a field workload needs spawn_blocking |
Python: relevant (GIL / thread pool); Node: worker_threads — design when porting |
string length/index semantics (plugin catalog: length, substring) |
UTF-16 code units (String.length() — JVM legacy, retained) |
Unicode scalar values (chars()) — the ports rule |
Python len() and Go RuneCountInString are scalar-native; Node uses [...str].length, NOT the UTF-16 .length — never retrofit the JVM legacy |
A port documents each N/A explicitly (the Rust docs' !!! note "Rust port" convention);
silence is not a disposition. The string-semantics row is a bounded, deliberate
divergence (maintainer ruling, 2026-07-26): identical for all Basic-Multilingual-Plane
text — English, Chinese, JSON keys, typical enterprise payloads — and differing only for
supplementary-plane characters (emoji, historical scripts), which Java counts as 2 code
units and every port counts as 1 scalar value.
Fixed semantics — every port MUST¶
- Attach at definition, resolve at boot.
envInstancesnames a configuration KEY (which may itself hold${ENV_VAR:default}); the value is read at framework boot — never at decoration, macro-expansion or import time. A numeric value wins; anything else falls back todeclaredInstances. Effective instances clamp to 1..1000. - Optional-service grammar is exactly the Java
Featureevaluator: comma-separated conditions are OR-ed;!negates;key=valuematches case-insensitively; a barekeymeanskey=true; no annotation means always required. Evaluated at boot against the application configuration; a false condition skips registration with a "Skip optional" log line. - Marker stacking is order-free.
@ZeroTracing/@EventInterceptor/@OptionalServicecombine with the primary annotation in any order, as Java annotations always have. Where a language's mechanism is order-sensitive by nature (Rust attribute macros expand outside-in; Python decorators apply bottom-up), the port owes the engineering to hide it — the Rust engine's markers re-attach themselves so either order works. - One conflict policy. Explicit programmatic registration wins over declarative (it runs later and replaces). Within any registry, a duplicate name is a WARNING ("Reloading ... please check duplicated ... name") and the later registration wins — never a silent replace, never an error. Registration order among declaratively discovered items is not guaranteed (classpath scan order, link order, import order); do not rely on it.
- Extension-point naming. A plugin or feature name is the positional string on the
carrier —
@FetchFeature("log-request-headers"),#[simple_plugin("getFirst")],@simple_plugin("getFirst"). For plugins only, omitting the name derives it from the declaration: Java lowercases the first letter of the class simple name; Rust camelCases the snake_case fn name — idiomatic declarations in every language yield the same registered name. Plugins are Event Script capabilities (flow vocabulary): they take no optional-service gating and are never conditionally on/off. Features are runtime behaviors: they honor optional-service gating. - Boot sequence: discover → register → override (
yaml.preload.override: configuration-driven route rename / fan-out / instance re-tuning, applied to the collected function set before registration) → resolve (envInstances) → validate (route uniqueness warnings, non-empty registries where built-ins are expected) → route table. Lifecycle anchors: plugins load before flow compilation; features load before graph execution; entry points run in ascending sequence with 0 reserved for the framework. - Discovery is explicit and fails loudly. Java: classpath scan over the base
packages plus
web.component.scan(free). Rust: link-timeinventorycollection — plus a startup assertion on expected built-in counts, because a crate that is never linked contributes nothing, silently. Python: a package walk (pkgutil.walk_packages) importing every module under the services packages before resolution — an unimported module's decorator never runs. Node: a directory glob + dynamicimport()with the same failure mode. Both future ports must fail loudly on an empty registry that should not be empty. - Misuse is a first-class contract. Invalid declarations fail at the earliest
possible stage with a helpful message — unknown parameters name the valid set, a
marker without a primary points at the correct form. Where the failure is
compile-time (Rust), compile-fail tests guard the messages (
trybuild); where it is import/boot-time (Java, Python, Node), unit tests do.
Conformance — the golden vectors¶
The contract is proven the same way the event envelope wire format is: golden vectors shared verbatim between repositories. Three files, one per metadata-rich kind:
| Vectors file | Kind | Java module | Rust crate |
|---|---|---|---|
registration-vectors/core.json |
function | platform-core | platform-core |
registration-vectors/plugin.json |
plugin | event-script-engine | event-script |
registration-vectors/feature.json |
feature | minigraph-playground-engine | knowledge-graph |
Each engine declares the same small fixture set through its own carrier (annotation /
macro / decorator), boots, and asserts that the resolved registrations match the golden
entries exactly — including boot-time envInstances resolution against the vectors'
assumedConfig, marker effects, name derivation, and the absence of every gatedOut
fixture. A new port passes the contract when all three vector suites pass against files
byte-identical to these.
The websocket and entry-point kinds carry little metadata beyond a name/namespace or a sequence; they are pinned by each engine's own registration tests rather than shared vectors.
Porting order (the playbook)¶
For a new language port, in order: (1) implement the carrier + local registry + boot
resolver for the function kind; (2) pass core.json; (3) add the extension points and
pass plugin.json / feature.json; (4) document every capability-field disposition and
intentionally unported carrier; (5) adopt the discovery-failure assertions. The
future-ports playbook covers the runtime
side (wire format, telemetry parity) — this page covers the declaration side; together
they are the port-acceptance bar.
See also¶
- Annotations Reference — the Java carrier surface (source of truth)
- Event Envelope Wire Format — the sibling contract + golden-vector precedent
- ADR-0009 in
docs/arch-decisions/ADR.md— the decision record behind this contract