Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# interfacectl

Interface contract tooling for the Surfaces ecosystem. Validates, compares, compiles, and enforces compliance between defined interface contracts and observed implementation artifacts across checked-out surfaces and browser-observed runtime sessions.
Governed UI AST tooling for the Surfaces ecosystem. `interfacectl` validates, compares, compiles, and enforces compliance between a semantic UI AST contract and observed implementation artifacts across checked-out surfaces and browser-observed runtime sessions.

## Planning

Expand Down Expand Up @@ -61,12 +61,18 @@ interfacectl init --url https://app.example.com --surface customer-app --auth-pr

`interfacectl` replays the saved browser session in Chromium, analyzes the rendered authenticated page, and keeps auth state out of generated artifacts.

For checked-out code and optional runtime observation, use `validate`:
Compatibility note:

- The canonical authored artifact is `contracts/ui.surface.ast.json`.
- `--contract` remains supported only as a deprecated migration/compatibility input for legacy `web.surface.contract` JSON.
- Use `interfacectl migrate-ui-ast --contract <legacy-path> --out contracts/ui.surface.ast.json` to move legacy inputs onto the canonical AST path.

For checked-out code and optional runtime observation, use `validate` against the canonical UI AST:

```bash
interfacectl validate --workspace-root . --contract ./contracts/surfaces.web.contract.json --format json --exit-codes v2
interfacectl validate --workspace-root . --ast ./contracts/ui.surface.ast.json --format json --exit-codes v2

interfacectl validate --workspace-root . --contract ./contracts/surfaces.web.contract.json \
interfacectl validate --workspace-root . --ast ./contracts/ui.surface.ast.json \
--surface customer-app \
--remote-url https://app.example.com/dashboard \
--format json \
Expand Down Expand Up @@ -159,12 +165,14 @@ interfacectl enforce [options]

### `compile`

Compiles a validated interface contract into a deterministic generation-and-runtime bundle. The bundle includes a manifest, `contract/normalized.json`, and per-surface slices for downstream generators, runtime adapters, repair guidance, and workbench consumers.
Compiles a validated UI AST into a deterministic generation-and-runtime bundle. The canonical bundle includes `ast/normalized.json`, per-surface AST/platform slices, and a derived contract compatibility file for downstream consumers that still need contract-shaped data.

`--contract` remains available here only to import or bridge legacy inputs during migration. New consumers should compile from `--ast`.

This command does **not** perform enforcement or runtime gating. It produces a stable artifact intended for inspection, tooling, or future runtime consumption.

```bash
interfacectl compile --contract <path> --out <dir>
interfacectl compile --ast <path> --out <dir>
```

### `prepare-generation`
Expand Down
58 changes: 58 additions & 0 deletions docs/plans/ui-ast-v2-cutover-pr-description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# PR Description: UI AST V2 Cutover

Use this content when opening the PR. Reference: docs/plans/ui-ast-v2-cutover-rfc.md.

---

## Title

feat: make UI AST the canonical governed UI contract

---

## Strategy check

- [x] I read "docs/strategy.md"
- [x] This PR strengthens the decision filter sentence by moving governed UI review and enforcement to a bounded semantic contract
- [x] Enforcement timing is explicit: generation time plus downstream runtime-consumption preparation
- [x] Violation handling is defined: unsupported or invalid AST input fails closed; legacy contract input is migration-only compatibility
- [x] CLI behavior is tied to governed UI semantics, not free-form UI generation

## What changed

- Added a canonical UI AST schema, types, and validator entrypoints for bounded governed UI semantics.
- Added AST resolution and migration helpers so CLI commands resolve `--ast` first and only fall back to legacy `--contract` input for migration compatibility.
- Added `migrate-ui-ast` to deterministically import legacy `web.surface.contract` files into AST drafts.
- Made `compile`, `prepare-generation`, and `prepare-runtime` AST-first on bundle format `3.0`.
- Canonical bundle artifacts are now `ast/normalized.json`, `surfaces/<surface>/ast.json`, and `surfaces/<surface>/platforms.json`, with `derived/contract.normalized.json` kept only for compatibility.
- Updated tests and compile goldens for AST-first bundle output, including multi-platform projection coverage.
- Added RFC/README framing so UI AST is the lead model and legacy contracts are described as migration-only compatibility.

## Why it matters

This makes the semantic contract explicit and reviewable before rendering. Models, validators, runtime consumers, and downstream repos now meet on one bounded artifact instead of inferring meaning from implementation-shaped input. It also creates a clear migration path off the legacy contract model without pretending both models are long-term co-equals.

## Contract and enforcement notes

1. Canonical contract model changed from legacy `web.surface.contract` input to UI AST v2 input.
2. Enforcement point remains generation time; runtime consumers still consume prepared artifacts derived from the compiled bundle.
3. Legacy contract support is explicitly deprecated and migration-only. Invalid AST or invalid migrated AST output fails closed.

## Not in scope

- No second rollout surface beyond the existing benchmark proof downstream.
- No AST-native local-workbench authoring flow.
- No broad consumer-doc sweep or root-script deprecation cleanup beyond the AST-first framing needed for this cutover.

## Tests

- `packages/interfacectl-validator`: `node --test test/authoring-contract.test.mjs test/ui-ast.test.mjs`
- `packages/interfacectl-cli`: `node --test test/color-deprecation.test.mjs test/compile.test.mjs test/prepare-generation.test.mjs test/prepare-runtime.test.mjs test/generation-adapter.test.mjs test/migrate-ui-ast.test.mjs`

## Review checklist

- [x] UI AST is clearly the canonical contract model in code and docs.
- [x] Legacy `--contract` support is described as migration-only compatibility.
- [x] Bundle `3.0` canonical files are AST-first and compatibility output is explicit.
- [x] Migration command and AST schema coverage are test-backed.
- [x] Scope stays limited to the AST cutover itself, without adding more rollout surfaces.
103 changes: 103 additions & 0 deletions docs/plans/ui-ast-v2-cutover-rfc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# UI AST V2 Cutover RFC

## Decision

Adopt UI AST v2 as the canonical semantic contract for governed UI in `interfacectl`.

Legacy `web.surface.contract` remains supported only as a migration input. It is no longer the canonical artifact.

## Why

- Intent needs a bounded, reviewable artifact before rendering.
- Generation should stop handing arbitrary UI code directly to downstream systems.
- Governance, accessibility, and design-system checks should run at the semantic boundary instead of after implementation drift appears.
- Multi-platform output needs one durable source of truth for node identity, actions, and policy metadata.

## Scope

v1 AST scope is limited to governed application surfaces:

- settings pages
- forms
- onboarding flows
- transactional detail views
- empty states
- alerts and confirmations
- simple list and table surfaces
- account and preference management

Excluded from the first rollout:

- marketing pages
- bespoke editorial experiences
- unconstrained canvases
- custom data visualizations
- animation-led or experimental interaction models

## Canonical Artifact

Canonical input path:

- `contracts/ui.surface.ast.json`

Primary CLI flag:

- `--ast`

Legacy compatibility input:

- `--contract` for existing `web.surface.contract` JSON
- deprecated and migration-oriented only

## Bundle Changes

Compiled bundles now use format `3.0`.

Canonical bundle source files:

- `ast/normalized.json`
- `surfaces/<surface>/ast.json`
- `surfaces/<surface>/platforms.json`

Compatibility output remains available for downstream consumers that still need contract-shaped data:

- `derived/contract.normalized.json`

## Implementation Notes

- Validator owns the UI AST schema and bounded vocabulary.
- CLI resolves AST first, falls back to legacy contracts, and migrates legacy input into AST drafts deterministically.
- `compile`, `prepare-generation`, and `prepare-runtime` now treat AST as the normalized source artifact.
- `migrate-ui-ast` imports legacy contracts into AST drafts and emits escalation markers when semantics cannot be preserved safely.

## Rollout

Phase 1:

- land AST schema, migration command, bundle v3, and AST-aware preparation flows in `interfacectl`

Phase 2:

- prove consumer compatibility in `surfaces-webapps` on `benchmark-async-data-web`
- validate the live fixture against the AST draft
- compile and prepare generation/runtime payloads from the AST-derived bundle

Phase 3:

- migrate additional governed surfaces
- remove legacy contract-as-canonical assumptions from downstream tooling

## Guardrails

- Semantics over presentation
- Stable node identity across generation, approval, rendering, and observation
- Bounded vocabulary over free-form styling
- Governance metadata is first-class
- Unsupported cases fail closed or escalate

## Exit Criteria

- AST schema fixtures cover valid ASTs plus unsupported styling, logic, ids, and vocabulary failures
- bundle v3 is deterministic and proven with multi-platform projections
- migration from legacy contracts is deterministic and test-covered
- `surfaces-webapps` consumes AST-derived bundles on at least one governed benchmark surface
5 changes: 4 additions & 1 deletion packages/interfacectl-cli/dist/adapter/bundle.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export declare const SUPPORTED_BUNDLE_VERSION = "2.0";
export declare const SUPPORTED_BUNDLE_VERSION = "3.0";
export interface JsonRecord {
[key: string]: unknown;
}
Expand All @@ -17,10 +17,13 @@ export interface LoadedCompiledSurfaceBundle {
contractId: string;
contractVersion: string;
manifest: LoadedJsonFile<BundleManifest>;
ast?: LoadedJsonFile;
contract: LoadedJsonFile;
surface: {
id: string;
dir: string;
ast?: LoadedJsonFile;
platforms?: LoadedJsonFile;
generation: LoadedJsonFile;
sections: LoadedJsonFile;
components: LoadedJsonFile;
Expand Down
2 changes: 1 addition & 1 deletion packages/interfacectl-cli/dist/adapter/bundle.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 47 additions & 5 deletions packages/interfacectl-cli/dist/adapter/bundle.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "node:fs";
import path from "node:path";
export const SUPPORTED_BUNDLE_VERSION = "2.0";
export const SUPPORTED_BUNDLE_VERSION = "3.0";
const SUPPORTED_BUNDLE_VERSIONS = new Set(["2.0", "3.0"]);
export class AdapterInputError extends Error {
code;
meta;
Expand Down Expand Up @@ -58,8 +59,8 @@ export function loadCompiledSurfaceBundle(bundleRootInput, surfaceId, cwd) {
const manifestPath = path.join(bundleRoot, "manifest.json");
ensureReadableFile(manifestPath, "Bundle manifest");
const manifest = readJsonFile(manifestPath, "bundle manifest");
if (manifest.bundleVersion !== SUPPORTED_BUNDLE_VERSION) {
throw new AdapterInputError(`Unsupported bundle version "${manifest.bundleVersion ?? "unknown"}". Expected ${SUPPORTED_BUNDLE_VERSION}.`, { code: "adapter.bundle.version-unsupported" });
if (!SUPPORTED_BUNDLE_VERSIONS.has(manifest.bundleVersion ?? "")) {
throw new AdapterInputError(`Unsupported bundle version "${manifest.bundleVersion ?? "unknown"}". Expected one of ${[...SUPPORTED_BUNDLE_VERSIONS].join(", ")}.`, { code: "adapter.bundle.version-unsupported" });
}
const surfaceDir = path.join(bundleRoot, "surfaces", surfaceId);
ensureReadableDirectory(surfaceDir, "Surface bundle");
Expand Down Expand Up @@ -98,9 +99,23 @@ export function loadCompiledSurfaceBundle(bundleRootInput, surfaceId, cwd) {
value: readJsonFile(repairMapPath, "repair map"),
};
const refs = isRecord(generation.value.refs) ? generation.value.refs : {};
let ast;
if (manifest.bundleVersion === "3.0") {
const astRef = typeof refs.ast === "string" && refs.ast.trim().length > 0
? refs.ast
: "../../ast/normalized.json";
const astPath = path.resolve(path.dirname(generationPath), astRef);
ensureReadableFile(astPath, "Compiled UI AST");
ast = {
path: astPath,
value: readJsonFile(astPath, "Compiled UI AST"),
};
}
const contractRef = typeof refs.contract === "string" && refs.contract.trim().length > 0
? refs.contract
: "../../contract/normalized.json";
: manifest.bundleVersion === "3.0"
? "../../derived/contract.normalized.json"
: "../../contract/normalized.json";
const contractPath = path.resolve(path.dirname(generationPath), contractRef);
ensureReadableFile(contractPath, "Compiled contract");
const contract = {
Expand All @@ -127,6 +142,30 @@ export function loadCompiledSurfaceBundle(bundleRootInput, surfaceId, cwd) {
value: readJsonFile(runtimePath, "Runtime bundle"),
};
}
let surfaceAst;
if (manifest.bundleVersion === "3.0") {
const astSliceRef = typeof refs.astSlice === "string" && refs.astSlice.trim().length > 0
? refs.astSlice
: "./ast.json";
const astSlicePath = path.resolve(path.dirname(generationPath), astSliceRef);
ensureReadableFile(astSlicePath, "Surface AST bundle");
surfaceAst = {
path: astSlicePath,
value: readJsonFile(astSlicePath, "Surface AST bundle"),
};
}
let platforms;
if (manifest.bundleVersion === "3.0") {
const platformsRef = typeof refs.platforms === "string" && refs.platforms.trim().length > 0
? refs.platforms
: "./platforms.json";
const platformsPath = path.resolve(path.dirname(generationPath), platformsRef);
ensureReadableFile(platformsPath, "Surface platform bundle");
platforms = {
path: platformsPath,
value: readJsonFile(platformsPath, "Surface platform bundle"),
};
}
const generationProvenance = isRecord(generation.value.provenance)
? generation.value.provenance
: undefined;
Expand All @@ -142,17 +181,20 @@ export function loadCompiledSurfaceBundle(bundleRootInput, surfaceId, cwd) {
"unknown";
return {
root: bundleRoot,
version: SUPPORTED_BUNDLE_VERSION,
version: manifest.bundleVersion ?? SUPPORTED_BUNDLE_VERSION,
contractId,
contractVersion,
manifest: {
path: manifestPath,
value: manifest,
},
...(ast ? { ast } : {}),
contract,
surface: {
id: surfaceId,
dir: surfaceDir,
...(surfaceAst ? { ast: surfaceAst } : {}),
...(platforms ? { platforms } : {}),
generation,
sections,
components,
Expand Down
3 changes: 2 additions & 1 deletion packages/interfacectl-cli/dist/commands/compile.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface CompileCommandOptions {
contractPath: string;
astPath?: string;
contractPath?: string;
outDir: string;
schemaPath?: string;
format?: "json";
Expand Down
2 changes: 1 addition & 1 deletion packages/interfacectl-cli/dist/commands/compile.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading