/*
ARD (Agentic Resource Discovery) ai-catalog.json projection. Pure, dependency-free
formatter that projects a site's Agent Surface Manifest into the ai-catalog data model
for /.well-known/ai-catalog.json. No I/O — the kernel command loads the manifest and
passes it here.
- Do not read files — callers load and pass the manifest.
- Do not duplicate logic from api-catalog or mcp-card — this is a separate spec.
- 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.
- RFC-1113: schema-conformant catalog — absolute-URI url fields (ai-catalog schema format "uri"), ARD-whitelisted types only (a2a-agent-card+json, mcp-server-card+json, agent-skills markdown profile for knowledge SKILL.md docs), per-entry @context + description; non-whitelisted support docs (openapi, search, agent.json) stay advertised via api-catalog/agent.json/Link headers instead of catalog entries.
*/
import type { AgentSurfaceManifest } from "./manifest.ts";
import { toAbsoluteUrl } from "../semantic/ids.ts";
import { AGENT_SKILL_MD_TYPE, knowledgeSkillMdPath } from "./skill-md.ts";
/**
* ARD base context (v0.91) — attached per entry so each JSON-LD node expands
* standalone in generic tooling (spec §4.1 SHOULD).
*/
export const ARD_BASE_CONTEXT = "https://agenticresourcediscovery.org/context/v1";
/**
* Media types recognized by the ARD conformance tooling / ai-catalog audits.
* Only agentic resources with a whitelisted type are catalog entries —
* supporting documents (OpenAPI, knowledge JSON, search, agent.json) stay
* advertised via the api-catalog linkset, agent.json, and Link headers.
*/
export const ARD_A2A_AGENT_CARD_TYPE = "application/a2a-agent-card+json";
export const ARD_MCP_SERVER_CARD_TYPE = "application/mcp-server-card+json";
/** ARD ai-catalog entry — one per discoverable resource. */
export interface ArdCatalogEntry {
"@context": string;
identifier: string;
displayName: string;
type: string;
url: string;
description: string;
representativeQueries: string[];
}
/** ARD ai-catalog host object. */
export interface ArdCatalogHost {
displayName: string;
identifier: string;
}
/** ARD ai-catalog top-level document. */
export interface ArdCatalog {
specVersion: string;
host: ArdCatalogHost;
entries: ArdCatalogEntry[];
}
/**
* Pure: project Agent Surface Manifest into ARD ai-catalog.json.
* Deterministic — entries are sorted by identifier for byte-identical output (DNA-58).
*/
export function buildArdCatalog(
manifest: AgentSurfaceManifest,
hostDisplayName?: string,
): ArdCatalog {
const domain = manifest.baseUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "");
const displayName = hostDisplayName ?? manifest.site;
const abs = (url: string): string => toAbsoluteUrl(manifest.baseUrl, url);
const entries: ArdCatalogEntry[] = [];
// MCP server card entry
if (manifest.interfaces.mcp) {
entries.push({
"@context": ARD_BASE_CONTEXT,
identifier: `urn:air:${domain}:server:mcp`,
displayName: "MCP Server",
type: ARD_MCP_SERVER_CARD_TYPE,
url: abs("/.well-known/mcp/server-card.json"),
description:
"MCP server card describing the site's Model Context Protocol tools and endpoint.",
representativeQueries: [
"What tools are available on this MCP server",
"List MCP server capabilities and endpoints",
],
});
}
// A2A Agent Card entry
entries.push({
"@context": ARD_BASE_CONTEXT,
identifier: `urn:air:${domain}:agent:a2a`,
displayName: "A2A Agent Card",
type: ARD_A2A_AGENT_CARD_TYPE,
url: abs("/.well-known/agent-card.json"),
description: "A2A agent card describing the site's agent capabilities, skills, and endpoint.",
representativeQueries: [
"What agent capabilities does this site offer",
"Get the A2A agent card for this service",
],
});
// Knowledge domain entries — each points at the domain's SKILL.md skill
// document, which carries the canonical JSON endpoint in its frontmatter.
for (const ref of manifest.knowledge) {
entries.push({
"@context": ARD_BASE_CONTEXT,
identifier: `urn:air:${domain}:knowledge:${ref.domain}`,
displayName: `${ref.domain.charAt(0).toUpperCase() + ref.domain.slice(1)} Knowledge`,
type: AGENT_SKILL_MD_TYPE,
url: abs(knowledgeSkillMdPath(ref.domain)),
description: `Agent skill document for the site's canonical ${ref.domain} knowledge.`,
representativeQueries: [
`What ${ref.domain} information is available on this site`,
`Get structured ${ref.domain} data for agent consumption`,
],
});
}
// Sort entries by identifier for deterministic output
entries.sort((a, b) => a.identifier.localeCompare(b.identifier));
return {
specVersion: "1.0",
host: {
displayName,
identifier: `did:web:${domain}`,
},
entries,
};
}