File format spec
§20
Decisions relative to the teardown (§10)
Status
Stable (format version
1.0.0, frozen 2026-10). Backward compatible within 1.x (§16.4). Supersedes the draft v0.1, kept as a historical record; 0.1 files open through the 0.1 → 1.0 migration (§16.2).Schema
https://getkimo.design/schema/kimo-1.0.json (JSON Schema draft 2020-12), generated into packages/core-schema/schema/kimo-1.0.schema.json . The 0.1 schema stays published at …/kimo-0.1.json (schema/kimo-0.1.schema.json, frozen).Reference
@kimo/core-schema (packages/core-schema). Where this document and the implementation disagree, that is a bug in one of them; file an issue.Plan of record
docs/teardown.md §10. Every place where this spec resolves an ambiguity in, or deviates from, that section is listed in §20.License
MIT
| # | Topic | Decision |
|---|---|---|
| 1 | Version | The draft format was 0.1.0 with $schema …/kimo-0.1.json; the frozen format is 1.0.0 with …/kimo-1.0.json, matching the teardown sketch. §19.1 is the teardown example in canonical form. Files newer than 1.0 (1.1.0, 2.0.0) open read-only (fixtures forward/newer-minor.kimo, forward/newer-major.kimo). |
| 2 | Icon glyph field | Renamed from name to icon: in the sketch Icon.name collided with the node's name (layer name) from Placement. |
| 3 | Derived components | Instances MAY carry component. The 10.5 example needs this: ui:BtnLabel01 is overridden through both ui:BtnPrimary and ui:BtnGhost01, and ids are unique, so ui:BtnGhost01 must be an instance of ui:BtnPrimary. Paths look through derived components (§9.5). |
| 4 | Undetailed node types | group, rect, ellipse, polygon, path, note, prompt, script, browser get fields based on teardown §4.7 under Kimo naming (d, fillRule, viewBox, sides, innerRadius, startAngle, sweepAngle, model, src, inputs, url, device, zoom, scrollX, scrollY). w/h are required where nothing else determines the size (rect, ellipse, polygon, icon, script, browser). The script and browser feature flags are named node/script and node/browser. |
| 5 | Key order | The teardown requires a "canonical key order" without defining it. This spec uses one global ranking (Appendix A) chosen so the example's node lines are reproduced, plus code-unit order for records and unknown data (§15.2), which keeps newer files byte-stable in older readers. |
| 6 | Table layout | The one-entry-per-line, leading-comma, blank-line layout (rule 2) also applies to variables and assets. Inline values are compact JSON; the example's spaced header ({ "name": … }) is illustrative jsonc. |
| 7 | Defaults table | Extended with the rows marked in DEFAULTS_ADDED_IN_SPEC (e.g. locked, component, radii, ellipse angles, fillRule, paint opacity/blend/visible, shadow blend, color alpha, browser fields). x/y are not defaulted. The example writes "fit":"cover", a default, which canonical writers omit. |
| 8 | Defaults and overrides | Defaults are never stripped or applied at the top level of props and set (they override); nested values are normalized (§14). |
| 9 | Asset manifest | New optional header member assets (§12), so asset references can be checked without the folder. Header order: …, variables, assets, components, nodes. |
| 10 | Override details | set is shallow and cannot change identity/tree fields; a slot override MUST target a frame with slot; on a nested instance, set uses instance fields; precedence is defined (§9.3). |
| 11 | Declared props | boolean props cannot target instances; enum options are validated against the target field; slot props take no args; default is informative (§9.6). |
| 12 | Themed values | "Themed" is defined structurally (non-empty array of objects that all have value), which disambiguates shadow-array values (§10.1). |
| 13 | Feature gates | Apply inside props/set too; a default-valued property never needs its feature (so applyDefaults keeps documents valid). |
| 14 | Upgrade diff | Rule 6 says format is "the single header line that changes"; $schema embeds the version, so it changes too. |
| 15 | Rule 7 conformance | Newer files open read-only, so "open, edit elsewhere, save" is tested at the library level (re-serialization after an edit), not through the app's save path. |
| 16 | Validation of other versions | Newer documents are not validated beyond the version; older ones must be migrated first. |
| 17 | Library-of-library | References that cross into a library imported by a library are not resolved by 1.0 validators (decided for 1.0: kept; resolving them needs a loader contract for transitive libraries, which a later minor can add without breaking 1.0 files). |
| 18 | Colors | #RGBA is not allowed (sketch lists #RGB, #RRGGBB, #RRGGBBAA); OKLCH hue is [0, 360). |
| 19 | JSON details | Duplicate keys are rejected; -0 is written 0; features are sorted. |
| 20 | Text fill | The default text fill rule (§8.3) is a resolution rule, not a serialized default; fill: [] is respected and linted. |
| 21 | Merge results | Measured, not inferred: two independent-edit patterns still conflict in plain git merge (§18). |
| 22 | Clipped-content lint | Teardown §10.6 lists a "clipped content" lint; it needs computed layout. Decided for 1.0: lints are not part of the format (§17), so it can ship in any release without a format change. |
| 23 | Derived component props | Not covered by the teardown: a derived component inherits the declared props of the component it instantiates, its own declarations merged over them (§9.6). |
| 24 | Slot suggestions | slot.suggested stays advisory for content, but each entry must name a component, like every other QId (§5.5, §11). |
| 25 | M2 styling (ADR 0010) | Additive, still format 0.1.0 (draft): node blend (LayerBlendMode, default normal, with pass-through for containers), stroke miterLimit (default 4), visible on every effect (default true), icon strokeWidth (stroke sets) and weight for phosphor. The teardown's Graphics sketch has no layer blend or effect visibility; the editor's Layer and Effects sections need both. No new feature flag: a reader that predates these fields reports UNKNOWN_PROPERTY and preserves them (§16.3). |
| 26 | M3 components and tokens (ADR 0011) | Additive, still 0.1.0: PropDef order (declaration order of a record-valued component.props); a prop target may name the declaring component itself (the root; board 12's size enum sets the root's padding); node meta key kimo.component.description and root meta key kimo.themes.order (axis display order, string[]; the themes record is sorted by name) in the kimo namespace; expansion rules (§9.8) incl. the component root's theme pins and library-namespace rewriting. Known limit kept: one override per path, so an instance cannot restyle a slot frame and fill it (no M3 design needs it). |
| 27 | M4 stored merge conflicts (ADR 0012) | Additive, still 0.1.0, behind feature merge/conflicts: root members merge and conflicts (§21). They are root-only and not added to Appendix A (which stays frozen); the serializer ranks them after components and lays conflicts out as a table. A reader without the feature opens such a file read-only, so it never re-serializes it. The teardown (§10.2 rule 8) only says kimo merge is semantic; the board 18 design needs conflicts the editor can load, so they are stored in the file rather than as text markers. |
| 28 | M6 note and prompt nodes (ADR 0014) | Additive, still 0.1.0: author on note/prompt; prompt model also accepts the struct ModelRef {provider, model} (the bare string stays valid but is legacy); prompt target (Id); lint PROMPT_TARGET_MISSING; placeholder.by holds the owner's origin. Appendix A stays frozen, so author and provider are unranked keys: they serialize after the ranked keys in code-unit order (…"model":{"model":…,). |
| 29 | 1.0 freeze | Format 1.0.0 = format 0.1 as of rows 1–28, declared stable; no field, default, node type or feature changed. Open items decided: mesh points and the shader protocol stay opaque (stored and round-tripped, rendering optional; a later minor defines them only through new properties or kinds, §16.4); library-of-library references stay unresolved (row 17); planned lints are not format (row 22). Migration 0.1 → 1.0 rewrites only the header. kimo://schema/1.0 is the MCP resource name of the schema. |
| 30 | v1 completeness (board 27), before the 1.0 tag | Additive, no feature flag, no migration (no 1.0 file was published before them): root pages (record Id → {order, name}) and the root-only placement field page (§5.6), lints PAGE_UNKNOWN / PAGE_NOT_ROOT (warnings, so a concurrent page delete never makes a document invalid); polygon star: {ratio} (default 0.38); image fit: "crop" with the crop semantics of §7.1. Pages are a header record rather than page container nodes so that every reader, exporter and tool that walks parent: null roots keeps working unchanged, and a page edit merges per id. pages, page, star and ratio are unranked (Appendix A stays frozen): pages follows components at the root, page and star sort after the ranked node keys. A file without pages is exactly the one-page document it always was. |
| 31 | Enum prop targets (board 27 §8) | Additive, no feature flag, no migration: EnumPropDef targets (record option → {target, field, value}[], field may be <property>/<key>), and target/field become optional (both or neither; then targets is required). Lets one option set several properties on several layers (board 27 "Enum targets": "old opacity-only files migrate to one opacity target per option" is an editor rewrite on edit, not a file migration). targets is unranked (Appendix A stays frozen) and serializes after options. Fixtures: valid/, invalid/. |
| 32 | Rich-text span color (board 30) | Additive, behind the existing feature text/runs, no migration: TextSpan fill (Color: a solid color or a color binding) paints one span over the node fill. Writers merge equal adjacent spans and fall back to a plain string when all spans are equal (§8.1). |
| 33 | Mesh and shader rendering (informative) | No format change, no feature or version bump: §7.1 mesh points and the shader protocol stay opaque and rendering stays optional (row 29). Kimo now renders both with pen.dev semantics so imported pen.dev designs draw fully. Mesh: cols × rows vertices row-major; each cell is a Coons patch bounded by cubic Béziers, handles from the point's left/right/top/bottomHandle (normalized offsets) or automatic (Catmull-Rom: ⅙ of the neighbour-to-neighbour vector, ⅓ toward the single neighbour on an edge); colors blend the four patch corners with smoothstep weights in sRGB (fitted to pen.dev output); clipped to the node shape like any fill. Shader: src (resolved against the .kimo's directory) is a WebGL 1.0 / GLSL ES 1.00 fragment shader; gl_FragCoord (bottom-left origin) and @resolution are in design px whatever the zoom or export scale, as in pen.dev; gl_FragColor is premultiplied; @time is seconds on screen (frozen at 0 under prefers-reduced-motion) and 0 in static exports; @mouse is 0. uniforms override annotated defaults by name: numbers, booleans, #RRGGBB[AA] (→ vec3/vec4 0..1), arrays of 2–4 numbers, { "$ref" } bindings; a missing one takes @default, else 0. Samplers (@sdf, @backdrop, images) and textureSize are not supported: such shaders, and ones that fail to compile, draw nothing and are reported. HTML/CSS/React export approximates a mesh with radial gradients, SVG/PDF use its mean color; shaders are exported only to PNG. |
| 34 | Fill shares from a zero basis (clarification) | No format change, no feature or version bump: §6.1 now states how main-axis fill shares space, which 1.0 left to "like flex-grow" and Kimo's engine had resolved from the content size (flex-basis: auto). Aligned with pen.dev and Figma: in a parent with a definite main size (fixed, or a fill whose parent is definite) that does not wrap, fill items share the space from a zero basis by weight, content and padding ignored (flex: <weight> 1 0, min-width/height: 0; CSS and Yoga floor a zero basis at the padding, so writers give padded siblings bases proportional to their weights: weight × the largest main-axis padding / weight). A fit parent keeps the content basis (a zero basis would size the children to 0 and so the parent), and so does a wrapping parent (lines break on the content sizes). Documents whose fill siblings had unequal content lay out differently: they now match pen.dev (e.g. a 480 px row with a 130 px label and two fill texts gives both texts 163 px, where content-based sharing gave 127.5 / 198.5 and taller wrapped text). |
1
Topic Version
Decision The draft format was
0.1.0 with $schema …/kimo-0.1.json; the frozen format is 1.0.0 with …/kimo-1.0.json, matching the teardown sketch. §19.1 is the teardown example in canonical form. Files newer than 1.0 (1.1.0, 2.0.0) open read-only (fixtures forward/newer-minor.kimo, forward/newer-major.kimo).2
Topic Icon glyph field
Decision Renamed from
name to icon: in the sketch Icon.name collided with the node's name (layer name) from Placement.3
Topic Derived components
Decision Instances MAY carry
component. The 10.5 example needs this: ui:BtnLabel01 is overridden through both ui:BtnPrimary and ui:BtnGhost01, and ids are unique, so ui:BtnGhost01 must be an instance of ui:BtnPrimary. Paths look through derived components (§9.5).4
Topic Undetailed node types
Decision
group, rect, ellipse, polygon, path, note, prompt, script, browser get fields based on teardown §4.7 under Kimo naming (d, fillRule, viewBox, sides, innerRadius, startAngle, sweepAngle, model, src, inputs, url, device, zoom, scrollX, scrollY). w/h are required where nothing else determines the size (rect, ellipse, polygon, icon, script, browser). The script and browser feature flags are named node/script and node/browser.5
Topic Key order
Decision The teardown requires a "canonical key order" without defining it. This spec uses one global ranking (Appendix A) chosen so the example's node lines are reproduced, plus code-unit order for records and unknown data (§15.2), which keeps newer files byte-stable in older readers.
6
Topic Table layout
Decision The one-entry-per-line, leading-comma, blank-line layout (rule 2) also applies to
variables and assets. Inline values are compact JSON; the example's spaced header ({ "name": … }) is illustrative jsonc.7
Topic Defaults table
Decision Extended with the rows marked in
DEFAULTS_ADDED_IN_SPEC (e.g. locked, component, radii, ellipse angles, fillRule, paint opacity/blend/visible, shadow blend, color alpha, browser fields). x/y are not defaulted. The example writes "fit":"cover", a default, which canonical writers omit.8
Topic Defaults and overrides
Decision Defaults are never stripped or applied at the top level of
props and set (they override); nested values are normalized (§14).9
Topic Asset manifest
Decision New optional header member
assets (§12), so asset references can be checked without the folder. Header order: …, variables, assets, components, nodes.10
Topic Override details
Decision
set is shallow and cannot change identity/tree fields; a slot override MUST target a frame with slot; on a nested instance, set uses instance fields; precedence is defined (§9.3).11
Topic Declared props
Decision
boolean props cannot target instances; enum options are validated against the target field; slot props take no args; default is informative (§9.6).12
Topic Themed values
Decision "Themed" is defined structurally (non-empty array of objects that all have
value), which disambiguates shadow-array values (§10.1).13
Topic Feature gates
Decision Apply inside
props/set too; a default-valued property never needs its feature (so applyDefaults keeps documents valid).14
Topic Upgrade diff
Decision Rule 6 says
format is "the single header line that changes"; $schema embeds the version, so it changes too.15
Topic Rule 7 conformance
Decision Newer files open read-only, so "open, edit elsewhere, save" is tested at the library level (re-serialization after an edit), not through the app's save path.
16
Topic Validation of other versions
Decision Newer documents are not validated beyond the version; older ones must be migrated first.
17
Topic Library-of-library
Decision References that cross into a library imported by a library are not resolved by 1.0 validators (decided for 1.0: kept; resolving them needs a loader contract for transitive libraries, which a later minor can add without breaking 1.0 files).
18
Topic Colors
Decision
#RGBA is not allowed (sketch lists #RGB, #RRGGBB, #RRGGBBAA); OKLCH hue is [0, 360).19
Topic JSON details
Decision Duplicate keys are rejected;
-0 is written 0; features are sorted.20
Topic Text fill
Decision The default text fill rule (§8.3) is a resolution rule, not a serialized default;
fill: [] is respected and linted.21
Topic Merge results
Decision Measured, not inferred: two independent-edit patterns still conflict in plain
git merge (§18).22
Topic Clipped-content lint
Decision Teardown §10.6 lists a "clipped content" lint; it needs computed layout. Decided for 1.0: lints are not part of the format (§17), so it can ship in any release without a format change.
23
Topic Derived component props
Decision Not covered by the teardown: a derived component inherits the declared props of the component it instantiates, its own declarations merged over them (§9.6).
24
Topic Slot suggestions
Decision
slot.suggested stays advisory for content, but each entry must name a component, like every other QId (§5.5, §11).25
Topic M2 styling (ADR 0010)
Decision Additive, still format
0.1.0 (draft): node blend (LayerBlendMode, default normal, with pass-through for containers), stroke miterLimit (default 4), visible on every effect (default true), icon strokeWidth (stroke sets) and weight for phosphor. The teardown's Graphics sketch has no layer blend or effect visibility; the editor's Layer and Effects sections need both. No new feature flag: a reader that predates these fields reports UNKNOWN_PROPERTY and preserves them (§16.3).26
Topic M3 components and tokens (ADR 0011)
Decision Additive, still
0.1.0: PropDef order (declaration order of a record-valued component.props); a prop target may name the declaring component itself (the root; board 12's size enum sets the root's padding); node meta key kimo.component.description and root meta key kimo.themes.order (axis display order, string[]; the themes record is sorted by name) in the kimo namespace; expansion rules (§9.8) incl. the component root's theme pins and library-namespace rewriting. Known limit kept: one override per path, so an instance cannot restyle a slot frame and fill it (no M3 design needs it).27
Topic M4 stored merge conflicts (ADR 0012)
Decision Additive, still
0.1.0, behind feature merge/conflicts: root members merge and conflicts (§21). They are root-only and not added to Appendix A (which stays frozen); the serializer ranks them after components and lays conflicts out as a table. A reader without the feature opens such a file read-only, so it never re-serializes it. The teardown (§10.2 rule 8) only says kimo merge is semantic; the board 18 design needs conflicts the editor can load, so they are stored in the file rather than as text markers.28
Topic M6 note and prompt nodes (ADR 0014)
Decision Additive, still
0.1.0: author on note/prompt; prompt model also accepts the struct ModelRef {provider, model} (the bare string stays valid but is legacy); prompt target (Id); lint PROMPT_TARGET_MISSING; placeholder.by holds the owner's origin. Appendix A stays frozen, so author and provider are unranked keys: they serialize after the ranked keys in code-unit order (…"model":{"model":…,"provider":…},"target":…,"author":… ).29
Topic 1.0 freeze
Decision Format
1.0.0 = format 0.1 as of rows 1–28, declared stable; no field, default, node type or feature changed. Open items decided: mesh points and the shader protocol stay opaque (stored and round-tripped, rendering optional; a later minor defines them only through new properties or kinds, §16.4); library-of-library references stay unresolved (row 17); planned lints are not format (row 22). Migration 0.1 → 1.0 rewrites only the header. kimo://schema/1.0 is the MCP resource name of the schema.30
Topic v1 completeness (board 27), before the 1.0 tag
Decision Additive, no feature flag, no migration (no 1.0 file was published before them): root
pages (record Id → {order, name}) and the root-only placement field page (§5.6), lints PAGE_UNKNOWN / PAGE_NOT_ROOT (warnings, so a concurrent page delete never makes a document invalid); polygon star: {ratio} (default 0.38); image fit: "crop" with the crop semantics of §7.1. Pages are a header record rather than page container nodes so that every reader, exporter and tool that walks parent: null roots keeps working unchanged, and a page edit merges per id. pages, page, star and ratio are unranked (Appendix A stays frozen): pages follows components at the root, page and star sort after the ranked node keys. A file without pages is exactly the one-page document it always was.31
Topic Enum prop targets (board 27 §8)
Decision Additive, no feature flag, no migration: EnumPropDef
targets (record option → {target, field, value}[], field may be <property>/<key>), and target/field become optional (both or neither; then targets is required). Lets one option set several properties on several layers (board 27 "Enum targets": "old opacity-only files migrate to one opacity target per option" is an editor rewrite on edit, not a file migration). targets is unranked (Appendix A stays frozen) and serializes after options. Fixtures: valid/components-enum-targets.kimo , invalid/PROP_TARGET_INVALID--enum-target-field.kimo .32
Topic Rich-text span color (board 30)
Decision Additive, behind the existing feature
text/runs, no migration: TextSpan fill (Color: a solid color or a color binding) paints one span over the node fill. Writers merge equal adjacent spans and fall back to a plain string when all spans are equal (§8.1).33
Topic Mesh and shader rendering (informative)
Decision No format change, no feature or version bump: §7.1 mesh
points and the shader protocol stay opaque and rendering stays optional (row 29). Kimo now renders both with pen.dev semantics so imported pen.dev designs draw fully. Mesh: cols × rows vertices row-major; each cell is a Coons patch bounded by cubic Béziers, handles from the point's left/right/top/bottomHandle (normalized offsets) or automatic (Catmull-Rom: ⅙ of the neighbour-to-neighbour vector, ⅓ toward the single neighbour on an edge); colors blend the four patch corners with smoothstep weights in sRGB (fitted to pen.dev output); clipped to the node shape like any fill. Shader: src (resolved against the .kimo's directory) is a WebGL 1.0 / GLSL ES 1.00 fragment shader; gl_FragCoord (bottom-left origin) and @resolution are in design px whatever the zoom or export scale, as in pen.dev; gl_FragColor is premultiplied; @time is seconds on screen (frozen at 0 under prefers-reduced-motion) and 0 in static exports; @mouse is 0. uniforms override annotated defaults by name: numbers, booleans, #RRGGBB[AA] (→ vec3/vec4 0..1), arrays of 2–4 numbers, { "$ref" } bindings; a missing one takes @default, else 0. Samplers (@sdf, @backdrop, images) and textureSize are not supported: such shaders, and ones that fail to compile, draw nothing and are reported. HTML/CSS/React export approximates a mesh with radial gradients, SVG/PDF use its mean color; shaders are exported only to PNG.34
Topic Fill shares from a zero basis (clarification)
Decision No format change, no feature or version bump: §6.1 now states how main-axis
fill shares space, which 1.0 left to "like flex-grow" and Kimo's engine had resolved from the content size (flex-basis: auto). Aligned with pen.dev and Figma: in a parent with a definite main size (fixed, or a fill whose parent is definite) that does not wrap, fill items share the space from a zero basis by weight, content and padding ignored (flex: <weight> 1 0, min-width/height: 0; CSS and Yoga floor a zero basis at the padding, so writers give padded siblings bases proportional to their weights: weight × the largest main-axis padding / weight). A fit parent keeps the content basis (a zero basis would size the children to 0 and so the parent), and so does a wrapping parent (lines break on the content sizes). Documents whose fill siblings had unequal content lay out differently: they now match pen.dev (e.g. a 480 px row with a 130 px label and two fill texts gives both texts 163 px, where content-based sharing gave 127.5 / 198.5 and taller wrapped text).Keywords:
MUST
MUST NOT
SHOULD
MAY
set in small mono caps wherever they are normative