Reproducibility

Reproducibility in Lolly means the inputs to a render are portable and durable: the entire state of a design lives in its URL, so the link is the artifact and the picture is a thing the link makes. Open it next year, on another device, and it renders again.

This is the neighbour of determinism and a different claim. Determinism says the same inputs give the same file. Reproducibility says you still have the inputs - they are a short piece of text you can paste in a ticket, commit to a repository or bookmark, rather than a document trapped in an account.

It is what the landing page means by "same inputs, same file, every time" being worth anything at all a year later.

Every input is expressible as a URL param

engine/src/url-mode.ts owns the round-trip in both directions: parseUrlState turns a query string into input values, serializeUrlState turns the current state back into a query string. Being expressible as a URL param is a hard requirement on every input type rather than a convenience some tools happen to implement, which is also why the CLI works: it is that same conversion under a different transport.

The names that mean something without being inputs are a closed set, RESERVED at engine/src/url-mode.ts:327: format, export, copy, full, options, slot, output, filename, _v, width/w, height/h, unit, dpi, bleed, marks, cuts, c2pa, imprint, durable, hdr, depth, password, profile, lang, z, zx and a handful more. Everything else in a link is the design.

Tools can opt into compact encoding - urlKey aliases, #-less colours, tilde-delimited block arrays - so a long design stays a manageable link. Where that is still not enough, engine/src/url-pack.ts packs the whole query into one token, and packEncrypted does the same under a password that is never in the link.

Physical intent travels too

A link carries what the render is meant to be, not just what it looked like on the screen that made it. width and height are values in unit (px, mm, cm, in or pt), dpi sets raster resolution for physical units, and bleed, marks and profile carry the print intent. The conversion is the engine's single source of truth in engine/src/units.ts, applied per format at export time - PDF to real points, SVG to unit plus a pixel viewBox, raster to pixels at the requested DPI with a pHYs chunk.

So ?unit=mm&width=210&height=297&dpi=300&bleed=3mm&marks=crop,reg is a reproducible A4 print job, not a screenshot someone hopes is big enough.

Brand references stay references

A colour input can hold a token reference rather than a baked hex value, and the reference is what travels. tests/tokens-value-path.test.ts pins exactly that: a token-backed colour survives serialization into a shared link and comes back as a reference, while the template only ever sees the resolved string.

The consequence is the useful one. A link made against color.brand.primary re-renders under whatever that token means when it is opened, so a rebrand updates old links instead of breaking them. A link made with a literal colour keeps that colour. You choose which one you wanted at the moment you picked the value.

A tool-sourced image is a recipe

Paste a Lolly link into an asset picker and the picture it makes becomes an input to another tool. engine/src/tool-url.ts recognises every shape the app can hand you - the embed form, the hash share route and the pretty path - and buildEmbedUrl canonicalises it into one strict embed URL that becomes the asset's persistent identity.

That identity round-trips through URL mode and saved sessions, and the runtime re-renders it through host.compose.renderUrl on every load rather than storing a bitmap. A nested chart inside a poster stays live: change the chart's link and the poster changes with it.

Sessions, projects and templates carry the same state

The same state serialization backs the app's own storage. A saved session holds the values; Share link on any session in Projects produces the URL form of it; a .lolly bundle carries one for handing to a colleague, and an operator who wants a starting point to be permanent records those values as a templates[] entry on the tool's manifest in the brand pack, deep-linkable as ?template=<id> (Deployment). Each is the same inputs in a different envelope.

Worth being concrete about the dependency list, because it is short and every item is something you can hold:

  1. The link. Plain text. Paste it in a ticket, a spreadsheet cell, a commit message or a README.
  2. The tool. A directory: tool.json, template.html, optional styles.css, optional hooks.js. Data, not a binary. Keep it in version control if the render matters.
  3. An engine that satisfies the tool's engineVersion range. The manifest declares it and the loader enforces it, so an incompatible pairing is refused rather than rendered wrongly.
  4. The assets the link names. Catalog assets have permanent ids by design - suse/logo/primary is never renamed or reused - so a reference stays valid as long as the pack does.

Nothing on that list is an account, a subscription or a running service. Every item is a file you can copy and keep.

Check it yourself

# a link and the CLI carry the same state; both write the same SVG
lolly qr-code --url=https://suse.com --export=svg --no-provenance --output=./a.svg

# the same inputs, expressed as a URL
https://lolly.tools/#/tool/qr-code?url=https://suse.com&format=svg&export

Then change one input in the app, copy the link out of the Share dialog and read it. Every value you set is visible in it, which is the whole claim in one glance.

The receipts

ClaimEnforced by
A query string round-trips through parse and serialize without losstests/engine.test.ts - url-mode: round-trips
The reserved-param set is exactly the documented one, so an input name can never silently collide with an output settingtests/engine.test.ts - url-mode: RESERVED set matches the documented reserved-param list
An unknown param is ignored rather than misread, so an old link survives a newer tooltests/engine.test.ts - url-mode: ignores unknown params (forward-compat)
Packing is lossless for any query string, including the compact block encodingtests/url-pack.test.ts - decode(encode(x)) === x
A pasted tool link canonicalises to a stable identity that re-parsestests/tool-url.test.ts
A tool-sourced asset is re-rendered on every load rather than cached as pixelstests/tool-url-asset.test.ts
A token reference survives a shared link and resolves before the templatetests/tokens-value-path.test.ts
Compact block encoding round-trips a value containing its own delimitertests/engine.test.ts - url-mode: a block value containing a ~ round-trips losslessly via the JSON form

Limits