Class definitions GitHub issue

vibecode
{"vibecode": {
    "doc": "class_definitions",
    "role": "canonical reference for how a class is declared on each of the surfaces in the ecoverse — Caspian DSL, CaspianJ (the engine's runtime format), and Mikobase JSON; every JSON example is shown inside the context of an entire worldlet, since a class definition's natural home is the classes section of a worldlet",
    "audience": "Caspian users and engine implementers (primary), Miko (secondary as a settled-decisions index)",
    "format": "construct_by_construct_side_by_side; each section shows the Caspian DSL form and the worldlet JSON; open questions surfaced inline rather than buried",
    "key_concepts": ["things_live_where_you_store_them",
        "caspianj_class_form", "mikobase_class_schema",
        "worldlet_envelope", "shared_class_definition_structure"],
    "related": ["caspian/index.md#classes", "caspian/caspianj.md",
        "mikobase/class-definition.md", "ecoverse/worldlets/index.md",
        "ecoverse/standard-fields.md"]
}}
Open issues (3)

File: documentation/requirements/caspian/classes/index.md § A complete example (#a-complete-example)

§ A complete example
Add some of the engine-only methods that we talked about.

File: documentation/requirements/caspian/classes/index.md

Take out all references to UNS. We don't do UNS anymore and we don't even bother talking about it.

File: documentation/requirements/caspian/classes/index.md § The worldlet envelope (#the-worldlet-envelope)

§ The worldlet envelope
This is not how classes are stored in a worldlet. Please consult worldlets.json

A complete example GitHub issue

Open issues (1)

File: documentation/requirements/caspian/classes/index.md § A complete example (#a-complete-example)

Add some of the engine-only methods that we talked about.
vibecode
{"vibecode": {
    "section": "complete_example",
    "role": "minimal but feature-complete class definition shown up front so readers see the construct's shape before the per-surface breakdown",
    "shows": ["class_inline_label", "field_with_type_and_required",
        "field_with_default", "method_definition",
        "instance_var_at_sigil", "kwarg_construction",
        "engine_invoked_init", "engine_invoked_to_string",
        "engine_invoked_on_close"]
}}

A class with the everyday pieces — typed fields, a default value, regular methods, and a few engine-invoked methods (init, to_string, on_close) — bound to a variable so it can be instantiated:

caspian
$character = class # character
    field :name,      class: :string, required: true
    field :rank,      class: :string, required: true
    field :soliloquy, class: :string, default: ''

    method &init(name:, rank:, soliloquy: '')
        @name      = name
        @rank      = rank
        @soliloquy = soliloquy
    end

    method &greet
        @rank + ' ' + @name
    end

    method &recite
        puts @name + ': ' + @soliloquy
    end

    method &to_string
        @rank + ' ' + @name
    end

    method &on_close($call)
        puts @name + ' exits.'
    end
end

A few of those are engine-invoked — not called by your code, called by the engine at the right moment:

Construction takes the field names as keyword arguments. Regular methods are called with dot notation; engine-invoked ones fire on their own triggers:

caspian
$hamlet = $character.new(name: 'Hamlet', rank: 'Prince', soliloquy: 'To be or not to be')
$hamlet.greet     # Prince Hamlet
$hamlet.recite    # Hamlet: To be or not to be
puts $hamlet      # Prince Hamlet            (to_string)
# ... when $hamlet's last reference drops:
                  # Hamlet exits.            (on_close)

The rest of this doc breaks down each piece — how it looks in the Caspian DSL, how it looks in CaspianJ / Mikobase JSON, and what is settled vs. still open.


This doc shows how a class definition looks on each of the surfaces in the ecoverse. The goal is a single place where any Caspian DSL construct can be matched to its CaspianJ / Mikobase JSON equivalent.

Two surfaces, one structure GitHub issue

A class definition has two valid surface forms:

The DSL is one surface, the JSON is the other. Anything one expresses, the other must too.

The worldlet envelope GitHub issue

Open issues (1)

File: documentation/requirements/caspian/classes/index.md § The worldlet envelope (#the-worldlet-envelope)

This is not how classes are stored in a worldlet. Please consult worldlets.json

A class definition's natural home in JSON is as a record inside a worldlet — see worldlets/worldlet.json for the canonical example. Class definitions use the whole-hash form that puck.uno/class opts into: class: "puck.uno/class" at the record's top level alongside the definition's properties (name, inherits, fields, methods, etc.), rather than wrapping them in a bucket.

A worldlet wrapping one class:

copy

json
{
    "format": "worldlet/1.0",
    "records": {
        "a": {
            "class": "puck.uno/class",
            "name": "foo.com/character"
        }
    }
}

Notes:

A full worldlet may also carry files, file_chunks, etc. — see worldlets/index.md for the complete spec.

A class at a glance GitHub issue

copy

caspian
class
    inherits 'starfleet.com/person'
    field 'name',      class: :string, required: true, get: true, set: true
    field 'nickname',  class: :string, get: true, set: true
    field 'rank',      class: :string, required: true, get: true, set: true
    field 'serial',    class: :string, required: true, unique: true, get: true, set: true
    field 'starship',  class: 'puck.uno/reference', allowed: 'starfleet.com/ship', get: true, set: true

    method &greet($casual:{required:false})
        puts 'Hello, ' + if $casual and @nickname
            @nickname
        else
            #bucket['name']
        end
    end
end

copy

json
{
    "format": "worldlet/1.0",
    "records": {
        "a": {
            "class": "puck.uno/class",
            "name": "starfleet.com/officer",
            "inherits": "starfleet.com/person",
            "fields": {
                "name": {"class": "string", "required": true,                   "get": true, "set": true},
                "nickname": {"class": "string",                                     "get": true, "set": true},
                "rank": {"class": "string", "required": true,                   "get": true, "set": true},
                "serial": {"class": "string", "required": true, "unique": true,   "get": true, "set": true},
                "starship": {"class": "puck.uno/reference", "allowed": "starfleet.com/ship", "get": true, "set": true}
            },
            "methods": {
                "greet": {
                    "params": {
                        "casual": {}
                    },
                    "body": "puts 'Hello, ' + (if $casual and @nickname; @nickname; else; #bucket['name']; end)"
                }
            }
        }
    }
}

The record uses the whole-hash form that puck.uno/class (the metaclass for class definitions) opts into: rather than wrapping the definition's properties in a bucket, they sit at the record's top level alongside class. It's a shorthand specifically for class-definition records — instance records of normal classes use the explicit {bucket, stack} shape from the objects spec.

Field-by-field: class declares the field's type (string, a named class reference, etc.); required, unique, allowed are constraints; get and set declare auto-generated getter/setter methods. Method-level: params is a hash of param names to option-hashes (empty options hash here just means "all defaults"); body carries the Caspian source string (or, for some classes, a CaspJ tree — both forms are valid).

Constructs GitHub issue

What follows is the construct-by-construct catalog. Each section shows the Caspian DSL form and the worldlet JSON. Where the JSON shape is not yet settled, the DSL is shown alone with the gap called out.

Class identity GitHub issue

copy

caspian
class
    ...
end

copy

json
{
    "format": "worldlet/1.0",
    "records": {
        "a": {
            "class": "puck.uno/class",
            "name": "foo.com/character"
        }
    }
}

The Caspian DSL declaration is just class ... end. The class is an object; it doesn't embed a publication address. The address is where you put the class — for external lookup via %puck['https://foo.com/character'], for fetching from a remote source, for naming an entry in a worldlet record.

In the JSON form, the name field on the class-definition record is the address this class is published at. That's a property of the record (where the class lives), not of the class itself. The record's own key in records (here "a") is an arbitrary short identifier, separate from both the class's content and its publication address. See worldlets/index.md for the worldlet envelope and worldlet.json for the canonical by-example reference.

The "things live where you store them" principle applies fully: a class declared as $gup = class ... end lives at $gup. A class published to a fetch-able URL lives at that URL. A class returned from a method lives wherever the caller stores it. A publication address is one specific storage location, not an intrinsic identity.

Inheritance GitHub issue

copy

caspian
class
    inherits 'foo.com/person'
end

copy

json
{
    "format": "worldlet/1.0",
    "records": {
        "a": {
            "class": "puck.uno/class",
            "name": "foo.com/character",
            "inherits": "foo.com/person"
        }
    }
}

Single parent. Inheritance is always explicit; no path-implied inheritance.

Abstract GitHub issue

copy

caspian
class
    abstract true
end

copy

json
{
    "format": "worldlet/1.0",
    "records": {
        "a": {
            "class": "puck.uno/class",
            "name": "puck.uno/mikobase",
            "abstract": true
        }
    }
}

Direct instantiation raises. Subclasses can be instantiated normally.

Fields GitHub issue

copy

caspian
class
    field :name,      class: :string, required: true, collapse: true
    field :age,       class: :number, min: 0, integer_only: true
    field :homeworld, class: 'puck.uno/reference', allowed_class: 'foo.com/planet'
end

copy

json
{
    "format": "worldlet/1.0",
    "records": {
        "a": {
            "class": "puck.uno/class",
            "name": "foo.com/character",
            "fields": {
                "name": {"class": "string", "required": true, "collapse": true},
                "age": {"class": "number", "min": 0, "integer_only": true},
                "homeworld": {"class": "puck.uno/reference", "allowed_class": "foo.com/planet"}
            }
        }
    }
}

Built-in type names are bare strings — :string and 'string' are equivalent in the DSL. Named class references use the quoted form. For the field-shape conventions and per-type constraint settings, see worldlets/worldlet.json — records a-f show fields with their constraints in use. A consolidated constraint catalog hasn't been written yet for the new spec; until it lands, the by-example reference is the source.

Inline vs named field types GitHub issue

Inline field-type definitions (constraints written directly in the field declaration) are only valid for the basic types (string, number, boolean, url, timestamp, hash, array). Named custom classes are referenced by name only — their structure lives in a separate class definition elsewhere in the worldlet.

The exception is hash, which can carry inline fields, an of element-type, and default field settings. See worldlets/worldlet.json record a for the canonical hash-with-fields example.

Field settings GitHub issue

For the common, type-specific, reference, and array/hash settings, see worldlets/worldlet.json records a-f. The Caspian DSL accepts the same keys as keyword arguments to field:

copy

caspian
class
    field :slug,    class: :string, required: true, unique: true, min_length: 1
    field :rating,  class: :number, gte: 0, lte: 10, integer_only: true
    field :tags,    class: :array,  of: :string, min_elements: 1
    field :avatar,  class: 'puck.uno/dbfile', required: true
end

copy

json
{
    "format": "worldlet/1.0",
    "records": {
        "a": {
            "class": "puck.uno/class",
            "name": "foo.com/show",
            "fields": {
                "slug": {"class": "string", "required": true, "unique": true, "min_length": 1},
                "rating": {"class": "number", "gte": 0, "lte": 10, "integer_only": true},
                "tags": {"class": "array",  "of": "string", "min_elements": 1},
                "avatar": {"class": "puck.uno/dbfile", "required": true}
            }
        }
    }
}

Reference fields GitHub issue

copy

caspian
class
    field :homeworld, class: 'puck.uno/reference', allowed_class: 'foo.com/planet'
    field :stop,      class: 'puck.uno/reference',
                      allowed_classes: ['foo.com/moon', 'foo.com/station']
end

copy

json
{
    "format": "worldlet/1.0",
    "records": {
        "a": {
            "class": "puck.uno/class",
            "name": "foo.com/character",
            "fields": {
                "homeworld": {"class": "puck.uno/reference",
                              "allowed_class": "foo.com/planet"},
                "stop": {"class": "puck.uno/reference",
                              "allowed_classes": ["foo.com/moon", "foo.com/station"]}
            }
        }
    }
}

allowed_class and allowed_classes may be combined — they merge. Subclasses of the named classes are also valid.

Single-field unique constraints GitHub issue

copy

caspian
class
    field :serial, class: :string, required: true, unique: true
end

copy

json
{
    "format": "worldlet/1.0",
    "records": {
        "a": {
            "class": "puck.uno/class",
            "name": "foo.com/officer",
            "fields": {
                "serial": {"class": "string", "required": true, "unique": true}
            }
        }
    }
}

Null values are excluded from the uniqueness check — two records may both have null for a unique field.

Multi-field unique constraints GitHub issue

copy

json
{
    "format": "worldlet/1.0",
    "records": {
        "a": {
            "class": "puck.uno/class",
            "name": "borg.com/appearance",
            "fields": {
                "person": {"class": "puck.uno/reference", "required": true},
                "episode": {"class": "puck.uno/reference", "required": true}
            },
            "uniques": [
                ["person", "episode"]
            ]
        }
    }
}

The shared JSON form is a class-level uniques array on the class definition. The Caspian DSL has no construct for multi-field unique today; see Open questions below.

Joins GitHub issue

copy

caspian
class
    field :person,  class: 'puck.uno/reference', allowed_class: 'foo.com/person'
    field :episode, class: 'puck.uno/reference', allowed_class: 'foo.com/episode'

    join :person, :episode
end

copy

json
{
    "format": "worldlet/1.0",
    "records": {
        "a": {
            "class": "puck.uno/class",
            "name": "foo.com/appearance",
            "fields": {
                "person": {"class": "puck.uno/reference", "allowed_class": "foo.com/person"},
                "episode": {"class": "puck.uno/reference", "allowed_class": "foo.com/episode"}
            },
            "join": ["person", "episode"]
        }
    }
}

join is a class-level shorthand for "required + unique-in-combination + immutable" on each listed field.

Methods GitHub issue

Methods live in a methods namespace, sibling to fields — not inside fields. Each entry is keyed by method name and carries the method's class ("function") and body.

copy

caspian
class
    method &greet(name:)
        'Hello, ' + $name
    end

    remote method &save(name:, rank:)
    end
end

copy

json
{
    "format": "worldlet/1.0",
    "records": {
        "a": {
            "class": "puck.uno/class",
            "name": "foo.com/character",
            "methods": {
                "greet": {
                    "body": "'Hello, ' + $name"
                },
                "save": {
                    "remote": true,
                    "body": "some CaspJ code"
                }
            }
        }
    }
}

Methods live in a methods namespace at the class-record's top level, sibling to fields. Each entry is keyed by method name; the body lives under body. See worldlet.json records b and c for the canonical method-shape examples (including the nested-method namespace and params blocks).

The remote method body adds remote: true as a sibling flag on the method entry.

Reserved pass-through fields GitHub issue

Six fields are reserved pass-throughs on every Puckverse object and require no declaration: vibecode, comment, misc, corporate, class, bucket. See standard-fields.md.

These do not appear in class definitions on either surface — they are always present implicitly.

Constructs not yet expressible in shared JSON GitHub issue

The following Caspian DSL constructs do not have a settled worldlet / shared JSON shape. They are shown in DSL form only. The gap is real and needs a decision.

Field auto-getters and auto-setters GitHub issue

field accepts :get and :set flags that auto-generate reader and writer methods. The Caspian DSL form has this surface; the worldlet/Mikobase JSON shape for the flags is not yet pinned down. See caspian/index.md § field.

copy

caspian
class
    field :nickname                    # private, no external access
    field :nickname, :get              # creates a getter
    field :nickname, :get, :set        # creates both getter and setter
end

(A previous accessor keyword filled this role; it has been removed and its functionality folded into field.)

No worldlet JSON shape exists today for the :get / :set flags. A natural fit would be additional keys inside each field's entry in the fields hash (get: true, set: true) alongside the existing class, required, etc. That is a proposal, not spec.

Helpers GitHub issue

helper creates a lazily-initialized helper object namespaced off the parent. See caspian/index.md § Helpers.

copy

caspian
class
    helper :stats
        method &average()
            ...
        end
    end
end

No worldlet JSON shape exists today. Helpers contain methods, so any helpers shape will need to nest a methods namespace inside each helper entry.

Lifecycle hooks GitHub issue

Caspian documents two hook systems for different events:

Neither is declared inside a class definition today — both are registered dynamically by external code. There is no worldlet JSON shape because there is no in-class declaration to serialize. Whether hooks should be declarable in-class is itself the open question; see Open questions.

Open questions GitHub issue

These are the decisions that need to be made before the shared JSON form is fully settled. Each blocks at least one construct above.

Reconciling the worldlet doc with the methods namespace GitHub issue

worldlets/index.md § classes currently shows methods stored inside fields with "class": "function". That shape is wrong — methods belong in a separate methods namespace, sibling to fields. The worldlet doc needs to be updated to match, and its example classes (starfleet.com/person, starfleet.com/officer) need their methods moved out of fields.

Body key naming: "caspian" vs "caspj" GitHub issue

The body key is named "caspian" (per worldlet doc) but the intended content is CaspianJ JSON — the engine's runtime format, not Caspian source text. Either the key name should change ("caspj" or "body") to match the content, or the content should be Caspian source string (and the key name stays). Pick one — the current mismatch will confuse implementers.

Remote methods in the methods shape GitHub issue

remote method is sketched above as "remote": true alongside "class": "function" in the methods entry. This has not been explicitly confirmed; needs a one-line sign-off.

Field :get / :set JSON shape GitHub issue

The Caspian DSL's :get / :set flags on field (which auto-generate getter and setter methods) don't have a pinned-down worldlet JSON shape. The natural fit is additional keys inside each field's entry in the fields hash (get: true, set: true) alongside class, required, etc. Not yet confirmed.

Helpers JSON shape GitHub issue

Helpers contain methods, so the shape depends on a decision about namespacing. The methods-as-fields pattern doesn't directly give helpers a home — they're sub-namespaces, not flat fields.

Multi-field unique in the Caspian DSL GitHub issue

The worldlet JSON has "uniques": [["a", "b"]] for non-join multi-field uniqueness. The Caspian DSL has no equivalent — join is the only multi-field construct, and it implies extra semantics (required, immutable) that uniqueness alone shouldn't carry. A unique :a, :b statement would close the gap.

Hooks in-class GitHub issue

on_close and before_save / after_save are external-only today. Whether they should also be declarable in-class — and if so, what the shape looks like — needs a call.

See also GitHub issue

What this doc is not GitHub issue

Not the field-constraint reference — that lives in class-definition.md. Not the DSL grammar reference — that lives in caspian/index.md § Classes. Not the worldlet spec — that lives in worldlets/index.md. This file is the bridge that shows how a single class definition looks across all of them.

© 2026 Puck.uno