/* Maintains packages/share/src/semantic/jsonld/organization.ts as an authored share authored module so agents can evolve it without rediscovering local boundaries. Do not validate input context. Do not fetch external data. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ import type { JsonLdContext } from "./context.ts"; import type { JsonLdNode } from "./types.ts"; export function buildOrganizationNode(context: JsonLdContext): JsonLdNode { const { page, ids } = context; return { // RFC-0242: a Bodenstation studio deployment overrides this to ["Organization", "ProfessionalService"]. "@type": page.organization.schemaType ?? ["Organization", "NGO"], "@id": ids.organization, name: page.organization.name, legalName: page.organization.legalName, description: page.organization.description, url: page.organization.url, email: page.organization.email, ...(page.organization.foundingYear ? { foundingDate: page.organization.foundingYear } : {}), ...(page.organization.registration ? { identifier: page.organization.registration } : {}), ...(page.organization.founders?.length ? { founder: page.organization.founders.map((person) => ({ "@id": ids.person(person.name), })), } : {}), ...(page.organization.boardMembers?.length ? { member: page.organization.boardMembers.map((person) => ({ "@id": ids.person(person.name), })), } : {}), ...(page.organization.contactPoints?.length ? { contactPoint: page.organization.contactPoints.map((contactPoint) => ({ "@type": "ContactPoint", contactType: contactPoint.contactType, email: contactPoint.email, })), } : {}), ...(page.organization.address ? { address: { "@type": "PostalAddress", streetAddress: page.organization.address.streetAddress, postalCode: page.organization.address.postalCode, addressLocality: page.organization.address.addressLocality, addressCountry: page.organization.address.addressCountry, }, } : {}), ...(page.organization.areaServed?.length ? { areaServed: page.organization.areaServed } : {}), ...(page.organization.location?.serviceArea?.length && !page.organization.areaServed?.length ? { areaServed: page.organization.location.serviceArea } : {}), ...(page.organization.team?.length ? { // RFC-0148: public team members as schema.org employees. employee: page.organization.team.map((member) => ({ "@type": "Person", name: member.name, ...(member.description ? { description: member.description } : {}), })), } : {}), ...(page.organization.offer?.prices?.length ? { // RFC-0147: project the canonical offer as schema.org Offer nodes. // RFC-0745: emit priceCurrency from canonical source currency when available. makesOffer: page.organization.offer.prices.map((price) => ({ "@type": "Offer", name: price.label, priceSpecification: { "@type": "PriceSpecification", price: price.amount, }, ...(price.currency ? { priceCurrency: price.currency } : {}), })), } : {}), // RFC-0163: entity-grounding signals for Google Knowledge Graph + LLM disambiguation. ...(page.organization.sameAs?.length ? { sameAs: page.organization.sameAs } : {}), ...(page.organization.logo ? { logo: { "@type": "ImageObject", url: page.organization.logo } } : {}), ...(page.organization.image ? { image: page.organization.image } : {}), ...(page.organization.offer?.prices?.length ? { priceRange: buildPriceRange(page.organization.offer.prices) } : {}), }; } function buildPriceRange(prices: Array<{ amount: string; currency?: string }>): string { const numericAmounts = prices .map((p) => Number.parseFloat(p.amount)) .filter((n) => !Number.isNaN(n)); if (numericAmounts.length === 0) return ""; const min = Math.min(...numericAmounts); const max = Math.max(...numericAmounts); const currency = prices.find((p) => p.currency)?.currency; const formatAmount = (n: number) => (Number.isInteger(n) ? String(n) : n.toFixed(2)); if (min === max) { return currency ? `${formatAmount(min)} ${currency}` : formatAmount(min); } return currency ? `${formatAmount(min)}–${formatAmount(max)} ${currency}` : `${formatAmount(min)}–${formatAmount(max)}`; }