NIP-VOCAB
Von Edufeed ·
== NIP-VOCAB=== Vocabularies
`draft` `optional`
This NIP defines a system for publishing controlled vocabularies,
taxonomies, thesauri, and classification schemes as Nostr events. It is
inspired by https://www.w3.org/TR/skos-reference/[SKOS] (Simple
Knowledge Organization System) but designed natively for Nostr.
=== Motivation
Nostr has labeling infrastructure (link:32.md[NIP-32]) and
domain-specific classification patterns (link:AMB.md[NIP-AMB],
link:35.md[NIP-35]), but no general-purpose mechanism for publishing,
discovering, and using shared controlled vocabularies on the protocol
itself. Vocabulary definitions currently live on external web servers.
This NIP brings them onto Nostr, making them decentralized,
discoverable, and maintainable by anyone.
=== Event Kinds
This NIP uses six addressable event kinds — one per entity type, with a
parallel draft kind following NIP-23’s pattern:
[cols=",,",options="header",]
|===
|Entity |Published |Draft
|ConceptScheme |`39737` |`39736`
|Concept |`39738` |`39735`
|Collection |`39739` |`39734`
|===
Standalone mappings between concepts (see
link:#standalone-mappings[Standalone Mappings]) use three further
addressable kinds, without draft counterparts:
[cols=",",options="header",]
|===
|Entity |Kind
|Mapping |`39740`
|Concordance |`39741`
|Mapping review |`39742`
|===
_Published kinds increment (39737 → 39738 → 39739); draft kinds
decrement (39736 → 39735 → 39734) so the `39737` / `39736` pair from an
earlier revision of this NIP is preserved._
Distinct kinds let relays (which only index single-letter tags per
NIP-01) return just the entity type a client asked for. The
`["type", ...]` tag is retained for human readability and secondary
validation but carries no query semantics.
Clients displaying _published_ vocabularies MUST filter by published
kinds and MUST NOT surface draft kinds as published.
=== Concept Scheme
A concept scheme represents a vocabulary, taxonomy, or classification
system.
The Nostr event itself provides standard metadata: `created_at` serves
as the publication/modification timestamp and `pubkey` identifies the
publisher. Publishers MAY add a `version` tag for explicit versioning.
[source,jsonc]
----
{
"kind": 39737,
"tags": [
["d", "schulfaecher"],
["type", "ConceptScheme"],
["prefLabel", "Schulfächerliste", "de"],
["prefLabel", "School Subject List", "en"],
["description", "Classification of subjects taught in German schools", "en"],
["a", "39738::s10", "", "hasTopConcept"],
["a", "39738::s20", "", "hasTopConcept"],
// optional: bridge to external URI
["i", "http://w3id.org/kim/schulfaecher/"]
],
"content": ""
}
----
==== Concept Scheme Tags
[width="100%",cols="15%,20%,28%,37%",options="header",]
|===
|Tag |Value |Required |Description
|`d` |identifier |yes |Stable identifier for the scheme
|`type` |`ConceptScheme` |yes |Distinguishes from Concept and Collection
events
|`prefLabel` |label, language |yes |Preferred display label. Third
element is a BCP47 language tag. There MUST be at most one `prefLabel`
per language. Repeat for multiple languages
|`description` |text, language |no |Description of the scheme. Third
element is a BCP47 language tag
|`a` |coordinates, relay, `hasTopConcept` |yes* |References to top-level
concepts. Required if the scheme has a hierarchy
|`i` |external URI |no |Bridge to an external URI identifier for this
scheme
|===
=== Concept
A concept represents a single term, category, or idea within a
vocabulary.
[source,jsonc]
----
{
"kind": 39738,
"tags": [
["d", "s1017"],
["type", "Concept"],
["prefLabel", "Mathematik", "de"],
["prefLabel", "Mathematics", "en"],
["altLabel", "Mathe", "de"],
["altLabel", "Maths", "en"],
["notation", "s1017"],
// scheme membership
["a", "39737::schulfaecher", "", "inScheme"],
// hierarchical relations (non-top concept — has broader)
["a", "39738::s10", "", "broader"],
["a", "39738::s101701", "", "narrower"],
// associative relation
["a", "39738::s1018", "", "related"],
// optional: bridge to external URI
["i", "http://w3id.org/kim/schulfaecher/s1017"]
],
"content": "Mathematics as taught in German school curricula"
}
----
==== Concept Tags
[width="100%",cols="15%,20%,28%,37%",options="header",]
|===
|Tag |Value |Required |Description
|`d` |identifier |yes |Stable identifier for the concept
|`type` |`Concept` |yes |Distinguishes from ConceptScheme and Collection
events
|`prefLabel` |label, language |yes |Preferred display label. Third
element is a BCP47 language tag. There MUST be at most one `prefLabel`
per language
|`altLabel` |label, language |no |Alternative label (synonym,
abbreviation). Third element is a BCP47 language tag
|`hiddenLabel` |label, language |no |Hidden label for search indexing
(catches misspellings, legacy terms). Not displayed to users
|`notation` |code |no |Classification code or notation within the scheme
|`a` |coordinates, relay, `inScheme` |yes |Every concept MUST reference
its containing ConceptScheme
|`a` |coordinates, relay, `broader` |yes* |Required for non-top
concepts. Top concepts omit this and are referenced via `hasTopConcept`
on the scheme
|`a` |coordinates, relay, `topConceptOf` |no |Top concepts SHOULD
include this marker pointing back to their scheme
|`a` |coordinates, relay, `narrower` |yes* |Required if the concept has
children. The child MUST have a corresponding `broader` tag
|`a` |coordinates, relay, marker |no |Other relations: `related`,
mapping markers (see link:#relations[Relations])
|`r` |external URI, marker |no |Mappings to external vocabularies (see
link:#external-mappings[External Mappings])
|`i` |external URI |no |Bridge to an external URI identifier for this
concept
|`definition` |text, language |no |Formal definition. Third element is a
BCP47 language tag. The `content` field serves as the primary
definition; `definition` tags provide additional translations
|`scopeNote` |text, language |no |Intended usage scope. Third element is
a BCP47 language tag
|`example` |text, language |no |Usage example. Third element is a BCP47
language tag
|`note` |text, language |no |Generic documentation note. Third element
is a BCP47 language tag
|===
A concept MAY belong to multiple concept schemes by including multiple
`inScheme` tags.
The `content` field SHOULD contain the concept’s definition or scope
note.
____
*Note:* SKOS also defines `historyNote`, `changeNote`, and
`editorialNote`. These are intentionally omitted — Nostr’s addressable
event replacement provides implicit version history.
____
==== Label Integrity
The following constraints MUST be observed:
* At most one `prefLabel` per language per concept
* `prefLabel`, `altLabel`, and `hiddenLabel` are pairwise disjoint: the
same literal MUST NOT appear as more than one label type on the same
event
=== Collection
A collection is a meaningful grouping of concepts within a scheme, used
for organizational purposes.
[source,jsonc]
----
{
"kind": 39739,
"tags": [
["d", "bachelor-programs"],
["type", "Collection"],
["prefLabel", "Bachelor Programs", "en"],
["prefLabel", "Bachelor-Studiengänge", "de"],
["a", "39737::schulfaecher", "", "inScheme"],
["a", "39738::civil-eng", "", "member"],
["a", "39738::mech-eng", "", "member"]
],
"content": ""
}
----
Collections are organizational only. Semantic relations (`broader`,
`narrower`, `related`) MUST NOT be used on or target Collection events.
____
*Note:* SKOS also defines `OrderedCollection` for collections with
meaningful ordering. This is intentionally omitted from this NIP for
simplicity and may be specified in a future extension.
____
=== Drafts
Each published kind has a matching draft kind with identical tag
structure:
* `kind:39736` — ConceptScheme draft
* `kind:39735` — Concept draft
* `kind:39734` — Collection draft
Drafts use the same `["d", ...]` identifier as their eventual published
counterpart. Clients SHOULD treat drafts as private-to-the-author by
default (e.g., publish only to the author’s outbox relays). When
publishing a draft as final, clients SHOULD delete the draft via NIP-09
and publish a new event under the corresponding published kind.
`a` tag references between vocabulary events SHOULD use the *published*
kind prefix of the target even when the target currently exists only as
a draft — i.e., a draft Concept whose `inScheme` points at a
still-unpublished ConceptScheme SHOULD reference it as
`39737::`, not `39736::`. This way references
resolve automatically the moment the target is published. A client
rendering a reference whose target has not yet been published MAY show
it as an unresolved reference.
`published_at` remains OPTIONAL metadata on published events (for
preserving first-publish time across edits) and is never a draft gate.
Its value, when present, is a stringified unix timestamp in seconds —
mirroring NIP-23’s use of the same tag.
==== Relay policy
Relays MAY accept or reject draft kinds per their policy. Public
discovery relays MAY choose to reject drafts; private or author-scoped
relays (e.g., a user’s outbox) SHOULD accept them to support
cross-device editing.
=== Relations
All relations between vocabulary events use `a` tags with a marker
(fourth element) indicating the relation type. This reuses the
established Nostr pattern for typed references to addressable events
(see link:AMB.md[NIP-AMB], link:53.md[NIP-53]).
==== Hierarchical Relations
[cols=",,",options="header",]
|===
|Marker |Inverse |Description
|`broader` |`narrower` |Direct parent in the hierarchy
|`narrower` |`broader` |Direct child in the hierarchy
|===
Publishers MUST assert *both directions*: when a concept has a `broader`
tag, the broader concept MUST include a corresponding `narrower` tag
pointing back. This enables traversal in both directions without
requiring additional queries.
Only assert `broader`/`narrower` for *direct* parent-child
relationships. Transitive ancestry is inferred by clients traversing the
chain.
==== Associative Relations
[cols=",,",options="header",]
|===
|Marker |Symmetric |Description
|`related` |yes |Non-hierarchical association between concepts
|===
Publishers SHOULD assert both directions: if concept A is `related` to
concept B, then B SHOULD include a corresponding `related` tag pointing
to A.
A concept MUST NOT be both hierarchically related (directly or
transitively) and associatively related to the same concept.
==== Scheme and Collection Relations
[width="100%",cols="27%,30%,43%",options="header",]
|===
|Marker |Used on |Description
|`inScheme` |Concept, Collection |Links to the containing ConceptScheme
|`hasTopConcept` |ConceptScheme |Links to top-level concepts
|`topConceptOf` |Concept |Links a top concept back to its ConceptScheme
|`member` |Collection |Links to a concept or nested collection belonging
to this collection
|===
==== Mapping Relations (Nostr-to-Nostr)
For linking concepts *across different concept schemes* published on
Nostr:
[width="100%",cols="26%,34%,40%",options="header",]
|===
|Marker |Symmetric |Description
|`exactMatch` |yes |Concepts are interchangeable in all contexts
|`closeMatch` |yes |Concepts are similar but not always interchangeable
|`broadMatch` |no |This concept is narrower than the target
(cross-scheme)
|`narrowMatch` |no |This concept is broader than the target
(cross-scheme)
|`relatedMatch` |yes |Non-hierarchical association across schemes
|===
`exactMatch` MUST NOT be combined with `broadMatch`, `narrowMatch`, or
`relatedMatch` for the same concept pair. `exactMatch` implies
`closeMatch` — every exact match is inherently a close match. Clients
querying for close matches SHOULD also include exact matches in their
results.
`exactMatch` is transitive: if concept A is an exact match of B, and B
is an exact match of C, then A is also an exact match of C. Clients
resolving mappings SHOULD account for transitive chains.
For symmetric mapping relations (`exactMatch`, `closeMatch`,
`relatedMatch`) embedded in Concept events, publishers SHOULD assert
both directions. `narrowMatch` is the inverse of `broadMatch` —
publishers SHOULD assert both directions: when a concept has a
`broadMatch` tag, the target concept SHOULD include a corresponding
`narrowMatch` tag pointing back, and vice versa. This rule applies to
embedded tags only; link:#standalone-mappings[standalone mappings] are
stored in one direction and clients infer the inverse.
Embedded mapping tags can only be added by the concept’s author. To link
concepts of different publishers, or to publish a crosswalk as a third
party, use link:#standalone-mappings[standalone mappings].
`broadMatch`, `narrowMatch`, and `relatedMatch` are cross-scheme
counterparts of `broader`, `narrower`, and `related` respectively.
Clients aggregating semantic relations SHOULD include mapping relations
in their results.
By convention, mapping markers link concepts in *different* schemes.
Example — mapping between two Nostr-native vocabularies:
[source,jsonc]
----
{
"kind": 39738,
"tags": [
["d", "video"],
["type", "Concept"],
["prefLabel", "Video", "de"],
["prefLabel", "Video", "en"],
["a", "39737::hcrt", "", "inScheme"],
// mapping to another publisher's vocabulary
["a", "39738::moving-image", "", "exactMatch"]
],
"content": ""
}
----
=== External Mappings
For mappings to concepts in external URI-based vocabularies (that are
not published on Nostr), use `r` tags with a mapping marker:
[source,jsonc]
----
["r", "https://w3id.org/kim/hcrt/video", "exactMatch"],
["r", "http://purl.org/dcx/lrmi-vocabs/mediaType/Video", "closeMatch"],
["r", "http://purl.org/dc/dcmitype/MovingImage", "broadMatch"]
----
The same mapping markers apply: `exactMatch`, `closeMatch`,
`broadMatch`, `narrowMatch`, `relatedMatch`.
This is distinct from the `i` tag, which asserts identity (``this
concept IS that external URI''), while `r` tags with mapping markers
assert a relationship (``this concept CORRESPONDS TO that external
concept'').
=== External Identity Bridge
The `i` tag (per link:73.md[NIP-73]) provides a bridge to external
URI-based identifier systems:
[source,jsonc]
----
["i", "https://w3id.org/kim/schulfaecher/s1017"]
----
This declares: ``this Nostr concept is a re-publication of the concept
at that external URI.'' Systems that know the concept by its HTTP URI
can use this to cross-reference.
=== Referencing Vocabulary Concepts from Other Events
Other Nostr events can reference concepts defined by this NIP using
standard `a` tags:
[source,jsonc]
----
// In a kind:30142 AMB event, a kind:1 note, or any other event:
["a", "39738::s1017", ""]
----
This makes the concept queryable: a relay filter
`{"#a": ["39738::s1017"]}` returns all events that reference
that concept, across all event kinds.
=== Standalone Mappings
Mapping relations embedded in Concept events (see
link:#mapping-relations-nostr-to-nostr[Mapping Relations
(Nostr-to-Nostr)]) can only be asserted by the concept’s own author, and
every added mapping rewrites the concept event. This section defines
three further addressable kinds — Mapping, Concordance, and Mapping
review — for mappings that link concepts across publishers or that a
third party (a curator or institution) publishes as a crosswalk, without
touching the concept events themselves. The model follows
https://gbv.github.io/jskos/[JSKOS], the data format used by Cocoda /
coli-conc, and the
https://ts4nfdi.github.io/mapping-sameness-identifier/[TS4NFDI Mapping
Sameness Identifier] for content-addressing mappings.
==== Concept Identifiers
Every mapping refers to concepts and schemes by a *concept identifier*
string:
* the concept’s external URI (the value of its `i` tag), if it has one;
* otherwise `nostr:::`
(e.g. `nostr:39738::ethik`,
`nostr:39737::klassenstufen`).
The same rule applies to schemes (the scheme’s `i` tag, else
`nostr:39737::`). Identifiers MUST be compared as exact
strings after NFC normalisation.
In the `nostr:::` form, `` is always the
*published* kind (39738 Concept, 39737 ConceptScheme), even when the
event being identified is itself a draft, and even when an `a` tag being
converted to this form points at a draft-kind address
(e.g. `39735::` resolves to `nostr:39738::`). If
an event carries several `i` tags, the identifier is the value of the
*first* one (matching the reference library, whose `vocabIdentifier`
uses the first `i` tag it finds).
Identifiers MUST NOT contain relay hints, event ids, labels, or
timestamps; the `nostr:` form is the raw address, not a bech32 `naddr`
(whose encoding varies with relay hints). Label, definition, notation,
and hierarchy edits therefore never change an identifier.
*Identifiers are immutable.* Once a concept or scheme is published:
* its `i` tag MUST NOT be added, changed, or removed — a concept
published without `i` keeps its `nostr:` identifier forever;
* its `d` tag and author are fixed by addressability.
A concept that needs a different identifier MUST be published as a new
concept; the old concept MAY be linked to it with `exactMatch`.
Publishing tools MUST refuse edits that would add, change, or remove the
`i` tag of a published concept, and SHOULD warn when a scheme is
published without URIs if it is expected to be referenced from other
events.
____
Known deviation: `nostr:::` is not a NIP-21 bech32 URI.
It is kept for compatibility with existing data that uses this form.
____
==== Mapping — `kind:39740` (addressable)
[source,jsonc]
----
{
"kind": 39740,
"content": "optional free-text note",
"tags": [
["d", "mapping:"],
["relation", "exactMatch"],
["from", "nostr:39738::5-6"], // klassenstufen concepts have no i-tag URI
["to", "https://w3id.org/kim/educationalLevel/level_2"],
["fromScheme", "nostr:39737::klassenstufen"],
["toScheme", "https://w3id.org/kim/educationalLevel/"],
// index tags (for relay filtering only; carry no semantics)
["a", "39738::5-6", "wss://relay.edufeed.org"],
["a", "39738::level_2", "wss://relay.edufeed.org"],
["a", "39737::klassenstufen"],
["a", "39737::educational-level"],
["i", "https://w3id.org/kim/educationalLevel/level_2"],
// optional
["a", "39741::klassenstufen--educational-level", "", "partOf"],
["relevance", "0.8"]
]
}
----
===== Mapping Tags
[width="100%",cols="34%,33%,33%",options="header",]
|===
|Tag |Card. |Meaning
|`d` |1 |Sameness identifier (see link:#sameness-identifier[Sameness
Identifier])
|`relation` |1 |One of `exactMatch`, `closeMatch`, `broadMatch`,
`narrowMatch`, `relatedMatch`
|`from` |exactly 1 |Source concept identifier
|`to` |1..n |Target concept identifiers; several = AND combination
(JSKOS `memberSet`)
|`fromScheme`, `toScheme` |1 each |Scheme identifiers
|`a` (no marker) |0..n |Index: address of every referenced
concept/scheme that exists as a Nostr event. SHOULD be present for each
such concept/scheme
|`i` |0..n |Index: every referenced concept identifier that is a URI
(not `nostr:`). SHOULD be present
|`a` … `partOf` |0..1 |Concordance membership (see
link:#concordance[Concordance])
|`relevance` |0..1 |Decimal 0..1, JSKOS `mappingRelevance`
|content |— |Free-text note (JSKOS `note`)
|===
Rules:
* `to` MUST be non-empty. Null mappings (``no equivalent in toScheme'')
are reserved for a future revision.
* OR alternatives are expressed as separate mappings (JSKOS
`memberChoice` is not supported).
* `from` and `to` concepts SHOULD be in different schemes.
* Relays can find all mappings touching a concept with
`{"kinds":[39740],"#a":["39738::"]}` or
`{"kinds":[39740],"#i":[""]}`.
* Clients MUST ignore mapping events whose `d` does not equal the
sameness identifier computed from their `from`, `relation` and `to`
tags.
===== Sameness Identifier
`d` is the
https://ts4nfdi.github.io/mapping-sameness-identifier/[TS4NFDI Mapping
Sameness Identifier], affirmative form:
[arabic]
. subjects = [`from`], objects = `to` values, each NFC-normalised;
duplicate values MUST be removed before sorting (subjects and objects
are sets);
. sort subjects and objects by Unicode code point, join each with `|`;
. predicate = the full SKOS IRI,
`http://www.w3.org/2004/02/skos/core#`;
. string = ` ` (single spaces), UTF-8;
. SHA-256, lowercase hex; `d` = `mapping:`.
The following are test vectors using `nostr:` identifiers:
[width="100%",cols="25%,25%,25%,25%",options="header",]
|===
|from |relation |to |`d`
|`nostr:39738:d2689e2f41dabfba953da26655a94ce2aa4e029c383ee921c6a4deafab99a612:5-6`
|`exactMatch` |`https://w3id.org/kim/educationalLevel/level_2`
|`mapping:79bbfff8bd56bf8d827a8687a91e6abe94bf496305e4258071bb73347ad99431`
|`https://edufeed.org/ns/ekw#lrt/arbeitsblatt` |`exactMatch`
|`https://w3id.org/kim/hcrt/worksheet`
|`mapping:3ee871bd4f4a105564793c8ff1476f8e72685eb5d8c6b6e3188e3ec95752c75c`
|`nostr:39738:d2689e2f41dabfba953da26655a94ce2aa4e029c383ee921c6a4deafab99a612:ubung-lernkontrolle`
|`narrowMatch` |`https://w3id.org/kim/hcrt/assessment`,
`https://w3id.org/kim/hcrt/drill_and_practice` (any order; duplicates
don’t matter)
|`mapping:eac6f2b08c2a3fa266f7e3a750abdd1643b7e4e4460b133d0e053f2a775443f7`
|===
Implementations MUST reproduce the two published
https://ts4nfdi.github.io/mapping-sameness-identifier/[TS4NFDI] test
vectors (the negative one by implementing the `~` suffix internally,
even though negative mappings are not published in this revision), plus
the test vectors above.
Consequences:
* The same assertion by different authors produces the same `d`, so
agreement and reviews aggregate across authors.
* An author republishing the same content (unchanged
`from`/`relation`/`to`) replaces their previous event, e.g. to edit the
note, `relevance`, or `partOf`.
* Changing the relation or the members changes `d`, producing a new
event; the author SHOULD delete the old one via NIP-09.
===== Direction
Only one direction is stored. Clients resolving mappings MUST infer the
inverse: `exactMatch`↔`exactMatch`, `closeMatch`↔`closeMatch`,
`relatedMatch`↔`relatedMatch`, `broadMatch`↔`narrowMatch`. Inverses are
only inferable for 1:1 mappings; a 1:n mapping is only readable in its
stored direction.
The rule ``publishers SHOULD assert both directions'' (see
link:#mapping-relations-nostr-to-nostr[Mapping Relations
(Nostr-to-Nostr)]) applies to *embedded* mapping tags only; for
standalone mappings, publishers SHOULD NOT publish the inverse as a
separate event.
==== Concordance — `kind:39741` (addressable)
[source,jsonc]
----
{
"kind": 39741,
"content": "Crosswalk der EKW-Klassenstufen auf KIM-Bildungsstufen.",
"tags": [
["d", "klassenstufen--educational-level"],
["title", "Klassenstufen ↔ Bildungsstufe", "de"],
["fromScheme", "nostr:39737::klassenstufen"], ["toScheme", "https://w3id.org/kim/educationalLevel/"], ["a", "39737::klassenstufen"], ["a", "39737::educational-level"], ["p", "", "", "maintainer"], ["license", "CC0-1.0"] ]}----===== Concordance Tags[width="100%",cols="34%,33%,33%",options="header",]|===|Tag |Card. |Meaning|`d` |1 |Author-chosen slug|`title` |1..n |Title, BCP47 language as 3rd element, at most one perlanguage|`fromScheme`, `toScheme` |1 each |Scheme identifiers|`a` (no marker) |0..2 |Index: scheme addresses, where schemes exist onNostr|`p` … `maintainer` |0..n |Additional pubkeys allowed to add members|`license` |0..1 |SPDX identifier|content |— |Description|===Rules:* A concordance does not list its mappings. Its members are mappingswith a `partOf` tag pointing at it *whose author is the concordanceauthor or a listed maintainer*. `partOf` from anyone else MUST beignored. Clients fetch members with`{"kinds":[39740],"#a":["39741::"],"authors":["", "", …]}`.* A member’s `fromScheme`/`toScheme` MUST equal the concordance’s;clients MUST ignore non-matching members.* To remove a member, the maintainer MUST delete the mapping (NIP-09) orrepublish it without `partOf`.* A mapping without `partOf` is a valid personal assertion; it MAYappear when browsing a concept but MUST NOT appear in trust-restrictedconsumers.* Concordances are the unit of trust for consumers (seelink:#resolving-mappings[Resolving Mappings]).==== Mapping Review — `kind:39742` (addressable)[source,jsonc]----{ "kind": 39742, "content": "Klasse 5-6 umfasst in manchen Ländern noch die Primarstufe.", "tags": [ ["d", "mapping:"], ["vote", "-1"], ["mismatch", "https://uri.gbv.de/terminology/mismatch/inexact"], ["a", "39740::mapping:"], ["a", "39741::klassenstufen--educational-level"] ]}----===== Mapping Review Tags[width="100%",cols="34%,33%,33%",options="header",]|===|Tag |Card. |Meaning|`d` |1 |Sameness identifier of the reviewed mapping content|`vote` |1 |`+1` or `-1`|`mismatch` |0..n |Only with `-1`: reason URI from`https://uri.gbv.de/terminology/mismatch/`|`a` |0..n |Optional context: the specific mapping event seen, theconcordance|content |— |Optional comment|===Rules:* There MUST be at most one current vote per reviewer per mappingcontent; a reviewer republishing a review for the same `d` replacestheir previous vote.* All reviews of a mapping content can be fetched with`{"kinds":[39742],"#d":["mapping:"]}`.* There is no separate ``confirmed'' event. A mapping is *confirmedwithin a concordance* when the concordance owner or a maintainer haspublished a mapping with the same `d` and `partOf` pointing at thatconcordance. Adopting someone else’s mapping means republishing the samecontent under one’s own key.* Web-of-trust weighting and aggregating votes into `relevance` are outof scope for this revision; clients SHOULD show the raw vote sum and whovoted.==== Resolving MappingsClient resolution SHOULD proceed as follows:[arabic]. *Normalise* both sources into one model`{id, from, to[], relation, fromScheme, toScheme, author, partOf?, origin}`:embedded `a` tags with mapping markers and embedded `r` tags withmapping markers on Concept events (author = concept author,`origin: "embedded"`), and standalone `kind:39740` events(`origin: "standalone"`). Converting an embedded `a`-tag target into aconcept identifier (see link:#concept-identifiers[Concept Identifiers])needs the target concept event, for its `i` tag; clients SHOULD fallback to the `nostr:` form when the target is not available. Embeddedmappings’ `id` SHOULD be computed with the same sameness algorithm asstandalone mappings; entries with an equal `id` SHOULD be merged intoone mapping with multiple sources.. *Inverses* SHOULD be computed per link:#direction[Direction], so alookup from either side finds a mapping.. The *trust set* is a parameter of resolution:`{concordances: ParsedConcordance[], authors?: pubkey[], includeEmbedded: boolean}`,or `'all'`. Clients SHOULD pass parsed concordance objects (carryingauthor, maintainers, and schemes), not just concordance addresses, sincemembership requires them. Editors SHOULD pass ``everything'' and groupresults by source; search consumers SHOULD pass their configuredconcordances.. *Transitivity*: clients SHOULD chain only `exactMatch`, only withinthe trust set, at most 3 hops. `closeMatch`, `broadMatch`,`narrowMatch`, and `relatedMatch` MUST NOT be chained in this revision.. *Conflicts*: an `exactMatch` together with a `broadMatch`,`narrowMatch`, or `relatedMatch` for the same concept pair — includingpairs where the `exactMatch` is reached only transitively — SHOULD bereported as a conflict. A conflicting target inside the trust set MUSTget no `exactMatch` result, direct or transitive, and exact chains MUSTNOT be expanded through it.=== Authority ModelIn Nostr, there is no domain authority. Instead, *the pubkey is thenamespace*. Multiple pubkeys can publish events for the same vocabulary(same `d` tags), resulting in different versions.Clients SHOULD use the user’s web of trust to select which publisher’sversion of a vocabulary to display, similar to how link:54.md[NIP-54](Wiki) handles competing article versions.A vocabulary publisher MAY signal their identity using alink:05.md[NIP-05] identifier or by including provenance metadata in theconcept scheme’s `content` field.=== Querying==== Fetch a Specific Scheme[source,json]----{"kinds": [39737], "authors": [""], "#d": ["schulfaecher"]}----==== Fetch a Specific Concept[source,json]----{"kinds": [39738], "authors": [""], "#d": ["s1017"]}----==== Fetch all ConceptSchemes from a publisher[source,json]----{ "kinds": [39737], "authors": [""] }----==== Fetch all Concepts belonging to a ConceptScheme[source,json]----{ "kinds": [39738], "#a": ["39737::"] }----==== Fetch all Collections belonging to a ConceptScheme[source,json]----{ "kinds": [39739], "#a": ["39737::"] }----All filters use only single-letter tags (`a`) or the `kinds` field, sothey work with any NIP-01-compliant relay. No multi-letter tag indexingis required.Clients that also want to show the viewing user’s drafts can widen thefilter by adding the matching draft kind — for example, schemes plusdraft schemes:[source,json]----{ "kinds": [39737, 39736], "authors": [""] }----==== Find All Events Tagged with a Concept[source,json]----{"#a": ["39738::s1017"]}----This works across all event kinds — AMB events, notes, labels, etc.==== Traversing HierarchiesClients can walk `broader`/`narrower` chains to compute transitiveancestry or descendant sets. To collect all narrower concepts under agiven concept:[arabic]. Fetch the concept and read its `narrower` tags. For each narrower concept, fetch it and read its `narrower` tags. Repeat recursively until no further `narrower` tags are foundSKOS defines `broaderTransitive` and `narrowerTransitive` as inferredproperties. In this model, clients compute transitive closures bytraversal rather than storing them as explicit tags.=== Examples==== Example 1: A Complete Small Vocabulary*Concept Scheme:*[source,json]----{ "kind": 39737, "pubkey": "abc123...", "tags": [ ["d", "hcrt"], ["type", "ConceptScheme"], ["prefLabel", "Hochschulcampus Ressourcentypen", "de"], ["prefLabel", "Higher Education Resource Types", "en"], ["a", "39738:abc123...:text", "wss://relay.example.com", "hasTopConcept"], ["a", "39738:abc123...:audiovisual", "wss://relay.example.com", "hasTopConcept"], ["i", "https://w3id.org/kim/hcrt/scheme"] ], "content": "A controlled vocabulary of resource types for higher education."}----*Top Concept:*[source,json]----{ "kind": 39738, "pubkey": "abc123...", "tags": [ ["d", "audiovisual"], ["type", "Concept"], ["prefLabel", "Audiovisuelles Medium", "de"], ["prefLabel", "Audiovisual Medium", "en"], ["a", "39737:abc123...:hcrt", "wss://relay.example.com", "inScheme"], ["a", "39737:abc123...:hcrt", "wss://relay.example.com", "topConceptOf"], ["a", "39738:abc123...:video", "wss://relay.example.com", "narrower"], ["a", "39738:abc123...:audio", "wss://relay.example.com", "narrower"], ["i", "https://w3id.org/kim/hcrt/audiovisual"] ], "content": ""}----*Leaf Concept with External Mappings:*[source,json]----{ "kind": 39738, "pubkey": "abc123...", "tags": [ ["d", "video"], ["type", "Concept"], ["prefLabel", "Video", "de"], ["prefLabel", "Video", "en"], ["altLabel", "Film", "de"], ["altLabel", "Moving Image", "en"], ["notation", "video"], ["a", "39737:abc123...:hcrt", "wss://relay.example.com", "inScheme"], ["a", "39738:abc123...:audiovisual", "wss://relay.example.com", "broader"], ["r", "https://w3id.org/kim/hcrt/video", "exactMatch"], ["r", "http://purl.org/dc/dcmitype/MovingImage", "closeMatch"], ["i", "https://w3id.org/kim/hcrt/video"] ], "content": "A recording of moving visual images."}----==== Example 2: Cross-Scheme MappingA concept in one Nostr-native vocabulary mapped to a concept in another:[source,json]----{ "kind": 39738, "pubkey": "abc123...", "tags": [ ["d", "math"], ["type", "Concept"], ["prefLabel", "Mathematik", "de"], ["prefLabel", "Mathematics", "en"], ["a", "39737:abc123...:schulfaecher", "wss://relay.example.com", "inScheme"], ["a", "39738:def456...:mathematics", "wss://relay2.example.com", "exactMatch"] ], "content": ""}----==== Example 3: Using a Vocabulary Concept in an AMB EventAn educational resource referencing a Nostr-native vocabulary concept:[source,json]----{ "kind": 30142, "tags": [ ["d", "pythagorean-theorem-video"], ["name", "Pythagorean Theorem Explained"], ["a", "39738:abc123...:s1017", "wss://relay.example.com"], ["a", "39738:abc123...:video", "wss://relay.example.com"], ["t", "Pythagoras"], ["t", "Geometrie"] ], "content": "An introductory video explaining the Pythagorean theorem"}----=== Migration from single-kindEvents published under the pre-split rule (all as `kind:39737`,disambiguated by the `type` tag) are not automatically valid under thisrevision. Publishers SHOULD republish Concept and Collection eventsunder `kind:39738` / `kind:39739`. Clients MAY continue to read legacyevents for backward compatibility but SHOULD treat any `kind:39737`event whose `type` tag is not `ConceptScheme` as legacy-only.==== Impact on existing referencesAny event in the wider Nostr ecosystem (for example, NIP-AMBlearning-resource events) that references a concept via an `a` tag withthe legacy `39737::` coordinate becomes stale once thatconcept is republished under `kind:39738`: the reference’s kind prefixno longer matches the target’s kind. Clients resolving such referencesSHOULD attempt a fallback lookup using `kind:39738` with the same`pubkey:d` suffix before treating the reference as unresolved.Publishers migrating their own vocabularies SHOULD:[arabic]. Republish each Concept event under `kind:39738` and each Collectionevent under `kind:39739`, preserving the original `d` identifier.. NIP-09-delete the legacy `kind:39737` Concept / Collection events sorelays can garbage-collect them.. Optionally announce the migration (e.g., via a kind:1 note) sodownstream consumers know to update their cached references.During a transition window, clients MAY dual-read both the legacy andthe new kinds to minimize broken references.=== References* https://www.w3.org/TR/skos-reference/[SKOS Reference (W3C)] — thevocabulary model this NIP is inspired by* https://skohub.io/[SkoHub] — SKOS vocabulary publishing infrastructure* link:01.md[NIP-01] — Basic protocol, addressable events* link:09.md[NIP-09] — Event deletion (draft-to-published cleanup)* link:23.md[NIP-23] — Long-form content (precedent for draft kind and`published_at` tag)* link:32.md[NIP-32] — Labeling* link:73.md[NIP-73] — External Content IDs (`i` tag)* link:54.md[NIP-54] — Wiki (web-of-trust authority model precedent)* link:AMB.md[NIP-AMB] — AMB metadata events (precedent for `a` tagmarkers)* https://gbv.github.io/jskos/[JSKOS data format] — mapping andconcordance model* https://ts4nfdi.github.io/mapping-sameness-identifier/[TS4NFDI MappingSameness Identifier]