From c3eb99aac7cf0bfbe9c367f11ab8792b49761d86 Mon Sep 17 00:00:00 2001 From: Mike Long Date: Sat, 7 Feb 2026 00:37:28 -0800 Subject: [PATCH] Enforce shell-owned primitives via contract; add extractor/validator support --- docs/contract-baseline.md | 11 + .../src/descriptors/static-analysis.ts | 44 +- packages/interfacectl-extractor/src/types.ts | 6 + .../simple-app/app/auth/login/route.ts | 3 + .../test/fixtures/simple-app/app/layout.tsx | 7 + .../test/fixtures/simple-app/app/page.tsx | 8 + .../test/primitives.test.mjs | 20 + .../dist/index.d.ts.map | 2 +- packages/interfacectl-validator/dist/index.js | 22 + .../schema/web.surface.contract.schema.json | 514 +++++++++++++++++- .../interfacectl-validator/dist/types.d.ts | 14 +- .../dist/types.d.ts.map | 2 +- packages/interfacectl-validator/src/index.ts | 25 + .../schema/web.surface.contract.schema.json | 514 +++++++++++++++++- packages/interfacectl-validator/src/types.ts | 17 +- .../test/primitives.test.mjs | 80 +++ 16 files changed, 1282 insertions(+), 7 deletions(-) create mode 100644 packages/interfacectl-extractor/test/fixtures/simple-app/app/auth/login/route.ts create mode 100644 packages/interfacectl-extractor/test/fixtures/simple-app/app/layout.tsx create mode 100644 packages/interfacectl-extractor/test/fixtures/simple-app/app/page.tsx create mode 100644 packages/interfacectl-extractor/test/primitives.test.mjs create mode 100644 packages/interfacectl-validator/test/primitives.test.mjs diff --git a/docs/contract-baseline.md b/docs/contract-baseline.md index c8ea987..324d183 100644 --- a/docs/contract-baseline.md +++ b/docs/contract-baseline.md @@ -40,6 +40,17 @@ The top-level **color** object is optional. When present it can include: - **semantics**: Optional **roles** (accent, text, background, border) each with **enforcement** (`"off"`, `"warn"`, `"strict"`). - **consistency**: Optional **acrossSurfaces** object with **enforcement** level and **signals** used for reporting. +## Optional: shell ownership boundary (new) + +Add these when a shell provides global layout so generators cannot recreate it inside surfaces: + +- **shell** (object): + - **owns** (array of strings): Shell-owned primitives (e.g. `nav`, `header`, `sidebar`, `authWrapper`, `appChrome`). Generation should not emit these inside surfaces. + - **contentSlot** (string): Identifier for the shell’s content mount point (frame name or DOM id). +- **surface.mustNotEmit** (array of strings, per-surface): Optional explicit ban list. If omitted, generators should default this to **shell.owns**. + +Backwards compatibility: `shell` is optional; existing contracts remain valid. Generators and validators should default an undefined **mustNotEmit** to **shell.owns** when present. + ## Deprecated fields - **allowedColors** (per-surface): Deprecated. The schema still accepts it for compatibility. Prefer the top-level **color** policy with **sourceOfTruth** and **rawValues**. Migration: move per-surface color allowlists into **color.sourceOfTruth** (e.g. token namespaces) or **color.rawValues** (allowlist/denylist). The CLI emits a deprecation warning when allowedColors is present. Deprecated fields are accepted for compatibility but may be removed in a future major version. diff --git a/packages/interfacectl-cli/src/descriptors/static-analysis.ts b/packages/interfacectl-cli/src/descriptors/static-analysis.ts index 9af1522..d01d4b2 100644 --- a/packages/interfacectl-cli/src/descriptors/static-analysis.ts +++ b/packages/interfacectl-cli/src/descriptors/static-analysis.ts @@ -10,6 +10,7 @@ import { type SurfaceMotionDescriptor, type SurfaceSectionDescriptor, type PageFrameLayoutDescriptor, + type SurfacePrimitiveDescriptor, } from "@surfaces/interfacectl-validator"; const SECTION_ATTRIBUTE_REGEX = @@ -70,6 +71,15 @@ const DURATION_DECL_REGEX = /(animation|transition)-duration\s*:\s*([^;]+);/gi; const TIMING_DECL_REGEX = /(animation|transition)-timing-function\s*:\s*([^;]+);/gi; +const NAV_REGEX = / = [ + { role: "nav", regex: NAV_REGEX }, + { role: "header", regex: HEADER_REGEX }, + { role: "sidebar", regex: ASIDE_REGEX }, +]; + export interface DescriptorIssue { surfaceId?: string; code: string; @@ -243,6 +253,8 @@ async function extractSurfaceDescriptor( }); } + const primitives = await extractPrimitives(sectionFiles, workspaceRoot, fileContentCache); + const structuralSurfaceDescriptor: SurfaceDescriptor = { surfaceId, sections, @@ -250,6 +262,7 @@ async function extractSurfaceDescriptor( colors, layout, motion, + primitives, }; return { descriptor: structuralSurfaceDescriptor, warnings, errors }; @@ -916,6 +929,36 @@ function collectColorsFromContent( } } +async function extractPrimitives( + sectionFiles: string[], + workspaceRoot: string, + fileContentCache: Map, +): Promise { + const counts = new Map }>(); + + for (const filePath of sectionFiles) { + const content = await readFileCached(filePath, fileContentCache); + for (const { role, regex } of PRIMITIVE_PATTERNS) { + regex.lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = regex.exec(content)) !== null) { + const entry = counts.get(role) ?? { count: 0, sources: new Set() }; + entry.count += 1; + entry.sources.add(path.relative(workspaceRoot, filePath)); + counts.set(role, entry); + } + } + } + + return [...counts.entries()] + .map(([role, { count, sources }]) => ({ + role, + count, + sources: [...sources], + })) + .sort((a, b) => a.role.localeCompare(b.role)); +} + function parseColorValue(value: string): string[] { const colors: string[] = []; const trimmed = value.trim(); @@ -1166,4 +1209,3 @@ function isTimingFunction(token: string): boolean { function toMotionKey(durationMs: number, timing: string): string { return `${durationMs}:${timing}`; } - diff --git a/packages/interfacectl-extractor/src/types.ts b/packages/interfacectl-extractor/src/types.ts index f45b300..35ca783 100644 --- a/packages/interfacectl-extractor/src/types.ts +++ b/packages/interfacectl-extractor/src/types.ts @@ -9,6 +9,12 @@ export interface ExtractedContractFields { authAware: boolean; } +export interface ExtractedPrimitives { + role: string; + count: number; + sources?: string[]; +} + export interface ExtractionWarning { code: string; message: string; diff --git a/packages/interfacectl-extractor/test/fixtures/simple-app/app/auth/login/route.ts b/packages/interfacectl-extractor/test/fixtures/simple-app/app/auth/login/route.ts new file mode 100644 index 0000000..3b7a507 --- /dev/null +++ b/packages/interfacectl-extractor/test/fixtures/simple-app/app/auth/login/route.ts @@ -0,0 +1,3 @@ +export async function GET() { + return new Response("OK"); +} diff --git a/packages/interfacectl-extractor/test/fixtures/simple-app/app/layout.tsx b/packages/interfacectl-extractor/test/fixtures/simple-app/app/layout.tsx new file mode 100644 index 0000000..db8dfec --- /dev/null +++ b/packages/interfacectl-extractor/test/fixtures/simple-app/app/layout.tsx @@ -0,0 +1,7 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/packages/interfacectl-extractor/test/fixtures/simple-app/app/page.tsx b/packages/interfacectl-extractor/test/fixtures/simple-app/app/page.tsx new file mode 100644 index 0000000..d90099a --- /dev/null +++ b/packages/interfacectl-extractor/test/fixtures/simple-app/app/page.tsx @@ -0,0 +1,8 @@ +export default function Page() { + return ( +
+ +
Hello
+
+ ); +} diff --git a/packages/interfacectl-extractor/test/primitives.test.mjs b/packages/interfacectl-extractor/test/primitives.test.mjs new file mode 100644 index 0000000..3d8e1f4 --- /dev/null +++ b/packages/interfacectl-extractor/test/primitives.test.mjs @@ -0,0 +1,20 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { extractContractFromNextApp } from "../dist/index.js"; +import path from "node:path"; + +const fixturesRoot = path.join(process.cwd(), "test", "fixtures"); +const fixture = path.join(fixturesRoot, "simple-app"); + +test("extracts nav/header primitives from section files", async () => { + const { report } = await extractContractFromNextApp({ + appRoot: fixture, + surfaceId: "runs", + }); + + const primitives = report.extracted.primitives; + assert.ok(Array.isArray(primitives)); + const nav = primitives.find((p) => p.role === "nav"); + assert.ok(nav); + assert.equal(nav.count, 1); +}); diff --git a/packages/interfacectl-validator/dist/index.d.ts.map b/packages/interfacectl-validator/dist/index.d.ts.map index 660d83c..4452476 100644 --- a/packages/interfacectl-validator/dist/index.d.ts.map +++ b/packages/interfacectl-validator/dist/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EAKlB,MAAM,YAAY,CAAC;AAOpB,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED,MAAM,WAAW,2BAA2B;IAC1C,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,OAAO,EACrB,MAAM,EAAE,MAAM,GACb,2BAA2B,CAyB7B;AA4ID,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,EAAE,iBAAiB,GAC5B,aAAa,CAqUf;AAED,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,iBAAiB,EAC3B,WAAW,EAAE,iBAAiB,EAAE,GAC/B,iBAAiB,CA6CnB;AAyCD,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,yBAAyB,EACzB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,OAAO,EACP,UAAU,EACV,SAAS,EACT,cAAc,EACd,SAAS,EACT,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,UAAU,EACV,QAAQ,EACR,QAAQ,GACT,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,0BAA0B,EAC1B,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,KAAK,gBAAgB,GACtB,MAAM,sBAAsB,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EAKlB,MAAM,YAAY,CAAC;AAOpB,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED,MAAM,WAAW,2BAA2B;IAC1C,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,OAAO,EACrB,MAAM,EAAE,MAAM,GACb,2BAA2B,CAyB7B;AA4ID,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,EAAE,iBAAiB,GAC5B,aAAa,CA8Vf;AAED,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,iBAAiB,EAC3B,WAAW,EAAE,iBAAiB,EAAE,GAC/B,iBAAiB,CA6CnB;AAyCD,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,yBAAyB,EACzB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,OAAO,EACP,UAAU,EACV,SAAS,EACT,cAAc,EACd,SAAS,EACT,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,UAAU,EACV,QAAQ,EACR,QAAQ,GACT,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,0BAA0B,EAC1B,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,KAAK,gBAAgB,GACtB,MAAM,sBAAsB,CAAC"} \ No newline at end of file diff --git a/packages/interfacectl-validator/dist/index.js b/packages/interfacectl-validator/dist/index.js index 13b10c2..ae084c3 100644 --- a/packages/interfacectl-validator/dist/index.js +++ b/packages/interfacectl-validator/dist/index.js @@ -209,6 +209,28 @@ export function evaluateSurfaceCompliance(contract, descriptor) { } } } + // Shell-owned primitives check + const banList = new Set((surface.mustNotEmit && surface.mustNotEmit.length > 0 + ? surface.mustNotEmit + : contract.shell?.owns ?? [])); + if (banList.size > 0 && descriptor.primitives) { + for (const primitive of descriptor.primitives) { + if (banList.has(primitive.role) && primitive.count > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "shell-owned-primitive-emitted", + message: `Primitive "${primitive.role}" is shell-owned and must not be emitted. Count: ${primitive.count}.`, + details: { + role: primitive.role, + count: primitive.count, + sources: primitive.sources, + banList: [...banList], + jsonPointer: "/shell/owns", + }, + }); + } + } + } // New color policy validation if (contract.color) { validateColorPolicy(contract, descriptor, violations); diff --git a/packages/interfacectl-validator/dist/schema/web.surface.contract.schema.json b/packages/interfacectl-validator/dist/schema/web.surface.contract.schema.json index 6c0895b..316ae11 100644 --- a/packages/interfacectl-validator/dist/schema/web.surface.contract.schema.json +++ b/packages/interfacectl-validator/dist/schema/web.surface.contract.schema.json @@ -1 +1,513 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://contracts.surfaces.local/web.surface.contract.schema.json","title":"Web Surface Contract Schema","description":"Built-in contract type schema for the web surface runtime. Not application-specific.","type":"object","additionalProperties":false,"required":["contractId","version","surfaces","sections","constraints"],"properties":{"$schema":{"type":"string","format":"uri-reference"},"contractId":{"type":"string","minLength":1},"version":{"type":"string","pattern":"^[0-9]+\\.[0-9]+\\.[0-9]+$"},"description":{"type":"string"},"surfaces":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/surface"}},"sections":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/section"}},"constraints":{"$ref":"#/$defs/constraints"},"color":{"$ref":"#/$defs/color"},"x_extracted":{"type":"object","description":"Phase 0: fields extracted from app code; not used by validator semantics.","additionalProperties":true,"properties":{"routes":{"type":"array","items":{"type":"string"},"uniqueItems":true},"hasShell":{"type":"boolean"},"designSystemComponents":{"type":"array","items":{"type":"string"},"uniqueItems":true},"authAware":{"type":"boolean"}}}},"$defs":{"surface":{"type":"object","additionalProperties":false,"required":["id","displayName","type","requiredSections","allowedFonts","layout"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9-]+$"},"displayName":{"type":"string","minLength":1},"type":{"type":"string","enum":["web","cli"]},"requiredSections":{"type":"array","minItems":1,"items":{"type":"string","pattern":"^[a-z0-9]+(\\.[a-z0-9]+)*$"},"uniqueItems":true},"allowedFonts":{"type":"array","minItems":1,"items":{"type":"string","minLength":1},"uniqueItems":true},"allowedColors":{"type":"array","minItems":1,"items":{"type":"string","minLength":1},"uniqueItems":true,"description":"Deprecated: Use color.sourceOfTruth and color.rawValues instead. This field will be removed in a future version."},"layout":{"type":"object","additionalProperties":false,"required":["maxContentWidth"],"properties":{"maxContentWidth":{"type":"number","minimum":1},"requiredContainers":{"type":"array","items":{"type":"string","minLength":1},"uniqueItems":true},"pageFrame":{"$ref":"#/$defs/pageFrame"}}},"phase0":{"type":"object","description":"Phase 0.5: optional expectations for extracted reality (routes, auth, shell, design system).","additionalProperties":false,"properties":{"authPosture":{"type":"string","enum":["public","auth-aware","auth-first"]},"requiresShell":{"type":"boolean"},"expectsAuthRoutes":{"type":"boolean"},"expectsDesignSystem":{"type":"boolean"}}},"domain":{"type":"string","description":"Informational: production domain (e.g. www.surfaces.systems)."},"owner":{"type":"string","description":"Phase 2: informational owner (email or GitHub handle). No enforcement."}}},"section":{"type":"object","additionalProperties":false,"required":["id","intent","description"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9]+(\\.[a-z0-9]+)*$"},"intent":{"type":"string","minLength":1},"description":{"type":"string","minLength":1}}},"constraints":{"type":"object","additionalProperties":false,"required":["motion"],"properties":{"motion":{"type":"object","additionalProperties":false,"required":["allowedDurationsMs","allowedTimingFunctions"],"properties":{"allowedDurationsMs":{"type":"array","minItems":1,"items":{"type":"integer","minimum":1},"uniqueItems":true},"allowedTimingFunctions":{"type":"array","minItems":1,"items":{"type":"string","minLength":1},"uniqueItems":true}}}}},"pageFrame":{"type":"object","additionalProperties":false,"required":["containerSelector","containerMaxWidthPx","paddingXpx"],"properties":{"containerSelector":{"type":"string","minLength":1},"containerMaxWidthPx":{"type":"number","minimum":0},"paddingXpx":{"type":"number","minimum":0},"alignment":{"type":"string","enum":["center","left"],"default":"center"},"enforcement":{"type":"string","enum":["strict","warn"],"default":"strict"}}},"color":{"type":"object","additionalProperties":false,"properties":{"sourceOfTruth":{"$ref":"#/$defs/colorSourceOfTruth"},"rawValues":{"$ref":"#/$defs/colorRawValues"},"semantics":{"$ref":"#/$defs/colorSemantics"},"consistency":{"$ref":"#/$defs/colorConsistency"}}},"colorSourceOfTruth":{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"type":"string","enum":["tokens","none"]},"tokenNamespaces":{"type":"array","items":{"type":"string","minLength":1},"uniqueItems":true}},"if":{"properties":{"type":{"const":"tokens"}}},"then":{"required":["tokenNamespaces"]}},"colorRawValues":{"type":"object","additionalProperties":false,"required":["policy"],"properties":{"policy":{"type":"string","enum":["off","warn","strict"]},"allowlist":{"type":"array","items":{"type":"string","minLength":1},"uniqueItems":true},"denylist":{"type":"array","items":{"type":"string","minLength":1},"uniqueItems":true}}},"colorSemantics":{"type":"object","additionalProperties":false,"properties":{"roles":{"type":"object","additionalProperties":false,"properties":{"accent":{"type":"object","additionalProperties":false,"properties":{"enforcement":{"type":"string","enum":["off","warn","strict"]}}},"text":{"type":"object","additionalProperties":false,"properties":{"enforcement":{"type":"string","enum":["off","warn","strict"]}}},"background":{"type":"object","additionalProperties":false,"properties":{"enforcement":{"type":"string","enum":["off","warn","strict"]}}},"border":{"type":"object","additionalProperties":false,"properties":{"enforcement":{"type":"string","enum":["off","warn","strict"]}}}}}}},"colorConsistency":{"type":"object","additionalProperties":false,"properties":{"acrossSurfaces":{"type":"object","additionalProperties":false,"properties":{"enforcement":{"type":"string","enum":["off","warn","strict"]},"signals":{"type":"array","items":{"type":"string","enum":["token-name","css-var-name","class-fragment"]},"uniqueItems":true}}}}}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://contracts.surfaces.local/web.surface.contract.schema.json", + "title": "Web Surface Contract Schema", + "description": "Built-in contract type schema for the web surface runtime. Not application-specific.", + "type": "object", + "additionalProperties": false, + "required": [ + "contractId", + "version", + "surfaces", + "sections", + "constraints" + ], + "properties": { + "$schema": { + "type": "string", + "format": "uri-reference" + }, + "contractId": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "description": { + "type": "string" + }, + "surfaces": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/surface" + } + }, + "sections": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/section" + } + }, + "constraints": { + "$ref": "#/$defs/constraints" + }, + "color": { + "$ref": "#/$defs/color" + }, + "x_extracted": { + "type": "object", + "description": "Phase 0: fields extracted from app code; not used by validator semantics.", + "additionalProperties": true, + "properties": { + "routes": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "hasShell": { + "type": "boolean" + }, + "designSystemComponents": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "authAware": { + "type": "boolean" + } + } + }, + "shell": { + "$ref": "#/$defs/shell" + } + }, + "$defs": { + "surface": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "displayName", + "type", + "requiredSections", + "allowedFonts", + "layout" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "displayName": { + "type": "string", + "minLength": 1 + }, + "type": { + "type": "string", + "enum": [ + "web", + "cli" + ] + }, + "requiredSections": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-z0-9]+(\\.[a-z0-9]+)*$" + }, + "uniqueItems": true + }, + "allowedFonts": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "allowedColors": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true, + "description": "Deprecated: Use color.sourceOfTruth and color.rawValues instead. This field will be removed in a future version." + }, + "layout": { + "type": "object", + "additionalProperties": false, + "required": [ + "maxContentWidth" + ], + "properties": { + "maxContentWidth": { + "type": "number", + "minimum": 1 + }, + "requiredContainers": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "pageFrame": { + "$ref": "#/$defs/pageFrame" + } + } + }, + "phase0": { + "type": "object", + "description": "Phase 0.5: optional expectations for extracted reality (routes, auth, shell, design system).", + "additionalProperties": false, + "properties": { + "authPosture": { + "type": "string", + "enum": [ + "public", + "auth-aware", + "auth-first" + ] + }, + "requiresShell": { + "type": "boolean" + }, + "expectsAuthRoutes": { + "type": "boolean" + }, + "expectsDesignSystem": { + "type": "boolean" + } + } + }, + "domain": { + "type": "string", + "description": "Informational: production domain (e.g. www.surfaces.systems)." + }, + "owner": { + "type": "string", + "description": "Phase 2: informational owner (email or GitHub handle). No enforcement." + }, + "mustNotEmit": { + "type": "array", + "description": "Elements or primitives the surface must not generate because the shell owns them. If omitted, consumers should default to shell.owns when present.", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + } + } + }, + "section": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "intent", + "description" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9]+(\\.[a-z0-9]+)*$" + }, + "intent": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + } + } + }, + "constraints": { + "type": "object", + "additionalProperties": false, + "required": [ + "motion" + ], + "properties": { + "motion": { + "type": "object", + "additionalProperties": false, + "required": [ + "allowedDurationsMs", + "allowedTimingFunctions" + ], + "properties": { + "allowedDurationsMs": { + "type": "array", + "minItems": 1, + "items": { + "type": "integer", + "minimum": 1 + }, + "uniqueItems": true + }, + "allowedTimingFunctions": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + } + } + } + } + }, + "pageFrame": { + "type": "object", + "additionalProperties": false, + "required": [ + "containerSelector", + "containerMaxWidthPx", + "paddingXpx" + ], + "properties": { + "containerSelector": { + "type": "string", + "minLength": 1 + }, + "containerMaxWidthPx": { + "type": "number", + "minimum": 0 + }, + "paddingXpx": { + "type": "number", + "minimum": 0 + }, + "alignment": { + "type": "string", + "enum": [ + "center", + "left" + ], + "default": "center" + }, + "enforcement": { + "type": "string", + "enum": [ + "strict", + "warn" + ], + "default": "strict" + } + } + }, + "color": { + "type": "object", + "additionalProperties": false, + "properties": { + "sourceOfTruth": { + "$ref": "#/$defs/colorSourceOfTruth" + }, + "rawValues": { + "$ref": "#/$defs/colorRawValues" + }, + "semantics": { + "$ref": "#/$defs/colorSemantics" + }, + "consistency": { + "$ref": "#/$defs/colorConsistency" + } + } + }, + "colorSourceOfTruth": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "tokens", + "none" + ] + }, + "tokenNamespaces": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + } + }, + "if": { + "properties": { + "type": { + "const": "tokens" + } + } + }, + "then": { + "required": [ + "tokenNamespaces" + ] + } + }, + "colorRawValues": { + "type": "object", + "additionalProperties": false, + "required": [ + "policy" + ], + "properties": { + "policy": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + }, + "allowlist": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "denylist": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + } + } + }, + "colorSemantics": { + "type": "object", + "additionalProperties": false, + "properties": { + "roles": { + "type": "object", + "additionalProperties": false, + "properties": { + "accent": { + "type": "object", + "additionalProperties": false, + "properties": { + "enforcement": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + } + } + }, + "text": { + "type": "object", + "additionalProperties": false, + "properties": { + "enforcement": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + } + } + }, + "background": { + "type": "object", + "additionalProperties": false, + "properties": { + "enforcement": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + } + } + }, + "border": { + "type": "object", + "additionalProperties": false, + "properties": { + "enforcement": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + } + } + } + } + } + } + }, + "colorConsistency": { + "type": "object", + "additionalProperties": false, + "properties": { + "acrossSurfaces": { + "type": "object", + "additionalProperties": false, + "properties": { + "enforcement": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + }, + "signals": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "token-name", + "css-var-name", + "class-fragment" + ] + }, + "uniqueItems": true + } + } + } + } + }, + "shell": { + "type": "object", + "additionalProperties": false, + "properties": { + "owns": { + "type": "array", + "description": "Shell-owned layout primitives. Generators must not emit these inside surfaces.", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "contentSlot": { + "type": "string", + "description": "Identifier for the shell's content mount point (e.g. a frame or DOM id).", + "minLength": 1 + } + }, + "required": [] + } + } +} \ No newline at end of file diff --git a/packages/interfacectl-validator/dist/types.d.ts b/packages/interfacectl-validator/dist/types.d.ts index 5ee66dc..5eab4a3 100644 --- a/packages/interfacectl-validator/dist/types.d.ts +++ b/packages/interfacectl-validator/dist/types.d.ts @@ -18,6 +18,7 @@ export interface ContractSurface { requiredContainers?: string[]; pageFrame?: PageFrameLayout; }; + mustNotEmit?: string[]; } export interface ContractSection { id: string; @@ -30,6 +31,10 @@ export interface ContractConstraints { allowedTimingFunctions: string[]; }; } +export interface ShellSpec { + owns?: string[]; + contentSlot?: string; +} export interface ColorSourceOfTruth { type: "tokens" | "none"; tokenNamespaces?: string[]; @@ -70,6 +75,7 @@ export interface InterfaceContract { sections: ContractSection[]; constraints: ContractConstraints; color?: ColorPolicy; + shell?: ShellSpec; } export interface SurfaceSectionDescriptor { id: string; @@ -88,6 +94,11 @@ export interface SurfaceMotionDescriptor { timingFunction: string; source?: string; } +export interface SurfacePrimitiveDescriptor { + role: string; + count: number; + sources?: string[]; +} export interface PageFrameLayoutDescriptor { containerSelector: string; maxWidthPx?: number | null; @@ -111,8 +122,9 @@ export interface SurfaceDescriptor { colors: SurfaceColorDescriptor[]; layout: SurfaceLayoutDescriptor; motion: SurfaceMotionDescriptor[]; + primitives?: SurfacePrimitiveDescriptor[]; } -export type DriftViolationType = "unknown-surface" | "missing-section" | "unknown-section" | "font-not-allowed" | "color-not-allowed" | "layout-width-exceeded" | "layout-width-undetermined" | "layout-container-missing" | "layout-pageframe-selector-unsupported" | "layout-pageframe-container-not-found" | "layout-pageframe-maxwidth-mismatch" | "layout-pageframe-padding-mismatch" | "layout-pageframe-non-deterministic-value" | "layout-pageframe-unextractable-value" | "motion-duration-not-allowed" | "motion-timing-not-allowed" | "descriptor-missing" | "descriptor-unused" | "color-raw-value-used" | "color-token-namespace-violation"; +export type DriftViolationType = "unknown-surface" | "missing-section" | "unknown-section" | "font-not-allowed" | "color-not-allowed" | "layout-width-exceeded" | "layout-width-undetermined" | "layout-container-missing" | "layout-pageframe-selector-unsupported" | "layout-pageframe-container-not-found" | "layout-pageframe-maxwidth-mismatch" | "layout-pageframe-padding-mismatch" | "layout-pageframe-non-deterministic-value" | "layout-pageframe-unextractable-value" | "motion-duration-not-allowed" | "motion-timing-not-allowed" | "descriptor-missing" | "descriptor-unused" | "color-raw-value-used" | "color-token-namespace-violation" | "shell-owned-primitive-emitted"; export interface DriftViolation { surfaceId: string; type: DriftViolationType; diff --git a/packages/interfacectl-validator/dist/types.d.ts.map b/packages/interfacectl-validator/dist/types.d.ts.map index 30e3346..48db2b0 100644 --- a/packages/interfacectl-validator/dist/types.d.ts.map +++ b/packages/interfacectl-validator/dist/types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/interfacectl-validator/src/index.ts b/packages/interfacectl-validator/src/index.ts index b0773e4..23ed83d 100644 --- a/packages/interfacectl-validator/src/index.ts +++ b/packages/interfacectl-validator/src/index.ts @@ -278,6 +278,31 @@ export function evaluateSurfaceCompliance( } } + // Shell-owned primitives check + const banList = new Set( + (surface.mustNotEmit && surface.mustNotEmit.length > 0 + ? surface.mustNotEmit + : contract.shell?.owns ?? []), + ); + if (banList.size > 0 && descriptor.primitives) { + for (const primitive of descriptor.primitives) { + if (banList.has(primitive.role) && primitive.count > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "shell-owned-primitive-emitted", + message: `Primitive "${primitive.role}" is shell-owned and must not be emitted. Count: ${primitive.count}.`, + details: { + role: primitive.role, + count: primitive.count, + sources: primitive.sources, + banList: [...banList], + jsonPointer: "/shell/owns", + }, + }); + } + } + } + // New color policy validation if (contract.color) { validateColorPolicy(contract, descriptor, violations); diff --git a/packages/interfacectl-validator/src/schema/web.surface.contract.schema.json b/packages/interfacectl-validator/src/schema/web.surface.contract.schema.json index 6c0895b..316ae11 100644 --- a/packages/interfacectl-validator/src/schema/web.surface.contract.schema.json +++ b/packages/interfacectl-validator/src/schema/web.surface.contract.schema.json @@ -1 +1,513 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://contracts.surfaces.local/web.surface.contract.schema.json","title":"Web Surface Contract Schema","description":"Built-in contract type schema for the web surface runtime. Not application-specific.","type":"object","additionalProperties":false,"required":["contractId","version","surfaces","sections","constraints"],"properties":{"$schema":{"type":"string","format":"uri-reference"},"contractId":{"type":"string","minLength":1},"version":{"type":"string","pattern":"^[0-9]+\\.[0-9]+\\.[0-9]+$"},"description":{"type":"string"},"surfaces":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/surface"}},"sections":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/section"}},"constraints":{"$ref":"#/$defs/constraints"},"color":{"$ref":"#/$defs/color"},"x_extracted":{"type":"object","description":"Phase 0: fields extracted from app code; not used by validator semantics.","additionalProperties":true,"properties":{"routes":{"type":"array","items":{"type":"string"},"uniqueItems":true},"hasShell":{"type":"boolean"},"designSystemComponents":{"type":"array","items":{"type":"string"},"uniqueItems":true},"authAware":{"type":"boolean"}}}},"$defs":{"surface":{"type":"object","additionalProperties":false,"required":["id","displayName","type","requiredSections","allowedFonts","layout"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9-]+$"},"displayName":{"type":"string","minLength":1},"type":{"type":"string","enum":["web","cli"]},"requiredSections":{"type":"array","minItems":1,"items":{"type":"string","pattern":"^[a-z0-9]+(\\.[a-z0-9]+)*$"},"uniqueItems":true},"allowedFonts":{"type":"array","minItems":1,"items":{"type":"string","minLength":1},"uniqueItems":true},"allowedColors":{"type":"array","minItems":1,"items":{"type":"string","minLength":1},"uniqueItems":true,"description":"Deprecated: Use color.sourceOfTruth and color.rawValues instead. This field will be removed in a future version."},"layout":{"type":"object","additionalProperties":false,"required":["maxContentWidth"],"properties":{"maxContentWidth":{"type":"number","minimum":1},"requiredContainers":{"type":"array","items":{"type":"string","minLength":1},"uniqueItems":true},"pageFrame":{"$ref":"#/$defs/pageFrame"}}},"phase0":{"type":"object","description":"Phase 0.5: optional expectations for extracted reality (routes, auth, shell, design system).","additionalProperties":false,"properties":{"authPosture":{"type":"string","enum":["public","auth-aware","auth-first"]},"requiresShell":{"type":"boolean"},"expectsAuthRoutes":{"type":"boolean"},"expectsDesignSystem":{"type":"boolean"}}},"domain":{"type":"string","description":"Informational: production domain (e.g. www.surfaces.systems)."},"owner":{"type":"string","description":"Phase 2: informational owner (email or GitHub handle). No enforcement."}}},"section":{"type":"object","additionalProperties":false,"required":["id","intent","description"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9]+(\\.[a-z0-9]+)*$"},"intent":{"type":"string","minLength":1},"description":{"type":"string","minLength":1}}},"constraints":{"type":"object","additionalProperties":false,"required":["motion"],"properties":{"motion":{"type":"object","additionalProperties":false,"required":["allowedDurationsMs","allowedTimingFunctions"],"properties":{"allowedDurationsMs":{"type":"array","minItems":1,"items":{"type":"integer","minimum":1},"uniqueItems":true},"allowedTimingFunctions":{"type":"array","minItems":1,"items":{"type":"string","minLength":1},"uniqueItems":true}}}}},"pageFrame":{"type":"object","additionalProperties":false,"required":["containerSelector","containerMaxWidthPx","paddingXpx"],"properties":{"containerSelector":{"type":"string","minLength":1},"containerMaxWidthPx":{"type":"number","minimum":0},"paddingXpx":{"type":"number","minimum":0},"alignment":{"type":"string","enum":["center","left"],"default":"center"},"enforcement":{"type":"string","enum":["strict","warn"],"default":"strict"}}},"color":{"type":"object","additionalProperties":false,"properties":{"sourceOfTruth":{"$ref":"#/$defs/colorSourceOfTruth"},"rawValues":{"$ref":"#/$defs/colorRawValues"},"semantics":{"$ref":"#/$defs/colorSemantics"},"consistency":{"$ref":"#/$defs/colorConsistency"}}},"colorSourceOfTruth":{"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"type":"string","enum":["tokens","none"]},"tokenNamespaces":{"type":"array","items":{"type":"string","minLength":1},"uniqueItems":true}},"if":{"properties":{"type":{"const":"tokens"}}},"then":{"required":["tokenNamespaces"]}},"colorRawValues":{"type":"object","additionalProperties":false,"required":["policy"],"properties":{"policy":{"type":"string","enum":["off","warn","strict"]},"allowlist":{"type":"array","items":{"type":"string","minLength":1},"uniqueItems":true},"denylist":{"type":"array","items":{"type":"string","minLength":1},"uniqueItems":true}}},"colorSemantics":{"type":"object","additionalProperties":false,"properties":{"roles":{"type":"object","additionalProperties":false,"properties":{"accent":{"type":"object","additionalProperties":false,"properties":{"enforcement":{"type":"string","enum":["off","warn","strict"]}}},"text":{"type":"object","additionalProperties":false,"properties":{"enforcement":{"type":"string","enum":["off","warn","strict"]}}},"background":{"type":"object","additionalProperties":false,"properties":{"enforcement":{"type":"string","enum":["off","warn","strict"]}}},"border":{"type":"object","additionalProperties":false,"properties":{"enforcement":{"type":"string","enum":["off","warn","strict"]}}}}}}},"colorConsistency":{"type":"object","additionalProperties":false,"properties":{"acrossSurfaces":{"type":"object","additionalProperties":false,"properties":{"enforcement":{"type":"string","enum":["off","warn","strict"]},"signals":{"type":"array","items":{"type":"string","enum":["token-name","css-var-name","class-fragment"]},"uniqueItems":true}}}}}}} +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://contracts.surfaces.local/web.surface.contract.schema.json", + "title": "Web Surface Contract Schema", + "description": "Built-in contract type schema for the web surface runtime. Not application-specific.", + "type": "object", + "additionalProperties": false, + "required": [ + "contractId", + "version", + "surfaces", + "sections", + "constraints" + ], + "properties": { + "$schema": { + "type": "string", + "format": "uri-reference" + }, + "contractId": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "description": { + "type": "string" + }, + "surfaces": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/surface" + } + }, + "sections": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/section" + } + }, + "constraints": { + "$ref": "#/$defs/constraints" + }, + "color": { + "$ref": "#/$defs/color" + }, + "x_extracted": { + "type": "object", + "description": "Phase 0: fields extracted from app code; not used by validator semantics.", + "additionalProperties": true, + "properties": { + "routes": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "hasShell": { + "type": "boolean" + }, + "designSystemComponents": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "authAware": { + "type": "boolean" + } + } + }, + "shell": { + "$ref": "#/$defs/shell" + } + }, + "$defs": { + "surface": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "displayName", + "type", + "requiredSections", + "allowedFonts", + "layout" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "displayName": { + "type": "string", + "minLength": 1 + }, + "type": { + "type": "string", + "enum": [ + "web", + "cli" + ] + }, + "requiredSections": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-z0-9]+(\\.[a-z0-9]+)*$" + }, + "uniqueItems": true + }, + "allowedFonts": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "allowedColors": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true, + "description": "Deprecated: Use color.sourceOfTruth and color.rawValues instead. This field will be removed in a future version." + }, + "layout": { + "type": "object", + "additionalProperties": false, + "required": [ + "maxContentWidth" + ], + "properties": { + "maxContentWidth": { + "type": "number", + "minimum": 1 + }, + "requiredContainers": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "pageFrame": { + "$ref": "#/$defs/pageFrame" + } + } + }, + "phase0": { + "type": "object", + "description": "Phase 0.5: optional expectations for extracted reality (routes, auth, shell, design system).", + "additionalProperties": false, + "properties": { + "authPosture": { + "type": "string", + "enum": [ + "public", + "auth-aware", + "auth-first" + ] + }, + "requiresShell": { + "type": "boolean" + }, + "expectsAuthRoutes": { + "type": "boolean" + }, + "expectsDesignSystem": { + "type": "boolean" + } + } + }, + "domain": { + "type": "string", + "description": "Informational: production domain (e.g. www.surfaces.systems)." + }, + "owner": { + "type": "string", + "description": "Phase 2: informational owner (email or GitHub handle). No enforcement." + }, + "mustNotEmit": { + "type": "array", + "description": "Elements or primitives the surface must not generate because the shell owns them. If omitted, consumers should default to shell.owns when present.", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + } + } + }, + "section": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "intent", + "description" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9]+(\\.[a-z0-9]+)*$" + }, + "intent": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + } + } + }, + "constraints": { + "type": "object", + "additionalProperties": false, + "required": [ + "motion" + ], + "properties": { + "motion": { + "type": "object", + "additionalProperties": false, + "required": [ + "allowedDurationsMs", + "allowedTimingFunctions" + ], + "properties": { + "allowedDurationsMs": { + "type": "array", + "minItems": 1, + "items": { + "type": "integer", + "minimum": 1 + }, + "uniqueItems": true + }, + "allowedTimingFunctions": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + } + } + } + } + }, + "pageFrame": { + "type": "object", + "additionalProperties": false, + "required": [ + "containerSelector", + "containerMaxWidthPx", + "paddingXpx" + ], + "properties": { + "containerSelector": { + "type": "string", + "minLength": 1 + }, + "containerMaxWidthPx": { + "type": "number", + "minimum": 0 + }, + "paddingXpx": { + "type": "number", + "minimum": 0 + }, + "alignment": { + "type": "string", + "enum": [ + "center", + "left" + ], + "default": "center" + }, + "enforcement": { + "type": "string", + "enum": [ + "strict", + "warn" + ], + "default": "strict" + } + } + }, + "color": { + "type": "object", + "additionalProperties": false, + "properties": { + "sourceOfTruth": { + "$ref": "#/$defs/colorSourceOfTruth" + }, + "rawValues": { + "$ref": "#/$defs/colorRawValues" + }, + "semantics": { + "$ref": "#/$defs/colorSemantics" + }, + "consistency": { + "$ref": "#/$defs/colorConsistency" + } + } + }, + "colorSourceOfTruth": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "tokens", + "none" + ] + }, + "tokenNamespaces": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + } + }, + "if": { + "properties": { + "type": { + "const": "tokens" + } + } + }, + "then": { + "required": [ + "tokenNamespaces" + ] + } + }, + "colorRawValues": { + "type": "object", + "additionalProperties": false, + "required": [ + "policy" + ], + "properties": { + "policy": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + }, + "allowlist": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "denylist": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + } + } + }, + "colorSemantics": { + "type": "object", + "additionalProperties": false, + "properties": { + "roles": { + "type": "object", + "additionalProperties": false, + "properties": { + "accent": { + "type": "object", + "additionalProperties": false, + "properties": { + "enforcement": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + } + } + }, + "text": { + "type": "object", + "additionalProperties": false, + "properties": { + "enforcement": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + } + } + }, + "background": { + "type": "object", + "additionalProperties": false, + "properties": { + "enforcement": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + } + } + }, + "border": { + "type": "object", + "additionalProperties": false, + "properties": { + "enforcement": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + } + } + } + } + } + } + }, + "colorConsistency": { + "type": "object", + "additionalProperties": false, + "properties": { + "acrossSurfaces": { + "type": "object", + "additionalProperties": false, + "properties": { + "enforcement": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + }, + "signals": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "token-name", + "css-var-name", + "class-fragment" + ] + }, + "uniqueItems": true + } + } + } + } + }, + "shell": { + "type": "object", + "additionalProperties": false, + "properties": { + "owns": { + "type": "array", + "description": "Shell-owned layout primitives. Generators must not emit these inside surfaces.", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "contentSlot": { + "type": "string", + "description": "Identifier for the shell's content mount point (e.g. a frame or DOM id).", + "minLength": 1 + } + }, + "required": [] + } + } +} \ No newline at end of file diff --git a/packages/interfacectl-validator/src/types.ts b/packages/interfacectl-validator/src/types.ts index 8379dc6..fd5dd04 100644 --- a/packages/interfacectl-validator/src/types.ts +++ b/packages/interfacectl-validator/src/types.ts @@ -20,6 +20,7 @@ export interface ContractSurface { requiredContainers?: string[]; pageFrame?: PageFrameLayout; }; + mustNotEmit?: string[]; } export interface ContractSection { @@ -35,6 +36,11 @@ export interface ContractConstraints { }; } +export interface ShellSpec { + owns?: string[]; + contentSlot?: string; +} + export interface ColorSourceOfTruth { type: "tokens" | "none"; tokenNamespaces?: string[]; @@ -81,6 +87,7 @@ export interface InterfaceContract { sections: ContractSection[]; constraints: ContractConstraints; color?: ColorPolicy; + shell?: ShellSpec; } export interface SurfaceSectionDescriptor { @@ -104,6 +111,12 @@ export interface SurfaceMotionDescriptor { source?: string; } +export interface SurfacePrimitiveDescriptor { + role: string; + count: number; + sources?: string[]; +} + export interface PageFrameLayoutDescriptor { containerSelector: string; maxWidthPx?: number | null; @@ -129,6 +142,7 @@ export interface SurfaceDescriptor { colors: SurfaceColorDescriptor[]; layout: SurfaceLayoutDescriptor; motion: SurfaceMotionDescriptor[]; + primitives?: SurfacePrimitiveDescriptor[]; } export type DriftViolationType = @@ -151,7 +165,8 @@ export type DriftViolationType = | "descriptor-missing" | "descriptor-unused" | "color-raw-value-used" - | "color-token-namespace-violation"; + | "color-token-namespace-violation" + | "shell-owned-primitive-emitted"; export interface DriftViolation { surfaceId: string; diff --git a/packages/interfacectl-validator/test/primitives.test.mjs b/packages/interfacectl-validator/test/primitives.test.mjs new file mode 100644 index 0000000..212fa8e --- /dev/null +++ b/packages/interfacectl-validator/test/primitives.test.mjs @@ -0,0 +1,80 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { evaluateSurfaceCompliance } from "../dist/index.js"; + +test("flags shell-owned primitive emitted using shell.owns default", () => { + const contract = { + contractId: "demo", + version: "1.0.0", + sections: [], + constraints: { motion: { allowedDurationsMs: [200], allowedTimingFunctions: ["ease"] } }, + surfaces: [ + { + id: "runs", + displayName: "Runs", + type: "web", + requiredSections: [], + allowedFonts: ["Inter"], + layout: { maxContentWidth: 1200 }, + }, + ], + shell: { + owns: ["nav", "header"], + contentSlot: "content", + }, + }; + + const descriptor = { + surfaceId: "runs", + sections: [], + fonts: [{ value: "Inter" }], + colors: [], + layout: { maxContentWidth: 1200, containers: [], containerSources: [] }, + motion: [], + primitives: [ + { role: "nav", count: 2, sources: ["app/runs/page.tsx"] }, + { role: "header", count: 1 }, + ], + }; + + const report = evaluateSurfaceCompliance(contract, descriptor); + const violation = report.violations.find((v) => v.type === "shell-owned-primitive-emitted"); + assert.ok(violation, "expected violation for shell-owned primitive"); + assert.equal(violation.details.role, "nav"); + assert.equal(violation.details.count, 2); +}); + +test("respects surface.mustNotEmit override when shell.owns missing", () => { + const contract = { + contractId: "demo", + version: "1.0.0", + sections: [], + constraints: { motion: { allowedDurationsMs: [200], allowedTimingFunctions: ["ease"] } }, + surfaces: [ + { + id: "runs", + displayName: "Runs", + type: "web", + requiredSections: [], + allowedFonts: ["Inter"], + layout: { maxContentWidth: 1200 }, + mustNotEmit: ["nav"], + }, + ], + }; + + const descriptor = { + surfaceId: "runs", + sections: [], + fonts: [{ value: "Inter" }], + colors: [], + layout: { maxContentWidth: 1200, containers: [], containerSources: [] }, + motion: [], + primitives: [{ role: "nav", count: 1, sources: ["app/runs/page.tsx"] }], + }; + + const report = evaluateSurfaceCompliance(contract, descriptor); + const violation = report.violations.find((v) => v.type === "shell-owned-primitive-emitted"); + assert.ok(violation, "expected violation for surface.mustNotEmit"); + assert.equal(violation.details.role, "nav"); +});