Format version 1.1.0 Guidance revision 2026-07-05 Page synced 2026-07-14
This page renders the canonical SEF specification. The SuperCards desktop repository is the source of truth; this copy states the format version it reflects.
Format version 1.1.0 Guidance revision 2026-07-05 Page synced 2026-07-14
This page renders the canonical SEF specification. The SuperCards desktop repository is the source of truth; this copy states the format version it reflects.
Format version: 1.1.0
Status: Final draft — open decisions ratified 2026-07-05 (extension, markdown default, one-semantics versioning); pending JP’s final read-through. Payload-kinds amendment (deck sharing, §2.2) added 2026-07-07 per the ratified Phase C+S plan — container-level, no format-version bump
Date: 2026-07-04 (updated 2026-07-05; payload-kinds revision 2026-07-07; §6 pdf-reference fragility note 2026-07-14)
Predecessor: specs/v0.5.3/LLM-integration/import-schema-spec-v2.md (format 1.0.0 — approved 2026-03-24)
Governing decisions: SEF_llm_integration_roadmap.md Decision Record (all six ratified); user journeys in connector_user_journey_map.md
The SuperCards Exchange Format (SEF) is a semantic JSON format with three use cases:
submit_deck), a downloaded file, or an import folder..supercards archive files (new in 1.1).The format intentionally omits spatial layout (coordinates, dimensions, zIndex). Content is described semantically — what each block is, not where it goes — and the layout engine positions it at import. v1.1 adds bounded creative control: rich text via markdown (now the default text format), code blocks, and advisory layout hints.
Carried from v1.0, with one addition:
role), not position.| Exchange Format | Native Format | Translation |
|---|---|---|
deck.* fields | deck.json | Direct mapping + ID generation |
card.front[] / card.back[] | front.elements[] / back.elements[] | Layout engine generates positions |
card.size (new) | CardStyle.size | Per-card override of deck default |
Text block role | ElementStyles (fontSize, bold, …) | Role-to-style table (§9) |
Text block text (plain) | element.content | Direct mapping |
Text block text (markdown) (new) | element.richContent (ProseMirror JSON) + element.content (plain projection) | Markdown parser (§4.5) |
Text block align (new) | Paragraph attrs.textAlign in richContent and styles.textAlign | Uniform by construction — see §5.1 |
| Code block (new) | Text element whose richContent is a single codeBlock node | §3.4.3 |
Block group (new) | (no native field) | Consumed by the layout engine only |
Image src | element.content (bare filename) | Asset resolution + copy to deck assets/ |
| Array order | element.zIndex / card_order.json | Index → zIndex / card order |
The native card format version does not change (formatVersion: "1.0.0" in card/deck JSON). richContent has existed on CardElement since v0.4.3; v1.1 is the first time the import pipeline populates it (today layout_engine.rs always emits rich_content: None).
| Addition | Where |
|---|---|
.supercards single-file archive container | §2.2 |
Markdown as the default text format (plain opt-out) | §3.4.1, §4 |
| One-semantics versioning — all 1.x files read under 1.1 rules | §11 |
code content block type | §3.4.3 |
align layout hint on text blocks | §5.1 |
group layout hint on content blocks | §5.2 |
size per-card override | §3.3, §5.3 |
math block type reserved (not supported) | §3.4.4 |
| Degradation matrix for older importers | §11 |
| Authoring guidance for models | §14 |
Every v1.0 file remains valid and importable. It is interpreted under 1.1 semantics rather than version-gated rules — see §11 for why this is safe and what it deliberately gives up (ratified 2026-07-05).
Manifests use the compound extension .supercards.json (unchanged). Exactly one manifest per import folder or archive; zero or multiple is a fatal error.
.supercards Archive (new — roadmap Decision #5)A .supercards file is a ZIP archive of a deck payload — an import folder (the original 1.1 form) or, since the deck-sharing revision (2026-07-07), a native deck folder. For the SEF-manifest kind the mental model is unchanged: anything valid in an import folder is valid inside the archive, and validation/import semantics are identical after extraction.
Spanish Verbs.supercards (ZIP archive)
├── spanish_verbs.supercards.json # exactly one manifest, at archive root
└── assets/ # optional; referenced files
├── ser_conjugation.png
└── estar_conjugation.png
{Deck Title}.supercards. The extension carries the brand on every shared file; .sc and .cards were evaluated and rejected (collisions — SuperCollider/Scala; existing iFlash/Anki-adjacent flashcard ZIP format). The shorter .sef and .scef were assessed and rejected 2026-07-05: .sef collides with Saxon XSLT’s compiled-stylesheet format (whose JSON flavor is literally .sef.json — a head-on collision with our compound-manifest convention, in an overlapping developer audience) plus the active EDI “Standard Exchange Format” and legacy Windows tools; .scef is collision-free but carries no brand and loses search to the 3GPP acronym. Extension length is functionally free on every target platform — macOS/iOS route by UTI, and AirDrop/share-sheet/mail/Windows impose no length constraints — with same-length category precedent in Flashcard Hero’s .flashcards.assets/ directory; optional source PDFs at root (so pdf: references work self-contained). Loose referenced images may sit at root or in assets/ — resolution checks both, assets/ preferred.stored or deflate entries only.io.supercards.deck, conforming to public.zip-archive + public.data. Registered in the app’s Info.plist so double-click, AirDrop, and drag-onto-app all route to the import inbox.submit_deck.Payload kinds (added 2026-07-07 — Phase C+S plan, Decisions #2/#3): the archive root determines — by parsing, never by filename sniffing — which of exactly two payload kinds the container carries:
| Payload kind | Root marker | Produced by | Import path |
|---|---|---|---|
| SEF manifest | exactly one *.supercards.json | LLM workflows (connector, Skill, universal prompts) | validate → resolve → layout → drip-feed (§8) |
| Native deck | deck.json parsing as a native deck file, no manifest present | Deck sharing: the desktop app’s per-deck export — the native deck folder zipped (deck.json + card_order.json + cards/ + assets/, preview PNGs included) | native structural validation → identity re-stamp → drip-feed |
formatVersion (currently 1.0.0) in deck.json and card files; receivers gate on its major version. supercards_format is not involved — payload-kind detection is container-level and orthogonal to the SEF version string, so this revision does not bump 1.1.0.card_order.json, and deck thumbnails rewritten to match; card createdAt preserved, updatedAt restamped; deck.source preserved when present, else stamped with a shared-deck provenance string. Re-importing the same archive twice yields two independent decks.Archive limits and hardening (validator MUST enforce):
| Rule | Limit / behavior |
|---|---|
| Total archive size | 500 MB hard cap; warning above 100 MB |
| Total decompressed size | 1 GB hard cap (zip-bomb guard) |
| Per-entry decompressed size | 200 MB hard cap |
| Entry count | 2,000 hard cap |
| Entry paths | Reject absolute paths, .. traversal, and backslash-separated paths (zip-slip) |
| Symlink entries | Rejected |
| Nested archives | Not extracted (treated as opaque files; ignored unless referenced) |
| Encrypted entries | Rejected with a clear error |
Unchanged from v1.0: a user- or connector-assembled folder containing exactly one *.supercards.json plus any referenced PDFs/images. Multi-PDF imports remain supported via the pdf:filename:page:N:index:N scheme.
| Artifact | URL |
|---|---|
| SEF hub (start here) | supercards.io/sef |
| Human-readable spec (this document) | supercards.io/sef/spec |
| JSON Schema (machine-readable) | supercards.io/sef/schema.json |
| Model-optimized page (llms.txt-style) | supercards.io/sef/llms.txt |
| Downloadable examples | supercards.io/sef/examples/ |
Prompts and connector tool descriptions reference these instead of inlining the spec. The connector packages also bundle copies so they work offline.
In-repo source of truth: this spec, SEF-schema-v1.1.json, and the example manifests live in the desktop repo’s top-level sef/ directory (sef/README.md documents how every derived copy propagates). The schema is draft 2020-12, authoring-strict (canonical enums, no unknown fields; the Rust validator stays the lenient authority). Verified against the §15 examples, both fixture manifests in sef/examples/, and a negative-case suite (2026-07-05).
{
"supercards_format": "1.1.0",
"deck": { ... },
"cards": [ ... ]
}
| Field | Type | Required | Description |
|---|---|---|---|
supercards_format | string | Yes | Write "1.1.0". Any 1.x string is accepted and read under 1.1 semantics (§11) |
deck | object | Yes | Deck metadata |
cards | array | Yes | Minimum 1 card; maximum 200 (warning above 100) |
Unchanged from v1.0:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | Yes | — | Max 150 chars (truncate + warn) |
summary | string | No | "" | Description |
color | string | No | "#F4F4F4" | Hex; applied to all cards unless overridden |
card_size | enum | No | "medium" | "small" | "medium" | "large" — deck default |
source | string | No | "" | Provenance (e.g., “Generated by Claude from workout.pdf”) |
tags | string[] | No | [] | Reserved |
{
"title": "Grasshoppers",
"tags": ["core"],
"color": "#F4F4F4",
"size": "large",
"front": [ ... ],
"back": [ ... ]
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | No | "" | Shown in card controls, not on canvas |
tags | string[] | No | [] | Card-level tags |
color | string | No | deck color | Per-card color override |
size (new) | enum | No | deck card_size | Per-card canvas size override — see §5.3 |
front | ContentBlock[] | Yes | — | Min 1 block |
back | ContentBlock[] | No | [] | Optional |
Every block has a type. v1.1 types: text, image, code. Unknown types are skipped with a warning (this is how v1.0 importers survive v1.1 files, and how v1.1 importers will survive v1.2’s math).
{
"type": "text",
"text": "**Grasshoppers** build conditioning:\n1. Hands and feet on the floor\n2. Bring right foot to left hand\n3. Alternate at a brisk pace",
"role": "body",
"align": "left",
"group": null
}
(The block above needs no format field — markdown is the default, so the bold span and numbered list just render.)
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type | string | Yes | — | "text" |
text | string | Yes | — | Content; \n preserved |
role | enum | No | "body" | Semantic role (§3.5) |
format (new) | enum | No | "markdown" | "markdown" (default) | "plain" (§4). Aliases: md/commonmark/gfm → markdown; plaintext/text/none → plain. Unknown → markdown + warning |
align (new) | enum | No | "left" | "left" | "center" | "right" (§5.1). Unknown → left + warning |
group (new) | string | No | none | Advisory clustering label (§5.2) |
{
"type": "image",
"src": "grasshopper_start.png",
"alt": "Start position",
"size": "medium",
"group": "steps"
}
Unchanged from v1.0 (src reference formats in §6; size hint semantics unchanged) except the new optional group field (§5.2).
{
"type": "code",
"code": "const isBrainDump = deck.id === \"brain_dump_deck\";",
"language": "typescript",
"group": null
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type | string | Yes | — | "code" |
code | string | Yes | — | Source text, newlines preserved, no fencing |
language | string | No | none (plain) | See language table below |
group | string | No | none | §5.2 |
Why a dedicated type when markdown has fences: a code block tells the layout engine the entire element is code (monospace metrics, code-dominant face profiles — the “code snippet on one side” use case), and it’s the natural shape for LLMs when a face is exclusively code. Fenced blocks inside markdown text remain fully supported for mixed prose-and-code content; both routes produce the same native codeBlock node.
Languages (the desktop editor’s curated lowlight common set): bash, c, cpp, go, javascript, json, markdown, python, rust, sql, swift, typescript, yaml, or omitted for plain text.
Alias normalization (same list applies to markdown fence info strings): js → javascript, ts → typescript, py → python, c++ → cpp, sh/shell/zsh → bash, golang → go, jsx → javascript, tsx → typescript. Unknown language → plain text + warning (source is always kept).
Native mapping: a text element whose richContent is a ProseMirror doc containing a single codeBlock node (attrs.language); content carries the plain source. role and align do not apply to code blocks and are ignored if present.
math (not supported in 1.1)Block math is deferred to format 1.2 (roadmap Decision #3): its renderability cannot be verified by the pure-Rust pipeline, which would break the “validated ⇒ renders” guarantee (§1.1). A {"type": "math", ...} block in a 1.1 file is skipped with a warning like any unknown type. Authoring guidance: express math as plain-text notation or a code block for now; do not invent LaTeX text blocks expecting them to render.
Unchanged from v1.0 — heading, subheading, body, caption, label, with alias normalization ("title"/"header"/"h1" → heading, "subtitle"/"h2"/"h3" → subheading, "paragraph"/"p"/"content" → body, "note"/"footnote" → caption, "tag"/"badge"/"annotation" → label; unknown → body).
Interplay with markdown: one block = one role. The role sets the block’s typographic baseline (size, weight, color per §9); markdown marks compose on top of it (a body block with **bold** words renders body-size with bold spans). Markdown heading syntax is not the way to make headings — use a separate block with role: "heading" (§4.2).
Applies to every text block that does not declare format: "plain" — markdown is the default (ratified 2026-07-05; see §12 for the rationale). The profile is a deliberately small CommonMark subset plus two extensions, chosen to match exactly what the Card Editor and iOS renderer support — nothing the profile accepts can fail to render. Standard CommonMark backslash escapes are honored (\*literal asterisks\*), and format: "plain" remains the full opt-out for text that must render exactly as typed.
| Syntax | Example | Native mapping |
|---|---|---|
| Bold | **text** | bold mark |
| Italic | *text* | italic mark |
| Bold+italic | ***text*** | both marks |
| Strikethrough (GFM) | ~~text~~ | strike mark |
| Inline code | `text` | code mark |
| Highlight (SuperCards extension) | ==text== | highlight mark (default yellow) |
| Paragraph break | blank line | new paragraph node |
| Hard break | single \n | hardBreak (§4.3) |
| Bullet list | - item | bulletList > listItem |
| Ordered list | 1. item | orderedList > listItem |
| Nested list (≤ 2 levels) | indented - item | nested list nodes; deeper levels flattened to level 2 + warning |
| Fenced code block | ```rust | codeBlock node (language table §3.4.3) |
==highlight== is not CommonMark; it is a SuperCards extension (Obsidian-style) requiring a custom inline pass in the parser. If a parser cannot support it, the literal == characters must be preserved (never dropped silently).
Highlight matching rules (errata, added 2026-07-05 — implemented from the start but previously unstated): pairs match non-greedily within a single text run; the inner span must be non-empty and must not begin or end with whitespace (so comparison prose like if a == b == c stays literal); == sequences inside code spans and code blocks are never candidates; unpaired or ill-formed == stays literal. Reference implementation: sef_markdown.rs.
Indented code blocks (errata, added 2026-07-05): 4-space-indented code blocks are core CommonMark and parse as language-less codeBlock nodes, identical to an unlabeled fence. The profile table lists only fences because fences are the recommended authoring form (they can carry a language); indented code is accepted rather than rejected per the §4.2 never-error rule.
The parser never errors on markdown content; it normalizes and warns:
| Syntax | Behavior |
|---|---|
ATX headings (# Heading) | Leading # marks stripped; text kept at the block’s role styling; warning MD_HEADING_IGNORED (“use a separate block with role: heading”) |
Links [text](url) | Link text kept as plain text; URL dropped; warning MD_LINK_FLATTENED |
Inline images  | Skipped; warning MD_IMAGE_IGNORED (“use an image content block”) |
Blockquotes (> text) | > stripped; treated as paragraph; warning |
| Tables | Kept as literal text; warning |
Horizontal rules (---) | Skipped silently |
| Raw HTML | Kept as literal text; warning |
| Underline | Not expressible in markdown; use bold/italic/highlight |
Within markdown blocks, a single \n is a hard line break (GFM “breaks” behavior), and a blank line starts a new paragraph. Rationale: LLMs use single newlines meaningfully in card content, and v1.0 plain-text blocks already preserved them — silent newline-swallowing (strict CommonMark soft breaks) would make markdown lose fidelity relative to plain text.
These semantics are also what make markdown-by-default safe: sigil-free text parses to exactly the paragraphs and line breaks v1.0 produced, so plain prose is a fixed point of the parser.
ElementStyles (§9), exactly as in v1.0.richContent, layered over the baseline.align → every paragraph in the block (§5.1).code content block.richContentFor each markdown text block, the import pipeline:
element.richContent (stringified JSON), matching the editor’s TipTap schema (same node/mark names the iOS ProseMirrorParser already reads).element.content as the plain-text projection of that document. This projection MUST be byte-identical with tipTapToPlainText (src/types/tiptap.ts) via its Rust mirror text_utils.rs — the same parity contract the save pipeline enforces.role to ElementStyles as the baseline (§9).format: "plain" blocks produce content only (richContent absent) — byte-for-byte the v1.0 text-element output.
Plain-reduction rule (errata, wording corrected 2026-07-05): implementations SHOULD reduce a markdown block — emit content only, richContent absent — when both conditions hold: (1) the parsed document is structurally plain (only unmarked paragraphs and hard breaks — no marks, no lists, no code blocks), and (2) the plain-text projection equals the input with blank-line runs collapsed to single newlines. Reduced blocks emit the input text verbatim as content (blank lines survive — byte-identical to v1.0 output). Condition (2) is load-bearing: a block that parses to plain paragraphs only because normalization stripped something (a # heading marker, an honored backslash escape, a dropped horizontal rule) fails the guard and takes the rich path — otherwise the verbatim copy would resurrect syntax the parser stripped. Non-reduced blocks emit content = the projection, matching the save-pipeline convention for elements that carry richContent. This is an output optimization, not a validation rule.
Conformance note — richContent consumers (errata, added 2026-07-05): richContent is JSON; key order is not significant and legitimately varies by producer (TipTap’s JS serializer emits type first; this pipeline’s serde_json emits keys alphabetically, e.g. {"content":[…],"type":"doc"}). Consumers MUST detect and parse the document structurally — parse the string, then check the root type == "doc" — never by byte-prefix matching against one particular serialization. (Motivating defect: a desktop startsWith('{"type":"doc"') check rejected every pipeline-produced document while iOS’s structural parser accepted the same files; fixed in desktop commit 1a5f95e1.)
All hints are advisory: the layout engine honors them when they fit and degrades gracefully when they don’t. Every hint maps to something the engine demonstrably supports — the vocabulary stays small by design (roadmap risk: hint sprawl).
align (text blocks)"left" (default) | "center" | "right".
Native mapping — two writes that agree by construction:
attrs.textAlign inside richContent (the authoritative field the editor and iOS read), on every paragraph of the block.styles.textAlign set to the same value.This satisfies the repo’s all-or-nothing alignment-mirror principle (see CLAUDE.md “Per-Paragraph Alignment”): a block-level hint is uniform across the element’s paragraphs by definition, so the element-level mirror can never misrepresent mixed alignment. For format: "plain" blocks — and markdown blocks reduced to plain output per §4.5 — styles.textAlign alone carries it, as in v1.0.
group (any content block)A string label (≤ 32 chars) marking blocks that should lay out together.
label/caption text) → caption-attachment treatment (text positioned with its image)."front": [
{ "type": "text", "text": "Grasshoppers", "role": "heading" },
{ "type": "image", "src": "step1.png", "alt": "Start", "size": "small", "group": "steps" },
{ "type": "image", "src": "step2.png", "alt": "Cross", "size": "small", "group": "steps" },
{ "type": "image", "src": "step3.png", "alt": "Return", "size": "small", "group": "steps" }
]
size (card object)Per-card canvas override of the deck’s card_size. Maps to native CardStyle.size; the layout engine uses the corresponding canvas dimensions (§10). Mixed-size decks are valid. Use sparingly — e.g., large for a card carrying a code block that needs the width — and let the deck default carry everything else.
size (image blocks)Unchanged from v1.0: "small" (~25–30% width) | "medium" (~40–50%, default) | "large" (~60–75%) | "full-width" (~90–100%).
Unchanged from v1.0 — four formats identified by prefix:
assets/ preferred, root accepted). PNG, JPEG, WebP, GIF.pdf:filename.pdf:page:N:index:M (1-based page, 0-based index; index-0 fallback with warning).data:image/png;base64,... (use sparingly).https://... downloaded at import (10 MB/image, 30 s timeout, 5 redirects, max 50 URL refs, content-type + magic-byte validated; failures → placeholder + warning).Fragility note (authoring guidance):
pdf:indices select an image by enumeration order inside the PDF, which follows the file’s internal structure — nested Form XObjects, decorative strips, and split images all claim indices — not what a reader sees as “the Mth picture on the page.” An index that looks plausible can silently resolve to the wrong image. Treatpdf:references as a convenience for verified extractions only: check every resolved image after import, and when an index can’t be verified, extract the image and bundle it as a plain file reference instead. Attached/bundled image files are the reliable path. This note adds no new semantics; resolution behavior is unchanged.
Archive note: inside a .supercards archive, file references resolve against the archive’s extracted contents exactly as against a folder.
All v1.0 fatals carry forward (invalid JSON, missing/unsupported supercards_format major version, missing deck/title/cards, empty card front, zero or multiple manifests), plus:
| Condition | Error |
|---|---|
| Archive exceeds size/entry caps (§2.2) | “This deck file is too large to import.” |
| Archive entry fails path/symlink/encryption rules (§2.2) | “This deck file contains invalid entries and cannot be imported.” |
| Archive is not a readable ZIP | ”This file is not a valid .supercards deck.” |
Code block missing code field | ”Card {i} has an invalid code block.” |
All v1.0 warnings carry forward (missing image file, failed PDF extraction, unknown role → body, unknown block type → skip, empty text → skip, unknown fields ignored, card count/title truncation, unknown image size → medium), plus:
| Condition | Behavior |
|---|---|
Unknown format value | Treat as markdown (the default); warn |
Unknown align value | Treat as left; warn |
Unknown language (code block or fence) | Plain text; warn; source kept |
Unknown card size value | Deck default; warn |
group longer than 32 chars | Ignore the hint; warn (GROUP_INVALID) |
group not a string | Hint dropped at parse time, silently (the lenient deserializer coerces wrong-typed optional fields to their defaults) |
| Unhonorable group | Sequential layout; warn |
| Markdown normalization events (§4.2) | Per-rule warning codes |
| List nesting > 2 levels | Flatten to 2; warn |
math block (reserved) | Skip; warn (“math arrives in a future version”) |
Machine-readable diagnostic codes extend the existing ImportDiagnostic enum; the connector’s validate_deck returns them verbatim so models can self-correct.
One new step; the rest carries forward:
0. UNPACK If input is a .supercards archive: enforce §2.2 hardening,
extract to a staging directory, then treat as an import folder
1. SCAN Find exactly one *.supercards.json
2. PARSE Parse + validate (§7); collect diagnostics
3. RESOLVE Resolve image references (file / pdf / data / url)
4. STAGE Write resolved assets to temp staging (outside iCloud container)
5. LAYOUT Classify face profiles; apply hints (§5); position elements;
parse markdown/code into richContent (§4.5); map roles (§9)
6. GENERATE Card JSONs, card_order.json, deck.json in staging
7. PREVIEW Placeholder preview PNGs (Rust-side)
8. MIGRATE Drip-feed staged files → library (WriteTracker, ~50ms pacing)
9. INDEX Update SharedLibraryIndex
10. NOTIFY deck-updated / refresh-library events
11. OPEN Card Editor on first card (existing post-import behavior)
Steps 0–7 are pure computation plus staging I/O; nothing touches the Library until MIGRATE. This is the single-write-path guarantee the connector relies on.
Unchanged baseline from v1.0; markdown marks compose on top (§4.4):
| Role | fontSize | bold | textAlign default | color |
|---|---|---|---|---|
heading | 24 | true | per align (default left) | #000000 |
subheading | 16 | true | per align | #000000 |
body | 16 | false | per align | #000000 |
caption | 14 | false | per align | #666666 |
label | 12 | false | per align | #666666 |
| (code block) | 13 (nominal — see note) | — | — | editor code-block theme |
Dark-background inversion (luminance-based, #FFFFFF/#BBBBBB) carries forward unchanged.
Code-block metrics (errata, corrected 2026-07-05): the editor renders code blocks at a fixed 11px × 1.2 line-height plus 38px of vertical chrome (.code-block-wrapper pre padding and the language-picker allowance); element fontSize does not scale code blocks and carries the nominal 13 for stylistic consistency only. The v1.0 wording “engine may shrink to fit” is therefore implemented as box-height clamping to the canvas, not font scaling — the layout engine’s height estimation uses the real 11px metrics.
Unchanged; now selectable per card via card.size (§5.3):
| Size | Width | Height |
|---|---|---|
small | 812 | 375 |
medium | 844 | 390 |
large | 956 | 440 |
check_format_version in the shipping validator accepts any major-version-1 file (SUPPORTED_FORMAT_MAJOR = 1) and rejects 2.x.x. Consequences:
A 1.x file in a 1.1 importer — one semantics, no version gating (ratified 2026-07-05): every major-version-1 file is read under 1.1 rules; the version string selects nothing. For 1.0-era files (which predate the format field) text now parses as markdown by default. §4.3’s hard-break semantics keep sigil-free text rendering identically, and the §4.5 plain-reduction rule keeps its native output byte-identical — so in practice only text containing markdown-significant characters changes, gaining the structure it appears to declare. This trade was accepted deliberately: the v1.0 corpus is essentially zero (the shipped beta’s copyable prompt templates — which still stamp "1.0.0" and must keep importing — plus two internal example manifests), which makes behavior-preservation machinery pure cost. Zero migration, one parser path, one documented behavior.
A 1.1 file in a pre-v1.1 app (graceful degradation, no data loss):
| Feature | Old-app behavior |
|---|---|
Markdown text (format field usually absent under the default) | All text treated as plain; raw markdown shown as literal text (asterisks visible) |
code blocks | Skipped with warning (existing Skip variant via #[serde(other)]) |
align, group, card size | Ignored silently (unknown-field rule) |
.supercards archive | Not recognized — user must unzip manually and folder-import |
The archive is the only hard gap; auto-update (4-hour check cadence) keeps the fleet current, and the Skill/connector can state the minimum app version.
Forward: 1.2 (math) will be additive; 1.1 importers will skip math blocks with a warning by the existing unknown-type rule.
| Decision | Rationale |
|---|---|
Markdown as a format field, not a new block type | Preserves v1.0 text-block shape; the field is the opt-out switch, not an opt-in |
| Markdown is the default format (2026-07-05) | Models write markdown by reflex; a forgotten opt-in flag fails silently (plain text with asterisks is valid — no diagnostic possible, so even the connector’s validate-loop can’t catch it). Sigil-free text is a fixed point of the parser (§4.3, §4.5), so the default costs v1.0-era files ~nothing |
| One semantics for all 1.x files (2026-07-05) | No version-gated behavior matrix to document and test forever; the ~zero v1.0 corpus (beta prompts + two fixtures) makes preservation pure cost. Old "1.0.0" version strings remain importable |
| Markdown profile = editor’s exact capability set | ”Validated ⇒ renders” (§1.1); nothing accepted can fail downstream |
| No markdown headings | Roles own block typography; two heading systems would fight. Strip + warn |
Single \n = hard break | LLM/card-content fidelity; consistent with v1.0 plain text |
==highlight== extension | Flagship flashcard affordance; Obsidian-familiar to LLMs; literal-preserve fallback if unparsed |
| No underline | No markdown syntax; bold/italic/highlight suffice |
Dedicated code block type and fences | Type = layout signal for code-dominant faces; fences = mixed prose. Same native node |
| Block math deferred to 1.2 | MathJax renders frontend-side; pipeline can’t verify renderability; JP wary of dependency; additive later |
align writes paragraph attrs + element mirror | Uniform-by-construction satisfies the all-or-nothing mirror principle (CLAUDE.md) |
group consecutive-only | Deterministic, cheap to honor, impossible to misread |
| Archive = zipped import folder | One mental model; validation/import reuse; folder form stays valid |
.supercards extension | Brand-carry on every shared file; .sc (SuperCollider/Scala) and .cards (existing flashcard ZIP format) rejected for collisions. Re-validated 2026-07-05 against shorter alternatives: .sef rejected (Saxon XSLT .sef/.sef.json compiled stylesheets, active EDI SEF standard, legacy Windows tools), .scef rejected (collision-free but brandless; 3GPP owns the acronym). Length is functionally free — UTIs, AirDrop, share sheet, mail, and Windows all ignore it; .flashcards (10 chars, same category) is shipping precedent |
| Guidance revision decoupled from format version (2026-07-05) | §14 is advisory content, not schema — edits must never require a format bump or re-validate a file; all surfaces derive from one canonical source (§14.1, roadmap Principle 2) |
| Hard archive caps (§2.2) | Zip-bomb/zip-slip hardening before deck sharing exposes the surface to strangers’ files |
| Native formatVersion unchanged | richContent existed since v0.4.3; no native migration |
| Native-passthrough share payload (2026-07-07) | Receiving devices render native JSON today (iOS ProseMirrorParser/CardContentRenderer); reverse-mapping to SEF is lossy on exactly what a hand-tuned deck uses — block math (no SEF slot until 1.2), per-character styling, free positioning. Export = zip + hygiene pass. The hybrid “stash the original manifest” idea survives as a post-launch rider for LLM re-editability |
| Identity re-stamp on native import (2026-07-07) | Never trust incoming IDs; asset filenames embed card IDs so renames must cascade through refs, order, and thumbnails; the same archive imported twice must yield two independent decks |
import_models.rs (additive)ImportContentBlock::Text gains format: String (default "markdown"), align: String (default "left"), group: Option<String>ImportContentBlock::Code { code: String, language: Option<String>, group: Option<String> }ImportCard gains size: Option<String>"supercards_format": "1.1.0" (the validator continues to accept any 1.x string — see §11)(Per CLAUDE.md, implementing these requires a docs/data/YYYY-MM-DD-UPDATE.md note — file-format change to the exchange format.)
Markdown-profile → ProseMirror JSON. Candidate: pulldown-cmark + custom ==highlight== inline pass + TipTap-schema serializer. Pure computation; unit-testable against editor-produced richContent fixtures. Plain-text projection MUST route through text_utils.rs (parity contract, §4.5).
layout_engine.rsrich_content for markdown/code blocks (today hardcoded None)align (element styles + paragraph attrs), group (row/cluster strategies), per-card size (canvas selection)import_validator.rs + new archive modulezip crate).supercards (Info.plist) + open-file/drag handling → inbox (roadmap B1)formatVersion unchanged; v1.1 only grows the vocabulary that may appear inside richContent stringscode mark (§4.1) required one small enablement on each platform, landed with A-2: the desktop editor dropped StarterKit’s code: false flag and added .ProseMirror code styling (TipTapEditor.tsx, globals.css), and iOS ProseMirrorParser.swift gained case "code" (Menlo). Old iOS builds degrade gracefully (unknown marks ignored); old desktop builds throw on unknown marks — which is why the enablement shipped before the pipeline ever emitted the mark.Guidance revision: 2026-07-05 (versioned independently of the format — see §14.1)
This section seeds the web spec’s guidance page, the connector’s get_examples, and the Skill (roadmap A3). It is normative for prompt/tool authors, advisory for models.
Card design:
heading block (unless face is image-only). Use caption for source attributions and category labels, label for step numbers and annotations.==highlight== the answer’s key phrase — sparingly.Layout hints:
group consecutive images that belong side-by-side (exercise steps, before/after). Two or three small/medium images group well; don’t group more than four.align: "center" suits short headings and single-image faces; leave body text left-aligned.size: "large" for faces that genuinely need width (code, wide diagrams).Formats and sources:
==highlight== render without any flag. Declare format: "plain" only when characters like *, _, or a leading 1. must render literally (sigil-heavy notation, ASCII diagrams); backslash escapes (\*) work too.code blocks with a language from the supported list — never in body text.x^2 + y^2 = r^2) in a format: "plain" block — or a code block — so the notation isn’t parsed as markdown; do not emit LaTeX text expecting rendering.pdf:filename:page:N:index:M (first image on a page = index 0); URLs must be direct image links you have verified exist.{deck_name}.supercards.json, or produce a .supercards ZIP (manifest at root + assets/) when you have the referenced files.Self-checks before delivering: valid JSON; every card has non-empty front; every image ref resolvable (file present, page/index plausible, URL direct); roles from the vocabulary; deck source filled in.
(Ratified 2026-07-05 — governs all future edits to §14 and its derived artifacts.)
Guidance is advisory content, not schema. Editing §14 never bumps supercards_format, never changes the validator, and by construction cannot invalidate an existing file. The litmus test: if a proposed edit would change what validates or how a file renders, it is not a guidance change — it belongs in the schema sections (§3–§11) and rides a format version bump. Pedagogy tweaks, new few-shot examples, recipe patterns, and better image advice are all guidance; they ship anytime.
Versioning. Guidance carries its own date-based revision stamp (Guidance revision: YYYY-MM-DD, above), independent of the format version. The web spec page keeps a guidance changelog. Connector responses (get_spec / get_examples) report both format_version and guidance_rev, so a badly authored deck can be traced to stale guidance.
One canonical source, derived surfaces (roadmap Principle 2). This section’s content lives in one canonical source file in the repo; the web guidance page, llms.txt, the Desktop extension’s bundled copy, and the claude.ai Skill’s content are all generated from it — never forked. Propagation per surface:
| Surface | Update path |
|---|---|
Web spec page + llms.txt | Edit anytime; browsing models and humans see it immediately |
Desktop extension (get_spec/get_examples) | Runtime refresh: fetch the canonical URL with a cache TTL, bundled copy as offline fallback — guidance edits never require repackaging/re-signing the extension |
| claude.ai Skill | The weak link (static zip once uploaded). The Skill prefers fetching the canonical llms.txt when it can; otherwise updates ride Skill releases + re-upload |
| Website prompt templates | Edit anytime |
{
"supercards_format": "1.1.0",
"deck": {
"title": "Exercise Library",
"summary": "Infinite Intensity conditioning movements",
"card_size": "medium",
"source": "Generated by Claude from infinite_intensity.pdf"
},
"cards": [
{
"title": "Grasshoppers",
"tags": ["core", "conditioning"],
"front": [
{ "type": "text", "text": "Core Exercises", "role": "caption" },
{ "type": "text", "text": "Grasshoppers", "role": "heading", "align": "center" },
{
"type": "image",
"src": "pdf:infinite_intensity.pdf:page:87:index:0",
"alt": "Start position",
"size": "small",
"group": "steps"
},
{
"type": "image",
"src": "pdf:infinite_intensity.pdf:page:87:index:1",
"alt": "Foot to hand",
"size": "small",
"group": "steps"
},
{
"type": "image",
"src": "pdf:infinite_intensity.pdf:page:87:index:2",
"alt": "Return",
"size": "small",
"group": "steps"
}
],
"back": [
{ "type": "text", "text": "Grasshoppers", "role": "heading" },
{
"type": "text",
"text": "An excellent **conditioning and core** movement.\n\n1. Begin with hands and feet touching the floor\n2. Bring your right foot under the body to your ==left hand==\n3. Return and repeat with the left foot to the right hand\n4. Continue alternating at a brisk pace",
"role": "body"
}
]
}
]
}
{
"supercards_format": "1.1.0",
"deck": {
"title": "SuperCards Codebase Tour",
"summary": "Key patterns, one per card",
"card_size": "medium",
"source": "Generated by Claude from the supercards-app repository"
},
"cards": [
{
"title": "Brain Dump identification",
"size": "large",
"front": [
{
"type": "text",
"text": "How does the app know a deck is the Brain Dump?",
"role": "heading"
},
{
"type": "code",
"code": "const isBrainDump = deck.id === \"brain_dump_deck\";",
"language": "typescript"
}
],
"back": [
{ "type": "text", "text": "Fixed ID, not a flag", "role": "heading" },
{
"type": "text",
"text": "The Brain Dump is identified by its **fixed ID** (`brain_dump_deck`), never by name or a boolean flag.\n\n- Defined on both sides: `models.rs` and `cards.ts`\n- It cannot be renamed or deleted\n- Every special case checks this one constant",
"role": "body"
}
]
}
]
}
{
"supercards_format": "1.1.0",
"deck": { "title": "My Deck" },
"cards": [{ "front": [{ "type": "text", "text": "Hello, world!" }] }]
}
(Text is markdown by default — **bold** in this block would render bold. Sigil-free text like this parses and outputs exactly as v1.0 plain text did.)
Exercise Library.supercards (ZIP)
├── exercise_library.supercards.json
├── infinite_intensity.pdf # pdf: refs resolve self-contained
└── assets/
└── cover_photo.png
Native-deck payload (deck sharing — same container, second payload kind, §2.2):
Biochem Unit 3.supercards (ZIP)
├── deck.json # native deck metadata (native formatVersion 1.0.0)
├── card_order.json
├── cards/
│ ├── card_1751856000000_a1b2.json
│ └── card_1751856000001_c3d4.json
└── assets/
├── card_1751856000000_a1b2_001_9f1e2abc.png
├── card_1751856000000_a1b2_front_preview.png # previews ride along
└── card_1751856000000_a1b2_math_000_5d6e7f8a.svg # block math survives losslessly
.supercards single-file archive container with UTI + hardening rules (§2.2)format: "plain" opts outcode content block type + fence/alias language normalization (§3.4.3)align, group, per-card size (§5)richContent for markdown/code (previously always absent)math explicitly reserved for 1.2 with interim authoring guidance (§3.4.4)supercards_format bump)math block type — {type: "math", latex}; requires solving import-time SVG generation (or first-open rendering) without breaking the validated⇒renders guarantee