diff --git a/docs/app.js b/docs/app.js
index 5445d2a..ffeedd4 100644
--- a/docs/app.js
+++ b/docs/app.js
@@ -87,6 +87,14 @@
/* ---------- renderers ---------- */
+ /* `license` is an SPDX identifier or a URL. The identifier is the label; a URL has no
+ short form that says anything true about the terms, so it says only that it is custom
+ and links nowhere — the tooltip carries the URL. */
+ function licenseLabel(license) {
+ if (!/^https?:/i.test(license)) return license;
+ return get(state.dict, 'adopters.licenseCustom') || 'Custom licence';
+ }
+
function renderEntries(items, containerId, emptyId) {
var container = document.getElementById(containerId);
var empty = document.getElementById(emptyId);
@@ -146,6 +154,20 @@
if (title) badge.title = title.replace('{version}', item.specVersion);
metas.appendChild(badge);
}
+ // What may be done with the data, which is the other half of "can I consume this
+ // feed". A feed whose events carry different licences says so instead of picking
+ // one: an aggregator has to check every event, and the badge must not imply
+ // otherwise. Inheritance is already resolved in feed-health.json.
+ if (item.licenses && item.licenses.length) {
+ var mixed = item.licenses.length > 1;
+ var label = mixed
+ ? (get(state.dict, 'adopters.licenseMixed') || '{count} licences').replace('{count}', item.licenses.length)
+ : licenseLabel(item.licenses[0]);
+ var lic = el('span', 'entry-meta entry-badge entry-badge-license', label);
+ var licTitle = get(state.dict, mixed ? 'adopters.licenseMixedTitle' : 'adopters.licenseBadge');
+ if (licTitle) lic.title = licTitle.replace('{licenses}', item.licenses.join(', '));
+ metas.appendChild(lic);
+ }
if (metas.childNodes.length) body.appendChild(metas);
card.appendChild(body);
@@ -280,7 +302,8 @@
var health = (state.data.health && state.data.health.feeds) || [];
return (adopters || []).map(function (adopter) {
var entry = health.find(function (f) { return f.feed === adopter.feed; });
- return entry && entry.specVersion ? Object.assign({}, adopter, { specVersion: entry.specVersion }) : adopter;
+ if (!entry) return adopter;
+ return Object.assign({}, adopter, { specVersion: entry.specVersion, licenses: entry.licenses });
});
}
diff --git a/docs/data/feed-health.json b/docs/data/feed-health.json
index 7bd25c6..640c751 100644
--- a/docs/data/feed-health.json
+++ b/docs/data/feed-health.json
@@ -1,5 +1,5 @@
{
- "_comment": "Health of the registered adopter feeds, refreshed daily by scripts/check-feeds.mjs. Feeds are validated against the spec version they declare; `supported` false means older than the supported window, which is information, not a failure. `specVersion` is the last version seen, kept through an outage. `updated` is when this status last changed.",
+ "_comment": "Health of the registered adopter feeds, refreshed daily by scripts/check-feeds.mjs. Feeds are validated against the spec version they declare; `supported` false means older than the supported window, which is information, not a failure. `specVersion` and `licenses` are the last values seen, kept through an outage; `licenses` lists every licence a consumer ends up with, feed-level inheritance already resolved. `updated` is when this status last changed.",
"updated": "2026-08-30",
"latestSpecVersion": "0.4.0",
"feeds": [
@@ -10,6 +10,9 @@
"status": "ok",
"specVersion": "0.3.0",
"supported": true,
+ "licenses": [
+ "CC0-1.0"
+ ],
"cors": true,
"failureKind": null,
"failingSince": null,
@@ -22,6 +25,9 @@
"status": "ok",
"specVersion": "0.4.0",
"supported": true,
+ "licenses": [
+ "CC-BY-4.0"
+ ],
"cors": true,
"failureKind": null,
"failingSince": null,
diff --git a/docs/i18n/en.json b/docs/i18n/en.json
index ef5b7e9..9e3df00 100644
--- a/docs/i18n/en.json
+++ b/docs/i18n/en.json
@@ -184,8 +184,12 @@
},
"adopters": {
"title": "Who publishes in OTE",
- "lead": "Communities and events already exposing their data in this format.",
+ "lead": "Communities, conferences and directories exposing their event data in this format.",
"specVersionBadge": "Publishes OTE {version}. Every published version stays supported — feeds are validated against the one they declare.",
+ "licenseBadge": "Licence of the data: {licenses}. It is what lets you reuse the feed, not the licence of the event.",
+ "licenseMixed": "{count} licences",
+ "licenseMixedTitle": "Its events don't share a licence: {licenses}. Anyone aggregating the whole feed has to check each event's, not only the feed's.",
+ "licenseCustom": "Custom licence",
"empty": "The spec is brand new, so this list is still short. Be one of the first — early adopters get a say in what the standard becomes.",
"emptyCta": "Be the first",
"machine": "This list is data, not markup: it renders adopters.json, a machine-readable registry you can build on. See the developer docs."
diff --git a/docs/i18n/es.json b/docs/i18n/es.json
index fc35d2d..b96e7b9 100644
--- a/docs/i18n/es.json
+++ b/docs/i18n/es.json
@@ -184,8 +184,12 @@
},
"adopters": {
"title": "Quién publica en OTE",
- "lead": "Comunidades y eventos que ya exponen sus datos en este formato.",
+ "lead": "Comunidades, conferencias, directorios que exponen los datos de sus eventos en este formato.",
"specVersionBadge": "Publica OTE {version}. Todas las versiones publicadas siguen soportadas — cada feed se valida contra la que declara.",
+ "licenseBadge": "Licencia de los datos: {licenses}. Es lo que permite reutilizar el feed, no la licencia del evento.",
+ "licenseMixed": "{count} licencias",
+ "licenseMixedTitle": "Sus eventos no comparten licencia: {licenses}. Quien agregue el feed entero tiene que mirar la de cada evento, no solo la del feed.",
+ "licenseCustom": "Licencia propia",
"empty": "La spec acaba de nacer, así que esta lista todavía es corta. Sé de los primeros: quien llega pronto decide en qué se convierte el estándar.",
"emptyCta": "Ser de los primeros",
"machine": "Esta lista es datos, no HTML: pinta adopters.json, un registro machine-readable sobre el que puedes construir. Mira la documentación para desarrolladores."
diff --git a/scripts/check-feeds.mjs b/scripts/check-feeds.mjs
index 053ed71..2571149 100644
--- a/scripts/check-feeds.mjs
+++ b/scripts/check-feeds.mjs
@@ -166,13 +166,25 @@ async function attempt(url) {
// Older than the support window: reachable, and that is all we claim. Judging it
// against rules we no longer support would report a failure nobody agreed to fix.
- if (!supported.has(version)) return ok({ cors, version });
+ if (!supported.has(version)) return ok({ cors, version, licenses: licensesIn(doc) });
const { ajv, validate } = validatorFor(version);
if (!validate(doc)) {
return hard("schema errors:\n" + ajv.errorsText(validate.errors, { separator: "\n" }), { cors, version });
}
- return ok({ cors, version });
+ return ok({ cors, version, licenses: licensesIn(doc) });
+}
+
+/* Every licence a consumer would actually end up with, inheritance resolved. An event
+ that declares its own replaces the feed's, and an aggregator may omit `feed.license`
+ entirely as long as every event carries one — so reporting `feed.license` alone would
+ be wrong precisely for the mixed feeds where the answer matters most. See the spec's
+ "license and source" section. */
+function licensesIn(doc) {
+ const events = Array.isArray(doc.events) ? doc.events : [];
+ const effective = events.map((e) => (e && typeof e.license === "string" ? e.license : doc.license));
+ const all = (effective.length ? effective : [doc.license]).filter((l) => typeof l === "string");
+ return [...new Set(all)].sort();
}
async function checkFeed(url) {
@@ -202,7 +214,7 @@ const state = existsSync(STATE) ? JSON.parse(readFileSync(STATE, "utf8")) : {};
const previous = existsSync(PUBLIC_STATE) ? JSON.parse(readFileSync(PUBLIC_STATE, "utf8")) : {};
// A feed that times out tells us nothing about its version: keep the last one we saw
// rather than blanking it, so the public status stays useful through an outage.
-const lastKnown = new Map((previous.feeds || []).map((f) => [f.feed, f.specVersion]));
+const lastKnown = new Map((previous.feeds || []).map((f) => [f.feed, { specVersion: f.specVersion, licenses: f.licenses }]));
const today = new Date().toISOString().slice(0, 10);
const next = {};
const status = [];
@@ -210,10 +222,11 @@ let failing = 0;
const noCors = [];
for (const adopter of adopters) {
- const { error, kind, cors, version } = await checkFeed(adopter.feed);
+ const { error, kind, cors, version, licenses } = await checkFeed(adopter.feed);
const prev = state[adopter.feed];
+ const last = lastKnown.get(adopter.feed) || {};
const stale = version && !supported.has(version);
- const seen = version || lastKnown.get(adopter.feed) || null;
+ const seen = version || last.specVersion || null;
status.push({
name: adopter.name,
@@ -224,6 +237,9 @@ for (const adopter of adopters) {
// Old but still checkable against its own schemas: information for consumers,
// never a health failure. See the support window at the top of this file.
supported: seen ? supported.has(seen) : null,
+ // Inheritance resolved, so a feed whose events carry different licences reports all
+ // of them: whoever aggregates has to look at each event, not only at feed.license.
+ licenses: licenses && licenses.length ? licenses : last.licenses || null,
cors: error ? null : cors === null,
failureKind: kind,
failingSince: error ? (prev ? prev.since : today) : null,
@@ -318,7 +334,7 @@ writeFileSync(
JSON.stringify(
{
_comment:
- "Health of the registered adopter feeds, refreshed daily by scripts/check-feeds.mjs. Feeds are validated against the spec version they declare; `supported` false means older than the supported window, which is information, not a failure. `specVersion` is the last version seen, kept through an outage. `updated` is when this status last changed.",
+ "Health of the registered adopter feeds, refreshed daily by scripts/check-feeds.mjs. Feeds are validated against the spec version they declare; `supported` false means older than the supported window, which is information, not a failure. `specVersion` and `licenses` are the last values seen, kept through an outage; `licenses` lists every licence a consumer ends up with, feed-level inheritance already resolved. `updated` is when this status last changed.",
updated: unchanged && previous.updated ? previous.updated : today,
latestSpecVersion: LATEST,
feeds: status,