import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join, dirname } from "node:path"; import { LinkGraph } from "../link-graph.js"; import type { CoverageTracker } from "../coverage.js"; import { highlightMustache } from "../mustache-highlight.js"; import { esc } from "../badges.js"; import { fmtAttrValue } from "./object-data.js"; // Template attrs rendered specifically (as their own badges) — everything else a // template authors (e.g. a consumer's @dataflow) is rendered generically below. const PROMPT_KNOWN_ATTRS = new Set(["format", "maxTokens", "requiredSlots", "model", "responseRef", "maxChars", "promptStyle", "payloadRef", "textRef", "description"]); export interface PayloadTreeRow { indent: number; name: string; type: string; isArray: boolean; anchor: string; desc: string; refHtml: string; } export interface PromptPageData { name: string; pkg: string; href: string; breadcrumbHtml: string; attrsHtml: string; desc: string; payloadName: string; payloadHref: string; payloadTree: PayloadTreeRow[]; sourceHtml?: string | undefined; sourceMissingNote?: string | undefined; tocHtml?: string | undefined; packageFiles: { file: string; html: string }[]; } export function buildPromptPage(fqn: string, g: LinkGraph, cov: CoverageTracker, sourceDirs: string[]): PromptPageData { const dn = g.byFqn(fqn)!; const t = dn.node; cov.consumeNode(t); const attrs: string[] = []; for (const a of ["format", "maxTokens", "requiredSlots", "model", "responseRef", "maxChars", "promptStyle"]) { const v = t.attr(a); if (v !== undefined) { cov.consumeAttr(t, a); attrs.push(`@${esc(a)} ${esc(v)}`); } } cov.consumeAttr(t, "payloadRef"); cov.consumeAttr(t, "textRef"); // any other authored attrs (e.g. a consumer's @dataflow) rendered generically for (const [n, v] of [...t.ownAttrs()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) { if (PROMPT_KNOWN_ATTRS.has(n)) continue; cov.consumeAttr(t, n); attrs.push(`@${esc(n)}=${esc(fmtAttrValue(v))}`); } // payload tree (root + one nested level) const pRef = g.refsFrom(fqn).find((r) => r.kind === "payload"); const payload = pRef ? g.byFqn(pRef.to) : undefined; const tree: PayloadTreeRow[] = []; const fieldsOf = (o: NonNullable, indent: number, prefix: string) => { for (const f of o.node.childrenOfType("field")) { const ref = f.attr("objectRef"); const target = typeof ref === "string" ? (g.byFqn(ref.includes("::") ? ref : `${o.pkg}::${ref}`) ?? g.byFqn(ref)) : undefined; tree.push({ indent, name: prefix + f.name, type: f.subType, isArray: f.resolvedIsArray(), anchor: `f-${prefix}${f.name}`, desc: esc(f.attr("description") ?? ""), refHtml: target ? `${esc(target.name)}` : "" }); if (target && indent === 0) fieldsOf(target, 1, `${f.name}.`); } }; if (payload) fieldsOf(payload, 0, ""); const anchors = new Map(tree.map((r) => [r.name, `#${r.anchor}`])); // source resolution const textRef = String(t.attr("textRef") ?? ""); let sourceHtml: string | undefined, tocHtml: string | undefined, sourceMissingNote: string | undefined; let pkgDir: string | undefined; for (const d of sourceDirs) { const p = join(d, ...textRef.split("/")) + ".mustache"; if (existsSync(p)) { pkgDir = dirname(p); const r = highlightMustache(readFileSync(p, "utf8"), (path) => anchors.get(path)); sourceHtml = r.html; tocHtml = r.toc.map((s) => `${esc(s.name)}`).join(" · "); break; } } if (!sourceHtml) sourceMissingNote = `text ref ${esc(textRef)} does not resolve under the metadata roots (forward-pointing ref).`; // other mustache files in the template's package dir const packageFiles: { file: string; html: string }[] = []; if (pkgDir) for (const f of readdirSync(pkgDir).filter((f) => f.endsWith(".mustache")).sort()) { if (join(pkgDir, f) === join(pkgDir, `${textRef.split("/").pop()}.mustache`)) continue; packageFiles.push({ file: f, html: highlightMustache(readFileSync(join(pkgDir, f), "utf8"), (p) => anchors.get(p)).html }); } return { name: dn.name, pkg: dn.pkg, href: dn.href, breadcrumbHtml: `index / ${esc(dn.pkg)} / ${esc(dn.name)}`, attrsHtml: attrs.join(" "), desc: esc(t.attr("description") ?? ""), payloadName: payload?.name ?? "", payloadHref: payload ? esc(g.relHref(dn.href, payload.href)) : "", payloadTree: tree, sourceHtml, sourceMissingNote, tocHtml, packageFiles }; }