Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion docs/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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 });
});
}

Expand Down
8 changes: 7 additions & 1 deletion docs/data/feed-health.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand All @@ -10,6 +10,9 @@
"status": "ok",
"specVersion": "0.3.0",
"supported": true,
"licenses": [
"CC0-1.0"
],
"cors": true,
"failureKind": null,
"failingSince": null,
Expand All @@ -22,6 +25,9 @@
"status": "ok",
"specVersion": "0.4.0",
"supported": true,
"licenses": [
"CC-BY-4.0"
],
"cors": true,
"failureKind": null,
"failingSince": null,
Expand Down
6 changes: 5 additions & 1 deletion docs/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href=\"/data/adopters.json\"><code>adopters.json</code></a>, a machine-readable registry you can build on. See the <a href=\"/developers/\">developer docs</a>."
Expand Down
6 changes: 5 additions & 1 deletion docs/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href=\"/data/adopters.json\"><code>adopters.json</code></a>, un registro machine-readable sobre el que puedes construir. Mira la <a href=\"/developers/\">documentación para desarrolladores</a>."
Expand Down
28 changes: 22 additions & 6 deletions scripts/check-feeds.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -202,18 +214,19 @@ 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 = [];
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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading