The Sloppy app manifest (v1) and sloppy check

A Sloppy app is a Synth App for the Sloppy stack: a creator (a person or the AI builder) ships a manifest + one Faust voice (+ an optional composition canvas); the host (Studio) supplies the blocks (piano roll, step grid, pads, keys, automation lane, knob tiles, navigation). Design and rationale: more-fire refactor/sloppy-stack.md (§2 the manifest, §2.3 the Faust contract, §2.8 the harness, §3 lowering). This page is the SDK-side reference.

The bundle

<id>/
  sloppy.json           the manifest (schema: sdk/manifest/sloppy-app.schema.json, `"sloppy": 1`)
  dsp/main.dsp          the Faust voice (the Sloppy Faust contract below)
  demo/preset.json      param values by id ($defs/preset)                 -> the slot's init params
  demo/pattern.json     the demonstration pattern ($defs/pattern): 16th steps, keys, lengths, velocities, slide flags
  canvas/canvas.json    optional composition canvas ($defs/canvas): rows of xy | lane | pads | knobs | steps | phrases
  samples/*.wav         optional, each with a licence in assets[]  (not built yet: see "Deferred")
  README.md, CHANGELOG.md

Every file but sloppy.json, README.md, CHANGELOG.md, LICENSE and demo/* is listed in assets with its sha256 (content-addressed). Three worked examples live in sdk/examples/sloppy/: sub-808 (mono-legato, FL slides, a DRILL canvas), dusty-kit (16 one-shots, poly steps, per-key params, a choke group), mono-lead (mono-legato, scale-aware keys, a wobble FX, a remix).

Validation: strict authoring, tolerant loading

sloppy validate <dir> (and the first stage of check) validates sloppy.json against the schema with unknown keys in known objects as errors, so a misspelled field never silently becomes "optional metadata", then applies the rules the schema cannot say: unique param ids, ids ≠ 50/51 (reserved: Next Glide / Slide To), labels ≤ 8 characters, devices.* / layout.sound / fx / stepLocks referring to existing params, roll needing a pitched app or labelled keys, every asset present with its sha256, no file outside assets, and requires naming what the manifest uses (poly-steps, slides, choke, per-key-params, canvas:composition, each block). A host loading a published package validates with strict: false (unknown optional fields are ignored) and refuses only on requires it lacks, by name. Studio's capabilities: steps roll keys auto slides poly-steps per-key-params choke canvas:composition (STUDIO_CAPABILITIES; mirrored in more-fire's :wire SloppyManifest.kt).

The validator (sdk/sloppy/src/schema.ts) implements the JSON Schema 2020-12 subset the schema uses; the test fails if the schema ever uses a keyword it does not know.

sloppy check <dir>: the gate

og sloppy check sdk/examples/sloppy/sub-808        (or: sloppy check …; --fast for iteration; --json; --out <dir>)
Stage What Fails when
schema the validation above any problem
faust the pinned faust 2.79.3 (-lang c -single -ftz 1 -json), the source scan of the Faust contract (no ffunction/fconstant/fvariable, only standard libraries, no [midi:on], no OSC, no soundfile() yet), the licence audit (GPL/AGPL code in the output fails) compile error, forbidden primitive, copyleft
crosscheck the manifest against the compiled descriptor: every control exists; gate is a button; the pitch control's range covers the keys (Hz: the pitch domain, or 20 Hz..8 kHz for melodic apps; midi: the domain; key-index: 0..n-1); glideTime is milliseconds and allows 0; each param's [min,max] inside the DSP's, units agree with [unit:], defaults in range; a lite target must lower with nothing dropped any mismatch
build gen/sloppy_app.h (the manifest's og_param_info table in plain units + 50 Next Glide when the app slides, roles, voice policy, key domain, choke groups, the demo pattern) + gen/main_faust.h (og faust) + og/faust/fog_sloppy.h → og build → build/fw/build/<id>.wasm (≤ 16 MiB) compile/link error
verify og verify --profile generic,kick,fire: boots, audio on pad presses, fuzz, deadline, levels, state round trip, params any checked row false
render 2 bars of the demo pattern (or, with demo: null, the default key on every beat) at the demo's tempo, at the default preset: audible (peak > −60 dBFS) and no clipping (no sample at or above −0.01 dBFS); in --fast too silent or clipping
sound headless on the firmware (HeadlessDevice, the og render path): every key of the pitch domain (or three octaves around the default key) is audible (> −60 dBFS) and, for pitched apps, in tune within ±2 cents (normalised autocorrelation + parabolic interpolation, measured 150–400 ms into the note); velocity monotonic in RMS (30/70/110/127); a slide (param 50 = 250 ms, then 51 = key + 7 at the same frame) lands within ±2 cents of the target, is linear in semitones at the midpoint, and shows no gate edge (no > 6 dB dip); one-shots ignore the release; no stuck notes: 2000 seeded gestures (notes, params, slides, pads), params back to default, everything released, then silence (< −60 dBFS 3 s later) or a still-falling tail any check
matrix the portability matrix (below): every target the creator chose, the same phrase a cell that ran and failed
lower the lossy-lowering report for og and lite (below) reported only (sloppy lower --strict exits 1 on a drop)

The report is build/sloppy-report.json (every stage with ms, errors, warnings; the package sha256; built: { faust, og_abi, sloppy_tools }; the checked record; the sound report; the lowering rows). The badge is one line that names what was tested, never just "ok":

sloppy check ✓ aran.sub-808@1.0.0 · sha256 ee317d2706b5 (20.7 KiB) · faust 2.79.3 · og-abi 0.1 · sloppy-tools 0.1.0 · og verify 3/3 profiles · keys 37/37 in tune (max 0.0¢) · slide lands (0¢) · velocity monotonic · no stuck notes (2000 gestures, tail -240 dBFS) · lowers to lite

--fast (iteration in the builder) stops after the build and a 2 s smoke render of the demo pattern. The AI builder (roadmap E5) calls exactly this command and feeds errors back to the model.

sloppy check is also an og-check suite (scripts/og-check --only sloppy; in the full tier, and in fast when sdk/sloppy, sdk/manifest, sdk/examples/sloppy, og/faust/*.h or the Faust CLI change): one row per bundle under sdk/examples/sloppy/, the badge as the metric, every report under the suite's artifacts. It needs faust 2.79.3 (the build box has it; GitHub's runners skip it, like showcase).

One format, four readers

The schema is the superset every reader of a manifest shares; never fork it per host:

Reader Reads
Studio (more-fire :wire SloppyManifestWire, :core ManifestSynthApp) everything but plugin; tolerant loading, refuses on requires only
the web slot (og-slot/slot-host.mjs) and the native slot (slot_runner.h) nothing of the manifest itself: they run the firmware sloppy check built; the param table is in the firmware (og_param_info) and cached in the project (SynthAppSlotWire.paramInfoCache)
the plugin shell (plugin/, tools/gen_params.py) engine.* (the Faust roles, params with stable ids, slide.live), devices.*, layout.*, plugin
the hub (planned, registry kind sloppy-app) identity, tags, targets, requires, assets, the report

Two fields exist for the plugin target: engine.slide.live (a parameter of its own, id + label + range in ms: the legato glide applied when LIVE notes overlap without a parked Next Glide; the sequenced slide never uses it; Studio's live pads do not use it yet, sloppy lower reports it substituted) and plugin (id reverse-DNS, name, vendor, url, auv2 four-character codes; stable once published, like param ids; only when targets is omitted or names plugin). demo may be null.

Two more top-level fields (founder, 2026-10-29): kind instrument (default: keys in, audio out) | effect (audio in -> audio out, an insert in a track's chain; readers refuse any other value; the slots have no audio-in yet, so sloppy check validates an effect's manifest and DSP and then stops with a clear error, and Studio refuses to mount one as a track instrument), and targets: the targets the creator chose to build for, plugin | kick | phone, omitted = all; sloppy check runs only those columns of its matrix (plus always the web render). budget.cpu (default 0.5) is the render's CPU time as a fraction of real time a target may spend.

The portability matrix (sloppy check, founder 2026-10-29)

sloppy check is the portability gate: one column per target, one row per gate, printed after the stages and in report.stages.matrix:

web native-slot plugin-shell
what Faust → wasm, the og-slot render (HeadlessDevice); what every target shares, always run og-run (wasmtime) on the same wasm; the KICK (targets: kick); needs runtime-native/build/og-run, else SKIP Faust → C++ in plugin/ (targets: plugin); macOS only; the E4 spike builds ONE app, so it runs only for that DSP (plugin/build/sloppy-render-host + the .clap), else SKIP
compiles the wasm built loads in wasmtime the .clap renders
param-ids og_param_info ids == the manifest's (+ 50) the same, from og-run's stats pinned by gen_params.py / clap-validator (SKIP)
render 2 bars of the demo at its tempo as MIDI (slides as og 50 before the note; no demo: the default key on every beat), audible, no clipping same phrase, same gate same phrase through the render host
parity the reference (the first target that rendered) plugin/tools/parity/compare.py vs the reference: RMS within 0.1 dB, onsets within 1 sample same
cpu render CPU time / real time ≤ budget.cpu (default 0.5) og-run's realtime_factor ≤ budget host wall time / real time ≤ budget

A cell that cannot run is · SKIP with its reason (once, as a footnote); the check exits non-zero only on a cell that ran and failed. The WAVs and compare.py reports are under build/matrix/.

The firmware a manifest becomes (og/faust/fog_sloppy.h)

One og ABI 0.1 instrument around N instances of the Faust voice, driven like 808-slide's external mode: MIDI notes are the keys (pitch unit hz | midi | key-index), velocity is written to the velocity zone once per hit before the gate (a DSP must not smooth it), OG_PARAM_CHANGE on a manifest id writes that zone sample-accurately on every voice, 50/51 implement the FL slide (glide_time before the pitch target, no gate edge). Voice modes: mono-legato (one voice; glide

0 over a sounding note = legato), poly (same key → free → oldest), one-shot (the gate is a one-sample trigger, the release is ignored, choke groups cut the other members at the same frame). A voice that has gone silent with its gate low is parked (not computed) until its next note, so a 16-voice kit costs only what sounds. Pads play the keys; PLAY runs the demo pattern (slides included) at the host tempo; state = the params by id under the manifest's state schema (unknown ids dropped, missing ids default). No display: the host owns the UI.

The Sloppy Faust contract (short)

One voice, 1–2 outputs, no inputs, declare options "[midi:off]", standard libraries only, no foreign functions. Controls the manifest binds: gate (a button), the pitch control ([unit:Hz] or a MIDI key or a key index), the velocity control (0..1, not smoothed), glide_time ([unit:ms], allows 0) for slides; the manifest's params are the other sliders/nentries (units via [unit:], [scale:log|exp] is the host's curve hint). A one-shot DSP owns its envelope and must not rely on the gate's length (hold the trigger for its attack: see dusty-kit/dsp/main.dsp). The slide idiom (linear in semitones, landing exactly glide_time later) is in sub-808/dsp/main.dsp.

sloppy lower <dir> --target og|lite

The lossy-lowering report: each manifest part is mapped (an exact twin), substituted (a stand-in exists: the roll becomes the step grid of the focused key, poly steps become one track per key, Free lanes become stepped locks) or dropped (lost: a wasm canvas or a declared soundfile on this toolchain, a > 64-step demo on lite). A report, not a gate (--strict exits 1 on a drop). The three examples lower to lite with nothing dropped ((b) substitutes poly steps).

Deferred (sloppy-tools 0.1)

  • engine.slide.live in the og firmware (Studio / Lite live pads): declared and reported, the firmware's table does not carry it yet (sequenced slides through 50/51 work everywhere).

  • engine.soundfile: schema-valid and reported, not built (Faust soundfile() needs the og asset binding in the wrapper). dusty-kit ships a synthesised kit for now.

  • perKey params: the firmware applies a param to every voice; the manifest marks the intent and the host edits all keys (the Drum Rack's pad:<i>:<param> storage is the next step).

  • Param-id reuse / unit-meaning checks against the previously published version (needs the hub).

  • The hub registry kind sloppy-app (source bundle in, firmware built by the hub).