ADR-0009: Each crafting app declared once, on one of two dialects, pinned on its own¶
Status: Accepted Date: 2026-10-18 Deciders: Alex Nodeland Amends: ADR-0005, whose pin, script and fidelity report were WordCraft's alone
Groundwork for the crafting apps after WordCraft, in lattice RFC-0003. Nine are integrated as native files (WordCraft, GridCraft, DeckCraft, PdfCraft, CADCraft, PhotoCraft, VectorCraft, DesignCraft, FigCraft), and four as export only (FilmCraft, EffectCraft, SoundCraft, LightCraft), whose projects point at media outside them.
Context¶
All thirteen apps release the same assets: Linux tarballs and a macOS zip holding <app>-cli, a browser build, and SHA256SUMS.txt, with no signatures. Each CLI's mcp is an MCP server on stdio. On 2026-10-18 we ran each pinned release's CLI and listed its tools and options; where an app's sources say otherwise, the release is what is recorded here:
- Two dialects. Seven servers have a tool for each step, as WordCraft's has (
open_document,batch,save_document,list_commands), under each app's own names (open_workbook,save_presentation,open,save). Six run every step as a command, throughcommand_run,command_batch,command_list,doc_inspectandrender_preview, as PdfCraft's and FilmCraft's do. Within that dialect PdfCraft opens a file by itsfile.opencommand and names the document in every command after, while PhotoCraft refusesfile.openand opens through a tool of its own,doc_open. - Confinement differs by release. WordCraft, PhotoCraft, VectorCraft, DesignCraft, EffectCraft and SoundCraft take a read root and a write root. PdfCraft 0.8.0 takes one
--root, though its sources have since gained the two. GridCraft, DeckCraft, CADCraft, FigCraft, FilmCraft and LightCraft confine nothing: their CLIs read and write wherever their user can. DeckCraft's and FigCraft's sources have roots, but no release has them yet. - Headless pages. Every app's server renders a page as a PNG but GridCraft's, FigCraft's and SoundCraft's, which have only a screenshot of the desktop app, an export, or a mix-down.
- Notices. PdfCraft's Linux tarballs carry its OCR's two models under CC-BY-SA-4.0, with their attribution and licence files beside them; its CLI-only tarballs and its browser build carry none. No other release ships a third-party model.
- The brand. Each app's brand licence is WordCraft's, word for word but the app's name: the brand's marks only in unmodified official builds, never in the name of anything else.
Decision¶
artifactr.files.craftdeclares every app once. ACraftnames the app (its keyfiles:<name>, its CLI<name>-cli, its repositorystorytold/<name>), itsdialect(toolsorcommands), itsroots(split,singleornone), theformatsit keeps, whether it canrenderheadless, the host surface its browser build has (bridge:parentorglobal), itsreach(file, kept as a native file, orexport, only what it renders), and itsnotices.CRAFTSholds the thirteen, as their pinned releases offer them. Formats with no registered media type (FigCraft's, and the native formats of VectorCraft and DesignCraft) are left to their adapters.- Each is an integration of lattice-core's registry,
files:<name>, off by default but WordCraft's, which stays on. lattice-core can't import artifactr, so its registry holds each key's default and description, andCraft.integrationreads them from it; a test holdsKNOWN'sfiles:keys toCRAFTS, in order, as portalr's tests hold its generated copy toKNOWN. Spawnis the one place a CLI starts: for every call, in an empty temporary directory that holds its root or roots, with the call's deadline, the version check, and the mapping of what goes wrong ontoAppFailed, which were WordCraft's adapter's. It passes the roots its app takes. An app whose CLI confines nothing is spawned only withtrusted=True, the operator's word that its files are trusted or that the process is already confined; without it,SpawnraisesValueErroras it is made, so a deployment can't run one by accident. Those apps' keys stay off by default for the same reason.- The worker is an
Appthat forwards the call. The sandboxed worker of RFC-0003's phase 3 is a separate job. On the application's side it is anAppthat sends each call to the worker; inside the worker, the same adapter and itsSpawnrun, withtrusted=True, since the sandbox confines what the CLI can reach. Nothing in a dialect or an app's adapter changes for it. - Two dialect bases, over one
Headless.Headlessis anApp:apply,inspect,renderandcommandseach spawn the CLI, open the source or make a blank file, and take their steps.openreturns what the server calls the file it opened, which every later step is given.ToolsDialecttakes each step with a tool its subclass names (new_tool,open_tool,save_tool,batch_tool,render_tool,list_tool); its servers hold one file, so it names none.CommandsDialecttakes each with a command (open_command,save_command,new_command), naming the open document byhandle,docby default, as the open command's result names it; a result that names none is a crash, and a batch whose results say a step failed is refused, whatever its reply's error flag. A step a server takes otherwise is overridden. A subclass that reads files and names nocraftorextensionis refused as it is defined. How a file is read into anInspectionis each app's own,inspection. An app that can't render refuses a page before any CLI starts. - WordCraft's
Subprocessis the firstToolsDialect, with its signature, its tools, its messages and its fidelity unchanged; its inspection and its.docxfingerprint stay inartifactr.files.wordcraft. scripts/craft <app> cli|web|noticesreplacesscripts/wordcraft, with a pin for each app inscripts/crafts/<app>.sha256: the release'sSHA256SUMS.txtlines for one asset on each of Linux x86_64 and aarch64 (the full tarball or the CLI-only one), the macOS CLI, and the browser build. A pin naming two assets for one target is refused before anything is downloaded. Each asset is fetched when it is first asked for, and only then.noticesprints the licence, notice and attribution files beside the CLI, for whatever ships it. A test holds every pin to that shape and toCRAFTS.- The notices travel with the CLI. PdfCraft's pin is its full tarballs, whose OCR models' CC-BY-SA-4.0 attribution its
Noticerecords, with the files that carry it; whatever ships PdfCraft's CLI, such as the worker's image, ships them. A browser build is served as released, its own licence files with it. - The brand's name appears in no key, title, description or document of lattice's but RFC-0003's record of the licence, which a test holds the registry to.
- The fidelity report is each app's, from a corpus of its own:
tests/files/corpus/<app>/, one generated file per feature, read by aCorpusthat says how each feature's value is read and what two saves in a row must keep.moon run artifactr:fidelityruns the appCRAFTnames, WordCraft by default, and Nightly'sfidelityjob runs one app of its matrix each; the matrix lists the apps with a corpus, which a test holds it to. Its live tests are markedcraft("<app>"), and run only whereARTIFACTR_TEST_CRAFTnames the app.
Options considered¶
Option A: A registry, two dialect bases, and an adapter for each app (chosen)¶
| Dimension | Assessment |
|---|---|
| Fit | The two dialects the thirteen servers speak, with each app's differences in a subclass |
| Safety | The confinement each release offers is declared, and an app without it needs a word from the operator |
| Cost | One adapter, one corpus and one pin for each app, on shared code |
Option B: An adapter for each app, written from scratch¶
Cons: thirteen copies of the spawn, the deadline, the error mapping and the version check, which ADR-0005 wrote once and which every worker call depends on.
Option C: One adapter, driven by a table of tool names¶
Cons: the servers differ in more than names. Within one dialect, one app opens by a command and another refuses that command and opens by a tool; one names the document in every command, another acts on the one open. A table that holds that is code.
Trade-off analysis¶
The dialect bases hold what is the same and leave what isn't to each app; the registry says once what each release offers, so the switch, the pins and the docs can't disagree with it.
Consequences¶
- Easier: an app's adapter is its tool or command names, the steps its server takes otherwise, and its
inspection; its corpus and pin plug into the harness and the script. - Easier: a release that gains roots, or a render, is one field changed, and a test says what follows from it.
- Harder: six apps can't run on untrusted files until the worker exists; their keys stay off.
- Harder: every app's talk level is still
save. The shim that reaches a build's host surface, and the level it gives, is a decision of its own. - Revisit: each descriptor with each pin raised; PdfCraft's to
splitonce a release takes the two roots; DeckCraft's and FigCraft's likewise.Headlessmakes its ownSpawn; a confinement that wraps the CLI where it runs, such as a command prefix, would add a parameter for it.
Action items¶
-
artifactr.files.craft:Craft,CRAFTS,Spawn,Headless,ToolsDialect,CommandsDialect; WordCraft onToolsDialect. - The keys in lattice-core's registry, and portalr's generated copies.
-
scripts/craft, a pin for each app, and their tests. - The fidelity harness and Nightly's
fidelityjob, by app. - Each app's adapter, corpus, normalizer and type, by its own work.