diff --git a/.github/workflows/validate-contract.yml b/.github/workflows/validate-contract.yml index ca94c12..3d39651 100644 --- a/.github/workflows/validate-contract.yml +++ b/.github/workflows/validate-contract.yml @@ -8,6 +8,9 @@ on: schedule: - cron: "0 7 * * *" +env: + INTERFACECTL_CI_REF: 1f2377c28ae8349250afae04408309d1d551d4e3 + jobs: validate: name: Validate Contract @@ -32,6 +35,7 @@ jobs: uses: actions/checkout@v4 with: repository: Surfaces-Platform/interfacectl + ref: ${{ env.INTERFACECTL_CI_REF }} path: interfacectl token: ${{ secrets.INTERFACECTL_TOKEN || github.token }} @@ -123,12 +127,22 @@ jobs: permissions: contents: read pull-requests: read + env: + SURFACES_INTERFACECTL_ROOT: ${{ github.workspace }}/interfacectl steps: - name: Checkout code uses: actions/checkout@v4 with: token: ${{ secrets.INTERFACECTL_TOKEN || github.token }} + - name: Checkout interfacectl + uses: actions/checkout@v4 + with: + repository: Surfaces-Platform/interfacectl + ref: ${{ env.INTERFACECTL_CI_REF }} + path: interfacectl + token: ${{ secrets.INTERFACECTL_TOKEN || github.token }} + - name: Setup pnpm uses: pnpm/action-setup@v4 @@ -141,9 +155,16 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Build interfacectl + run: pnpm run interfacectl:build + - name: Build platform auth package run: pnpm --filter @surfaces/platform-auth build + - name: Install interfacectl Playwright browser + working-directory: interfacectl + run: pnpm --filter @surfaces/interfacectl-cli exec playwright install chromium + - name: Install Playwright browser run: pnpm exec playwright install --with-deps chromium diff --git a/apps/surfaceops-web/app/api/onboarding/extract/route.ts b/apps/surfaceops-web/app/api/onboarding/extract/route.ts index 12c1024..113e33f 100644 --- a/apps/surfaceops-web/app/api/onboarding/extract/route.ts +++ b/apps/surfaceops-web/app/api/onboarding/extract/route.ts @@ -24,8 +24,9 @@ export async function POST(request: Request) { ); } - const result = runOnboardingExtraction({ + const result = await runOnboardingExtraction({ url: payload.url, + appRoot: payload.appRoot, surfaceName: payload.surfaceName, surfaceId: payload.surfaceId, authMode: payload.authMode === "browser-session" ? "browser-session" : "none", diff --git a/apps/surfaceops-web/app/contracts/_components/onboarding-form.tsx b/apps/surfaceops-web/app/contracts/_components/onboarding-form.tsx index 71dad85..8eeec48 100644 --- a/apps/surfaceops-web/app/contracts/_components/onboarding-form.tsx +++ b/apps/surfaceops-web/app/contracts/_components/onboarding-form.tsx @@ -11,6 +11,7 @@ export function OnboardingForm() { const [state, setState] = useState("idle"); const [error, setError] = useState(null); const [url, setUrl] = useState(""); + const [appRoot, setAppRoot] = useState(""); const [surfaceName, setSurfaceName] = useState(""); const [surfaceId, setSurfaceId] = useState(""); const [authMode, setAuthMode] = useState<"none" | "browser-session">("none"); @@ -25,22 +26,24 @@ export function OnboardingForm() { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ - url, + url: extractMode === "remote-url" ? url : undefined, + appRoot: extractMode === "local-root" ? appRoot : undefined, surfaceName: surfaceName.trim() || undefined, surfaceId: surfaceId.trim() || undefined, - authMode, + authMode: extractMode === "remote-url" ? authMode : "none", extractMode, }), }); const data = (await response.json()) as { state?: string; surfaceId?: string; + recommendedRoute?: string; error?: string; }; if (!response.ok || data.state !== "completed" || !data.surfaceId) { throw new Error(data.error || "Onboarding failed."); } - router.push(`/contracts/runs/${data.surfaceId}`); + router.push(data.recommendedRoute || "/contracts/governance"); router.refresh(); } catch (submissionError) { setState("failed"); @@ -58,19 +61,47 @@ export function OnboardingForm() { padding: "20px", }} > +
+
+ + +
+
setUrl(event.target.value)} + placeholder={extractMode === "remote-url" ? "https://customer.example.com" : "apps/reference-target-web"} + value={extractMode === "remote-url" ? url : appRoot} + onChange={(event) => { + if (extractMode === "remote-url") { + setUrl(event.target.value); + return; + } + setAppRoot(event.target.value); + }} style={{ width: "100%", borderRadius: "8px", @@ -112,44 +143,28 @@ export function OnboardingForm() { padding: "8px 10px", fontSize: "13px", }} - /> - -
- - -
-
- - -
+ /> + + {extractMode === "remote-url" && ( +
+ + +
+ )} @@ -172,7 +187,7 @@ export function OnboardingForm() { cursor: state === "submitting" ? "wait" : "pointer", }} > - {state === "submitting" ? "Generating..." : "Generate First Contract"} + {state === "submitting" ? "Bootstrapping UI AST..." : "Create Governed Surface"} ); diff --git a/apps/surfaceops-web/app/contracts/governance/_components/local-workbench.tsx b/apps/surfaceops-web/app/contracts/governance/_components/local-workbench.tsx index ca3367d..d222d86 100644 --- a/apps/surfaceops-web/app/contracts/governance/_components/local-workbench.tsx +++ b/apps/surfaceops-web/app/contracts/governance/_components/local-workbench.tsx @@ -213,7 +213,7 @@ function deriveLatestResultState({ tone: "neutral", label: "Ready", title: "No rerun yet", - summary: "Review the surface and contract, then trigger the first regeneration when you are ready to test the flow.", + summary: "Review the surface and UI AST, then trigger the first regeneration when you are ready to test the flow.", detail: null, }; } @@ -245,13 +245,13 @@ function nextActionCopy({ acceptedSuggestionCount: number; }) { if (!modelConfigured) { - return "Refine the contract in chat now, then configure the local model before attempting another regeneration."; + return "Refine the UI AST in chat now, then configure the local model before attempting another regeneration."; } if (openSuggestionCount > 0) { return `There ${openSuggestionCount === 1 ? "is" : "are"} ${openSuggestionCount} open suggestion${openSuggestionCount === 1 ? "" : "s"}. Review them below, accept the ones that should steer the next pass, then regenerate.`; } if (latestRun?.status === "failed") { - return "Review the latest failure, then try another regeneration or switch to chat if the contract needs more guidance first."; + return "Review the latest failure, then try another regeneration or switch to chat if the UI AST needs more guidance first."; } if (latestRunOperatorNote) { return "The last pass made progress but still needs another targeted patch. Regenerate again after reviewing the evidence."; @@ -260,7 +260,7 @@ function nextActionCopy({ return "Accepted guidance is active. Regenerate when you want to test the next implementation pass against those decisions."; } if (!latestRun) { - return "Trigger the first regeneration when you are ready to evaluate the surface against the current contract."; + return "Trigger the first regeneration when you are ready to evaluate the surface against the current UI AST."; } return "If the latest result is close, refine in chat. If you need another implementation pass, regenerate."; } @@ -357,13 +357,15 @@ export function LocalWorkbench({ ? (promotionReadiness.summary.readyForPromotion ? "neutral" : "warning") : "info", ); + const lifecycle = data.lifecycle; + const review = data.review; const selectedRunSummary = data.runs.find((entry) => entry.runId === selectedRunId) ?? latestRun ?? null; const activeTraceRun = selectedDetail?.run ?? selectedRunSummary ?? null; const previewDebug = selectedDetail?.previewDebug ?? null; const showImplementationDebug = Boolean( data.latestRunAgentInput.path || data.latestRunAgentInput.content - || latestRun?.effectiveContractPath + || latestRun?.effectiveAstPath || latestRun?.explainabilityPath, ); @@ -486,7 +488,7 @@ export function LocalWorkbench({ throw new Error(json.error ?? "Contract chat update failed."); } setChatDraft(""); - setFeedback("Working contract updated. Review the latest result and regenerate when you want to test the change."); + setFeedback("Working UI AST updated. Review the latest result and regenerate when you want to test the change."); router.refresh(); } @@ -498,9 +500,9 @@ export function LocalWorkbench({ }); const json = await response.json(); if (!response.ok || !json.ok) { - throw new Error(json.error ?? "Local contract publish failed."); + throw new Error(json.error ?? "Local UI AST publish failed."); } - setFeedback(`Published revision ${json.publish.publishId}. Runtime and generation artifacts are frozen for this contract snapshot.`); + setFeedback(`Published revision ${json.publish.publishId}. Runtime and generation artifacts are frozen for this UI AST snapshot.`); router.refresh(); } @@ -549,14 +551,14 @@ export function LocalWorkbench({ >
- Contract workbench + UI AST workbench

{data.target.label}

- {data.contract.contractId} - v{data.contract.version} + {data.ast.astId} + v{data.ast.version} Local-only reference-target flow
@@ -773,7 +775,7 @@ export function LocalWorkbench({ } style={secondaryButton(isPending)} > - {isPending && pendingAction === "publish" ? "Publishing..." : "Publish current contract"} + {isPending && pendingAction === "publish" ? "Publishing..." : "Publish current UI AST"} {data.localModel.managementPath ? ( @@ -1438,14 +1477,14 @@ export function LocalWorkbench({