JigDAW plugin profiles
A plugin profile is a machine-readable description of a plugin: what it is, what it does musically, and what a browser needs in order to run it.
JigDAW does not invent this format. It extends one that is already published and already in use, at plugin-universe.com/about/profiles, over a catalogue of hundreds of plugins. A profile written for that catalogue stays valid here. A profile written for JigDAW is also a valid catalogue entry. There is one format, and JigDAW adds the part about running in a browser.
The vocabulary is vocabs/jigdaw.ttl. The rules are vocabs/shapes.ttl. The behaviour a
profile promises is specified in host-plugin-contract.md.
The idea
A plugin is a URL. Dereference it and you get its profile. The profile says what the plugin is and links to the code. There is no registry, no identifier scheme, and no install step distinct from having fetched it.
This makes three things one thing: the plugin's identity, its metadata, and its delivery. A plugin author publishes by publishing.
Three layers
A profile is written in three vocabularies, and it matters which statement belongs to which.
What the plugin is, musically. The transmissions vocabulary, trn:, used unchanged:
trn:PluginProfile, the role taxonomy, the signal types, and the routing properties
trn:accepts, trn:produces, trn:requires, trn:recommendedBefore,
trn:recommendedAfter, trn:companion. Defined in
~/github/transmission/vocabs/profile.ttl and published at
plugin-universe.com/ns.
What it takes to run it in a browser. The JigDAW vocabulary, jig:: the WebAssembly
module, the AudioWorklet processor, the user interface, integrity digests, host
capabilities, and the audio and parameter port shape.
Its parameters. LV2, lv2: and units:, exactly as valis declares them. No bespoke
terms, which is why an LV2 plugin's existing port descriptions map in without translation.
A term general enough to belong to trn: is proposed upstream to the transmission
repository rather than redefined in jig:. That rule comes from plugin-universe and it is
what keeps four projects speaking one language.
The subject is an IRI you control
The subject of a profile is normally the plugin's homepage:
<https://example.org/plugins/cascade/>
a jig:WebPlugin , trn:PluginProfile ;
rdfs:label "Cascade" ;
foaf:homepage <https://example.org/plugins/cascade/> .This is the convention the published spec already uses, and it is precisely what JigDAW
needs: an IRI the author controls, that resolves, and that can be fetched. If you prefer
your own namespace, use it, and keep the foaf:homepage statement so the two can be joined
up.
jig:WebPlugin is a subclass, not a replacement
A JigDAW-loadable plugin declares itself both trn:PluginProfile and jig:WebPlugin.
The subclass matters. vocabs/shapes.ttl targets jig:WebPlugin and never
trn:PluginProfile, so the profiles already published there are untouched by JigDAW's rules.
An existing profile becomes loadable by adding statements, never by being rewritten, and a
profile that describes a native-only plugin stays valid and stays findable. It is simply
not installable here.
That claim is checked rather than asserted: a live profile fetched from
plugin-universe.com validates clean against vocabs/shapes.ttl.
Content negotiation
A plugin IRI serves Turtle to Accept: text/turtle, JSON-LD to
Accept: application/ld+json, and a human-readable page by default. JigDAW hosts send
Accept: text/turtle, application/ld+json;q=0.9.
A .ttl file served as text/plain, which is what most static hosts and GitHub's raw view
return, is read as a profile. The syntax is determined by looking at the content, not by
trusting the media type.
A file: URL is read the same way by a host that has a filesystem, and one naming a
directory reads profile.ttl inside it, because an http plugin IRI ends in a slash and
serves the profile by content negotiation while a filesystem has no such thing. That is what
makes the @base rule below usable: a profile read from a checkout during development means
what it means when it is published. A browser host cannot do this, and does not need to.
CORS is not optional. The profile and every resource it names must be served with
Access-Control-Allow-Origin. AudioWorklet.addModule() fetches cross-origin in CORS
mode, so a response without the header is not merely untrusted, it is unreadable. This is
the one hosting requirement JigDAW cannot relax, and section 1.3 of the contract explains
why.
Resources are named, never blank
Each fetchable file gets its own IRI, skolemised as a fragment of the profile's:
<#processor>
a jig:Processor ;
jig:location <cascade-processor.js> ;
jig:mediaType "text/javascript" ;
jig:registeredName "cascade" ;
jig:integrity "sha384-ggOyR3iMd5o6dRsuOBsWl2VgVBq1TVwNNNXlKOJ6rPGxwsMiBfPGZBpQXKDzHGfz" .Blank nodes are not used for anything addressable. The rationale is taken from downspout's sample descriptor specification, and is worth repeating because the shortcut is tempting: blank nodes are not diffable in version control, they duplicate rather than replace themselves when a document is re-ingested into a triplestore, and they make queries over a corpus needlessly awkward. Skolemising costs nothing and removes all three problems.
jig:integrity is required on every resource. A profile without it is invalid and is
refused. The profile and the code it names need not come from the same origin, and an
unverified profile is an instruction to execute whatever currently sits at a URL.
Set an explicit @base
Write @base at the top of the file rather than relying on the document's own URL. A
profile then means the same thing wherever it is read from, including from a file on disk
during development, and jig:location stays readable as a relative path.
Without it, a profile whose subject is .../cascade/ but which is served at
.../cascade/profile.ttl resolves <#processor> against the wrong base, and every
resource IRI is quietly one directory off.
Capabilities, not assumptions
A plugin says what it needs with trn:requires and what it can use but does without with
jig:prefers. The host answers before fetching any code.
jig:SharedMemory is the one to think about. SharedArrayBuffer needs the page to be
cross-origin isolated, which forces every cross-origin subresource anywhere to opt in with
Cross-Origin-Resource-Policy. Requiring it would mean a plugin author publishes by
publishing and then discovers publishing was not enough. Prefer it; do not require it.
A plugin that speaks MIDI must declare trn:requires jig:MidiEvents. The Web Audio graph
carries audio and nothing else, so MIDI is a host service over a message port rather than a
property of an audio connection. vocabs/shapes.ttl enforces this, because the failure is
otherwise silent: the plugin loads, runs, and its MIDI goes nowhere.
Parameters are declared once
A parameter is an lv2:port on the profile. From that single declaration the host derives
both the processor's parameterDescriptors and the control drawn on the panel. They cannot
disagree, because there is nothing for them to disagree about.
The widget follows the shape of the declaration and is never named by the author:
| Declaration | Control |
|---|---|
lv2:portProperty lv2:toggled, or an enumeration with two scale points |
two-position switch |
| an enumeration with more scale points | selector, every option named |
| anything else | dial |
An author who wants a switch declares a switch's shape. This rule comes from valis, where
no view is permitted to test toggled itself. A widget name in a profile would be a second
source of truth about a port that the port already describes, and the two drift.
A plugin with no jig:ui gets a generated panel from these declarations. That is the
expected case rather than a degraded one: it is consistent with every other plugin, it is
accessible, and it costs the author nothing.
Discovered and curated
JigDAW keeps two kinds of knowledge about a plugin, and inherits the distinction from transmission:
- Curated, the profile: what the plugin is for, what its signals mean, what it pairs with, what to watch out for.
- Discovered, a
jig:Inspection: what a host observed when it actually loaded the module, including failures.
Discovery is authoritative for technical facts. The profile is authoritative for behavioural meaning. The distinction earns its keep, and transmission's example is the one to remember: DrumGen's two audio output channels are intentionally silent compatibility outputs, while its meaningful output is MIDI. No amount of inspecting the binary tells you that, and no amount of curation tells you the real port count of a module that has been rebuilt.
Load failures are recorded too. Which plugins work in which browsers is a fact about the ecosystem, and it is discoverable only if unsuccessful loads are written down.
Getting one
The quickest route is to copy examples/reference-profile.ttl and edit it. It is a complete
profile exercising most of the vocabulary, and it validates.
examples/counterexample-profile.ttl is the opposite: a profile in which every constraint
is violated once. It exists so the shapes are known to fire. A shape that has never
rejected anything is indistinguishable from one that does not run, and two of these did not
run until that file was written.
Validating
rapper -i turtle -c yourplugin/profile.ttl # syntax
npm run validate -- yourplugin/profile.ttl # shapesValidation is a gate, not a diagnostic. A profile that does not validate is not written to the store, because JigDAW ingests profiles from origins it does not control.
Sending one to somebody
A profile and the files it names make a plugin, and a plugin is normally reached by
dereferencing its IRI. plugin-bundles.md defines the two ways to hand the
same plugin over as a file instead, for archiving, mirroring, offline installation, or simply
sending it to a person: a flattened profile, which inlines every resource and which any host
already reads, and a .jig archive, which unpacks into a working plugin origin.
Both depend on the profile naming every file the plugin uses. A plugin that fetches a
resource its profile does not declare cannot be bundled, and bin/bundle.js refuses rather
than making one that will fail on the machine it is sent to.