@mercury-fx/schema
v3.0.0
Published
Mercury documents — the portable, versioned, JSON-safe page format: block schemas, validation with declared limits, atomic commands and migrations. No React, no Effector, no DOM handles.
Readme
@mercury-fx/schema
The portable document format of the Mercury design system: versioned, JSON-safe page data with block schemas, validation at declared limits, and one command layer that applies edits atomically or refuses them. No React, no Effector, no DOM: a renderer, an editor and a server read the same documents. The contract is the AUS mercury.20.
import { applyCommand, createSchemaRegistry, parseDocument } from "@mercury-fx/schema";
const registry = createSchemaRegistry({
blocks: [
{ type: "section", version: 1, props: {}, slots: { content: {} } },
{ type: "heading", version: 1, props: { text: { kind: "string" }, level: { kind: "number", integer: true, min: 1, max: 6 } } },
],
});
const parsed = parseDocument(jsonText, registry); // { ok: true, document } or { ok: false, code, path, reason }
if (parsed.ok) {
const result = applyCommand(
{ document: parsed.document, revision: 0 },
{ kind: "set-properties", expectedRevision: 0, node: "title", props: { level: 2 } },
{ registry },
); // { ok: true, local: { document, revision: 1 }, impact } or a refusal; the input is unchanged
}- Document:
format,id,kind(page,fragment,template), orderedrootsand a flatnodesmap. A node hasid,type,version, JSONpropsand namedslotsof child IDs. Fields the format does not name are kept as they are. - Refusals share one shape: a typed
code, a JSON Pointerpathin its URI-fragment form (#/…, percent-encoded), areason, thesubjectthe path points into (document,commandorstate) and, when there is one, thenode. Parsing, commands and migration never return a partial document, and hostile input (a throwing getter, a revoked proxy) is refused, not thrown. - Commands: insert, remove, move, duplicate and set-properties, each against an expected local revision. Duplicate mints fresh node IDs and keeps references. An edit to a fragment reports every document that references it.
- Limits (
maxDepth,maxBytes,maxNodes,maxPropDepth) are declared per registry and hold for every result: an edit or a migration that would outgrow them is refused.maxBytescounts the document's compact JSON, so whitespace never changes a verdict. - Tokens: a
tokenprop admits only a name from a set the registry declares (tokens). - Migration:
migrateDocumentsteps older block versions forward through each definition's deterministic migrations, on a copy; a refusal hands back the untouchedoriginal, and a step that drops data needs{ allowLossy: true }. Format 1 is the first portable format. - Unknown blocks (an unregistered type or a newer version) are kept unchanged through parsing,
edits elsewhere, saves and migration, their slots included;
inspectDocumentreports themunsupported. - References (
{ "$ref": "asset" | "route" | "entity" | "fragment", "id" }, plus any JSON fields a later version adds) resolve beside the document withresolveReferences; anunauthorizedtarget keeps only its identity.indexConsumersfollows fragments inside fragments. - Responsive values (
{ "$responsive": { "base", "overrides" } }) keep absent, explicit and removed ({ "$removed": true }) distinct;resolveResponsivereads one width and writes nothing.
Client validation helps authors; the host still authorizes every change on its server.
