Plugin collections
Status: normative for a host that opens a collection and for anyone publishing one. Extends: host-plugin-contract.md sections 1 and 3.
A collection is a list of plugins published as one Turtle file at one URL. It gives a person something to hand a host that means "these", whether that is a set of reverbs, the plugins from one author, or the ones a tutorial uses. A host given the URL can list the plugins, check each one, and offer the ones that will run.
A collection is an opinion, not a registry. Contract section 1.1 says a plugin is published by publishing its IRI, and a collection does not change that. Including a plugin in a collection confers no trust on it and needs no permission from its author, and leaving a plugin out of every collection does not make it any less loadable.
1. What a collection says
A collection states four things and no more: its own name, a description, and the IRI and name of each plugin it includes.
@base <https://example.org/collections/reverbs> .
@prefix jig: <http://purl.org/stuff/jigdaw/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix dcterms: <http://purl.org/dc/terms/> .
<>
a jig:PluginCollection ;
rdfs:label "Reverbs" ;
rdfs:comment "Rooms, plates and springs, from small to very large." ;
dcterms:hasPart <https://example.org/plugins/reference/> ,
<https://plugins.example.net/plate/> .
<https://example.org/plugins/reference/> rdfs:label "Reference" .
<https://plugins.example.net/plate/> rdfs:label "Plate" .| Term | Subject | Required | Meaning |
|---|---|---|---|
a jig:PluginCollection |
the collection | exactly one per document | what a host looks for |
rdfs:label |
the collection | exactly one | its name, shown in place of the URL |
rdfs:comment |
the collection | at most one; SHOULD be present | what it is for |
dcterms:hasPart |
the collection | at least one | a plugin's IRI |
rdfs:label |
each plugin | exactly one | the plugin's name, as the collection gives it |
A document MUST contain exactly one jig:PluginCollection. The collection MUST be named by
an IRI, either the document's own (<>) or a fragment of it, never a blank node
(CLAUDE.md, RDF conventions).
Each dcterms:hasPart value MUST be a plugin IRI as contract section 1.1 defines it: https:,
or http: on loopback only. It is the same IRI a host would be given to load the plugin by
hand.
Each included plugin MUST have an rdfs:label in the collection document. This is a copy of
the name, and it exists so that a host can draw the list before any profile has arrived. The
profile's own rdfs:label governs once it has (section 3.3).
A collection states no order. RDF has none to give without rdf:List, which this project
does not use for anything addressable, and a host sorts by name.
Anything else in the document is permitted and ignored: dcterms:creator, prov: metadata, a
foaf:homepage. A collection MUST NOT carry the plugins' profiles, resources or digests. The
profile at each IRI is the only statement of what a plugin is, and a second copy is one that
can be out of date.
The terms are jig:PluginCollection, a subclass of trn:PluginCollection
(transmission/vocabs/profile.ttl, itself a subclass of
dcmitype:Collection),
and dcterms:hasPart
and rdfs:label, reused unchanged. jig:PluginCollection exists because nothing about a
collection is specific to a web plugin; a native catalogue could publish one the same shape
under trn:PluginCollection directly. vocabs/shapes.ttl enforces this section as
jig:PluginCollectionShape, examples/reference-collection.ttl is a valid collection and
examples/counterexample-collection.ttl breaks each constraint once.
1.1 Relative IRIs name plugins served beside the collection
A profile states an explicit @base so it means the same thing wherever it is read. A
collection MAY omit @base. Relative references then resolve against the URL the collection
was fetched from, so a collection published next to its plugins names them as
<../plugins/pulse/> and keeps working on every host that serves the pair, including
localhost. web/collections/jigdaw.ttl is written this way.
A collection of plugins served from somewhere else names them absolutely. The same reasoning extends to referring to a plugin anywhere outside a collection document too: see for-plugin-authors.md, "Refer to it by its absolute IRI".
2. Publishing one
A collection MUST be served as Turtle, with Access-Control-Allow-Origin permitting the host's
origin, for the reason contract section 1.3 gives: without that header a browser host
cannot read the response at all.
The media type SHOULD be text/turtle. A host MUST accept text/plain, which is what most
static hosts and GitHub's raw view return for a .ttl file, and determines the syntax by
reading the content. The file extension SHOULD be .ttl.
A collection is an ordinary cacheable resource. Contract section 1.4 applies to it as it does to a profile.
3. Opening one
A host opening a collection MUST do the following in order.
3.1 The document is refused whole or not at all
- Fetch the collection URL, sending
Accept: text/turtle. - Parse it, resolving relative IRIs against the URL fetched.
- Validate it against
vocabs/shapes.ttl. - Find exactly one
jig:PluginCollection.
A failure at any of these MUST refuse the whole collection, before any member is fetched, and MUST name the step that failed. A host that fetched members of a document it could not read would be acting on a guess about what the document said.
A result of sh:Warning severity, such as a missing description, MUST NOT refuse the
collection. A host SHOULD report it.
3.2 Each member is checked, and no code is fetched
For each included plugin, a host MUST perform steps 1 and 2 of contract section 3.1 against
its IRI: fetch the profile, parse it, validate it, and evaluate trn:requires and
jig:wasmFeature against the capabilities the host offers.
A member that is a composite plugin (nested-plugins.md) is checked as its own profile and its own
trn:requires. Its members are not fetched, since a collection of forty racks would otherwise fetch forty trees to draw a list: the
whole tree is checked when a person loads it, as section 6 of that document says.
A host MUST NOT fetch any jig:module, jig:processor, jig:ui or jig:asset while opening a
collection. Those are fetched and verified against their jig:integrity digests when a person
loads the plugin, and a host MUST perform the whole of contract section 3.1 again at that
point. Two reasons:
- Verifying the code now proves nothing about the bytes fetched later. The digests are checked on the bytes that are about to run, so the check has to happen then regardless.
- A collection of forty plugins would download forty plugins' code to show a list from which a person picks one. Contract section 2.1 exists to avoid running or fetching code for no reason, and the same argument applies.
This is the check worth doing at collection time: it finds everything that can be known without code. A dead link, a moved plugin, a profile that no longer validates, and a plugin this host cannot run all show up before a person chooses, rather than after.
Members are independent. A member that fails MUST NOT stop the others from being checked or shown, and MUST be reported with the step of contract section 3.1 at which it failed and the reason, in the same terms a failed load would give. A host SHOULD limit how many profiles it fetches at once.
3.3 The profile governs
Where a collection and a profile disagree, the profile is right.
- Name. A host MUST show the profile's
rdfs:labelfor a member whose profile has arrived, and SHOULD report a difference from the collection's name to the person, since it usually means the collection is out of date. - Identity. A member's IRI is where its profile is fetched from, and the subject of the
profile is the plugin's identity. They differ legitimately for a mirror or a local
checkout, where the profile's explicit
@basenames the canonical origin (see plugin-profiles.md). A host SHOULD report the difference and MUST NOT refuse the member for it.
3.4 What a host shows
A host MUST offer to load each member that passed section 3.2, by the IRI the collection gives. A host MUST NOT offer a load control for a member that failed; it shows the member's name and the reason instead. A disabled control reads the same as an enabled one to a screen reader, so leaving it out is the version of this that carries the information (CLAUDE.md, interface rules).
Loading a member is loading a plugin, with no shortcut. Being in a collection does not skip consent for a foreign plugin (contract section 12.4), integrity verification (section 3.2), or anything else.
4. In the browser host
src/rdf/CollectionReader.js reads a parsed collection into a plain object.
src/catalogue/CollectionLoader.js implements section 3, taking the member check as an
injected function; the page passes PluginLoader.loadProfile, which is steps 1 and 2 of
contract section 3.1 and nothing after.
The page has an "Open a collection" form under "Load by IRI", and opens one given in the query string, so a collection can be shared as a link:
https://strandz.it/jigdaw/?collection=https://example.org/collections/reverbsweb/collections/jigdaw.ttl is the collection of every plugin in this repository, and is what
the form offers by default. Published, it is
strandz.it/jigdaw/collections/jigdaw.ttl:
curl -H "Accept: text/turtle" https://strandz.it/jigdaw/collections/jigdaw.ttlThe same set with screenshots instead of triples is the plugin gallery, built by bin/build-gallery.js: one headless screenshot per generated panel, grouped into generative, MIDI, instruments and processors.
tests/catalogue/CollectionLoader.test.js opens it against the real profiles on disk, fails
if it and plugins/ disagree in either direction, and checks that opening it fetches the
document and the profiles and nothing else.
A foreign plugin (contract section 12) is refused by loadProfile, which reads native
profiles only, so a collection that includes one shows it as not loadable here. Loading it
through the foreign path by hand still works.