Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213// linked-art.mjs// Serialize Aesthetic Computer entities to Linked Art — the JSON-LD profile of// CIDOC CRM that Getty and peer cultural-heritage institutions actually ingest.// This is the read-side sibling of bluesky-mirror.mjs: instead of broadcasting a// mood to a social firehose, we expose opted-in works as linked open data.//// These functions are PURE: they take already-resolved plain fields and return a// JSON-LD object. All MongoDB lookups + the consent/rights gate live in the// crm.mjs function. Keeping them pure makes them unit-testable against fixtures.//// Canonical identity URIs live on data.aesthetic.computer; each carries a link// back to the human-facing page on the apex. See crm/SCORE.md.// 2026.06.29
export const LA_CONTEXT = "https://linked.art/ns/v1/linked-art.json";export const DATA_BASE = "https://data.aesthetic.computer";export const WEB_BASE = "https://aesthetic.computer";
// Getty AAT vocabulary terms. Every id below was verified against the live// Getty SPARQL endpoint (https://vocab.getty.edu/sparql) on 2026-06-29; labels// are Getty's own preferred labels. (crm/SCORE.md §8, open decision 3 — closed.)const AAT = { primaryName: ["300404670", "preferred terms"], paintings: ["300033618", "paintings (visual works)"], digitalArt: ["300386810", "digital art (visual works)"], digitalImages: ["300215302", "digital images"], software: ["300028566", "software"], briefTexts: ["300418049", "brief texts"], english: ["300388277", "English (language)"],};
// Build a Getty AAT classification node.function aat(key) { const [id, label] = AAT[key]; return { id: `http://vocab.getty.edu/aat/${id}`, type: "Type", _label: label };}
// Supported licenses → Creative Commons URIs. A work cannot be broadcast// without one (Getty pipelines reject objects lacking a rights statement).export const LICENSES = { "CC0-1.0": ["https://creativecommons.org/publicdomain/zero/1.0/", "CC0 1.0 Universal"], "CC-BY-4.0": ["https://creativecommons.org/licenses/by/4.0/", "CC BY 4.0"], "CC-BY-SA-4.0": ["https://creativecommons.org/licenses/by-sa/4.0/", "CC BY-SA 4.0"], "CC-BY-NC-4.0": ["https://creativecommons.org/licenses/by-nc/4.0/", "CC BY-NC 4.0"],};
// Standardized rights statements (rightsstatements.org) used by Europeana/DPLA.// "InC" is the default for works we describe but don't hold reuse rights to:// the record is open + citable, but copyright (and the right to sell) stays// entirely with the artist. CC keys above are an explicit opt-in upgrade.export const RIGHTS_STATEMENTS = { InC: ["http://rightsstatements.org/vocab/InC/1.0/", "In Copyright"],};
export const DEFAULT_RIGHTS = "InC";
export function isLicense(key) { return Object.prototype.hasOwnProperty.call(LICENSES, key);}
// E30 Right → a `subject_to` rights statement. Accepts a CC license key or a// rightsstatements.org key (e.g. "InC").function rights(key) { const entry = LICENSES[key] || RIGHTS_STATEMENTS[key]; if (!entry) return []; const [id, label] = entry; return [{ type: "Right", _label: label, classified_as: [{ id, type: "Type", _label: label }] }];}
// E52 Time-Span from a Date or ISO string.function timespan(when) { if (!when) return undefined; const iso = when instanceof Date ? when.toISOString() : new Date(when).toISOString(); return { type: "TimeSpan", begin_of_the_begin: iso, end_of_the_end: iso };}
// Public image URL for a painting. Two slug shapes exist:// "{sub}/{kind}/{ts}" (kind varies: chat, painting, …) → strip the sub prefix// "{ts}" (older bare-timestamp slugs) → live under painting/{ts}// Either way the URL keys off @handle, not the auth0 sub.export function paintingImageUrl(handle, slug, sub) { let mediaPath = String(slug || ""); if (!mediaPath) return undefined; if (sub && mediaPath.startsWith(sub + "/")) { mediaPath = mediaPath.slice(sub.length + 1); } else if (!mediaPath.includes("/")) { mediaPath = "painting/" + mediaPath; } return `${WEB_BASE}/media/@${handle.replace(/^@/, "")}/${mediaPath}.png`;}
// The author as an E21 Person reference (the hub every work points back to).function personRef(handle) { const clean = handle.replace(/^@/, ""); return { id: `${DATA_BASE}/@${clean}`, type: "Person", _label: `@${clean}` };}
// E12 Production / E65 Creation event carrying authorship + time.function madeBy(type, handle, when, place = true) { const ev = { type, carried_out_by: [personRef(handle)] }; const ts = timespan(when); if (ts) ev.timespan = ts; if (place) { ev.took_place_at = [{ id: WEB_BASE, type: "Place", _label: "Aesthetic Computer" }]; } return ev;}
// A `subject_of` / seeAlso link back to the human-facing apex page.function seeAlso(webUrl, label) { return { type: "LinguisticObject", _label: label, digitally_carried_by: [ { type: "DigitalObject", access_point: [{ id: webUrl, type: "DigitalObject" }] }, ], };}
// ⸻ Person — @sat → E21 Person ⸻export function personToLinkedArt({ handle, latestMood } = {}) { const clean = handle.replace(/^@/, ""); const doc = { "@context": LA_CONTEXT, id: `${DATA_BASE}/@${clean}`, type: "Person", _label: `@${clean}`, identified_by: [ { type: "Name", content: `@${clean}`, classified_as: [aat("primaryName")] }, ], subject_of: [seeAlso(`${WEB_BASE}/@${clean}`, "Aesthetic Computer profile")], }; if (latestMood?.mood) { doc.subject_of.push({ type: "LinguisticObject", _label: "latest mood", classified_as: [aat("briefTexts")], content: latestMood.mood, }); } return doc;}
// ⸻ Painting → E22 Human-Made Object / D1 Digital Object ⸻export function paintingToLinkedArt({ code, handle, when, imageUrl, license } = {}) { const clean = handle.replace(/^@/, ""); return { "@context": LA_CONTEXT, id: `${DATA_BASE}/painting/${code}`, type: "DigitalObject", _label: `painting ${code} by @${clean}`, classified_as: [aat("paintings"), aat("digitalArt"), aat("digitalImages")], produced_by: madeBy("Production", handle, when), subject_to: rights(license), subject_of: [seeAlso(`${WEB_BASE}/painting/${code}`, "view on Aesthetic Computer")], digitally_shown_by: imageUrl ? [ { type: "DigitalObject", format: "image/png", access_point: [{ id: imageUrl, type: "DigitalObject" }], }, ] : undefined, };}
// ⸻ Piece (KidLisp / .mjs) → E73 Information Object + E65 Creation ⸻export function pieceToLinkedArt({ code, handle, when, source, hash, license } = {}) { const clean = handle.replace(/^@/, ""); const doc = { "@context": LA_CONTEXT, id: `${DATA_BASE}/piece/${code}`, type: "DigitalObject", _label: `piece $${code} by @${clean}`, classified_as: [aat("software"), aat("digitalArt")], created_by: madeBy("Creation", handle, when), subject_to: rights(license), subject_of: [seeAlso(`${WEB_BASE}/${code}`, "run on Aesthetic Computer")], }; if (hash) { // No Getty AAT term for "checksum"; an unclassified Identifier with a // descriptive label is the honest representation. doc.identified_by = [{ type: "Identifier", content: hash, _label: "sha-256 checksum" }]; } if (source) { // No Getty AAT term for "source code"; carry the program text unclassified. doc.carries = [{ type: "LinguisticObject", content: source, _label: "source code" }]; } return doc;}
// ⸻ Mood → E33 Linguistic Object + E65 Creation ⸻export function moodToLinkedArt({ handle, rkey, mood, when, blueskyUri, license } = {}) { const clean = handle.replace(/^@/, ""); const doc = { "@context": LA_CONTEXT, id: `${DATA_BASE}/mood/${clean}/${rkey}`, type: "LinguisticObject", _label: `mood by @${clean}`, classified_as: [aat("briefTexts")], content: mood, language: [aat("english")], created_by: madeBy("Creation", handle, when, false), subject_to: rights(license), subject_of: [seeAlso(`${WEB_BASE}/moods~${clean}~${rkey}`, "view on Aesthetic Computer")], }; if (blueskyUri) { doc.equivalent = [{ id: blueskyUri, type: "LinguisticObject", _label: "Bluesky post" }]; } return doc;}