From 6b2ec5b39548170d3707e5a5c61df84095ee7493 Mon Sep 17 00:00:00 2001 From: Mike Long Date: Tue, 10 Mar 2026 12:09:46 -0700 Subject: [PATCH 1/2] feat(chrome): seed policy from portable markers --- API.md | 7 +- docs/contract-baseline.md | 12 + .../dist/commands/generate-contract.d.ts.map | 2 +- .../dist/commands/generate-contract.js | 7 +- .../dist/descriptors/static-analysis.d.ts.map | 2 +- .../dist/descriptors/static-analysis.js | 493 ++++++++++++- .../dist/utils/chrome-policy-seeding.d.ts | 10 +- .../dist/utils/chrome-policy-seeding.d.ts.map | 2 +- .../dist/utils/chrome-policy-seeding.js | 97 ++- .../src/commands/generate-contract.ts | 7 +- .../src/descriptors/static-analysis.ts | 669 +++++++++++++++++- .../src/utils/chrome-policy-seeding.ts | 151 +++- .../test/chrome-policy-seeding.test.mjs | 354 ++++++++- .../test/static-analysis.test.mjs | 383 ++++++++++ .../interfacectl-validator/dist/index.d.ts | 2 +- .../dist/index.d.ts.map | 2 +- .../interfacectl-validator/dist/types.d.ts | 9 + .../dist/types.d.ts.map | 2 +- packages/interfacectl-validator/src/index.ts | 6 + packages/interfacectl-validator/src/types.ts | 11 + 20 files changed, 2118 insertions(+), 110 deletions(-) diff --git a/API.md b/API.md index a5db3ef..4aa2607 100644 --- a/API.md +++ b/API.md @@ -423,6 +423,11 @@ interfacectl generate-contract --app-root --surface [--out ] [ **Description:** - Scans the app directory for routes (app router), layout shell (`app/layout.tsx` or `app/(shell)/layout.tsx`), `@surfaces/ui` component imports, and `/auth` routes. - Seeds `color.allowedValues` from observed descriptors and seeds `surfaces[*].icons` for web surfaces with `policy: "warn"` plus discovered icon source libraries. +- Seeds `surfaces[*].layout.chromePolicy` conservatively when portable chrome markers are present and deterministic: + - `.contract-container` for `layout-container` + - top-level `data-contract-section` for `top-level-section` + - optional `data-contract="page-container"` for `page-container` + - legacy `data-contract-container` remains supported for compatibility - Writes a contract JSON (default `contracts/generated/.contract.json`) and an extraction report (default `contracts/generated/.extraction.json`). - Validates the generated contract against the schema before writing. Running the command twice produces identical output (stable key order, no timestamps). @@ -440,7 +445,7 @@ interfacectl generate-contract --app-root --surface [--out ] [ - `0`: Contract and report written successfully - `1`: Schema validation failed for generated contract -**Phase 0 scope:** Routes, hasShell, designSystemComponents (from `@surfaces/ui`), authAware. Other fields use placeholders or defaults; the report lists warnings for any omitted extraction. +**Phase 0 scope:** Routes, hasShell, designSystemComponents (from `@surfaces/ui`), authAware. Other fields use placeholders, conservative defaults, or descriptor seeding; the report lists warnings for omitted or ambiguous extraction. **Phase 0 guardrails:** No Babel or heavy AST. Uses filesystem + regex for determinism, debuggability, and minimal deps. See [docs/plans/phase-0-extraction-guardrails.md](docs/plans/phase-0-extraction-guardrails.md) for extraction limits and when AST tooling may be added. diff --git a/docs/contract-baseline.md b/docs/contract-baseline.md index 26a8054..880c5e6 100644 --- a/docs/contract-baseline.md +++ b/docs/contract-baseline.md @@ -29,6 +29,18 @@ Optional surface layout fields: - `layout.requiredContainers` (array of strings) - `layout.pageFrame` (when present, enforced by validator) - `layout.pageFrame.containerMinWidthPx` (optional): exact deterministic min-width check in px for `[data-contract="page-container"]`. +- `layout.chromePolicy` (when present, enforced by validator) + - Shared `generate-contract` can seed this conservatively from portable contract markers. + +### Portable chrome extraction + +Portable chrome extraction is opt-in and deterministic. Shared `interfacectl` only observes actual elements carrying contract-aware markers; it does not infer wrapper intent from generic DOM structure. + +- Use `.contract-container` on governed layout containers. This is the recommended portable `layout-container` signal. +- Use top-level `data-contract-section=""` wrappers for governed sections. Nested sections do not count toward chrome extraction. +- Use `data-contract="page-container"` only when the repo wants explicit page-frame semantics. It remains optional. +- `data-contract-container` remains supported for backward compatibility, but `.contract-container` is the recommended path for new adopters. +- Dynamic radius/shadow values, unresolved custom properties, and runtime-only expressions are treated as ambiguous and will suppress generated `chromePolicy` seeding. Optional per-surface icon policy (web surfaces): diff --git a/packages/interfacectl-cli/dist/commands/generate-contract.d.ts.map b/packages/interfacectl-cli/dist/commands/generate-contract.d.ts.map index 139b44c..71437c3 100644 --- a/packages/interfacectl-cli/dist/commands/generate-contract.d.ts.map +++ b/packages/interfacectl-cli/dist/commands/generate-contract.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"generate-contract.d.ts","sourceRoot":"","sources":["../../src/commands/generate-contract.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,MAAM,CAAC,CAyFjB"} \ No newline at end of file +{"version":3,"file":"generate-contract.d.ts","sourceRoot":"","sources":["../../src/commands/generate-contract.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,MAAM,CAAC,CA4FjB"} \ No newline at end of file diff --git a/packages/interfacectl-cli/dist/commands/generate-contract.js b/packages/interfacectl-cli/dist/commands/generate-contract.js index 4e4d83f..66ea1bc 100644 --- a/packages/interfacectl-cli/dist/commands/generate-contract.js +++ b/packages/interfacectl-cli/dist/commands/generate-contract.js @@ -3,7 +3,7 @@ import { writeFile, mkdir, readFile } from "node:fs/promises"; import { extractContractFromNextApp, stableStringify, } from "@surfaces/interfacectl-extractor"; import { getBundledContractSchema, validateContractStructure, } from "@surfaces/interfacectl-validator"; import { seedColorPolicyFromObservedDescriptors } from "../utils/color-policy-seeding.js"; -import { seedChromePolicyDefaults } from "../utils/chrome-policy-seeding.js"; +import { seedChromePolicyFromObservedDescriptors } from "../utils/chrome-policy-seeding.js"; import { seedIconPolicyFromObservedDescriptors } from "../utils/icon-policy-seeding.js"; const DEFAULT_OUT_DIR = "contracts/generated"; export async function runGenerateContractCommand(options) { @@ -35,7 +35,10 @@ export async function runGenerateContractCommand(options) { surfaceId, contract: seeded.contract, }); - const chromeSeeded = await seedChromePolicyDefaults({ + const chromeSeeded = await seedChromePolicyFromObservedDescriptors({ + workspaceRoot: cwd, + appRoot, + surfaceId, contract: iconSeeded.contract, }); const contract = chromeSeeded.contract; diff --git a/packages/interfacectl-cli/dist/descriptors/static-analysis.d.ts.map b/packages/interfacectl-cli/dist/descriptors/static-analysis.d.ts.map index 487ae17..c871883 100644 --- a/packages/interfacectl-cli/dist/descriptors/static-analysis.d.ts.map +++ b/packages/interfacectl-cli/dist/descriptors/static-analysis.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"static-analysis.d.ts","sourceRoot":"","sources":["../../src/descriptors/static-analysis.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EAUvB,MAAM,kCAAkC,CAAC;AAoH1C,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gCAAgC;IAC/C,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,+BAA+B;IAC9C,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,+BAA+B,CAAC,CA0C1C"} \ No newline at end of file +{"version":3,"file":"static-analysis.d.ts","sourceRoot":"","sources":["../../src/descriptors/static-analysis.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EAUvB,MAAM,kCAAkC,CAAC;AAyI1C,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gCAAgC;IAC/C,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,+BAA+B;IAC9C,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,+BAA+B,CAAC,CA0C1C"} \ No newline at end of file diff --git a/packages/interfacectl-cli/dist/descriptors/static-analysis.js b/packages/interfacectl-cli/dist/descriptors/static-analysis.js index 0d07379..ff20b2c 100644 --- a/packages/interfacectl-cli/dist/descriptors/static-analysis.js +++ b/packages/interfacectl-cli/dist/descriptors/static-analysis.js @@ -5,9 +5,13 @@ const SECTION_ATTRIBUTE_REGEX = /data-(?:contract-)?section\s*=\s*(?:"([^"]+)"|' const CONTAINER_ATTRIBUTE_REGEX = /data-contract-container\s*=\s*(?:"([^"]+)"|'([^']+)'|{`([^`]+)`}|{\s*["'`]([^"'`]+)["'`]\s*})/g; const CONTRACT_CONTAINER_TOKEN = "contract-container"; const PAGE_CONTAINER_ATTRIBUTE_REGEX = /data-contract\s*=\s*(?:"page-container"|'page-container'|{`page-container`}|{\s*["'`]page-container["'`]\s*})/g; +const CHROME_IGNORE_ATTRIBUTE_REGEX = /data-contract-chrome-ignore\s*=\s*(?:"true"|'true'|{true}|{\s*true\s*})/; const MIN_SCREEN_CLASS_REGEX = /className\s*=\s*(?:"[^"]*\bmin-h-screen\b[^"]*\bw-full\b[^"]*"|'[^']*\bmin-h-screen\b[^']*\bw-full\b[^']*')/; // Inline style extraction const INLINE_STYLE_REGEX = /style\s*=\s*(?:"([^"]+)"|'([^']+)'|{`([^`]+)`}|{\s*["'`]([^"'`]+)["'`]\s*})/g; +const STYLE_OBJECT_ATTRIBUTE_REGEX = /\bstyle\s*=\s*{{([\s\S]*?)}}/; +const STYLE_STRING_ATTRIBUTE_REGEX = /\bstyle\s*=\s*(?:"([^"]+)"|'([^']+)'|{`([^`]+)`}|{\s*["'`]([^"'`]+)["'`]\s*})/; +const CLASSNAME_ATTRIBUTE_REGEX = /\bclass(?:Name)?\s*=\s*(?:"([^"]+)"|'([^']+)'|{`([^`]+)`}|{\s*["'`]([^"'`]+)["'`]\s*})/; const INLINE_MAX_WIDTH_REGEX = /max-width\s*:\s*([0-9.]+)\s*px/gi; const INLINE_MIN_WIDTH_REGEX = /min-width\s*:\s*([0-9.]+)\s*px/gi; const INLINE_PADDING_LEFT_REGEX = /padding-left\s*:\s*([0-9.]+)\s*px/gi; @@ -33,6 +37,9 @@ const CALC_REGEX = /calc\s*\(/i; const PAGE_FRAME_MAX_WIDTH_VAR_REGEX = /--contract-page-frame-max-width\s*:\s*([0-9.]+)\s*px/i; const PAGE_FRAME_MIN_WIDTH_VAR_REGEX = /--contract-page-frame-min-width\s*:\s*([0-9.]+)\s*px/i; const PAGE_FRAME_PADDING_VAR_REGEX = /--contract-page-frame-padding-x\s*:\s*([0-9.]+)\s*px/i; +const TAG_REGEX = /<\/?[A-Za-z][\w.:-]*\b[^>]*>/g; +const CSS_RULE_REGEX = /([^{}]+)\{([^{}]+)\}/g; +const CSS_CLASS_SELECTOR_REGEX = /\.([_a-zA-Z]+[\w-]*)/g; const COMMON_GLOBBY_IGNORES = [ "**/node_modules/**", "**/.next/**", @@ -95,6 +102,17 @@ const ICON_LIBRARY_PATTERNS = [ /^remixicon-react$/, /^@fortawesome\//, ]; +const RADIUS_TOKEN_MAP = new Map([ + ["rounded-none", 0], + ["rounded-sm", 2], + ["rounded", 4], + ["rounded-md", 6], + ["rounded-lg", 8], + ["rounded-xl", 12], + ["rounded-2xl", 16], + ["rounded-3xl", 24], + ["rounded-full", Number.POSITIVE_INFINITY], +]); export async function collectSurfaceDescriptors(options) { const structuralDescriptors = []; const warnings = []; @@ -152,7 +170,9 @@ async function extractSurfaceDescriptor(workspaceRoot, surfaceRoot, surfaceId, s gitignore: true, ignore: COMMON_GLOBBY_IGNORES, }); - const layout = await extractLayout(layoutCssFiles, sectionFiles, workspaceRoot, fileContentCache, surface); + const layoutResult = await extractLayout(layoutCssFiles, sectionFiles, workspaceRoot, fileContentCache, surface); + warnings.push(...layoutResult.warnings); + const layout = layoutResult.layout; const fonts = await extractFonts(surfaceRoot, sectionFiles, workspaceRoot, fileContentCache); if (fonts.length === 0) { const globalsPath = path.join(surfaceRoot, "app", "globals.css"); @@ -264,6 +284,7 @@ async function extractColors(surfaceRoot, cssFilePaths, sectionFiles, workspaceR async function extractLayout(cssFilePaths, sectionFiles, workspaceRoot, fileContentCache, surface) { let maxWidth = null; let layoutSource; + const warnings = []; for (const cssPath of cssFilePaths) { const cssContent = await readFileCached(cssPath, fileContentCache); const match = cssContent.match(MAX_WIDTH_VAR_REGEX); @@ -298,15 +319,473 @@ async function extractLayout(cssFilePaths, sectionFiles, workspaceRoot, fileCont if (surface?.layout.landingPattern) { landingPattern = await extractLandingPattern(sectionFiles, workspaceRoot, fileContentCache); } + let chrome; + if (surface?.type === "web") { + const chromeResult = await extractChromeLayout(cssFilePaths, sectionFiles, workspaceRoot, fileContentCache, surface.id); + chrome = chromeResult.chrome; + warnings.push(...chromeResult.warnings); + } + return { + layout: { + maxContentWidth: maxWidth, + containers: [...containers].sort(), + containerSources: [...containerSources].sort(), + source: layoutSource, + pageFrame, + chrome, + landingPattern, + }, + warnings, + }; +} +async function extractChromeLayout(cssFilePaths, sectionFiles, workspaceRoot, fileContentCache, surfaceId) { + const wrappers = []; + for (const filePath of sectionFiles) { + const content = await readFileCached(filePath, fileContentCache); + wrappers.push(...collectChromeTargetWrappers(content, path.relative(workspaceRoot, filePath))); + } + if (wrappers.length === 0) { + return { + chrome: undefined, + warnings: [], + }; + } + const cssRules = []; + for (const cssPath of cssFilePaths) { + const cssContent = await readFileCached(cssPath, fileContentCache); + cssRules.push(...parseChromeCssRules(cssContent, path.relative(workspaceRoot, cssPath))); + } + const targets = new Set(); + const shadowKinds = new Set(); + const sources = new Set(); + let maxBorderRadiusPx = null; + let ambiguousRadius = false; + let ambiguousShadow = false; + for (const wrapper of wrappers) { + targets.add(wrapper.targetType); + sources.add(wrapper.source); + const inlineResult = parseChromeInlineSignals(wrapper.styleText, wrapper.source); + const classResult = parseChromeClassSignals(wrapper.classNames, wrapper.source); + ambiguousRadius = ambiguousRadius || inlineResult.ambiguousRadius || classResult.ambiguousRadius; + ambiguousShadow = ambiguousShadow || inlineResult.ambiguousShadow || classResult.ambiguousShadow; + const radiusSignals = [ + ...inlineResult.radiusSignals, + ...classResult.radiusSignals, + ]; + const shadowSignals = [ + ...inlineResult.shadowSignals, + ...classResult.shadowSignals, + ]; + for (const rule of cssRules) { + if (!rule.selectors.some((selector) => selectorMatchesChromeWrapper(selector, wrapper))) { + continue; + } + const cssResult = parseChromeCssSignals(rule); + ambiguousRadius = ambiguousRadius || cssResult.ambiguousRadius; + ambiguousShadow = ambiguousShadow || cssResult.ambiguousShadow; + radiusSignals.push(...cssResult.radiusSignals); + shadowSignals.push(...cssResult.shadowSignals); + } + for (const signal of radiusSignals) { + sources.add(signal.source); + if (maxBorderRadiusPx === null || signal.value > maxBorderRadiusPx) { + maxBorderRadiusPx = signal.value; + } + } + for (const signal of shadowSignals) { + sources.add(signal.source); + shadowKinds.add(signal.value); + } + } + const warnings = []; + if (ambiguousRadius) { + warnings.push({ + surfaceId, + code: "chrome.radius-undetermined", + message: `Chrome border radius could not be deterministically extracted for one or more portable chrome markers on surface "${surfaceId}".`, + }); + } + if (ambiguousShadow) { + warnings.push({ + surfaceId, + code: "chrome.shadow-undetermined", + message: `Chrome shadow treatment could not be deterministically extracted for one or more portable chrome markers on surface "${surfaceId}".`, + }); + } + return { + chrome: { + targets: [...targets].sort(), + maxBorderRadiusPx, + shadowKinds: [...shadowKinds].sort(), + source: [...sources].sort(), + hasAmbiguousSignals: ambiguousRadius || ambiguousShadow || undefined, + }, + warnings, + }; +} +function collectChromeTargetWrappers(content, source) { + const wrappers = []; + const sectionStack = []; + let match; + TAG_REGEX.lastIndex = 0; + while ((match = TAG_REGEX.exec(content)) !== null) { + const tag = match[0]; + const tagName = extractTagName(tag); + if (!tagName) { + continue; + } + if (tag.startsWith("= 0; index -= 1) { + if (sectionStack[index] === tagName) { + sectionStack.splice(index, 1); + break; + } + } + continue; + } + const sectionId = extractAttributeValue(tag, SECTION_ATTRIBUTE_REGEX); + const containerMarkerValue = extractAttributeValue(tag, CONTAINER_ATTRIBUTE_REGEX); + const classNames = extractClassNames(tag); + const styleText = extractTagStyleText(tag); + const isIgnored = hasChromeIgnoreAttribute(tag); + const isSelfClosing = /\/>\s*$/.test(tag); + const hasContractContainerClass = classNames.includes(CONTRACT_CONTAINER_TOKEN); + const isPageContainer = hasPageContainerAttribute(tag); + if (!isIgnored && isPageContainer) { + wrappers.push({ + targetType: "page-container", + source, + classNames, + styleText, + }); + } + if (!isIgnored && sectionId && sectionStack.length === 0) { + wrappers.push({ + targetType: "top-level-section", + sectionId, + source, + classNames, + styleText, + }); + } + if (!isIgnored && (hasContractContainerClass || containerMarkerValue)) { + wrappers.push({ + targetType: "layout-container", + containerMarkerValue, + hasContractContainerClass, + source, + classNames, + styleText, + }); + } + if (sectionId && !isSelfClosing) { + sectionStack.push(tagName); + } + } + return wrappers; +} +function extractTagName(tag) { + const match = tag.match(/^<\/?([A-Za-z][\w.:-]*)\b/); + return match?.[1]; +} +function hasChromeIgnoreAttribute(tag) { + CHROME_IGNORE_ATTRIBUTE_REGEX.lastIndex = 0; + return CHROME_IGNORE_ATTRIBUTE_REGEX.test(tag); +} +function hasPageContainerAttribute(tag) { + PAGE_CONTAINER_ATTRIBUTE_REGEX.lastIndex = 0; + return PAGE_CONTAINER_ATTRIBUTE_REGEX.test(tag); +} +function extractClassNames(tag) { + const match = CLASSNAME_ATTRIBUTE_REGEX.exec(tag); + CLASSNAME_ATTRIBUTE_REGEX.lastIndex = 0; + const raw = match?.[1] ?? + match?.[2] ?? + match?.[3] ?? + match?.[4] ?? + ""; + if (!raw) { + return []; + } + return raw + .split(/\s+/) + .map((token) => token.trim()) + .filter(Boolean); +} +function extractTagStyleText(tag) { + const objectMatch = STYLE_OBJECT_ATTRIBUTE_REGEX.exec(tag); + STYLE_OBJECT_ATTRIBUTE_REGEX.lastIndex = 0; + if (objectMatch?.[1]) { + return { + kind: "object", + value: objectMatch[1], + }; + } + const stringMatch = STYLE_STRING_ATTRIBUTE_REGEX.exec(tag); + STYLE_STRING_ATTRIBUTE_REGEX.lastIndex = 0; + const raw = stringMatch?.[1] ?? + stringMatch?.[2] ?? + stringMatch?.[3] ?? + stringMatch?.[4] ?? + ""; + if (!raw) { + return undefined; + } return { - maxContentWidth: maxWidth, - containers: [...containers].sort(), - containerSources: [...containerSources].sort(), - source: layoutSource, - pageFrame, - landingPattern, + kind: "string", + value: raw, }; } +function parseChromeInlineSignals(styleText, source) { + const radiusSignals = []; + const shadowSignals = []; + let ambiguousRadius = false; + let ambiguousShadow = false; + if (!styleText) { + return { radiusSignals, shadowSignals, ambiguousRadius, ambiguousShadow }; + } + const body = styleText.value; + if (styleText.kind === "object") { + const radiusMatch = body.match(/\bborderRadius\s*:\s*([^,}]+)/); + if (radiusMatch?.[1]) { + const value = parseChromePxValue(radiusMatch[1]); + if (value === undefined) { + ambiguousRadius = true; + } + else { + radiusSignals.push({ type: "radius", source, value }); + } + } + const shadowMatch = body.match(/\bboxShadow\s*:\s*([^,}]+(?:,(?!\s*[a-zA-Z0-9_]+\s*:)[^,}]+)*)/); + if (shadowMatch?.[1]) { + const value = classifyDeterministicShadow(shadowMatch[1]); + if (value === undefined) { + ambiguousShadow = true; + } + else { + shadowSignals.push({ type: "shadow", source, value }); + } + } + return { radiusSignals, shadowSignals, ambiguousRadius, ambiguousShadow }; + } + const radiusDecl = body.match(/\bborder-radius\s*:\s*([^;]+)/i); + if (radiusDecl?.[1]) { + const value = parseChromePxValue(radiusDecl[1]); + if (value === undefined) { + ambiguousRadius = true; + } + else { + radiusSignals.push({ type: "radius", source, value }); + } + } + const shadowDecl = body.match(/\bbox-shadow\s*:\s*([^;]+)/i); + if (shadowDecl?.[1]) { + const value = classifyDeterministicShadow(shadowDecl[1]); + if (value === undefined) { + ambiguousShadow = true; + } + else { + shadowSignals.push({ type: "shadow", source, value }); + } + } + return { radiusSignals, shadowSignals, ambiguousRadius, ambiguousShadow }; +} +function parseChromeClassSignals(classNames, source) { + const radiusSignals = []; + const shadowSignals = []; + let ambiguousRadius = false; + let ambiguousShadow = false; + for (const className of classNames) { + if (RADIUS_TOKEN_MAP.has(className)) { + const value = RADIUS_TOKEN_MAP.get(className); + if (value !== undefined && Number.isFinite(value)) { + radiusSignals.push({ type: "radius", source, value }); + } + else { + ambiguousRadius = true; + } + continue; + } + const arbitraryRadiusMatch = className.match(/^rounded(?:-[trblsexy]{1,2})?-\[(.+)\]$/); + if (arbitraryRadiusMatch?.[1]) { + const value = parseChromePxValue(arbitraryRadiusMatch[1]); + if (value === undefined) { + ambiguousRadius = true; + } + else { + radiusSignals.push({ type: "radius", source, value }); + } + continue; + } + if (className === "shadow-none") { + shadowSignals.push({ type: "shadow", source, value: "none" }); + continue; + } + if (className === "shadow-inner") { + shadowSignals.push({ type: "shadow", source, value: "inset" }); + continue; + } + if (className === "shadow" || + /^shadow-(xs|sm|md|lg|xl|2xl)$/.test(className) || + /^drop-shadow(?:-.+)?$/.test(className)) { + shadowSignals.push({ type: "shadow", source, value: "outer" }); + continue; + } + if (/^shadow-\[.+\]$/.test(className) || /^drop-shadow-\[.+\]$/.test(className)) { + ambiguousShadow = true; + } + } + return { radiusSignals, shadowSignals, ambiguousRadius, ambiguousShadow }; +} +function parseChromeCssRules(content, source) { + const rules = []; + let match; + CSS_RULE_REGEX.lastIndex = 0; + while ((match = CSS_RULE_REGEX.exec(content)) !== null) { + const selectorText = match[1]?.trim(); + const declarationText = match[2]?.trim(); + if (!selectorText || !declarationText) { + continue; + } + const selectors = selectorText + .split(",") + .map((selector) => selector.trim()) + .filter(Boolean); + if (selectors.length === 0) { + continue; + } + rules.push({ + selectors, + declarationText, + source, + }); + } + return rules; +} +function selectorMatchesChromeWrapper(selector, wrapper) { + const classMatches = [...selector.matchAll(CSS_CLASS_SELECTOR_REGEX)].map((match) => match[1]); + if (classMatches.length > 0 && + !classMatches.every((className) => wrapper.classNames.includes(className))) { + return false; + } + if (wrapper.targetType === "page-container") { + const pageContainerSelector = selector.includes('[data-contract="page-container"]') || + selector.includes("[data-contract='page-container']") || + selector.includes("[data-contract=page-container]"); + if (pageContainerSelector) { + return true; + } + } + if (wrapper.targetType === "top-level-section") { + const genericSection = selector.includes("[data-contract-section]"); + const specificSection = wrapper.sectionId && + (selector.includes(`[data-contract-section="${wrapper.sectionId}"]`) || + selector.includes(`[data-contract-section='${wrapper.sectionId}']`) || + selector.includes(`[data-contract-section=${wrapper.sectionId}]`)); + if (genericSection || specificSection) { + return true; + } + } + if (wrapper.targetType === "layout-container") { + const contractContainerClassSelector = wrapper.hasContractContainerClass && + classMatches.includes(CONTRACT_CONTAINER_TOKEN); + const genericContainerAttribute = wrapper.containerMarkerValue && selector.includes("[data-contract-container]"); + const specificContainerAttribute = wrapper.containerMarkerValue && + (selector.includes(`[data-contract-container="${wrapper.containerMarkerValue}"]`) || + selector.includes(`[data-contract-container='${wrapper.containerMarkerValue}']`) || + selector.includes(`[data-contract-container=${wrapper.containerMarkerValue}]`)); + if (contractContainerClassSelector || + genericContainerAttribute || + specificContainerAttribute) { + return true; + } + } + return false; +} +function parseChromeCssSignals(rule) { + const radiusSignals = []; + const shadowSignals = []; + let ambiguousRadius = false; + let ambiguousShadow = false; + const radiusMatch = rule.declarationText.match(/\bborder-radius\s*:\s*([^;]+)/i); + if (radiusMatch?.[1]) { + const value = parseChromePxValue(radiusMatch[1]); + if (value === undefined) { + ambiguousRadius = true; + } + else { + radiusSignals.push({ type: "radius", source: rule.source, value }); + } + } + const shadowMatch = rule.declarationText.match(/\bbox-shadow\s*:\s*([^;]+)/i); + if (shadowMatch?.[1]) { + const value = classifyDeterministicShadow(shadowMatch[1]); + if (value === undefined) { + ambiguousShadow = true; + } + else { + shadowSignals.push({ type: "shadow", source: rule.source, value }); + } + } + return { radiusSignals, shadowSignals, ambiguousRadius, ambiguousShadow }; +} +function parseChromePxValue(rawValue) { + const normalized = String(rawValue ?? "") + .trim() + .replace(/['"`]/g, ""); + if (!normalized) { + return undefined; + } + if (normalized === "0") { + return 0; + } + const pxMatch = normalized.match(/^(-?\d+(?:\.\d+)?)px$/i); + if (pxMatch) { + return Number.parseFloat(pxMatch[1]); + } + if (/^-?\d+(?:\.\d+)?$/.test(normalized)) { + return Number.parseFloat(normalized); + } + return undefined; +} +function classifyDeterministicShadow(rawValue) { + const raw = String(rawValue ?? "").trim(); + if (!raw) { + return undefined; + } + if (/^[A-Za-z_$][\w$]*$/.test(raw)) { + return undefined; + } + const normalized = raw.toLowerCase(); + if (normalized.includes("var(") || + normalized.includes("${") || + normalized === "inherit" || + normalized === "initial" || + normalized === "unset" || + normalized === "revert" || + normalized === "revert-layer") { + return undefined; + } + if (normalized === "none" || normalized === "0" || normalized === "0px") { + return "none"; + } + const parts = normalized + .split(/,(?![^()]*\))/) + .map((part) => part.trim()) + .filter(Boolean); + if (parts.length === 0) { + return undefined; + } + const hasInset = parts.some((part) => /\binset\b/.test(part)); + const hasOuter = parts.some((part) => !/\binset\b/.test(part)); + if (hasInset && hasOuter) { + return "mixed"; + } + if (hasInset) { + return "inset"; + } + return "outer"; +} async function extractLandingPattern(sectionFiles, workspaceRoot, fileContentCache) { const landingPagePath = selectLandingPageFile(sectionFiles); if (!landingPagePath) { diff --git a/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts b/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts index 062bf70..99700d7 100644 --- a/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts +++ b/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts @@ -1,4 +1,10 @@ import type { InterfaceContract } from "@surfaces/interfacectl-validator"; +export interface ChromePolicySeedingInput { + workspaceRoot: string; + appRoot: string; + surfaceId: string; + contract: InterfaceContract; +} export interface ChromePolicySeedResult { contract: InterfaceContract; warnings: Array<{ @@ -6,7 +12,5 @@ export interface ChromePolicySeedResult { message: string; }>; } -export declare function seedChromePolicyDefaults({ contract, }: { - contract: InterfaceContract; -}): Promise; +export declare function seedChromePolicyFromObservedDescriptors(input: ChromePolicySeedingInput): Promise; //# sourceMappingURL=chrome-policy-seeding.d.ts.map \ No newline at end of file diff --git a/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts.map b/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts.map index c833569..eca2cf0 100644 --- a/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts.map +++ b/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"chrome-policy-seeding.d.ts","sourceRoot":"","sources":["../../src/utils/chrome-policy-seeding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAE1E,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpD;AAUD,wBAAsB,wBAAwB,CAAC,EAC7C,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,iBAAiB,CAAC;CAC7B,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAyBlC"} \ No newline at end of file +{"version":3,"file":"chrome-policy-seeding.d.ts","sourceRoot":"","sources":["../../src/utils/chrome-policy-seeding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,iBAAiB,EAClB,MAAM,kCAAkC,CAAC;AAG1C,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,iBAAiB,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpD;AAeD,wBAAsB,uCAAuC,CAC3D,KAAK,EAAE,wBAAwB,GAC9B,OAAO,CAAC,sBAAsB,CAAC,CA8FjC"} \ No newline at end of file diff --git a/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.js b/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.js index 762ee09..21b67b8 100644 --- a/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.js +++ b/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.js @@ -1,33 +1,86 @@ -const DEFAULT_CHROME_POLICY = { - policy: "off", - targets: ["page-container", "top-level-section", "layout-container"], - maxBorderRadiusPx: 8, - allowOuterShadow: false, - allowInsetShadow: true, -}; -export async function seedChromePolicyDefaults({ contract, }) { +import { collectSurfaceDescriptors } from "../descriptors/static-analysis.js"; +function normalizeTargets(targets) { + return [...new Set(targets.map((value) => value.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b)); +} +function hasShadowKind(shadowKinds, expected) { + return shadowKinds.some((kind) => kind === expected || kind === "mixed"); +} +export async function seedChromePolicyFromObservedDescriptors(input) { + const warnings = []; + const surfaceRootMap = new Map([[input.surfaceId, input.appRoot]]); + const descriptorResult = await collectSurfaceDescriptors({ + workspaceRoot: input.workspaceRoot, + contract: input.contract, + surfaceFilters: new Set([input.surfaceId]), + surfaceRootMap, + }); + for (const warning of descriptorResult.warnings) { + warnings.push({ + code: `chrome-seed.${warning.code}`, + message: `Descriptor warning during chrome seed: ${warning.message}`, + }); + } + for (const error of descriptorResult.errors) { + warnings.push({ + code: `chrome-seed.${error.code}`, + message: `Descriptor error during chrome seed: ${error.message}`, + }); + } + const targetSurface = input.contract.surfaces.find((surface) => surface.id === input.surfaceId); + if (!targetSurface || targetSurface.type !== "web") { + return { + contract: input.contract, + warnings, + }; + } + if (targetSurface.layout.chromePolicy) { + return { + contract: input.contract, + warnings, + }; + } + const descriptor = descriptorResult.descriptors.find((entry) => entry.surfaceId === input.surfaceId); + const observedChrome = descriptor?.layout.chrome; + const observedTargets = normalizeTargets(observedChrome?.targets ?? []); + if (!observedChrome || observedTargets.length === 0) { + warnings.push({ + code: "chrome-seed.none-detected", + message: "Chrome policy was not seeded from extraction because no portable chrome markers were detected.", + }); + return { + contract: input.contract, + warnings, + }; + } + if (observedChrome.hasAmbiguousSignals) { + warnings.push({ + code: "chrome-seed.ambiguous-signals", + message: "Chrome policy was not seeded from extraction because one or more chrome signals on portable markers were not deterministically extractable.", + }); + return { + contract: input.contract, + warnings, + }; + } return { contract: { - ...contract, - surfaces: contract.surfaces.map((surface) => { - if (surface.type !== "web") { - return surface; - } - if (surface.layout.chromePolicy) { - return surface; - } - return { + ...input.contract, + surfaces: input.contract.surfaces.map((surface) => surface.id === input.surfaceId && surface.type === "web" + ? { ...surface, layout: { ...surface.layout, chromePolicy: { - ...DEFAULT_CHROME_POLICY, - targets: [...DEFAULT_CHROME_POLICY.targets], + policy: "off", + targets: observedTargets, + maxBorderRadiusPx: observedChrome.maxBorderRadiusPx ?? 0, + allowOuterShadow: hasShadowKind(observedChrome.shadowKinds, "outer"), + allowInsetShadow: hasShadowKind(observedChrome.shadowKinds, "inset"), }, }, - }; - }), + } + : surface), }, - warnings: [], + warnings, }; } diff --git a/packages/interfacectl-cli/src/commands/generate-contract.ts b/packages/interfacectl-cli/src/commands/generate-contract.ts index 096f300..2e1e922 100644 --- a/packages/interfacectl-cli/src/commands/generate-contract.ts +++ b/packages/interfacectl-cli/src/commands/generate-contract.ts @@ -10,7 +10,7 @@ import { validateContractStructure, } from "@surfaces/interfacectl-validator"; import { seedColorPolicyFromObservedDescriptors } from "../utils/color-policy-seeding.js"; -import { seedChromePolicyDefaults } from "../utils/chrome-policy-seeding.js"; +import { seedChromePolicyFromObservedDescriptors } from "../utils/chrome-policy-seeding.js"; import { seedIconPolicyFromObservedDescriptors } from "../utils/icon-policy-seeding.js"; export interface GenerateContractOptions { @@ -59,7 +59,10 @@ export async function runGenerateContractCommand( surfaceId, contract: seeded.contract, }); - const chromeSeeded = await seedChromePolicyDefaults({ + const chromeSeeded = await seedChromePolicyFromObservedDescriptors({ + workspaceRoot: cwd, + appRoot, + surfaceId, contract: iconSeeded.contract, }); const contract = chromeSeeded.contract; diff --git a/packages/interfacectl-cli/src/descriptors/static-analysis.ts b/packages/interfacectl-cli/src/descriptors/static-analysis.ts index 1b644a4..572c085 100644 --- a/packages/interfacectl-cli/src/descriptors/static-analysis.ts +++ b/packages/interfacectl-cli/src/descriptors/static-analysis.ts @@ -2,6 +2,9 @@ import path from "node:path"; import { readFile, stat } from "node:fs/promises"; import { globby } from "globby"; import { + type ChromeLayoutDescriptor, + type ChromePolicyTarget, + type ChromeShadowKind, type InterfaceContract, type SurfaceDescriptor, type SurfaceFontDescriptor, @@ -23,10 +26,17 @@ const CONTAINER_ATTRIBUTE_REGEX = const CONTRACT_CONTAINER_TOKEN = "contract-container"; const PAGE_CONTAINER_ATTRIBUTE_REGEX = /data-contract\s*=\s*(?:"page-container"|'page-container'|{`page-container`}|{\s*["'`]page-container["'`]\s*})/g; +const CHROME_IGNORE_ATTRIBUTE_REGEX = + /data-contract-chrome-ignore\s*=\s*(?:"true"|'true'|{true}|{\s*true\s*})/; const MIN_SCREEN_CLASS_REGEX = /className\s*=\s*(?:"[^"]*\bmin-h-screen\b[^"]*\bw-full\b[^"]*"|'[^']*\bmin-h-screen\b[^']*\bw-full\b[^']*')/; // Inline style extraction const INLINE_STYLE_REGEX = /style\s*=\s*(?:"([^"]+)"|'([^']+)'|{`([^`]+)`}|{\s*["'`]([^"'`]+)["'`]\s*})/g; +const STYLE_OBJECT_ATTRIBUTE_REGEX = /\bstyle\s*=\s*{{([\s\S]*?)}}/; +const STYLE_STRING_ATTRIBUTE_REGEX = + /\bstyle\s*=\s*(?:"([^"]+)"|'([^']+)'|{`([^`]+)`}|{\s*["'`]([^"'`]+)["'`]\s*})/; +const CLASSNAME_ATTRIBUTE_REGEX = + /\bclass(?:Name)?\s*=\s*(?:"([^"]+)"|'([^']+)'|{`([^`]+)`}|{\s*["'`]([^"'`]+)["'`]\s*})/; const INLINE_MAX_WIDTH_REGEX = /max-width\s*:\s*([0-9.]+)\s*px/gi; const INLINE_MIN_WIDTH_REGEX = /min-width\s*:\s*([0-9.]+)\s*px/gi; const INLINE_PADDING_LEFT_REGEX = /padding-left\s*:\s*([0-9.]+)\s*px/gi; @@ -55,6 +65,9 @@ const PAGE_FRAME_MIN_WIDTH_VAR_REGEX = /--contract-page-frame-min-width\s*:\s*([0-9.]+)\s*px/i; const PAGE_FRAME_PADDING_VAR_REGEX = /--contract-page-frame-padding-x\s*:\s*([0-9.]+)\s*px/i; +const TAG_REGEX = /<\/?[A-Za-z][\w.:-]*\b[^>]*>/g; +const CSS_RULE_REGEX = /([^{}]+)\{([^{}]+)\}/g; +const CSS_CLASS_SELECTOR_REGEX = /\.([_a-zA-Z]+[\w-]*)/g; const COMMON_GLOBBY_IGNORES = [ "**/node_modules/**", "**/.next/**", @@ -128,6 +141,17 @@ const ICON_LIBRARY_PATTERNS = [ /^remixicon-react$/, /^@fortawesome\//, ]; +const RADIUS_TOKEN_MAP = new Map([ + ["rounded-none", 0], + ["rounded-sm", 2], + ["rounded", 4], + ["rounded-md", 6], + ["rounded-lg", 8], + ["rounded-xl", 12], + ["rounded-2xl", 16], + ["rounded-3xl", 24], + ["rounded-full", Number.POSITIVE_INFINITY], +]); export interface DescriptorIssue { surfaceId?: string; @@ -249,13 +273,15 @@ async function extractSurfaceDescriptor( ignore: COMMON_GLOBBY_IGNORES, }); - const layout = await extractLayout( + const layoutResult = await extractLayout( layoutCssFiles, sectionFiles, workspaceRoot, fileContentCache, surface, ); + warnings.push(...layoutResult.warnings); + const layout = layoutResult.layout; const fonts = await extractFonts( surfaceRoot, sectionFiles, @@ -456,9 +482,13 @@ async function extractLayout( workspaceRoot: string, fileContentCache: Map, surface?: InterfaceContract["surfaces"][number], -): Promise { +): Promise<{ + layout: SurfaceLayoutDescriptor; + warnings: DescriptorIssue[]; +}> { let maxWidth: number | null = null; let layoutSource: string | undefined; + const warnings: DescriptorIssue[] = []; for (const cssPath of cssFilePaths) { const cssContent = await readFileCached(cssPath, fileContentCache); @@ -508,16 +538,639 @@ async function extractLayout( ); } + let chrome: ChromeLayoutDescriptor | undefined; + if (surface?.type === "web") { + const chromeResult = await extractChromeLayout( + cssFilePaths, + sectionFiles, + workspaceRoot, + fileContentCache, + surface.id, + ); + chrome = chromeResult.chrome; + warnings.push(...chromeResult.warnings); + } + + return { + layout: { + maxContentWidth: maxWidth, + containers: [...containers].sort(), + containerSources: [...containerSources].sort(), + source: layoutSource, + pageFrame, + chrome, + landingPattern, + }, + warnings, + }; +} + +interface ChromeTargetWrapper { + targetType: ChromePolicyTarget; + source: string; + classNames: string[]; + sectionId?: string; + containerMarkerValue?: string; + hasContractContainerClass?: boolean; + styleText?: { + kind: "object" | "string"; + value: string; + }; +} + +interface ChromeCssRule { + selectors: string[]; + declarationText: string; + source: string; +} + +interface ChromeSignal { + type: "radius" | "shadow"; + source: string; + value: TValue; +} + +async function extractChromeLayout( + cssFilePaths: string[], + sectionFiles: string[], + workspaceRoot: string, + fileContentCache: Map, + surfaceId: string, +): Promise<{ + chrome?: ChromeLayoutDescriptor; + warnings: DescriptorIssue[]; +}> { + const wrappers: ChromeTargetWrapper[] = []; + + for (const filePath of sectionFiles) { + const content = await readFileCached(filePath, fileContentCache); + wrappers.push( + ...collectChromeTargetWrappers( + content, + path.relative(workspaceRoot, filePath), + ), + ); + } + + if (wrappers.length === 0) { + return { + chrome: undefined, + warnings: [], + }; + } + + const cssRules: ChromeCssRule[] = []; + for (const cssPath of cssFilePaths) { + const cssContent = await readFileCached(cssPath, fileContentCache); + cssRules.push( + ...parseChromeCssRules( + cssContent, + path.relative(workspaceRoot, cssPath), + ), + ); + } + + const targets = new Set(); + const shadowKinds = new Set(); + const sources = new Set(); + let maxBorderRadiusPx: number | null = null; + let ambiguousRadius = false; + let ambiguousShadow = false; + + for (const wrapper of wrappers) { + targets.add(wrapper.targetType); + sources.add(wrapper.source); + + const inlineResult = parseChromeInlineSignals(wrapper.styleText, wrapper.source); + const classResult = parseChromeClassSignals(wrapper.classNames, wrapper.source); + ambiguousRadius = ambiguousRadius || inlineResult.ambiguousRadius || classResult.ambiguousRadius; + ambiguousShadow = ambiguousShadow || inlineResult.ambiguousShadow || classResult.ambiguousShadow; + + const radiusSignals: Array> = [ + ...inlineResult.radiusSignals, + ...classResult.radiusSignals, + ]; + const shadowSignals: Array> = [ + ...inlineResult.shadowSignals, + ...classResult.shadowSignals, + ]; + + for (const rule of cssRules) { + if (!rule.selectors.some((selector) => selectorMatchesChromeWrapper(selector, wrapper))) { + continue; + } + const cssResult = parseChromeCssSignals(rule); + ambiguousRadius = ambiguousRadius || cssResult.ambiguousRadius; + ambiguousShadow = ambiguousShadow || cssResult.ambiguousShadow; + radiusSignals.push(...cssResult.radiusSignals); + shadowSignals.push(...cssResult.shadowSignals); + } + + for (const signal of radiusSignals) { + sources.add(signal.source); + if (maxBorderRadiusPx === null || signal.value > maxBorderRadiusPx) { + maxBorderRadiusPx = signal.value; + } + } + for (const signal of shadowSignals) { + sources.add(signal.source); + shadowKinds.add(signal.value); + } + } + + const warnings: DescriptorIssue[] = []; + if (ambiguousRadius) { + warnings.push({ + surfaceId, + code: "chrome.radius-undetermined", + message: + `Chrome border radius could not be deterministically extracted for one or more portable chrome markers on surface "${surfaceId}".`, + }); + } + if (ambiguousShadow) { + warnings.push({ + surfaceId, + code: "chrome.shadow-undetermined", + message: + `Chrome shadow treatment could not be deterministically extracted for one or more portable chrome markers on surface "${surfaceId}".`, + }); + } + return { - maxContentWidth: maxWidth, - containers: [...containers].sort(), - containerSources: [...containerSources].sort(), - source: layoutSource, - pageFrame, - landingPattern, + chrome: { + targets: [...targets].sort(), + maxBorderRadiusPx, + shadowKinds: [...shadowKinds].sort(), + source: [...sources].sort(), + hasAmbiguousSignals: ambiguousRadius || ambiguousShadow || undefined, + }, + warnings, }; } +function collectChromeTargetWrappers( + content: string, + source: string, +): ChromeTargetWrapper[] { + const wrappers: ChromeTargetWrapper[] = []; + const sectionStack: string[] = []; + let match: RegExpExecArray | null; + + TAG_REGEX.lastIndex = 0; + while ((match = TAG_REGEX.exec(content)) !== null) { + const tag = match[0]; + const tagName = extractTagName(tag); + if (!tagName) { + continue; + } + if (tag.startsWith("= 0; index -= 1) { + if (sectionStack[index] === tagName) { + sectionStack.splice(index, 1); + break; + } + } + continue; + } + + const sectionId = extractAttributeValue(tag, SECTION_ATTRIBUTE_REGEX); + const containerMarkerValue = extractAttributeValue(tag, CONTAINER_ATTRIBUTE_REGEX); + const classNames = extractClassNames(tag); + const styleText = extractTagStyleText(tag); + const isIgnored = hasChromeIgnoreAttribute(tag); + const isSelfClosing = /\/>\s*$/.test(tag); + const hasContractContainerClass = classNames.includes(CONTRACT_CONTAINER_TOKEN); + const isPageContainer = hasPageContainerAttribute(tag); + + if (!isIgnored && isPageContainer) { + wrappers.push({ + targetType: "page-container", + source, + classNames, + styleText, + }); + } + + if (!isIgnored && sectionId && sectionStack.length === 0) { + wrappers.push({ + targetType: "top-level-section", + sectionId, + source, + classNames, + styleText, + }); + } + + if (!isIgnored && (hasContractContainerClass || containerMarkerValue)) { + wrappers.push({ + targetType: "layout-container", + containerMarkerValue, + hasContractContainerClass, + source, + classNames, + styleText, + }); + } + + if (sectionId && !isSelfClosing) { + sectionStack.push(tagName); + } + } + + return wrappers; +} + +function extractTagName(tag: string): string | undefined { + const match = tag.match(/^<\/?([A-Za-z][\w.:-]*)\b/); + return match?.[1]; +} + +function hasChromeIgnoreAttribute(tag: string): boolean { + CHROME_IGNORE_ATTRIBUTE_REGEX.lastIndex = 0; + return CHROME_IGNORE_ATTRIBUTE_REGEX.test(tag); +} + +function hasPageContainerAttribute(tag: string): boolean { + PAGE_CONTAINER_ATTRIBUTE_REGEX.lastIndex = 0; + return PAGE_CONTAINER_ATTRIBUTE_REGEX.test(tag); +} + +function extractClassNames(tag: string): string[] { + const match = CLASSNAME_ATTRIBUTE_REGEX.exec(tag); + CLASSNAME_ATTRIBUTE_REGEX.lastIndex = 0; + const raw = + match?.[1] ?? + match?.[2] ?? + match?.[3] ?? + match?.[4] ?? + ""; + if (!raw) { + return []; + } + return raw + .split(/\s+/) + .map((token) => token.trim()) + .filter(Boolean); +} + +function extractTagStyleText( + tag: string, +): ChromeTargetWrapper["styleText"] | undefined { + const objectMatch = STYLE_OBJECT_ATTRIBUTE_REGEX.exec(tag); + STYLE_OBJECT_ATTRIBUTE_REGEX.lastIndex = 0; + if (objectMatch?.[1]) { + return { + kind: "object", + value: objectMatch[1], + }; + } + + const stringMatch = STYLE_STRING_ATTRIBUTE_REGEX.exec(tag); + STYLE_STRING_ATTRIBUTE_REGEX.lastIndex = 0; + const raw = + stringMatch?.[1] ?? + stringMatch?.[2] ?? + stringMatch?.[3] ?? + stringMatch?.[4] ?? + ""; + if (!raw) { + return undefined; + } + + return { + kind: "string", + value: raw, + }; +} + +function parseChromeInlineSignals( + styleText: ChromeTargetWrapper["styleText"], + source: string, +): { + radiusSignals: Array>; + shadowSignals: Array>; + ambiguousRadius: boolean; + ambiguousShadow: boolean; +} { + const radiusSignals: Array> = []; + const shadowSignals: Array> = []; + let ambiguousRadius = false; + let ambiguousShadow = false; + + if (!styleText) { + return { radiusSignals, shadowSignals, ambiguousRadius, ambiguousShadow }; + } + + const body = styleText.value; + if (styleText.kind === "object") { + const radiusMatch = body.match(/\bborderRadius\s*:\s*([^,}]+)/); + if (radiusMatch?.[1]) { + const value = parseChromePxValue(radiusMatch[1]); + if (value === undefined) { + ambiguousRadius = true; + } else { + radiusSignals.push({ type: "radius", source, value }); + } + } + + const shadowMatch = body.match( + /\bboxShadow\s*:\s*([^,}]+(?:,(?!\s*[a-zA-Z0-9_]+\s*:)[^,}]+)*)/, + ); + if (shadowMatch?.[1]) { + const value = classifyDeterministicShadow(shadowMatch[1]); + if (value === undefined) { + ambiguousShadow = true; + } else { + shadowSignals.push({ type: "shadow", source, value }); + } + } + + return { radiusSignals, shadowSignals, ambiguousRadius, ambiguousShadow }; + } + + const radiusDecl = body.match(/\bborder-radius\s*:\s*([^;]+)/i); + if (radiusDecl?.[1]) { + const value = parseChromePxValue(radiusDecl[1]); + if (value === undefined) { + ambiguousRadius = true; + } else { + radiusSignals.push({ type: "radius", source, value }); + } + } + + const shadowDecl = body.match(/\bbox-shadow\s*:\s*([^;]+)/i); + if (shadowDecl?.[1]) { + const value = classifyDeterministicShadow(shadowDecl[1]); + if (value === undefined) { + ambiguousShadow = true; + } else { + shadowSignals.push({ type: "shadow", source, value }); + } + } + + return { radiusSignals, shadowSignals, ambiguousRadius, ambiguousShadow }; +} + +function parseChromeClassSignals( + classNames: string[], + source: string, +): { + radiusSignals: Array>; + shadowSignals: Array>; + ambiguousRadius: boolean; + ambiguousShadow: boolean; +} { + const radiusSignals: Array> = []; + const shadowSignals: Array> = []; + let ambiguousRadius = false; + let ambiguousShadow = false; + + for (const className of classNames) { + if (RADIUS_TOKEN_MAP.has(className)) { + const value = RADIUS_TOKEN_MAP.get(className); + if (value !== undefined && Number.isFinite(value)) { + radiusSignals.push({ type: "radius", source, value }); + } else { + ambiguousRadius = true; + } + continue; + } + + const arbitraryRadiusMatch = className.match( + /^rounded(?:-[trblsexy]{1,2})?-\[(.+)\]$/, + ); + if (arbitraryRadiusMatch?.[1]) { + const value = parseChromePxValue(arbitraryRadiusMatch[1]); + if (value === undefined) { + ambiguousRadius = true; + } else { + radiusSignals.push({ type: "radius", source, value }); + } + continue; + } + + if (className === "shadow-none") { + shadowSignals.push({ type: "shadow", source, value: "none" }); + continue; + } + + if (className === "shadow-inner") { + shadowSignals.push({ type: "shadow", source, value: "inset" }); + continue; + } + + if ( + className === "shadow" || + /^shadow-(xs|sm|md|lg|xl|2xl)$/.test(className) || + /^drop-shadow(?:-.+)?$/.test(className) + ) { + shadowSignals.push({ type: "shadow", source, value: "outer" }); + continue; + } + + if (/^shadow-\[.+\]$/.test(className) || /^drop-shadow-\[.+\]$/.test(className)) { + ambiguousShadow = true; + } + } + + return { radiusSignals, shadowSignals, ambiguousRadius, ambiguousShadow }; +} + +function parseChromeCssRules( + content: string, + source: string, +): ChromeCssRule[] { + const rules: ChromeCssRule[] = []; + let match: RegExpExecArray | null; + + CSS_RULE_REGEX.lastIndex = 0; + while ((match = CSS_RULE_REGEX.exec(content)) !== null) { + const selectorText = match[1]?.trim(); + const declarationText = match[2]?.trim(); + if (!selectorText || !declarationText) { + continue; + } + + const selectors = selectorText + .split(",") + .map((selector) => selector.trim()) + .filter(Boolean); + if (selectors.length === 0) { + continue; + } + + rules.push({ + selectors, + declarationText, + source, + }); + } + + return rules; +} + +function selectorMatchesChromeWrapper( + selector: string, + wrapper: ChromeTargetWrapper, +): boolean { + const classMatches = [...selector.matchAll(CSS_CLASS_SELECTOR_REGEX)].map( + (match) => match[1], + ); + if ( + classMatches.length > 0 && + !classMatches.every((className) => wrapper.classNames.includes(className)) + ) { + return false; + } + + if (wrapper.targetType === "page-container") { + const pageContainerSelector = + selector.includes('[data-contract="page-container"]') || + selector.includes("[data-contract='page-container']") || + selector.includes("[data-contract=page-container]"); + if (pageContainerSelector) { + return true; + } + } + + if (wrapper.targetType === "top-level-section") { + const genericSection = selector.includes("[data-contract-section]"); + const specificSection = + wrapper.sectionId && + (selector.includes(`[data-contract-section="${wrapper.sectionId}"]`) || + selector.includes(`[data-contract-section='${wrapper.sectionId}']`) || + selector.includes(`[data-contract-section=${wrapper.sectionId}]`)); + if (genericSection || specificSection) { + return true; + } + } + + if (wrapper.targetType === "layout-container") { + const contractContainerClassSelector = + wrapper.hasContractContainerClass && + classMatches.includes(CONTRACT_CONTAINER_TOKEN); + const genericContainerAttribute = + wrapper.containerMarkerValue && selector.includes("[data-contract-container]"); + const specificContainerAttribute = + wrapper.containerMarkerValue && + (selector.includes(`[data-contract-container="${wrapper.containerMarkerValue}"]`) || + selector.includes(`[data-contract-container='${wrapper.containerMarkerValue}']`) || + selector.includes(`[data-contract-container=${wrapper.containerMarkerValue}]`)); + if ( + contractContainerClassSelector || + genericContainerAttribute || + specificContainerAttribute + ) { + return true; + } + } + + return false; +} + +function parseChromeCssSignals( + rule: ChromeCssRule, +): { + radiusSignals: Array>; + shadowSignals: Array>; + ambiguousRadius: boolean; + ambiguousShadow: boolean; +} { + const radiusSignals: Array> = []; + const shadowSignals: Array> = []; + let ambiguousRadius = false; + let ambiguousShadow = false; + + const radiusMatch = rule.declarationText.match(/\bborder-radius\s*:\s*([^;]+)/i); + if (radiusMatch?.[1]) { + const value = parseChromePxValue(radiusMatch[1]); + if (value === undefined) { + ambiguousRadius = true; + } else { + radiusSignals.push({ type: "radius", source: rule.source, value }); + } + } + + const shadowMatch = rule.declarationText.match(/\bbox-shadow\s*:\s*([^;]+)/i); + if (shadowMatch?.[1]) { + const value = classifyDeterministicShadow(shadowMatch[1]); + if (value === undefined) { + ambiguousShadow = true; + } else { + shadowSignals.push({ type: "shadow", source: rule.source, value }); + } + } + + return { radiusSignals, shadowSignals, ambiguousRadius, ambiguousShadow }; +} + +function parseChromePxValue(rawValue: string): number | undefined { + const normalized = String(rawValue ?? "") + .trim() + .replace(/['"`]/g, ""); + if (!normalized) { + return undefined; + } + if (normalized === "0") { + return 0; + } + const pxMatch = normalized.match(/^(-?\d+(?:\.\d+)?)px$/i); + if (pxMatch) { + return Number.parseFloat(pxMatch[1]); + } + if (/^-?\d+(?:\.\d+)?$/.test(normalized)) { + return Number.parseFloat(normalized); + } + return undefined; +} + +function classifyDeterministicShadow( + rawValue: string, +): ChromeShadowKind | undefined { + const raw = String(rawValue ?? "").trim(); + if (!raw) { + return undefined; + } + if (/^[A-Za-z_$][\w$]*$/.test(raw)) { + return undefined; + } + + const normalized = raw.toLowerCase(); + if ( + normalized.includes("var(") || + normalized.includes("${") || + normalized === "inherit" || + normalized === "initial" || + normalized === "unset" || + normalized === "revert" || + normalized === "revert-layer" + ) { + return undefined; + } + if (normalized === "none" || normalized === "0" || normalized === "0px") { + return "none"; + } + + const parts = normalized + .split(/,(?![^()]*\))/) + .map((part) => part.trim()) + .filter(Boolean); + if (parts.length === 0) { + return undefined; + } + + const hasInset = parts.some((part) => /\binset\b/.test(part)); + const hasOuter = parts.some((part) => !/\binset\b/.test(part)); + + if (hasInset && hasOuter) { + return "mixed"; + } + if (hasInset) { + return "inset"; + } + return "outer"; +} + async function extractLandingPattern( sectionFiles: string[], workspaceRoot: string, diff --git a/packages/interfacectl-cli/src/utils/chrome-policy-seeding.ts b/packages/interfacectl-cli/src/utils/chrome-policy-seeding.ts index 3c5e833..17c6979 100644 --- a/packages/interfacectl-cli/src/utils/chrome-policy-seeding.ts +++ b/packages/interfacectl-cli/src/utils/chrome-policy-seeding.ts @@ -1,45 +1,128 @@ -import type { InterfaceContract } from "@surfaces/interfacectl-validator"; +import type { + ChromePolicyTarget, + InterfaceContract, +} from "@surfaces/interfacectl-validator"; +import { collectSurfaceDescriptors } from "../descriptors/static-analysis.js"; + +export interface ChromePolicySeedingInput { + workspaceRoot: string; + appRoot: string; + surfaceId: string; + contract: InterfaceContract; +} export interface ChromePolicySeedResult { contract: InterfaceContract; warnings: Array<{ code: string; message: string }>; } -const DEFAULT_CHROME_POLICY = { - policy: "off", - targets: ["page-container", "top-level-section", "layout-container"], - maxBorderRadiusPx: 8, - allowOuterShadow: false, - allowInsetShadow: true, -} as const; - -export async function seedChromePolicyDefaults({ - contract, -}: { - contract: InterfaceContract; -}): Promise { +function normalizeTargets(targets: ChromePolicyTarget[]): ChromePolicyTarget[] { + return [...new Set(targets.map((value) => value.trim()).filter(Boolean))].sort( + (a, b) => a.localeCompare(b), + ) as ChromePolicyTarget[]; +} + +function hasShadowKind( + shadowKinds: string[], + expected: "outer" | "inset", +): boolean { + return shadowKinds.some((kind) => kind === expected || kind === "mixed"); +} + +export async function seedChromePolicyFromObservedDescriptors( + input: ChromePolicySeedingInput, +): Promise { + const warnings: Array<{ code: string; message: string }> = []; + const surfaceRootMap = new Map([[input.surfaceId, input.appRoot]]); + + const descriptorResult = await collectSurfaceDescriptors({ + workspaceRoot: input.workspaceRoot, + contract: input.contract, + surfaceFilters: new Set([input.surfaceId]), + surfaceRootMap, + }); + + for (const warning of descriptorResult.warnings) { + warnings.push({ + code: `chrome-seed.${warning.code}`, + message: `Descriptor warning during chrome seed: ${warning.message}`, + }); + } + for (const error of descriptorResult.errors) { + warnings.push({ + code: `chrome-seed.${error.code}`, + message: `Descriptor error during chrome seed: ${error.message}`, + }); + } + + const targetSurface = input.contract.surfaces.find( + (surface) => surface.id === input.surfaceId, + ); + if (!targetSurface || targetSurface.type !== "web") { + return { + contract: input.contract, + warnings, + }; + } + + if (targetSurface.layout.chromePolicy) { + return { + contract: input.contract, + warnings, + }; + } + + const descriptor = descriptorResult.descriptors.find( + (entry) => entry.surfaceId === input.surfaceId, + ); + const observedChrome = descriptor?.layout.chrome; + const observedTargets = normalizeTargets(observedChrome?.targets ?? []); + + if (!observedChrome || observedTargets.length === 0) { + warnings.push({ + code: "chrome-seed.none-detected", + message: + "Chrome policy was not seeded from extraction because no portable chrome markers were detected.", + }); + return { + contract: input.contract, + warnings, + }; + } + + if (observedChrome.hasAmbiguousSignals) { + warnings.push({ + code: "chrome-seed.ambiguous-signals", + message: + "Chrome policy was not seeded from extraction because one or more chrome signals on portable markers were not deterministically extractable.", + }); + return { + contract: input.contract, + warnings, + }; + } + return { contract: { - ...contract, - surfaces: contract.surfaces.map((surface) => { - if (surface.type !== "web") { - return surface; - } - if (surface.layout.chromePolicy) { - return surface; - } - return { - ...surface, - layout: { - ...surface.layout, - chromePolicy: { - ...DEFAULT_CHROME_POLICY, - targets: [...DEFAULT_CHROME_POLICY.targets], - }, - }, - }; - }), + ...input.contract, + surfaces: input.contract.surfaces.map((surface) => + surface.id === input.surfaceId && surface.type === "web" + ? { + ...surface, + layout: { + ...surface.layout, + chromePolicy: { + policy: "off", + targets: observedTargets, + maxBorderRadiusPx: observedChrome.maxBorderRadiusPx ?? 0, + allowOuterShadow: hasShadowKind(observedChrome.shadowKinds, "outer"), + allowInsetShadow: hasShadowKind(observedChrome.shadowKinds, "inset"), + }, + }, + } + : surface, + ), }, - warnings: [], + warnings, }; } diff --git a/packages/interfacectl-cli/test/chrome-policy-seeding.test.mjs b/packages/interfacectl-cli/test/chrome-policy-seeding.test.mjs index db8ffa4..b6a461c 100644 --- a/packages/interfacectl-cli/test/chrome-policy-seeding.test.mjs +++ b/packages/interfacectl-cli/test/chrome-policy-seeding.test.mjs @@ -1,14 +1,17 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { seedChromePolicyDefaults } from "../dist/utils/chrome-policy-seeding.js"; +import os from "node:os"; +import path from "node:path"; +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { seedChromePolicyFromObservedDescriptors } from "../dist/utils/chrome-policy-seeding.js"; -function baseContract() { +function baseContract(surfaceId = "demo-web") { return { contractId: "seed-test", version: "1.0.0", surfaces: [ { - id: "demo-web", + id: surfaceId, displayName: "Demo Web", type: "web", requiredSections: ["header"], @@ -38,23 +41,294 @@ function baseContract() { }; } -test("seedChromePolicyDefaults seeds safe defaults for web surfaces only", async () => { - const seeded = await seedChromePolicyDefaults({ - contract: baseContract(), - }); +test("seedChromePolicyFromObservedDescriptors seeds chromePolicy from portable layout-container and top-level-section markers", async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "interfacectl-seed-chrome-")); + const surfaceId = "demo-web"; + const appRoot = path.join(tempRoot, "apps", surfaceId); - assert.deepEqual(seeded.contract.surfaces[0].layout.chromePolicy, { - policy: "off", - targets: ["page-container", "top-level-section", "layout-container"], - maxBorderRadiusPx: 8, - allowOuterShadow: false, - allowInsetShadow: true, - }); - assert.equal(seeded.contract.surfaces[1].layout.chromePolicy, undefined); + try { + await mkdir(path.join(appRoot, "app"), { recursive: true }); + + await writeFile( + path.join(appRoot, "app", "layout.tsx"), + ` + export default function RootLayout({ children }) { + return ( + + {children} + + ); + } + `, + "utf8", + ); + + await writeFile( + path.join(appRoot, "app", "page.tsx"), + ` + export default function Page() { + return ( +
+ Hello +
+ ); + } + `, + "utf8", + ); + + await writeFile( + path.join(appRoot, "app", "globals.css"), + ` + .contract-container { + border-radius: 8px; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12); + } + `, + "utf8", + ); + + const seeded = await seedChromePolicyFromObservedDescriptors({ + workspaceRoot: tempRoot, + appRoot, + surfaceId, + contract: baseContract(surfaceId), + }); + + assert.deepEqual(seeded.contract.surfaces[0].layout.chromePolicy, { + policy: "off", + targets: ["layout-container", "top-level-section"], + maxBorderRadiusPx: 8, + allowOuterShadow: true, + allowInsetShadow: true, + }); + assert.equal(seeded.contract.surfaces[1].layout.chromePolicy, undefined); + assert.equal( + seeded.warnings.some((warning) => warning.code === "chrome-seed.none-detected"), + false, + ); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); + +test("seedChromePolicyFromObservedDescriptors preserves page-container compatibility", async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "interfacectl-seed-chrome-page-container-")); + const surfaceId = "demo-web"; + const appRoot = path.join(tempRoot, "apps", surfaceId); + + try { + await mkdir(path.join(appRoot, "app"), { recursive: true }); + + await writeFile( + path.join(appRoot, "app", "layout.tsx"), + ` + export default function RootLayout({ children }) { + return {children}; + } + `, + "utf8", + ); + + await writeFile( + path.join(appRoot, "app", "page.tsx"), + ` + export default function Page() { + return ( +
+
+ Hello +
+
+ ); + } + `, + "utf8", + ); + + const seeded = await seedChromePolicyFromObservedDescriptors({ + workspaceRoot: tempRoot, + appRoot, + surfaceId, + contract: baseContract(surfaceId), + }); + + assert.deepEqual(seeded.contract.surfaces[0].layout.chromePolicy, { + policy: "off", + targets: ["page-container", "top-level-section"], + maxBorderRadiusPx: 8, + allowOuterShadow: true, + allowInsetShadow: true, + }); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); + +test("seedChromePolicyFromObservedDescriptors preserves legacy data-contract-container compatibility", async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "interfacectl-seed-chrome-legacy-container-")); + const surfaceId = "demo-web"; + const appRoot = path.join(tempRoot, "apps", surfaceId); + + try { + await mkdir(path.join(appRoot, "app"), { recursive: true }); + + await writeFile( + path.join(appRoot, "app", "layout.tsx"), + ` + export default function RootLayout({ children }) { + return ( + + {children} + + ); + } + `, + "utf8", + ); + + await writeFile( + path.join(appRoot, "app", "page.tsx"), + ` + export default function Page() { + return ( +
+ Hello +
+ ); + } + `, + "utf8", + ); + + await writeFile( + path.join(appRoot, "app", "globals.css"), + ` + [data-contract-container="shell"] { + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12); + } + `, + "utf8", + ); + + const seeded = await seedChromePolicyFromObservedDescriptors({ + workspaceRoot: tempRoot, + appRoot, + surfaceId, + contract: baseContract(surfaceId), + }); + + assert.deepEqual(seeded.contract.surfaces[0].layout.chromePolicy, { + policy: "off", + targets: ["layout-container", "top-level-section"], + maxBorderRadiusPx: 6, + allowOuterShadow: true, + allowInsetShadow: true, + }); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } }); -test("seedChromePolicyDefaults preserves existing chromePolicy", async () => { - const contract = baseContract(); +test("seedChromePolicyFromObservedDescriptors skips seeding when chrome signals are ambiguous", async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "interfacectl-seed-chrome-ambiguous-")); + const surfaceId = "demo-web"; + const appRoot = path.join(tempRoot, "apps", surfaceId); + + try { + await mkdir(path.join(appRoot, "app"), { recursive: true }); + + await writeFile( + path.join(appRoot, "app", "layout.tsx"), + ` + export default function RootLayout({ children }) { + return ( + + {children} + + ); + } + `, + "utf8", + ); + + await writeFile( + path.join(appRoot, "app", "page.tsx"), + ` + export default function Page() { + return
Hello
; + } + `, + "utf8", + ); + + const seeded = await seedChromePolicyFromObservedDescriptors({ + workspaceRoot: tempRoot, + appRoot, + surfaceId, + contract: baseContract(surfaceId), + }); + + assert.equal(seeded.contract.surfaces[0].layout.chromePolicy, undefined); + assert.equal( + seeded.warnings.some((warning) => warning.code === "chrome-seed.ambiguous-signals"), + true, + ); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); + +test("seedChromePolicyFromObservedDescriptors skips seeding when no portable markers are present", async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "interfacectl-seed-chrome-none-")); + const surfaceId = "demo-web"; + const appRoot = path.join(tempRoot, "apps", surfaceId); + + try { + await mkdir(path.join(appRoot, "app"), { recursive: true }); + + await writeFile( + path.join(appRoot, "app", "layout.tsx"), + ` + export default function RootLayout({ children }) { + return {children}; + } + `, + "utf8", + ); + + await writeFile( + path.join(appRoot, "app", "page.tsx"), + ` + export default function Page() { + return
Hello
; + } + `, + "utf8", + ); + + const seeded = await seedChromePolicyFromObservedDescriptors({ + workspaceRoot: tempRoot, + appRoot, + surfaceId, + contract: baseContract(surfaceId), + }); + + assert.equal(seeded.contract.surfaces[0].layout.chromePolicy, undefined); + assert.equal( + seeded.warnings.some((warning) => warning.code === "chrome-seed.none-detected"), + true, + ); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); + +test("seedChromePolicyFromObservedDescriptors preserves existing chromePolicy", async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "interfacectl-seed-chrome-preserve-")); + const surfaceId = "demo-web"; + const appRoot = path.join(tempRoot, "apps", surfaceId); + const contract = baseContract(surfaceId); contract.surfaces[0].layout.chromePolicy = { policy: "strict", targets: ["page-container"], @@ -63,12 +337,42 @@ test("seedChromePolicyDefaults preserves existing chromePolicy", async () => { allowInsetShadow: false, }; - const seeded = await seedChromePolicyDefaults({ contract }); - assert.deepEqual(seeded.contract.surfaces[0].layout.chromePolicy, { - policy: "strict", - targets: ["page-container"], - maxBorderRadiusPx: 6, - allowOuterShadow: false, - allowInsetShadow: false, - }); + try { + await mkdir(path.join(appRoot, "app"), { recursive: true }); + await writeFile( + path.join(appRoot, "app", "layout.tsx"), + ` + export default function RootLayout({ children }) { + return {children}; + } + `, + "utf8", + ); + await writeFile( + path.join(appRoot, "app", "page.tsx"), + ` + export default function Page() { + return
Hello
; + } + `, + "utf8", + ); + + const seeded = await seedChromePolicyFromObservedDescriptors({ + workspaceRoot: tempRoot, + appRoot, + surfaceId, + contract, + }); + + assert.deepEqual(seeded.contract.surfaces[0].layout.chromePolicy, { + policy: "strict", + targets: ["page-container"], + maxBorderRadiusPx: 6, + allowOuterShadow: false, + allowInsetShadow: false, + }); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } }); diff --git a/packages/interfacectl-cli/test/static-analysis.test.mjs b/packages/interfacectl-cli/test/static-analysis.test.mjs index bd41d06..05c21fc 100644 --- a/packages/interfacectl-cli/test/static-analysis.test.mjs +++ b/packages/interfacectl-cli/test/static-analysis.test.mjs @@ -398,6 +398,389 @@ test("collectSurfaceDescriptors captures deterministic icon sources from surface } }); +test("collectSurfaceDescriptors captures portable chrome markers and deterministic chrome signals", async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "interfacectl-chrome-static-")); + const surfaceId = "demo-chrome"; + const surfaceRoot = path.join(tempRoot, "apps", surfaceId); + + try { + await mkdir(path.join(surfaceRoot, "app"), { recursive: true }); + + await writeFile( + path.join(surfaceRoot, "app", "layout.tsx"), + ` + export default function RootLayout({ children }) { + return ( + + +
+ {children} +
+ + + ); + } + `, + "utf-8", + ); + + await writeFile( + path.join(surfaceRoot, "app", "page.tsx"), + ` + export default function Page() { + return ( +
+
+ Hello +
+
+ ); + } + `, + "utf-8", + ); + + await writeFile( + path.join(surfaceRoot, "app", "globals.css"), + ` + .contract-container { + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12); + } + `, + "utf-8", + ); + + const contract = { + contractId: "test.contract", + version: "1.0.0", + sections: [{ id: "main.hero", intent: "Hero", description: "Hero section" }], + constraints: { + motion: { + allowedDurationsMs: [120], + allowedTimingFunctions: ["linear"], + }, + }, + surfaces: [ + { + id: surfaceId, + displayName: "Demo Chrome", + type: "web", + requiredSections: ["main.hero"], + allowedFonts: ["Inter"], + layout: { maxContentWidth: 960 }, + }, + ], + color: { + policy: "off", + allowedValues: [], + }, + }; + + const result = await collectSurfaceDescriptors({ + workspaceRoot: tempRoot, + contract, + surfaceFilters: new Set(), + surfaceRootMap: new Map(), + }); + + assert.equal(result.errors.length, 0); + assert.equal( + result.warnings.some((warning) => warning.code.startsWith("chrome.")), + false, + ); + const descriptor = result.descriptors[0]; + assert.ok(descriptor?.layout.chrome, "chrome descriptor should be extracted"); + assert.deepEqual(descriptor.layout.chrome.targets, [ + "layout-container", + "page-container", + "top-level-section", + ]); + assert.equal(descriptor.layout.chrome.maxBorderRadiusPx, 8); + assert.deepEqual(descriptor.layout.chrome.shadowKinds, ["inset", "outer"]); + assert.equal(descriptor.layout.chrome.hasAmbiguousSignals, undefined); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); + +test("collectSurfaceDescriptors flags ambiguous chrome signals", async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "interfacectl-chrome-ambiguous-")); + const surfaceId = "demo-chrome-ambiguous"; + const surfaceRoot = path.join(tempRoot, "apps", surfaceId); + + try { + await mkdir(path.join(surfaceRoot, "app"), { recursive: true }); + + await writeFile( + path.join(surfaceRoot, "app", "layout.tsx"), + ` + export default function RootLayout({ children }) { + return ( + + + {children} + + + ); + } + `, + "utf-8", + ); + + await writeFile( + path.join(surfaceRoot, "app", "page.tsx"), + ` + export default function Page() { + return ( +
+ Hello +
+ ); + } + `, + "utf-8", + ); + + const contract = { + contractId: "test.contract", + version: "1.0.0", + sections: [{ id: "main.hero", intent: "Hero", description: "Hero section" }], + constraints: { + motion: { + allowedDurationsMs: [120], + allowedTimingFunctions: ["linear"], + }, + }, + surfaces: [ + { + id: surfaceId, + displayName: "Demo Chrome Ambiguous", + type: "web", + requiredSections: ["main.hero"], + allowedFonts: ["Inter"], + layout: { maxContentWidth: 960 }, + }, + ], + color: { + policy: "off", + allowedValues: [], + }, + }; + + const result = await collectSurfaceDescriptors({ + workspaceRoot: tempRoot, + contract, + surfaceFilters: new Set(), + surfaceRootMap: new Map(), + }); + + assert.equal(result.errors.length, 0); + assert.equal( + result.warnings.some((warning) => warning.code === "chrome.radius-undetermined"), + true, + ); + assert.equal( + result.warnings.some((warning) => warning.code === "chrome.shadow-undetermined"), + true, + ); + const descriptor = result.descriptors[0]; + assert.ok(descriptor?.layout.chrome, "chrome descriptor should still exist"); + assert.equal(descriptor.layout.chrome.hasAmbiguousSignals, true); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); + +test("collectSurfaceDescriptors preserves legacy data-contract-container chrome extraction", async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "interfacectl-chrome-legacy-")); + const surfaceId = "demo-chrome-legacy"; + const surfaceRoot = path.join(tempRoot, "apps", surfaceId); + + try { + await mkdir(path.join(surfaceRoot, "app"), { recursive: true }); + + await writeFile( + path.join(surfaceRoot, "app", "layout.tsx"), + ` + export default function RootLayout({ children }) { + return ( + + {children} + + ); + } + `, + "utf-8", + ); + + await writeFile( + path.join(surfaceRoot, "app", "page.tsx"), + ` + export default function Page() { + return ( +
+ Hello +
+ ); + } + `, + "utf-8", + ); + + await writeFile( + path.join(surfaceRoot, "app", "globals.css"), + ` + [data-contract-container="shell"] { + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12); + } + `, + "utf-8", + ); + + const contract = { + contractId: "test.contract", + version: "1.0.0", + sections: [{ id: "main.hero", intent: "Hero", description: "Hero section" }], + constraints: { + motion: { + allowedDurationsMs: [120], + allowedTimingFunctions: ["linear"], + }, + }, + surfaces: [ + { + id: surfaceId, + displayName: "Demo Chrome Legacy", + type: "web", + requiredSections: ["main.hero"], + allowedFonts: ["Inter"], + layout: { maxContentWidth: 960 }, + }, + ], + color: { + policy: "off", + allowedValues: [], + }, + }; + + const result = await collectSurfaceDescriptors({ + workspaceRoot: tempRoot, + contract, + surfaceFilters: new Set(), + surfaceRootMap: new Map(), + }); + + assert.equal(result.errors.length, 0); + const descriptor = result.descriptors[0]; + assert.ok(descriptor?.layout.chrome, "chrome descriptor should be extracted"); + assert.deepEqual(descriptor.layout.chrome.targets, [ + "layout-container", + "top-level-section", + ]); + assert.equal(descriptor.layout.chrome.maxBorderRadiusPx, 6); + assert.deepEqual(descriptor.layout.chrome.shadowKinds, ["inset", "outer"]); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); + +test("collectSurfaceDescriptors only uses top-level contract sections for chrome extraction", async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "interfacectl-chrome-top-level-")); + const surfaceId = "demo-chrome-top-level"; + const surfaceRoot = path.join(tempRoot, "apps", surfaceId); + + try { + await mkdir(path.join(surfaceRoot, "app"), { recursive: true }); + + await writeFile( + path.join(surfaceRoot, "app", "layout.tsx"), + ` + export default function RootLayout({ children }) { + return {children}; + } + `, + "utf-8", + ); + + await writeFile( + path.join(surfaceRoot, "app", "page.tsx"), + ` + export default function Page() { + return ( +
+
+
+ Nested content +
+
+
+ ); + } + `, + "utf-8", + ); + + const contract = { + contractId: "test.contract", + version: "1.0.0", + sections: [ + { id: "landing.hero", intent: "Hero", description: "Hero section" }, + { id: "landing.guidance", intent: "Guidance", description: "Guidance section" }, + ], + constraints: { + motion: { + allowedDurationsMs: [120], + allowedTimingFunctions: ["linear"], + }, + }, + surfaces: [ + { + id: surfaceId, + displayName: "Demo Chrome Top Level", + type: "web", + requiredSections: ["landing.hero", "landing.guidance"], + allowedFonts: ["Inter"], + layout: { maxContentWidth: 960 }, + }, + ], + color: { + policy: "off", + allowedValues: [], + }, + }; + + const result = await collectSurfaceDescriptors({ + workspaceRoot: tempRoot, + contract, + surfaceFilters: new Set(), + surfaceRootMap: new Map(), + }); + + assert.equal(result.errors.length, 0); + assert.equal( + result.warnings.some((warning) => warning.code === "chrome.shadow-undetermined"), + false, + ); + const descriptor = result.descriptors[0]; + assert.ok(descriptor?.layout.chrome, "chrome descriptor should be extracted"); + assert.deepEqual(descriptor.layout.chrome.targets, ["top-level-section"]); + assert.equal(descriptor.layout.chrome.maxBorderRadiusPx, 6); + assert.equal(descriptor.layout.chrome.hasAmbiguousSignals, undefined); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); + test("collectSurfaceDescriptors captures landing pattern topology and background mode", async () => { const tempRoot = await mkdtemp(path.join(os.tmpdir(), "interfacectl-landing-pattern-")); const surfaceId = "demo-landing"; diff --git a/packages/interfacectl-validator/dist/index.d.ts b/packages/interfacectl-validator/dist/index.d.ts index ba0f5e4..5d78ce9 100644 --- a/packages/interfacectl-validator/dist/index.d.ts +++ b/packages/interfacectl-validator/dist/index.d.ts @@ -8,7 +8,7 @@ export interface ContractStructureValidation { export declare function validateContractStructure(contractData: unknown, schema: object): ContractStructureValidation; export declare function evaluateSurfaceCompliance(contract: InterfaceContract, descriptor: SurfaceDescriptor): SurfaceReport; export declare function evaluateContractCompliance(contract: InterfaceContract, descriptors: SurfaceDescriptor[]): ValidationSummary; -export type { InterfaceContract, ContractSurface, ContractSection, ContractConstraints, SurfaceDescriptor, SurfaceSectionDescriptor, SurfaceFontDescriptor, SurfaceColorDescriptor, SurfaceIconDescriptor, SurfaceMotionDescriptor, SurfaceLayoutDescriptor, PageFrameLayoutDescriptor, LandingPatternDescriptor, SurfacePrimitiveDescriptor, SurfaceReport, DriftViolation, ValidationSummary, DriftViolationType, ContractRef, RuleRef, DiffOutput, DiffEntry, DiffChangeType, DriftRisk, Severity, SafetyLevel, EnforcementPolicy, EnforcementMode, IconPolicy, FlowPolicy, FlowRequirement, FlowTransitionRequirement, LandingPatternPolicy, SurfaceFlowDescriptor, SurfaceFlowStepDescriptor, SurfaceFlowTransitionDescriptor, AutofixRule, FixSummary, FixEntry, FixError, } from "./types.js"; +export type { InterfaceContract, ContractSurface, ContractSection, ContractConstraints, SurfaceDescriptor, SurfaceSectionDescriptor, SurfaceFontDescriptor, SurfaceColorDescriptor, SurfaceIconDescriptor, SurfaceMotionDescriptor, SurfaceLayoutDescriptor, PageFrameLayoutDescriptor, ChromeLayoutDescriptor, ChromePolicyTarget, ChromeShadowKind, LandingPatternDescriptor, SurfacePrimitiveDescriptor, SurfaceReport, DriftViolation, ValidationSummary, DriftViolationType, ContractRef, RuleRef, DiffOutput, DiffEntry, DiffChangeType, DriftRisk, Severity, SafetyLevel, EnforcementPolicy, EnforcementMode, IconPolicy, FlowPolicy, FlowRequirement, FlowTransitionRequirement, LandingPatternPolicy, SurfaceFlowDescriptor, SurfaceFlowStepDescriptor, SurfaceFlowTransitionDescriptor, AutofixRule, FixSummary, FixEntry, FixError, } from "./types.js"; export { getBundledDiffSchema, getBundledPolicySchema, getBundledFixSummarySchema, validateDiffOutput, validatePolicy, validateFixSummary, type ValidationResult, } from "./schema-validate.js"; export { normalizeColorValue, normalizeColorValues, } from "./color-policy.js"; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/interfacectl-validator/dist/index.d.ts.map b/packages/interfacectl-validator/dist/index.d.ts.map index 261b2dd..2d347ea 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;AAQpB,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;AA0VD,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,EAAE,iBAAiB,GAC5B,aAAa,CA8af;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,qBAAqB,EACrB,uBAAuB,EACvB,uBAAuB,EACvB,yBAAyB,EACzB,wBAAwB,EACxB,0BAA0B,EAC1B,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,UAAU,EACV,UAAU,EACV,eAAe,EACf,yBAAyB,EACzB,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,+BAA+B,EAC/B,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;AAE9B,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,mBAAmB,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,EAQlB,MAAM,YAAY,CAAC;AAQpB,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;AA0VD,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,EAAE,iBAAiB,GAC5B,aAAa,CA8af;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,qBAAqB,EACrB,uBAAuB,EACvB,uBAAuB,EACvB,yBAAyB,EACzB,sBAAsB,EACtB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,EACxB,0BAA0B,EAC1B,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,UAAU,EACV,UAAU,EACV,eAAe,EACf,yBAAyB,EACzB,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,+BAA+B,EAC/B,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;AAE9B,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC"} \ 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 d4ce422..26b4b8d 100644 --- a/packages/interfacectl-validator/dist/types.d.ts +++ b/packages/interfacectl-validator/dist/types.d.ts @@ -15,6 +15,7 @@ export interface ChromePolicy { allowOuterShadow: boolean; allowInsetShadow: boolean; } +export type ChromeShadowKind = "none" | "outer" | "inset" | "mixed"; export interface LandingPatternPolicy { policy: "off" | "warn" | "strict"; requireTopLevelSections?: string[]; @@ -152,12 +153,20 @@ export interface LandingPatternDescriptor { pageBackgroundMode?: "solid" | "custom" | "unknown"; source?: string; } +export interface ChromeLayoutDescriptor { + targets: ChromePolicyTarget[]; + maxBorderRadiusPx?: number | null; + shadowKinds: ChromeShadowKind[]; + source?: string[]; + hasAmbiguousSignals?: boolean; +} export interface SurfaceLayoutDescriptor { maxContentWidth?: number | null; containers?: string[]; containerSources?: string[]; source?: string; pageFrame?: PageFrameLayoutDescriptor; + chrome?: ChromeLayoutDescriptor; landingPattern?: LandingPatternDescriptor; } export interface SurfaceDescriptor { diff --git a/packages/interfacectl-validator/dist/types.d.ts.map b/packages/interfacectl-validator/dist/types.d.ts.map index acad1c8..caa9c99 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 4d64d8c..f7e878c 100644 --- a/packages/interfacectl-validator/src/index.ts +++ b/packages/interfacectl-validator/src/index.ts @@ -9,6 +9,9 @@ import { ContractSection, ContractSurface, PageFrameLayoutDescriptor, + ChromeLayoutDescriptor, + ChromePolicyTarget, + ChromeShadowKind, } from "./types.js"; import bundledSchema from "./schema/web.surface.contract.schema.json" with { type: "json", @@ -938,6 +941,9 @@ export type { SurfaceMotionDescriptor, SurfaceLayoutDescriptor, PageFrameLayoutDescriptor, + ChromeLayoutDescriptor, + ChromePolicyTarget, + ChromeShadowKind, LandingPatternDescriptor, SurfacePrimitiveDescriptor, SurfaceReport, diff --git a/packages/interfacectl-validator/src/types.ts b/packages/interfacectl-validator/src/types.ts index bc4b820..6c85c45 100644 --- a/packages/interfacectl-validator/src/types.ts +++ b/packages/interfacectl-validator/src/types.ts @@ -22,6 +22,8 @@ export interface ChromePolicy { allowInsetShadow: boolean; } +export type ChromeShadowKind = "none" | "outer" | "inset" | "mixed"; + export interface LandingPatternPolicy { policy: "off" | "warn" | "strict"; requireTopLevelSections?: string[]; @@ -181,12 +183,21 @@ export interface LandingPatternDescriptor { source?: string; } +export interface ChromeLayoutDescriptor { + targets: ChromePolicyTarget[]; + maxBorderRadiusPx?: number | null; + shadowKinds: ChromeShadowKind[]; + source?: string[]; + hasAmbiguousSignals?: boolean; +} + export interface SurfaceLayoutDescriptor { maxContentWidth?: number | null; containers?: string[]; containerSources?: string[]; source?: string; pageFrame?: PageFrameLayoutDescriptor; + chrome?: ChromeLayoutDescriptor; landingPattern?: LandingPatternDescriptor; } From b12fae9d3b25bf57b999952c02099c208d142c86 Mon Sep 17 00:00:00 2001 From: Mike Long Date: Tue, 10 Mar 2026 12:18:45 -0700 Subject: [PATCH 2/2] fix(vercel): add static preview output --- public/index.html | 187 ++++++++++++++++++++++++++++++++++++++++++++++ vercel.json | 5 ++ 2 files changed, 192 insertions(+) create mode 100644 public/index.html create mode 100644 vercel.json diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..c5f4733 --- /dev/null +++ b/public/index.html @@ -0,0 +1,187 @@ + + + + + + interfacectl + + + +
+
+
Shared Contract Tooling
+

interfacectl

+

+ This Vercel deployment is a lightweight landing page for the shared + `interfacectl` monorepo. The repo itself ships CLI and validator + packages, so previews point to source and docs rather than a full web + application. +

+
+ + + + +
+ + diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..954a80d --- /dev/null +++ b/vercel.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "buildCommand": "pnpm run build", + "outputDirectory": "public" +}