From 9421f288a87d3ed24a05cc88a053d9a713569384 Mon Sep 17 00:00:00 2001 From: Mike Long Date: Wed, 22 Apr 2026 21:43:36 -0700 Subject: [PATCH 1/3] Add local loopback MCP install runtime --- app/mcp/route.ts | 34 +-- content/docs/start/what-is-judgmentkit.mdx | 6 +- content/product-surface.json | 2 +- lib/constants.ts | 14 ++ lib/install-contract.ts | 30 ++- lib/install-mcp.ts | 278 ++++++++++++++++----- lib/mcp-http.ts | 103 ++++++++ lib/mcp-reference.ts | 2 + lib/mcp-server.ts | 15 +- lib/product-surface.ts | 78 +++++- lib/site.ts | 30 ++- lib/types.ts | 26 +- package.json | 1 + scripts/install-mcp.ts | 2 +- scripts/judgmentkit-mcp-local.ts | 132 ++++++++++ tests/homepage-install-smoke.test.ts | 137 ++-------- tests/install-script.test.ts | 30 ++- tests/landing-page.test.ts | 5 +- tests/mcp-local.test.ts | 99 ++++++++ tests/product-surface.test.ts | 74 +++++- tests/site-build.test.ts | 26 +- 21 files changed, 880 insertions(+), 244 deletions(-) create mode 100644 lib/mcp-http.ts create mode 100644 scripts/judgmentkit-mcp-local.ts create mode 100644 tests/mcp-local.test.ts diff --git a/app/mcp/route.ts b/app/mcp/route.ts index 6566fa3..7b34e26 100644 --- a/app/mcp/route.ts +++ b/app/mcp/route.ts @@ -1,38 +1,20 @@ -import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; -import { NextResponse } from "next/server"; - -import { createJudgmentKitMcpServer, getMcpMetadata } from "@/lib/mcp-server"; +import { handleMcpHttpRequest } from "@/lib/mcp-http"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; -function wantsSse(request: Request) { - return request.headers.get("accept")?.includes("text/event-stream") ?? false; -} - -async function handleStreamableHttpRequest(request: Request) { - const server = createJudgmentKitMcpServer(); - const transport = new WebStandardStreamableHTTPServerTransport({ - sessionIdGenerator: undefined, - enableJsonResponse: true, - }); - - await server.connect(transport); - return transport.handleRequest(request); -} - export async function GET(request: Request) { - if (!wantsSse(request)) { - return NextResponse.json(getMcpMetadata("streamable-http")); - } - - return handleStreamableHttpRequest(request); + return handleMcpHttpRequest(request); } export async function POST(request: Request) { - return handleStreamableHttpRequest(request); + return handleMcpHttpRequest(request); } export async function DELETE(request: Request) { - return handleStreamableHttpRequest(request); + return handleMcpHttpRequest(request); +} + +export async function OPTIONS(request: Request) { + return handleMcpHttpRequest(request, { allowOptions: true, cors: true }); } diff --git a/content/docs/start/what-is-judgmentkit.mdx b/content/docs/start/what-is-judgmentkit.mdx index b091d3d..2abe6ce 100644 --- a/content/docs/start/what-is-judgmentkit.mdx +++ b/content/docs/start/what-is-judgmentkit.mdx @@ -32,7 +32,7 @@ Use JudgmentKit when you want an agent to do real work without making up its own ## Use it with agents -Install JudgmentKit from a local checkout over stdio, fetch the workflow you care about, call `resolve_related` to pull the linked guardrails and examples, then run the model with those artifacts in context. +Install JudgmentKit from a local checkout over loopback HTTP at `http://127.0.0.1:8765/mcp`, fetch the workflow you care about, call `resolve_related` to pull the linked guardrails and examples, then run the model with those artifacts in context. For example, for support you would: @@ -40,7 +40,7 @@ For example, for support you would: - call `resolve_related` for `workflow.support-assistant` - call `get_example` for `example.brand-tone.support-coercive-copy` -`/mcp` is not the install target. It is the hosted reference/debug endpoint that mirrors the same public truth as the local JudgmentKit checkout. +Hosted `/mcp` is not the install target. It is the hosted reference/debug endpoint that mirrors the same public truth as the local JudgmentKit checkout. ## Problem in human terms @@ -73,7 +73,7 @@ Without a shared judgment layer, product, design, governance, and engineering ma - two concrete workflows - three synthetic examples - a public artifact inventory and schema set -- a hosted read-only MCP endpoint that mirrors the same public truth as the local stdio install +- a hosted read-only MCP endpoint that mirrors the same public truth as the local loopback HTTP install ## Related pages diff --git a/content/product-surface.json b/content/product-surface.json index a2bfedf..126ea8f 100644 --- a/content/product-surface.json +++ b/content/product-surface.json @@ -4,7 +4,7 @@ "utility_sentence": "Connect the MCP. Paste the first message. Run the first pass.", "run_sequence": ["Connect", "Paste", "Run"], "workbench_label": "Run the first pass", - "workbench_support": "Run the hosted installer to clone JudgmentKit, install dependencies, wire the client to the local stdio server, and verify the local tools/list response.", + "workbench_support": "Run the hosted installer to clone JudgmentKit, install dependencies, wire the client to the local loopback MCP endpoint, and verify the local tools/list response.", "proof_heading": "Generated UI proof", "proof_support": "Same brief. One uncontrolled pass and one JudgmentKit-guided pass.", "proof_notes": [ diff --git a/lib/constants.ts b/lib/constants.ts index 8dbf445..c58f43c 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -9,7 +9,14 @@ export const JUDGMENTKIT_REPOSITORY_CLONE_URL = export const DEFAULT_LOCAL_JUDGMENTKIT_CHECKOUT_PATH = "$HOME/judgmentkit"; export const LOCAL_JUDGMENTKIT_CHECKOUT_PLACEHOLDER = ""; +export const LOCAL_MCP_DEFAULT_HOST = "127.0.0.1"; +export const LOCAL_MCP_DEFAULT_PORT = 8765; +export const LOCAL_MCP_ENDPOINT_PATH = "/mcp"; +export const LOCAL_MCP_HOST_ENV = "JUDGMENTKIT_MCP_HOST"; +export const LOCAL_MCP_PORT_ENV = "JUDGMENTKIT_MCP_PORT"; +export const LOCAL_MCP_DEFAULT_URL = `http://${LOCAL_MCP_DEFAULT_HOST}:${LOCAL_MCP_DEFAULT_PORT}${LOCAL_MCP_ENDPOINT_PATH}`; export const LOCAL_JUDGMENTKIT_INSTALL_COMMAND = `npm --prefix ${LOCAL_JUDGMENTKIT_CHECKOUT_PLACEHOLDER} install`; +export const LOCAL_JUDGMENTKIT_MCP_LOCAL_COMMAND = `npm --prefix ${LOCAL_JUDGMENTKIT_CHECKOUT_PLACEHOLDER} run mcp:local`; export const LOCAL_JUDGMENTKIT_STDIO_ARGS = [ "--prefix", LOCAL_JUDGMENTKIT_CHECKOUT_PLACEHOLDER, @@ -21,6 +28,13 @@ export const LOCAL_JUDGMENTKIT_INSTALLER_COMMAND = "node --import tsx ./scripts/install-mcp.ts"; export const HOSTED_JUDGMENTKIT_BOOTSTRAP_COMMAND = `curl -fsSL ${CANONICAL_INSTALL_URL} | bash -s -- --client `; +export function createLocalMcpUrl( + host = LOCAL_MCP_DEFAULT_HOST, + port: number | string = LOCAL_MCP_DEFAULT_PORT, +) { + return `http://${host}:${port}${LOCAL_MCP_ENDPOINT_PATH}`; +} + function normalizeSiteUrl(value: string) { const trimmed = value.trim(); const withProtocol = /^https?:\/\//i.test(trimmed) diff --git a/lib/install-contract.ts b/lib/install-contract.ts index a5b1d7a..f020a47 100644 --- a/lib/install-contract.ts +++ b/lib/install-contract.ts @@ -4,6 +4,7 @@ import rawProductSurface from "@/content/product-surface.json"; import { CANONICAL_INSTALL_URL, CANONICAL_SITE_URL, + createLocalMcpUrl, DEFAULT_LOCAL_JUDGMENTKIT_CHECKOUT_PATH, HOSTED_MCP_REFERENCE_URL, HOSTED_JUDGMENTKIT_BOOTSTRAP_COMMAND, @@ -11,7 +12,12 @@ import { LOCAL_JUDGMENTKIT_CHECKOUT_PLACEHOLDER, LOCAL_JUDGMENTKIT_INSTALL_COMMAND, LOCAL_JUDGMENTKIT_INSTALLER_COMMAND, - LOCAL_JUDGMENTKIT_STDIO_ARGS, + LOCAL_JUDGMENTKIT_MCP_LOCAL_COMMAND, + LOCAL_MCP_DEFAULT_HOST, + LOCAL_MCP_DEFAULT_PORT, + LOCAL_MCP_ENDPOINT_PATH, + LOCAL_MCP_HOST_ENV, + LOCAL_MCP_PORT_ENV, } from "@/lib/constants"; import { createCommandReferenceUrl, @@ -55,7 +61,7 @@ export function loadInstallContract(): InstallContract { version: "3.0.0", product_name: content.product_name, command_reference_url: createCommandReferenceUrl(CANONICAL_SITE_URL), - warning: `Install JudgmentKit from a local checkout over stdio via the hosted bootstrap script at ${CANONICAL_INSTALL_URL}. ${HOSTED_MCP_REFERENCE_URL} is a hosted reference/debug endpoint, not the install target.`, + warning: `Install JudgmentKit from a local checkout over loopback HTTP via the hosted bootstrap script at ${CANONICAL_INSTALL_URL}. ${HOSTED_MCP_REFERENCE_URL} is a hosted reference/debug endpoint, not the install target.`, installer: { mode: "hosted-bootstrap", bootstrap_url: CANONICAL_INSTALL_URL, @@ -72,10 +78,20 @@ export function loadInstallContract(): InstallContract { install_command: LOCAL_JUDGMENTKIT_INSTALL_COMMAND, }, server_name: "judgmentkit", - install_transport: "stdio", + install_transport: "http", connection: { - command: "npm", - args: LOCAL_JUDGMENTKIT_STDIO_ARGS, + transport: "http", + url: createLocalMcpUrl(), + loopback_runtime: { + start_command: LOCAL_JUDGMENTKIT_MCP_LOCAL_COMMAND, + host: LOCAL_MCP_DEFAULT_HOST, + port: LOCAL_MCP_DEFAULT_PORT, + endpoint: LOCAL_MCP_ENDPOINT_PATH, + env_overrides: { + host: LOCAL_MCP_HOST_ENV, + port: LOCAL_MCP_PORT_ENV, + }, + }, }, supported_clients: getSupportedClientIds(content.install_targets), clients: content.install_targets.map((target) => ({ @@ -87,8 +103,10 @@ export function loadInstallContract(): InstallContract { verification: { method: "tools/list", server_name: "judgmentkit", + endpoint: createLocalMcpUrl(), + start_command: LOCAL_JUDGMENTKIT_MCP_LOCAL_COMMAND, instructions: - `After configuring the local "judgmentkit" MCP server, call MCP tools/list against that local server to confirm the install is reachable. Then use ${createCommandReferenceUrl( + `Start the local loopback server with ${LOCAL_JUDGMENTKIT_MCP_LOCAL_COMMAND}, then call MCP tools/list against ${createLocalMcpUrl()} to confirm the install is reachable. Then use ${createCommandReferenceUrl( CANONICAL_SITE_URL, )} to attach docs URLs to the returned command names.`, expected_tools: listTools().map((tool) => tool.name), diff --git a/lib/install-mcp.ts b/lib/install-mcp.ts index 457f000..973554a 100644 --- a/lib/install-mcp.ts +++ b/lib/install-mcp.ts @@ -4,10 +4,13 @@ import os from "node:os"; import path from "node:path"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { + createLocalMcpUrl, JUDGMENTKIT_REPOSITORY_CLONE_URL, + LOCAL_MCP_DEFAULT_HOST, + LOCAL_MCP_DEFAULT_PORT, } from "@/lib/constants"; import { loadInstallContract } from "@/lib/install-contract"; import type { InstallContract, InstallContractClient, InstallerClientId } from "@/lib/types"; @@ -30,6 +33,8 @@ export type InstallerCliOptions = { dryRun?: boolean; manual?: boolean; noVerify?: boolean; + host?: string; + port?: number; cwd?: string; }; @@ -40,11 +45,10 @@ export type InstallResult = { backupPath?: string; wroteConfig: boolean; verified: boolean; - manualSnippet: string; - command: { - command: string; - args: string[]; - }; + endpoint: string; + startCommand: string; + configSnippet: string; + bridgeFallbackSnippet: string; }; export class InstallerError extends Error { @@ -109,6 +113,18 @@ function normalizeClient(value: string | undefined): InstallerClientId { ); } +function normalizePort(value: string | undefined) { + const parsed = Number(value); + if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65_535) { + return parsed; + } + + throw new InstallerError( + "args", + "Missing or invalid --port. Provide a TCP port between 1 and 65535.", + ); +} + function expandHomePath(value: string, homeDir: string) { return value.startsWith("~/") ? path.join(homeDir, value.slice(2)) : value; } @@ -120,6 +136,8 @@ export function parseInstallerArgs(argv: string[]): InstallerCliOptions { let dryRun = false; let manual = false; let noVerify = false; + let host: string | undefined; + let port: number | undefined; for (let index = 0; index < argv.length; index += 1) { const argument = argv[index]; @@ -145,10 +163,18 @@ export function parseInstallerArgs(argv: string[]): InstallerCliOptions { case "--no-verify": noVerify = true; break; + case "--host": + host = argv[index + 1]; + index += 1; + break; + case "--port": + port = normalizePort(argv[index + 1]); + index += 1; + break; case "--help": throw new InstallerError( "args", - "Usage: node --import tsx ./scripts/install-mcp.ts --client [--path ] [--config-path ] [--dry-run] [--manual] [--no-verify]", + "Usage: node --import tsx ./scripts/install-mcp.ts --client [--path ] [--config-path ] [--host ] [--port ] [--dry-run] [--manual] [--no-verify]", ); default: if (argument.startsWith("--")) { @@ -165,6 +191,8 @@ export function parseInstallerArgs(argv: string[]): InstallerCliOptions { dryRun, manual, noVerify, + host, + port, }; } @@ -198,15 +226,6 @@ function materializeLocalPath(value: string, contract: InstallContract, checkout return value.replaceAll(contract.repository.local_path_placeholder, checkoutPath); } -function materializeConnection(contract: InstallContract, checkoutPath: string) { - return { - command: contract.connection.command, - args: contract.connection.args.map((argument) => - materializeLocalPath(argument, contract, checkoutPath), - ), - }; -} - function getClientContract( contract: InstallContract, client: InstallerClientId, @@ -219,19 +238,90 @@ function getClientContract( return target; } -export function renderManualConfigSnippet(client: InstallContractClient, checkoutPath: string) { - const connection = materializeConnection(loadInstallContract(), checkoutPath); +function getHttpConnection(contract: InstallContract) { + if (contract.connection.transport !== "http") { + throw new InstallerError( + "config", + "JudgmentKit install contract does not expose an HTTP connection.", + ); + } + + return contract.connection; +} + +function resolveHost(host: string | undefined) { + const trimmed = host?.trim(); + return trimmed || LOCAL_MCP_DEFAULT_HOST; +} + +function resolvePort(port: number | undefined) { + return port ?? LOCAL_MCP_DEFAULT_PORT; +} + +function materializeLoopbackRuntime( + contract: InstallContract, + checkoutPath: string, + host: string, + port: number, +) { + const connection = getHttpConnection(contract); + + return { + endpoint: createLocalMcpUrl(host, port), + startCommand: materializeLocalPath( + connection.loopback_runtime.start_command, + contract, + checkoutPath, + ), + }; +} + +function createBridgeConfig(endpoint: string) { + return { + command: "npx", + args: ["-y", "mcp-remote", endpoint], + }; +} + +export function renderManualConfigSnippet( + client: InstallContractClient, + _checkoutPath: string, + endpoint = createLocalMcpUrl(), +) { + if (client.config_format === "toml") { + return `[mcp_servers.judgmentkit] +url = "${endpoint}"`; + } + + return `${JSON.stringify( + { + mcpServers: { + judgmentkit: { + url: endpoint, + }, + }, + }, + null, + 2, + )}\n`; +} + +export function renderBridgeFallbackSnippet( + client: InstallContractClient, + endpoint = createLocalMcpUrl(), +) { + const bridgeConfig = createBridgeConfig(endpoint); if (client.config_format === "toml") { return `[mcp_servers.judgmentkit] -command = "${connection.command}" -args = ${JSON.stringify(connection.args)}`; +command = "${bridgeConfig.command}" +args = ${JSON.stringify(bridgeConfig.args)}`; } return `${JSON.stringify( { mcpServers: { - judgmentkit: connection, + judgmentkit: bridgeConfig, }, }, null, @@ -276,7 +366,7 @@ export function upsertCodexTomlConfig(existingText: string, judgmentKitBlock: st return `${prefix}${prefix ? "\n\n" : ""}${judgmentKitBlock.trim()}\n`; } -export function upsertJsonMcpConfig(existingText: string, serverConfig: { command: string; args: string[] }) { +export function upsertJsonMcpConfig(existingText: string, serverConfig: { url: string }) { const trimmed = existingText.trim(); const parsed = trimmed.length > 0 ? JSON.parse(trimmed) : {}; @@ -285,7 +375,7 @@ export function upsertJsonMcpConfig(existingText: string, serverConfig: { comman } const root = parsed as { - mcpServers?: Record; + mcpServers?: Record; }; if ( @@ -353,10 +443,10 @@ async function writeClientConfig( client: InstallContractClient, configPath: string, checkoutPath: string, + endpoint: string, deps: InstallDependencies, ) { - const manualSnippet = renderManualConfigSnippet(client, checkoutPath); - const connection = materializeConnection(loadInstallContract(), checkoutPath); + const configSnippet = renderManualConfigSnippet(client, checkoutPath, endpoint); const existingText = (await pathExists(deps.fs, configPath)) ? await deps.fs.readFile(configPath, "utf8") : ""; @@ -364,19 +454,19 @@ async function writeClientConfig( let nextText: string; try { if (client.config_format === "toml") { - nextText = upsertCodexTomlConfig(existingText, manualSnippet); + nextText = upsertCodexTomlConfig(existingText, configSnippet); } else { - nextText = upsertJsonMcpConfig(existingText, connection); + nextText = upsertJsonMcpConfig(existingText, { url: endpoint }); } } catch (error) { if (error instanceof InstallerError) { - throw new InstallerError("config", error.message, manualSnippet); + throw new InstallerError("config", error.message, configSnippet); } throw new InstallerError( "config", `Failed to update ${configPath}: ${String(error)}`, - manualSnippet, + configSnippet, ); } @@ -395,30 +485,19 @@ async function writeClientConfig( throw new InstallerError( "config", `Failed to write client config at ${configPath}: ${String(error)}`, - manualSnippet, + configSnippet, ); } return { backupPath, - manualSnippet, + configSnippet, }; } -export async function verifyInstalledMcp(checkoutPath: string) { +async function verifyHttpToolsList(endpoint: string) { const contract = loadInstallContract(); - const connection = materializeConnection(contract, checkoutPath); - const stderrOutput: string[] = []; - const transport = new StdioClientTransport({ - command: connection.command, - args: connection.args, - cwd: checkoutPath, - stderr: "pipe", - }); - - transport.stderr?.on("data", (chunk: Buffer | string) => { - stderrOutput.push(chunk.toString()); - }); + const transport = new StreamableHTTPClientTransport(new URL(endpoint)); const client = new Client({ name: "judgmentkit-install-verifier", @@ -435,13 +514,71 @@ export async function verifyInstalledMcp(checkoutPath: string) { `Unexpected tools/list response. Expected ${expected.join(", ")} but received ${toolNames.join(", ")}.`, ); } + } finally { + await transport.close(); + } +} + +async function waitForHttpToolsList(endpoint: string, timeoutMs: number) { + const startedAt = Date.now(); + let lastError: unknown; + + while (Date.now() - startedAt < timeoutMs) { + try { + await verifyHttpToolsList(endpoint); + return; + } catch (error) { + lastError = error; + await new Promise((resolve) => setTimeout(resolve, 250)); + } + } + + throw lastError instanceof Error ? lastError : new Error(String(lastError)); +} + +export async function verifyInstalledMcp( + checkoutPath: string, + options: { + endpoint?: string; + host?: string; + port?: number; + } = {}, +) { + const host = resolveHost(options.host); + const port = resolvePort(options.port); + const endpoint = options.endpoint ?? createLocalMcpUrl(host, port); + const stderrOutput: string[] = []; + + try { + await verifyHttpToolsList(endpoint); + return; + } catch { + // If no loopback server is already running, start one just for verification. + } + + const child = spawn("npm", ["--prefix", checkoutPath, "run", "mcp:local"], { + cwd: checkoutPath, + env: { + ...process.env, + JUDGMENTKIT_MCP_HOST: host, + JUDGMENTKIT_MCP_PORT: String(port), + }, + stdio: ["ignore", "ignore", "pipe"], + }); + + child.stderr.on("data", (chunk: Buffer | string) => { + stderrOutput.push(chunk.toString()); + }); + + try { + await waitForHttpToolsList(endpoint, 10_000); } catch (error) { throw new InstallerError( "verify", - `Failed to verify the local JudgmentKit MCP install: ${String(error)} ${stderrOutput.join("").trim()}`.trim(), + `Failed to verify the local JudgmentKit MCP install at ${endpoint}: ${String(error)} ${stderrOutput.join("").trim()}`.trim(), ); } finally { - await transport.close(); + child.kill(); } } @@ -455,14 +592,22 @@ export async function installJudgmentKitMcp( }; const contract = loadInstallContract(); const checkoutPath = resolveCheckoutPath(options.checkoutPath, deps.homeDir()); + const host = resolveHost(options.host); + const port = resolvePort(options.port); + const { endpoint, startCommand } = materializeLoopbackRuntime( + contract, + checkoutPath, + host, + port, + ); const client = getClientContract(contract, options.client); const configPath = resolveClientConfigPath(client.id, { configPath: options.configPath, cwd: options.cwd, homeDir: deps.homeDir(), }); - const manualSnippet = renderManualConfigSnippet(client, checkoutPath); - const connection = materializeConnection(contract, checkoutPath); + const configSnippet = renderManualConfigSnippet(client, checkoutPath, endpoint); + const bridgeFallbackSnippet = renderBridgeFallbackSnippet(client, endpoint); if (options.dryRun || options.manual) { return { @@ -471,17 +616,25 @@ export async function installJudgmentKitMcp( configPath, wroteConfig: false, verified: false, - manualSnippet, - command: connection, + endpoint, + startCommand, + configSnippet, + bridgeFallbackSnippet, }; } await ensureCheckout(checkoutPath, deps); await ensureDependencies(checkoutPath, deps); - const configResult = await writeClientConfig(client, configPath, checkoutPath, deps); + const configResult = await writeClientConfig( + client, + configPath, + checkoutPath, + endpoint, + deps, + ); if (!options.noVerify) { - await verifyInstalledMcp(checkoutPath); + await verifyInstalledMcp(checkoutPath, { endpoint, host, port }); } return { @@ -491,8 +644,10 @@ export async function installJudgmentKitMcp( backupPath: configResult.backupPath, wroteConfig: true, verified: !options.noVerify, - manualSnippet, - command: connection, + endpoint, + startCommand, + configSnippet, + bridgeFallbackSnippet, }; } @@ -501,7 +656,8 @@ export function formatInstallerResult(result: InstallResult) { `JudgmentKit installer prepared client: ${result.client}`, `Checkout path: ${result.checkoutPath}`, `Config path: ${result.configPath}`, - `Command: ${result.command.command} ${result.command.args.join(" ")}`, + `Endpoint: ${result.endpoint}`, + `Start local MCP: ${result.startCommand}`, ]; if (result.backupPath) { @@ -516,6 +672,16 @@ export function formatInstallerResult(result: InstallResult) { lines.push("Verification: tools/list succeeded"); } - lines.push("", "Manual fallback snippet:", result.manualSnippet); + lines.push( + "", + "Before using the configured client, run:", + result.startCommand, + "", + "Config snippet:", + result.configSnippet, + "", + "Bridge fallback snippet for URL-incompatible clients:", + result.bridgeFallbackSnippet, + ); return `${lines.join("\n")}\n`; } diff --git a/lib/mcp-http.ts b/lib/mcp-http.ts new file mode 100644 index 0000000..a0d839d --- /dev/null +++ b/lib/mcp-http.ts @@ -0,0 +1,103 @@ +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; + +import { createJudgmentKitMcpServer, getMcpMetadata } from "@/lib/mcp-server"; + +type McpHttpMetadataTransport = "streamable-http" | "local-loopback-http"; + +const LOCAL_CORS_HEADERS = { + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET, POST, OPTIONS", + "access-control-allow-headers": + "content-type, accept, mcp-protocol-version, mcp-session-id", +} as const; + +function wantsSse(request: Request) { + return request.headers.get("accept")?.includes("text/event-stream") ?? false; +} + +function withCors(response: Response) { + const headers = new Headers(response.headers); + for (const [key, value] of Object.entries(LOCAL_CORS_HEADERS)) { + headers.set(key, value); + } + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); +} + +function jsonResponse(payload: unknown, init?: ResponseInit) { + return new Response(JSON.stringify(payload, null, 2), { + ...init, + headers: { + "content-type": "application/json; charset=utf-8", + ...(init?.headers ?? {}), + }, + }); +} + +export function createMcpMetadataResponse( + transport: McpHttpMetadataTransport, + options: { cors?: boolean } = {}, +) { + const response = jsonResponse(getMcpMetadata(transport)); + return options.cors ? withCors(response) : response; +} + +export function createMcpOptionsResponse() { + return new Response(null, { + status: 204, + headers: LOCAL_CORS_HEADERS, + }); +} + +export function createMcpNotFoundResponse(message = "Use /mcp.") { + return withCors( + jsonResponse( + { + error: "not_found", + message, + }, + { status: 404 }, + ), + ); +} + +export async function handleStreamableMcpRequest( + request: Request, + options: { cors?: boolean } = {}, +) { + const server = createJudgmentKitMcpServer(); + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + + await server.connect(transport); + const response = await transport.handleRequest(request); + return options.cors ? withCors(response) : response; +} + +export async function handleMcpHttpRequest( + request: Request, + options: { + metadataTransport?: McpHttpMetadataTransport; + cors?: boolean; + allowOptions?: boolean; + } = {}, +) { + if (request.method === "OPTIONS" && options.allowOptions) { + return createMcpOptionsResponse(); + } + + if (request.method === "GET" && !wantsSse(request)) { + return createMcpMetadataResponse( + options.metadataTransport ?? "streamable-http", + { cors: options.cors }, + ); + } + + return handleStreamableMcpRequest(request, { cors: options.cors }); +} diff --git a/lib/mcp-reference.ts b/lib/mcp-reference.ts index 8fb7069..13817e8 100644 --- a/lib/mcp-reference.ts +++ b/lib/mcp-reference.ts @@ -37,6 +37,8 @@ function getPromptExampleCall(name: string) { return 'summarize_example_incident({ resource_id: "example.ui-generation.onboarding-clarity-drift" })'; case "start_design_workflow": return 'start_design_workflow({ feature_intent: "Generate the JudgmentKit homepage" })'; + case "start_no_design_system_workflow": + return 'start_no_design_system_workflow({ feature_intent: "Generate a JudgmentKit-native review workspace without an external design system" })'; case "refine_design_first_pass": return 'refine_design_first_pass({ feature_intent: "Refine the JudgmentKit homepage", draft: "...", refinement_goal: "first-time usability" })'; default: diff --git a/lib/mcp-server.ts b/lib/mcp-server.ts index 059dac2..ebc3ce7 100644 --- a/lib/mcp-server.ts +++ b/lib/mcp-server.ts @@ -171,6 +171,17 @@ export function createJudgmentKitMcpServer() { async (args) => createPromptResult("start_design_workflow", args), ); + server.registerPrompt( + "start_no_design_system_workflow", + { + description: PROMPT_DEFINITIONS.start_no_design_system_workflow.description, + argsSchema: { + feature_intent: z.string().optional(), + }, + }, + async (args) => createPromptResult("start_no_design_system_workflow", args), + ); + server.registerPrompt( "refine_design_first_pass", { @@ -189,7 +200,9 @@ export function createJudgmentKitMcpServer() { return server; } -export function getMcpMetadata(transport: "stdio" | "streamable-http") { +export function getMcpMetadata( + transport: "stdio" | "streamable-http" | "local-loopback-http", +) { return { name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION, diff --git a/lib/product-surface.ts b/lib/product-surface.ts index ac2382a..ad6ea3f 100644 --- a/lib/product-surface.ts +++ b/lib/product-surface.ts @@ -64,6 +64,8 @@ const workflowArtifactSchema = z.object({ common_guardrails: z.array(z.string()), links: z.object({ example_ids: z.array(z.string()), + constraint_pack_ids: z.array(z.string()).default([]), + guideline_profile_ids: z.array(z.string()).default([]), }), }); @@ -86,6 +88,8 @@ const resourceIndexSchema = z.object({ const INSPECT_CATEGORY_LABELS = { workflow: "Workflows", example: "Examples", + constraint_pack: "Constraint packs", + guideline_profile: "Guideline profiles", guardrail: "Guardrails", } as const; @@ -105,7 +109,7 @@ function createHomepageInstallCommand() { } function createHomepageVerifyPrompt() { - return "Call MCP tools/list against the local judgmentkit server"; + return "Start the local JudgmentKit loopback server, then call MCP tools/list against http://127.0.0.1:8765/mcp"; } function createPublishedInspectId(url: string) { @@ -135,7 +139,7 @@ function createResourcePromptText(resource: { switch (resource.type) { case "workflow": return `Use JudgmentKit workflow "${resource.title}" for this task. -Retrieve the linked guardrails and examples, then guide the first pass. +Retrieve the linked guardrails, constraint packs, guideline profiles, and examples, then guide the first pass. Task: [paste your request here]`; @@ -145,6 +149,18 @@ Point out where it drifts, explain why, then rewrite it inside the guardrail. Draft: [paste your draft here]`; + case "constraint_pack": + return `Use JudgmentKit constraint pack "${resource.title}" as the authority for this task. +Map the surface to the published primitives, tokens, states, and handoff contract before drafting output. + +Task: +[paste your request here]`; + case "guideline_profile": + return `Use JudgmentKit guideline profile "${resource.title}" as normative rules for this task. +Apply the published rules directly and do not exceed the profile's intended scope. + +Task: +[paste your request here]`; case "example": return `Use JudgmentKit example "${resource.title}" as calibration for this task. Compare the raw output to the corrected output, then help me prompt the next pass. @@ -162,15 +178,15 @@ Task: function createPublishedPromptText(link: ProductSurfaceReferenceLink) { switch (link.url) { case "/install": - return "Use this when you want the hosted bootstrap script that clones JudgmentKit, installs dependencies, and delegates to the repo-local installer."; + return "Use this when you want the hosted bootstrap script that clones JudgmentKit, installs dependencies, and configures the client for the local loopback MCP endpoint."; case "/mcp-inventory.json": return "Use this when you want the published command inventory and inspect anchors. It is the fastest way to verify which tools and prompts the deployed JudgmentKit surface exposes."; case "/llms.txt": return "Use this when you want the machine-readable discovery listing for the public site and its published artifacts."; case "/resources/index.json": - return "Use this when you want the canonical published index of workflows, guardrails, examples, and schemas before drilling into a specific artifact."; + return "Use this when you want the canonical published index of workflows, guardrails, constraint packs, guideline profiles, examples, and schemas before drilling into a specific artifact."; case "/mcp": - return "Use this when you need the hosted MCP metadata/debug surface, not the local install target. It is for inspecting the published route contract and parity with the inventory."; + return "Use this when you need the hosted MCP metadata/debug surface, not the local loopback install target. It is for inspecting the published route contract and parity with the inventory."; default: break; } @@ -245,6 +261,48 @@ function createInspectPrimaryItems( raw_format: "json", })); + const constraintPacks = resources + .filter((resource) => resource.type === "constraint_pack") + .sort((left, right) => left.title.localeCompare(right.title)) + .map((resource) => ({ + id: resource.id, + category: INSPECT_CATEGORY_LABELS.constraint_pack, + type: resource.type, + version: resource.version, + title: resource.title, + summary: resource.summary, + subtitle: resource.id, + url: toRelativeUrl(resource.url), + schema_url: toRelativeUrl(resource.schema_url), + last_reviewed: resource.last_reviewed, + tags: resource.tags, + available_view_modes: ["prompt", "json", "schema"], + default_view_mode: "prompt", + prompt_text: createResourcePromptText(resource), + raw_format: "json", + })); + + const guidelineProfiles = resources + .filter((resource) => resource.type === "guideline_profile") + .sort((left, right) => left.title.localeCompare(right.title)) + .map((resource) => ({ + id: resource.id, + category: INSPECT_CATEGORY_LABELS.guideline_profile, + type: resource.type, + version: resource.version, + title: resource.title, + summary: resource.summary, + subtitle: resource.id, + url: toRelativeUrl(resource.url), + schema_url: toRelativeUrl(resource.schema_url), + last_reviewed: resource.last_reviewed, + tags: resource.tags, + available_view_modes: ["prompt", "json", "schema"], + default_view_mode: "prompt", + prompt_text: createResourcePromptText(resource), + raw_format: "json", + })); + const guardrails = resources .filter((resource) => resource.type === "guardrail") .sort((left, right) => left.title.localeCompare(right.title)) @@ -266,7 +324,13 @@ function createInspectPrimaryItems( raw_format: "json", })); - return [...examples, ...workflows, ...guardrails]; + return [ + ...examples, + ...workflows, + ...constraintPacks, + ...guidelineProfiles, + ...guardrails, + ]; } function createInspectReferenceItems(referenceLinks: ProductSurfaceReferenceLink[]) { @@ -295,6 +359,8 @@ export function loadProductSurface(): ProductSurfaceContent { const loadedContextIds = [ workflowArtifact.id, + ...workflowArtifact.links.constraint_pack_ids, + ...workflowArtifact.links.guideline_profile_ids, ...workflowArtifact.common_guardrails, ...workflowArtifact.links.example_ids, ]; diff --git a/lib/site.ts b/lib/site.ts index beecc06..4de91ca 100644 --- a/lib/site.ts +++ b/lib/site.ts @@ -15,6 +15,14 @@ import { RESOURCES_DIR, ROOT_URL, SCHEMAS_DIR, + HOSTED_MCP_REFERENCE_URL, + LOCAL_JUDGMENTKIT_MCP_LOCAL_COMMAND, + LOCAL_MCP_DEFAULT_HOST, + LOCAL_MCP_DEFAULT_PORT, + LOCAL_MCP_DEFAULT_URL, + LOCAL_MCP_ENDPOINT_PATH, + LOCAL_MCP_HOST_ENV, + LOCAL_MCP_PORT_ENV, } from "@/lib/constants"; import { loadChangelogEntries, loadDocPages, loadResources, loadSchemas } from "@/lib/content"; import { createMirrorContent } from "@/lib/markdown"; @@ -65,6 +73,7 @@ function collectResourceTags(resource: Record) { ...(Array.isArray(resource.guardrail_ids) ? resource.guardrail_ids : []), ...(Array.isArray(resource.common_guardrails) ? resource.common_guardrails : []), ...(Array.isArray(resource.workflows) ? resource.workflows : []), + ...(typeof resource.workflow_id === "string" ? [resource.workflow_id] : []), ] .map((value) => String(value)) .filter(Boolean); @@ -180,7 +189,7 @@ function createLlmsText(pages: DocPage[], resourceIndex: ResourceIndex) { JudgmentKit makes AI decisions visible, measurable, and usable at runtime. -JudgmentKit is an MCP-first product. Humans use the run surface and inspect surface to connect and verify the system. Agents install the local JudgmentKit server over stdio, then use the published Markdown mirrors, JSON resources, schemas, examples, and hosted reference surfaces. +JudgmentKit is an MCP-first product. Humans use the run surface and inspect surface to connect and verify the system. Agents install the local JudgmentKit server over loopback HTTP, then use the published Markdown mirrors, JSON resources, schemas, examples, and hosted reference surfaces. ## Overview - ${ROOT_URL}/ @@ -205,7 +214,7 @@ ${schemaUrls.map((url) => `- ${url}`).join("\n")} ${examplePages.map((url) => `- ${url}`).join("\n")} ## MCP -Install uses a local stdio checkout. The hosted endpoint below is for reference, debug, and parity with the published inventory. +Install uses a local loopback HTTP checkout at ${LOCAL_MCP_DEFAULT_URL}. The hosted endpoint below is for reference, debug, and parity with the published inventory. - ${ROOT_URL}/mcp - ${ROOT_URL}/mcp-inventory.json @@ -221,10 +230,21 @@ function createMcpInventory(resourceIndex: ResourceIndex) { return { version: "1.0.0", - endpoint: `${ROOT_URL}/mcp`, - install_transport: "stdio", + endpoint: LOCAL_MCP_DEFAULT_URL, + hosted_reference_endpoint: HOSTED_MCP_REFERENCE_URL, + install_transport: "http", + local_loopback_runtime: { + start_command: LOCAL_JUDGMENTKIT_MCP_LOCAL_COMMAND, + host: LOCAL_MCP_DEFAULT_HOST, + port: LOCAL_MCP_DEFAULT_PORT, + endpoint: LOCAL_MCP_ENDPOINT_PATH, + env_overrides: { + host: LOCAL_MCP_HOST_ENV, + port: LOCAL_MCP_PORT_ENV, + }, + }, warning: - "Install JudgmentKit from a local checkout over stdio. The hosted /mcp endpoint is for reference/debug only.", + "Install JudgmentKit from a local checkout over loopback HTTP. The hosted /mcp endpoint is for reference/debug only.", command_reference_url: createCommandReferenceUrl(ROOT_URL), tools: toolReference.map((entry) => entry.name), prompts: promptReference.map((entry) => entry.name), diff --git a/lib/types.ts b/lib/types.ts index 1c4a258..d162e8a 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -139,11 +139,33 @@ export type InstallContractRepository = { install_command: string; }; -export type InstallContractConnection = { +export type InstallContractLoopbackRuntime = { + start_command: string; + host: string; + port: number; + endpoint: string; + env_overrides: { + host: string; + port: string; + }; +}; + +export type InstallContractHttpConnection = { + transport: "http"; + url: string; + loopback_runtime: InstallContractLoopbackRuntime; +}; + +export type InstallContractStdioConnection = { + transport: "stdio"; command: string; args: string[]; }; +export type InstallContractConnection = + | InstallContractHttpConnection + | InstallContractStdioConnection; + export type InstallContractCommandReference = { name: string; description: string; @@ -155,6 +177,8 @@ export type InstallContractCommandReference = { export type InstallContractVerification = { method: "tools/list"; server_name: string; + endpoint: string; + start_command: string; instructions: string; expected_tools: string[]; expected_prompts: string[]; diff --git a/package.json b/package.json index 64bc198..2cb6005 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint .", "generate": "tsx scripts/generate-site.ts", "mcp:install": "node --import tsx ./scripts/install-mcp.ts", + "mcp:local": "node --import tsx ./scripts/judgmentkit-mcp-local.ts", "mcp:stdio": "node --import tsx ./scripts/judgmentkit-mcp-stdio.ts", "test": "vitest run", "check": "npm run generate && npm run test && npm run lint" diff --git a/scripts/install-mcp.ts b/scripts/install-mcp.ts index c45c451..2ef0fb7 100644 --- a/scripts/install-mcp.ts +++ b/scripts/install-mcp.ts @@ -15,7 +15,7 @@ main().catch((error) => { if (error instanceof InstallerError) { process.stderr.write(`JudgmentKit installer failed during ${error.phase}: ${error.message}\n`); if (error.manualSnippet) { - process.stderr.write(`Manual fallback snippet:\n${error.manualSnippet}\n`); + process.stderr.write(`Manual config snippet:\n${error.manualSnippet}\n`); } process.exit(1); } diff --git a/scripts/judgmentkit-mcp-local.ts b/scripts/judgmentkit-mcp-local.ts new file mode 100644 index 0000000..94e0beb --- /dev/null +++ b/scripts/judgmentkit-mcp-local.ts @@ -0,0 +1,132 @@ +import { createServer } from "node:http"; +import type { IncomingMessage, ServerResponse } from "node:http"; + +import { + createMcpMetadataResponse, + createMcpNotFoundResponse, + handleMcpHttpRequest, +} from "@/lib/mcp-http"; + +const DEFAULT_HOST = "127.0.0.1"; +const DEFAULT_PORT = 8765; +const MCP_ENDPOINT = "/mcp"; + +function resolveHost() { + return process.env.JUDGMENTKIT_MCP_HOST?.trim() || DEFAULT_HOST; +} + +function resolvePort() { + const value = process.env.JUDGMENTKIT_MCP_PORT ?? process.env.PORT; + const parsed = Number(value); + return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_PORT; +} + +function createRequest(req: IncomingMessage, origin: string) { + const url = new URL(req.url || MCP_ENDPOINT, origin); + const headers = new Headers(); + + for (const [key, value] of Object.entries(req.headers)) { + if (Array.isArray(value)) { + for (const item of value) { + headers.append(key, item); + } + continue; + } + + if (typeof value === "string") { + headers.set(key, value); + } + } + + const init: RequestInit & { duplex?: "half" } = { + method: req.method, + headers, + }; + + if (!["GET", "HEAD"].includes(req.method ?? "GET")) { + init.body = req as unknown as BodyInit; + init.duplex = "half"; + } + + return new Request(url, init); +} + +async function sendResponse(res: ServerResponse, response: Response) { + res.statusCode = response.status; + res.statusMessage = response.statusText; + response.headers.forEach((value, key) => { + res.setHeader(key, value); + }); + + const body = Buffer.from(await response.arrayBuffer()); + res.end(body); +} + +async function handleNodeRequest( + req: IncomingMessage, + res: ServerResponse, + origin: string, +) { + const request = createRequest(req, origin); + const pathname = new URL(request.url).pathname; + + if (pathname === "/" && request.method === "GET") { + await sendResponse( + res, + createMcpMetadataResponse("local-loopback-http", { cors: true }), + ); + return; + } + + if (pathname !== MCP_ENDPOINT) { + await sendResponse(res, createMcpNotFoundResponse()); + return; + } + + if (!["GET", "POST", "OPTIONS"].includes(request.method)) { + await sendResponse(res, createMcpNotFoundResponse("Use GET or POST /mcp.")); + return; + } + + const response = await handleMcpHttpRequest(request, { + metadataTransport: "local-loopback-http", + cors: true, + allowOptions: true, + }); + await sendResponse(res, response); +} + +const host = resolveHost(); +const port = resolvePort(); +const origin = `http://${host}:${port}`; + +const server = createServer((req, res) => { + handleNodeRequest(req, res, origin).catch((error) => { + const message = + error instanceof Error ? error.message : "Unknown local MCP error."; + sendResponse( + res, + new Response( + JSON.stringify({ + error: "internal_error", + message, + }), + { + status: 500, + headers: { + "content-type": "application/json; charset=utf-8", + }, + }, + ), + ).catch(() => { + res.statusCode = 500; + res.end(); + }); + }); +}); + +server.listen(port, host, () => { + process.stdout.write( + `JudgmentKit local MCP listening at ${origin}${MCP_ENDPOINT}\n`, + ); +}); diff --git a/tests/homepage-install-smoke.test.ts b/tests/homepage-install-smoke.test.ts index 6a8c552..c89c5a3 100644 --- a/tests/homepage-install-smoke.test.ts +++ b/tests/homepage-install-smoke.test.ts @@ -1,76 +1,39 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { describe, expect, it } from "vitest"; -import { renderManualConfigSnippet } from "@/lib/install-mcp"; +import { renderManualConfigSnippet, verifyInstalledMcp } from "@/lib/install-mcp"; import { loadInstallContract } from "@/lib/install-contract"; import { loadLandingPage } from "@/lib/landing-page"; import type { InstallContract, InstallContractClient } from "@/lib/types"; -function withTimeout(promise: Promise, timeoutMs: number) { - return Promise.race([ - promise, - new Promise((_, reject) => { - setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms.`)), timeoutMs); - }), - ]); -} - -function materializeLocalPath(value: string, contract: InstallContract) { - return value.replaceAll( - contract.repository.local_path_placeholder, - process.cwd(), - ); -} - -function materializeConnection(contract: InstallContract) { - return { - command: contract.connection.command, - args: contract.connection.args.map((argument) => - materializeLocalPath(argument, contract), - ), - }; -} - function parseCodexConfigSnippet(snippet: string) { if (!snippet.includes("[mcp_servers.judgmentkit]")) { throw new Error("Codex install snippet is missing the judgmentkit server block."); } - const commandMatch = snippet.match(/^\s*command\s*=\s*"([^"]+)"\s*$/m); - const argsMatch = snippet.match(/^\s*args\s*=\s*(\[[^\n]+\])\s*$/m); + const urlMatch = snippet.match(/^\s*url\s*=\s*"([^"]+)"\s*$/m); - if (!commandMatch || !argsMatch) { - throw new Error("Codex install snippet is missing a command or args assignment."); + if (!urlMatch) { + throw new Error("Codex install snippet is missing a URL assignment."); } - const args = JSON.parse(argsMatch[1]) as string[]; - - return { - command: commandMatch[1], - args, - }; + return { url: urlMatch[1] }; } function parseJsonConfigSnippet(snippet: string) { const parsed = JSON.parse(snippet) as { mcpServers?: { judgmentkit?: { - command?: string; - args?: string[]; + url?: string; }; }; }; const serverConfig = parsed.mcpServers?.judgmentkit; - if (!serverConfig?.command || !Array.isArray(serverConfig.args)) { - throw new Error("JSON install snippet is missing mcpServers.judgmentkit command/args."); + if (!serverConfig?.url) { + throw new Error("JSON install snippet is missing mcpServers.judgmentkit url."); } - return { - command: serverConfig.command, - args: serverConfig.args, - }; + return { url: serverConfig.url }; } function parseClientConnection(clientConfig: InstallContractClient) { @@ -83,21 +46,6 @@ function parseClientConnection(clientConfig: InstallContractClient) { return parseJsonConfigSnippet(materializedSnippet); } -function createFailure( - clientId: string, - command: string, - args: string[], - stderrOutput: string, - error: unknown, -) { - const reason = error instanceof Error ? error.message : String(error); - const stderr = stderrOutput.trim() || ""; - - return new Error( - `Homepage install smoke failed for ${clientId}.\nCommand: ${command} ${args.join(" ")}\nReason: ${reason}\nStderr:\n${stderr}`, - ); -} - function loadInternalInstallContract(): InstallContract { return loadInstallContract(); } @@ -106,58 +54,13 @@ async function verifyClientInstall( contract: InstallContract, clientConfig: InstallContractClient, ) { - const configuredConnection = parseClientConnection(clientConfig); - const expectedConnection = materializeConnection(contract); - - expect(configuredConnection).toEqual(expectedConnection); - - const stderrOutput: string[] = []; - const transport = new StdioClientTransport({ - command: configuredConnection.command, - args: configuredConnection.args, - cwd: process.cwd(), - stderr: "pipe", - }); - const client = new Client({ - name: `homepage-install-smoke-${clientConfig.id}`, - version: "1.0.0", - }); - - transport.stderr?.on("data", (chunk: Buffer | string) => { - stderrOutput.push(chunk.toString()); - }); - - try { - await withTimeout(client.connect(transport), 5_000); - - const toolsResponse = await withTimeout(client.listTools(), 5_000); - expect(toolsResponse.tools.map((tool) => tool.name)).toEqual( - contract.verification.expected_tools, - ); - - const promptsResponse = await withTimeout(client.listPrompts(), 5_000); - expect(promptsResponse.prompts.map((prompt) => prompt.name)).toEqual( - contract.verification.expected_prompts, - ); + if (contract.connection.transport !== "http") { + throw new Error("Expected HTTP install contract."); + } - const promptResponse = await withTimeout( - client.getPrompt({ name: "start_design_workflow", arguments: {} }), - 5_000, - ); + const configuredConnection = parseClientConnection(clientConfig); - expect(promptResponse.messages.length).toBeGreaterThan(0); - expect(promptResponse.messages[0]?.content.type).toBe("text"); - } catch (error) { - throw createFailure( - clientConfig.id, - configuredConnection.command, - configuredConnection.args, - stderrOutput.join(""), - error, - ); - } finally { - await transport.close(); - } + expect(configuredConnection).toEqual({ url: contract.connection.url }); } describe("homepage install smoke", () => { @@ -182,7 +85,7 @@ describe("homepage install smoke", () => { "curl -fsSL https://judgmentkit.ai/install | bash -s -- --client ", ); expect(content.verify_prompt).toBe( - "Call MCP tools/list against the local judgmentkit server", + "Start the local JudgmentKit loopback server, then call MCP tools/list against http://127.0.0.1:8765/mcp", ); }); @@ -196,5 +99,15 @@ describe("homepage install smoke", () => { for (const clientConfig of contract.clients) { await verifyClientInstall(contract, clientConfig); } + + if (contract.connection.transport !== "http") { + throw new Error("Expected HTTP install contract."); + } + + await verifyInstalledMcp(process.cwd(), { + endpoint: contract.connection.url, + host: contract.connection.loopback_runtime.host, + port: contract.connection.loopback_runtime.port, + }); }); }); diff --git a/tests/install-script.test.ts b/tests/install-script.test.ts index 03db43e..019bd1e 100644 --- a/tests/install-script.test.ts +++ b/tests/install-script.test.ts @@ -98,12 +98,11 @@ describe("install script", () => { const next = upsertCodexTomlConfig( existing, `[mcp_servers.judgmentkit] -command = "npm" -args = ["--prefix", "/tmp/new-judgmentkit", "run", "mcp:stdio"]`, +url = "http://127.0.0.1:18765/mcp"`, ); expect(next).toContain('[mcp_servers.other]'); - expect(next).toContain('/tmp/new-judgmentkit'); + expect(next).toContain('url = "http://127.0.0.1:18765/mcp"'); expect(next).not.toContain('/tmp/old-judgmentkit'); expect(next).toContain('[projects.demo]'); }); @@ -113,17 +112,16 @@ args = ["--prefix", "/tmp/new-judgmentkit", "run", "mcp:stdio"]`, const cursorExisting = await loadFixture("cursor-existing.json"); const claudeNext = upsertJsonMcpConfig(claudeExisting, { - command: "npm", - args: ["--prefix", "/tmp/judgmentkit", "run", "mcp:stdio"], + url: "http://127.0.0.1:18766/mcp", }); const cursorNext = upsertJsonMcpConfig(cursorExisting, { - command: "npm", - args: ["--prefix", "/tmp/judgmentkit", "run", "mcp:stdio"], + url: "http://127.0.0.1:18767/mcp", }); expect(JSON.parse(claudeNext).mcpServers.other.command).toBe("node"); - expect(JSON.parse(claudeNext).mcpServers.judgmentkit.args[1]).toBe("/tmp/judgmentkit"); + expect(JSON.parse(claudeNext).mcpServers.judgmentkit.url).toBe("http://127.0.0.1:18766/mcp"); expect(JSON.parse(cursorNext).mcpServers.other.command).toBe("node"); + expect(JSON.parse(cursorNext).mcpServers.judgmentkit.url).toBe("http://127.0.0.1:18767/mcp"); expect(JSON.parse(cursorNext).theme).toBe("dark"); }); @@ -157,12 +155,14 @@ args = ["--prefix", "/tmp/new-judgmentkit", "run", "mcp:stdio"]`, const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "judgmentkit-workspace-")); const clients = ["codex", "claude", "cursor"] as const; - for (const client of clients) { + for (const [index, client] of clients.entries()) { + const port = 18_765 + index; const result = await installJudgmentKitMcp( { client, checkoutPath: process.cwd(), cwd: workspaceDir, + port, }, { homeDir: () => homeDir, @@ -172,10 +172,13 @@ args = ["--prefix", "/tmp/new-judgmentkit", "run", "mcp:stdio"]`, expect(result.wroteConfig).toBe(true); expect(result.verified).toBe(true); - expect(formatInstallerResult(result)).toContain("Manual fallback snippet:"); + expect(result.endpoint).toBe(`http://127.0.0.1:${port}/mcp`); + expect(result.startCommand).toBe(`npm --prefix ${process.cwd()} run mcp:local`); + expect(result.bridgeFallbackSnippet).toContain("mcp-remote"); + expect(formatInstallerResult(result)).toContain("Bridge fallback snippet"); const writtenConfig = await fs.readFile(result.configPath, "utf8"); expect(writtenConfig).toContain("judgmentkit"); - expect(writtenConfig).toContain(process.cwd()); + expect(writtenConfig).toContain(`http://127.0.0.1:${port}/mcp`); } }); @@ -199,8 +202,11 @@ args = ["--prefix", "/tmp/new-judgmentkit", "run", "mcp:stdio"]`, expect(stderr).toBe(""); expect(stdout).toContain("JudgmentKit installer prepared client: codex"); expect(stdout).toContain(`Checkout path: ${path.join(homeDir, "judgmentkit")}`); + expect(stdout).toContain("Endpoint: http://127.0.0.1:8765/mcp"); + expect(stdout).toContain(`Start local MCP: npm --prefix ${path.join(homeDir, "judgmentkit")} run mcp:local`); expect(stdout).toContain("Mode: dry-run/manual"); - expect(stdout).toContain("Manual fallback snippet:"); + expect(stdout).toContain("Config snippet:"); + expect(stdout).toContain("Bridge fallback snippet"); expect(stdout).not.toContain("MODULE_NOT_FOUND"); }); }); diff --git a/tests/landing-page.test.ts b/tests/landing-page.test.ts index 2b21668..165c0da 100644 --- a/tests/landing-page.test.ts +++ b/tests/landing-page.test.ts @@ -34,7 +34,7 @@ describe("landing page", () => { "curl -fsSL https://judgmentkit.ai/install | bash -s -- --client ", ); expect(content.verify_prompt).toBe( - "Call MCP tools/list against the local judgmentkit server", + "Start the local JudgmentKit loopback server, then call MCP tools/list against http://127.0.0.1:8765/mcp", ); }); @@ -60,7 +60,8 @@ describe("landing page", () => { expect(markup).toContain( "curl -fsSL https://judgmentkit.ai/install | bash -s -- --client codex", ); - expect(markup).toContain("Call MCP tools/list against the local judgmentkit server"); + expect(markup).toContain("Start the local JudgmentKit loopback server"); + expect(markup).toContain("http://127.0.0.1:8765/mcp"); expect(markup).not.toContain("Manual fallback"); expect(markup).not.toContain("through MCP"); expect(markup).not.toContain("~/.codex/config.toml"); diff --git a/tests/mcp-local.test.ts b/tests/mcp-local.test.ts new file mode 100644 index 0000000..7ed8b6b --- /dev/null +++ b/tests/mcp-local.test.ts @@ -0,0 +1,99 @@ +import { spawn } from "node:child_process"; + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { describe, expect, it } from "vitest"; + +import { listTools } from "@/lib/mcp"; + +function withTimeout(promise: Promise, timeoutMs: number) { + return Promise.race([ + promise, + new Promise((_, reject) => { + setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms.`)), timeoutMs); + }), + ]); +} + +async function waitForMetadata(url: string) { + const startedAt = Date.now(); + let lastError: unknown; + + while (Date.now() - startedAt < 10_000) { + try { + const response = await fetch(url); + if (response.ok) { + return; + } + lastError = new Error(`Metadata returned ${response.status}.`); + } catch (error) { + lastError = error; + } + + await new Promise((resolve) => setTimeout(resolve, 250)); + } + + throw lastError instanceof Error ? lastError : new Error(String(lastError)); +} + +describe("local loopback MCP server", () => { + it("serves metadata, Streamable HTTP tools/list, and local 404s", async () => { + const port = 19_765 + Math.floor(Math.random() * 1_000); + const endpoint = `http://127.0.0.1:${port}/mcp`; + const root = `http://127.0.0.1:${port}/`; + const stderrOutput: string[] = []; + + const child = spawn("npm", ["--prefix", process.cwd(), "run", "mcp:local"], { + cwd: process.cwd(), + env: { + ...process.env, + JUDGMENTKIT_MCP_HOST: "127.0.0.1", + JUDGMENTKIT_MCP_PORT: String(port), + }, + stdio: ["ignore", "ignore", "pipe"], + }); + + child.stderr.on("data", (chunk: Buffer | string) => { + stderrOutput.push(chunk.toString()); + }); + + try { + await waitForMetadata(root); + + const rootMetadata = await fetch(root).then((response) => response.json()); + expect(rootMetadata.transport).toBe("local-loopback-http"); + + const mcpMetadata = await fetch(endpoint, { + headers: { + accept: "application/json", + }, + }).then((response) => response.json()); + expect(mcpMetadata.transport).toBe("local-loopback-http"); + + const missing = await fetch(`http://127.0.0.1:${port}/wrong`); + expect(missing.status).toBe(404); + + const transport = new StreamableHTTPClientTransport(new URL(endpoint)); + const client = new Client({ + name: "judgmentkit-local-loopback-test", + version: "1.0.0", + }); + + try { + await withTimeout(client.connect(transport), 5_000); + const tools = await withTimeout(client.listTools(), 5_000); + expect(tools.tools.map((tool) => tool.name)).toEqual( + listTools().map((tool) => tool.name), + ); + } finally { + await transport.close(); + } + } catch (error) { + throw new Error( + `Local loopback MCP test failed: ${String(error)}\n${stderrOutput.join("")}`, + ); + } finally { + child.kill(); + } + }); +}); diff --git a/tests/product-surface.test.ts b/tests/product-surface.test.ts index e260485..6bfeccd 100644 --- a/tests/product-surface.test.ts +++ b/tests/product-surface.test.ts @@ -17,7 +17,8 @@ import { LOCAL_JUDGMENTKIT_CHECKOUT_PLACEHOLDER, LOCAL_JUDGMENTKIT_INSTALL_COMMAND, LOCAL_JUDGMENTKIT_INSTALLER_COMMAND, - LOCAL_JUDGMENTKIT_STDIO_ARGS, + LOCAL_JUDGMENTKIT_MCP_LOCAL_COMMAND, + LOCAL_MCP_DEFAULT_URL, } from "@/lib/constants"; import { loadInstallContract } from "@/lib/install-contract"; import { listPrompts, listTools } from "@/lib/mcp"; @@ -25,7 +26,7 @@ import { loadProductSurface } from "@/lib/product-surface"; import rawExampleArtifact from "@/public/resources/examples/ui-generation-drift.v1.json"; describe("product surface content", () => { - it("defines stdio install targets and the derived loaded context", () => { + it("defines HTTP install targets and the derived loaded context", () => { const content = loadProductSurface(); expect(content.install_targets.map((target) => target.id)).toEqual([ @@ -52,7 +53,15 @@ describe("product surface content", () => { ]); expect(content.loaded_context.map((item) => item.id)).toEqual([ "workflow.ai-ui-generation", + "constraint-pack.ai-ui-no-design-system", + "guideline-profile.ai-ui-generation-authority", + "guideline-profile.ai-ui-review-checks", "guardrail.design-system-integrity", + "guardrail.spec-completeness", + "guardrail.surface-mode-structure", + "guardrail.visual-planning-contract", + "guardrail.motion-media-purpose", + "guardrail.frontend-output-contract", "guardrail.ui-copy-clarity", "guardrail.control-proximity", "guardrail.surface-theme-parity", @@ -60,10 +69,23 @@ describe("product surface content", () => { "guardrail.provenance-escalation", "example.ui-generation.component-drift", "example.ui-generation.embellishment-drift", + "example.ui-generation.mode-structure-drift", + "example.ui-generation.visual-planning-gap", + "example.ui-generation.motion-media-drift", + "example.ui-generation.output-contract-gap", "example.ui-generation.onboarding-clarity-drift", "example.ui-generation.repetitive-copy-drift", "example.ui-generation.control-proximity-drift", "example.ui-generation.surface-theme-parity-drift", + "example.ui-generation.token-vagueness-drift", + "example.ui-generation.primitive-sprawl-drift", + "example.ui-generation.shallow-handoff-drift", + "example.ui-generation.state-coverage-drift", + "example.ui-generation.component-mapping-name-only-drift", + "example.ui-generation.non-reusable-recipe-drift", + "example.ui-generation.missing-accessibility-api-drift", + "example.ui-generation.hand-authored-preview-drift", + "example.ui-generation.theme-binding-recipe-drift", ]); }); @@ -72,7 +94,7 @@ describe("product surface content", () => { expect(content.install_command).toBe(HOSTED_JUDGMENTKIT_BOOTSTRAP_COMMAND); expect(content.verify_prompt).toBe( - "Call MCP tools/list against the local judgmentkit server", + "Start the local JudgmentKit loopback server, then call MCP tools/list against http://127.0.0.1:8765/mcp", ); }); @@ -91,8 +113,18 @@ describe("product surface content", () => { install_command: LOCAL_JUDGMENTKIT_INSTALL_COMMAND, }); expect(contract.connection).toEqual({ - command: "npm", - args: LOCAL_JUDGMENTKIT_STDIO_ARGS, + transport: "http", + url: LOCAL_MCP_DEFAULT_URL, + loopback_runtime: { + start_command: LOCAL_JUDGMENTKIT_MCP_LOCAL_COMMAND, + host: "127.0.0.1", + port: 8765, + endpoint: "/mcp", + env_overrides: { + host: "JUDGMENTKIT_MCP_HOST", + port: "JUDGMENTKIT_MCP_PORT", + }, + }, }); expect(contract.supported_clients).toEqual(["codex", "claude", "cursor"]); expect(contract.clients).toEqual([ @@ -117,9 +149,12 @@ describe("product surface content", () => { ]); expect(contract.clients[0]).not.toHaveProperty("config_snippet"); expect(contract.clients[0]).not.toHaveProperty("install_note"); - expect(contract.clients[0]).not.toHaveProperty("transport"); expect(contract.verification.method).toBe("tools/list"); expect(contract.verification.server_name).toBe("judgmentkit"); + expect(contract.verification.endpoint).toBe(LOCAL_MCP_DEFAULT_URL); + expect(contract.verification.start_command).toBe( + LOCAL_JUDGMENTKIT_MCP_LOCAL_COMMAND, + ); expect(contract.verification.instructions).toContain("tools/list"); expect(contract.verification.expected_tools).toEqual( listTools().map((tool) => tool.name), @@ -176,6 +211,12 @@ describe("product surface content", () => { const exampleItem = content.inspect_primary_items.find( (item) => item.id === "example.ui-generation.embellishment-drift", ); + const constraintPackItem = content.inspect_primary_items.find( + (item) => item.id === "constraint-pack.ai-ui-no-design-system", + ); + const guidelineProfileItem = content.inspect_primary_items.find( + (item) => item.id === "guideline-profile.ai-ui-generation-authority", + ); const installScriptItem = content.inspect_reference_items.find((item) => item.url === "/install"); expect(content.inspect_primary_items[0]?.id).toBe( @@ -188,6 +229,22 @@ describe("product surface content", () => { }); expect(workflowItem?.prompt_text).toContain('Use JudgmentKit workflow "AI UI generation"'); expect(workflowItem?.prompt_text).toContain("Task:"); + expect(constraintPackItem).toMatchObject({ + category: "Constraint packs", + available_view_modes: ["prompt", "json", "schema"], + default_view_mode: "prompt", + }); + expect(constraintPackItem?.prompt_text).toContain( + 'Use JudgmentKit constraint pack "Portable no-design-system implementation authority"', + ); + expect(guidelineProfileItem).toMatchObject({ + category: "Guideline profiles", + available_view_modes: ["prompt", "json", "schema"], + default_view_mode: "prompt", + }); + expect(guidelineProfileItem?.prompt_text).toContain( + 'Use JudgmentKit guideline profile "AI UI generation authority rules"', + ); expect(guardrailItem?.prompt_text).toContain("Apply JudgmentKit guardrail"); expect(guardrailItem?.prompt_text).toContain("Draft:"); expect(exampleItem?.prompt_text).toContain("Use JudgmentKit example"); @@ -219,6 +276,8 @@ describe("product surface content", () => { expect(markup).toContain("inspect-browser-shell"); expect(markup).toContain(">Examples<"); expect(markup).toContain(">Workflows<"); + expect(markup).toContain(">Constraint packs<"); + expect(markup).toContain(">Guideline profiles<"); expect(markup).toContain(">Guardrails<"); expect(markup).toContain("Use JudgmentKit example"); expect(markup).toContain("inspect-viewer-toolbar"); @@ -251,6 +310,9 @@ describe("product surface content", () => { expect(markup).not.toContain("Command inventory"); expect(markup.indexOf(">Examples<")).toBeLessThan(markup.indexOf(">Workflows<")); expect(markup.indexOf(">Examples<")).toBeLessThan(markup.indexOf(">Guardrails<")); + expect(markup.indexOf(">Workflows<")).toBeLessThan(markup.indexOf(">Constraint packs<")); + expect(markup.indexOf(">Constraint packs<")).toBeLessThan(markup.indexOf(">Guideline profiles<")); + expect(markup.indexOf(">Guideline profiles<")).toBeLessThan(markup.indexOf(">Guardrails<")); expect(markup.indexOf("Zero-shot UI generation rewritten to design-system-first restrained output")).toBeLessThan( markup.indexOf("Landing page first pass rewritten for clearer onboarding"), ); diff --git a/tests/site-build.test.ts b/tests/site-build.test.ts index e3a0c96..397effb 100644 --- a/tests/site-build.test.ts +++ b/tests/site-build.test.ts @@ -6,14 +6,28 @@ describe("site build data", () => { it("builds the expected MVP corpus", async () => { const site = await buildSiteData(); - expect(site.pages).toHaveLength(23); - expect(site.resourceIndex.resources).toHaveLength(18); - expect(site.resourceIndex.schemas).toHaveLength(5); + expect(site.pages.length).toBeGreaterThanOrEqual(29); + expect(site.resourceIndex.resources.length).toBeGreaterThanOrEqual(24); + expect(site.resourceIndex.schemas.length).toBeGreaterThanOrEqual(6); expect(site.resourceIndex.resources[0]?.url).toMatch(/^https:\/\/judgmentkit\.ai\//); expect(site.resourceIndex.schemas[0]?.url).toMatch(/^https:\/\/judgmentkit\.ai\//); + expect( + site.resourceIndex.resources.some( + (resource) => resource.id === "guideline-profile.ai-ui-generation-authority", + ), + ).toBe(true); + expect( + site.resourceIndex.schemas.some((schema) => + schema.url.endsWith("/schemas/guideline_profile.schema.json"), + ), + ).toBe(true); expect(site.llms).toContain("https://judgmentkit.ai/"); - expect(site.mcpInventory.endpoint).toBe("https://judgmentkit.ai/mcp"); - expect(site.mcpInventory.install_transport).toBe("stdio"); + expect(site.mcpInventory.endpoint).toBe("http://127.0.0.1:8765/mcp"); + expect(site.mcpInventory.hosted_reference_endpoint).toBe("https://judgmentkit.ai/mcp"); + expect(site.mcpInventory.install_transport).toBe("http"); + expect(site.mcpInventory.local_loopback_runtime.start_command).toBe( + "npm --prefix run mcp:local", + ); expect(site.mcpInventory.command_reference_url).toBe( "https://judgmentkit.ai/inspect#commands", ); @@ -33,7 +47,7 @@ describe("site build data", () => { (page) => page.frontmatter.page_type === "guardrail", ); - expect(guardrails).toHaveLength(8); + expect(guardrails).toHaveLength(13); for (const page of guardrails) { expect(page.frontmatter.workflows?.length).toBeGreaterThan(0); expect(page.frontmatter.related_resources.length).toBeGreaterThan(0); From 2848fcc6f350a84528fd402c414101f88c02def8 Mon Sep 17 00:00:00 2001 From: Mike Long Date: Wed, 22 Apr 2026 21:44:00 -0700 Subject: [PATCH 2/3] Fold frontend visual judgment into AI UI workflow --- .../docs/examples/mode-structure-drift.mdx | 65 +++ content/docs/examples/motion-media-drift.mdx | 64 +++ content/docs/examples/output-contract-gap.mdx | 65 +++ .../docs/examples/primitive-sprawl-drift.mdx | 66 +++ .../docs/examples/shallow-handoff-drift.mdx | 65 +++ .../docs/examples/state-coverage-drift.mdx | 66 +++ .../docs/examples/token-vagueness-drift.mdx | 66 +++ content/docs/examples/visual-planning-gap.mdx | 64 +++ .../guardrails/frontend-output-contract.mdx | 69 +++ .../docs/guardrails/motion-media-purpose.mdx | 71 +++ content/docs/guardrails/spec-completeness.mdx | 84 +++ .../guardrails/surface-mode-structure.mdx | 69 +++ .../guardrails/visual-planning-contract.mdx | 69 +++ .../portable-no-design-system-pack.mdx | 147 ++++++ content/docs/workflows/ai-ui-generation.mdx | 138 ++++- .../ai-ui-no-design-system.v1.json | 482 ++++++++++++++++++ .../component-mapping-name-only-drift.v1.json | 47 ++ .../hand-authored-preview-drift.v1.json | 47 ++ .../missing-accessibility-api-drift.v1.json | 46 ++ .../examples/mode-structure-drift.v1.json | 42 ++ .../examples/motion-media-drift.v1.json | 42 ++ .../non-reusable-recipe-drift.v1.json | 45 ++ .../examples/output-contract-gap.v1.json | 42 ++ .../examples/primitive-sprawl-drift.v1.json | 47 ++ .../examples/shallow-handoff-drift.v1.json | 45 ++ .../examples/state-coverage-drift.v1.json | 47 ++ .../theme-binding-recipe-drift.v1.json | 47 ++ .../examples/token-vagueness-drift.v1.json | 47 ++ .../examples/visual-planning-gap.v1.json | 42 ++ .../frontend-output-contract.v1.json | 79 +++ .../guardrails/motion-media-purpose.v1.json | 79 +++ .../guardrails/spec-completeness.v1.json | 85 +++ .../guardrails/surface-mode-structure.v1.json | 79 +++ .../visual-planning-contract.v1.json | 79 +++ .../ai-ui-generation-authority.v1.json | 83 +++ .../ai-ui-review-checks.v1.json | 65 +++ .../workflows/ai-ui-generation.v1.json | 61 ++- content/schemas/constraint_pack.schema.json | 450 ++++++++++++++++ content/schemas/guideline_profile.schema.json | 112 ++++ content/schemas/workflow.schema.json | 12 + lib/mcp.ts | 290 ++++++++++- tests/mcp-route.test.ts | 74 ++- tests/mcp.test.ts | 328 +++++++++++- 43 files changed, 4055 insertions(+), 57 deletions(-) create mode 100644 content/docs/examples/mode-structure-drift.mdx create mode 100644 content/docs/examples/motion-media-drift.mdx create mode 100644 content/docs/examples/output-contract-gap.mdx create mode 100644 content/docs/examples/primitive-sprawl-drift.mdx create mode 100644 content/docs/examples/shallow-handoff-drift.mdx create mode 100644 content/docs/examples/state-coverage-drift.mdx create mode 100644 content/docs/examples/token-vagueness-drift.mdx create mode 100644 content/docs/examples/visual-planning-gap.mdx create mode 100644 content/docs/guardrails/frontend-output-contract.mdx create mode 100644 content/docs/guardrails/motion-media-purpose.mdx create mode 100644 content/docs/guardrails/spec-completeness.mdx create mode 100644 content/docs/guardrails/surface-mode-structure.mdx create mode 100644 content/docs/guardrails/visual-planning-contract.mdx create mode 100644 content/docs/reference/portable-no-design-system-pack.mdx create mode 100644 content/resources/constraint-packs/ai-ui-no-design-system.v1.json create mode 100644 content/resources/examples/component-mapping-name-only-drift.v1.json create mode 100644 content/resources/examples/hand-authored-preview-drift.v1.json create mode 100644 content/resources/examples/missing-accessibility-api-drift.v1.json create mode 100644 content/resources/examples/mode-structure-drift.v1.json create mode 100644 content/resources/examples/motion-media-drift.v1.json create mode 100644 content/resources/examples/non-reusable-recipe-drift.v1.json create mode 100644 content/resources/examples/output-contract-gap.v1.json create mode 100644 content/resources/examples/primitive-sprawl-drift.v1.json create mode 100644 content/resources/examples/shallow-handoff-drift.v1.json create mode 100644 content/resources/examples/state-coverage-drift.v1.json create mode 100644 content/resources/examples/theme-binding-recipe-drift.v1.json create mode 100644 content/resources/examples/token-vagueness-drift.v1.json create mode 100644 content/resources/examples/visual-planning-gap.v1.json create mode 100644 content/resources/guardrails/frontend-output-contract.v1.json create mode 100644 content/resources/guardrails/motion-media-purpose.v1.json create mode 100644 content/resources/guardrails/spec-completeness.v1.json create mode 100644 content/resources/guardrails/surface-mode-structure.v1.json create mode 100644 content/resources/guardrails/visual-planning-contract.v1.json create mode 100644 content/resources/guideline-profiles/ai-ui-generation-authority.v1.json create mode 100644 content/resources/guideline-profiles/ai-ui-review-checks.v1.json create mode 100644 content/schemas/constraint_pack.schema.json create mode 100644 content/schemas/guideline_profile.schema.json diff --git a/content/docs/examples/mode-structure-drift.mdx b/content/docs/examples/mode-structure-drift.mdx new file mode 100644 index 0000000..30122af --- /dev/null +++ b/content/docs/examples/mode-structure-drift.mdx @@ -0,0 +1,65 @@ +--- +title: Mode structure drift +slug: /docs/examples/mode-structure-drift +page_type: example +summary: A product-surface request defaults to a marketing hero, then gets rewritten to lead with the working surface. +agent_summary: > + This example calibrates visually led UI generation against the surface mode + contract, especially product surfaces that drift into marketing-page + structure. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.surface-mode-structure +owners: + primary: Design Systems + risk: Product + operational: Frontend Platform +status: active +last_reviewed: 2026-04-23 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/guardrails/surface-mode-structure +related_resources: + - /resources/examples/mode-structure-drift.v1.json +related_schemas: + - /schemas/example.schema.json + - /schemas/verdict.schema.json +toc: true +--- + +## Scenario + +A team asks for a calmer review workspace where operators triage generated UI candidates and compare evidence. + +## Raw decision or output + +`Start with a premium hero explaining the AI review platform, add three benefit cards, a stat strip, and a floating dashboard preview below the fold.` + +## What JudgmentKit detected + +- the task was operational but the first viewport used marketing structure +- product proof was delayed behind generic hero, card, and stat patterns +- no single surface mode was named before layout decisions were made + +## What action was taken + +JudgmentKit selected product-surface mode and moved the working review surface into the first viewport. + +## Corrected result + +`Mode: product surface. First viewport: a triage workspace with candidate list, active preview, evidence inspector, and local decision actions. Follow with orientation copy that explains scope and freshness, then secondary context for guideline coverage and unresolved review questions.` + +## Why the correction matters + +Visually led product surfaces still need to satisfy the operator's first job before they sell the product. + +## JSON artifact links + +- Example resource: `/resources/examples/mode-structure-drift.v1.json` +- Schema: `/schemas/example.schema.json` diff --git a/content/docs/examples/motion-media-drift.mdx b/content/docs/examples/motion-media-drift.mdx new file mode 100644 index 0000000..e76da62 --- /dev/null +++ b/content/docs/examples/motion-media-drift.mdx @@ -0,0 +1,64 @@ +--- +title: Motion media drift +slug: /docs/examples/motion-media-drift +page_type: example +summary: Decorative media and motion are rewritten into product proof, restrained transitions, and reduced-motion handling. +agent_summary: > + This example calibrates visual UI generation so motion and media serve + hierarchy, affordance, or product proof rather than decoration. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.motion-media-purpose +owners: + primary: Design Systems + risk: Accessibility + operational: Frontend Platform +status: active +last_reviewed: 2026-04-23 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/guardrails/motion-media-purpose +related_resources: + - /resources/examples/motion-media-drift.v1.json +related_schemas: + - /schemas/example.schema.json + - /schemas/verdict.schema.json +toc: true +--- + +## Scenario + +A model is asked to make a launch page feel cinematic for a developer tool without supplied product imagery. + +## Raw decision or output + +`Use abstract animated blobs, parallax glow layers, a rotating carousel of screenshots, and continuous floating motion behind the headline.` + +## What JudgmentKit detected + +- media was atmospheric rather than evidence of the product, state, or workflow +- motion was continuous decoration rather than hierarchy or affordance +- reduced-motion and readability constraints were missing + +## What action was taken + +JudgmentKit pivoted to product UI as the visual anchor and downgraded motion to purposeful, reduced-motion-safe transitions. + +## Corrected result + +`Use the product UI state as the primary visual anchor. Motion plan: stagger the headline and install action once on entry, reveal the proof plane with opacity and translate, and use a small hover transition on the primary action. Honor prefers-reduced-motion by removing translate and keeping instant opacity changes.` + +## Why the correction matters + +Motion and media should carry meaning. If they do not clarify hierarchy, proof, or affordance, they add review cost and accessibility risk. + +## JSON artifact links + +- Example resource: `/resources/examples/motion-media-drift.v1.json` +- Schema: `/schemas/example.schema.json` diff --git a/content/docs/examples/output-contract-gap.mdx b/content/docs/examples/output-contract-gap.mdx new file mode 100644 index 0000000..f4a78c9 --- /dev/null +++ b/content/docs/examples/output-contract-gap.mdx @@ -0,0 +1,65 @@ +--- +title: Output contract gap +slug: /docs/examples/output-contract-gap +page_type: example +summary: A visual implementation summary omits mode, visual thesis, and motion plan, then gets completed with contract evidence. +agent_summary: > + This example shows how JudgmentKit completes visually led implementation + responses so reviewers can audit the mode, thesis, motion plan, and any + downgrade. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.frontend-output-contract +owners: + primary: Design Systems + risk: Product + operational: AI Application Developers +status: active +last_reviewed: 2026-04-23 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/guardrails/frontend-output-contract +related_resources: + - /resources/examples/output-contract-gap.v1.json +related_schemas: + - /schemas/example.schema.json + - /schemas/verdict.schema.json +toc: true +--- + +## Scenario + +An agent updates a product UI to feel calmer and more deliberate, but the final response omits the visual contract evidence. + +## Raw decision or output + +`Updated the page styling, improved spacing, and made the interface feel more polished. Tests passed.` + +## What JudgmentKit detected + +- the final response did not state the selected mode +- the final response omitted the visual thesis and motion plan +- the implementation could not be audited from the response + +## What action was taken + +JudgmentKit completed the response with the required visual output contract and disclosed the motion downgrade. + +## Corrected result + +`Updated the actual product surface to lead with the workspace, reduce card chrome, and tighten copy. Visual Thesis: calm operational clarity with strong typography and low-chrome structure. Mode: product surface. Motion Plan: hover affordance on primary actions and reduced-motion-safe opacity transitions; richer scroll motion was intentionally skipped to preserve runtime budget.` + +## Why the correction matters + +Visual implementation needs review evidence. The final answer should make the mode, thesis, motion choices, and downgrades explicit without forcing the user to reconstruct intent. + +## JSON artifact links + +- Example resource: `/resources/examples/output-contract-gap.v1.json` +- Schema: `/schemas/example.schema.json` diff --git a/content/docs/examples/primitive-sprawl-drift.mdx b/content/docs/examples/primitive-sprawl-drift.mdx new file mode 100644 index 0000000..1fd3d91 --- /dev/null +++ b/content/docs/examples/primitive-sprawl-drift.mdx @@ -0,0 +1,66 @@ +--- +title: Primitive sprawl drift +slug: /docs/examples/primitive-sprawl-drift +page_type: example +summary: A no-design-system workspace draft invents bespoke modules, then gets rewritten into the portable JudgmentKit primitive inventory. +agent_summary: > + This example shows how JudgmentKit rewrites bespoke visual modules into the + published primitive vocabulary when no external design system exists. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.design-system-integrity + - guardrail.spec-completeness +owners: + primary: Design Systems + risk: Accessibility + operational: Frontend Platform +status: active +last_reviewed: 2026-04-14 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/reference/portable-no-design-system-pack + - /docs/guardrails/design-system-integrity + - /docs/guardrails/spec-completeness +related_resources: + - /resources/examples/primitive-sprawl-drift.v1.json +related_schemas: + - /schemas/example.schema.json + - /schemas/verdict.schema.json +toc: true +--- + +## Scenario + +A model tries to make a review workspace feel premium in one pass without a real design system. + +## Raw decision or output + +`Build a floating insight ribbon, a holographic evidence capsule, a decision dock, and a metadata halo around the selected artifact.` + +## What JudgmentKit detected + +- bespoke primitives without authority +- visual modules that cannot be compared against a stable component vocabulary + +## What action was taken + +JudgmentKit rewrote the surface using the published layout shell, artifact panel, inspector, card, and button primitives. + +## Corrected result + +`Use a layout shell with a queue list on the left, a header plus artifact panel in the workspace center, and a persistent inspector on the right. Represent decision actions with buttons inside the artifact panel header instead of inventing a new decision dock primitive.` + +## Why the correction matters + +Portable governance depends on a closed primitive vocabulary. Recomposition is allowed. Primitive invention is not. + +## JSON artifact links + +- Example resource: `/resources/examples/primitive-sprawl-drift.v1.json` +- Schema: `/schemas/example.schema.json` diff --git a/content/docs/examples/shallow-handoff-drift.mdx b/content/docs/examples/shallow-handoff-drift.mdx new file mode 100644 index 0000000..9eddf3d --- /dev/null +++ b/content/docs/examples/shallow-handoff-drift.mdx @@ -0,0 +1,65 @@ +--- +title: Shallow handoff drift +slug: /docs/examples/shallow-handoff-drift +page_type: example +summary: A clean-looking UI brief omits the implementation contract, then gets rewritten into the required portable handoff sections. +agent_summary: > + This example shows how JudgmentKit rewrites aesthetic summaries into a real + token, recipe, composition, state, theme, accessibility, and escalation + handoff packet. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.spec-completeness +owners: + primary: Frontend Platform + risk: Design Systems + operational: AI Application Developers +status: active +last_reviewed: 2026-04-14 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/reference/portable-no-design-system-pack + - /docs/guardrails/spec-completeness +related_resources: + - /resources/examples/shallow-handoff-drift.v1.json +related_schemas: + - /schemas/example.schema.json + - /schemas/verdict.schema.json +toc: true +--- + +## Scenario + +A team asks for an implementation-ready export flow, but the first pass only describes how the interface should feel. + +## Raw decision or output + +`The export page should feel simple and trustworthy, with clear cards, obvious hierarchy, and a polished summary area before the final handoff action.` + +## What JudgmentKit detected + +- a visual summary with no token, component, or theme contract +- no explicit state coverage or escalation list + +## What action was taken + +JudgmentKit rewrote the handoff into the required portable sections. + +## Corrected result + +`Return core_screens, token_spec, component_recipes, screen_composition, state_coverage, theme_contract, accessibility_contract, and escalation_items. Map the summary region to card plus artifact-panel recipes, include React+Tailwind composition snippets with slots and interaction rules, define loading, empty, ready, error, review-needed, and disabled states, bind light-dark tokens explicitly, and list the export edge cases that still require review.` + +## Why the correction matters + +Portable UI authority only helps implementation when the handoff survives beyond the model run itself. + +## JSON artifact links + +- Example resource: `/resources/examples/shallow-handoff-drift.v1.json` +- Schema: `/schemas/example.schema.json` diff --git a/content/docs/examples/state-coverage-drift.mdx b/content/docs/examples/state-coverage-drift.mdx new file mode 100644 index 0000000..0e44da7 --- /dev/null +++ b/content/docs/examples/state-coverage-drift.mdx @@ -0,0 +1,66 @@ +--- +title: State coverage drift +slug: /docs/examples/state-coverage-drift +page_type: example +summary: A review flow spec names the ready state only, then gets rewritten into the required portable state matrix. +agent_summary: > + This example shows how JudgmentKit rewrites happy-path-only UI specs into a + full loading, empty, error, review-needed, and disabled state contract. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.spec-completeness + - guardrail.surface-theme-parity +owners: + primary: Frontend Platform + risk: Design Systems + operational: AI Application Developers +status: active +last_reviewed: 2026-04-14 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/reference/portable-no-design-system-pack + - /docs/guardrails/spec-completeness + - /docs/guardrails/surface-theme-parity +related_resources: + - /resources/examples/state-coverage-drift.v1.json +related_schemas: + - /schemas/example.schema.json + - /schemas/verdict.schema.json +toc: true +--- + +## Scenario + +A model proposes an artifact review flow and assumes a record is always present and valid. + +## Raw decision or output + +`Show the artifact in the center panel, add approve and request changes actions, and place the supporting notes in a right-side inspector.` + +## What JudgmentKit detected + +- ready-state-only thinking +- missing empty, error, review-needed, and disabled behavior for the artifact panel + +## What action was taken + +JudgmentKit attached the required state matrix to the local artifact panel and its decision controls. + +## Corrected result + +`State coverage: loading uses structural placeholders in the artifact panel; empty explains that no artifact is selected and offers one next action; ready shows the artifact plus adjacent decision buttons; error keeps the layout stable while exposing retry and details; review-needed adds the unresolved owner callout beside the affected artifact; disabled explains why approve or export actions are unavailable.` + +## Why the correction matters + +State coverage is part of the surface contract, not an implementation detail left for later. + +## JSON artifact links + +- Example resource: `/resources/examples/state-coverage-drift.v1.json` +- Schema: `/schemas/example.schema.json` diff --git a/content/docs/examples/token-vagueness-drift.mdx b/content/docs/examples/token-vagueness-drift.mdx new file mode 100644 index 0000000..c9094ec --- /dev/null +++ b/content/docs/examples/token-vagueness-drift.mdx @@ -0,0 +1,66 @@ +--- +title: Token vagueness drift +slug: /docs/examples/token-vagueness-drift +page_type: example +summary: A no-design-system UI draft uses stylistic adjectives instead of actual token bindings, then gets rewritten into the portable JudgmentKit contract. +agent_summary: > + This example shows how JudgmentKit rewrites vague design language into + concrete token values and light-dark bindings. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.design-system-integrity + - guardrail.spec-completeness +owners: + primary: Frontend Platform + risk: Design Systems + operational: AI Application Developers +status: active +last_reviewed: 2026-04-14 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/reference/portable-no-design-system-pack + - /docs/guardrails/design-system-integrity + - /docs/guardrails/spec-completeness +related_resources: + - /resources/examples/token-vagueness-drift.v1.json +related_schemas: + - /schemas/example.schema.json + - /schemas/verdict.schema.json +toc: true +--- + +## Scenario + +A team asks for a restrained, implementation-ready workspace UI without an external design system. + +## Raw decision or output + +`Use soft neutral surfaces, slightly darker side panels, roomy spacing, and modest rounding so the interface feels calm and premium.` + +## What JudgmentKit detected + +- token language that sounds disciplined but is not implementable +- theme guidance implied without explicit light-dark bindings + +## What action was taken + +JudgmentKit rewrote the surface into named token bindings from the portable no-design-system pack. + +## Corrected result + +`Use --jk-color-canvas (#f6f5f2 / #121315) for the page background, --jk-color-surface (#ffffff / #1b1d21) for cards and drawers, --jk-space-4 (16px) for section padding, --jk-space-5 (24px) for inter-section gaps, and --jk-radius-2 (6px) for cards, inputs, and drawers.` + +## Why the correction matters + +Portable authority breaks if reviewers and implementation teams still need to translate adjectives into actual values. + +## JSON artifact links + +- Example resource: `/resources/examples/token-vagueness-drift.v1.json` +- Schema: `/schemas/example.schema.json` diff --git a/content/docs/examples/visual-planning-gap.mdx b/content/docs/examples/visual-planning-gap.mdx new file mode 100644 index 0000000..9047fe4 --- /dev/null +++ b/content/docs/examples/visual-planning-gap.mdx @@ -0,0 +1,64 @@ +--- +title: Visual planning gap +slug: /docs/examples/visual-planning-gap +page_type: example +summary: A vague premium/modern request is rewritten into mode, visual thesis, content plan, and interaction thesis. +agent_summary: > + This example shows how JudgmentKit turns vague visual adjectives into an + actionable planning contract before implementation starts. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.visual-planning-contract +owners: + primary: Design Systems + risk: Product + operational: Frontend Platform +status: active +last_reviewed: 2026-04-23 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/guardrails/visual-planning-contract +related_resources: + - /resources/examples/visual-planning-gap.v1.json +related_schemas: + - /schemas/example.schema.json + - /schemas/verdict.schema.json +toc: true +--- + +## Scenario + +A model receives a short brief to make an existing product page feel more premium and modern. + +## Raw decision or output + +`Make the page pop with glass panels, a bold gradient background, large rounded corners, soft shadows, animated stats, and modern cards.` + +## What JudgmentKit detected + +- premium and modern were not translated into concrete hierarchy, composition, or motion decisions +- decoration was selected before a visual thesis or content plan existed +- the output could not be implemented without reinterpreting the intended direction + +## What action was taken + +JudgmentKit rewrote the answer into a compact planning contract before implementation details. + +## Corrected result + +`Mode: hybrid demo. Visual Thesis: quiet editorial confidence using strong typography, one real product proof plane, and restrained contrast. Content Plan: branded entry, product proof, one workflow detail, final install action. Interaction Thesis: a short entrance sequence for the entry text, hover reveal for the proof plane, and reduced-motion-safe opacity transitions only.` + +## Why the correction matters + +Visual adjectives do not give implementation teams enough to build or evaluate. The plan must name the actual hierarchy, composition, and motion choices. + +## JSON artifact links + +- Example resource: `/resources/examples/visual-planning-gap.v1.json` +- Schema: `/schemas/example.schema.json` diff --git a/content/docs/guardrails/frontend-output-contract.mdx b/content/docs/guardrails/frontend-output-contract.mdx new file mode 100644 index 0000000..4d21210 --- /dev/null +++ b/content/docs/guardrails/frontend-output-contract.mdx @@ -0,0 +1,69 @@ +--- +title: Frontend output contract +slug: /docs/guardrails/frontend-output-contract +page_type: guardrail +summary: Require visually led UI implementation or direction to return the selected mode, visual thesis, motion plan, and disclosed constraints. +agent_summary: > + This guardrail keeps the final response for visual UI work auditable by + requiring the implementation result or direction headings to carry the + visual contract evidence. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.frontend-output-contract +owners: + primary: Design Systems + risk: Product + operational: AI Application Developers +status: active +last_reviewed: 2026-04-23 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/examples/output-contract-gap +related_resources: + - /resources/guardrails/frontend-output-contract.v1.json +related_schemas: + - /schemas/guardrail.schema.json +toc: true +--- + +## Why this matters + +The final response is part of the handoff. If visually led work ends with "updated the styling" and no thesis, mode, or motion plan, reviewers cannot tell whether the work followed the intended contract or simply applied decorative polish. + +## What decision is being governed + +This guardrail governs whether a visually led implementation or direction-only response includes the required evidence for review. + +## What good judgment looks like + +- implementation work updates the actual UI +- implementation responses include a short summary, `Visual Thesis`, `Mode`, and `Motion Plan` +- direction-only work uses exactly `Visual Thesis`, `Structure`, `Motion Plan`, `Asset Needs`, and `Risks` +- any failed accessibility, mobile, asset, runtime, or motion check is disclosed + +## What drift looks like + +1. The final response omits the selected mode. +2. Motion is absent without a downgrade reason. +3. Direction-only output uses custom headings and hides asset needs. +4. The answer describes intent while files remain unchanged. + +## How JudgmentKit responds + +Small gaps get completed before returning. Medium gaps get reviewed for traceability. Severe gaps block the response until the required implementation evidence or direction headings are present. + +## Technical reference + +- Resource: `/resources/guardrails/frontend-output-contract.v1.json` +- Schema: `/schemas/guardrail.schema.json` + +## Related pages + +- /docs/workflows/ai-ui-generation +- /docs/examples/output-contract-gap diff --git a/content/docs/guardrails/motion-media-purpose.mdx b/content/docs/guardrails/motion-media-purpose.mdx new file mode 100644 index 0000000..2f1df6b --- /dev/null +++ b/content/docs/guardrails/motion-media-purpose.mdx @@ -0,0 +1,71 @@ +--- +title: Motion and media purpose +slug: /docs/guardrails/motion-media-purpose +page_type: guardrail +summary: Keep imagery and motion tied to narrative, hierarchy, or affordance instead of decorative load. +agent_summary: > + This guardrail makes AI UI generation justify motion and media choices and + downgrade them before they harm readability, accessibility, mobile fit, or + runtime budget. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.motion-media-purpose +owners: + primary: Design Systems + risk: Accessibility + operational: Frontend Platform +status: active +last_reviewed: 2026-04-23 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/examples/motion-media-drift +related_resources: + - /resources/guardrails/motion-media-purpose.v1.json +related_schemas: + - /schemas/guardrail.schema.json +toc: true +--- + +## Why this matters + +Media and motion can make a surface feel deliberate, but they can also become a shortcut around structure. Abstract visuals, constant movement, and carousels with no narrative purpose make generated UI harder to read, operate, and review. + +## What decision is being governed + +This guardrail governs whether media and motion have a clear job and remain bounded by accessibility, readability, mobile, and runtime constraints. + +## What good judgment looks like + +- use imagery to reveal the product, place, object, state, workflow, or narrative +- use motion for presence, hierarchy, affordance, or transition clarity +- prefer opacity and transform motion +- honor `prefers-reduced-motion` +- pivot to typography, shape, contrast, or product UI when imagery is weak + +## What drift looks like + +1. Abstract media is treated as the primary proof. +2. Motion is continuous decoration. +3. A carousel or sticky effect has no narrative job. +4. Text contrast or focus clarity depends on fragile media treatment. +5. Runtime-heavy animation appears in a constrained first pass. + +## How JudgmentKit responds + +Small drift gets downgraded to purposeful transitions and a clearer media anchor. Medium drift receives design and accessibility review. Severe drift blocks the output when media or motion undermines usability, accessibility, or product truth. + +## Technical reference + +- Resource: `/resources/guardrails/motion-media-purpose.v1.json` +- Schema: `/schemas/guardrail.schema.json` + +## Related pages + +- /docs/workflows/ai-ui-generation +- /docs/examples/motion-media-drift diff --git a/content/docs/guardrails/spec-completeness.mdx b/content/docs/guardrails/spec-completeness.mdx new file mode 100644 index 0000000..a8d3932 --- /dev/null +++ b/content/docs/guardrails/spec-completeness.mdx @@ -0,0 +1,84 @@ +--- +title: Spec completeness +slug: /docs/guardrails/spec-completeness +page_type: guardrail +summary: Require AI-generated UI output to name concrete primitives, tokens, states, and handoff details instead of relying on vague design language. +agent_summary: > + This guardrail explains how JudgmentKit blocks or rewrites underspecified UI + specs so they remain implementation-ready and comparable. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.spec-completeness +owners: + primary: Frontend Platform + risk: Design Systems + operational: AI Application Developers +status: active +last_reviewed: 2026-04-14 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/reference/portable-no-design-system-pack + - /docs/guardrails/design-system-integrity + - /docs/examples/token-vagueness-drift + - /docs/examples/primitive-sprawl-drift + - /docs/examples/shallow-handoff-drift + - /docs/examples/state-coverage-drift +related_resources: + - /resources/guardrails/spec-completeness.v1.json +related_schemas: + - /schemas/guardrail.schema.json +toc: true +--- + +## Why this matters + +A generated UI plan can sound disciplined while still forcing humans to infer the actual build. That is a cleanup cost. It also makes model-to-model comparisons unreliable because the judgment depends on whoever fills the missing pieces afterward. + +## What decision is being governed + +This guardrail governs whether a UI output is concrete enough to implement, review, or compare without hidden assumptions. + +## What good judgment looks like + +- names the concrete primitive inventory +- names exact token bindings or values +- names required light and dark theme pairs +- names loading, empty, ready, error, review-needed, and disabled states when the surface can encounter them +- carries the handoff sections needed for implementation and review + +## What drift looks like + +1. The output says clean, premium, neutral, slightly raised, or roomy instead of naming actual tokens. +2. New component wrappers appear without mapping to a published primitive inventory. +3. The happy path is specified, but error or empty states are left implicit. +4. The model claims theme completeness without naming the light and dark bindings. +5. Implementation teams receive a visual description instead of a real handoff packet. + +## How JudgmentKit responds + +Small gaps get auto-normalized into explicit tokens or sections. Medium gaps get rewritten into the full contract. Severe gaps block the spec until the missing sections are completed. + +## Boundaries + +Compact output is allowed. Vague output is not. The goal is not verbosity. The goal is enough specificity to build and judge the result without filling in hidden design decisions afterward. + +## Technical reference + +- Resource: `/resources/guardrails/spec-completeness.v1.json` +- Schema: `/schemas/guardrail.schema.json` + +## Related pages + +- /docs/workflows/ai-ui-generation +- /docs/reference/portable-no-design-system-pack +- /docs/guardrails/design-system-integrity +- /docs/examples/token-vagueness-drift +- /docs/examples/primitive-sprawl-drift +- /docs/examples/shallow-handoff-drift +- /docs/examples/state-coverage-drift diff --git a/content/docs/guardrails/surface-mode-structure.mdx b/content/docs/guardrails/surface-mode-structure.mdx new file mode 100644 index 0000000..255593c --- /dev/null +++ b/content/docs/guardrails/surface-mode-structure.mdx @@ -0,0 +1,69 @@ +--- +title: Surface mode structure +slug: /docs/guardrails/surface-mode-structure +page_type: guardrail +summary: Require visually led UI generation to choose one mode before structuring the first viewport and section order. +agent_summary: > + This guardrail keeps marketing, product, and hybrid demo structures from + mixing into a generic hero or card grid when the user's job needs a specific + first-screen model. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.surface-mode-structure +owners: + primary: Design Systems + risk: Product + operational: Frontend Platform +status: active +last_reviewed: 2026-04-23 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/examples/mode-structure-drift +related_resources: + - /resources/guardrails/surface-mode-structure.v1.json +related_schemas: + - /schemas/guardrail.schema.json +toc: true +--- + +## Why this matters + +AI-generated UI often defaults to a familiar landing-page shell even when the requested surface is operational. That produces a good-looking first impression with the wrong job: the user sees campaign copy, cards, and proof claims before they can operate the product. + +## What decision is being governed + +This guardrail governs whether a visually led UI task chooses exactly one mode before layout work starts: `marketing surface`, `product surface`, or `hybrid demo`. + +## What good judgment looks like + +- marketing surfaces lead with brand or product, promise, CTA, and one dominant visual +- product surfaces lead with the working surface, status, context, and action areas +- hybrid demos move quickly from a branded entry into believable product proof +- every section has one job and supports the selected mode + +## What drift looks like + +1. An operational tool starts with a marketing hero. +2. A launch page hides the brand behind generic product copy. +3. A hybrid demo spends multiple sections on promise before showing product proof. +4. Card grids, stat strips, and floating dashboards appear before the surface job is clear. + +## How JudgmentKit responds + +Small mode gaps get restructured into the correct first viewport and section order. Medium gaps receive design review. Severe mode mismatch blocks the output until the surface structure matches the user's actual job. + +## Technical reference + +- Resource: `/resources/guardrails/surface-mode-structure.v1.json` +- Schema: `/schemas/guardrail.schema.json` + +## Related pages + +- /docs/workflows/ai-ui-generation +- /docs/examples/mode-structure-drift diff --git a/content/docs/guardrails/visual-planning-contract.mdx b/content/docs/guardrails/visual-planning-contract.mdx new file mode 100644 index 0000000..e208920 --- /dev/null +++ b/content/docs/guardrails/visual-planning-contract.mdx @@ -0,0 +1,69 @@ +--- +title: Visual planning contract +slug: /docs/guardrails/visual-planning-contract +page_type: guardrail +summary: Require visually led UI work to define a visual thesis, content plan, and interaction thesis before implementation. +agent_summary: > + This guardrail turns vague art-direction asks into concrete hierarchy, + composition, content, and motion decisions before UI generation starts. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.visual-planning-contract +owners: + primary: Design Systems + risk: Product + operational: Frontend Platform +status: active +last_reviewed: 2026-04-23 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/examples/visual-planning-gap +related_resources: + - /resources/guardrails/visual-planning-contract.v1.json +related_schemas: + - /schemas/guardrail.schema.json +toc: true +--- + +## Why this matters + +Prompts like "make it premium" or "make it feel modern" are not implementation contracts. Without a planning frame, the model usually adds gradients, shadows, radius, and cards before it decides what hierarchy or composition should change. + +## What decision is being governed + +This guardrail governs whether the generator has translated visual intent into actionable planning before it drafts layout, styling, or code. + +## What good judgment looks like + +- define `Visual Thesis` as mood, material, and energy +- define `Content Plan` as ordered section jobs for the selected mode +- define `Interaction Thesis` as purposeful motion or interaction ideas +- translate vague asks into hierarchy, composition, and motion decisions +- preserve existing design-system tokens and patterns unless the user requested a rework + +## What drift looks like + +1. The draft says premium, calm, or modern without concrete decisions. +2. Decorative styling appears before structure is clear. +3. Sections repeat the same job because no content plan exists. +4. The motion plan is described as polish instead of a hierarchy or affordance decision. + +## How JudgmentKit responds + +Small gaps get filled with the missing planning frame. Medium gaps get rewritten before implementation. Severe gaps block the output until the visual direction can be made buildable. + +## Technical reference + +- Resource: `/resources/guardrails/visual-planning-contract.v1.json` +- Schema: `/schemas/guardrail.schema.json` + +## Related pages + +- /docs/workflows/ai-ui-generation +- /docs/examples/visual-planning-gap diff --git a/content/docs/reference/portable-no-design-system-pack.mdx b/content/docs/reference/portable-no-design-system-pack.mdx new file mode 100644 index 0000000..938045a --- /dev/null +++ b/content/docs/reference/portable-no-design-system-pack.mdx @@ -0,0 +1,147 @@ +--- +title: Portable no-design-system pack +slug: /docs/reference/portable-no-design-system-pack +page_type: reference +summary: JudgmentKit's portable UI implementation authority for cases where no external design system is present. +agent_summary: > + This reference defines the published primitive inventory, token contract, + reusable component recipes, state matrix, layout archetypes, vendored + guideline profiles, and handoff requirements for no-design-system AI UI + generation. +audiences: + - design-leaders + - product-managers + - platform-engineering + - ai-application-developers +workflows: + - workflow.ai-ui-generation +guardrails: + - guardrail.design-system-integrity + - guardrail.spec-completeness +owners: + primary: Design Systems + risk: Accessibility + operational: Frontend Platform +status: active +last_reviewed: 2026-04-14 +related_pages: + - /docs/workflows/ai-ui-generation + - /docs/guardrails/design-system-integrity + - /docs/guardrails/spec-completeness + - /docs/examples/token-vagueness-drift + - /docs/examples/primitive-sprawl-drift +related_resources: + - /resources/constraint-packs/ai-ui-no-design-system.v1.json +related_schemas: + - /schemas/constraint_pack.schema.json +toc: true +--- + +## Why this pack exists + +JudgmentKit previously governed no-design-system UI generation mostly through restraint language and escalation rules. That reduced drift, but it still left too much room for vague tokens, invented primitives, and shallow handoff. This pack closes that gap by becoming the positive implementation authority when no external design system is available. + +## When to use it + +Use this pack when the repo, prompt, and brief do not supply an authoritative external design system, or when the task explicitly asks for a portable JudgmentKit-native UI authority. + +If a referenced external design system exists and has a confirmed accessibility baseline or owner-approved review status, that system still takes precedence. + +## What it governs + +- the approved primitive inventory +- the default token contract +- reusable React+Tailwind component recipes for every approved primitive +- required light and dark theme pairs +- layout archetypes by surface type +- the required state matrix +- the minimum implementation handoff contract +- the vendored generation and review guideline profiles derived from the Vercel web interface guidelines + +## Primitive inventory + +The pack constrains no-design-system output to a closed primitive vocabulary: + +- layout shell +- sidebar or rail +- header +- card +- field +- button +- tabs +- table or list +- sheet or drawer +- dialog +- inspector +- artifact panel + +The model may recombine these primitives, but it should not invent bespoke wrappers or visual modules unless that gap is escalated. + +## Token contract + +The pack publishes concrete spacing, radius, elevation, typography, and color bindings. The point is not visual branding. The point is implementation clarity. + +- spacing scale runs from `--jk-space-1 = 4px` through `--jk-space-6 = 32px` +- radius scale runs from `--jk-radius-1 = 4px` through `--jk-radius-3 = 8px` +- default stroke is `1px solid var(--jk-color-border-subtle)` +- elevation is limited to none, raised cards, and modal-level elevation +- color roles publish both light and dark values for canvas, surface, muted surface, border, text, accent, success, warning, and danger + +## Required output contract + +No-design-system output is not considered complete unless it includes these exact sections: + +- `core_screens` +- `token_spec` +- `component_recipes` +- `screen_composition` +- `state_coverage` +- `theme_contract` +- `accessibility_contract` +- `escalation_items` + +These sections are the portable handoff contract. They give implementation teams something concrete to build and reviewers something concrete to judge. + +## Recipe and guideline model + +Every approved primitive now includes: + +- slot structure +- allowed variants +- interaction rules +- accessibility contract +- a React+Tailwind recipe snippet + +The pack also links two vendored guideline profiles: + +- `guideline-profile.ai-ui-generation-authority` +- `guideline-profile.ai-ui-review-checks` + +## State and layout requirements + +The published state matrix requires explicit coverage for loading, empty, ready, error, review-needed, and disabled states. + +The published layout archetypes cover: + +- app workspace +- settings form +- dashboard +- review flow +- handoff or export flow + +## Boundaries + +This pack is not a substitute for a product-specific design system. It is a portable authority for situations where the alternative would otherwise be generic restraint language and hidden implementation assumptions. + +## Technical reference + +- Resource: `/resources/constraint-packs/ai-ui-no-design-system.v1.json` +- Schema: `/schemas/constraint_pack.schema.json` + +## Related pages + +- /docs/workflows/ai-ui-generation +- /docs/guardrails/design-system-integrity +- /docs/guardrails/spec-completeness +- /docs/examples/token-vagueness-drift +- /docs/examples/primitive-sprawl-drift diff --git a/content/docs/workflows/ai-ui-generation.mdx b/content/docs/workflows/ai-ui-generation.mdx index b5b9b5b..d5df7c4 100644 --- a/content/docs/workflows/ai-ui-generation.mdx +++ b/content/docs/workflows/ai-ui-generation.mdx @@ -2,10 +2,12 @@ title: AI UI generation slug: /docs/workflows/ai-ui-generation page_type: workflow -summary: A builder workflow that turns product intent into interface proposals while staying inside design-system, accessibility, and budget constraints. +summary: A builder workflow that turns product intent into interface proposals while staying inside design-system or portable implementation authority, frontend guardrails/examples, accessibility, and budget constraints. agent_summary: > This workflow explains how JudgmentKit governs AI-generated interface output - so that component choices, accessibility, and runtime budgets remain explicit. + so that visual direction, component choices, reusable recipes, token + contracts, state coverage, accessibility, and runtime budgets remain + explicit. audiences: - design-leaders - product-managers @@ -17,24 +19,48 @@ owners: primary: Design Systems operational: Frontend Platform status: active -last_reviewed: 2026-04-11 +last_reviewed: 2026-04-23 related_pages: - /docs/guardrails/design-system-integrity + - /docs/guardrails/spec-completeness + - /docs/guardrails/surface-mode-structure + - /docs/guardrails/visual-planning-contract + - /docs/guardrails/motion-media-purpose + - /docs/guardrails/frontend-output-contract - /docs/guardrails/ui-copy-clarity - /docs/guardrails/control-proximity - /docs/guardrails/surface-theme-parity - /docs/guardrails/runtime-and-cost - /docs/guardrails/provenance-and-escalation + - /docs/reference/portable-no-design-system-pack - /docs/examples/ui-generation-drift - /docs/examples/embellishment-drift + - /docs/examples/mode-structure-drift + - /docs/examples/visual-planning-gap + - /docs/examples/motion-media-drift + - /docs/examples/output-contract-gap - /docs/examples/onboarding-clarity-drift - /docs/examples/repetitive-copy-drift - /docs/examples/control-proximity-drift - /docs/examples/surface-theme-parity-drift + - /docs/examples/token-vagueness-drift + - /docs/examples/primitive-sprawl-drift + - /docs/examples/shallow-handoff-drift + - /docs/examples/state-coverage-drift related_resources: - /resources/workflows/ai-ui-generation.v1.json + - /resources/constraint-packs/ai-ui-no-design-system.v1.json + - /resources/guideline-profiles/ai-ui-generation-authority.v1.json + - /resources/guideline-profiles/ai-ui-review-checks.v1.json + - /resources/guardrails/spec-completeness.v1.json + - /resources/guardrails/surface-mode-structure.v1.json + - /resources/guardrails/visual-planning-contract.v1.json + - /resources/guardrails/motion-media-purpose.v1.json + - /resources/guardrails/frontend-output-contract.v1.json related_schemas: - /schemas/workflow.schema.json + - /schemas/constraint_pack.schema.json + - /schemas/guideline_profile.schema.json - /schemas/decision-record.schema.json - /schemas/verdict.schema.json toc: true @@ -42,24 +68,36 @@ toc: true ## Why this workflow matters -Generated UI can look impressive and still create downstream cleanup. The main risk is not only visual quality. It is unapproved primitives, silent overrides of the design system, ornamental zero-shot styling, missing theme support, unclear provenance, and unnecessary runtime spend hiding inside a “creative” result. +Generated UI can look impressive and still create downstream cleanup. The main risk is not only visual quality. It is the wrong surface mode, vague visual planning, ornamental media or motion, unapproved primitives, name-only component mapping, vague token language, incomplete state coverage, shallow handoff, missing theme support, inaccessible recipes, unclear provenance, and unnecessary runtime spend hiding inside a creative result. ## What decisions exist inside the workflow -- which components and tokens may be used +- which components, recipes, and tokens may be used - whether the referenced design system is authoritative for the requested surface +- when the JudgmentKit portable no-design-system pack becomes the source of truth +- which frontend visual mode governs the surface +- when visual thesis, content plan, and interaction thesis are required before implementation - whether that design system has an accessibility baseline or owner-approved review status - how much variation is acceptable before review +- whether imagery and motion have a clear purpose or should be downgraded +- whether final output includes the required visual contract evidence +- whether the current output is concrete enough to build without backfilling recipes, tokens, states, or handoff detail - when local controls are too detached from the surface they govern - when a first pass is too ornamental for zero-shot generation - when light and dark mode should be assumed by default - what accessibility baseline must hold - how much runtime complexity is justified - what evidence should travel with the implementation handoff +- whether screenshots and previews are derived from the same component evidence the judge reads ## Which guardrails apply - `guardrail.design-system-integrity` +- `guardrail.spec-completeness` +- `guardrail.surface-mode-structure` +- `guardrail.visual-planning-contract` +- `guardrail.motion-media-purpose` +- `guardrail.frontend-output-contract` - `guardrail.ui-copy-clarity` - `guardrail.control-proximity` - `guardrail.surface-theme-parity` @@ -70,50 +108,116 @@ Generated UI can look impressive and still create downstream cleanup. The main r - feature intent - target surface and breakpoint expectations -- approved component and token inventory +- approved component and token inventory, or explicit no-design-system confirmation +- visual-direction intent or explicit purely functional scope - accessibility rules or confirmed design-system review status - budget and latency target ## Examples in practice -The workflow now includes six calibration patterns. One example shows an over-scoped request that asks for novelty, custom primitives, and unlimited reasoning in one pass. Another shows a zero-shot UI pass drifting toward decorative chrome and single-theme output while ignoring a referenced design system. A third shows a landing-page draft that over-exposes internals and proof before it makes onboarding obvious. A fourth shows a dense control cluster where headings, helper copy, and actions repeat the same words until the next step is unclear. A fifth shows local viewer controls drifting into a separate details zone, making it harder to tell what surface they govern. The sixth shows a dark terminal-style code block dropped into an otherwise light interface instead of using a theme-matched artifact surface. In each case JudgmentKit rewrites the request into a clearer, system-safe pass plus explicit review questions. +The workflow now includes calibration patterns for visual direction, structural drift, and component-authority drift. The original system boundaries still apply: component drift, ornamental chrome, onboarding clarity, repetitive copy, control proximity, and surface-theme parity. The frontend visual direction case is calibrated with surface mode drift, visual planning gaps, motion/media drift, and missing output contract evidence. The no-design-system case is calibrated with token vagueness, primitive sprawl, shallow handoff, missing state coverage, name-only component mapping, non-reusable screen markup, missing accessibility API, hand-authored preview drift, and theme bindings that are not attached to real recipes. In each case JudgmentKit rewrites the request into a clearer, system-safe pass plus explicit review questions or a portable implementation contract. + +## Frontend visual direction + +When the task is visually led, JudgmentKit treats frontend visual direction as workflow guardrails plus calibration examples rather than a separate playbook collection or guideline profile. The frontend guardrails require: + +- exactly one mode: `marketing surface`, `product surface`, or `hybrid demo` +- `Visual Thesis`, `Content Plan`, and `Interaction Thesis` before implementation +- structure rules for first viewport, section jobs, product proof, and card usage +- motion and media rules that tie animation and imagery to hierarchy, narrative, or affordance +- downgrade rules for accessibility, mobile fit, runtime budget, missing assets, and existing design-system authority +- final output shape for implementation work or direction-only work + +These frontend guardrails do not override the product model. If the surface is operational, it leads with the working product. If the repo has an established design system, the workflow improves hierarchy, spacing, composition, and emphasis before inventing new language. + +## Portable no-design-system authority + +When an external design system does not exist, JudgmentKit should not fall back to generic restraint language alone. It now publishes a portable implementation authority pack that becomes the source of truth for: + +- approved primitives +- token bindings and light-dark pairs +- reusable React+Tailwind component recipes +- layout archetypes by surface type +- required state coverage +- the minimum handoff contract +- vendored generation and review rules derived from the Vercel web interface guidelines + +The pack also defines the exact sections that no-design-system output must include: + +- `core_screens` +- `token_spec` +- `component_recipes` +- `screen_composition` +- `state_coverage` +- `theme_contract` +- `accessibility_contract` +- `escalation_items` ## Common drift patterns 1. The model invents new visual primitives instead of recombining approved ones. 2. Decorative gradients, gratuitous shadows, and oversized radii become the default language of a first pass. -3. The output assumes a single theme when dark and light mode should be present by default. -4. A referenced design system is treated as authority without confirming whether it is accessibility-reviewed. -5. Accessibility semantics disappear in the pursuit of style. -6. Headings, helper text, and CTA labels reuse the same words until different UI elements sound interchangeable. -7. Local controls drift into a separate header or metadata zone from the viewer, panel, or artifact they change. -8. A code block or artifact viewer introduces a separate dark or light theme model that the surrounding interface does not use. -9. Runtime cost grows because the prompt asks for open-ended refinement. -10. The output leaves implementation teams guessing what is fixed versus exploratory. -11. The page explains internal artifacts before it explains what the product is or how to start. +3. A product surface starts with a marketing hero or generic SaaS card grid. +4. Vague visual adjectives replace mode, thesis, content plan, and interaction thesis. +5. Media and motion are ornamental instead of tied to hierarchy, narrative, or affordance. +6. A visually led final response omits mode, visual thesis, motion plan, asset gaps, or downgrade notes. +7. The output assumes a single theme when dark and light mode should be present by default. +8. A referenced design system is treated as authority without confirming whether it is accessibility-reviewed. +9. Accessibility semantics disappear in the pursuit of style. +10. Headings, helper text, and CTA labels reuse the same words until different UI elements sound interchangeable. +11. Local controls drift into a separate header or metadata zone from the viewer, panel, or artifact they change. +12. A code block or artifact viewer introduces a separate dark or light theme model that the surrounding interface does not use. +13. Runtime cost grows because the prompt asks for open-ended refinement. +14. The output leaves implementation teams guessing what is fixed versus exploratory. +15. The page explains internal artifacts before it explains what the product is or how to start. +16. The output sounds restrained, but the recipes, tokens, states, and handoff sections are still implicit. ## Escalation points -Escalate when the requested pattern cannot be expressed with approved primitives, when the brief conflicts with the design system, when the design system's accessibility status is unknown, when accessibility tradeoffs are unclear, or when the workflow is being used as a substitute for design review. +Escalate when the requested pattern cannot be expressed with approved primitives, when the brief conflicts with the design system, when the design system's accessibility status is unknown, when accessibility tradeoffs are unclear, when the portable pack and the brief are both silent on a needed decision, or when the workflow is being used as a substitute for design review. ## Related resources and schemas - Resource: `/resources/workflows/ai-ui-generation.v1.json` +- Portable pack: `/resources/constraint-packs/ai-ui-no-design-system.v1.json` +- Generation authority profile: `/resources/guideline-profiles/ai-ui-generation-authority.v1.json` +- Review checks profile: `/resources/guideline-profiles/ai-ui-review-checks.v1.json` +- Spec completeness guardrail: `/resources/guardrails/spec-completeness.v1.json` +- Surface mode guardrail: `/resources/guardrails/surface-mode-structure.v1.json` +- Visual planning guardrail: `/resources/guardrails/visual-planning-contract.v1.json` +- Motion/media guardrail: `/resources/guardrails/motion-media-purpose.v1.json` +- Frontend output contract guardrail: `/resources/guardrails/frontend-output-contract.v1.json` - Schema: `/schemas/workflow.schema.json` +- Constraint pack schema: `/schemas/constraint_pack.schema.json` +- Guideline profile schema: `/schemas/guideline_profile.schema.json` - Decision schema: `/schemas/decision-record.schema.json` - Verdict schema: `/schemas/verdict.schema.json` ## Related pages - /docs/guardrails/design-system-integrity +- /docs/guardrails/spec-completeness +- /docs/guardrails/surface-mode-structure +- /docs/guardrails/visual-planning-contract +- /docs/guardrails/motion-media-purpose +- /docs/guardrails/frontend-output-contract - /docs/guardrails/ui-copy-clarity - /docs/guardrails/control-proximity - /docs/guardrails/surface-theme-parity - /docs/guardrails/runtime-and-cost - /docs/guardrails/provenance-and-escalation +- /docs/reference/portable-no-design-system-pack - /docs/examples/ui-generation-drift - /docs/examples/embellishment-drift +- /docs/examples/mode-structure-drift +- /docs/examples/visual-planning-gap +- /docs/examples/motion-media-drift +- /docs/examples/output-contract-gap - /docs/examples/onboarding-clarity-drift - /docs/examples/repetitive-copy-drift - /docs/examples/control-proximity-drift - /docs/examples/surface-theme-parity-drift +- /docs/examples/token-vagueness-drift +- /docs/examples/primitive-sprawl-drift +- /docs/examples/shallow-handoff-drift +- /docs/examples/state-coverage-drift diff --git a/content/resources/constraint-packs/ai-ui-no-design-system.v1.json b/content/resources/constraint-packs/ai-ui-no-design-system.v1.json new file mode 100644 index 0000000..4342a8b --- /dev/null +++ b/content/resources/constraint-packs/ai-ui-no-design-system.v1.json @@ -0,0 +1,482 @@ +{ + "id": "constraint-pack.ai-ui-no-design-system", + "type": "constraint_pack", + "version": "1.0.0", + "title": "Portable no-design-system implementation authority", + "summary": "A JudgmentKit-native implementation authority for UI generation when no external design system is present, with concrete primitives, React+Tailwind recipes, accessibility rules, state coverage, and handoff requirements.", + "status": "active", + "workflows": ["workflow.ai-ui-generation"], + "guardrail_ids": [ + "guardrail.design-system-integrity", + "guardrail.spec-completeness", + "guardrail.control-proximity", + "guardrail.surface-theme-parity" + ], + "guideline_profile_ids": [ + "guideline-profile.ai-ui-generation-authority", + "guideline-profile.ai-ui-review-checks" + ], + "authority": { + "when_to_use": "Use this pack when the repo, prompt, and brief do not provide an authoritative external design system or when the task explicitly asks for a portable JudgmentKit-native UI authority.", + "priority_rules": [ + "If a referenced external design system exists and has a confirmed accessibility baseline or owner-approved review status, that system takes precedence.", + "If no external design system exists, this pack becomes the source of truth for primitives, tokens, layout archetypes, required states, light-dark theme parity, reusable recipes, and handoff depth.", + "If the pack, vendored guideline profiles, and brief are all silent about a requirement, escalate instead of inventing a new primitive, vague token, or unsupported interaction." + ], + "output_contract_sections": [ + "core_screens", + "token_spec", + "component_recipes", + "screen_composition", + "state_coverage", + "theme_contract", + "accessibility_contract", + "escalation_items" + ] + }, + "implementation_model": { + "target_stack": "react-tailwind", + "recipe_format": "React+Tailwind snippets with explicit slots, variants, interaction rules, and accessibility API", + "preview_artifacts": [ + "implementation-contract.json", + "preview-source.tsx", + "preview.html" + ] + }, + "primitives": { + "inventory": [ + { + "id": "layout-shell", + "label": "Layout shell", + "description": "Top-level application frame with optional rail, header, workspace body, and secondary inspector region.", + "usage": "Required for app workspace, dashboard, review, and export surfaces.", + "component_recipe": { + "slots": ["rail", "header", "main", "inspector"], + "variants": ["with-rail", "with-inspector", "centered-main"], + "interaction_rules": [ + "Keep the primary task in main and secondary metadata in inspector.", + "Do not move local controls away from the surface they govern." + ], + "accessibility_contract": [ + "Use main, nav, header, and aside landmarks where applicable.", + "Preserve heading order and skip-link destination." + ], + "react_tailwind": "
...
" + } + }, + { + "id": "sidebar-rail", + "label": "Sidebar or rail", + "description": "Persistent navigation or collection switcher aligned to the main workspace frame.", + "usage": "Use for global navigation, queue switching, or mode changes.", + "component_recipe": { + "slots": ["label", "items", "footer"], + "variants": ["nav", "history", "queue"], + "interaction_rules": [ + "Active item must be visually distinct and keyboard reachable.", + "Keep mode changes and run history grouped, not interleaved." + ], + "accessibility_contract": [ + "Use nav with an accessible label.", + "Use links for navigation and buttons for actions." + ], + "react_tailwind": "" + } + }, + { + "id": "header", + "label": "Header", + "description": "Surface-level title, status, and primary actions anchored to the region they govern.", + "usage": "Use for page title, collection metadata, and scoped actions.", + "component_recipe": { + "slots": ["title", "meta", "actions"], + "variants": ["workspace", "panel", "dialog"], + "interaction_rules": [ + "Primary action stays in the same header as the governed surface.", + "Keep status and secondary metadata subordinate to the title." + ], + "accessibility_contract": [ + "Use heading tags in hierarchy order.", + "If actions change nearby content, keep them adjacent in DOM order." + ], + "react_tailwind": "
...
" + } + }, + { + "id": "card", + "label": "Card", + "description": "Contained surface block with a single content responsibility and explicit header-content-footer structure when needed.", + "usage": "Use for summaries, settings groups, metrics, and review blocks.", + "component_recipe": { + "slots": ["header", "body", "footer"], + "variants": ["summary", "settings", "review", "muted"], + "interaction_rules": [ + "Use cards to group meaning, not to add decorative nesting.", + "Promote content hierarchy before adding elevation." + ], + "accessibility_contract": [ + "Keep heading and body text associated in reading order.", + "If footer actions exist, ensure they remain within the same card." + ], + "react_tailwind": "
...
" + } + }, + { + "id": "field", + "label": "Field", + "description": "Label, control, helper text, and validation message grouped as one primitive.", + "usage": "Use for text input, toggles, selects, and textarea entry.", + "component_recipe": { + "slots": ["label", "control", "helper", "error"], + "variants": ["text", "textarea", "select", "toggle"], + "interaction_rules": [ + "Do not rely on placeholder text as the only label.", + "Validation copy must stay next to the affected control." + ], + "accessibility_contract": [ + "Use label or aria-label on every control.", + "Use aria-describedby for helper and error text when present." + ], + "react_tailwind": "