/* RFC-1075: derive persistent canonical URIs for PBP entity envelopes from site origin and entity identity. Used for Linked Data @id fields in JSON-LD and llms-full.txt projections. RFC-1115: URIs carry a .json suffix and are dereferenceable — agent.knowledge.generate emits a static JSON-LD document at each path. Does not validate that the entity ID is well-formed — callers are responsible for providing valid IDs. Does not perform URL normalization beyond trailing-slash removal on the site origin. RFC-1075: initial — deriveCanonicalUri pure function for persistent entity @id URIs. RFC-1115: canonical entity URIs gain a .json suffix and become dereferenceable — agent.knowledge.generate emits a JSON-LD document at each emitted path. */ /** * Entity types supported by the PBP canonical URI scheme. * * The URI path pattern is `/.well-known/entity/{type}/{id}.json` for typed entities * (e.g. offerings) and `/.well-known/entity/business.json` for the single business entity. * RFC-1115: the .json suffix makes every URI dereferenceable on a static host — * agent.knowledge.generate emits a JSON-LD document at each path. */ export type CanonicalEntityType = "business" | "offering"; /** * RFC-1075: derive a persistent canonical URI for a PBP entity. * * The URI follows the pattern (RFC-1115 — dereferenceable .json form): * - Business: `{siteOrigin}/.well-known/entity/business.json` * - Offering: `{siteOrigin}/.well-known/entity/offering/{entityId}.json` * * The `siteOrigin` is stripped of trailing slashes to produce a clean base. * When `siteOrigin` is absent or empty, the function returns `undefined` — * callers should skip emitting `@id` in that case. * * @param siteOrigin - The site origin URL (e.g. `https://example.com`) * @param type - The entity type (`"business"` or `"offering"`) * @param entityId - The entity ID (required for `"offering"`, ignored for `"business"`) * @returns The canonical URI string, or `undefined` when `siteOrigin` is absent */ /** * Reduce a PBP entity id to the slug used in canonical entity URIs and file * paths. Entity IDs are URIs (e.g. `https://warpgogol.com/id/offerings/automation`); * the last non-empty path segment is the slug (`automation`). Plain slugs pass * through unchanged. Single source for the id→slug mapping so the canonical * `@id`, the emitted file path, and the fact-parity comparator stay consistent. */ export function canonicalEntitySlug(entityId: string): string { return entityId.split("/").filter(Boolean).pop() ?? entityId; } export function deriveCanonicalUri( siteOrigin: string | undefined, type: CanonicalEntityType, entityId?: string, ): string | undefined { if (!siteOrigin) return undefined; const base = siteOrigin.replace(/\/+$/, ""); if (type === "business") { return `${base}/.well-known/entity/business.json`; } if (!entityId) return undefined; // Entity IDs are URIs (e.g. `https://warpgogol.com/id/offerings/automation`). // Embedding the raw URI into the path produces a malformed double-scheme URI // (`.../offering/https://warpgogol.com/...`) and a non-filesystem-safe path // (`path.join` collapses `//` → `/`, breaking the @id = served-path parity). const slug = canonicalEntitySlug(entityId); return `${base}/.well-known/entity/offering/${slug}.json`; }