From afd7700c78b7d597e4673a05ec794b85681b73bf Mon Sep 17 00:00:00 2001 From: Ryan Dombrowski Date: Tue, 14 Jul 2026 15:00:54 -0400 Subject: [PATCH] =?UTF-8?q?feat(FM-11):=20take-it-home=20view=20=E2=80=94?= =?UTF-8?q?=20client-side=20validator,=20MCP=20config,=20local-agent=20pat?= =?UTF-8?q?h?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The validate-ui spike CONFIRMED client-side execution: dspack-gen's /core subpath (zero-network by its own boundary test) bundles into the Next static export and lintSurface runs in-browser against the byte-synced contract. First-load JS is unchanged (103 kB shell; core rides the async app chunk). No live-only fallback needed. - take-home view in the shell switcher: MCP client config (npx ds-mcp ^0.3.1, --dspack absolute path), contract download served as a build-time byte copy (scripts/take-home-assets.mjs -> public/take-home/, gitignored), the real S1/S2/S3 validator on pasted surfaces with findings passed through unmodified, and the clone-and-run local agent path (launch-day option; a published npx package is a fast-follow decision, deliberately unreferenced in copy). - Honesty tests in the receipts spirit: fixture-001's recorded attempt-0 surface reproduces the RECORDED findings byte-for-byte through the bundled validator (unit) and through the UI (e2e); the downloaded contract byte-matches packages/contracts/astryx.dspack.json (e2e); the MCP config's ds-mcp floor is literally asserted >= 0.3.1 (unit + e2e). - take-home.spec.ts blocks the agent at the network layer (the deployed static site's state) and everything still works; registered in the production suite; axe clean. - apps/web gains vitest (+ CI unit step); permalink v=home supported. Sequencing: merging this waits on ds-mcp 0.3.1 being live on npm (aestheticfunction/ds-mcp#21 + trusted-publisher registration + v0.3.1 tag) — the copy references ^0.3.1. Verified end to end: static export in the browser (validator reproduces rule.destructive-requires-alertdialog on the caught example; contract download 200 with exact bytes; console clean) and the clone-and-run commands executed verbatim from a cold GitHub clone (agent answered health and model discovery). Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 1 + .gitignore | 1 + apps/web/app/permalink.ts | 4 +- apps/web/app/studio.tsx | 11 +- apps/web/app/take-home-view.tsx | 209 ++++++++++++++++++++++++++ apps/web/app/take-home.test.ts | 77 ++++++++++ apps/web/app/take-home.ts | 113 ++++++++++++++ apps/web/package.json | 9 +- apps/web/scripts/take-home-assets.mjs | 17 +++ e2e/take-home.spec.ts | 108 +++++++++++++ playwright.production.config.ts | 3 + pnpm-lock.yaml | 6 + 12 files changed, 552 insertions(+), 7 deletions(-) create mode 100644 apps/web/app/take-home-view.tsx create mode 100644 apps/web/app/take-home.test.ts create mode 100644 apps/web/app/take-home.ts create mode 100644 apps/web/scripts/take-home-assets.mjs create mode 100644 e2e/take-home.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a694b59..1e264eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,7 @@ jobs: pnpm --filter @dspack-studio/replay test pnpm --filter @dspack-studio/scenarios test pnpm --filter agent test + pnpm --filter web test - name: Type checks run: pnpm -r typecheck - name: Static export (the deploy artifact) diff --git a/.gitignore b/.gitignore index d320d4d..2c42b8e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ coverage/ test-results/ playwright-report/ *.tsbuildinfo +apps/web/public/take-home/ diff --git a/apps/web/app/permalink.ts b/apps/web/app/permalink.ts index 2a05c78..77f1af2 100644 --- a/apps/web/app/permalink.ts +++ b/apps/web/app/permalink.ts @@ -20,7 +20,7 @@ */ export interface PermalinkState { scenario?: string; - view?: "replay" | "live" | "break" | "canvas"; + view?: "replay" | "live" | "break" | "canvas" | "home"; fixture?: string; fork?: { parentKey: string; forkIndex: number }; breakCondition?: string; @@ -30,7 +30,7 @@ export interface PermalinkState { } const PANELS = new Set(["receipt", "wire", "pipeline"]); -const VIEWS = new Set(["replay", "live", "break", "canvas"]); +const VIEWS = new Set(["replay", "live", "break", "canvas", "home"]); export function parsePermalink(hash: string): { state: PermalinkState; error?: string } { const raw = hash.replace(/^#\/?/, ""); diff --git a/apps/web/app/studio.tsx b/apps/web/app/studio.tsx index 5456013..7bd4716 100644 --- a/apps/web/app/studio.tsx +++ b/apps/web/app/studio.tsx @@ -19,15 +19,18 @@ const AGENT_URL = process.env.NEXT_PUBLIC_AGENT_URL ?? "http://localhost:8787"; import { LiveView } from "./live-view"; import { BreakView } from "./break-view"; import { RestyleView } from "./restyle-view"; +import { TakeHomeView } from "./take-home-view"; import { useAgentStatus } from "./use-agent-status"; import { btnClass, linkClass } from "./ui"; /** One plain sentence of context per view, shown under the switcher. */ -function viewHelp(view: "replay" | "live" | "break" | "canvas", agentOnline: boolean | null): string { +function viewHelp(view: "replay" | "live" | "break" | "canvas" | "home", agentOnline: boolean | null): string { const offline = agentOnline === false; switch (view) { case "replay": return "Recorded real runs, replayed from their event streams. Works everywhere, no setup."; + case "home": + return "The ecosystem in your own editor: the MCP config, the contract, the validator, and the local agent. Same packages, published versions."; case "live": return offline ? "Streams a new run from an agent on your machine. The local agent is offline; start it: pnpm --filter agent dev. Replay works without it." @@ -410,7 +413,7 @@ function ReplayPane({ scenario, deepLink, onLinkError }: { scenario: Scenario; d export function Studio() { const [scenarioId, setScenarioId] = useState(readyScenarios[0]?.id); - const [view, setView] = useState<"replay" | "live" | "break" | "canvas">("replay"); + const [view, setView] = useState<"replay" | "live" | "break" | "canvas" | "home">("replay"); // A planned scenario the visitor tapped: its "what it needs" line replaces // the tagline until a ready scenario is chosen or it is tapped again. const [revealedPlanned, setRevealedPlanned] = useState(null); @@ -597,6 +600,9 @@ export function Studio() { +

{viewHelp(view, agentOnline)} @@ -625,6 +631,7 @@ export function Studio() { /> )} {view === "canvas" && scenario && } + {view === "home" && } {/* Orientation, below the primary experience: the approved pipeline language and the existing capabilities, stated once, compactly. */}

diff --git a/apps/web/app/take-home-view.tsx b/apps/web/app/take-home-view.tsx new file mode 100644 index 0000000..630b4bd --- /dev/null +++ b/apps/web/app/take-home-view.tsx @@ -0,0 +1,209 @@ +"use client"; + +/** + * FM-11: take it home. Three honest paths from liking the studio to running + * the ecosystem yourself: the ds-mcp config for your editor, the real + * validator in this page, and the local agent for live governed generation. + * Everything here names published versions only; the validator and the + * contract are the same ones the studio runs. Capabilities that need the + * local agent say so plainly. + */ +import { useState } from "react"; +import type { Finding, GateReport } from "@aestheticfunction/dspack-gen/core"; +import { + CONTRACT_DOWNLOAD_PATH, + CONTRACT_PATH_PLACEHOLDER, + DS_MCP_RANGE, + LOCAL_AGENT_COMMANDS, + caughtExample, + mcpConfig, + validatePasted, + type ValidateOutcome, +} from "./take-home"; +import { btnClass, linkClass } from "./ui"; + +const sectionStyle: React.CSSProperties = { + border: "1px solid var(--line)", + borderRadius: 6, + padding: "14px 16px", + marginBottom: 14, +}; + +const preStyle: React.CSSProperties = { + margin: "8px 0", + padding: 10, + background: "var(--bg-2)", + borderRadius: 3, + overflow: "auto", + fontFamily: "var(--mono)", + fontSize: 12, + lineHeight: 1.5, +}; + +function download(filename: string, text: string, type = "application/json") { + const url = URL.createObjectURL(new Blob([text], { type })); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} + +function CopyButton({ text, testid }: { text: string; testid: string }) { + const [copied, setCopied] = useState(false); + return ( + + ); +} + +/** One gate line + its findings, the same information the gates tab shows for a run. */ +function ReportView({ outcome }: { outcome: ValidateOutcome }) { + if (outcome.kind === "rejected") { + return ( +
+ The validator said no: {outcome.reason}. Nothing was evaluated. +
+ ); + } + const { report } = outcome; + return ( +
+

+ {report.pass + ? "All gates pass. This surface is contract-valid." + : `Gates failed: ${report.errorCount} error finding${report.errorCount === 1 ? "" : "s"}.`} +

+

+ {(report.gates as GateReport[]).map((g) => `${g.gate} ${g.status}`).join(" · ")} +

+ {(report.findings as Finding[]).map((f, i) => ( +
+

+ {f.ruleId} · {f.level} · at {f.location.path} +

+

{f.message}

+

{f.rationale}

+
+ ))} +
+ the report as the linter prints it +
{outcome.text}
+
+
+ ); +} + +export function TakeHomeView({ agentOnline }: { agentOnline: boolean | null }) { + const [pasted, setPasted] = useState(""); + const [outcome, setOutcome] = useState(null); + const config = mcpConfig(); + + return ( +
+
+

The design system in your editor

+

+ ds-mcp serves a dspack contract to MCP coding agents: components, tokens, patterns, the compiled + generation context, and the same S1/S2/S3 validator this studio runs. It is read-only, makes no network + calls, and runs from npm. This config points it at the studio's Astryx contract; download both, set + the absolute path, and add the config to your MCP client. +

+

+ Requires ds-mcp {DS_MCP_RANGE}: earlier versions serve a pre-0.1.1 generation schema that constrains + models differently than this studio's pipeline. +

+
{config}
+
+ + + + download the contract (astryx.dspack.json) + +
+

+ The downloaded contract is the byte-synced copy of dspack main this studio validates against. Replace{" "} + {CONTRACT_PATH_PLACEHOLDER} with where you saved it. +

+
+ +
+

The validator, right here

+

+ Paste a dspack surface and the real linter evaluates it against the studio's contract: S1 surface + schema, S2 contract vocabulary, S3 governance rules. This runs entirely in this page; nothing you paste + leaves your browser. +

+