@runcontext/site 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/erickittelson/Desktop/ContextKit/packages/site/dist/index.cjs","../src/generator.ts","../src/templates.ts","../src/search/build-index.ts"],"names":[],"mappings":"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","file":"/Users/erickittelson/Desktop/ContextKit/packages/site/dist/index.cjs","sourcesContent":[null,"/**\n * Site generator for ContextKit.\n *\n * `generateSite` accepts a Manifest and returns a Map of relative file path\n * to HTML content. The caller is responsible for writing files to disk.\n *\n * `buildSite` is a convenience function used by the CLI that compiles context,\n * generates the site, and writes files to the output directory.\n */\n\nimport ejs from 'ejs';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { Manifest, SiteConfig, ContextKitConfig } from '@runcontext/core';\nimport {\n indexTemplate,\n modelTemplate,\n schemaTemplate,\n rulesTemplate,\n glossaryTemplate,\n ownerTemplate,\n searchTemplate,\n} from './templates.js';\nimport { buildSearchIndex } from './search/build-index.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface GenerateSiteOptions {\n manifest: Manifest;\n config?: SiteConfig;\n}\n\n// ---------------------------------------------------------------------------\n// generateSite — pure function returning Map<path, html>\n// ---------------------------------------------------------------------------\n\n/**\n * Generate all HTML pages for the documentation site.\n *\n * @param manifest - The compiled ContextKit manifest\n * @param config - Optional site configuration (title, base_path)\n * @returns Map of relative file paths to HTML content\n */\nexport function generateSite(\n manifest: Manifest,\n config?: SiteConfig,\n): Map<string, string> {\n const files = new Map<string, string>();\n const siteTitle = config?.title ?? 'ContextKit';\n const basePath = (config?.base_path ?? '').replace(/\\/+$/, '');\n\n const commonData = {\n siteTitle,\n basePath,\n };\n\n // --- Index page ---\n files.set(\n 'index.html',\n ejs.render(indexTemplate, {\n ...commonData,\n pageTitle: 'Home',\n models: manifest.models,\n governance: manifest.governance,\n tiers: manifest.tiers,\n owners: manifest.owners,\n }),\n );\n\n // --- Model pages ---\n for (const [name, model] of Object.entries(manifest.models)) {\n const gov = manifest.governance[name] ?? null;\n const tier = manifest.tiers[name] ?? null;\n const rules = manifest.rules[name] ?? null;\n\n // Main model page\n files.set(\n `models/${name}.html`,\n ejs.render(modelTemplate, {\n ...commonData,\n pageTitle: name,\n model,\n gov,\n tier,\n rules,\n }),\n );\n\n // Schema browser page\n files.set(\n `models/${name}/schema.html`,\n ejs.render(schemaTemplate, {\n ...commonData,\n pageTitle: `${name} — Schema`,\n model,\n gov,\n tier,\n }),\n );\n\n // Rules page\n files.set(\n `models/${name}/rules.html`,\n ejs.render(rulesTemplate, {\n ...commonData,\n pageTitle: `${name} — Rules`,\n modelName: name,\n rules,\n }),\n );\n }\n\n // --- Glossary page ---\n files.set(\n 'glossary.html',\n ejs.render(glossaryTemplate, {\n ...commonData,\n pageTitle: 'Glossary',\n terms: manifest.terms,\n }),\n );\n\n // --- Owner pages ---\n for (const [oid, owner] of Object.entries(manifest.owners)) {\n // Find models governed by this owner\n const governedModels: Array<{ name: string; tier: string | null }> = [];\n for (const [modelName, gov] of Object.entries(manifest.governance)) {\n if (gov.owner === oid) {\n const tierScore = manifest.tiers[modelName];\n governedModels.push({\n name: modelName,\n tier: tierScore?.tier ?? null,\n });\n }\n }\n\n files.set(\n `owners/${oid}.html`,\n ejs.render(ownerTemplate, {\n ...commonData,\n pageTitle: owner.display_name,\n owner,\n governedModels,\n }),\n );\n }\n\n // --- Search page ---\n const searchIndex = buildSearchIndex(manifest, basePath);\n files.set(\n 'search.html',\n ejs.render(searchTemplate, {\n ...commonData,\n pageTitle: 'Search',\n searchIndexJson: JSON.stringify(searchIndex),\n }),\n );\n\n // --- Search index JSON (for programmatic access) ---\n files.set('search-index.json', JSON.stringify(searchIndex, null, 2));\n\n return files;\n}\n\n// ---------------------------------------------------------------------------\n// buildSite — convenience function used by CLI\n// ---------------------------------------------------------------------------\n\n/**\n * Build the documentation site and write files to disk.\n *\n * Called by the CLI `site` command. Accepts a manifest, config, and output\n * directory, then generates and writes all site files.\n *\n * @param manifest - The compiled ContextKit manifest\n * @param config - The full ContextKit configuration\n * @param outputDir - Directory to write site files to\n */\nexport async function buildSite(\n manifest: Manifest,\n config: ContextKitConfig,\n outputDir: string,\n): Promise<void> {\n const siteConfig = config.site;\n const files = generateSite(manifest, siteConfig);\n\n for (const [filePath, content] of files) {\n const fullPath = path.join(outputDir, filePath);\n const dir = path.dirname(fullPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n fs.writeFileSync(fullPath, content, 'utf-8');\n }\n}\n","/**\n * EJS template strings for the ContextKit documentation site.\n *\n * All templates use Tailwind CDN for styling and are rendered\n * via `ejs.render()` with embedded template strings.\n */\n\n// ---------------------------------------------------------------------------\n// Shared layout helpers\n// ---------------------------------------------------------------------------\n\nconst HEAD = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title><%= pageTitle %> — <%= siteTitle %></title>\n <script src=\"https://cdn.tailwindcss.com\"></script>\n</head>`;\n\nconst NAV = `<nav class=\"bg-gray-900 text-white px-6 py-3 flex items-center gap-6\">\n <a href=\"<%= basePath %>/\" class=\"font-bold text-lg\"><%- siteTitle %></a>\n <a href=\"<%= basePath %>/\" class=\"hover:underline\">Models</a>\n <a href=\"<%= basePath %>/glossary.html\" class=\"hover:underline\">Glossary</a>\n <a href=\"<%= basePath %>/search.html\" class=\"hover:underline\">Search</a>\n</nav>`;\n\nconst FOOTER = `<footer class=\"mt-12 border-t py-4 px-6 text-gray-500 text-sm\">\n Generated by <a href=\"https://github.com/erickittelson/ContextKit\" class=\"underline\">ContextKit</a>\n</footer>`;\n\n// ---------------------------------------------------------------------------\n// Tier badge helper (used in multiple templates)\n// ---------------------------------------------------------------------------\n\nconst TIER_BADGE = `<% function tierBadge(tier) {\n var colors = { none: 'bg-gray-200 text-gray-700', bronze: 'bg-amber-700 text-white', silver: 'bg-gray-400 text-white', gold: 'bg-yellow-400 text-black' };\n var cls = colors[tier] || colors.none;\n return '<span class=\"px-2 py-0.5 rounded text-xs font-semibold uppercase ' + cls + '\">' + tier + '</span>';\n} %>`;\n\n// ---------------------------------------------------------------------------\n// Index (home) template\n// ---------------------------------------------------------------------------\n\nexport const indexTemplate = `${HEAD}\n<body class=\"bg-white text-gray-900 min-h-screen\">\n${NAV}\n${TIER_BADGE}\n<main class=\"max-w-5xl mx-auto p-6\">\n <h1 class=\"text-3xl font-bold mb-6\"><%- siteTitle %></h1>\n\n <section>\n <h2 class=\"text-xl font-semibold mb-4\">Models</h2>\n <% if (Object.keys(models).length === 0) { %>\n <p class=\"text-gray-500\">No models found.</p>\n <% } else { %>\n <div class=\"grid gap-4\">\n <% for (var name of Object.keys(models)) { %>\n <div class=\"border rounded-lg p-4 hover:shadow transition\">\n <div class=\"flex items-center gap-3\">\n <a href=\"<%= basePath %>/models/<%= name %>.html\" class=\"text-lg font-medium text-blue-600 hover:underline\"><%= name %></a>\n <% if (tiers[name]) { %><%- tierBadge(tiers[name].tier) %><% } %>\n </div>\n <% if (models[name].description) { %>\n <p class=\"text-gray-600 mt-1\"><%= models[name].description %></p>\n <% } %>\n <% if (governance[name]) { %>\n <div class=\"flex gap-2 mt-2 text-xs text-gray-500\">\n <% if (governance[name].owner) { %>\n <span>Owner: <a href=\"<%= basePath %>/owners/<%= governance[name].owner %>.html\" class=\"underline\"><%= governance[name].owner %></a></span>\n <% } %>\n <% if (governance[name].trust) { %>\n <span>Trust: <%= governance[name].trust %></span>\n <% } %>\n </div>\n <% } %>\n </div>\n <% } %>\n </div>\n <% } %>\n </section>\n\n <% if (Object.keys(owners).length > 0) { %>\n <section class=\"mt-8\">\n <h2 class=\"text-xl font-semibold mb-4\">Owners</h2>\n <ul class=\"space-y-1\">\n <% for (var oid of Object.keys(owners)) { %>\n <li><a href=\"<%= basePath %>/owners/<%= oid %>.html\" class=\"text-blue-600 hover:underline\"><%= owners[oid].display_name %></a></li>\n <% } %>\n </ul>\n </section>\n <% } %>\n</main>\n${FOOTER}\n</body>\n</html>`;\n\n// ---------------------------------------------------------------------------\n// Model page template\n// ---------------------------------------------------------------------------\n\nexport const modelTemplate = `${HEAD}\n<body class=\"bg-white text-gray-900 min-h-screen\">\n${NAV}\n${TIER_BADGE}\n<main class=\"max-w-5xl mx-auto p-6\">\n <div class=\"flex items-center gap-3 mb-2\">\n <h1 class=\"text-3xl font-bold\"><%= model.name %></h1>\n <% if (tier) { %><%- tierBadge(tier.tier) %><% } %>\n </div>\n <% if (model.description) { %>\n <p class=\"text-gray-600 mb-4\"><%= model.description %></p>\n <% } %>\n\n <div class=\"flex gap-3 mb-6 text-sm\">\n <a href=\"<%= basePath %>/models/<%= model.name %>/schema.html\" class=\"text-blue-600 hover:underline\">Schema Browser</a>\n <a href=\"<%= basePath %>/models/<%= model.name %>/rules.html\" class=\"text-blue-600 hover:underline\">Rules &amp; Queries</a>\n </div>\n\n <% if (gov) { %>\n <section class=\"mb-6\">\n <h2 class=\"text-xl font-semibold mb-2\">Governance</h2>\n <table class=\"table-auto border-collapse text-sm\">\n <tbody>\n <tr><td class=\"pr-4 font-medium\">Owner</td><td><a href=\"<%= basePath %>/owners/<%= gov.owner %>.html\" class=\"text-blue-600 underline\"><%= gov.owner %></a></td></tr>\n <% if (gov.trust) { %><tr><td class=\"pr-4 font-medium\">Trust</td><td><%= gov.trust %></td></tr><% } %>\n <% if (gov.security) { %><tr><td class=\"pr-4 font-medium\">Security</td><td><%= gov.security %></td></tr><% } %>\n <% if (gov.tags && gov.tags.length > 0) { %><tr><td class=\"pr-4 font-medium\">Tags</td><td><%= gov.tags.join(', ') %></td></tr><% } %>\n </tbody>\n </table>\n </section>\n <% } %>\n\n <% if (model.datasets && model.datasets.length > 0) { %>\n <section class=\"mb-6\">\n <h2 class=\"text-xl font-semibold mb-2\">Datasets</h2>\n <div class=\"space-y-3\">\n <% for (var ds of model.datasets) { %>\n <div class=\"border rounded p-3\">\n <div class=\"font-medium\"><%= ds.name %></div>\n <div class=\"text-xs text-gray-500\">Source: <%= ds.source %></div>\n <% if (ds.description) { %><p class=\"text-sm text-gray-600 mt-1\"><%= ds.description %></p><% } %>\n <% if (ds.fields && ds.fields.length > 0) { %>\n <div class=\"text-xs text-gray-500 mt-1\"><%= ds.fields.length %> field(s)</div>\n <% } %>\n </div>\n <% } %>\n </div>\n </section>\n <% } %>\n\n <% if (model.relationships && model.relationships.length > 0) { %>\n <section class=\"mb-6\">\n <h2 class=\"text-xl font-semibold mb-2\">Relationships</h2>\n <table class=\"table-auto w-full text-sm border-collapse\">\n <thead>\n <tr class=\"border-b\"><th class=\"text-left py-1 pr-3\">Name</th><th class=\"text-left py-1 pr-3\">From</th><th class=\"text-left py-1\">To</th></tr>\n </thead>\n <tbody>\n <% for (var rel of model.relationships) { %>\n <tr class=\"border-b\"><td class=\"py-1 pr-3\"><%= rel.name %></td><td class=\"py-1 pr-3\"><%= rel.from %></td><td class=\"py-1\"><%= rel.to %></td></tr>\n <% } %>\n </tbody>\n </table>\n </section>\n <% } %>\n\n <% if (model.metrics && model.metrics.length > 0) { %>\n <section class=\"mb-6\">\n <h2 class=\"text-xl font-semibold mb-2\">Metrics</h2>\n <div class=\"space-y-2\">\n <% for (var metric of model.metrics) { %>\n <div class=\"border rounded p-3\">\n <div class=\"font-medium\"><%= metric.name %></div>\n <% if (metric.description) { %><p class=\"text-sm text-gray-600\"><%= metric.description %></p><% } %>\n </div>\n <% } %>\n </div>\n </section>\n <% } %>\n\n <% if (tier) { %>\n <section class=\"mb-6\">\n <h2 class=\"text-xl font-semibold mb-2\">Tier Details</h2>\n <% var tierLevels = ['bronze', 'silver', 'gold']; %>\n <% for (var lvl of tierLevels) { %>\n <div class=\"mb-3\">\n <h3 class=\"font-medium capitalize flex items-center gap-2\">\n <%= lvl %>\n <% if (tier[lvl].passed) { %>\n <span class=\"text-green-600 text-xs\">Passed</span>\n <% } else { %>\n <span class=\"text-red-500 text-xs\">Not passed</span>\n <% } %>\n </h3>\n <% if (tier[lvl].checks && tier[lvl].checks.length > 0) { %>\n <ul class=\"text-sm ml-4 list-disc\">\n <% for (var chk of tier[lvl].checks) { %>\n <li class=\"<%= chk.passed ? 'text-green-700' : 'text-red-600' %>\"><%= chk.label %><% if (chk.detail) { %> — <%= chk.detail %><% } %></li>\n <% } %>\n </ul>\n <% } %>\n </div>\n <% } %>\n </section>\n <% } %>\n</main>\n${FOOTER}\n</body>\n</html>`;\n\n// ---------------------------------------------------------------------------\n// Schema browser template\n// ---------------------------------------------------------------------------\n\nexport const schemaTemplate = `${HEAD}\n<body class=\"bg-white text-gray-900 min-h-screen\">\n${NAV}\n${TIER_BADGE}\n<main class=\"max-w-5xl mx-auto p-6\">\n <div class=\"flex items-center gap-3 mb-2\">\n <h1 class=\"text-3xl font-bold\"><%= model.name %> — Schema Browser</h1>\n <% if (tier) { %><%- tierBadge(tier.tier) %><% } %>\n </div>\n <div class=\"mb-4 text-sm\">\n <a href=\"<%= basePath %>/models/<%= model.name %>.html\" class=\"text-blue-600 hover:underline\">&larr; Back to model</a>\n </div>\n\n <% if (model.datasets && model.datasets.length > 0) { %>\n <% for (var ds of model.datasets) { %>\n <section class=\"mb-8 border rounded-lg p-4\">\n <h2 class=\"text-xl font-semibold mb-1\"><%= ds.name %></h2>\n <div class=\"text-xs text-gray-500 mb-2\">Source: <%= ds.source %></div>\n <% if (ds.description) { %><p class=\"text-sm text-gray-600 mb-3\"><%= ds.description %></p><% } %>\n\n <% var dsGov = gov && gov.datasets && gov.datasets[ds.name]; %>\n <% if (dsGov) { %>\n <div class=\"text-xs text-gray-500 mb-3 flex gap-4\">\n <% if (dsGov.grain) { %><span>Grain: <strong><%= dsGov.grain %></strong></span><% } %>\n <% if (dsGov.table_type) { %><span>Type: <strong><%= dsGov.table_type %></strong></span><% } %>\n <% if (dsGov.refresh) { %><span>Refresh: <strong><%= dsGov.refresh %></strong></span><% } %>\n <% if (dsGov.security) { %><span>Security: <strong><%= dsGov.security %></strong></span><% } %>\n </div>\n <% } %>\n\n <% if (ds.fields && ds.fields.length > 0) { %>\n <table class=\"table-auto w-full text-sm border-collapse\">\n <thead>\n <tr class=\"border-b bg-gray-50\">\n <th class=\"text-left py-2 px-2\">Field</th>\n <th class=\"text-left py-2 px-2\">Description</th>\n <th class=\"text-left py-2 px-2\">Semantic Role</th>\n <th class=\"text-left py-2 px-2\">Aggregation</th>\n </tr>\n </thead>\n <tbody>\n <% for (var field of ds.fields) { %>\n <% var fieldKey = ds.name + '.' + field.name; %>\n <% var fGov = gov && gov.fields && gov.fields[fieldKey]; %>\n <tr class=\"border-b\">\n <td class=\"py-1 px-2 font-mono text-xs\"><%= field.name %></td>\n <td class=\"py-1 px-2\"><%= field.description || '' %></td>\n <td class=\"py-1 px-2\"><%= fGov && fGov.semantic_role ? fGov.semantic_role : '' %></td>\n <td class=\"py-1 px-2\"><%= fGov && fGov.default_aggregation ? fGov.default_aggregation : '' %></td>\n </tr>\n <% } %>\n </tbody>\n </table>\n <% } else { %>\n <p class=\"text-gray-400 text-sm\">No fields defined.</p>\n <% } %>\n </section>\n <% } %>\n <% } else { %>\n <p class=\"text-gray-500\">No datasets found.</p>\n <% } %>\n</main>\n${FOOTER}\n</body>\n</html>`;\n\n// ---------------------------------------------------------------------------\n// Rules page template (golden queries, business rules, guardrails)\n// ---------------------------------------------------------------------------\n\nexport const rulesTemplate = `${HEAD}\n<body class=\"bg-white text-gray-900 min-h-screen\">\n${NAV}\n<main class=\"max-w-5xl mx-auto p-6\">\n <h1 class=\"text-3xl font-bold mb-2\"><%= modelName %> — Rules &amp; Queries</h1>\n <div class=\"mb-4 text-sm\">\n <a href=\"<%= basePath %>/models/<%= modelName %>.html\" class=\"text-blue-600 hover:underline\">&larr; Back to model</a>\n </div>\n\n <% if (rules && rules.golden_queries && rules.golden_queries.length > 0) { %>\n <section class=\"mb-8\">\n <h2 class=\"text-xl font-semibold mb-3\">Golden Queries</h2>\n <div class=\"space-y-4\">\n <% for (var gq of rules.golden_queries) { %>\n <div class=\"border rounded-lg p-4\">\n <div class=\"font-medium mb-2\"><%= gq.question %></div>\n <pre class=\"bg-gray-100 rounded p-3 text-sm overflow-x-auto\"><code><%= gq.sql %></code></pre>\n <% if (gq.dialect) { %><div class=\"text-xs text-gray-500 mt-1\">Dialect: <%= gq.dialect %></div><% } %>\n <% if (gq.tags && gq.tags.length > 0) { %><div class=\"text-xs text-gray-500 mt-1\">Tags: <%= gq.tags.join(', ') %></div><% } %>\n </div>\n <% } %>\n </div>\n </section>\n <% } %>\n\n <% if (rules && rules.business_rules && rules.business_rules.length > 0) { %>\n <section class=\"mb-8\">\n <h2 class=\"text-xl font-semibold mb-3\">Business Rules</h2>\n <div class=\"space-y-3\">\n <% for (var br of rules.business_rules) { %>\n <div class=\"border rounded p-4\">\n <div class=\"font-medium\"><%= br.name %></div>\n <p class=\"text-sm text-gray-600 mt-1\"><%= br.definition %></p>\n <% if (br.enforcement && br.enforcement.length > 0) { %>\n <div class=\"text-xs text-gray-500 mt-1\">Enforcement: <%= br.enforcement.join(', ') %></div>\n <% } %>\n <% if (br.avoid && br.avoid.length > 0) { %>\n <div class=\"text-xs text-red-500 mt-1\">Avoid: <%= br.avoid.join(', ') %></div>\n <% } %>\n </div>\n <% } %>\n </div>\n </section>\n <% } %>\n\n <% if (rules && rules.guardrail_filters && rules.guardrail_filters.length > 0) { %>\n <section class=\"mb-8\">\n <h2 class=\"text-xl font-semibold mb-3\">Guardrail Filters</h2>\n <div class=\"space-y-3\">\n <% for (var gf of rules.guardrail_filters) { %>\n <div class=\"border rounded p-4\">\n <div class=\"font-medium\"><%= gf.name %></div>\n <pre class=\"bg-gray-100 rounded p-2 text-sm mt-1\"><code><%= gf.filter %></code></pre>\n <p class=\"text-sm text-gray-600 mt-1\"><%= gf.reason %></p>\n </div>\n <% } %>\n </div>\n </section>\n <% } %>\n\n <% if (rules && rules.hierarchies && rules.hierarchies.length > 0) { %>\n <section class=\"mb-8\">\n <h2 class=\"text-xl font-semibold mb-3\">Hierarchies</h2>\n <div class=\"space-y-3\">\n <% for (var h of rules.hierarchies) { %>\n <div class=\"border rounded p-4\">\n <div class=\"font-medium\"><%= h.name %></div>\n <div class=\"text-sm text-gray-600 mt-1\">Dataset: <%= h.dataset %></div>\n <div class=\"text-sm text-gray-600\">Levels: <%= h.levels.join(' &rarr; ') %></div>\n </div>\n <% } %>\n </div>\n </section>\n <% } %>\n\n <% if (!rules || ((!rules.golden_queries || rules.golden_queries.length === 0) && (!rules.business_rules || rules.business_rules.length === 0) && (!rules.guardrail_filters || rules.guardrail_filters.length === 0) && (!rules.hierarchies || rules.hierarchies.length === 0))) { %>\n <p class=\"text-gray-500\">No rules or queries defined for this model.</p>\n <% } %>\n</main>\n${FOOTER}\n</body>\n</html>`;\n\n// ---------------------------------------------------------------------------\n// Glossary template\n// ---------------------------------------------------------------------------\n\nexport const glossaryTemplate = `${HEAD}\n<body class=\"bg-white text-gray-900 min-h-screen\">\n${NAV}\n<main class=\"max-w-5xl mx-auto p-6\">\n <h1 class=\"text-3xl font-bold mb-6\">Glossary</h1>\n\n <% var termIds = Object.keys(terms).sort(); %>\n <% if (termIds.length === 0) { %>\n <p class=\"text-gray-500\">No terms defined.</p>\n <% } else { %>\n <div class=\"space-y-4\">\n <% for (var tid of termIds) { %>\n <% var term = terms[tid]; %>\n <div class=\"border rounded-lg p-4\" id=\"term-<%= tid %>\">\n <h2 class=\"text-lg font-semibold\"><%= tid %></h2>\n <p class=\"text-gray-700 mt-1\"><%= term.definition %></p>\n <% if (term.synonyms && term.synonyms.length > 0) { %>\n <div class=\"text-sm text-gray-500 mt-1\">Synonyms: <%= term.synonyms.join(', ') %></div>\n <% } %>\n <% if (term.maps_to && term.maps_to.length > 0) { %>\n <div class=\"text-sm text-gray-500 mt-1\">Maps to: <%= term.maps_to.join(', ') %></div>\n <% } %>\n <% if (term.owner) { %>\n <div class=\"text-sm text-gray-500 mt-1\">Owner: <a href=\"<%= basePath %>/owners/<%= term.owner %>.html\" class=\"text-blue-600 underline\"><%= term.owner %></a></div>\n <% } %>\n </div>\n <% } %>\n </div>\n <% } %>\n</main>\n${FOOTER}\n</body>\n</html>`;\n\n// ---------------------------------------------------------------------------\n// Owner page template\n// ---------------------------------------------------------------------------\n\nexport const ownerTemplate = `${HEAD}\n<body class=\"bg-white text-gray-900 min-h-screen\">\n${NAV}\n${TIER_BADGE}\n<main class=\"max-w-5xl mx-auto p-6\">\n <h1 class=\"text-3xl font-bold mb-2\"><%= owner.display_name %></h1>\n <div class=\"text-sm text-gray-500 mb-4\">\n <% if (owner.email) { %><span>Email: <%= owner.email %></span><% } %>\n <% if (owner.team) { %><span class=\"ml-4\">Team: <%= owner.team %></span><% } %>\n </div>\n <% if (owner.description) { %>\n <p class=\"text-gray-600 mb-4\"><%= owner.description %></p>\n <% } %>\n\n <% if (governedModels.length > 0) { %>\n <section>\n <h2 class=\"text-xl font-semibold mb-3\">Governed Models</h2>\n <div class=\"space-y-2\">\n <% for (var gm of governedModels) { %>\n <div class=\"flex items-center gap-3\">\n <a href=\"<%= basePath %>/models/<%= gm.name %>.html\" class=\"text-blue-600 hover:underline\"><%= gm.name %></a>\n <% if (gm.tier) { %><%- tierBadge(gm.tier) %><% } %>\n </div>\n <% } %>\n </div>\n </section>\n <% } else { %>\n <p class=\"text-gray-500\">No models governed by this owner.</p>\n <% } %>\n</main>\n${FOOTER}\n</body>\n</html>`;\n\n// ---------------------------------------------------------------------------\n// Search page template (client-side MiniSearch)\n// ---------------------------------------------------------------------------\n\nexport const searchTemplate = `${HEAD}\n<body class=\"bg-white text-gray-900 min-h-screen\">\n${NAV}\n<main class=\"max-w-5xl mx-auto p-6\">\n <h1 class=\"text-3xl font-bold mb-6\">Search</h1>\n\n <input type=\"text\" id=\"search-input\"\n placeholder=\"Search models, datasets, terms...\"\n class=\"w-full border rounded-lg px-4 py-2 mb-4 focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n\n <div id=\"search-results\" class=\"space-y-2\"></div>\n</main>\n${FOOTER}\n\n<script src=\"https://cdn.jsdelivr.net/npm/minisearch@7.1.0/dist/umd/index.min.js\"></script>\n<script>\n(function() {\n var indexData = <%- searchIndexJson %>;\n var miniSearch = MiniSearch.loadJSON(JSON.stringify(indexData.index), indexData.options);\n\n var input = document.getElementById('search-input');\n var resultsContainer = document.getElementById('search-results');\n var docs = indexData.documents;\n\n function clearResults() {\n while (resultsContainer.firstChild) {\n resultsContainer.removeChild(resultsContainer.firstChild);\n }\n }\n\n function createResultElement(doc) {\n var wrapper = document.createElement('div');\n wrapper.className = 'border rounded p-3';\n\n var link = document.createElement('a');\n link.href = doc.url;\n link.className = 'text-blue-600 font-medium hover:underline';\n link.textContent = doc.title;\n wrapper.appendChild(link);\n\n var badge = document.createElement('span');\n badge.className = 'ml-2 text-xs text-gray-400 uppercase';\n badge.textContent = doc.type;\n wrapper.appendChild(badge);\n\n if (doc.description) {\n var desc = document.createElement('p');\n desc.className = 'text-sm text-gray-600 mt-1';\n desc.textContent = doc.description;\n wrapper.appendChild(desc);\n }\n\n return wrapper;\n }\n\n input.addEventListener('input', function() {\n var query = input.value.trim();\n clearResults();\n if (!query) return;\n var hits = miniSearch.search(query, { prefix: true, fuzzy: 0.2 });\n if (hits.length === 0) {\n var noResults = document.createElement('p');\n noResults.className = 'text-gray-500';\n noResults.textContent = 'No results found.';\n resultsContainer.appendChild(noResults);\n return;\n }\n hits.slice(0, 20).forEach(function(hit) {\n var doc = docs[hit.id];\n if (doc) {\n resultsContainer.appendChild(createResultElement(doc));\n }\n });\n });\n})();\n</script>\n</body>\n</html>`;\n","/**\n * Builds a MiniSearch index from a Manifest for client-side search.\n *\n * The index is serialized to JSON so it can be embedded in the search page\n * and loaded by MiniSearch on the client side.\n */\n\nimport MiniSearch from 'minisearch';\nimport type { Manifest } from '@runcontext/core';\n\nexport interface SearchDocument {\n id: string;\n type: string;\n title: string;\n description: string;\n url: string;\n}\n\nexport interface SearchIndex {\n /** Serialized MiniSearch index (JSON-parsed object). */\n index: unknown;\n /** MiniSearch constructor options needed to reload the index. */\n options: {\n fields: string[];\n storeFields: string[];\n idField: string;\n };\n /** Map of document ID to document metadata for rendering results. */\n documents: Record<string, SearchDocument>;\n}\n\nconst MINISEARCH_OPTIONS = {\n fields: ['title', 'description', 'type'],\n storeFields: ['title', 'description', 'type', 'url'],\n idField: 'id',\n};\n\n/**\n * Build a search index from a manifest.\n *\n * @param manifest - The compiled ContextKit manifest\n * @param basePath - The base URL path for links (e.g. '' or '/docs')\n * @returns A SearchIndex object ready for JSON serialization\n */\nexport function buildSearchIndex(manifest: Manifest, basePath: string): SearchIndex {\n const docs: SearchDocument[] = [];\n let idCounter = 0;\n\n // Index models\n for (const [name, model] of Object.entries(manifest.models)) {\n docs.push({\n id: String(idCounter++),\n type: 'model',\n title: name,\n description: model.description ?? '',\n url: `${basePath}/models/${name}.html`,\n });\n\n // Index datasets within each model\n if (model.datasets) {\n for (const ds of model.datasets) {\n docs.push({\n id: String(idCounter++),\n type: 'dataset',\n title: `${name} / ${ds.name}`,\n description: ds.description ?? '',\n url: `${basePath}/models/${name}/schema.html`,\n });\n }\n }\n }\n\n // Index glossary terms\n for (const [termId, term] of Object.entries(manifest.terms)) {\n docs.push({\n id: String(idCounter++),\n type: 'term',\n title: termId,\n description: term.definition,\n url: `${basePath}/glossary.html#term-${termId}`,\n });\n }\n\n // Index owners\n for (const [oid, owner] of Object.entries(manifest.owners)) {\n docs.push({\n id: String(idCounter++),\n type: 'owner',\n title: owner.display_name,\n description: owner.description ?? '',\n url: `${basePath}/owners/${oid}.html`,\n });\n }\n\n // Build MiniSearch index\n const miniSearch = new MiniSearch(MINISEARCH_OPTIONS);\n miniSearch.addAll(docs);\n\n // Create document lookup map by id\n const documentsMap: Record<string, SearchDocument> = {};\n for (const doc of docs) {\n documentsMap[doc.id] = doc;\n }\n\n return {\n index: JSON.parse(JSON.stringify(miniSearch)),\n options: MINISEARCH_OPTIONS,\n documents: documentsMap,\n };\n}\n"]}
1
+ {"version":3,"sources":["/Users/erickittelson/Desktop/ContextKit/packages/site/dist/index.cjs","../src/generator.ts","../src/templates/shared.ts","../src/templates/index.ts","../src/templates/model.ts","../src/templates/schema.ts","../src/templates/rules.ts","../src/templates/glossary.ts","../src/templates/owner.ts","../src/templates/search.ts","../src/search/build-index.ts"],"names":[],"mappings":"AAAA;ACUA,oEAAgB;AAChB,+DAAoB;AACpB,uEAAsB;ADRtB;AACA;AEMO,IAAM,KAAA,EAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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","file":"/Users/erickittelson/Desktop/ContextKit/packages/site/dist/index.cjs","sourcesContent":[null,"/**\n * Site generator for ContextKit.\n *\n * `generateSite` accepts a Manifest and returns a Map of relative file path\n * to HTML content. The caller is responsible for writing files to disk.\n *\n * `buildSite` is a convenience function used by the CLI that compiles context,\n * generates the site, and writes files to the output directory.\n */\n\nimport ejs from 'ejs';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { Manifest, SiteConfig, ContextKitConfig } from '@runcontext/core';\nimport {\n indexTemplate,\n modelTemplate,\n schemaTemplate,\n rulesTemplate,\n glossaryTemplate,\n ownerTemplate,\n searchTemplate,\n} from './templates.js';\nimport { buildSearchIndex } from './search/build-index.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface GenerateSiteOptions {\n manifest: Manifest;\n config?: SiteConfig;\n}\n\n// ---------------------------------------------------------------------------\n// generateSite — pure function returning Map<path, html>\n// ---------------------------------------------------------------------------\n\n/**\n * Generate all HTML pages for the documentation site.\n *\n * @param manifest - The compiled ContextKit manifest\n * @param config - Optional site configuration (title, base_path)\n * @returns Map of relative file paths to HTML content\n */\nexport function generateSite(\n manifest: Manifest,\n config?: SiteConfig,\n): Map<string, string> {\n const files = new Map<string, string>();\n const siteTitle = config?.title ?? 'ContextKit';\n const basePath = (config?.base_path ?? '').replace(/\\/+$/, '') || '.';\n\n const commonData = {\n siteTitle,\n basePath,\n };\n\n // --- Index page ---\n files.set(\n 'index.html',\n ejs.render(indexTemplate, {\n ...commonData,\n pageTitle: 'Home',\n models: manifest.models,\n governance: manifest.governance,\n tiers: manifest.tiers,\n owners: manifest.owners,\n terms: manifest.terms,\n }),\n );\n\n // --- Model pages ---\n for (const [name, model] of Object.entries(manifest.models)) {\n const gov = manifest.governance[name] ?? null;\n const tier = manifest.tiers[name] ?? null;\n const rules = manifest.rules[name] ?? null;\n const lineage = manifest.lineage[name] ?? null;\n\n // Main model page\n files.set(\n `models/${name}.html`,\n ejs.render(modelTemplate, {\n ...commonData,\n pageTitle: name,\n model,\n gov,\n tier,\n rules,\n lineage,\n }),\n );\n\n // Schema browser page\n files.set(\n `models/${name}/schema.html`,\n ejs.render(schemaTemplate, {\n ...commonData,\n pageTitle: `${name} — Schema`,\n model,\n gov,\n tier,\n }),\n );\n\n // Rules page\n files.set(\n `models/${name}/rules.html`,\n ejs.render(rulesTemplate, {\n ...commonData,\n pageTitle: `${name} — Rules`,\n modelName: name,\n rules,\n }),\n );\n }\n\n // --- Glossary page ---\n files.set(\n 'glossary.html',\n ejs.render(glossaryTemplate, {\n ...commonData,\n pageTitle: 'Glossary',\n terms: manifest.terms,\n }),\n );\n\n // --- Owner pages ---\n for (const [oid, owner] of Object.entries(manifest.owners)) {\n // Find models governed by this owner\n const governedModels: Array<{ name: string; tier: string | null }> = [];\n for (const [modelName, gov] of Object.entries(manifest.governance)) {\n if (gov.owner === oid) {\n const tierScore = manifest.tiers[modelName];\n governedModels.push({\n name: modelName,\n tier: tierScore?.tier ?? null,\n });\n }\n }\n\n files.set(\n `owners/${oid}.html`,\n ejs.render(ownerTemplate, {\n ...commonData,\n pageTitle: owner.display_name,\n owner,\n governedModels,\n }),\n );\n }\n\n // --- Search page ---\n const searchIndex = buildSearchIndex(manifest, basePath);\n files.set(\n 'search.html',\n ejs.render(searchTemplate, {\n ...commonData,\n pageTitle: 'Search',\n searchIndexJson: JSON.stringify(searchIndex),\n }),\n );\n\n // --- Search index JSON (for programmatic access) ---\n files.set('search-index.json', JSON.stringify(searchIndex, null, 2));\n\n return files;\n}\n\n// ---------------------------------------------------------------------------\n// buildSite — convenience function used by CLI\n// ---------------------------------------------------------------------------\n\n/**\n * Build the documentation site and write files to disk.\n *\n * Called by the CLI `site` command. Accepts a manifest, config, and output\n * directory, then generates and writes all site files.\n *\n * @param manifest - The compiled ContextKit manifest\n * @param config - The full ContextKit configuration\n * @param outputDir - Directory to write site files to\n */\nexport async function buildSite(\n manifest: Manifest,\n config: ContextKitConfig,\n outputDir: string,\n): Promise<void> {\n const siteConfig = config.site;\n const files = generateSite(manifest, siteConfig);\n\n for (const [filePath, content] of files) {\n const fullPath = path.join(outputDir, filePath);\n const dir = path.dirname(fullPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n fs.writeFileSync(fullPath, content, 'utf-8');\n }\n}\n","/**\n * Shared layout constants for the ContextKit dark editorial site theme.\n *\n * Exports string fragments used by all page templates:\n * HEAD, NAV, FOOTER, TIER_BADGE, SCRIPTS\n */\n\n// ---------------------------------------------------------------------------\n// HEAD — doctype, meta, fonts, full CSS design system\n// ---------------------------------------------------------------------------\n\nexport const HEAD = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title><%= pageTitle %> — <%= siteTitle %></title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600&family=JetBrains+Mono:wght@300;400;500&display=swap\" rel=\"stylesheet\">\n <style>\n :root {\n --gold: #D4A855;\n --gold-light: #E8C878;\n --gold-dark: #A07D3A;\n --gold-glow: rgba(212, 168, 85, 0.15);\n --bg: #0A0A0C;\n --bg-card: #111114;\n --bg-elevated: #18181C;\n --text: #E8E6E1;\n --text-muted: #8A8880;\n --text-dim: #5A5850;\n --border: #2A2A2E;\n --green: #4ADE80;\n --green-dim: rgba(74, 222, 128, 0.12);\n --blue: #60A5FA;\n --red: #F87171;\n --serif: 'Cormorant Garamond', Georgia, serif;\n --sans: 'DM Sans', system-ui, sans-serif;\n --mono: 'JetBrains Mono', monospace;\n }\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html { scroll-behavior: smooth; }\n body {\n font-family: var(--sans);\n background: var(--bg);\n color: var(--text);\n line-height: 1.6;\n overflow-x: hidden;\n }\n a { color: var(--gold); text-decoration: none; }\n a:hover { text-decoration: underline; }\n\n /* === GRAIN OVERLAY === */\n body::before {\n content: '';\n position: fixed;\n inset: 0;\n background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.03'/%3E%3C/svg%3E\");\n pointer-events: none;\n z-index: 9999;\n }\n\n /* === LAYOUT === */\n .page { max-width: 1200px; margin: 0 auto; padding: 3rem 2rem 6rem; }\n .section { margin-bottom: 3rem; }\n .section-label {\n font-family: var(--mono);\n font-size: 0.65rem;\n letter-spacing: 0.3em;\n text-transform: uppercase;\n color: var(--gold-dark);\n margin-bottom: 0.75rem;\n }\n .section h2, h2.section-title {\n font-family: var(--serif);\n font-weight: 400;\n font-size: clamp(1.6rem, 3vw, 2.4rem);\n letter-spacing: -0.01em;\n margin-bottom: 1rem;\n color: var(--text);\n }\n .section-intro {\n color: var(--text-muted);\n font-size: 1rem;\n max-width: 600px;\n margin-bottom: 2rem;\n font-weight: 300;\n }\n .divider {\n width: 100%;\n height: 1px;\n background: var(--border);\n margin: 0 auto;\n }\n\n /* === HERO (index page) === */\n .hero {\n text-align: center;\n padding: 5rem 2rem 4rem;\n position: relative;\n }\n .hero::before {\n content: '';\n position: absolute;\n top: -30%;\n left: 50%;\n transform: translateX(-50%);\n width: 700px;\n height: 700px;\n background: radial-gradient(circle, var(--gold-glow) 0%, transparent 70%);\n pointer-events: none;\n }\n .hero-eyebrow {\n font-family: var(--mono);\n font-size: 0.7rem;\n letter-spacing: 0.3em;\n text-transform: uppercase;\n color: var(--gold);\n margin-bottom: 1.5rem;\n position: relative;\n }\n .hero h1 {\n font-family: var(--serif);\n font-weight: 300;\n font-size: clamp(2.5rem, 6vw, 5rem);\n line-height: 1.1;\n letter-spacing: -0.02em;\n color: var(--text);\n position: relative;\n }\n .hero h1 em { font-style: italic; color: var(--gold); }\n .hero-sub {\n font-size: 1.05rem;\n color: var(--text-muted);\n max-width: 520px;\n margin: 1.5rem auto 0;\n font-weight: 300;\n position: relative;\n }\n\n /* === STATS === */\n .stats {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));\n gap: 1px;\n background: var(--border);\n border: 1px solid var(--border);\n border-radius: 12px;\n overflow: hidden;\n margin-bottom: 3rem;\n }\n .stat {\n background: var(--bg-card);\n padding: 1.5rem;\n text-align: center;\n }\n .stat-value {\n font-family: var(--serif);\n font-size: 2.4rem;\n font-weight: 300;\n color: var(--gold);\n line-height: 1;\n }\n .stat-label {\n font-size: 0.7rem;\n color: var(--text-muted);\n margin-top: 0.4rem;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n /* === CARDS === */\n .card {\n border: 1px solid var(--border);\n border-radius: 12px;\n padding: 1.5rem;\n background: var(--bg-card);\n transition: border-color 0.3s, box-shadow 0.3s;\n }\n .card:hover {\n border-color: var(--gold-dark);\n box-shadow: 0 0 40px rgba(212,168,85,0.06);\n }\n .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1.5rem; }\n .card-grid-sm { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; }\n\n /* === TAGS === */\n .tag {\n display: inline-block;\n font-family: var(--mono);\n font-size: 0.6rem;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n padding: 0.2rem 0.55rem;\n border-radius: 4px;\n background: var(--bg);\n border: 1px solid var(--border);\n color: var(--text-muted);\n }\n .tag-gold { color: var(--gold); border-color: rgba(212,168,85,0.3); background: rgba(212,168,85,0.08); }\n .tag-silver { color: #C0C0C0; border-color: rgba(192,192,192,0.3); background: rgba(192,192,192,0.08); }\n .tag-bronze { color: #CD7F32; border-color: rgba(205,127,50,0.3); background: rgba(205,127,50,0.08); }\n .tag-none { color: var(--text-dim); border-color: var(--border); }\n .tag-green { color: var(--green); border-color: rgba(74,222,128,0.3); background: var(--green-dim); }\n .tag-red { color: var(--red); border-color: rgba(248,113,113,0.3); background: rgba(248,113,113,0.08); }\n .tag-blue { color: var(--blue); border-color: rgba(96,165,250,0.3); background: rgba(96,165,250,0.08); }\n .tag-nav {\n padding: 0.35rem 0.75rem;\n font-size: 0.65rem;\n border-radius: 6px;\n text-decoration: none;\n transition: border-color 0.2s;\n }\n .tag-nav:hover { border-color: var(--gold); text-decoration: none; }\n\n /* === TABLES (dark) === */\n .table-dark { width: 100%; border-collapse: collapse; }\n .table-dark th {\n font-family: var(--mono);\n font-size: 0.65rem;\n letter-spacing: 0.15em;\n text-transform: uppercase;\n color: var(--text-dim);\n text-align: left;\n padding: 0.75rem 1rem;\n border-bottom: 1px solid var(--border);\n }\n .table-dark td {\n padding: 0.75rem 1rem;\n border-bottom: 1px solid rgba(42,42,46,0.5);\n font-size: 0.85rem;\n vertical-align: top;\n }\n .table-dark tr:hover td { background: rgba(212,168,85,0.02); }\n .mono { font-family: var(--mono); font-size: 0.82rem; }\n\n /* === SEMANTIC ROLE PILLS === */\n .role-identifier { color: var(--gold); border-color: rgba(212,168,85,0.3); background: rgba(212,168,85,0.08); }\n .role-metric { color: #06B6D4; border-color: rgba(6,182,212,0.3); background: rgba(6,182,212,0.08); }\n .role-dimension { color: #818CF8; border-color: rgba(129,140,248,0.3); background: rgba(129,140,248,0.08); }\n .role-date { color: var(--green); border-color: rgba(74,222,128,0.3); background: var(--green-dim); }\n .role-attribute { color: var(--text-muted); border-color: var(--border); }\n\n /* === DS TYPE PILLS === */\n .ds-type {\n font-family: var(--mono);\n font-size: 0.65rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n padding: 0.15rem 0.45rem;\n border-radius: 4px;\n display: inline-block;\n }\n .ds-type-fact { color: #818CF8; background: rgba(129,140,248,0.1); }\n .ds-type-dimension { color: #34D399; background: rgba(52,211,153,0.1); }\n .ds-type-event { color: #FB923C; background: rgba(251,146,60,0.1); }\n .ds-type-view { color: #A78BFA; background: rgba(167,139,250,0.1); }\n\n /* === METRIC CARDS === */\n .metric-name {\n font-family: var(--mono);\n font-size: 0.9rem;\n color: var(--gold-light);\n margin-bottom: 0.5rem;\n }\n .metric-desc {\n color: var(--text-muted);\n font-size: 0.85rem;\n line-height: 1.6;\n margin-bottom: 1rem;\n }\n .metric-formula {\n font-family: var(--mono);\n font-size: 0.72rem;\n color: var(--text-dim);\n background: var(--bg);\n border: 1px solid var(--border);\n border-radius: 6px;\n padding: 0.6rem 0.8rem;\n line-height: 1.5;\n overflow-x: auto;\n }\n\n /* === QUERY CARDS === */\n .query-card {\n border: 1px solid var(--border);\n border-radius: 12px;\n overflow: hidden;\n margin-bottom: 1.5rem;\n background: var(--bg-card);\n transition: border-color 0.3s;\n }\n .query-card:hover { border-color: var(--gold-dark); }\n .query-question {\n padding: 1.25rem 1.5rem;\n font-family: var(--serif);\n font-size: 1.1rem;\n font-weight: 400;\n font-style: italic;\n color: var(--text);\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: flex-start;\n gap: 0.6rem;\n }\n .query-question::before {\n content: 'Q';\n font-family: var(--mono);\n font-size: 0.6rem;\n font-style: normal;\n letter-spacing: 0.1em;\n color: var(--gold);\n background: rgba(212,168,85,0.1);\n border: 1px solid rgba(212,168,85,0.2);\n padding: 0.15rem 0.35rem;\n border-radius: 3px;\n flex-shrink: 0;\n margin-top: 0.3rem;\n }\n .query-sql {\n padding: 1rem 1.5rem;\n font-family: var(--mono);\n font-size: 0.78rem;\n color: var(--text-muted);\n line-height: 1.6;\n background: var(--bg);\n overflow-x: auto;\n white-space: pre-wrap;\n }\n .query-meta {\n padding: 0.6rem 1.5rem;\n display: flex;\n gap: 1rem;\n font-size: 0.7rem;\n color: var(--text-dim);\n border-top: 1px solid var(--border);\n }\n\n /* === GUARDRAILS === */\n .guardrail {\n border: 1px solid rgba(248, 113, 113, 0.2);\n border-radius: 12px;\n padding: 1.5rem;\n background: rgba(248, 113, 113, 0.03);\n margin-bottom: 1rem;\n }\n .guardrail-name {\n font-family: var(--mono);\n font-size: 0.82rem;\n color: var(--red);\n margin-bottom: 0.4rem;\n }\n .guardrail-filter {\n font-family: var(--mono);\n font-size: 0.78rem;\n color: var(--text);\n background: var(--bg);\n padding: 0.4rem 0.65rem;\n border-radius: 4px;\n border: 1px solid var(--border);\n display: inline-block;\n margin-bottom: 0.6rem;\n }\n .guardrail-reason {\n font-size: 0.82rem;\n color: var(--text-muted);\n font-weight: 300;\n }\n\n /* === LINEAGE === */\n .lineage-flow {\n display: flex;\n align-items: stretch;\n gap: 0;\n overflow-x: auto;\n padding: 1.5rem 0;\n }\n .lineage-col {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n min-width: 220px;\n }\n .lineage-col-label {\n font-family: var(--mono);\n font-size: 0.6rem;\n letter-spacing: 0.2em;\n text-transform: uppercase;\n color: var(--text-dim);\n margin-bottom: 0.25rem;\n padding-left: 0.5rem;\n }\n .lineage-node {\n border: 1px solid var(--border);\n border-radius: 8px;\n padding: 0.75rem 1rem;\n background: var(--bg-card);\n transition: border-color 0.3s;\n }\n .lineage-node:hover { border-color: var(--gold-dark); }\n .lineage-node-name {\n font-family: var(--mono);\n font-size: 0.78rem;\n color: var(--text);\n margin-bottom: 0.15rem;\n }\n .lineage-node-detail {\n font-size: 0.7rem;\n color: var(--text-dim);\n }\n .lineage-arrow {\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 50px;\n color: var(--gold-dark);\n font-size: 1.3rem;\n }\n\n /* === SCORECARD === */\n .scorecard {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 1px;\n background: var(--border);\n border: 1px solid var(--border);\n border-radius: 12px;\n overflow: hidden;\n }\n @media (max-width: 768px) { .scorecard { grid-template-columns: 1fr; } }\n .scorecard-tier {\n background: var(--bg-card);\n padding: 1.5rem;\n }\n .scorecard-tier-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 1rem;\n }\n .scorecard-tier-name {\n font-family: var(--serif);\n font-size: 1.3rem;\n font-weight: 500;\n text-transform: capitalize;\n }\n .scorecard-tier-name.bronze { color: #CD7F32; }\n .scorecard-tier-name.silver { color: #C0C0C0; }\n .scorecard-tier-name.gold { color: var(--gold); }\n .scorecard-pass {\n font-family: var(--mono);\n font-size: 0.6rem;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n padding: 0.15rem 0.5rem;\n border-radius: 4px;\n }\n .scorecard-pass.passed { color: var(--green); background: var(--green-dim); }\n .scorecard-pass.failed { color: var(--red); background: rgba(248,113,113,0.1); }\n .check-list { list-style: none; }\n .check-item {\n display: flex;\n align-items: flex-start;\n gap: 0.4rem;\n padding: 0.3rem 0;\n font-size: 0.75rem;\n color: var(--text-muted);\n line-height: 1.4;\n }\n .check-icon { flex-shrink: 0; margin-top: 1px; font-size: 0.8rem; }\n .check-icon.pass { color: var(--green); }\n .check-icon.fail { color: var(--red); }\n\n /* === GLOSSARY === */\n .glossary-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n gap: 1.5rem;\n }\n .glossary-card {\n border: 1px solid var(--border);\n border-radius: 12px;\n padding: 1.5rem;\n background: var(--bg-card);\n position: relative;\n overflow: hidden;\n transition: border-color 0.3s;\n }\n .glossary-card:hover { border-color: var(--gold-dark); }\n .glossary-card::before {\n content: '';\n position: absolute;\n top: 0; left: 0; right: 0;\n height: 2px;\n background: linear-gradient(90deg, var(--gold-dark), transparent);\n }\n .glossary-term {\n font-family: var(--serif);\n font-size: 1.3rem;\n font-weight: 500;\n margin-bottom: 0.5rem;\n color: var(--text);\n }\n .glossary-def {\n font-size: 0.85rem;\n color: var(--text-muted);\n line-height: 1.7;\n font-weight: 300;\n }\n\n /* === EXPANDABLE === */\n .expandable-header {\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: space-between;\n user-select: none;\n }\n .expand-icon {\n font-family: var(--mono);\n font-size: 0.9rem;\n color: var(--text-dim);\n transition: transform 0.2s;\n width: 20px;\n text-align: center;\n }\n .expandable-content {\n max-height: 0;\n overflow: hidden;\n transition: max-height 0.3s ease;\n }\n\n /* === SEARCH === */\n .search-input {\n width: 100%;\n background: var(--bg-card);\n border: 1px solid var(--border);\n border-radius: 8px;\n padding: 0.8rem 1.2rem;\n font-family: var(--sans);\n font-size: 1rem;\n color: var(--text);\n outline: none;\n transition: border-color 0.2s, box-shadow 0.2s;\n }\n .search-input::placeholder { color: var(--text-dim); }\n .search-input:focus {\n border-color: var(--gold-dark);\n box-shadow: 0 0 0 3px var(--gold-glow);\n }\n\n /* === BACK LINK === */\n .back-link {\n font-size: 0.85rem;\n color: var(--text-muted);\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n margin-bottom: 1.5rem;\n text-decoration: none;\n }\n .back-link:hover { color: var(--gold); text-decoration: none; }\n\n /* === GOVERNANCE GRID === */\n .gov-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 1px;\n background: var(--border);\n border: 1px solid var(--border);\n border-radius: 8px;\n overflow: hidden;\n }\n .gov-item {\n background: var(--bg-card);\n padding: 1rem 1.25rem;\n }\n .gov-label {\n font-family: var(--mono);\n font-size: 0.6rem;\n letter-spacing: 0.15em;\n text-transform: uppercase;\n color: var(--text-dim);\n margin-bottom: 0.3rem;\n }\n .gov-value {\n font-size: 0.9rem;\n color: var(--text);\n }\n\n /* === ANIMATIONS === */\n @keyframes fadeUp {\n from { opacity: 0; transform: translateY(20px); }\n to { opacity: 1; transform: translateY(0); }\n }\n @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n }\n .reveal {\n opacity: 0;\n transform: translateY(25px);\n transition: opacity 0.6s ease, transform 0.6s ease;\n }\n .reveal.visible {\n opacity: 1;\n transform: translateY(0);\n }\n\n /* === SQL HIGHLIGHT === */\n .sql-kw { color: #818CF8; }\n .sql-fn { color: #34D399; }\n .sql-str { color: #FCD34D; }\n .sql-num { color: #FB923C; }\n .sql-cm { color: var(--text-dim); font-style: italic; }\n </style>\n</head>`;\n\n// ---------------------------------------------------------------------------\n// NAV — sticky dark navigation\n// ---------------------------------------------------------------------------\n\nexport const NAV = `<nav style=\"position:sticky;top:0;z-index:100;background:var(--bg-card);border-bottom:1px solid var(--border);padding:0.75rem 2rem;display:flex;align-items:center;gap:2rem;\">\n <a href=\"<%= basePath %>/index.html\" style=\"font-family:var(--serif);font-size:1.25rem;font-weight:600;color:var(--gold);text-decoration:none;\"><%- siteTitle %></a>\n <div style=\"display:flex;gap:1.5rem;font-size:0.82rem;\">\n <a href=\"<%= basePath %>/index.html\" style=\"color:var(--text-muted);text-decoration:none;\">Models</a>\n <a href=\"<%= basePath %>/glossary.html\" style=\"color:var(--text-muted);text-decoration:none;\">Glossary</a>\n <a href=\"<%= basePath %>/search.html\" style=\"color:var(--text-muted);text-decoration:none;\">Search</a>\n </div>\n</nav>`;\n\n// ---------------------------------------------------------------------------\n// FOOTER\n// ---------------------------------------------------------------------------\n\nexport const FOOTER = `<footer style=\"text-align:center;padding:3rem 2rem;color:var(--text-dim);font-size:0.78rem;border-top:1px solid var(--border);margin-top:4rem;\">\n Generated by <a href=\"https://github.com/erickittelson/ContextKit\" style=\"color:var(--gold-dark);\">ContextKit</a>\n</footer>`;\n\n// ---------------------------------------------------------------------------\n// TIER_BADGE — EJS helper function (injected into templates that need it)\n// ---------------------------------------------------------------------------\n\nexport const TIER_BADGE = `<% function tierBadge(tier) {\n var cls = { none: 'tag-none', bronze: 'tag-bronze', silver: 'tag-silver', gold: 'tag-gold' };\n var c = cls[tier] || cls.none;\n return '<span class=\"tag ' + c + '\">' + tier + '</span>';\n} %>`;\n\n// ---------------------------------------------------------------------------\n// SCRIPTS — shared JavaScript for interactivity\n// ---------------------------------------------------------------------------\n\nexport const SCRIPTS = `<script>\n(function() {\n // Scroll-reveal observer\n var reveals = document.querySelectorAll('.reveal');\n if (reveals.length > 0 && 'IntersectionObserver' in window) {\n var observer = new IntersectionObserver(function(entries) {\n entries.forEach(function(e) {\n if (e.isIntersecting) {\n e.target.classList.add('visible');\n observer.unobserve(e.target);\n }\n });\n }, { threshold: 0.1 });\n reveals.forEach(function(el) { observer.observe(el); });\n } else {\n reveals.forEach(function(el) { el.classList.add('visible'); });\n }\n\n // Expandable sections\n window.toggleExpand = function(id) {\n var el = document.getElementById(id);\n var icon = document.getElementById(id + '-icon');\n if (!el) return;\n if (el.style.maxHeight && el.style.maxHeight !== '0px') {\n el.style.maxHeight = '0px';\n if (icon) icon.textContent = '+';\n } else {\n el.style.maxHeight = el.scrollHeight + 'px';\n if (icon) icon.textContent = '\\\\u2212';\n }\n };\n\n // Simple SQL syntax highlighter — operates on trusted template-rendered content\n window.highlightSQL = function() {\n var blocks = document.querySelectorAll('.sql-highlight');\n var kw = /\\\\b(SELECT|FROM|WHERE|JOIN|LEFT|RIGHT|INNER|OUTER|CROSS|ON|AND|OR|NOT|IN|AS|GROUP|BY|ORDER|HAVING|LIMIT|OFFSET|UNION|ALL|DISTINCT|CASE|WHEN|THEN|ELSE|END|IS|NULL|BETWEEN|LIKE|EXISTS|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SET|VALUES|INTO|WITH|DESC|ASC|OVER|PARTITION|WINDOW|FILTER|LATERAL|UNNEST|TRUE|FALSE)\\\\b/gi;\n var fn = /\\\\b(SUM|COUNT|AVG|MIN|MAX|ROUND|COALESCE|CAST|NULLIF|ABS|UPPER|LOWER|LENGTH|TRIM|SUBSTRING|CONCAT|ROW_NUMBER|RANK|DENSE_RANK|LAG|LEAD|FIRST_VALUE|LAST_VALUE|NTILE|PERCENTILE_CONT|STRING_AGG|ARRAY_AGG|LIST|STRUCT_PACK)\\\\b/gi;\n var str = /('(?:[^'\\\\\\\\]|\\\\\\\\.)*')/g;\n var num = /\\\\b(\\\\d+\\\\.?\\\\d*)\\\\b/g;\n\n blocks.forEach(function(block) {\n var text = block.textContent || '';\n text = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n text = text.replace(str, '<span class=\"sql-str\">$1</span>');\n text = text.replace(kw, '<span class=\"sql-kw\">$&</span>');\n text = text.replace(fn, '<span class=\"sql-fn\">$&</span>');\n block.innerHTML = text;\n });\n };\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', window.highlightSQL);\n } else {\n window.highlightSQL();\n }\n})();\n</script>`;\n","import { HEAD, NAV, FOOTER, TIER_BADGE, SCRIPTS } from './shared.js';\n\nexport const indexTemplate = `${HEAD}\n<body>\n${NAV}\n${TIER_BADGE}\n\n<section class=\"hero\">\n <div class=\"hero-eyebrow\"><%- siteTitle %></div>\n <h1>Metadata<br><em>Catalog</em></h1>\n <p class=\"hero-sub\">Explore semantic models, governed datasets, business glossary, and data quality tiers.</p>\n</section>\n\n<div class=\"divider\"></div>\n\n<main class=\"page\">\n <%\n var modelNames = Object.keys(models);\n var totalDatasets = 0;\n var totalFields = 0;\n for (var mn of modelNames) {\n var m = models[mn];\n if (m.datasets) {\n totalDatasets += m.datasets.length;\n for (var ds of m.datasets) {\n if (ds.fields) totalFields += ds.fields.length;\n }\n }\n }\n var totalTerms = Object.keys(terms).length;\n var totalOwners = Object.keys(owners).length;\n %>\n\n <div class=\"stats reveal\">\n <div class=\"stat\">\n <div class=\"stat-value\"><%= modelNames.length %></div>\n <div class=\"stat-label\">Models</div>\n </div>\n <div class=\"stat\">\n <div class=\"stat-value\"><%= totalDatasets %></div>\n <div class=\"stat-label\">Datasets</div>\n </div>\n <div class=\"stat\">\n <div class=\"stat-value\"><%= totalFields %></div>\n <div class=\"stat-label\">Fields</div>\n </div>\n <div class=\"stat\">\n <div class=\"stat-value\"><%= totalTerms %></div>\n <div class=\"stat-label\">Terms</div>\n </div>\n <div class=\"stat\">\n <div class=\"stat-value\"><%= totalOwners %></div>\n <div class=\"stat-label\">Owners</div>\n </div>\n </div>\n\n <div class=\"section reveal\">\n <div class=\"section-label\">Semantic Models</div>\n <h2 class=\"section-title\">Models</h2>\n <% if (modelNames.length === 0) { %>\n <p style=\"color:var(--text-muted);\">No models found.</p>\n <% } else { %>\n <div class=\"card-grid\">\n <% for (var name of modelNames) { %>\n <div class=\"card\">\n <div style=\"display:flex;align-items:center;gap:0.6rem;margin-bottom:0.6rem;\">\n <a href=\"<%= basePath %>/models/<%= name %>.html\" style=\"font-family:var(--mono);font-size:0.95rem;color:var(--gold-light);text-decoration:none;\">\n <%= name %>\n </a>\n <% if (tiers[name]) { %><%- tierBadge(tiers[name].tier) %><% } %>\n </div>\n <% if (models[name].description) { %>\n <p style=\"color:var(--text-muted);font-size:0.85rem;margin-bottom:0.75rem;font-weight:300;\"><%= models[name].description %></p>\n <% } %>\n <% if (governance[name]) { %>\n <div style=\"display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;\">\n <% if (governance[name].owner) { %>\n <a href=\"<%= basePath %>/owners/<%= governance[name].owner %>.html\" class=\"tag tag-nav\" style=\"text-decoration:none;\"><%= governance[name].owner %></a>\n <% } %>\n <% if (governance[name].trust) { %>\n <span class=\"tag tag-green\"><%= governance[name].trust %></span>\n <% } %>\n <% if (governance[name].tags) { for (var t of governance[name].tags) { %>\n <span class=\"tag\"><%= t %></span>\n <% } } %>\n </div>\n <% } %>\n </div>\n <% } %>\n </div>\n <% } %>\n </div>\n\n <% if (Object.keys(owners).length > 0) { %>\n <div class=\"section reveal\">\n <div class=\"section-label\">Data Stewardship</div>\n <h2 class=\"section-title\">Owners</h2>\n <div class=\"card-grid-sm\">\n <% for (var oid of Object.keys(owners)) { %>\n <a href=\"<%= basePath %>/owners/<%= oid %>.html\" class=\"card\" style=\"text-decoration:none;\">\n <div style=\"font-size:1rem;font-weight:500;color:var(--text);margin-bottom:0.25rem;\"><%= owners[oid].display_name %></div>\n <% if (owners[oid].team) { %>\n <div style=\"font-size:0.78rem;color:var(--text-dim);\"><%= owners[oid].team %></div>\n <% } %>\n </a>\n <% } %>\n </div>\n </div>\n <% } %>\n</main>\n\n${FOOTER}\n${SCRIPTS}\n</body>\n</html>`;\n","import { HEAD, NAV, FOOTER, TIER_BADGE, SCRIPTS } from './shared.js';\n\nexport const modelTemplate = `${HEAD}\n<body>\n${NAV}\n${TIER_BADGE}\n\n<main class=\"page\">\n <a href=\"<%= basePath %>/\" class=\"back-link\">&larr; All Models</a>\n\n <div style=\"display:flex;align-items:center;gap:0.75rem;margin-bottom:0.5rem;\">\n <h1 style=\"font-family:var(--serif);font-weight:300;font-size:clamp(2rem,5vw,3.5rem);color:var(--text);\"><%= model.name %></h1>\n <% if (tier) { %><%- tierBadge(tier.tier) %><% } %>\n </div>\n <% if (model.description) { %>\n <p style=\"color:var(--text-muted);font-size:1rem;margin-bottom:1.5rem;max-width:700px;font-weight:300;\"><%= model.description %></p>\n <% } %>\n\n <div style=\"display:flex;gap:0.6rem;margin-bottom:2.5rem;\">\n <a href=\"<%= basePath %>/models/<%= model.name %>/schema.html\" class=\"tag tag-nav tag-blue\">Schema Browser</a>\n <a href=\"<%= basePath %>/models/<%= model.name %>/rules.html\" class=\"tag tag-nav tag-gold\">Rules &amp; Queries</a>\n </div>\n\n <% if (gov) { %>\n <div class=\"section reveal\">\n <div class=\"section-label\">Governance</div>\n <div class=\"gov-grid\">\n <div class=\"gov-item\">\n <div class=\"gov-label\">Owner</div>\n <div class=\"gov-value\"><a href=\"<%= basePath %>/owners/<%= gov.owner %>.html\"><%= gov.owner %></a></div>\n </div>\n <% if (gov.trust) { %>\n <div class=\"gov-item\">\n <div class=\"gov-label\">Trust</div>\n <div class=\"gov-value\"><%= gov.trust %></div>\n </div>\n <% } %>\n <% if (gov.security) { %>\n <div class=\"gov-item\">\n <div class=\"gov-label\">Security</div>\n <div class=\"gov-value\"><%= gov.security %></div>\n </div>\n <% } %>\n <% if (gov.tags && gov.tags.length > 0) { %>\n <div class=\"gov-item\">\n <div class=\"gov-label\">Tags</div>\n <div class=\"gov-value\" style=\"display:flex;gap:0.4rem;flex-wrap:wrap;\">\n <% for (var t of gov.tags) { %><span class=\"tag\"><%= t %></span><% } %>\n </div>\n </div>\n <% } %>\n </div>\n </div>\n <% } %>\n\n <% if (model.datasets && model.datasets.length > 0) { %>\n <div class=\"section reveal\">\n <div class=\"section-label\">Data Explorer</div>\n <h2 class=\"section-title\">Datasets</h2>\n <p style=\"color:var(--text-muted);font-size:0.85rem;margin-bottom:1.5rem;font-weight:300;\">Click a dataset to explore its fields, governance, and metadata.</p>\n <div style=\"display:flex;flex-direction:column;gap:1rem;\">\n <% for (var i = 0; i < model.datasets.length; i++) { var ds = model.datasets[i]; %>\n <% var dsGov = gov && gov.datasets && gov.datasets[ds.name]; %>\n <div class=\"card\">\n <div class=\"expandable-header\" onclick=\"toggleExpand('ds-<%= i %>')\">\n <div>\n <div style=\"display:flex;align-items:center;gap:0.6rem;margin-bottom:0.25rem;\">\n <span class=\"mono\" style=\"color:var(--text);\"><%= ds.name %></span>\n <% if (dsGov && dsGov.table_type) { %>\n <span class=\"ds-type ds-type-<%= dsGov.table_type %>\"><%= dsGov.table_type %></span>\n <% } %>\n <% if (ds.fields) { %><span class=\"tag\"><%= ds.fields.length %> fields</span><% } %>\n </div>\n <div style=\"font-size:0.75rem;color:var(--text-dim);font-family:var(--mono);\">\n <%= ds.source %>\n <% if (dsGov && dsGov.grain) { %> &middot; <span style=\"color:var(--text-muted);font-family:var(--sans);\"><%= dsGov.grain %></span><% } %>\n <% if (dsGov && dsGov.refresh) { %> &middot; <span style=\"color:var(--text-muted);font-family:var(--sans);\"><%= dsGov.refresh %></span><% } %>\n </div>\n <% if (ds.description) { %>\n <p style=\"font-size:0.82rem;color:var(--text-muted);margin-top:0.4rem;font-weight:300;\"><%= ds.description %></p>\n <% } %>\n </div>\n <span class=\"expand-icon\" id=\"ds-<%= i %>-icon\">+</span>\n </div>\n <div class=\"expandable-content\" id=\"ds-<%= i %>\">\n <% if (ds.fields && ds.fields.length > 0) { %>\n <table class=\"table-dark\" style=\"margin-top:1rem;\">\n <thead>\n <tr>\n <th>Field</th>\n <th>Description</th>\n <th>Role</th>\n <th>Aggregation</th>\n </tr>\n </thead>\n <tbody>\n <% for (var field of ds.fields) { %>\n <% var fKey = ds.name + '.' + field.name; %>\n <% var fGov = gov && gov.fields && gov.fields[fKey]; %>\n <% var role = fGov && fGov.semantic_role ? fGov.semantic_role : ''; %>\n <tr>\n <td class=\"mono\" style=\"font-size:0.78rem;\"><%= field.name %></td>\n <td style=\"color:var(--text-muted);font-size:0.82rem;\"><%= field.description || '' %></td>\n <td><% if (role) { %><span class=\"tag role-<%= role %>\"><%= role %></span><% } %></td>\n <td style=\"font-family:var(--mono);font-size:0.78rem;color:var(--text-dim);\"><%= fGov && fGov.default_aggregation ? fGov.default_aggregation : '' %></td>\n </tr>\n <% } %>\n </tbody>\n </table>\n <% } %>\n </div>\n </div>\n <% } %>\n </div>\n </div>\n <% } %>\n\n <% if (model.relationships && model.relationships.length > 0) { %>\n <div class=\"section reveal\">\n <div class=\"section-label\">Data Model</div>\n <h2 class=\"section-title\">Relationships</h2>\n <table class=\"table-dark\">\n <thead>\n <tr><th>Name</th><th>From</th><th></th><th>To</th></tr>\n </thead>\n <tbody>\n <% for (var rel of model.relationships) { %>\n <tr>\n <td style=\"color:var(--text-muted);font-size:0.82rem;\"><%= rel.name %></td>\n <td class=\"mono\" style=\"font-size:0.78rem;\"><%= rel.from %></td>\n <td style=\"color:var(--gold-dark);text-align:center;\">&rarr;</td>\n <td class=\"mono\" style=\"font-size:0.78rem;\"><%= rel.to %></td>\n </tr>\n <% } %>\n </tbody>\n </table>\n </div>\n <% } %>\n\n <% if (model.metrics && model.metrics.length > 0) { %>\n <div class=\"section reveal\">\n <div class=\"section-label\">Computed Metrics</div>\n <h2 class=\"section-title\">Metrics</h2>\n <div class=\"card-grid\">\n <% for (var metric of model.metrics) { %>\n <div class=\"card\">\n <div class=\"metric-name\"><%= metric.name %></div>\n <% if (metric.description) { %><div class=\"metric-desc\"><%= metric.description %></div><% } %>\n <% if (metric.expression && metric.expression.dialects && metric.expression.dialects.length > 0) { %>\n <div class=\"metric-formula\"><%= metric.expression.dialects[0].expression %></div>\n <% } %>\n </div>\n <% } %>\n </div>\n </div>\n <% } %>\n\n <% if (lineage && (lineage.upstream && lineage.upstream.length > 0 || lineage.downstream && lineage.downstream.length > 0)) { %>\n <div class=\"section reveal\">\n <div class=\"section-label\">Data Lineage</div>\n <h2 class=\"section-title\">Lineage</h2>\n <div class=\"lineage-flow\">\n <% if (lineage.upstream && lineage.upstream.length > 0) { %>\n <div class=\"lineage-col\">\n <div class=\"lineage-col-label\">Upstream</div>\n <% for (var u of lineage.upstream) { %>\n <div class=\"lineage-node\">\n <div class=\"lineage-node-name\"><%= u.source %></div>\n <div class=\"lineage-node-detail\"><%= u.type || '' %><% if (u.tool) { %> via <%= u.tool %><% } %></div>\n </div>\n <% } %>\n </div>\n <div class=\"lineage-arrow\">&rarr;</div>\n <% } %>\n <div class=\"lineage-col\">\n <div class=\"lineage-col-label\">This Model</div>\n <div class=\"lineage-node\" style=\"border-color:var(--gold-dark);\">\n <div class=\"lineage-node-name\" style=\"color:var(--gold);\"><%= model.name %></div>\n </div>\n </div>\n <% if (lineage.downstream && lineage.downstream.length > 0) { %>\n <div class=\"lineage-arrow\">&rarr;</div>\n <div class=\"lineage-col\">\n <div class=\"lineage-col-label\">Downstream</div>\n <% for (var d of lineage.downstream) { %>\n <div class=\"lineage-node\">\n <div class=\"lineage-node-name\"><%= d.target %></div>\n <div class=\"lineage-node-detail\"><%= d.type || '' %><% if (d.tool) { %> via <%= d.tool %><% } %></div>\n </div>\n <% } %>\n </div>\n <% } %>\n </div>\n </div>\n <% } %>\n\n <% if (tier) { %>\n <div class=\"section reveal\">\n <div class=\"section-label\">Data Quality</div>\n <h2 class=\"section-title\">Tier Scorecard</h2>\n <div class=\"scorecard\">\n <% var tierLevels = ['bronze', 'silver', 'gold']; %>\n <% for (var lvl of tierLevels) { %>\n <div class=\"scorecard-tier\">\n <div class=\"scorecard-tier-header\">\n <span class=\"scorecard-tier-name <%= lvl %>\"><%= lvl %></span>\n <% if (tier[lvl].passed) { %>\n <span class=\"scorecard-pass passed\">Passed</span>\n <% } else { %>\n <span class=\"scorecard-pass failed\">Not passed</span>\n <% } %>\n </div>\n <% if (tier[lvl].checks && tier[lvl].checks.length > 0) { %>\n <ul class=\"check-list\">\n <% for (var chk of tier[lvl].checks) { %>\n <li class=\"check-item\">\n <span class=\"check-icon <%= chk.passed ? 'pass' : 'fail' %>\"><%= chk.passed ? '\\\\u2713' : '\\\\u2717' %></span>\n <span><%= chk.label %><% if (chk.detail) { %> &mdash; <span style=\"color:var(--text-dim);\"><%= chk.detail %></span><% } %></span>\n </li>\n <% } %>\n </ul>\n <% } %>\n </div>\n <% } %>\n </div>\n </div>\n <% } %>\n</main>\n\n${FOOTER}\n${SCRIPTS}\n</body>\n</html>`;\n","import { HEAD, NAV, FOOTER, TIER_BADGE, SCRIPTS } from './shared.js';\n\nexport const schemaTemplate = `${HEAD}\n<body>\n${NAV}\n${TIER_BADGE}\n\n<main class=\"page\">\n <a href=\"<%= basePath %>/models/<%= model.name %>.html\" class=\"back-link\">&larr; Back to <%= model.name %></a>\n\n <div style=\"display:flex;align-items:center;gap:0.75rem;margin-bottom:2rem;\">\n <h1 style=\"font-family:var(--serif);font-weight:300;font-size:clamp(1.8rem,4vw,2.8rem);color:var(--text);\"><%= model.name %></h1>\n <% if (tier) { %><%- tierBadge(tier.tier) %><% } %>\n <span style=\"font-size:0.85rem;color:var(--text-dim);\">Schema Browser</span>\n </div>\n\n <% if (model.datasets && model.datasets.length > 0) { %>\n <% for (var i = 0; i < model.datasets.length; i++) { var ds = model.datasets[i]; %>\n <% var dsGov = gov && gov.datasets && gov.datasets[ds.name]; %>\n <div class=\"section reveal\">\n <div class=\"card\" style=\"padding:0;overflow:hidden;\">\n <div style=\"padding:1.25rem 1.5rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:0.5rem;\">\n <div style=\"display:flex;align-items:center;gap:0.6rem;\">\n <span class=\"mono\" style=\"font-size:1rem;color:var(--text);\"><%= ds.name %></span>\n <% if (dsGov && dsGov.table_type) { %>\n <span class=\"ds-type ds-type-<%= dsGov.table_type %>\"><%= dsGov.table_type %></span>\n <% } %>\n </div>\n <div style=\"display:flex;gap:0.75rem;flex-wrap:wrap;\">\n <% if (dsGov && dsGov.grain) { %><span class=\"tag\"><%= dsGov.grain %></span><% } %>\n <% if (dsGov && dsGov.refresh) { %><span class=\"tag\"><%= dsGov.refresh %></span><% } %>\n <% if (dsGov && dsGov.security) { %><span class=\"tag\"><%= dsGov.security %></span><% } %>\n </div>\n </div>\n <div style=\"padding:0 1.5rem 0.5rem;font-size:0.75rem;color:var(--text-dim);font-family:var(--mono);padding-top:0.75rem;\">\n Source: <%= ds.source %>\n </div>\n <% if (ds.description) { %>\n <div style=\"padding:0 1.5rem 1rem;font-size:0.85rem;color:var(--text-muted);font-weight:300;\"><%= ds.description %></div>\n <% } %>\n\n <% if (ds.fields && ds.fields.length > 0) { %>\n <table class=\"table-dark\">\n <thead>\n <tr>\n <th>Field</th>\n <th>Description</th>\n <th>Semantic Role</th>\n <th>Aggregation</th>\n </tr>\n </thead>\n <tbody>\n <% for (var field of ds.fields) { %>\n <% var fieldKey = ds.name + '.' + field.name; %>\n <% var fGov = gov && gov.fields && gov.fields[fieldKey]; %>\n <% var role = fGov && fGov.semantic_role ? fGov.semantic_role : ''; %>\n <tr>\n <td class=\"mono\" style=\"font-size:0.78rem;\"><%= field.name %></td>\n <td style=\"color:var(--text-muted);font-size:0.82rem;\"><%= field.description || '' %></td>\n <td><% if (role) { %><span class=\"tag role-<%= role %>\"><%= role %></span><% } %></td>\n <td style=\"font-family:var(--mono);font-size:0.78rem;color:var(--text-dim);\"><%= fGov && fGov.default_aggregation ? fGov.default_aggregation : '' %></td>\n </tr>\n <% } %>\n </tbody>\n </table>\n <% } else { %>\n <p style=\"padding:1.5rem;color:var(--text-dim);font-size:0.85rem;\">No fields defined.</p>\n <% } %>\n </div>\n </div>\n <% } %>\n <% } else { %>\n <p style=\"color:var(--text-muted);\">No datasets found.</p>\n <% } %>\n</main>\n\n${FOOTER}\n${SCRIPTS}\n</body>\n</html>`;\n","import { HEAD, NAV, FOOTER, SCRIPTS } from './shared.js';\n\nexport const rulesTemplate = `${HEAD}\n<body>\n${NAV}\n\n<main class=\"page\">\n <a href=\"<%= basePath %>/models/<%= modelName %>.html\" class=\"back-link\">&larr; Back to <%= modelName %></a>\n\n <h1 style=\"font-family:var(--serif);font-weight:300;font-size:clamp(1.8rem,4vw,2.8rem);color:var(--text);margin-bottom:2rem;\">\n <%= modelName %> <span style=\"color:var(--text-dim);font-weight:300;\">&mdash; Rules &amp; Queries</span>\n </h1>\n\n <% if (rules && rules.golden_queries && rules.golden_queries.length > 0) { %>\n <div class=\"section reveal\">\n <div class=\"section-label\">Golden Queries</div>\n <h2 class=\"section-title\">Pre-validated questions</h2>\n <% for (var gq of rules.golden_queries) { %>\n <div class=\"query-card\">\n <div class=\"query-question\"><%= gq.question %></div>\n <div class=\"query-sql sql-highlight\"><%= gq.sql %></div>\n <% if (gq.dialect || (gq.tags && gq.tags.length > 0)) { %>\n <div class=\"query-meta\">\n <% if (gq.dialect) { %><span>Dialect: <%= gq.dialect %></span><% } %>\n <% if (gq.tags && gq.tags.length > 0) { %><span>Tags: <%= gq.tags.join(', ') %></span><% } %>\n </div>\n <% } %>\n </div>\n <% } %>\n </div>\n <% } %>\n\n <% if (rules && rules.business_rules && rules.business_rules.length > 0) { %>\n <div class=\"section reveal\">\n <div class=\"section-label\">Business Rules</div>\n <h2 class=\"section-title\">Business Rules</h2>\n <div style=\"display:flex;flex-direction:column;gap:1rem;\">\n <% for (var br of rules.business_rules) { %>\n <div class=\"card\">\n <div style=\"font-family:var(--mono);font-size:0.88rem;color:var(--gold-light);margin-bottom:0.4rem;\"><%= br.name %></div>\n <p style=\"font-size:0.85rem;color:var(--text-muted);line-height:1.6;font-weight:300;\"><%= br.definition %></p>\n <% if (br.enforcement && br.enforcement.length > 0) { %>\n <div style=\"display:flex;gap:0.4rem;margin-top:0.6rem;flex-wrap:wrap;\">\n <% for (var e of br.enforcement) { %><span class=\"tag tag-green\"><%= e %></span><% } %>\n </div>\n <% } %>\n <% if (br.avoid && br.avoid.length > 0) { %>\n <div style=\"display:flex;gap:0.4rem;margin-top:0.4rem;flex-wrap:wrap;\">\n <% for (var a of br.avoid) { %><span class=\"tag tag-red\"><%= a %></span><% } %>\n </div>\n <% } %>\n </div>\n <% } %>\n </div>\n </div>\n <% } %>\n\n <% if (rules && rules.guardrail_filters && rules.guardrail_filters.length > 0) { %>\n <div class=\"section reveal\">\n <div class=\"section-label\">Safety</div>\n <h2 class=\"section-title\">Guardrail Filters</h2>\n <% for (var gf of rules.guardrail_filters) { %>\n <div class=\"guardrail\">\n <div class=\"guardrail-name\"><%= gf.name %></div>\n <div class=\"guardrail-filter\"><%= gf.filter %></div>\n <div class=\"guardrail-reason\"><%= gf.reason %></div>\n <% if (gf.tables && gf.tables.length > 0) { %>\n <div style=\"display:flex;gap:0.4rem;margin-top:0.6rem;\">\n <% for (var tb of gf.tables) { %><span class=\"tag\"><%= tb %></span><% } %>\n </div>\n <% } %>\n </div>\n <% } %>\n </div>\n <% } %>\n\n <% if (rules && rules.hierarchies && rules.hierarchies.length > 0) { %>\n <div class=\"section reveal\">\n <div class=\"section-label\">Drill Paths</div>\n <h2 class=\"section-title\">Hierarchies</h2>\n <div style=\"display:flex;flex-direction:column;gap:1rem;\">\n <% for (var h of rules.hierarchies) { %>\n <div class=\"card\">\n <div style=\"font-family:var(--mono);font-size:0.88rem;color:var(--gold-light);margin-bottom:0.4rem;\"><%= h.name %></div>\n <div style=\"font-size:0.82rem;color:var(--text-muted);margin-bottom:0.4rem;\">Dataset: <span class=\"mono\"><%= h.dataset %></span></div>\n <div style=\"display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;\">\n <% for (var li = 0; li < h.levels.length; li++) { %>\n <span class=\"tag tag-blue\"><%= h.levels[li] %></span>\n <% if (li < h.levels.length - 1) { %><span style=\"color:var(--gold-dark);\">&rarr;</span><% } %>\n <% } %>\n </div>\n </div>\n <% } %>\n </div>\n </div>\n <% } %>\n\n <% if (!rules || ((!rules.golden_queries || rules.golden_queries.length === 0) && (!rules.business_rules || rules.business_rules.length === 0) && (!rules.guardrail_filters || rules.guardrail_filters.length === 0) && (!rules.hierarchies || rules.hierarchies.length === 0))) { %>\n <p style=\"color:var(--text-muted);\">No rules or queries defined for this model.</p>\n <% } %>\n</main>\n\n${FOOTER}\n${SCRIPTS}\n</body>\n</html>`;\n","import { HEAD, NAV, FOOTER, SCRIPTS } from './shared.js';\n\nexport const glossaryTemplate = `${HEAD}\n<body>\n${NAV}\n\n<main class=\"page\">\n <h1 style=\"font-family:var(--serif);font-weight:300;font-size:clamp(2rem,5vw,3.5rem);color:var(--text);margin-bottom:0.5rem;\">Glossary</h1>\n <p style=\"color:var(--text-muted);font-size:1rem;margin-bottom:2rem;font-weight:300;\">Business term definitions and mappings.</p>\n\n <% var termIds = Object.keys(terms).sort(); %>\n <% if (termIds.length === 0) { %>\n <p style=\"color:var(--text-muted);\">No terms defined.</p>\n <% } else { %>\n <input type=\"text\" id=\"glossary-filter\"\n placeholder=\"Filter terms...\"\n class=\"search-input\"\n style=\"margin-bottom:2rem;max-width:400px;\" />\n\n <div class=\"glossary-grid\" id=\"glossary-grid\">\n <% for (var tid of termIds) { %>\n <% var term = terms[tid]; %>\n <div class=\"glossary-card\" id=\"term-<%= tid %>\" data-term=\"<%= tid %>\">\n <div class=\"glossary-term\"><%= tid %></div>\n <div class=\"glossary-def\"><%= term.definition %></div>\n <% if (term.synonyms && term.synonyms.length > 0) { %>\n <div style=\"display:flex;gap:0.4rem;margin-top:0.75rem;flex-wrap:wrap;\">\n <% for (var s of term.synonyms) { %><span class=\"tag\"><%= s %></span><% } %>\n </div>\n <% } %>\n <% if (term.maps_to && term.maps_to.length > 0) { %>\n <div style=\"display:flex;gap:0.4rem;margin-top:0.5rem;flex-wrap:wrap;\">\n <% for (var m of term.maps_to) { %><span class=\"tag tag-blue\"><%= m %></span><% } %>\n </div>\n <% } %>\n <% if (term.owner) { %>\n <div style=\"font-size:0.78rem;color:var(--text-dim);margin-top:0.6rem;\">\n Owner: <a href=\"<%= basePath %>/owners/<%= term.owner %>.html\"><%= term.owner %></a>\n </div>\n <% } %>\n </div>\n <% } %>\n </div>\n <% } %>\n</main>\n\n${FOOTER}\n${SCRIPTS}\n<script>\n(function() {\n var input = document.getElementById('glossary-filter');\n if (!input) return;\n var cards = document.querySelectorAll('.glossary-card');\n input.addEventListener('input', function() {\n var q = input.value.toLowerCase();\n cards.forEach(function(card) {\n var text = card.textContent.toLowerCase();\n card.style.display = text.indexOf(q) !== -1 ? '' : 'none';\n });\n });\n})();\n</script>\n</body>\n</html>`;\n","import { HEAD, NAV, FOOTER, TIER_BADGE, SCRIPTS } from './shared.js';\n\nexport const ownerTemplate = `${HEAD}\n<body>\n${NAV}\n${TIER_BADGE}\n\n<main class=\"page\">\n <a href=\"<%= basePath %>/\" class=\"back-link\">&larr; All Models</a>\n\n <h1 style=\"font-family:var(--serif);font-weight:300;font-size:clamp(2rem,5vw,3.5rem);color:var(--text);margin-bottom:0.5rem;\"><%= owner.display_name %></h1>\n\n <div style=\"display:flex;gap:1.5rem;font-size:0.85rem;color:var(--text-dim);margin-bottom:1.5rem;\">\n <% if (owner.email) { %><span>Email: <span style=\"color:var(--text-muted);\"><%= owner.email %></span></span><% } %>\n <% if (owner.team) { %><span>Team: <span style=\"color:var(--text-muted);\"><%= owner.team %></span></span><% } %>\n </div>\n\n <% if (owner.description) { %>\n <p style=\"color:var(--text-muted);font-size:0.95rem;margin-bottom:2rem;font-weight:300;max-width:600px;\"><%= owner.description %></p>\n <% } %>\n\n <% if (governedModels.length > 0) { %>\n <div class=\"section reveal\">\n <div class=\"section-label\">Stewardship</div>\n <h2 class=\"section-title\">Governed Models</h2>\n <div class=\"card-grid-sm\">\n <% for (var gm of governedModels) { %>\n <a href=\"<%= basePath %>/models/<%= gm.name %>.html\" class=\"card\" style=\"text-decoration:none;\">\n <div style=\"display:flex;align-items:center;gap:0.5rem;\">\n <span class=\"mono\" style=\"color:var(--gold-light);\"><%= gm.name %></span>\n <% if (gm.tier) { %><%- tierBadge(gm.tier) %><% } %>\n </div>\n </a>\n <% } %>\n </div>\n </div>\n <% } else { %>\n <p style=\"color:var(--text-muted);\">No models governed by this owner.</p>\n <% } %>\n</main>\n\n${FOOTER}\n${SCRIPTS}\n</body>\n</html>`;\n","import { HEAD, NAV, FOOTER, SCRIPTS } from './shared.js';\n\nexport const searchTemplate = `${HEAD}\n<body>\n${NAV}\n\n<main class=\"page\">\n <h1 style=\"font-family:var(--serif);font-weight:300;font-size:clamp(2rem,5vw,3.5rem);color:var(--text);margin-bottom:2rem;\">Search</h1>\n\n <input type=\"text\" id=\"search-input\"\n placeholder=\"Search models, datasets, terms...\"\n class=\"search-input\"\n style=\"margin-bottom:2rem;\" />\n\n <div id=\"search-results\" style=\"display:flex;flex-direction:column;gap:0.75rem;\"></div>\n</main>\n\n${FOOTER}\n${SCRIPTS}\n\n<script src=\"https://cdn.jsdelivr.net/npm/minisearch@7.1.0/dist/umd/index.min.js\"></script>\n<script>\n(function() {\n var indexData = <%- searchIndexJson %>;\n var miniSearch = MiniSearch.loadJSON(JSON.stringify(indexData.index), indexData.options);\n\n var input = document.getElementById('search-input');\n var resultsContainer = document.getElementById('search-results');\n var docs = indexData.documents;\n\n var typeColors = {\n model: 'tag-gold',\n dataset: 'tag-blue',\n term: 'tag-green',\n owner: 'tag-bronze'\n };\n\n function clearResults() {\n while (resultsContainer.firstChild) {\n resultsContainer.removeChild(resultsContainer.firstChild);\n }\n }\n\n function createResultElement(doc) {\n var wrapper = document.createElement('div');\n wrapper.className = 'card';\n wrapper.style.padding = '1rem 1.25rem';\n\n var top = document.createElement('div');\n top.style.display = 'flex';\n top.style.alignItems = 'center';\n top.style.gap = '0.5rem';\n\n var link = document.createElement('a');\n link.href = doc.url;\n link.style.fontFamily = 'var(--mono)';\n link.style.fontSize = '0.88rem';\n link.style.color = 'var(--gold-light)';\n link.textContent = doc.title;\n top.appendChild(link);\n\n var badge = document.createElement('span');\n badge.className = 'tag ' + (typeColors[doc.type] || '');\n badge.textContent = doc.type;\n top.appendChild(badge);\n\n wrapper.appendChild(top);\n\n if (doc.description) {\n var desc = document.createElement('p');\n desc.style.fontSize = '0.82rem';\n desc.style.color = 'var(--text-muted)';\n desc.style.marginTop = '0.3rem';\n desc.style.fontWeight = '300';\n desc.textContent = doc.description;\n wrapper.appendChild(desc);\n }\n\n return wrapper;\n }\n\n input.addEventListener('input', function() {\n var query = input.value.trim();\n clearResults();\n if (!query) return;\n var hits = miniSearch.search(query, { prefix: true, fuzzy: 0.2 });\n if (hits.length === 0) {\n var noResults = document.createElement('p');\n noResults.style.color = 'var(--text-muted)';\n noResults.textContent = 'No results found.';\n resultsContainer.appendChild(noResults);\n return;\n }\n hits.slice(0, 20).forEach(function(hit) {\n var doc = docs[hit.id];\n if (doc) {\n resultsContainer.appendChild(createResultElement(doc));\n }\n });\n });\n})();\n</script>\n</body>\n</html>`;\n","/**\n * Builds a MiniSearch index from a Manifest for client-side search.\n *\n * The index is serialized to JSON so it can be embedded in the search page\n * and loaded by MiniSearch on the client side.\n */\n\nimport MiniSearch from 'minisearch';\nimport type { Manifest } from '@runcontext/core';\n\nexport interface SearchDocument {\n id: string;\n type: string;\n title: string;\n description: string;\n url: string;\n}\n\nexport interface SearchIndex {\n /** Serialized MiniSearch index (JSON-parsed object). */\n index: unknown;\n /** MiniSearch constructor options needed to reload the index. */\n options: {\n fields: string[];\n storeFields: string[];\n idField: string;\n };\n /** Map of document ID to document metadata for rendering results. */\n documents: Record<string, SearchDocument>;\n}\n\nconst MINISEARCH_OPTIONS = {\n fields: ['title', 'description', 'type'],\n storeFields: ['title', 'description', 'type', 'url'],\n idField: 'id',\n};\n\n/**\n * Build a search index from a manifest.\n *\n * @param manifest - The compiled ContextKit manifest\n * @param basePath - The base URL path for links (e.g. '' or '/docs')\n * @returns A SearchIndex object ready for JSON serialization\n */\nexport function buildSearchIndex(manifest: Manifest, basePath: string): SearchIndex {\n const docs: SearchDocument[] = [];\n let idCounter = 0;\n\n // Index models\n for (const [name, model] of Object.entries(manifest.models)) {\n docs.push({\n id: String(idCounter++),\n type: 'model',\n title: name,\n description: model.description ?? '',\n url: `${basePath}/models/${name}.html`,\n });\n\n // Index datasets within each model\n if (model.datasets) {\n for (const ds of model.datasets) {\n docs.push({\n id: String(idCounter++),\n type: 'dataset',\n title: `${name} / ${ds.name}`,\n description: ds.description ?? '',\n url: `${basePath}/models/${name}/schema.html`,\n });\n }\n }\n }\n\n // Index glossary terms\n for (const [termId, term] of Object.entries(manifest.terms)) {\n docs.push({\n id: String(idCounter++),\n type: 'term',\n title: termId,\n description: term.definition,\n url: `${basePath}/glossary.html#term-${termId}`,\n });\n }\n\n // Index owners\n for (const [oid, owner] of Object.entries(manifest.owners)) {\n docs.push({\n id: String(idCounter++),\n type: 'owner',\n title: owner.display_name,\n description: owner.description ?? '',\n url: `${basePath}/owners/${oid}.html`,\n });\n }\n\n // Build MiniSearch index\n const miniSearch = new MiniSearch(MINISEARCH_OPTIONS);\n miniSearch.addAll(docs);\n\n // Create document lookup map by id\n const documentsMap: Record<string, SearchDocument> = {};\n for (const doc of docs) {\n documentsMap[doc.id] = doc;\n }\n\n return {\n index: JSON.parse(JSON.stringify(miniSearch)),\n options: MINISEARCH_OPTIONS,\n documents: documentsMap,\n };\n}\n"]}