K2F format specification v0.3

Version: 0.3 (on-disk package contract)

K2F (Key-to-Flow) is a ZIP document format. Authors write meaning as JSON. One reference engine compiles that meaning into geometry. Every viewer and every export paints the compiled lock, so output matches.

This page is the readable contract: container layout, processing, integrity, and rules JSON Schema cannot express. Exact fields live in the format JSON Schemas (schema/*.json in the repo). How to write a package: Authoring and Allowed keys. Engine matching: COMPATIBILITY.md.

This spec line is released with K2F SDK 0.3.x (Rust engine, Python k2f, npm @openk2f/k2f). SDK patch releases keep this document unless embedded schemas or the lock format change. Each compiled lock still records engine_version (full compiler semver) for appearance_hash — that is provenance, not a second public spec number.

A breaking change to schemas or the lock format bumps this spec version and ships in the same SDK release.

This spec describes the packed .K2F ZIP. An author directory may omit schema/ (pack injects the five format schemas) and changelog.json (pack writes {"entries":[]}).

Model

State A   content/root.json      semantic tree — you edit this
   ↓      pack / compile
State C   document.K2F.lock      geometry + paint — viewers paint this

Opening a .K2F file is not compiling. The engine is not a third file in the package.

Lengths are millipt integers (1 pt = 1000): page size, margins, font size, padding, gaps, and image size.

v0.3 is paged only (canvas_mode is always "paged"). Slide and infinite-canvas modes are not in this spec.

Contents

Container

A .K2F file is a ZIP archive. Unexpected paths are rejected (UNEXPECTED_PATH).

PathRequiredPurpose
manifest.jsonyesTitle, page size, margins, engine version
content/root.jsonyesSemantic tree entry (may include other content JSON)
content/**/*.jsonwhen referencedOne semantic node per file; only paths referenced from root.json (directly or transitively) are allowed
styles/theme.jsonyesRole-based appearance
styles/tokens.jsonnoOptional extra JSON stored with the package; not a format schema and not required to compile
changelog.jsonyesEdit history ({"entries":[]} when empty)
document.K2F.lockafter compileGeometry and render plan
signatures/v1.jsonnoEd25519 signature record
schema/*.jsonyesThe five embedded format schemas
assets/fonts/*yesEmbedded font binaries (at least one .ttf / .otf)
assets/images/*noPNG, JPEG, WebP, or SVG
assets/data/*noStructured data (for example table rows)

License or sidecar files may sit under assets/fonts/; only .ttf / .otf load as faces. Pack rejects files outside assets/fonts/, assets/images/, and assets/data/ (UNEXPECTED_PATH).

Manifest

Required: title, canvas_mode: "paged", page_config (width, height, margin as [top, right, bottom, left] millipt), engine_version. Optional: author, created_at (UTC unix seconds; pack must not stamp wall-clock time), generated_by, running_blocks.

There is no page_config.background. Sheet fill is the root node’s role box_decoration.background (full page, including margins).

running_blocks is an array of { "position": "header" | "footer", "node": <SemanticNode> }. Blocks repeat on every page. They are valid only with canvas_mode: "paged". Form fields are forbidden inside them. Text may use {{page_current}} and {{page_total}} only.

Contract: manifest.schema.json.

Semantic tree

The tree lives in content/root.json. At pack time, { "include": "content/...." } stubs in container children are replaced by the referenced file’s single semantic node (which may contain nested includes). The engine compiles one expanded tree; content_hash binds that tree, not individual files. Unreferenced files under content/ are rejected (UNEXPECTED_PATH).

State A must not contain layout coordinates, arbitrary vector paths, inline CSS, or executable content. Style fields (color, font_size, font_family, padding, text_align, box_decoration, …) belong on the theme role, never on the node.

Contract: nodes.schema.json. Human index: Allowed keys.

Nodes

Every node has:

  • id — stable dotted id, pattern ^[A-Za-z0-9][A-Za-z0-9_]*(\.[A-Za-z0-9][A-Za-z0-9_]*)*$, unique in the document
  • role — a name defined under this package’s theme.roles. There is no format-wide role enum. Nodes have no font_variant; monospace is a role plus font_aliases
  • content — see Content types
  • variant — optional; must exist on that role in the theme
  • modifiers — optional; see Modifiers
  • layout — optional; containers only. See Layout

list_item also requires list_id; depth defaults to 0 and marker_type defaults to bullet.

Content types

content.typeMeaning
textString value. Headings, body, quotes, and inline code differ by role
mathDisplay TeX subset in value. Role must be math
imagevalue.src under assets/images/, plus millipt width and height. PNG, JPEG, WebP, or SVG (rasterized at paint). Not GIF. SVG <text> / <tspan> / <textPath> / <foreignObject> fail (SVG_TEXT)
containerOptional children (nodes or { "include": "content/...." } stubs)
tableNative table: column_widths ({pt} / {fr} only) and data (inline rows of cell nodes, or asset under assets/data/). Optional header_rows, gap, row_gap, column_gap. Cell colspan (default 1) occupies that many tracks in the same row. No rowspan
form_fieldReserved fillable box — Form fields
code_blockListing; role must be code_block, layout is forbidden, modifiers are syntax_highlight only. preserve_whitespace must be true or omitted. Inline code is role: "code" with content.type: "text"
table_referenceExternal table view with declared millipt size

Display math is role: "math" with content.type: "math". Inline math stays on a text node as U+FFFC plus a math modifier whose intent is the TeX. Unknown commands fail compile (MATH_UNSUPPORTED).

Modifiers

On a text node, modifiers is a closed list (max 50). Each item needs type, intent, and range.

  • type: emphasis, link, underline, strikethrough, subscript, superscript, math, syntax_highlight. There are no font-size / font-family / color modifier types
  • intent: a string that must exist under theme.modifiers.styles[type] (URL for link, TeX for math, highlight token, …)
  • range: UTF-8 byte offsets [start, end) on character boundaries — not character indices. \n is 1 byte

For superscript / subscript, the engine applies script sizing (7/10 of the surrounding run) and a deterministic baseline shift. Theme patches may change color or bold on those types, not absolute font_size.

Layout

layout applies to containers. Omit it, or use a vertical stack, for ordinary flow. content/root.json must omit layout or use a vertical stack only — nest grid, overlay, columns, and horizontal stacks under a child.

layout.typeMeaning
stack (default)direction vertical or horizontal; gap; align_items; justify_content (start / center / end only — not CSS space-between); optional millipt width / height
gridRequired columns; optional rows, gap, row_gap, column_gap, cell_align, width / height. Tracks are {pt}, {fr}, or {auto:true} — never bare integers. Omit rows → {auto:true} tracks ceil(n_children / n_columns). Declared rows do not grow. {auto:true} is content-sized, not CSS auto-fit. fr needs a finite outer size on that axis
overlayChildren share one origin; later children paint on top. Height is the max of children. No z-index
columnsContinuous multi-column flow: count 2..=4, optional gap. A child may set column_span: "all"

Walkthroughs: Layout.

Pagination

Optional on any node:

FieldValuesMeaning
break_insideauto (default), avoidauto splits at line or child boundaries when the remainder of the page is too small. avoid never splits
keep_with_nextbooleanKeep this node with the following sibling when both fit
break_beforeauto (default), pagepage starts the node on a new page

Includes

An include stub is { "include": "content/...." } — not a semantic node. The path must match content/<segments>.json, must not be content/root.json, and must point at a file that is a single node. Orphan JSON under content/ fails pack.

Form fields

A form field is a leaf like an image: the engine reserves a box from declared width / height / lines and role metrics. Filling must not reflow following nodes. Role must be form_field and content.type must be form_field. layout and modifiers are forbidden. Set break_inside: "avoid" so the box does not split across pages. placeholder is metadata and is not painted into the lock. value is NFC text and does not drive measure.

JSON fieldMeaning
kindtext | multiline | checkbox (required)
valueNFC string. Checkbox allows only "" or "true"
placeholderViewer-only hint; not in lock ops
width / heightOptional millipt > 0. Omit width in a bounded vertical stack or {fr:1} cell; a horizontal stack child must set width or compile fails FORM_FIELD_UNBOUNDED_WIDTH
linesOptional >= 1. Defaults: text 1, multiline 3. Checkbox ignores lines (square of role font_size)
max_lengthOptional >= 1 character cap on value
requiredOptional; empty required fields still compile (blank templates must lock)

Viewers overlay native controls on the reserved rectangle. Saving writes value and relocks. PDF export may emit AcroForm widgets; that is export, not a package script.

Theme

Appearance lives only in styles/theme.json. palette is the only color table. Role and primitive colors are palette keys or #RRGGBB / #RRGGBBAA. There is no primitives.colors.

SectionPurpose
paletteNamed colors
primitivesNamed surfaces, gradients, shadows, blurs, corners, borders
rolesTypography and decoration keyed by role name
font_aliasesRole font_family → embedded font stem under assets/fonts/
font_facesOptional family → {regular,bold,italic,bold_italic} stems. Omitted slots stay synthetic. Not CSS font-weight numbers
modifiersprecedence plus styles[type][intent] patches

Role default must set font_family, font_size, line_height_mult, and color. Other roles inherit omitted text fields from default. Optional on a role: text_align (start | center | end | justify), bold, italic, letter_spacing_pt, first_line_indent_pt, self_align, list_style, image_fit (contain default, or cover), box_decoration, variants. Container, rule, and image roles need not repeat typography when they do not paint text.

A node’s variant, if set, must exist on that role. Variant keys are only box_decoration, self_align, text_overrides, list_style, and image_fit. Put bold, color, text_align, and font_size under text_overrides, not at the variant root.

box_decoration values are named primitive strings (background, border, corner_radius, shadow, blur) except padding_pt (millipt number or per-edge object). Unknown names fail compile (UNKNOWN_PRIMITIVE). Inline fill or border objects on a role fail schema.

Justify expands U+0020 gaps on non-final wrapped lines only. Tracking is extra advance on every glyph except the last in a shaped run. Role box_decoration.padding_pt applies to text, code, math, form fields, containers, tables, and image nodes.

K2F never uses system fonts. Missing glyphs fail closed (FONT_MISSING_GLYPH).

Contract: styles.schema.json and visual_primitives.schema.json. Walkthrough: Theme and fonts.

Compile and lock

pack / compile turns State A + theme + fonts (+ optional assets) into document.K2F.lock. Do not hand-edit the lock: change the tree or theme, then relock.

The lock records:

  • engine_version — semver of the compiling engine
  • engine_commit_sha — git commit of the engine build (release builds must not use UNKNOWN)
  • Geometry — page tree with millipt positions and sizes
  • Render plan — ordered paint operations (DrawText, DrawImage, fills, …)
  • Hashes — content_hash (expanded semantic tree) and appearance_hash (semantic + theme + fonts + page config + engine identity)

Viewers paint the lock only. PDF, PPTX, DOCX, and IDML are one-way drawings of that lock, not a second source.

Integrity verification

k2f verify and inspect_package return status codes:

CodeMeaning
VALIDLock matches semantic content, theme, fonts, and the reader's engine identity
UNSIGNEDSelf-consistent unsigned package (hash VALID or ENGINE_MISMATCH)
SIGNEDSelf-consistent package and valid Ed25519 signature
SIGNED_BUT_BROKENSignature present but hash or crypto check failed
UNLOCKEDNo document.K2F.lock
CONTENT_CHANGEDSemantic tree changed since lock
APPEARANCE_CHANGEDTheme, fonts, page config, or unbound engine identity changed since lock
ENGINE_MISMATCHHash chain matches the lock's recorded engine; reader's engine_version / engine_commit_sha differs
FONT_MISSINGRequired embedded fonts absent

Additional package errors: SCHEMA_INVALID, NODE_ID, UNEXPECTED_PATH, UNKNOWN_PAINT_OP, PDF_IS_NOT_A_SOURCE, IMAGE_SIZE, SVG_TEXT.

In-browser check: Verify. Banner rules: Integrity.

Viewer banners

Viewers must surface integrity status derived from IntegrityStatus. The banner code is always available via banner() / k2f-open; default UI uses tiered visibility (like PDF viewers):

BannerWhenDefault UI
SIGNEDValid signature and hash chainCompact positive strip
UNSIGNEDSelf-consistent, unsignedHidden (quiet read)
SIGNED_BUT_BROKENSignature file invalid or tamperedProminent warning
BROKEN_INTEGRITYContent, appearance, or font failureProminent warning
UNLOCKEDPackage not compiledCompact draft strip

Embedders may pass banner: "full" for the legacy verbose strip on every state, or banner: "off" / no-banner to hide chrome entirely. Broken states must never be silently treated as signed.

BROKEN_INTEGRITY and SIGNED_BUT_BROKEN must not be treated as signed.

Signing

Signing is a human or organizational step. Agents produce UNSIGNED packages by default. signatures/v1.json records Ed25519 public key, signature over canonical lock JSON, signed_by, and signed_at.

Contract: signatures.schema.json.

Prohibitions

  • No scripts — no JavaScript, WASM, or other executable content inside .K2F
  • No inline CSS in semantic nodes
  • No PDF, PPTX, DOCX, or IDML as source — those files are drawings of the lock (PDF_IS_NOT_A_SOURCE, PPTX_IS_NOT_A_SOURCE, DOCX_IS_NOT_A_SOURCE, IDML_IS_NOT_A_SOURCE)
  • No arbitrary vector paths in State A
  • No system fonts — faces are embedded; missing glyphs fail closed

Embedded schemas

Each packed package embeds copies under schema/ (format contract only):

  • manifest.schema.json
  • nodes.schema.json
  • styles.schema.json
  • visual_primitives.schema.json
  • signatures.schema.json

Agent writing dialects live outside the format (engine/k2f_sdk/profiles/). Schema changes require a spec version bump and matching engine release.

Repo copies: schema/nodes.schema.json and the four siblings above. The skill folder ships the same five files so agents without a checkout can look them up. Do not copy schemas into an author directory.

DocumentWhat it is
IntroductionPackage model and doc map
Getting startedFirst .K2F
Text · Images · Tables · Layout · ThemeHow to author
Allowed keysField cheat sheet
CatalogPackable examples
COMPATIBILITY.mdEngine matching
Integrity · VerifyBanner UI and in-browser check