Class reference
Document
The Document
A Document is Core's single source of truth: a flat, ordered set of path-identified Elements plus the Connections between their slots, backed by a path→element index. There are no UUIDs. The path is the only identity, and every mutation keeps it consistent — rename a subtree and every affected element path and every connection endpoint moves with it, so wiring never dangles.
Document doc;
doc.append(Path("/world"), "group");
doc.append(Path("/world/red"), "material").set("out", Float3(0.9f, 0.15f, 0.15f));
doc.append(Path("/world/ball"), "object").set("radius", 1.5f);
doc.connect(Path("/world/red.out"), Path("/world/ball.albedo")); // source drives target
Value albedo = doc.eval(Path("/world/ball.albedo")); // reads through the wireimport kinogaki as kg
doc = kg.Document()
doc.append("/world", "group")
doc.append("/world/red", "material").set("out", (0.9, 0.15, 0.15))
doc.append("/world/ball", "object").set("radius", 1.5)
doc.connect("/world/red.out", "/world/ball.albedo")
albedo = doc.eval("/world/ball.albedo")The C++ Document holds elements by value in a vector; a raw Element& would dangle after the next insert, so editing goes through an ElementHandle that re-resolves by path. Python mirrors the same model over the C ABI: a Handle carries a Document and a path, and Document is a value type you serialize to bytes and deserialize back.
Construction
Document doc; // an empty document — no elements, no connections
A default-constructed Document is empty. In Python, kg.Document() allocates the native document and frees it on garbage collection. To build a Document from bytes or a file, use deserialize / load rather than the constructor.
doc = kg.Document()
Reading the contents
elements
const std::vector<Element>& elements() const;
Every Element in stage (document) order. Python returns a list of lightweight Element views, each carrying just path and type — read an element's values through doc.edit(el.path).
for el in doc.elements():
print(el.path, el.type)
element
const Element* element(const Path& path) const;
Element* element(const Path& path);
The Element at path, or nullptr if none. In Python, doc.element(path) returns an Element view or None.
el = doc.element("/world/ball") # Element('/world/ball', 'object') or None
has
bool has(const Path& path) const;
True when an element lives at path. Python: doc.has(path).
if doc.has("/world/ball"):
...
children / roots / child
std::vector<Path> children(const Path& parent) const; // direct children, in stage order
std::vector<Path> roots() const; // children(Path{})
std::optional<Path> child(const Path& parent, std::size_t index) const;
children lists a parent's direct children in stage order; roots lists the top-level elements. child returns the index-th child of parent, or empty if out of range. For anonymous elements child is purely positional (the i-th in document order), which can differ from a frozen [N] label after a middle sibling is removed — a serialize→deserialize round-trip renumbers labels back to positions.
child is bound in Python (doc.child(parent, index) returns a Handle or None); children and roots are not bound — derive them in Python by filtering doc.elements() on el.path.
first = doc.child("/world", 0) # Handle at the 0th child of /world, or None
find
std::vector<const Element*> find(const PathPattern& pattern) const;
std::vector<const Element*> find(std::string_view pattern) const;
Every element a glob selects, in stage order. * matches one segment (/parts/*, /parts/bolt_*); ** matches any depth (/world/**/camera). A pattern with no wildcard matches at most one element, the same as element. A malformed pattern yields an empty result. Python returns the matching Element views.
for el in doc.find("/world/**/camera"):
doc.edit(el.path).set("fov", 50.0)
Editing the structure
append / edit / appendChild — the editing handle
ElementHandle append(const Path& path, std::string type); // construct + add, return a handle
ElementHandle edit(const Path& path); // a handle to an existing element
ElementHandle appendChild(const Path& parent, std::string type);// anonymous child + handle to it
append constructs an element of type at path, adds it (uniquifying its leaf name among siblings — the parent must already exist), and returns a chainable, reallocation-safe handle. edit returns a handle to an existing element for further reading or editing (check .valid() if it might be absent). appendChild adds an anonymous (nameless) child addressed by its index among siblings (/parent/[2]) — for ordered body content where position is the identity — and returns a handle to it.
The handle forwards to the Element authoring sugar and chains, so a scene reads like a description:
doc.append(Path("/world/lens"), "object")
.set("radius", 2.0f)
.animate("rotation", {{0, 0.0f}, {24, 6.28f}});
Python binds the same three as append, edit, and append_child, each returning a Handle:
doc.append("/world/lens", "object") \
.set("radius", 2.0) \
.animate("rotation", {0: 0.0, 24: 6.28})
para = doc.append_child("/doc/body", "paragraph") # anonymous → /doc/body/[N]
add / addChild / insert
Path add(Element element); // add, uniquifying the leaf name; returns final path
Path addChild(const Path& parent, std::string type);// add an anonymous child; returns final path (e.g. /parent/[2])
void insert(Element element); // raw append, no uniquifying — paths already final
add and addChild are the lower-level forms behind append / appendChild, returning the final path instead of a handle. insert appends an element with no name uniquifying — for undo restore and prefab instantiate, where paths are already final. These take a constructed Element, which Python does not expose, so Python authors exclusively through append / append_child.
remove
void remove(const Path& path);
Remove an element and its whole subtree, plus every connection that touches the subtree. Python: doc.remove(path) returns a bool (true if something was removed).
doc.remove("/world/ball") # the element, its descendants, and any wires to them — gone
rename
Path rename(const Path& from, const Path& to);
Rename or reparent from to to (a parent plus a desired leaf name), uniquifying on collision and rejecting a move under its own descendant. It rewrites every path in the moved subtree and every connection endpoint, then returns the final new path — equal to from on a no-op or rejection. This is the path-and-connection fixup that keeps wiring intact across a move.
Path now = doc.rename(Path("/world/ball"), Path("/world/sphere"));
// /world/ball/skin → /world/sphere/skin, and any connection to /world/ball.* now points at /world/sphere.*
Python: doc.rename(frm, to) returns the final path string (and raises PrismError if the rename fails outright).
now = doc.rename("/world/ball", "/world/sphere")
Connections
A Connection is the single reference mechanism: one output slot drives one input slot. Both endpoints are property paths — an element path plus a .slot. Material binding, node wiring, and value→slot all become connections.
struct Connection {
Path source; // output slot
Path target; // input slot
bool operator==(const Connection&) const = default;
};
Python's Connection is the same shape: source and target strings, with a source → target repr.
connect / disconnect
void connect(const Path& source, const Path& target); // one source per target slot (replaces)
void disconnect(const Path& target);
connect wires source (an output slot) to drive target (an input slot). Both must be slots — an element path plus a .property; a connect with a bare element path is ignored. There is one source per target: connecting a target again replaces its incoming wire. disconnect removes the wire feeding target.
Python returns self from both for chaining:
doc.connect("/world/red.out", "/world/ball.albedo") \
.connect("/world/red.out", "/world/floor.albedo")
doc.disconnect("/world/ball.albedo")
connections
const std::vector<Connection>& connections() const;
Every connection in the document. Python returns a list of Connection views.
for c in doc.connections():
print(c.source, "→", c.target)
findIncoming
std::optional<Path> findIncoming(const Path& target) const;
The single source feeding target, or empty if nothing drives it. Not bound in Python — find it by scanning doc.connections() for a matching target.
if (auto src = doc.findIncoming(Path("/world/ball.albedo")))
/* src is /world/red.out */;
Evaluation
eval
Value eval(const Path& slot, double time = 0) const;
Resolve the value at a slot (/element.property) through connections and time samples — the read "through the graph." If the slot has an incoming connection, eval follows it to the source; if the property is animated, it samples at time.
Value a = doc.eval(Path("/world/ball.albedo")); // follows the wire to /world/red.out
Value r = doc.eval(Path("/world/lens.rotation"), 12); // sampled at time 12
Python's doc.eval(slot, time=0.0) returns a native value (float / int / bool / tuple) or None if unresolvable. The free function kg.evaluate(doc, slot, time) is the same call.
albedo = doc.eval("/world/ball.albedo") # (0.9, 0.15, 0.15)
Layer tombstones
A base document carries no tombstones — these matter only in a proposal or overlay layer, where overlay() applies them before laying the layer's own elements down. Both lists are kept sorted and unique so the serialized form is deterministic and round-trips.
deletions / addDeletion
const std::vector<Path>& deletions() const;
void addDeletion(const Path& p);
deletions are paths a layer removes: when the layer is applied, each path's whole subtree is removed before the layer's elements are placed. addDeletion records one (normalized to its element path, inserted in sorted order, deduplicated).
Document layer;
layer.addDeletion(Path("/world/ball")); // applying this layer removes /world/ball and its subtree
connectionDeletions / addConnectionDeletion
const std::vector<Path>& connectionDeletions() const;
void addConnectionDeletion(const Path& target);
Connection tombstones: target slots whose incoming wire a layer removes while both endpoints survive — the analog of delete for a connection rather than an element. overlay() applies these after the element deletions. addConnectionDeletion records one target (sorted, deduplicated).
layer.addConnectionDeletion(Path("/world/ball.albedo")); // the wire goes; both elements stay
Neither tombstone family is bound in Python — they are an overlay-layer concern handled on the C++ / composition side.
Equality
operator==
bool operator==(const Document& o) const;
Structural equality: elements, connections, deletions, and connection tombstones all match. The index is derived, so it does not enter the comparison.
Python compares by canonical bytes: doc == other is true when serialize(doc) == serialize(other).
assert kg.deserialize(kg.serialize(doc)) == doc
Templates
extract / instantiate
Document extract(const Path& root) const;
std::vector<Path> instantiate(const Document& fragment, const Path& parent);
extract returns the subtree under root — plus any externally-bound materials it references — rebased so its top sits at the fragment root, as a self-contained Document. instantiate stamps a fragment under parent, uniquifying top-level names and remapping every connection so the copy's internal wiring survives, and returns the new top-level paths. These pass Documents by value and are not bound in Python; for file-level reuse, Python composes through reference + compose_file.
Codec I/O on the document
Read and write any format straight on the Document. The Codec selector picks the format; Codec::Auto resolves from a file's extension (or sniffs native bytes). load / loadString replace the document's contents.
bool load(const std::string& path, Codec codec = Codec::Auto);
bool loadString(std::string_view bytes, Codec codec);
std::string toString(Codec codec = Codec::Prism) const;
bool save(const std::string& path, Codec codec = Codec::Auto) const;
bool loadComposed(const std::string& path);
load reads a file (false on a read/parse failure); loadString parses in-memory bytes in a given codec; toString renders the document — the .prisma ASCII by default, any codec's bytes otherwise; save writes a file (Auto picks the format by extension); loadComposed loads a file and flattens every reference it transitively names into this document.
Document doc;
doc.load("notes.md", Codec::Markdown); // or load("notes.md") — Auto picks by extension
std::string text = doc.toString(); // the .prisma ASCII, to read it back
doc.save("notes.html", Codec::Html); // emit through the shared model
Python binds all five as load, load_string, to_string, save, and load_composed, using the kg.Codec enum. to_string returns a str for text codecs and bytes for PRISM_BINARY / BLOB.
doc.load("notes.md", kg.Codec.MARKDOWN) # or doc.load("notes.md") — AUTO by extension
text = doc.to_string() # .prisma ASCII
doc.save("notes.html", kg.Codec.HTML)
doc.load_composed("scene.prisma") # flatten every reference into this document
Serialization entry points
In C++ the format functions live with the codecs (serialize / deserialize, plus the codec I/O methods above). In Python they are module-level functions, with Document.serialize as a convenience that forwards to the free function.
text = kg.serialize(doc) # canonical .prisma ASCII (str)
binary = kg.serialize(doc, binary=True) # .prism binary (bytes)
again = kg.deserialize(text) # parse .prisma text or .prism binary (auto-detected) → Document
kg.serialize(doc, binary=False)— the document's canonical bytes:.prismaASCII (str) by default, or.prismbinary (bytes) withbinary=True.kg.deserialize(data)— parse.prismatext or.prismbinary (auto-detected) into a Document; raisesPrismErroron invalid input.kg.compose_file(path)— load a file from disk and compose it: everyreferenceis resolved relative to each document's directory and flattened into one Document. RaisesPrismErroron a missing/malformed file or a reference cycle. For trusted document sets only.kg.decode(data, codec)/kg.encode(doc, codec)— normalize in-memory bytes in a codec into a Document, and render a Document as a codec's bytes. The method formsdoc.load_string/doc.to_stringusually read better.doc.serialize()— a method shorthand forkg.serialize(doc).
scene = kg.compose_file("scene.prisma") # references resolved and flattened
md = kg.decode(markdown_text, kg.Codec.MARKDOWN)
html = kg.encode(scene, kg.Codec.HTML)
From here, values are the atoms a Document holds, properties add a name and time, and connections let one value flow into another.