Your first document
Get the library
There are two ways to get Kinogaki Core.
With a coding agent. Point it at agent.kinogaki.com and paste:
Read agent.kinogaki.com and install Kinogaki Core into my project.
The agent reads the download URLs, checksums, and build flags from that page, pulls the universal static library and headers from the CDN, checksum-verifies them, and wires up the include and link flags.
Yourself. For Python, pip install kinogaki.
For C++ by hand, Install → covers the download, checksum, and compiler flags step by step. The download is unsigned and self-contained, with the codecs built in. Everything lives under the kinogaki:: namespace and the kinogaki/ include root.
A document by hand
A .prisma file is the ASCII form of a document: plain, diff-clean, the form you author and commit. Writing one shows the model directly:
#prisma 4.0
def group "world" {
def material "red" {
float3 out = (0.9, 0.15, 0.15)
}
def object "ball" {
float3 position = (0, 1, 0)
float radius = 1.5
float3 albedo = (1, 1, 1)
connect albedo = </world/red.out>
}
}
Three elements, one connection. def <type> "<name>" { … } declares an element; dtype name = value is a typed property; connect name = <path> connects a property to another's output (the consumer names its source). Hierarchy is the nesting, and the path of the ball is /world/ball.
The same document in code
The API mirrors the model: append an element at a path, then set typed values on the chainable handle. In C++ values are constructed explicitly (Float(…), Float3(…)) so a scalar reads like a vector; in Python the binding infers the type from native values.
#include "kinogaki/Document.h"
using namespace kinogaki;
Document doc;
doc.append(Path("/world"), "group"); // the parent must exist first
doc.append(Path("/world/red"), "material").set("out", Float3(0.9, 0.15, 0.15));
doc.append(Path("/world/ball"), "object")
.set("position", Float3(0, 1, 0))
.set("radius", Float(1.5))
.set("albedo", Float3(1, 1, 1));
doc.connect(Path("/world/red.out"), Path("/world/ball.albedo")); // source output drives target input
doc.save("world.prisma"); // the same .prisma you wrote by hand, byte for byteimport kinogaki as kg
doc = kg.Document()
doc.append("/world", "group") # the parent must exist first
doc.append("/world/red", "material").set("out", (0.9, 0.15, 0.15))
doc.append("/world/ball", "object") \
.set("position", (0, 1, 0)) \
.set("radius", 1.5) \
.set("albedo", (1, 1, 1))
doc.connect("/world/red.out", "/world/ball.albedo") # source output drives target input
doc.save("world.prisma") # the same .prisma you wrote by hand, byte for byteRead it back
eval resolves a slot through its connections and time samples:
Document doc;
doc.load("world.prisma");
Value albedo = doc.eval(Path("/world/ball.albedo")); // → (0.9, 0.15, 0.15): the colour flows from the materialdoc = kg.Document()
doc.load("world.prisma")
albedo = doc.eval("/world/ball.albedo") # → (0.9, 0.15, 0.15): the colour flows from the materialValidate from the command line
The CLI checks either encoding and reports a file-absolute location for any error:
$ kinogaki check world.prisma
world.prisma: valid kinogaki document (3 elements, 1 connections)
That is the entire loop: author, save, eval, check.
Compose across files
A document can pull in another. An element declares a reference, and composition resolves it at read time: the referenced element becomes the referencing element, rebased under its Path, and any Property the local file states itself wins. References resolve recursively, so a scene stays a set of small files until you ask for the whole.
A shared part, parts.prisma:
#prisma 4.0
def object "bolt" {
float radius = 0.5
int threads = 24
}
A scene that places one and overrides its radius, scene.prisma:
#prisma 4.0
def object "bolt_1" {
reference "parts.prisma" // become the library bolt, resolved from this file's directory
float radius = 0.7 // a local opinion; it wins over the reference
}
composeFile reads the scene and flattens every reference into one standalone Document:
#include "kinogaki/Compose.h"
using namespace kinogaki;
Document scene = *composeFile("scene.prisma");
scene.eval(Path("/bolt_1.threads")); // → 24, supplied by the reference
scene.eval(Path("/bolt_1.radius")); // → 0.7, the local opinion winsscene = kg.compose_file("scene.prisma")
scene.eval("/bolt_1.threads") # → 24, supplied by the reference
scene.eval("/bolt_1.radius") # → 0.7, the local opinion winsThe reference is sugar over the model: it is reserved metadata that composeFile reads, so an uncomposed scene.prisma is still a plain document, and the parts stay editable on their own. Composition covers references, overlays, and the layer vocabulary in full.
Patch a document with a layer
A reference pulls a whole file in. A layer does the opposite: it states only what changes. A layer is itself a .prisma document, sparse by design, holding the few properties and elements an edit touches. overlay bakes a base and a layer into one standalone document, and the result is plain .prisma you can read.
A base scene, world.prisma:
#prisma 4.0
def group "world" {
def object "ball" {
float3 position = (0, 1, 0)
float radius = 1.5
float3 albedo = (1, 1, 1)
}
}
A layer that grows the ball and adds a shadow, edit.prisma:
#prisma 4.0
def group "world" {
def object "ball" {
float radius = 2.0 // the one property this edit changes
}
def object "shadow" { // a new element, appended under /world
float3 position = (0, 0, 0)
float radius = 2.5
}
}
overlay applies the layer over the base:
#include "kinogaki/Compose.h"
using namespace kinogaki;
Document base, layer;
base.load("world.prisma");
layer.load("edit.prisma");
overlay(base, layer).save("baked.prisma");base = kg.Document(); base.load("world.prisma")
layer = kg.Document(); layer.load("edit.prisma")
base.overlay(layer).save("baked.prisma")$ kinogaki overlay world.prisma edit.prisma > baked.prisma
baked.prisma is one document. The ball keeps the position and albedo the layer never mentioned, takes the new radius, and gains a shadow sibling:
#prisma 4.0
def group "world" {
def object "ball" {
float3 albedo = (1, 1, 1)
float3 position = (0, 1, 0)
float radius = 2
}
def object "shadow" {
float3 position = (0, 0, 0)
float radius = 2.5
}
}
A property the layer states wins; a property it omits falls through from the base. diff runs the other direction: base.diff(target) returns the minimal layer that turns one document into another, so an edit, a saved revision, or an agent's proposal travels as a small patch and bakes back to the whole. Composition covers layers, references, and the full vocabulary.
Normalizing external data: Markdown → Prism → HTML
Authoring a document by hand is only half the story. The format's deeper job is normalization: taking data that arrives in someone else's format and turning it into one typed, path-addressed model your tools and agents can safely read and edit. Prism is the neutral form in the middle; a codec maps each foreign format in and back out, every codec of a kind targeting the same model.
Take an ordinary Markdown file:
# Release notes
Ship **faster** with a [changelog](https://kinogaki.com/log).
- typed model
- any format
Normalizing it is one call. doc.load(path) reads a file and picks the codec from the extension (pass a Codec to force it). The Markdown codec lands it on the Document model: a tree of typed elements whose blocks and inline runs are nameless elements, so their order is their identity and the file reads as the structure itself:
#include "kinogaki/Codecs.h"
using namespace kinogaki;
Document doc;
doc.load("notes.md"); // Markdown → Document (Codec::Auto picks it from the .md extension)
std::string prisma = doc.toString(); // the normalized document, as .prisma ASCIIimport kinogaki as kg
doc = kg.Document()
doc.load("notes.md") # Markdown → Document (codec from the .md extension)
prisma = doc.to_string() # the normalized document, as .prisma textEither way prisma is:
#prisma 4.0
def document "document" {
def heading {
int32 level = 1
def text {
str text = "Release notes"
}
}
def paragraph {
def text {
str text = "Ship "
}
def strong {
def text {
str text = "faster"
}
}
def text {
str text = " with a "
}
def link {
str href = "https://kinogaki.com/log"
def text {
str text = "changelog"
}
}
def text {
str text = "."
}
}
def list {
bool ordered = false
def item {
def paragraph {
def text {
str text = "typed model"
}
}
}
def item {
def paragraph {
def text {
str text = "any format"
}
}
}
}
}
That is now a plain, diff-clean Prism document: inspectable, queryable, and editable through the same C++ API, Python binding, or C ABI as anything you author by hand. A program (or an agent) operates on heading, paragraph, link elements and typed href/text values, reading the structured document rather than raw Markdown bytes.
Saving it as HTML is the same call with a different codec: the HTML codec writes the same model directly, with no Markdown-to-HTML adapter:
doc.save("notes.html", Codec::Html);doc.save("notes.html", kg.Codec.HTML)<h1>Release notes</h1>
<p>Ship <strong>faster</strong> with a <a href="https://kinogaki.com/log">changelog</a>.</p>
<ul>
<li>typed model</li>
<li>any format</li>
</ul>
A round-trip back to the same format is the identity; cross-format loss is defined, never silent. This stops external data from being opaque: normalize once into Prism, then read, edit, and save it in any format the codecs cover. (These docs are built this way: authored in Markdown, normalized through this model, rendered to the page you're reading.)
The rest of this manual goes deep on each piece: the model, values, properties and time, connections, composition, the formats, codecs, and the C++ API.