From dc42b68f8517199a3ae4018bc751aa98d2a98f10 Mon Sep 17 00:00:00 2001 From: Mike Long Date: Wed, 4 Mar 2026 11:30:32 -0800 Subject: [PATCH 1/3] feat: add generic flow policy framework v1 --- docs/plans/flow-policy-generic-v1.md | 78 +++++ .../interfacectl-cli/dist/commands/compile.js | 1 + .../dist/commands/describe.d.ts | 2 +- .../dist/commands/describe.d.ts.map | 2 +- .../dist/commands/describe.js | 165 +++++++++- .../dist/commands/validate.d.ts.map | 2 +- .../dist/commands/validate.js | 230 +++++++++++++- .../interfacectl-cli/src/commands/compile.ts | 1 + .../interfacectl-cli/src/commands/describe.ts | 213 ++++++++++++- .../interfacectl-cli/src/commands/validate.ts | 285 +++++++++++++++++- .../interfacectl-cli/test/compile.test.mjs | 76 +++++ .../test/flow-policy.test.mjs | 266 ++++++++++++++++ .../test/generation-boundaries-guard.test.mjs | 106 +++++++ .../test/violation-classifier.test.mjs | 6 + .../interfacectl-validator/dist/index.d.ts | 2 +- .../dist/index.d.ts.map | 2 +- packages/interfacectl-validator/dist/index.js | 122 ++++++++ .../schema/web.surface.contract.schema.json | 86 ++++++ .../interfacectl-validator/dist/types.d.ts | 33 +- .../dist/types.d.ts.map | 2 +- packages/interfacectl-validator/src/index.ts | 162 ++++++++++ .../schema/web.surface.contract.schema.json | 86 ++++++ packages/interfacectl-validator/src/types.ts | 43 +++ .../test/contract-structure.test.mjs | 41 +++ .../test/evaluate-contract.test.mjs | 183 +++++++++++ tools/check-generation-boundaries.mjs | 212 +++++++++++++ 26 files changed, 2394 insertions(+), 13 deletions(-) create mode 100644 docs/plans/flow-policy-generic-v1.md create mode 100644 packages/interfacectl-cli/test/flow-policy.test.mjs diff --git a/docs/plans/flow-policy-generic-v1.md b/docs/plans/flow-policy-generic-v1.md new file mode 100644 index 0000000..7044dd0 --- /dev/null +++ b/docs/plans/flow-policy-generic-v1.md @@ -0,0 +1,78 @@ +# Feature Plan + +## Summary +Flow Policy Generic v1 introduces contract-level flow requirements so Surfaces can evaluate graph-shaped user journeys, not only page/component-level structure. The increment adds additive schema/types/validator support for per-surface flow policies and requirement sets (`flowId`, step constraints, transition constraints, terminal constraints). `interfacectl` consumes explicit per-surface flow descriptor artifacts and enforces the same semantics at `Generation time` and `CI/CD time` with policy-driven severity (`off|warn|strict`). + +## Strategy alignment +This work strengthens the decision filter by extending deterministic allow/block behavior to higher-order flow decisions that agents frequently generate. It removes an enforcement gap where flow intent existed only in prompts or documentation and converts it into contract-backed checks with machine-readable findings and lifecycle-consistent handling. + +Decision filter sentence +"When an agent proposes a change to the user experience, Surfaces determines whether that change is allowed, under what conditions it may proceed, and blocks or corrects it if it violates intent." + +## Contract impact +1. Contract definition +- Add optional `surfaces[].flows` with: + - `policy`: `off|warn|strict` + - `requirements[]`: `{ flowId, minSteps?, requiredSteps?, requiredTransitions?, terminalSteps? }` +- Add flow descriptor shape consumed by validators: + - `flows[]`: `{ flowId, steps[{id}], transitions[{from,to}], source? }` +- No breaking changes to existing contracts; `flows` is optional. + +2. Enforcement timing +- `Generation time`: `check-generation-boundaries` evaluates flow policy from descriptor payloads. +- `CI/CD time`: `interfacectl validate` loads flow descriptor artifacts and applies flow checks during compliance evaluation. +- `Runtime (edge)`: out of scope in this repo; handled by consuming surfaces, but compile output carries flow policy for runtime consumers. + +3. Violation handling +- `off`: flow checks skipped. +- `warn`: flow findings emitted as warnings, non-blocking. +- `strict`: flow findings emitted as errors, blocking. +- Malformed flow descriptor artifact is treated as `E0` artifact invalid. + +## Surface impact +- `interfacectl`: primary implementation surface; schema, validator, CLI ingestion, generation guard, compile output, tests. +- `surfaceops.ai`: indirectly benefits through richer finding taxonomy consumed from run artifacts. +- `surfaces.systems`: indirectly benefits when runtime consumers load compile/runtime manifest flow policy. + +## Non-goals +- No scenario-specific scaffolds (checkout/onboarding templates) in this increment. +- No automatic flow extraction from source code or AST-based graph inference. +- No ownership workflow, assignment, or post-hoc observability-only UI work. + +## Acceptance criteria +1. Contracts without `flows` remain schema-valid and behaviorally unchanged. +2. Contracts with valid `surfaces[].flows` pass schema validation. +3. Invalid flow schema (for example `minSteps: 0`) fails schema validation with actionable errors. +4. Validator emits all six flow violation types when conditions are met. +5. `validate` maps flow violations to finding codes: + - `descriptor.flows.missing` + - `flow.required.missing` + - `flow.steps.min` + - `flow.steps.required` + - `flow.transition.required` + - `flow.terminal.invalid` +6. `validate` returns warning severity for flow findings under warn policy and error severity under strict policy. +7. Missing flow descriptor artifact produces policy-based flow findings (not `E0`). +8. Malformed flow descriptor artifact fails as `E0`. +9. Generation guard JSON includes `evaluation.flowPolicyEvaluated` and flow findings; text mode remains backward-compatible. +10. Compile output includes `surfaces/.json.flows` when configured. + +## Risks and drift checks +1. Risk: drifting into prompt-level guidance instead of enforcement. +- Mitigation: all flow behavior is contract-schema backed and evaluated by validator/guard only. + +2. Risk: introducing observability-only signals with no gate behavior. +- Mitigation: preserve policy severity mapping so strict remains blocking in both generation guard and validate. + +3. Risk: hidden dependency on extraction or app runtime. +- Mitigation: require explicit flow descriptor artifacts in CI path; no runtime app execution. + +## Implementation outline +1. Add `flows` schema definitions and type interfaces. +2. Add validator flow compliance checks and new drift violation taxonomy. +3. Add flow descriptor artifact loading to `validate` and `describe` with fallback path support. +4. Map flow violations to stable finding codes and severity behavior. +5. Extend generation guard JSON flow checks and coverage metadata. +6. Update compile output to include surface flow policy data. +7. Add/extend unit and integration tests across validator, CLI, and guard. +8. Document contract/flow semantics in strategy-adjacent docs. diff --git a/packages/interfacectl-cli/dist/commands/compile.js b/packages/interfacectl-cli/dist/commands/compile.js index 25d7522..4161fe7 100644 --- a/packages/interfacectl-cli/dist/commands/compile.js +++ b/packages/interfacectl-cli/dist/commands/compile.js @@ -155,5 +155,6 @@ function surfaceToBundlePayload(surface) { allowedFonts: surface.allowedFonts, layout: surface.layout, ...(surface.icons ? { icons: surface.icons } : {}), + ...(surface.flows ? { flows: surface.flows } : {}), }; } diff --git a/packages/interfacectl-cli/dist/commands/describe.d.ts b/packages/interfacectl-cli/dist/commands/describe.d.ts index f6e34a7..bbbdabf 100644 --- a/packages/interfacectl-cli/dist/commands/describe.d.ts +++ b/packages/interfacectl-cli/dist/commands/describe.d.ts @@ -8,7 +8,7 @@ export interface DescribeCommandOptions { } /** * Produce descriptor(s) with primitives for pre-emit guard (check-generation-boundaries). - * Output format: array of { surfaceId, primitives, sections, fonts, colors, layout, motion }. + * Output format: array of { surfaceId, primitives, sections, fonts, colors, flows, layout, motion }. */ export declare function runDescribeCommand(options: DescribeCommandOptions): Promise; //# sourceMappingURL=describe.d.ts.map \ No newline at end of file diff --git a/packages/interfacectl-cli/dist/commands/describe.d.ts.map b/packages/interfacectl-cli/dist/commands/describe.d.ts.map index e365bd8..5547a4c 100644 --- a/packages/interfacectl-cli/dist/commands/describe.d.ts.map +++ b/packages/interfacectl-cli/dist/commands/describe.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"describe.d.ts","sourceRoot":"","sources":["../../src/commands/describe.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAoED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAmFjB"} \ No newline at end of file +{"version":3,"file":"describe.d.ts","sourceRoot":"","sources":["../../src/commands/describe.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA4PD;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,MAAM,CAAC,CA0GjB"} \ No newline at end of file diff --git a/packages/interfacectl-cli/dist/commands/describe.js b/packages/interfacectl-cli/dist/commands/describe.js index 6bb0a05..784d400 100644 --- a/packages/interfacectl-cli/dist/commands/describe.js +++ b/packages/interfacectl-cli/dist/commands/describe.js @@ -49,9 +49,150 @@ async function writeFileWithParents(filePath, contents) { await mkdir(path.dirname(filePath), { recursive: true }); await writeFile(filePath, contents, "utf8"); } +async function loadFlowDescriptorArtifacts({ workspaceRoot, contract, surfaceFilters, flowDescriptorPathMap, }) { + const flowsBySurface = new Map(); + const paths = new Map(); + for (const surface of contract.surfaces) { + if (!surface.flows || surface.flows.policy === "off") { + continue; + } + if (surfaceFilters.size > 0 && !surfaceFilters.has(surface.id)) { + continue; + } + const configuredPath = flowDescriptorPathMap.get(surface.id) ?? + `contracts/generated/${surface.id}.flow-descriptor.json`; + const absolutePath = path.isAbsolute(configuredPath) + ? configuredPath + : path.resolve(workspaceRoot, configuredPath); + const relativePath = path.isAbsolute(configuredPath) + ? path.relative(workspaceRoot, configuredPath) + : configuredPath; + paths.set(surface.id, relativePath); + let raw; + try { + raw = await readFile(absolutePath, "utf8"); + } + catch (error) { + if (error.code === "ENOENT") { + continue; + } + return { + ok: false, + error: `Failed to read flow descriptor for surface "${surface.id}" at ${absolutePath}: ${error.message}`, + path: absolutePath, + surfaceId: surface.id, + }; + } + let parsed; + try { + parsed = JSON.parse(raw); + } + catch (error) { + return { + ok: false, + error: `Flow descriptor for surface "${surface.id}" is not valid JSON at ${absolutePath}: ${error.message}`, + path: absolutePath, + surfaceId: surface.id, + }; + } + if (!Array.isArray(parsed)) { + return { + ok: false, + error: `Flow descriptor for surface "${surface.id}" must be a JSON array at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const normalizedFlows = []; + for (const [index, entry] of parsed.entries()) { + if (!entry || typeof entry !== "object") { + return { + ok: false, + error: `Flow descriptor entry ${index} for surface "${surface.id}" must be an object at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const entryRecord = entry; + const flowIdValue = entryRecord.flowId; + const flowId = typeof flowIdValue === "string" ? flowIdValue.trim() : ""; + if (!flowId) { + return { + ok: false, + error: `Flow descriptor entry ${index} for surface "${surface.id}" is missing a non-empty flowId at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const stepsRaw = entryRecord.steps; + if (!Array.isArray(stepsRaw)) { + return { + ok: false, + error: `Flow descriptor "${flowId}" for surface "${surface.id}" must include steps[] at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const steps = []; + for (const [stepIndex, step] of stepsRaw.entries()) { + const stepRecord = step && typeof step === "object" + ? step + : undefined; + const stepIdValue = stepRecord?.id; + const stepId = typeof stepIdValue === "string" ? stepIdValue.trim() : ""; + if (!stepId) { + return { + ok: false, + error: `Flow descriptor "${flowId}" step ${stepIndex} for surface "${surface.id}" must include non-empty id at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + steps.push({ id: stepId }); + } + const transitionsRaw = entryRecord.transitions; + if (!Array.isArray(transitionsRaw)) { + return { + ok: false, + error: `Flow descriptor "${flowId}" for surface "${surface.id}" must include transitions[] at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const transitions = []; + for (const [transitionIndex, transition] of transitionsRaw.entries()) { + const transitionRecord = transition && typeof transition === "object" + ? transition + : undefined; + const fromValue = transitionRecord?.from; + const toValue = transitionRecord?.to; + const from = typeof fromValue === "string" ? fromValue.trim() : ""; + const to = typeof toValue === "string" ? toValue.trim() : ""; + if (!from || !to) { + return { + ok: false, + error: `Flow descriptor "${flowId}" transition ${transitionIndex} for surface "${surface.id}" must include non-empty from/to at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + transitions.push({ from, to }); + } + const sourceValue = entryRecord.source; + normalizedFlows.push({ + flowId, + steps, + transitions, + source: typeof sourceValue === "string" ? sourceValue : relativePath, + }); + } + flowsBySurface.set(surface.id, normalizedFlows); + } + return { ok: true, flowsBySurface, paths }; +} /** * Produce descriptor(s) with primitives for pre-emit guard (check-generation-boundaries). - * Output format: array of { surfaceId, primitives, sections, fonts, colors, layout, motion }. + * Output format: array of { surfaceId, primitives, sections, fonts, colors, flows, layout, motion }. */ export async function runDescribeCommand(options) { const cwd = process.cwd(); @@ -92,11 +233,17 @@ export async function runDescribeCommand(options) { const contract = structureResult.contract; const configResult = await loadConfigFile(configPath); const surfaceRootMap = new Map(); + const flowDescriptorPathMap = new Map(); if (configResult.ok && configResult.config?.surfaceRoots) { for (const [surfaceId, surfaceRoot] of Object.entries(configResult.config.surfaceRoots)) { surfaceRootMap.set(surfaceId, surfaceRoot); } } + if (configResult.ok && configResult.config?.flowDescriptorPaths) { + for (const [surfaceId, flowDescriptorPath] of Object.entries(configResult.config.flowDescriptorPaths)) { + flowDescriptorPathMap.set(surfaceId, flowDescriptorPath); + } + } const surfaceFilters = new Set((options.surface ?? []).map((s) => s.trim()).filter(Boolean)); const descriptorResult = await collectSurfaceDescriptors({ workspaceRoot, @@ -111,7 +258,21 @@ export async function runDescribeCommand(options) { } return 1; } - const descriptors = descriptorResult.descriptors; + const flowDescriptorResult = await loadFlowDescriptorArtifacts({ + workspaceRoot, + contract, + surfaceFilters, + flowDescriptorPathMap, + }); + if (!flowDescriptorResult.ok) { + console.error(`Error: ${flowDescriptorResult.error}`); + return 1; + } + const descriptors = descriptorResult.descriptors.map((descriptor) => ({ + ...descriptor, + flows: flowDescriptorResult.flowsBySurface.get(descriptor.surfaceId), + flowDescriptorPath: flowDescriptorResult.paths.get(descriptor.surfaceId), + })); const serialized = `${JSON.stringify(descriptors, null, 2)}\n`; await writeFileWithParents(outPath, serialized); return 0; diff --git a/packages/interfacectl-cli/dist/commands/validate.d.ts.map b/packages/interfacectl-cli/dist/commands/validate.d.ts.map index c4edbc2..2c1603a 100644 --- a/packages/interfacectl-cli/dist/commands/validate.d.ts.map +++ b/packages/interfacectl-cli/dist/commands/validate.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAeA,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAOlF,KAAK,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;AAmCpC,MAAM,WAAW,sBAAsB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AAED,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,MAAM,CAAC,CA8QjB"} \ No newline at end of file +{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAOlF,KAAK,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;AAoCpC,MAAM,WAAW,sBAAsB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AAED,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAyTjB"} \ No newline at end of file diff --git a/packages/interfacectl-cli/dist/commands/validate.js b/packages/interfacectl-cli/dist/commands/validate.js index a0600ae..a8847a7 100644 --- a/packages/interfacectl-cli/dist/commands/validate.js +++ b/packages/interfacectl-cli/dist/commands/validate.js @@ -34,6 +34,7 @@ export async function runValidateCommand(options) { }); const findings = []; let surfaceRootMap = new Map(); + let flowDescriptorPathMap = new Map(); // Determine exit code version const exitCodeVersion = getExitCodeVersion({ exitCodes: options.exitCodes }); const finalize = async (exitCode, contractVersion) => { @@ -77,6 +78,7 @@ export async function runValidateCommand(options) { const configResult = await loadConfigFile(configPath); if (configResult.ok) { surfaceRootMap = new Map(Object.entries(configResult.config.surfaceRoots ?? {}).map(([surfaceId, surfaceRoot]) => [surfaceId, surfaceRoot])); + flowDescriptorPathMap = new Map(Object.entries(configResult.config.flowDescriptorPaths ?? {}).map(([surfaceId, flowDescriptorPath]) => [surfaceId, flowDescriptorPath])); } else if (!(configResult.reason === "missing" && !configWasExplicit)) { const message = `Failed to load config: ${configResult.error}`; @@ -180,7 +182,38 @@ export async function runValidateCommand(options) { const e0ExitCode = exitCodeVersion === "v2" ? 10 : 2; return finalize(e0ExitCode, contract.version ?? initialContractVersion); } - const summary = evaluateContractCompliance(contract, structuralDescriptorResult.descriptors); + const flowDescriptorResult = await loadFlowDescriptorArtifacts({ + workspaceRoot, + contract, + surfaceFilters, + flowDescriptorPathMap, + }); + if (!flowDescriptorResult.ok) { + const message = `Failed to load flow descriptor artifact: ${flowDescriptorResult.error}`; + if (!isJson) { + printHeader(pc.red("✖ Flow descriptor artifact load failed"), textReporter); + textReporter.error(pc.red(flowDescriptorResult.error)); + } + findings.push({ + code: "flow-descriptor.load-error", + severity: "error", + category: "E0", + message, + surface: flowDescriptorResult.surfaceId, + location: flowDescriptorResult.path, + }); + const e0ExitCode = exitCodeVersion === "v2" ? 10 : 2; + return finalize(e0ExitCode, contract.version ?? initialContractVersion); + } + const descriptorsWithFlowArtifacts = structuralDescriptorResult.descriptors.map((descriptor) => { + const flowDescriptorPath = flowDescriptorResult.paths.get(descriptor.surfaceId); + return { + ...descriptor, + flows: flowDescriptorResult.flowsBySurface.get(descriptor.surfaceId), + flowDescriptorPath, + }; + }); + const summary = evaluateContractCompliance(contract, descriptorsWithFlowArtifacts); const violationFindings = mapViolationsToFindings(summary); findings.push(...violationFindings); if (!isJson) { @@ -293,6 +326,12 @@ function mapViolationsToFindings(summary) { "layout-pageframe-unextractable-value": "layout.pageframe.unextractable-value", "motion-duration-not-allowed": "motion.duration", "motion-timing-not-allowed": "motion.timing", + "descriptor-flows-missing": "descriptor.flows.missing", + "flow-required-missing": "flow.required.missing", + "flow-steps-min": "flow.steps.min", + "flow-steps-required": "flow.steps.required", + "flow-transition-required": "flow.transition.required", + "flow-terminal-invalid": "flow.terminal.invalid", "shell-owned-primitive-emitted": "shell.primitive.disallowed", }; for (const report of summary.surfaceReports) { @@ -347,6 +386,54 @@ function mapViolationsToFindings(summary) { } break; } + case "descriptor-flows-missing": { + finding.expected = details.requiredFlowIds; + finding.found = null; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "flow-required-missing": { + finding.expected = details.flowId; + finding.found = null; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "flow-steps-min": { + finding.expected = details.minSteps; + finding.found = details.actualStepCount; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "flow-steps-required": { + finding.expected = details.requiredSteps; + finding.found = details.missingRequiredSteps; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "flow-transition-required": { + finding.expected = details.requiredTransitions; + finding.found = details.missingRequiredTransitions; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "flow-terminal-invalid": { + finding.expected = details.terminalSteps; + finding.found = details.invalidTransitions; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } case "layout-width-undetermined": { finding.expected = details.expectedMaxWidth; finding.found = null; @@ -445,6 +532,147 @@ async function writeFileWithParents(filePath, contents) { await mkdir(path.dirname(filePath), { recursive: true }); await writeFile(filePath, contents, "utf8"); } +async function loadFlowDescriptorArtifacts({ workspaceRoot, contract, surfaceFilters, flowDescriptorPathMap, }) { + const flowsBySurface = new Map(); + const paths = new Map(); + for (const surface of contract.surfaces) { + if (surface.flows?.policy === "off" || !surface.flows) { + continue; + } + if (surfaceFilters.size > 0 && !surfaceFilters.has(surface.id)) { + continue; + } + const configuredPath = flowDescriptorPathMap.get(surface.id) ?? + `contracts/generated/${surface.id}.flow-descriptor.json`; + const absolutePath = path.isAbsolute(configuredPath) + ? configuredPath + : path.resolve(workspaceRoot, configuredPath); + const relativePath = path.isAbsolute(configuredPath) + ? path.relative(workspaceRoot, configuredPath) + : configuredPath; + paths.set(surface.id, relativePath); + let raw; + try { + raw = await readFile(absolutePath, "utf8"); + } + catch (error) { + if (error.code === "ENOENT") { + continue; + } + return { + ok: false, + error: `Failed to read flow descriptor for surface "${surface.id}" at ${absolutePath}: ${error.message}`, + path: absolutePath, + surfaceId: surface.id, + }; + } + let parsed; + try { + parsed = JSON.parse(raw); + } + catch (error) { + return { + ok: false, + error: `Flow descriptor for surface "${surface.id}" is not valid JSON at ${absolutePath}: ${error.message}`, + path: absolutePath, + surfaceId: surface.id, + }; + } + if (!Array.isArray(parsed)) { + return { + ok: false, + error: `Flow descriptor for surface "${surface.id}" must be a JSON array at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const normalizedFlows = []; + for (const [index, entry] of parsed.entries()) { + if (!entry || typeof entry !== "object") { + return { + ok: false, + error: `Flow descriptor entry ${index} for surface "${surface.id}" must be an object at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const entryRecord = entry; + const flowIdValue = entryRecord.flowId; + const flowId = typeof flowIdValue === "string" ? flowIdValue.trim() : ""; + if (!flowId) { + return { + ok: false, + error: `Flow descriptor entry ${index} for surface "${surface.id}" is missing a non-empty flowId at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const stepsRaw = entryRecord.steps; + if (!Array.isArray(stepsRaw)) { + return { + ok: false, + error: `Flow descriptor "${flowId}" for surface "${surface.id}" must include steps[] at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const steps = []; + for (const [stepIndex, step] of stepsRaw.entries()) { + const stepRecord = step && typeof step === "object" + ? step + : undefined; + const stepIdValue = stepRecord?.id; + const stepId = typeof stepIdValue === "string" ? stepIdValue.trim() : ""; + if (!stepId) { + return { + ok: false, + error: `Flow descriptor "${flowId}" step ${stepIndex} for surface "${surface.id}" must include non-empty id at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + steps.push({ id: stepId }); + } + const transitionsRaw = entryRecord.transitions; + if (!Array.isArray(transitionsRaw)) { + return { + ok: false, + error: `Flow descriptor "${flowId}" for surface "${surface.id}" must include transitions[] at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const transitions = []; + for (const [transitionIndex, transition] of transitionsRaw.entries()) { + const transitionRecord = transition && typeof transition === "object" + ? transition + : undefined; + const fromValue = transitionRecord?.from; + const toValue = transitionRecord?.to; + const from = typeof fromValue === "string" ? fromValue.trim() : ""; + const to = typeof toValue === "string" ? toValue.trim() : ""; + if (!from || !to) { + return { + ok: false, + error: `Flow descriptor "${flowId}" transition ${transitionIndex} for surface "${surface.id}" must include non-empty from/to at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + transitions.push({ from, to }); + } + const sourceValue = entryRecord.source; + normalizedFlows.push({ + flowId, + steps, + transitions, + source: typeof sourceValue === "string" ? sourceValue : relativePath, + }); + } + flowsBySurface.set(surface.id, normalizedFlows); + } + return { ok: true, flowsBySurface, paths }; +} async function loadConfigFile(configPath) { try { const raw = await readFile(configPath, "utf-8"); diff --git a/packages/interfacectl-cli/src/commands/compile.ts b/packages/interfacectl-cli/src/commands/compile.ts index 1102020..321e463 100644 --- a/packages/interfacectl-cli/src/commands/compile.ts +++ b/packages/interfacectl-cli/src/commands/compile.ts @@ -212,5 +212,6 @@ function surfaceToBundlePayload(surface: ContractSurface): Record; + flowDescriptorPaths?: Record; } export interface DescribeCommandOptions { @@ -86,9 +88,193 @@ async function writeFileWithParents( await writeFile(filePath, contents, "utf8"); } +type FlowDescriptorArtifactLoadResult = + | { + ok: true; + flowsBySurface: Map; + paths: Map; + } + | { + ok: false; + error: string; + path: string; + surfaceId: string; + }; + +async function loadFlowDescriptorArtifacts({ + workspaceRoot, + contract, + surfaceFilters, + flowDescriptorPathMap, +}: { + workspaceRoot: string; + contract: InterfaceContract; + surfaceFilters: Set; + flowDescriptorPathMap: Map; +}): Promise { + const flowsBySurface = new Map(); + const paths = new Map(); + + for (const surface of contract.surfaces) { + if (!surface.flows || surface.flows.policy === "off") { + continue; + } + if (surfaceFilters.size > 0 && !surfaceFilters.has(surface.id)) { + continue; + } + + const configuredPath = + flowDescriptorPathMap.get(surface.id) ?? + `contracts/generated/${surface.id}.flow-descriptor.json`; + const absolutePath = path.isAbsolute(configuredPath) + ? configuredPath + : path.resolve(workspaceRoot, configuredPath); + const relativePath = path.isAbsolute(configuredPath) + ? path.relative(workspaceRoot, configuredPath) + : configuredPath; + + paths.set(surface.id, relativePath); + + let raw: string; + try { + raw = await readFile(absolutePath, "utf8"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + continue; + } + return { + ok: false, + error: `Failed to read flow descriptor for surface "${surface.id}" at ${absolutePath}: ${ + (error as Error).message + }`, + path: absolutePath, + surfaceId: surface.id, + }; + } + + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (error) { + return { + ok: false, + error: `Flow descriptor for surface "${surface.id}" is not valid JSON at ${absolutePath}: ${ + (error as Error).message + }`, + path: absolutePath, + surfaceId: surface.id, + }; + } + + if (!Array.isArray(parsed)) { + return { + ok: false, + error: `Flow descriptor for surface "${surface.id}" must be a JSON array at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + + const normalizedFlows: SurfaceFlowDescriptor[] = []; + for (const [index, entry] of parsed.entries()) { + if (!entry || typeof entry !== "object") { + return { + ok: false, + error: `Flow descriptor entry ${index} for surface "${surface.id}" must be an object at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const entryRecord = entry as Record; + const flowIdValue = entryRecord.flowId; + + const flowId = typeof flowIdValue === "string" ? flowIdValue.trim() : ""; + if (!flowId) { + return { + ok: false, + error: `Flow descriptor entry ${index} for surface "${surface.id}" is missing a non-empty flowId at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + + const stepsRaw = entryRecord.steps; + if (!Array.isArray(stepsRaw)) { + return { + ok: false, + error: `Flow descriptor "${flowId}" for surface "${surface.id}" must include steps[] at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const steps: { id: string }[] = []; + for (const [stepIndex, step] of stepsRaw.entries()) { + const stepRecord = + step && typeof step === "object" + ? (step as Record) + : undefined; + const stepIdValue = stepRecord?.id; + const stepId = + typeof stepIdValue === "string" ? stepIdValue.trim() : ""; + if (!stepId) { + return { + ok: false, + error: `Flow descriptor "${flowId}" step ${stepIndex} for surface "${surface.id}" must include non-empty id at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + steps.push({ id: stepId }); + } + + const transitionsRaw = entryRecord.transitions; + if (!Array.isArray(transitionsRaw)) { + return { + ok: false, + error: `Flow descriptor "${flowId}" for surface "${surface.id}" must include transitions[] at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const transitions: { from: string; to: string }[] = []; + for (const [transitionIndex, transition] of transitionsRaw.entries()) { + const transitionRecord = + transition && typeof transition === "object" + ? (transition as Record) + : undefined; + const fromValue = transitionRecord?.from; + const toValue = transitionRecord?.to; + const from = typeof fromValue === "string" ? fromValue.trim() : ""; + const to = typeof toValue === "string" ? toValue.trim() : ""; + if (!from || !to) { + return { + ok: false, + error: `Flow descriptor "${flowId}" transition ${transitionIndex} for surface "${surface.id}" must include non-empty from/to at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + transitions.push({ from, to }); + } + const sourceValue = entryRecord.source; + + normalizedFlows.push({ + flowId, + steps, + transitions, + source: typeof sourceValue === "string" ? sourceValue : relativePath, + }); + } + + flowsBySurface.set(surface.id, normalizedFlows); + } + + return { ok: true, flowsBySurface, paths }; +} + /** * Produce descriptor(s) with primitives for pre-emit guard (check-generation-boundaries). - * Output format: array of { surfaceId, primitives, sections, fonts, colors, layout, motion }. + * Output format: array of { surfaceId, primitives, sections, fonts, colors, flows, layout, motion }. */ export async function runDescribeCommand( options: DescribeCommandOptions, @@ -143,6 +329,7 @@ export async function runDescribeCommand( const configResult = await loadConfigFile(configPath); const surfaceRootMap = new Map(); + const flowDescriptorPathMap = new Map(); if (configResult.ok && configResult.config?.surfaceRoots) { for (const [surfaceId, surfaceRoot] of Object.entries( configResult.config.surfaceRoots, @@ -150,6 +337,13 @@ export async function runDescribeCommand( surfaceRootMap.set(surfaceId, surfaceRoot); } } + if (configResult.ok && configResult.config?.flowDescriptorPaths) { + for (const [surfaceId, flowDescriptorPath] of Object.entries( + configResult.config.flowDescriptorPaths, + )) { + flowDescriptorPathMap.set(surfaceId, flowDescriptorPath); + } + } const surfaceFilters = new Set( (options.surface ?? []).map((s) => s.trim()).filter(Boolean), @@ -170,7 +364,22 @@ export async function runDescribeCommand( return 1; } - const descriptors = descriptorResult.descriptors; + const flowDescriptorResult = await loadFlowDescriptorArtifacts({ + workspaceRoot, + contract, + surfaceFilters, + flowDescriptorPathMap, + }); + if (!flowDescriptorResult.ok) { + console.error(`Error: ${flowDescriptorResult.error}`); + return 1; + } + + const descriptors = descriptorResult.descriptors.map((descriptor) => ({ + ...descriptor, + flows: flowDescriptorResult.flowsBySurface.get(descriptor.surfaceId), + flowDescriptorPath: flowDescriptorResult.paths.get(descriptor.surfaceId), + })); const serialized = `${JSON.stringify(descriptors, null, 2)}\n`; await writeFileWithParents(outPath, serialized); diff --git a/packages/interfacectl-cli/src/commands/validate.ts b/packages/interfacectl-cli/src/commands/validate.ts index 2b49f16..09289f4 100644 --- a/packages/interfacectl-cli/src/commands/validate.ts +++ b/packages/interfacectl-cli/src/commands/validate.ts @@ -8,6 +8,7 @@ import { type InterfaceContract, type ValidationSummary, type DriftViolationType, + type SurfaceFlowDescriptor, } from "@surfaces/interfacectl-validator"; import { collectSurfaceDescriptors, @@ -53,6 +54,7 @@ interface TextReporter { interface InterfacectlConfig { surfaceRoots?: Record; + flowDescriptorPaths?: Record; } export interface ValidateCommandOptions { @@ -103,6 +105,7 @@ export async function runValidateCommand( const findings: JsonFinding[] = []; let surfaceRootMap = new Map(); + let flowDescriptorPathMap = new Map(); // Determine exit code version const exitCodeVersion = getExitCodeVersion({ exitCodes: options.exitCodes }); @@ -163,6 +166,11 @@ export async function runValidateCommand( ([surfaceId, surfaceRoot]) => [surfaceId, surfaceRoot], ), ); + flowDescriptorPathMap = new Map( + Object.entries(configResult.config.flowDescriptorPaths ?? {}).map( + ([surfaceId, flowDescriptorPath]) => [surfaceId, flowDescriptorPath], + ), + ); } else if ( !(configResult.reason === "missing" && !configWasExplicit) ) { @@ -290,9 +298,46 @@ export async function runValidateCommand( return finalize(e0ExitCode, contract.version ?? initialContractVersion); } + const flowDescriptorResult = await loadFlowDescriptorArtifacts({ + workspaceRoot, + contract, + surfaceFilters, + flowDescriptorPathMap, + }); + if (!flowDescriptorResult.ok) { + const message = `Failed to load flow descriptor artifact: ${flowDescriptorResult.error}`; + if (!isJson) { + printHeader(pc.red("✖ Flow descriptor artifact load failed"), textReporter); + textReporter.error(pc.red(flowDescriptorResult.error)); + } + findings.push({ + code: "flow-descriptor.load-error", + severity: "error", + category: "E0", + message, + surface: flowDescriptorResult.surfaceId, + location: flowDescriptorResult.path, + }); + const e0ExitCode = exitCodeVersion === "v2" ? 10 : 2; + return finalize(e0ExitCode, contract.version ?? initialContractVersion); + } + + const descriptorsWithFlowArtifacts = structuralDescriptorResult.descriptors.map( + (descriptor) => { + const flowDescriptorPath = flowDescriptorResult.paths.get( + descriptor.surfaceId, + ); + return { + ...descriptor, + flows: flowDescriptorResult.flowsBySurface.get(descriptor.surfaceId), + flowDescriptorPath, + }; + }, + ); + const summary = evaluateContractCompliance( contract, - structuralDescriptorResult.descriptors, + descriptorsWithFlowArtifacts, ); const violationFindings = mapViolationsToFindings(summary); findings.push(...violationFindings); @@ -433,6 +478,12 @@ function mapViolationsToFindings( "layout-pageframe-unextractable-value": "layout.pageframe.unextractable-value", "motion-duration-not-allowed": "motion.duration", "motion-timing-not-allowed": "motion.timing", + "descriptor-flows-missing": "descriptor.flows.missing", + "flow-required-missing": "flow.required.missing", + "flow-steps-min": "flow.steps.min", + "flow-steps-required": "flow.steps.required", + "flow-transition-required": "flow.transition.required", + "flow-terminal-invalid": "flow.terminal.invalid", "shell-owned-primitive-emitted": "shell.primitive.disallowed", }; @@ -491,6 +542,54 @@ function mapViolationsToFindings( } break; } + case "descriptor-flows-missing": { + finding.expected = details.requiredFlowIds; + finding.found = null; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "flow-required-missing": { + finding.expected = details.flowId; + finding.found = null; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "flow-steps-min": { + finding.expected = details.minSteps; + finding.found = details.actualStepCount; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "flow-steps-required": { + finding.expected = details.requiredSteps; + finding.found = details.missingRequiredSteps; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "flow-transition-required": { + finding.expected = details.requiredTransitions; + finding.found = details.missingRequiredTransitions; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "flow-terminal-invalid": { + finding.expected = details.terminalSteps; + finding.found = details.invalidTransitions; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } case "layout-width-undetermined": { finding.expected = details.expectedMaxWidth; finding.found = null; @@ -596,6 +695,190 @@ async function writeFileWithParents( await writeFile(filePath, contents, "utf8"); } +type FlowDescriptorArtifactLoadResult = + | { + ok: true; + flowsBySurface: Map; + paths: Map; + } + | { + ok: false; + error: string; + path: string; + surfaceId: string; + }; + +async function loadFlowDescriptorArtifacts({ + workspaceRoot, + contract, + surfaceFilters, + flowDescriptorPathMap, +}: { + workspaceRoot: string; + contract: InterfaceContract; + surfaceFilters: Set; + flowDescriptorPathMap: Map; +}): Promise { + const flowsBySurface = new Map(); + const paths = new Map(); + + for (const surface of contract.surfaces) { + if (surface.flows?.policy === "off" || !surface.flows) { + continue; + } + if (surfaceFilters.size > 0 && !surfaceFilters.has(surface.id)) { + continue; + } + + const configuredPath = + flowDescriptorPathMap.get(surface.id) ?? + `contracts/generated/${surface.id}.flow-descriptor.json`; + const absolutePath = path.isAbsolute(configuredPath) + ? configuredPath + : path.resolve(workspaceRoot, configuredPath); + const relativePath = path.isAbsolute(configuredPath) + ? path.relative(workspaceRoot, configuredPath) + : configuredPath; + + paths.set(surface.id, relativePath); + + let raw: string; + try { + raw = await readFile(absolutePath, "utf8"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + continue; + } + return { + ok: false, + error: `Failed to read flow descriptor for surface "${surface.id}" at ${absolutePath}: ${ + (error as Error).message + }`, + path: absolutePath, + surfaceId: surface.id, + }; + } + + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (error) { + return { + ok: false, + error: `Flow descriptor for surface "${surface.id}" is not valid JSON at ${absolutePath}: ${ + (error as Error).message + }`, + path: absolutePath, + surfaceId: surface.id, + }; + } + + if (!Array.isArray(parsed)) { + return { + ok: false, + error: `Flow descriptor for surface "${surface.id}" must be a JSON array at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + + const normalizedFlows: SurfaceFlowDescriptor[] = []; + for (const [index, entry] of parsed.entries()) { + if (!entry || typeof entry !== "object") { + return { + ok: false, + error: `Flow descriptor entry ${index} for surface "${surface.id}" must be an object at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const entryRecord = entry as Record; + const flowIdValue = entryRecord.flowId; + + const flowId = typeof flowIdValue === "string" ? flowIdValue.trim() : ""; + if (!flowId) { + return { + ok: false, + error: `Flow descriptor entry ${index} for surface "${surface.id}" is missing a non-empty flowId at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + + const stepsRaw = entryRecord.steps; + if (!Array.isArray(stepsRaw)) { + return { + ok: false, + error: `Flow descriptor "${flowId}" for surface "${surface.id}" must include steps[] at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const steps: { id: string }[] = []; + for (const [stepIndex, step] of stepsRaw.entries()) { + const stepRecord = + step && typeof step === "object" + ? (step as Record) + : undefined; + const stepIdValue = stepRecord?.id; + const stepId = + typeof stepIdValue === "string" ? stepIdValue.trim() : ""; + if (!stepId) { + return { + ok: false, + error: `Flow descriptor "${flowId}" step ${stepIndex} for surface "${surface.id}" must include non-empty id at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + steps.push({ id: stepId }); + } + + const transitionsRaw = entryRecord.transitions; + if (!Array.isArray(transitionsRaw)) { + return { + ok: false, + error: `Flow descriptor "${flowId}" for surface "${surface.id}" must include transitions[] at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + const transitions: { from: string; to: string }[] = []; + for (const [transitionIndex, transition] of transitionsRaw.entries()) { + const transitionRecord = + transition && typeof transition === "object" + ? (transition as Record) + : undefined; + const fromValue = transitionRecord?.from; + const toValue = transitionRecord?.to; + const from = typeof fromValue === "string" ? fromValue.trim() : ""; + const to = typeof toValue === "string" ? toValue.trim() : ""; + if (!from || !to) { + return { + ok: false, + error: `Flow descriptor "${flowId}" transition ${transitionIndex} for surface "${surface.id}" must include non-empty from/to at ${absolutePath}.`, + path: absolutePath, + surfaceId: surface.id, + }; + } + transitions.push({ from, to }); + } + const sourceValue = entryRecord.source; + + normalizedFlows.push({ + flowId, + steps, + transitions, + source: typeof sourceValue === "string" ? sourceValue : relativePath, + }); + } + + flowsBySurface.set(surface.id, normalizedFlows); + } + + return { ok: true, flowsBySurface, paths }; +} + type ConfigLoadResult = | { ok: true; config: InterfacectlConfig } | { ok: false; reason: "missing" | "invalid"; error: string }; diff --git a/packages/interfacectl-cli/test/compile.test.mjs b/packages/interfacectl-cli/test/compile.test.mjs index a8a63b2..0967995 100644 --- a/packages/interfacectl-cli/test/compile.test.mjs +++ b/packages/interfacectl-cli/test/compile.test.mjs @@ -207,6 +207,82 @@ test("compile: includes surface icons policy when present in contract", async () } }); +test("compile: includes surface flow policy when present in contract", async () => { + const outDir = await mkdtemp(path.join(os.tmpdir(), "interfacectl-compile-flows-")); + const contractWithFlowsPath = path.join(outDir, "contract-with-flows.json"); + try { + const { writeFile } = await import("node:fs/promises"); + await writeFile( + contractWithFlowsPath, + JSON.stringify( + { + contractId: "demo-ui-flows", + version: "1.0.0", + surfaces: [ + { + id: "demo-surface", + displayName: "Demo Surface", + type: "web", + requiredSections: ["main.hero"], + allowedFonts: ["Inter"], + layout: { maxContentWidth: 960 }, + flows: { + policy: "warn", + requirements: [ + { + flowId: "checkout", + minSteps: 2, + requiredSteps: ["start", "review"], + requiredTransitions: [{ from: "start", to: "review" }], + terminalSteps: ["review"], + }, + ], + }, + }, + ], + sections: [ + { id: "main.hero", intent: "Hero", description: "Main hero" }, + ], + constraints: { + motion: { + allowedDurationsMs: [120], + allowedTimingFunctions: ["linear"], + }, + }, + color: { + policy: "off", + allowedValues: [], + }, + }, + null, + 2, + ), + "utf8", + ); + + const result = await runCompile(contractWithFlowsPath, outDir); + assert.equal(result.exitCode, 0, `compile should exit 0: ${result.stderr}`); + + const generatedSurface = await readJson( + path.join(outDir, "surfaces", "demo-surface.json"), + ); + assert.deepEqual(generatedSurface.flows, { + policy: "warn", + requirements: [ + { + flowId: "checkout", + minSteps: 2, + requiredSteps: ["start", "review"], + requiredTransitions: [{ from: "start", to: "review" }], + terminalSteps: ["review"], + }, + ], + }); + } finally { + await rm(outDir, { recursive: true, force: true }); + } +}); + test("compile: invalid contract fails with non-zero exit", async () => { const outDir = await mkdtemp(path.join(os.tmpdir(), "interfacectl-compile-fail-")); const invalidContract = path.join(outDir, "invalid.json"); diff --git a/packages/interfacectl-cli/test/flow-policy.test.mjs b/packages/interfacectl-cli/test/flow-policy.test.mjs new file mode 100644 index 0000000..2d5a19b --- /dev/null +++ b/packages/interfacectl-cli/test/flow-policy.test.mjs @@ -0,0 +1,266 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import os from "node:os"; +import path from "node:path"; +import { spawn } from "node:child_process"; +import { once } from "node:events"; +import { + mkdtemp, + mkdir, + rm, + writeFile, +} from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const cliExecutable = path.resolve(__dirname, "..", "dist", "index.js"); + +async function runValidate(workspaceRoot, contractPath) { + const child = spawn( + "node", + [ + cliExecutable, + "validate", + "--contract", + contractPath, + "--workspace-root", + workspaceRoot, + "--format", + "json", + "--exit-codes", + "v2", + ], + { cwd: workspaceRoot }, + ); + + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on("data", (chunk) => { + stderr += chunk.toString(); + }); + const [exitCode] = await once(child, "exit"); + return { + exitCode: Number(exitCode), + stdout, + stderr, + }; +} + +async function createWorkspace({ flowPolicy = "strict", includeFlowDescriptor = true, malformedFlowDescriptor = false } = {}) { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "interfacectl-flow-policy-")); + const surfaceId = "test-surface"; + const appDir = path.join(tempRoot, "apps", surfaceId, "app"); + const generatedDir = path.join(tempRoot, "contracts", "generated"); + const contractPath = path.join(tempRoot, "contract.json"); + + await mkdir(appDir, { recursive: true }); + await mkdir(generatedDir, { recursive: true }); + + await writeFile( + path.join(tempRoot, "interfacectl.config.json"), + JSON.stringify( + { + surfaceRoots: { + [surfaceId]: `apps/${surfaceId}`, + }, + }, + null, + 2, + ), + "utf-8", + ); + + await writeFile( + path.join(appDir, "layout.tsx"), + ` + import "./globals.css"; + export default function Layout({ children }) { + return {children}; + } + `, + "utf-8", + ); + + await writeFile( + path.join(appDir, "page.tsx"), + ` + export default function Page() { + return ( +
+

Flow policy test

+
+ ); + } + `, + "utf-8", + ); + + await writeFile( + path.join(appDir, "globals.css"), + ` + :root { + --contract-max-width: 960px; + --contract-motion-duration: 120ms; + --contract-motion-timing: linear; + } + body { + font-family: sans-serif; + } + .contract-container { + transition: opacity var(--contract-motion-duration) var(--contract-motion-timing); + } + `, + "utf-8", + ); + + const contract = { + contractId: "flow-policy-test", + version: "1.0.0", + surfaces: [ + { + id: surfaceId, + displayName: "Flow Surface", + type: "web", + requiredSections: ["main.hero"], + allowedFonts: ["sans-serif"], + layout: { maxContentWidth: 960 }, + flows: { + policy: flowPolicy, + requirements: [ + { + flowId: "checkout", + minSteps: 2, + requiredSteps: ["start", "review"], + requiredTransitions: [{ from: "start", to: "review" }], + terminalSteps: ["review"], + }, + ], + }, + }, + ], + sections: [{ id: "main.hero", intent: "Hero", description: "Main hero section" }], + constraints: { + motion: { + allowedDurationsMs: [120], + allowedTimingFunctions: ["linear"], + }, + }, + color: { + policy: "off", + allowedValues: [], + }, + }; + + await writeFile(contractPath, JSON.stringify(contract, null, 2), "utf-8"); + + if (includeFlowDescriptor) { + const flowDescriptorPath = path.join(generatedDir, `${surfaceId}.flow-descriptor.json`); + if (malformedFlowDescriptor) { + await writeFile(flowDescriptorPath, "{\n invalid-json\n", "utf-8"); + } else { + await writeFile( + flowDescriptorPath, + JSON.stringify( + [ + { + flowId: "checkout", + steps: [{ id: "start" }, { id: "review" }], + transitions: [{ from: "start", to: "review" }], + source: "contracts/generated/test-surface.flow-descriptor.json", + }, + ], + null, + 2, + ), + "utf-8", + ); + } + } + + return { tempRoot, contractPath }; +} + +test("validate: strict flow policy passes when compliant flow descriptor artifact exists", async () => { + const { tempRoot, contractPath } = await createWorkspace({ + flowPolicy: "strict", + includeFlowDescriptor: true, + }); + try { + const result = await runValidate(tempRoot, contractPath); + assert.equal(result.exitCode, 0, result.stderr); + const payload = JSON.parse(result.stdout); + assert.equal(payload.summary.errors, 0); + assert.equal( + payload.findings.some((entry) => entry.code.startsWith("flow.")), + false, + `unexpected flow findings: ${result.stdout}`, + ); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); + +test("validate: warn flow policy emits descriptor.flows.missing warning when artifact is missing", async () => { + const { tempRoot, contractPath } = await createWorkspace({ + flowPolicy: "warn", + includeFlowDescriptor: false, + }); + try { + const result = await runValidate(tempRoot, contractPath); + assert.equal(result.exitCode, 0, result.stderr); + const payload = JSON.parse(result.stdout); + const finding = payload.findings.find( + (entry) => entry.code === "descriptor.flows.missing", + ); + assert.ok(finding, `missing descriptor.flows.missing finding: ${result.stdout}`); + assert.equal(finding.severity, "warning"); + assert.equal(finding.category, "E2"); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); + +test("validate: strict flow policy blocks when flow descriptor artifact is missing", async () => { + const { tempRoot, contractPath } = await createWorkspace({ + flowPolicy: "strict", + includeFlowDescriptor: false, + }); + try { + const result = await runValidate(tempRoot, contractPath); + assert.equal(result.exitCode, 30, result.stderr); + const payload = JSON.parse(result.stdout); + const finding = payload.findings.find( + (entry) => entry.code === "descriptor.flows.missing", + ); + assert.ok(finding, `missing descriptor.flows.missing finding: ${result.stdout}`); + assert.equal(finding.severity, "error"); + assert.equal(finding.category, "E2"); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); + +test("validate: malformed flow descriptor artifact returns E0", async () => { + const { tempRoot, contractPath } = await createWorkspace({ + flowPolicy: "strict", + includeFlowDescriptor: true, + malformedFlowDescriptor: true, + }); + try { + const result = await runValidate(tempRoot, contractPath); + assert.equal(result.exitCode, 10, result.stderr); + const payload = JSON.parse(result.stdout); + const finding = payload.findings.find( + (entry) => entry.code === "flow-descriptor.load-error", + ); + assert.ok(finding, `missing flow-descriptor.load-error finding: ${result.stdout}`); + assert.equal(finding.severity, "error"); + assert.equal(finding.category, "E0"); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); diff --git a/packages/interfacectl-cli/test/generation-boundaries-guard.test.mjs b/packages/interfacectl-cli/test/generation-boundaries-guard.test.mjs index 5ecf07f..7a6934f 100644 --- a/packages/interfacectl-cli/test/generation-boundaries-guard.test.mjs +++ b/packages/interfacectl-cli/test/generation-boundaries-guard.test.mjs @@ -73,6 +73,46 @@ function makeCanonicalContract({ iconPolicy = "strict" } = {}) { }; } +function makeFlowContract({ policy = "strict" } = {}) { + return { + contractId: "surfaces.web", + version: "0.1.0", + surfaces: [ + { + id: "reference-target-web", + displayName: "Reference", + type: "web", + requiredSections: [], + allowedFonts: ["Inter"], + layout: { maxContentWidth: 1120 }, + flows: { + policy, + requirements: [ + { + flowId: "checkout", + minSteps: 2, + requiredSteps: ["start", "review"], + requiredTransitions: [{ from: "start", to: "review" }], + terminalSteps: ["review"], + }, + ], + }, + }, + ], + sections: [], + constraints: { + motion: { + allowedDurationsMs: [120], + allowedTimingFunctions: ["linear"], + }, + }, + color: { + policy: "off", + allowedValues: [], + }, + }; +} + test("json: strict canonical color policy blocks disallowed color", () => { const result = runGuard({ contract: makeCanonicalContract(), @@ -151,6 +191,72 @@ test("json: strict icon policy emits descriptor.icons.missing when icons are abs assert.ok(payload.findings.some((finding) => finding.code === "descriptor.icons.missing")); }); +test("json: strict flow policy emits descriptor.flows.missing when flows are absent", () => { + const result = runGuard({ + contract: makeFlowContract({ policy: "strict" }), + descriptor: [ + { + surfaceId: "reference-target-web", + primitives: [], + colors: [], + }, + ], + }); + + assert.equal(result.status, 2, result.stderr || result.stdout); + const payload = parseJsonOutput(result); + assert.equal(payload.status, "block"); + assert.equal(payload.evaluation.flowPolicyEvaluated, true); + assert.ok(payload.findings.some((finding) => finding.code === "descriptor.flows.missing")); +}); + +test("json: warn flow policy emits warnings and stays non-blocking", () => { + const result = runGuard({ + contract: makeFlowContract({ policy: "warn" }), + descriptor: [ + { + surfaceId: "reference-target-web", + primitives: [], + colors: [], + }, + ], + }); + + assert.equal(result.status, 0, result.stderr || result.stdout); + const payload = parseJsonOutput(result); + assert.equal(payload.status, "warn"); + const finding = payload.findings.find((entry) => entry.code === "descriptor.flows.missing"); + assert.ok(finding, "expected descriptor.flows.missing finding"); + assert.equal(finding.severity, "warning"); + assert.equal(finding.policy, "warn"); +}); + +test("json: strict flow policy enforces steps, transitions, and terminal invariants", () => { + const result = runGuard({ + contract: makeFlowContract({ policy: "strict" }), + descriptor: [ + { + surfaceId: "reference-target-web", + primitives: [], + colors: [], + flows: [ + { + flowId: "checkout", + steps: [{ id: "start" }, { id: "review" }, { id: "done" }], + transitions: [{ from: "review", to: "done" }], + }, + ], + }, + ], + }); + + assert.equal(result.status, 2, result.stderr || result.stdout); + const payload = parseJsonOutput(result); + assert.equal(payload.status, "block"); + assert.ok(payload.findings.some((finding) => finding.code === "flow.transition.required")); + assert.ok(payload.findings.some((finding) => finding.code === "flow.terminal.invalid")); +}); + test("text: canonical color/icon checks remain backward-compatible in text mode", () => { const result = runGuard({ contract: makeCanonicalContract({ iconPolicy: "strict" }), diff --git a/packages/interfacectl-cli/test/violation-classifier.test.mjs b/packages/interfacectl-cli/test/violation-classifier.test.mjs index 71eba0f..658267b 100644 --- a/packages/interfacectl-cli/test/violation-classifier.test.mjs +++ b/packages/interfacectl-cli/test/violation-classifier.test.mjs @@ -24,6 +24,12 @@ test("classifyViolationType: E2 violations", () => { assert.equal(classifyViolationType("unknown-surface"), "E2"); assert.equal(classifyViolationType("descriptor-missing"), "E2"); assert.equal(classifyViolationType("descriptor-unused"), "E2"); + assert.equal(classifyViolationType("descriptor-flows-missing"), "E2"); + assert.equal(classifyViolationType("flow-required-missing"), "E2"); + assert.equal(classifyViolationType("flow-steps-min"), "E2"); + assert.equal(classifyViolationType("flow-steps-required"), "E2"); + assert.equal(classifyViolationType("flow-transition-required"), "E2"); + assert.equal(classifyViolationType("flow-terminal-invalid"), "E2"); }); test("getExitCodeForCategory: v2 exit codes", () => { diff --git a/packages/interfacectl-validator/dist/index.d.ts b/packages/interfacectl-validator/dist/index.d.ts index ff6305f..e649f0c 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, SurfacePrimitiveDescriptor, SurfaceReport, DriftViolation, ValidationSummary, DriftViolationType, ContractRef, RuleRef, DiffOutput, DiffEntry, DiffChangeType, DriftRisk, Severity, SafetyLevel, EnforcementPolicy, EnforcementMode, IconPolicy, AutofixRule, FixSummary, FixEntry, FixError, } from "./types.js"; +export type { InterfaceContract, ContractSurface, ContractSection, ContractConstraints, SurfaceDescriptor, SurfaceSectionDescriptor, SurfaceFontDescriptor, SurfaceColorDescriptor, SurfaceIconDescriptor, SurfaceMotionDescriptor, SurfaceLayoutDescriptor, PageFrameLayoutDescriptor, SurfacePrimitiveDescriptor, SurfaceReport, DriftViolation, ValidationSummary, DriftViolationType, ContractRef, RuleRef, DiffOutput, DiffEntry, DiffChangeType, DriftRisk, Severity, SafetyLevel, EnforcementPolicy, EnforcementMode, IconPolicy, FlowPolicy, FlowRequirement, FlowTransitionRequirement, 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 0731bf6..a6fd63b 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;AA4ED,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,EAAE,iBAAiB,GAC5B,aAAa,CA4af;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,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,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,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;AAuOD,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,EAAE,iBAAiB,GAC5B,aAAa,CA6af;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,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,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/index.js b/packages/interfacectl-validator/dist/index.js index 358fbe2..9fb3b5d 100644 --- a/packages/interfacectl-validator/dist/index.js +++ b/packages/interfacectl-validator/dist/index.js @@ -83,6 +83,127 @@ function validateIconPolicy(surface, descriptor, violations) { }); } } +function validateFlowPolicy(surface, descriptor, violations) { + const flowPolicy = surface.flows; + if (!flowPolicy || flowPolicy.policy === "off") { + return; + } + const policy = flowPolicy.policy; + const requirements = flowPolicy.requirements ?? []; + const descriptorFlows = descriptor.flows; + const defaultSource = descriptor.flowDescriptorPath; + if (!Array.isArray(descriptorFlows)) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "descriptor-flows-missing", + message: `Flow descriptor is missing for surface "${descriptor.surfaceId}" while flow policy is "${policy}".`, + details: { + policy, + source: defaultSource, + flowDescriptorPath: defaultSource, + requiredFlowIds: requirements.map((requirement) => requirement.flowId), + }, + }); + return; + } + const flowMap = new Map(descriptorFlows + .map((flow) => [flow.flowId?.trim(), flow]) + .filter(([flowId]) => typeof flowId === "string" && flowId.length > 0)); + for (const requirement of requirements) { + const requirementSource = defaultSource; + const flowId = requirement.flowId; + const descriptorFlow = flowMap.get(flowId); + if (!descriptorFlow) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "flow-required-missing", + message: `Required flow "${flowId}" is missing for surface "${descriptor.surfaceId}".`, + details: { + flowId, + policy, + source: requirementSource, + flowDescriptorPath: defaultSource, + }, + }); + continue; + } + const flowSource = descriptorFlow.source ?? defaultSource; + const stepIds = new Set((descriptorFlow.steps ?? []) + .map((step) => step.id?.trim()) + .filter((stepId) => Boolean(stepId))); + if (typeof requirement.minSteps === "number" && + stepIds.size < requirement.minSteps) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "flow-steps-min", + message: `Flow "${flowId}" has ${stepIds.size} step(s); minimum is ${requirement.minSteps}.`, + details: { + flowId, + minSteps: requirement.minSteps, + actualStepCount: stepIds.size, + stepIds: [...stepIds], + policy, + source: flowSource, + }, + }); + } + const requiredSteps = requirement.requiredSteps ?? []; + const missingRequiredSteps = requiredSteps.filter((stepId) => !stepIds.has(stepId)); + if (missingRequiredSteps.length > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "flow-steps-required", + message: `Flow "${flowId}" is missing required step(s): ${missingRequiredSteps.join(", ")}.`, + details: { + flowId, + requiredSteps, + missingRequiredSteps, + policy, + source: flowSource, + }, + }); + } + const transitionList = (descriptorFlow.transitions ?? []) + .map((transition) => ({ + from: transition.from?.trim(), + to: transition.to?.trim(), + })) + .filter((transition) => Boolean(transition.from && transition.to)); + const transitionKeys = new Set(transitionList.map((transition) => `${transition.from}->${transition.to}`)); + const requiredTransitions = requirement.requiredTransitions ?? []; + const missingRequiredTransitions = requiredTransitions.filter((transition) => !transitionKeys.has(`${transition.from}->${transition.to}`)); + if (missingRequiredTransitions.length > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "flow-transition-required", + message: `Flow "${flowId}" is missing required transition(s).`, + details: { + flowId, + requiredTransitions, + missingRequiredTransitions, + policy, + source: flowSource, + }, + }); + } + const terminalSteps = requirement.terminalSteps ?? []; + const invalidTerminalTransitions = transitionList.filter((transition) => terminalSteps.includes(transition.from)); + if (invalidTerminalTransitions.length > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "flow-terminal-invalid", + message: `Flow "${flowId}" has outgoing transition(s) from terminal step(s).`, + details: { + flowId, + terminalSteps, + invalidTransitions: invalidTerminalTransitions, + policy, + source: flowSource, + }, + }); + } + } +} export function evaluateSurfaceCompliance(contract, descriptor) { const surface = findSurface(contract.surfaces, descriptor.surfaceId); const violations = []; @@ -204,6 +325,7 @@ export function evaluateSurfaceCompliance(contract, descriptor) { } validateColorPolicy(contract, descriptor, violations); validateIconPolicy(surface, descriptor, violations); + validateFlowPolicy(surface, descriptor, violations); const reportedWidth = descriptor.layout.maxContentWidth; if (reportedWidth === null || reportedWidth === undefined) { violations.push({ 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 88a2d63..ed53266 100644 --- a/packages/interfacectl-validator/dist/schema/web.surface.contract.schema.json +++ b/packages/interfacectl-validator/dist/schema/web.surface.contract.schema.json @@ -161,6 +161,9 @@ "icons": { "$ref": "#/$defs/iconPolicy" }, + "flows": { + "$ref": "#/$defs/flowPolicy" + }, "phase0": { "type": "object", "description": "Phase 0.5: optional expectations for extracted reality (routes, auth, shell, design system).", @@ -342,6 +345,89 @@ } } }, + "flowPolicy": { + "type": "object", + "additionalProperties": false, + "required": [ + "policy", + "requirements" + ], + "properties": { + "policy": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + }, + "requirements": { + "type": "array", + "items": { + "$ref": "#/$defs/flowRequirement" + }, + "uniqueItems": true + } + } + }, + "flowRequirement": { + "type": "object", + "additionalProperties": false, + "required": [ + "flowId" + ], + "properties": { + "flowId": { + "type": "string", + "minLength": 1 + }, + "minSteps": { + "type": "integer", + "minimum": 1 + }, + "requiredSteps": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "requiredTransitions": { + "type": "array", + "items": { + "$ref": "#/$defs/flowTransition" + }, + "uniqueItems": true + }, + "terminalSteps": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + } + } + }, + "flowTransition": { + "type": "object", + "additionalProperties": false, + "required": [ + "from", + "to" + ], + "properties": { + "from": { + "type": "string", + "minLength": 1 + }, + "to": { + "type": "string", + "minLength": 1 + } + } + }, "color": { "type": "object", "additionalProperties": false, diff --git a/packages/interfacectl-validator/dist/types.d.ts b/packages/interfacectl-validator/dist/types.d.ts index d2bc248..f2222f9 100644 --- a/packages/interfacectl-validator/dist/types.d.ts +++ b/packages/interfacectl-validator/dist/types.d.ts @@ -7,6 +7,21 @@ export interface PageFrameLayout { alignment?: "center" | "left"; enforcement?: "strict" | "warn"; } +export interface FlowTransitionRequirement { + from: string; + to: string; +} +export interface FlowRequirement { + flowId: string; + minSteps?: number; + requiredSteps?: string[]; + requiredTransitions?: FlowTransitionRequirement[]; + terminalSteps?: string[]; +} +export interface FlowPolicy { + policy: "off" | "warn" | "strict"; + requirements: FlowRequirement[]; +} export interface ContractSurface { id: string; displayName: string; @@ -19,6 +34,7 @@ export interface ContractSurface { pageFrame?: PageFrameLayout; }; icons?: IconPolicy; + flows?: FlowPolicy; mustNotEmit?: string[]; shellOwnedPrimitiveAllowSources?: string[]; } @@ -89,6 +105,19 @@ export interface SurfacePrimitiveDescriptor { count: number; sources?: string[]; } +export interface SurfaceFlowStepDescriptor { + id: string; +} +export interface SurfaceFlowTransitionDescriptor { + from: string; + to: string; +} +export interface SurfaceFlowDescriptor { + flowId: string; + steps: SurfaceFlowStepDescriptor[]; + transitions: SurfaceFlowTransitionDescriptor[]; + source?: string; +} export interface PageFrameLayoutDescriptor { containerSelector: string; maxWidthPx?: number | null; @@ -113,11 +142,13 @@ export interface SurfaceDescriptor { fonts: SurfaceFontDescriptor[]; colors: SurfaceColorDescriptor[]; icons?: SurfaceIconDescriptor[]; + flows?: SurfaceFlowDescriptor[]; + flowDescriptorPath?: string; layout: SurfaceLayoutDescriptor; motion: SurfaceMotionDescriptor[]; primitives?: SurfacePrimitiveDescriptor[]; } -export type DriftViolationType = "unknown-surface" | "missing-section" | "unknown-section" | "font-not-allowed" | "color-not-allowed" | "icon-source-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-minwidth-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" | "shell-owned-primitive-emitted"; +export type DriftViolationType = "unknown-surface" | "missing-section" | "unknown-section" | "font-not-allowed" | "color-not-allowed" | "icon-source-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-minwidth-mismatch" | "layout-pageframe-padding-mismatch" | "layout-pageframe-non-deterministic-value" | "layout-pageframe-unextractable-value" | "motion-duration-not-allowed" | "motion-timing-not-allowed" | "descriptor-flows-missing" | "flow-required-missing" | "flow-steps-min" | "flow-steps-required" | "flow-transition-required" | "flow-terminal-invalid" | "descriptor-missing" | "descriptor-unused" | "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 d65f9db..d365347 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 bb38e11..9d10dc8 100644 --- a/packages/interfacectl-validator/src/index.ts +++ b/packages/interfacectl-validator/src/index.ts @@ -131,6 +131,161 @@ function validateIconPolicy( } } +function validateFlowPolicy( + surface: ContractSurface, + descriptor: SurfaceDescriptor, + violations: DriftViolation[], +): void { + const flowPolicy = surface.flows; + if (!flowPolicy || flowPolicy.policy === "off") { + return; + } + + const policy = flowPolicy.policy; + const requirements = flowPolicy.requirements ?? []; + const descriptorFlows = descriptor.flows; + const defaultSource = descriptor.flowDescriptorPath; + + if (!Array.isArray(descriptorFlows)) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "descriptor-flows-missing", + message: `Flow descriptor is missing for surface "${descriptor.surfaceId}" while flow policy is "${policy}".`, + details: { + policy, + source: defaultSource, + flowDescriptorPath: defaultSource, + requiredFlowIds: requirements.map((requirement) => requirement.flowId), + }, + }); + return; + } + + const flowMap = new Map( + descriptorFlows + .map((flow) => [flow.flowId?.trim(), flow] as const) + .filter(([flowId]) => typeof flowId === "string" && flowId.length > 0), + ); + + for (const requirement of requirements) { + const requirementSource = defaultSource; + const flowId = requirement.flowId; + const descriptorFlow = flowMap.get(flowId); + + if (!descriptorFlow) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "flow-required-missing", + message: `Required flow "${flowId}" is missing for surface "${descriptor.surfaceId}".`, + details: { + flowId, + policy, + source: requirementSource, + flowDescriptorPath: defaultSource, + }, + }); + continue; + } + + const flowSource = descriptorFlow.source ?? defaultSource; + const stepIds = new Set( + (descriptorFlow.steps ?? []) + .map((step) => step.id?.trim()) + .filter((stepId): stepId is string => Boolean(stepId)), + ); + + if ( + typeof requirement.minSteps === "number" && + stepIds.size < requirement.minSteps + ) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "flow-steps-min", + message: `Flow "${flowId}" has ${stepIds.size} step(s); minimum is ${requirement.minSteps}.`, + details: { + flowId, + minSteps: requirement.minSteps, + actualStepCount: stepIds.size, + stepIds: [...stepIds], + policy, + source: flowSource, + }, + }); + } + + const requiredSteps = requirement.requiredSteps ?? []; + const missingRequiredSteps = requiredSteps.filter( + (stepId) => !stepIds.has(stepId), + ); + if (missingRequiredSteps.length > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "flow-steps-required", + message: `Flow "${flowId}" is missing required step(s): ${missingRequiredSteps.join(", ")}.`, + details: { + flowId, + requiredSteps, + missingRequiredSteps, + policy, + source: flowSource, + }, + }); + } + + const transitionList = (descriptorFlow.transitions ?? []) + .map((transition) => ({ + from: transition.from?.trim(), + to: transition.to?.trim(), + })) + .filter( + (transition): transition is { from: string; to: string } => + Boolean(transition.from && transition.to), + ); + const transitionKeys = new Set( + transitionList.map((transition) => `${transition.from}->${transition.to}`), + ); + + const requiredTransitions = requirement.requiredTransitions ?? []; + const missingRequiredTransitions = requiredTransitions.filter( + (transition) => + !transitionKeys.has(`${transition.from}->${transition.to}`), + ); + if (missingRequiredTransitions.length > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "flow-transition-required", + message: `Flow "${flowId}" is missing required transition(s).`, + details: { + flowId, + requiredTransitions, + missingRequiredTransitions, + policy, + source: flowSource, + }, + }); + } + + const terminalSteps = requirement.terminalSteps ?? []; + const invalidTerminalTransitions = transitionList.filter((transition) => + terminalSteps.includes(transition.from), + ); + if (invalidTerminalTransitions.length > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "flow-terminal-invalid", + message: `Flow "${flowId}" has outgoing transition(s) from terminal step(s).`, + details: { + flowId, + terminalSteps, + invalidTransitions: invalidTerminalTransitions, + policy, + source: flowSource, + }, + }); + } + } +} + export function evaluateSurfaceCompliance( contract: InterfaceContract, descriptor: SurfaceDescriptor, @@ -267,6 +422,7 @@ export function evaluateSurfaceCompliance( validateColorPolicy(contract, descriptor, violations); validateIconPolicy(surface, descriptor, violations); + validateFlowPolicy(surface, descriptor, violations); const reportedWidth = descriptor.layout.maxContentWidth; if (reportedWidth === null || reportedWidth === undefined) { @@ -682,6 +838,12 @@ export type { EnforcementPolicy, EnforcementMode, IconPolicy, + FlowPolicy, + FlowRequirement, + FlowTransitionRequirement, + SurfaceFlowDescriptor, + SurfaceFlowStepDescriptor, + SurfaceFlowTransitionDescriptor, AutofixRule, FixSummary, FixEntry, 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 88a2d63..ed53266 100644 --- a/packages/interfacectl-validator/src/schema/web.surface.contract.schema.json +++ b/packages/interfacectl-validator/src/schema/web.surface.contract.schema.json @@ -161,6 +161,9 @@ "icons": { "$ref": "#/$defs/iconPolicy" }, + "flows": { + "$ref": "#/$defs/flowPolicy" + }, "phase0": { "type": "object", "description": "Phase 0.5: optional expectations for extracted reality (routes, auth, shell, design system).", @@ -342,6 +345,89 @@ } } }, + "flowPolicy": { + "type": "object", + "additionalProperties": false, + "required": [ + "policy", + "requirements" + ], + "properties": { + "policy": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + }, + "requirements": { + "type": "array", + "items": { + "$ref": "#/$defs/flowRequirement" + }, + "uniqueItems": true + } + } + }, + "flowRequirement": { + "type": "object", + "additionalProperties": false, + "required": [ + "flowId" + ], + "properties": { + "flowId": { + "type": "string", + "minLength": 1 + }, + "minSteps": { + "type": "integer", + "minimum": 1 + }, + "requiredSteps": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "requiredTransitions": { + "type": "array", + "items": { + "$ref": "#/$defs/flowTransition" + }, + "uniqueItems": true + }, + "terminalSteps": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + } + } + }, + "flowTransition": { + "type": "object", + "additionalProperties": false, + "required": [ + "from", + "to" + ], + "properties": { + "from": { + "type": "string", + "minLength": 1 + }, + "to": { + "type": "string", + "minLength": 1 + } + } + }, "color": { "type": "object", "additionalProperties": false, diff --git a/packages/interfacectl-validator/src/types.ts b/packages/interfacectl-validator/src/types.ts index 2b47f8c..e459526 100644 --- a/packages/interfacectl-validator/src/types.ts +++ b/packages/interfacectl-validator/src/types.ts @@ -9,6 +9,24 @@ export interface PageFrameLayout { enforcement?: "strict" | "warn"; } +export interface FlowTransitionRequirement { + from: string; + to: string; +} + +export interface FlowRequirement { + flowId: string; + minSteps?: number; + requiredSteps?: string[]; + requiredTransitions?: FlowTransitionRequirement[]; + terminalSteps?: string[]; +} + +export interface FlowPolicy { + policy: "off" | "warn" | "strict"; + requirements: FlowRequirement[]; +} + export interface ContractSurface { id: string; displayName: string; @@ -21,6 +39,7 @@ export interface ContractSurface { pageFrame?: PageFrameLayout; }; icons?: IconPolicy; + flows?: FlowPolicy; mustNotEmit?: string[]; shellOwnedPrimitiveAllowSources?: string[]; } @@ -104,6 +123,22 @@ export interface SurfacePrimitiveDescriptor { sources?: string[]; } +export interface SurfaceFlowStepDescriptor { + id: string; +} + +export interface SurfaceFlowTransitionDescriptor { + from: string; + to: string; +} + +export interface SurfaceFlowDescriptor { + flowId: string; + steps: SurfaceFlowStepDescriptor[]; + transitions: SurfaceFlowTransitionDescriptor[]; + source?: string; +} + export interface PageFrameLayoutDescriptor { containerSelector: string; maxWidthPx?: number | null; @@ -130,6 +165,8 @@ export interface SurfaceDescriptor { fonts: SurfaceFontDescriptor[]; colors: SurfaceColorDescriptor[]; icons?: SurfaceIconDescriptor[]; + flows?: SurfaceFlowDescriptor[]; + flowDescriptorPath?: string; layout: SurfaceLayoutDescriptor; motion: SurfaceMotionDescriptor[]; primitives?: SurfacePrimitiveDescriptor[]; @@ -154,6 +191,12 @@ export type DriftViolationType = | "layout-pageframe-unextractable-value" | "motion-duration-not-allowed" | "motion-timing-not-allowed" + | "descriptor-flows-missing" + | "flow-required-missing" + | "flow-steps-min" + | "flow-steps-required" + | "flow-transition-required" + | "flow-terminal-invalid" | "descriptor-missing" | "descriptor-unused" | "shell-owned-primitive-emitted"; diff --git a/packages/interfacectl-validator/test/contract-structure.test.mjs b/packages/interfacectl-validator/test/contract-structure.test.mjs index bd1f983..dc86506 100644 --- a/packages/interfacectl-validator/test/contract-structure.test.mjs +++ b/packages/interfacectl-validator/test/contract-structure.test.mjs @@ -109,3 +109,44 @@ test("validateContractStructure accepts optional pageFrame.containerMinWidthPx", const result = validateContractStructure(contract, schema); assert.equal(result.ok, true); }); + +test("validateContractStructure accepts optional surface.flows policy", () => { + const schema = getBundledContractSchema(); + const contract = buildContract({ + flows: { + policy: "warn", + requirements: [ + { + flowId: "checkout", + minSteps: 2, + requiredSteps: ["start", "review"], + requiredTransitions: [{ from: "start", to: "review" }], + terminalSteps: ["review"], + }, + ], + }, + }); + const result = validateContractStructure(contract, schema); + assert.equal(result.ok, true); +}); + +test("validateContractStructure rejects malformed surface.flows requirements", () => { + const schema = getBundledContractSchema(); + const contract = buildContract({ + flows: { + policy: "strict", + requirements: [ + { + flowId: "checkout", + minSteps: 0, + }, + ], + }, + }); + const result = validateContractStructure(contract, schema); + assert.equal(result.ok, false); + assert.ok( + result.errors.some((error) => error.includes("minSteps")), + `expected minSteps validation error, got ${JSON.stringify(result.errors)}`, + ); +}); diff --git a/packages/interfacectl-validator/test/evaluate-contract.test.mjs b/packages/interfacectl-validator/test/evaluate-contract.test.mjs index 0e2a2f6..81ffe18 100644 --- a/packages/interfacectl-validator/test/evaluate-contract.test.mjs +++ b/packages/interfacectl-validator/test/evaluate-contract.test.mjs @@ -541,3 +541,186 @@ test("reports selector unsupported even when pageFrame descriptor is unavailable ); assert.ok(violation); }); + +function makeFlowContract(policy = "strict", requirementOverrides = {}) { + return { + ...baseContract, + surfaces: [ + { + id: "surface-flow", + displayName: "Surface Flow", + type: "web", + requiredSections: ["main.hero"], + allowedFonts: ["var(--font-flow)"], + layout: { + maxContentWidth: 1200, + requiredContainers: [], + }, + flows: { + policy, + requirements: [ + { + flowId: "checkout", + minSteps: 2, + requiredSteps: ["start", "review"], + requiredTransitions: [{ from: "start", to: "review" }], + terminalSteps: ["review"], + ...requirementOverrides, + }, + ], + }, + }, + ], + }; +} + +function makeFlowDescriptor(overrides = {}) { + return { + surfaceId: "surface-flow", + sections: [{ id: "main.hero" }], + fonts: [{ value: "var(--font-flow)" }], + colors: [{ value: "var(--color-primary)" }], + layout: { + maxContentWidth: 1000, + containers: [], + }, + motion: [], + ...overrides, + }; +} + +test("flow policy emits descriptor-flows-missing when descriptor artifact is absent", () => { + const contract = makeFlowContract("warn"); + const descriptor = makeFlowDescriptor({ + flowDescriptorPath: "contracts/generated/surface-flow.flow-descriptor.json", + }); + + const report = evaluateSurfaceCompliance(contract, descriptor); + const violation = report.violations.find( + (item) => item.type === "descriptor-flows-missing", + ); + assert.ok(violation); + assert.equal(violation.details?.policy, "warn"); + assert.equal( + violation.details?.flowDescriptorPath, + "contracts/generated/surface-flow.flow-descriptor.json", + ); +}); + +test("flow policy emits flow-required-missing when required flow is missing", () => { + const contract = makeFlowContract("strict"); + const descriptor = makeFlowDescriptor({ + flows: [], + }); + + const report = evaluateSurfaceCompliance(contract, descriptor); + const violation = report.violations.find( + (item) => item.type === "flow-required-missing", + ); + assert.ok(violation); + assert.equal(violation.details?.flowId, "checkout"); + assert.equal(violation.details?.policy, "strict"); +}); + +test("flow policy emits flow-steps-min when step count is below minimum", () => { + const contract = makeFlowContract("strict", { + requiredSteps: [], + requiredTransitions: [], + terminalSteps: [], + }); + const descriptor = makeFlowDescriptor({ + flows: [ + { + flowId: "checkout", + steps: [{ id: "start" }], + transitions: [], + }, + ], + }); + + const report = evaluateSurfaceCompliance(contract, descriptor); + const violation = report.violations.find( + (item) => item.type === "flow-steps-min", + ); + assert.ok(violation); + assert.equal(violation.details?.flowId, "checkout"); + assert.equal(violation.details?.actualStepCount, 1); + assert.equal(violation.details?.minSteps, 2); +}); + +test("flow policy emits flow-steps-required when required step is missing", () => { + const contract = makeFlowContract("strict", { + minSteps: 1, + requiredTransitions: [], + terminalSteps: [], + }); + const descriptor = makeFlowDescriptor({ + flows: [ + { + flowId: "checkout", + steps: [{ id: "start" }], + transitions: [], + }, + ], + }); + + const report = evaluateSurfaceCompliance(contract, descriptor); + const violation = report.violations.find( + (item) => item.type === "flow-steps-required", + ); + assert.ok(violation); + assert.deepEqual(violation.details?.missingRequiredSteps, ["review"]); +}); + +test("flow policy emits flow-transition-required when required transition is missing", () => { + const contract = makeFlowContract("strict", { + minSteps: 1, + requiredSteps: ["start", "review"], + terminalSteps: [], + }); + const descriptor = makeFlowDescriptor({ + flows: [ + { + flowId: "checkout", + steps: [{ id: "start" }, { id: "review" }], + transitions: [], + }, + ], + }); + + const report = evaluateSurfaceCompliance(contract, descriptor); + const violation = report.violations.find( + (item) => item.type === "flow-transition-required", + ); + assert.ok(violation); + assert.deepEqual(violation.details?.missingRequiredTransitions, [ + { from: "start", to: "review" }, + ]); +}); + +test("flow policy emits flow-terminal-invalid when terminal step has outgoing transition", () => { + const contract = makeFlowContract("strict", { + minSteps: 1, + requiredSteps: [], + requiredTransitions: [], + terminalSteps: ["review"], + }); + const descriptor = makeFlowDescriptor({ + flows: [ + { + flowId: "checkout", + steps: [{ id: "review" }, { id: "done" }], + transitions: [{ from: "review", to: "done" }], + }, + ], + }); + + const report = evaluateSurfaceCompliance(contract, descriptor); + const violation = report.violations.find( + (item) => item.type === "flow-terminal-invalid", + ); + assert.ok(violation); + assert.deepEqual(violation.details?.invalidTransitions, [ + { from: "review", to: "done" }, + ]); +}); diff --git a/tools/check-generation-boundaries.mjs b/tools/check-generation-boundaries.mjs index 3f4f263..0c7266d 100755 --- a/tools/check-generation-boundaries.mjs +++ b/tools/check-generation-boundaries.mjs @@ -410,6 +410,214 @@ function evaluateIconPolicy(contract, descriptors) { return { findings, evaluated }; } +function evaluateFlowPolicy(contract, descriptors) { + const findings = []; + let evaluated = false; + const surfaces = Array.isArray(contract?.surfaces) ? contract.surfaces : []; + + for (const desc of descriptors) { + const surfaceId = desc?.surfaceId; + if (!surfaceId) continue; + + const surface = surfaces.find((entry) => entry.id === surfaceId); + const flowPolicy = surface?.flows; + if (!flowPolicy || flowPolicy.policy === "off") continue; + if (flowPolicy.policy !== "warn" && flowPolicy.policy !== "strict") continue; + evaluated = true; + + const requirements = Array.isArray(flowPolicy.requirements) + ? flowPolicy.requirements + : []; + const flowDescriptorPath = + typeof desc?.flowDescriptorPath === "string" ? desc.flowDescriptorPath : ""; + if (!Array.isArray(desc?.flows)) { + findings.push( + makeFinding({ + code: "descriptor.flows.missing", + severity: flowPolicy.policy === "strict" ? "error" : "warning", + policy: flowPolicy.policy, + message: `Descriptor for surface "${surfaceId}" is missing flows[] while flow policy is "${flowPolicy.policy}".`, + source: flowDescriptorPath, + evidence: { + source: "generationGuard", + surfaceId, + expected: "flows[]", + flowDescriptorPath, + }, + }), + ); + continue; + } + + const descriptorFlows = new Map( + desc.flows + .map((flow) => [ + typeof flow?.flowId === "string" ? flow.flowId.trim() : "", + flow, + ]) + .filter(([flowId]) => flowId.length > 0), + ); + + for (const requirement of requirements) { + const flowId = + typeof requirement?.flowId === "string" ? requirement.flowId.trim() : ""; + if (!flowId) continue; + + const flow = descriptorFlows.get(flowId); + if (!flow) { + findings.push( + makeFinding({ + code: "flow.required.missing", + severity: flowPolicy.policy === "strict" ? "error" : "warning", + policy: flowPolicy.policy, + message: `Flow "${flowId}" is required but missing for surface "${surfaceId}".`, + source: flowDescriptorPath, + evidence: { + source: "generationGuard", + surfaceId, + flowId, + flowDescriptorPath, + }, + }), + ); + continue; + } + + const flowSource = + typeof flow?.source === "string" && flow.source.trim().length > 0 + ? flow.source + : flowDescriptorPath; + const stepIds = new Set( + (Array.isArray(flow?.steps) ? flow.steps : []) + .map((step) => + typeof step?.id === "string" ? step.id.trim() : "", + ) + .filter(Boolean), + ); + const transitions = (Array.isArray(flow?.transitions) ? flow.transitions : []) + .map((transition) => ({ + from: typeof transition?.from === "string" ? transition.from.trim() : "", + to: typeof transition?.to === "string" ? transition.to.trim() : "", + })) + .filter((transition) => transition.from && transition.to); + const transitionKeys = new Set( + transitions.map((transition) => `${transition.from}->${transition.to}`), + ); + + const minSteps = + typeof requirement?.minSteps === "number" + ? Math.trunc(requirement.minSteps) + : undefined; + if (typeof minSteps === "number" && stepIds.size < minSteps) { + findings.push( + makeFinding({ + code: "flow.steps.min", + severity: flowPolicy.policy === "strict" ? "error" : "warning", + policy: flowPolicy.policy, + message: `Flow "${flowId}" has ${stepIds.size} step(s); minimum is ${minSteps}.`, + source: flowSource, + evidence: { + source: "generationGuard", + surfaceId, + flowId, + minSteps, + actualStepCount: stepIds.size, + stepIds: [...stepIds], + }, + }), + ); + } + + const requiredSteps = Array.isArray(requirement?.requiredSteps) + ? requirement.requiredSteps + .map((step) => String(step).trim()) + .filter(Boolean) + : []; + const missingRequiredSteps = requiredSteps.filter( + (step) => !stepIds.has(step), + ); + if (missingRequiredSteps.length > 0) { + findings.push( + makeFinding({ + code: "flow.steps.required", + severity: flowPolicy.policy === "strict" ? "error" : "warning", + policy: flowPolicy.policy, + message: `Flow "${flowId}" is missing required step(s): ${missingRequiredSteps.join(", ")}.`, + source: flowSource, + evidence: { + source: "generationGuard", + surfaceId, + flowId, + requiredSteps, + missingRequiredSteps, + }, + }), + ); + } + + const requiredTransitions = Array.isArray(requirement?.requiredTransitions) + ? requirement.requiredTransitions + .map((transition) => ({ + from: String(transition?.from ?? "").trim(), + to: String(transition?.to ?? "").trim(), + })) + .filter((transition) => transition.from && transition.to) + : []; + const missingTransitions = requiredTransitions.filter( + (transition) => + !transitionKeys.has(`${transition.from}->${transition.to}`), + ); + if (missingTransitions.length > 0) { + findings.push( + makeFinding({ + code: "flow.transition.required", + severity: flowPolicy.policy === "strict" ? "error" : "warning", + policy: flowPolicy.policy, + message: `Flow "${flowId}" is missing required transition(s).`, + source: flowSource, + evidence: { + source: "generationGuard", + surfaceId, + flowId, + requiredTransitions, + missingRequiredTransitions: missingTransitions, + }, + }), + ); + } + + const terminalSteps = Array.isArray(requirement?.terminalSteps) + ? requirement.terminalSteps + .map((step) => String(step).trim()) + .filter(Boolean) + : []; + const invalidTransitions = transitions.filter((transition) => + terminalSteps.includes(transition.from), + ); + if (invalidTransitions.length > 0) { + findings.push( + makeFinding({ + code: "flow.terminal.invalid", + severity: flowPolicy.policy === "strict" ? "error" : "warning", + policy: flowPolicy.policy, + message: `Flow "${flowId}" has outgoing transition(s) from terminal step(s).`, + source: flowSource, + evidence: { + source: "generationGuard", + surfaceId, + flowId, + terminalSteps, + invalidTransitions, + }, + }), + ); + } + } + } + + return { findings, evaluated }; +} + function summarizeFindings(findings) { const blocking = findings.filter((finding) => finding.severity === "error").length; const warnings = findings.filter((finding) => finding.severity === "warning").length; @@ -484,6 +692,7 @@ function failJson(msg) { shellBoundaryEvaluated: false, colorPolicyEvaluated: false, iconPolicyEvaluated: false, + flowPolicyEvaluated: false, }, error: { code: "input.invalid", @@ -516,6 +725,7 @@ function main() { ? { findings: [], evaluated: false } : evaluateColorPolicyLegacy(contract, descriptors); const iconPolicy = evaluateIconPolicy(contract, descriptors); + const flowPolicy = evaluateFlowPolicy(contract, descriptors); if (format === "text") { const legacyFindings = [...shellFindings, ...colorLegacy.findings]; @@ -530,6 +740,7 @@ function main() { ...colorCanonical.findings, ...colorLegacy.findings, ...iconPolicy.findings, + ...flowPolicy.findings, ]; const summary = summarizeFindings(findings); outputJson({ @@ -544,6 +755,7 @@ function main() { shellBoundaryEvaluated: true, colorPolicyEvaluated: colorCanonical.evaluated || colorLegacy.evaluated, iconPolicyEvaluated: iconPolicy.evaluated, + flowPolicyEvaluated: flowPolicy.evaluated, }, }); process.exit(summary.blocking > 0 ? 2 : 0); From 8bf78906e23ef9d2c0829dc8cae6466854a973cd Mon Sep 17 00:00:00 2001 From: Mike Long Date: Sun, 8 Mar 2026 13:56:14 -0700 Subject: [PATCH 2/3] Add optional chrome policy contract support --- .../dist/commands/generate-contract.d.ts.map | 2 +- .../dist/commands/generate-contract.js | 13 +++- .../dist/utils/chrome-policy-seeding.d.ts | 12 +++ .../dist/utils/chrome-policy-seeding.d.ts.map | 1 + .../dist/utils/chrome-policy-seeding.js | 33 +++++++++ .../src/commands/generate-contract.ts | 13 +++- .../src/utils/chrome-policy-seeding.ts | 45 +++++++++++ .../test/chrome-policy-seeding.test.mjs | 74 +++++++++++++++++++ .../schema/web.surface.contract.schema.json | 47 ++++++++++++ .../interfacectl-validator/dist/types.d.ts | 9 +++ .../dist/types.d.ts.map | 2 +- .../schema/web.surface.contract.schema.json | 47 ++++++++++++ packages/interfacectl-validator/src/types.ts | 14 ++++ .../test/contract-structure.test.mjs | 40 ++++++++++ 14 files changed, 346 insertions(+), 6 deletions(-) create mode 100644 packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts create mode 100644 packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts.map create mode 100644 packages/interfacectl-cli/dist/utils/chrome-policy-seeding.js create mode 100644 packages/interfacectl-cli/src/utils/chrome-policy-seeding.ts create mode 100644 packages/interfacectl-cli/test/chrome-policy-seeding.test.mjs 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 a6485c0..139b44c 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":"AAcA,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,CAiFjB"} \ 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,CAyFjB"} \ 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 7b123eb..4e4d83f 100644 --- a/packages/interfacectl-cli/dist/commands/generate-contract.js +++ b/packages/interfacectl-cli/dist/commands/generate-contract.js @@ -3,6 +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 { seedIconPolicyFromObservedDescriptors } from "../utils/icon-policy-seeding.js"; const DEFAULT_OUT_DIR = "contracts/generated"; export async function runGenerateContractCommand(options) { @@ -34,10 +35,18 @@ export async function runGenerateContractCommand(options) { surfaceId, contract: seeded.contract, }); - const contract = iconSeeded.contract; + const chromeSeeded = await seedChromePolicyDefaults({ + contract: iconSeeded.contract, + }); + const contract = chromeSeeded.contract; const reportWithSeedWarnings = { ...report, - warnings: [...report.warnings, ...seeded.warnings, ...iconSeeded.warnings], + warnings: [ + ...report.warnings, + ...seeded.warnings, + ...iconSeeded.warnings, + ...chromeSeeded.warnings, + ], }; let schema; if (options.schemaPath) { diff --git a/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts b/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts new file mode 100644 index 0000000..062bf70 --- /dev/null +++ b/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts @@ -0,0 +1,12 @@ +import type { InterfaceContract } from "@surfaces/interfacectl-validator"; +export interface ChromePolicySeedResult { + contract: InterfaceContract; + warnings: Array<{ + code: string; + message: string; + }>; +} +export declare function seedChromePolicyDefaults({ contract, }: { + contract: InterfaceContract; +}): 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 new file mode 100644 index 0000000..c833569 --- /dev/null +++ b/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.d.ts.map @@ -0,0 +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 diff --git a/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.js b/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.js new file mode 100644 index 0000000..762ee09 --- /dev/null +++ b/packages/interfacectl-cli/dist/utils/chrome-policy-seeding.js @@ -0,0 +1,33 @@ +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, }) { + 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], + }, + }, + }; + }), + }, + warnings: [], + }; +} diff --git a/packages/interfacectl-cli/src/commands/generate-contract.ts b/packages/interfacectl-cli/src/commands/generate-contract.ts index e4a99b7..096f300 100644 --- a/packages/interfacectl-cli/src/commands/generate-contract.ts +++ b/packages/interfacectl-cli/src/commands/generate-contract.ts @@ -10,6 +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 { seedIconPolicyFromObservedDescriptors } from "../utils/icon-policy-seeding.js"; export interface GenerateContractOptions { @@ -58,10 +59,18 @@ export async function runGenerateContractCommand( surfaceId, contract: seeded.contract, }); - const contract = iconSeeded.contract; + const chromeSeeded = await seedChromePolicyDefaults({ + contract: iconSeeded.contract, + }); + const contract = chromeSeeded.contract; const reportWithSeedWarnings = { ...report, - warnings: [...report.warnings, ...seeded.warnings, ...iconSeeded.warnings], + warnings: [ + ...report.warnings, + ...seeded.warnings, + ...iconSeeded.warnings, + ...chromeSeeded.warnings, + ], }; let schema: object; diff --git a/packages/interfacectl-cli/src/utils/chrome-policy-seeding.ts b/packages/interfacectl-cli/src/utils/chrome-policy-seeding.ts new file mode 100644 index 0000000..3c5e833 --- /dev/null +++ b/packages/interfacectl-cli/src/utils/chrome-policy-seeding.ts @@ -0,0 +1,45 @@ +import type { InterfaceContract } from "@surfaces/interfacectl-validator"; + +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 { + 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], + }, + }, + }; + }), + }, + warnings: [], + }; +} diff --git a/packages/interfacectl-cli/test/chrome-policy-seeding.test.mjs b/packages/interfacectl-cli/test/chrome-policy-seeding.test.mjs new file mode 100644 index 0000000..db8ffa4 --- /dev/null +++ b/packages/interfacectl-cli/test/chrome-policy-seeding.test.mjs @@ -0,0 +1,74 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { seedChromePolicyDefaults } from "../dist/utils/chrome-policy-seeding.js"; + +function baseContract() { + return { + contractId: "seed-test", + version: "1.0.0", + surfaces: [ + { + id: "demo-web", + displayName: "Demo Web", + type: "web", + requiredSections: ["header"], + allowedFonts: ["Inter"], + layout: { maxContentWidth: 1200 }, + }, + { + id: "demo-cli", + displayName: "Demo CLI", + type: "cli", + requiredSections: ["header"], + allowedFonts: ["Inter"], + layout: { maxContentWidth: 1200 }, + }, + ], + sections: [{ id: "header", intent: "Header", description: "Header" }], + constraints: { + motion: { + allowedDurationsMs: [200], + allowedTimingFunctions: ["ease"], + }, + }, + color: { + policy: "warn", + allowedValues: [], + }, + }; +} + +test("seedChromePolicyDefaults seeds safe defaults for web surfaces only", async () => { + const seeded = await seedChromePolicyDefaults({ + contract: baseContract(), + }); + + 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); +}); + +test("seedChromePolicyDefaults preserves existing chromePolicy", async () => { + const contract = baseContract(); + contract.surfaces[0].layout.chromePolicy = { + policy: "strict", + targets: ["page-container"], + maxBorderRadiusPx: 6, + allowOuterShadow: false, + 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, + }); +}); 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 ed53266..9385014 100644 --- a/packages/interfacectl-validator/dist/schema/web.surface.contract.schema.json +++ b/packages/interfacectl-validator/dist/schema/web.surface.contract.schema.json @@ -155,6 +155,9 @@ }, "pageFrame": { "$ref": "#/$defs/pageFrame" + }, + "chromePolicy": { + "$ref": "#/$defs/chromePolicy" } } }, @@ -319,6 +322,50 @@ } } }, + "chromePolicy": { + "type": "object", + "additionalProperties": false, + "required": [ + "policy", + "targets", + "maxBorderRadiusPx", + "allowOuterShadow", + "allowInsetShadow" + ], + "properties": { + "policy": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "page-container", + "top-level-section", + "layout-container" + ] + }, + "uniqueItems": true + }, + "maxBorderRadiusPx": { + "type": "number", + "minimum": 0 + }, + "allowOuterShadow": { + "type": "boolean" + }, + "allowInsetShadow": { + "type": "boolean" + } + } + }, "iconPolicy": { "type": "object", "additionalProperties": false, diff --git a/packages/interfacectl-validator/dist/types.d.ts b/packages/interfacectl-validator/dist/types.d.ts index f2222f9..d90f82c 100644 --- a/packages/interfacectl-validator/dist/types.d.ts +++ b/packages/interfacectl-validator/dist/types.d.ts @@ -7,6 +7,14 @@ export interface PageFrameLayout { alignment?: "center" | "left"; enforcement?: "strict" | "warn"; } +export type ChromePolicyTarget = "page-container" | "top-level-section" | "layout-container"; +export interface ChromePolicy { + policy: "off" | "warn" | "strict"; + targets: ChromePolicyTarget[]; + maxBorderRadiusPx: number; + allowOuterShadow: boolean; + allowInsetShadow: boolean; +} export interface FlowTransitionRequirement { from: string; to: string; @@ -32,6 +40,7 @@ export interface ContractSurface { maxContentWidth: number; requiredContainers?: string[]; pageFrame?: PageFrameLayout; + chromePolicy?: ChromePolicy; }; icons?: IconPolicy; flows?: FlowPolicy; diff --git a/packages/interfacectl-validator/dist/types.d.ts.map b/packages/interfacectl-validator/dist/types.d.ts.map index d365347..fdfca1c 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/schema/web.surface.contract.schema.json b/packages/interfacectl-validator/src/schema/web.surface.contract.schema.json index ed53266..9385014 100644 --- a/packages/interfacectl-validator/src/schema/web.surface.contract.schema.json +++ b/packages/interfacectl-validator/src/schema/web.surface.contract.schema.json @@ -155,6 +155,9 @@ }, "pageFrame": { "$ref": "#/$defs/pageFrame" + }, + "chromePolicy": { + "$ref": "#/$defs/chromePolicy" } } }, @@ -319,6 +322,50 @@ } } }, + "chromePolicy": { + "type": "object", + "additionalProperties": false, + "required": [ + "policy", + "targets", + "maxBorderRadiusPx", + "allowOuterShadow", + "allowInsetShadow" + ], + "properties": { + "policy": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "page-container", + "top-level-section", + "layout-container" + ] + }, + "uniqueItems": true + }, + "maxBorderRadiusPx": { + "type": "number", + "minimum": 0 + }, + "allowOuterShadow": { + "type": "boolean" + }, + "allowInsetShadow": { + "type": "boolean" + } + } + }, "iconPolicy": { "type": "object", "additionalProperties": false, diff --git a/packages/interfacectl-validator/src/types.ts b/packages/interfacectl-validator/src/types.ts index e459526..f7c5a47 100644 --- a/packages/interfacectl-validator/src/types.ts +++ b/packages/interfacectl-validator/src/types.ts @@ -9,6 +9,19 @@ export interface PageFrameLayout { enforcement?: "strict" | "warn"; } +export type ChromePolicyTarget = + | "page-container" + | "top-level-section" + | "layout-container"; + +export interface ChromePolicy { + policy: "off" | "warn" | "strict"; + targets: ChromePolicyTarget[]; + maxBorderRadiusPx: number; + allowOuterShadow: boolean; + allowInsetShadow: boolean; +} + export interface FlowTransitionRequirement { from: string; to: string; @@ -37,6 +50,7 @@ export interface ContractSurface { maxContentWidth: number; requiredContainers?: string[]; pageFrame?: PageFrameLayout; + chromePolicy?: ChromePolicy; }; icons?: IconPolicy; flows?: FlowPolicy; diff --git a/packages/interfacectl-validator/test/contract-structure.test.mjs b/packages/interfacectl-validator/test/contract-structure.test.mjs index dc86506..045a0b1 100644 --- a/packages/interfacectl-validator/test/contract-structure.test.mjs +++ b/packages/interfacectl-validator/test/contract-structure.test.mjs @@ -130,6 +130,46 @@ test("validateContractStructure accepts optional surface.flows policy", () => { assert.equal(result.ok, true); }); +test("validateContractStructure accepts optional surface.layout.chromePolicy", () => { + const schema = getBundledContractSchema(); + const contract = buildContract({ + layout: { + maxContentWidth: 1200, + chromePolicy: { + policy: "off", + targets: ["page-container", "top-level-section", "layout-container"], + maxBorderRadiusPx: 8, + allowOuterShadow: false, + allowInsetShadow: true, + }, + }, + }); + const result = validateContractStructure(contract, schema); + assert.equal(result.ok, true); +}); + +test("validateContractStructure rejects invalid surface.layout.chromePolicy target", () => { + const schema = getBundledContractSchema(); + const contract = buildContract({ + layout: { + maxContentWidth: 1200, + chromePolicy: { + policy: "strict", + targets: ["page-container", "card-grid"], + maxBorderRadiusPx: 8, + allowOuterShadow: false, + allowInsetShadow: true, + }, + }, + }); + const result = validateContractStructure(contract, schema); + assert.equal(result.ok, false); + assert.ok( + result.errors.some((error) => error.includes("targets")), + `expected chromePolicy targets validation error, got ${JSON.stringify(result.errors)}`, + ); +}); + test("validateContractStructure rejects malformed surface.flows requirements", () => { const schema = getBundledContractSchema(); const contract = buildContract({ From 8f086343d40e18d314a95b937e623a670d043f57 Mon Sep 17 00:00:00 2001 From: Mike Long Date: Fri, 6 Mar 2026 10:11:36 -0800 Subject: [PATCH 3/3] Enforce landing pattern consistency --- .../dist/commands/validate.js | 45 +++++++ .../dist/descriptors/static-analysis.d.ts.map | 2 +- .../dist/descriptors/static-analysis.js | 102 ++++++++++++++ .../interfacectl-cli/src/commands/validate.ts | 45 +++++++ .../src/descriptors/static-analysis.ts | 125 ++++++++++++++++++ .../test/static-analysis.test.mjs | 103 +++++++++++++++ .../interfacectl-validator/dist/index.d.ts | 2 +- .../dist/index.d.ts.map | 2 +- packages/interfacectl-validator/dist/index.js | 98 ++++++++++++++ .../schema/web.surface.contract.schema.json | 43 ++++++ .../interfacectl-validator/dist/types.d.ts | 17 ++- .../dist/types.d.ts.map | 2 +- packages/interfacectl-validator/src/index.ts | 118 +++++++++++++++++ .../schema/web.surface.contract.schema.json | 43 ++++++ packages/interfacectl-validator/src/types.ts | 22 +++ .../test/evaluate-contract.test.mjs | 116 ++++++++++++++++ 16 files changed, 880 insertions(+), 5 deletions(-) diff --git a/packages/interfacectl-cli/dist/commands/validate.js b/packages/interfacectl-cli/dist/commands/validate.js index a8847a7..3d00846 100644 --- a/packages/interfacectl-cli/dist/commands/validate.js +++ b/packages/interfacectl-cli/dist/commands/validate.js @@ -324,6 +324,11 @@ function mapViolationsToFindings(summary) { "layout-pageframe-selector-unsupported": "layout.pageframe.selector-unsupported", "layout-pageframe-non-deterministic-value": "layout.pageframe.non-deterministic-value", "layout-pageframe-unextractable-value": "layout.pageframe.unextractable-value", + "landing-pattern-signal-missing": "landing.pattern.signal-missing", + "landing-pattern-top-level-missing": "landing.pattern.top-level-missing", + "landing-pattern-section-order": "landing.pattern.section-order", + "landing-pattern-section-nested": "landing.pattern.section-nested", + "landing-pattern-background-mode": "landing.pattern.background-mode", "motion-duration-not-allowed": "motion.duration", "motion-timing-not-allowed": "motion.timing", "descriptor-flows-missing": "descriptor.flows.missing", @@ -495,6 +500,46 @@ function mapViolationsToFindings(summary) { }; break; } + case "landing-pattern-signal-missing": { + finding.expected = "landing pattern signals"; + finding.found = null; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "landing-pattern-top-level-missing": { + finding.expected = details.expectedTopLevelSections; + finding.found = details.missingTopLevelSections; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "landing-pattern-section-order": { + finding.expected = details.expectedSectionOrder; + finding.found = details.foundSectionOrder; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "landing-pattern-section-nested": { + finding.expected = []; + finding.found = details.nestedSections; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "landing-pattern-background-mode": { + finding.expected = details.expectedBackgroundMode; + finding.found = details.actualBackgroundMode; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } case "motion-duration-not-allowed": { finding.expected = details.allowedDurations; finding.found = details.durationMs; 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 edcce8b..487ae17 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,EASvB,MAAM,kCAAkC,CAAC;AAkH1C,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,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 diff --git a/packages/interfacectl-cli/dist/descriptors/static-analysis.js b/packages/interfacectl-cli/dist/descriptors/static-analysis.js index ea52c7d..0d07379 100644 --- a/packages/interfacectl-cli/dist/descriptors/static-analysis.js +++ b/packages/interfacectl-cli/dist/descriptors/static-analysis.js @@ -5,6 +5,7 @@ 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 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 INLINE_MAX_WIDTH_REGEX = /max-width\s*:\s*([0-9.]+)\s*px/gi; @@ -293,14 +294,115 @@ async function extractLayout(cssFilePaths, sectionFiles, workspaceRoot, fileCont if (surface?.layout.pageFrame) { pageFrame = await extractPageFrameLayout(cssFilePaths, sectionFiles, workspaceRoot, fileContentCache, surface.layout.pageFrame); } + let landingPattern; + if (surface?.layout.landingPattern) { + landingPattern = await extractLandingPattern(sectionFiles, workspaceRoot, fileContentCache); + } return { maxContentWidth: maxWidth, containers: [...containers].sort(), containerSources: [...containerSources].sort(), source: layoutSource, pageFrame, + landingPattern, }; } +async function extractLandingPattern(sectionFiles, workspaceRoot, fileContentCache) { + const landingPagePath = selectLandingPageFile(sectionFiles); + if (!landingPagePath) { + return undefined; + } + const content = await readFileCached(landingPagePath, fileContentCache); + const sectionOrder = []; + const topLevelSections = []; + const nestedSections = []; + const sectionStack = []; + const tagRegex = /<\/?([A-Za-z][\w.-]*)\b[^>]*>/g; + let match; + while ((match = tagRegex.exec(content)) !== null) { + const tag = match[0]; + const tagName = match[1]; + const isClosing = tag.startsWith("\s*$/.test(tag); + if (isClosing) { + for (let index = sectionStack.length - 1; index >= 0; index -= 1) { + if (sectionStack[index]?.tagName === tagName) { + sectionStack.splice(index, 1); + break; + } + } + continue; + } + const sectionId = extractAttributeValue(tag, SECTION_ATTRIBUTE_REGEX); + if (!sectionId) { + continue; + } + sectionOrder.push(sectionId); + if (sectionStack.length === 0) { + topLevelSections.push(sectionId); + } + else if (!nestedSections.includes(sectionId)) { + nestedSections.push(sectionId); + } + if (!isSelfClosing) { + sectionStack.push({ tagName, id: sectionId }); + } + } + return { + sectionOrder, + topLevelSections, + nestedSections, + pageBackgroundMode: extractPageBackgroundMode(content), + source: path.relative(workspaceRoot, landingPagePath), + }; +} +function selectLandingPageFile(filePaths) { + const candidates = filePaths.filter((filePath) => filePath.endsWith(`${path.sep}page.tsx`)); + if (candidates.length === 0) { + return undefined; + } + const ranked = [...candidates].sort((left, right) => { + const leftRank = getLandingPageRank(left); + const rightRank = getLandingPageRank(right); + if (leftRank !== rightRank) { + return leftRank - rightRank; + } + return left.localeCompare(right); + }); + return ranked[0]; +} +function getLandingPageRank(filePath) { + if (filePath.includes(`${path.sep}app${path.sep}page.tsx`)) + return 0; + if (filePath.includes(`${path.sep}app${path.sep}(overview)${path.sep}page.tsx`)) + return 1; + if (filePath.includes(`${path.sep}app${path.sep}(marketing)${path.sep}page.tsx`)) + return 2; + return 10; +} +function extractAttributeValue(tag, regex) { + regex.lastIndex = 0; + const match = regex.exec(tag); + const value = match?.[1] ?? match?.[2] ?? match?.[3] ?? match?.[4] ?? ""; + return value || undefined; +} +function extractPageBackgroundMode(content) { + const tagRegex = /<[A-Za-z][\w.-]*\b[^>]*>/g; + let match; + while ((match = tagRegex.exec(content)) !== null) { + const tag = match[0]; + if (!MIN_SCREEN_CLASS_REGEX.test(tag)) { + continue; + } + if (/\bbackground\s*:/.test(tag)) { + return "custom"; + } + if (/\bbackgroundColor\s*:/.test(tag)) { + return "solid"; + } + } + return "unknown"; +} async function extractPageFrameLayout(cssFilePaths, sectionFiles, workspaceRoot, fileContentCache, pageFrameContract) { if (!pageFrameContract) { return undefined; diff --git a/packages/interfacectl-cli/src/commands/validate.ts b/packages/interfacectl-cli/src/commands/validate.ts index 09289f4..f51d43d 100644 --- a/packages/interfacectl-cli/src/commands/validate.ts +++ b/packages/interfacectl-cli/src/commands/validate.ts @@ -476,6 +476,11 @@ function mapViolationsToFindings( "layout-pageframe-selector-unsupported": "layout.pageframe.selector-unsupported", "layout-pageframe-non-deterministic-value": "layout.pageframe.non-deterministic-value", "layout-pageframe-unextractable-value": "layout.pageframe.unextractable-value", + "landing-pattern-signal-missing": "landing.pattern.signal-missing", + "landing-pattern-top-level-missing": "landing.pattern.top-level-missing", + "landing-pattern-section-order": "landing.pattern.section-order", + "landing-pattern-section-nested": "landing.pattern.section-nested", + "landing-pattern-background-mode": "landing.pattern.background-mode", "motion-duration-not-allowed": "motion.duration", "motion-timing-not-allowed": "motion.timing", "descriptor-flows-missing": "descriptor.flows.missing", @@ -651,6 +656,46 @@ function mapViolationsToFindings( }; break; } + case "landing-pattern-signal-missing": { + finding.expected = "landing pattern signals"; + finding.found = null; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "landing-pattern-top-level-missing": { + finding.expected = details.expectedTopLevelSections; + finding.found = details.missingTopLevelSections; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "landing-pattern-section-order": { + finding.expected = details.expectedSectionOrder; + finding.found = details.foundSectionOrder; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "landing-pattern-section-nested": { + finding.expected = []; + finding.found = details.nestedSections; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } + case "landing-pattern-background-mode": { + finding.expected = details.expectedBackgroundMode; + finding.found = details.actualBackgroundMode; + if (details.policy === "warn") { + finding.severity = "warning"; + } + break; + } case "motion-duration-not-allowed": { finding.expected = details.allowedDurations; finding.found = details.durationMs; diff --git a/packages/interfacectl-cli/src/descriptors/static-analysis.ts b/packages/interfacectl-cli/src/descriptors/static-analysis.ts index fa88951..1b644a4 100644 --- a/packages/interfacectl-cli/src/descriptors/static-analysis.ts +++ b/packages/interfacectl-cli/src/descriptors/static-analysis.ts @@ -11,6 +11,7 @@ import { type SurfaceMotionDescriptor, type SurfaceSectionDescriptor, type PageFrameLayoutDescriptor, + type LandingPatternDescriptor, type SurfacePrimitiveDescriptor, } from "@surfaces/interfacectl-validator"; @@ -22,6 +23,8 @@ 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 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 INLINE_MAX_WIDTH_REGEX = /max-width\s*:\s*([0-9.]+)\s*px/gi; @@ -496,15 +499,137 @@ async function extractLayout( ); } + let landingPattern: LandingPatternDescriptor | undefined; + if (surface?.layout.landingPattern) { + landingPattern = await extractLandingPattern( + sectionFiles, + workspaceRoot, + fileContentCache, + ); + } + return { maxContentWidth: maxWidth, containers: [...containers].sort(), containerSources: [...containerSources].sort(), source: layoutSource, pageFrame, + landingPattern, }; } +async function extractLandingPattern( + sectionFiles: string[], + workspaceRoot: string, + fileContentCache: Map, +): Promise { + const landingPagePath = selectLandingPageFile(sectionFiles); + if (!landingPagePath) { + return undefined; + } + + const content = await readFileCached(landingPagePath, fileContentCache); + const sectionOrder: string[] = []; + const topLevelSections: string[] = []; + const nestedSections: string[] = []; + const sectionStack: Array<{ tagName: string; id: string }> = []; + const tagRegex = /<\/?([A-Za-z][\w.-]*)\b[^>]*>/g; + let match: RegExpExecArray | null; + + while ((match = tagRegex.exec(content)) !== null) { + const tag = match[0]; + const tagName = match[1]; + const isClosing = tag.startsWith("\s*$/.test(tag); + + if (isClosing) { + for (let index = sectionStack.length - 1; index >= 0; index -= 1) { + if (sectionStack[index]?.tagName === tagName) { + sectionStack.splice(index, 1); + break; + } + } + continue; + } + + const sectionId = extractAttributeValue(tag, SECTION_ATTRIBUTE_REGEX); + if (!sectionId) { + continue; + } + + sectionOrder.push(sectionId); + if (sectionStack.length === 0) { + topLevelSections.push(sectionId); + } else if (!nestedSections.includes(sectionId)) { + nestedSections.push(sectionId); + } + + if (!isSelfClosing) { + sectionStack.push({ tagName, id: sectionId }); + } + } + + return { + sectionOrder, + topLevelSections, + nestedSections, + pageBackgroundMode: extractPageBackgroundMode(content), + source: path.relative(workspaceRoot, landingPagePath), + }; +} + +function selectLandingPageFile(filePaths: string[]): string | undefined { + const candidates = filePaths.filter((filePath) => filePath.endsWith(`${path.sep}page.tsx`)); + if (candidates.length === 0) { + return undefined; + } + + const ranked = [...candidates].sort((left, right) => { + const leftRank = getLandingPageRank(left); + const rightRank = getLandingPageRank(right); + if (leftRank !== rightRank) { + return leftRank - rightRank; + } + return left.localeCompare(right); + }); + + return ranked[0]; +} + +function getLandingPageRank(filePath: string): number { + if (filePath.includes(`${path.sep}app${path.sep}page.tsx`)) return 0; + if (filePath.includes(`${path.sep}app${path.sep}(overview)${path.sep}page.tsx`)) return 1; + if (filePath.includes(`${path.sep}app${path.sep}(marketing)${path.sep}page.tsx`)) return 2; + return 10; +} + +function extractAttributeValue(tag: string, regex: RegExp): string | undefined { + regex.lastIndex = 0; + const match = regex.exec(tag); + const value = match?.[1] ?? match?.[2] ?? match?.[3] ?? match?.[4] ?? ""; + return value || undefined; +} + +function extractPageBackgroundMode(content: string): "solid" | "custom" | "unknown" { + const tagRegex = /<[A-Za-z][\w.-]*\b[^>]*>/g; + let match: RegExpExecArray | null; + + while ((match = tagRegex.exec(content)) !== null) { + const tag = match[0]; + if (!MIN_SCREEN_CLASS_REGEX.test(tag)) { + continue; + } + if (/\bbackground\s*:/.test(tag)) { + return "custom"; + } + if (/\bbackgroundColor\s*:/.test(tag)) { + return "solid"; + } + } + + return "unknown"; +} + async function extractPageFrameLayout( cssFilePaths: string[], sectionFiles: string[], diff --git a/packages/interfacectl-cli/test/static-analysis.test.mjs b/packages/interfacectl-cli/test/static-analysis.test.mjs index 25c62ed..bd41d06 100644 --- a/packages/interfacectl-cli/test/static-analysis.test.mjs +++ b/packages/interfacectl-cli/test/static-analysis.test.mjs @@ -397,3 +397,106 @@ test("collectSurfaceDescriptors captures deterministic icon sources from surface 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"; + const surfaceRoot = path.join(tempRoot, "apps", surfaceId); + + try { + await mkdir(path.join(surfaceRoot, "app", "(overview)"), { 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", "(overview)", "page.tsx"), + ` + export default function Page() { + return ( +
+
+

Hero

+
+

Guidance

+
+
+
+ +
+
+ ); + } + `, + "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" }, + { id: "landing.actions", intent: "actions", description: "Action section" }, + ], + constraints: { + motion: { + allowedDurationsMs: [120], + allowedTimingFunctions: ["linear"], + }, + }, + surfaces: [ + { + id: surfaceId, + displayName: "Demo Landing", + type: "web", + requiredSections: ["landing.hero", "landing.guidance", "landing.actions"], + allowedFonts: ["Inter"], + layout: { + maxContentWidth: 1120, + landingPattern: { + policy: "strict", + requireTopLevelSections: ["landing.hero", "landing.guidance", "landing.actions"], + sectionOrder: ["landing.hero", "landing.guidance", "landing.actions"], + pageBackgroundMode: "solid", + }, + }, + }, + ], + color: { + policy: "off", + allowedValues: [], + }, + }; + + const result = await collectSurfaceDescriptors({ + workspaceRoot: tempRoot, + contract, + surfaceFilters: new Set(), + surfaceRootMap: new Map(), + }); + + const descriptor = result.descriptors[0]; + assert.ok(descriptor?.layout.landingPattern, "landingPattern should be extracted"); + assert.deepEqual(descriptor.layout.landingPattern.sectionOrder, [ + "landing.hero", + "landing.guidance", + "landing.actions", + ]); + assert.deepEqual(descriptor.layout.landingPattern.topLevelSections, [ + "landing.hero", + "landing.actions", + ]); + assert.deepEqual(descriptor.layout.landingPattern.nestedSections, ["landing.guidance"]); + assert.equal(descriptor.layout.landingPattern.pageBackgroundMode, "custom"); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +}); diff --git a/packages/interfacectl-validator/dist/index.d.ts b/packages/interfacectl-validator/dist/index.d.ts index e649f0c..ba0f5e4 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, SurfacePrimitiveDescriptor, SurfaceReport, DriftViolation, ValidationSummary, DriftViolationType, ContractRef, RuleRef, DiffOutput, DiffEntry, DiffChangeType, DriftRisk, Severity, SafetyLevel, EnforcementPolicy, EnforcementMode, IconPolicy, FlowPolicy, FlowRequirement, FlowTransitionRequirement, SurfaceFlowDescriptor, SurfaceFlowStepDescriptor, SurfaceFlowTransitionDescriptor, AutofixRule, FixSummary, FixEntry, FixError, } from "./types.js"; +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 { 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 a6fd63b..261b2dd 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;AAuOD,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,EAAE,iBAAiB,GAC5B,aAAa,CA6af;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,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,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,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 diff --git a/packages/interfacectl-validator/dist/index.js b/packages/interfacectl-validator/dist/index.js index 9fb3b5d..01b66d2 100644 --- a/packages/interfacectl-validator/dist/index.js +++ b/packages/interfacectl-validator/dist/index.js @@ -204,6 +204,103 @@ function validateFlowPolicy(surface, descriptor, violations) { } } } +function validateLandingPattern(surface, descriptor, violations) { + const landingPattern = surface.layout.landingPattern; + if (!landingPattern || landingPattern.policy === "off") { + return; + } + const descriptorPattern = descriptor.layout.landingPattern; + if (!descriptorPattern) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "landing-pattern-signal-missing", + message: `Landing pattern signals are missing for surface "${descriptor.surfaceId}".`, + details: { + policy: landingPattern.policy, + }, + }); + return; + } + const topLevelSections = new Set(descriptorPattern.topLevelSections); + const nestedSections = new Set(descriptorPattern.nestedSections); + const orderedSections = descriptorPattern.sectionOrder; + const requiredTopLevel = landingPattern.requireTopLevelSections ?? []; + const missingTopLevel = requiredTopLevel.filter((sectionId) => !topLevelSections.has(sectionId)); + if (missingTopLevel.length > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "landing-pattern-top-level-missing", + message: `Landing sections must appear as top-level blocks for surface "${descriptor.surfaceId}": ${missingTopLevel.join(", ")}.`, + details: { + policy: landingPattern.policy, + expectedTopLevelSections: requiredTopLevel, + missingTopLevelSections: missingTopLevel, + source: descriptorPattern.source, + }, + }); + } + const disallowedNestedSections = requiredTopLevel.filter((sectionId) => nestedSections.has(sectionId)); + if (disallowedNestedSections.length > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "landing-pattern-section-nested", + message: `Landing sections must not be nested inside other contract sections for surface "${descriptor.surfaceId}": ${disallowedNestedSections.join(", ")}.`, + details: { + policy: landingPattern.policy, + nestedSections: disallowedNestedSections, + source: descriptorPattern.source, + }, + }); + } + const expectedOrder = landingPattern.sectionOrder ?? []; + if (expectedOrder.length > 1) { + let lastIndex = -1; + const outOfOrderSections = []; + for (const sectionId of expectedOrder) { + const sectionIndex = orderedSections.indexOf(sectionId); + if (sectionIndex === -1) { + continue; + } + if (sectionIndex < lastIndex) { + outOfOrderSections.push(sectionId); + } + else { + lastIndex = sectionIndex; + } + } + if (outOfOrderSections.length > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "landing-pattern-section-order", + message: `Landing section order does not match the shared pattern for surface "${descriptor.surfaceId}".`, + details: { + policy: landingPattern.policy, + expectedSectionOrder: expectedOrder, + foundSectionOrder: orderedSections, + outOfOrderSections, + source: descriptorPattern.source, + }, + }); + } + } + const expectedBackgroundMode = landingPattern.pageBackgroundMode; + if (expectedBackgroundMode && + descriptorPattern.pageBackgroundMode && + descriptorPattern.pageBackgroundMode !== "unknown" && + descriptorPattern.pageBackgroundMode !== expectedBackgroundMode) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "landing-pattern-background-mode", + message: `Landing page background treatment for surface "${descriptor.surfaceId}" must be "${expectedBackgroundMode}".`, + details: { + policy: landingPattern.policy, + expectedBackgroundMode, + actualBackgroundMode: descriptorPattern.pageBackgroundMode, + source: descriptorPattern.source, + }, + }); + } +} export function evaluateSurfaceCompliance(contract, descriptor) { const surface = findSurface(contract.surfaces, descriptor.surfaceId); const violations = []; @@ -326,6 +423,7 @@ export function evaluateSurfaceCompliance(contract, descriptor) { validateColorPolicy(contract, descriptor, violations); validateIconPolicy(surface, descriptor, violations); validateFlowPolicy(surface, descriptor, violations); + validateLandingPattern(surface, descriptor, violations); const reportedWidth = descriptor.layout.maxContentWidth; if (reportedWidth === null || reportedWidth === undefined) { violations.push({ 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 9385014..f710cdf 100644 --- a/packages/interfacectl-validator/dist/schema/web.surface.contract.schema.json +++ b/packages/interfacectl-validator/dist/schema/web.surface.contract.schema.json @@ -158,6 +158,9 @@ }, "chromePolicy": { "$ref": "#/$defs/chromePolicy" + }, + "landingPattern": { + "$ref": "#/$defs/landingPattern" } } }, @@ -392,6 +395,46 @@ } } }, + "landingPattern": { + "type": "object", + "additionalProperties": false, + "required": [ + "policy" + ], + "properties": { + "policy": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + }, + "requireTopLevelSections": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9]+(\\.[a-z0-9]+)*$" + }, + "uniqueItems": true + }, + "sectionOrder": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9]+(\\.[a-z0-9]+)*$" + }, + "uniqueItems": true + }, + "pageBackgroundMode": { + "type": "string", + "enum": [ + "solid", + "custom" + ] + } + } + }, "flowPolicy": { "type": "object", "additionalProperties": false, diff --git a/packages/interfacectl-validator/dist/types.d.ts b/packages/interfacectl-validator/dist/types.d.ts index d90f82c..d4ce422 100644 --- a/packages/interfacectl-validator/dist/types.d.ts +++ b/packages/interfacectl-validator/dist/types.d.ts @@ -15,6 +15,12 @@ export interface ChromePolicy { allowOuterShadow: boolean; allowInsetShadow: boolean; } +export interface LandingPatternPolicy { + policy: "off" | "warn" | "strict"; + requireTopLevelSections?: string[]; + sectionOrder?: string[]; + pageBackgroundMode?: "solid" | "custom"; +} export interface FlowTransitionRequirement { from: string; to: string; @@ -41,6 +47,7 @@ export interface ContractSurface { requiredContainers?: string[]; pageFrame?: PageFrameLayout; chromePolicy?: ChromePolicy; + landingPattern?: LandingPatternPolicy; }; icons?: IconPolicy; flows?: FlowPolicy; @@ -138,12 +145,20 @@ export interface PageFrameLayoutDescriptor { minWidthHasClampCalc?: boolean; paddingHasClampCalc?: boolean; } +export interface LandingPatternDescriptor { + sectionOrder: string[]; + topLevelSections: string[]; + nestedSections: string[]; + pageBackgroundMode?: "solid" | "custom" | "unknown"; + source?: string; +} export interface SurfaceLayoutDescriptor { maxContentWidth?: number | null; containers?: string[]; containerSources?: string[]; source?: string; pageFrame?: PageFrameLayoutDescriptor; + landingPattern?: LandingPatternDescriptor; } export interface SurfaceDescriptor { surfaceId: string; @@ -157,7 +172,7 @@ export interface SurfaceDescriptor { motion: SurfaceMotionDescriptor[]; primitives?: SurfacePrimitiveDescriptor[]; } -export type DriftViolationType = "unknown-surface" | "missing-section" | "unknown-section" | "font-not-allowed" | "color-not-allowed" | "icon-source-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-minwidth-mismatch" | "layout-pageframe-padding-mismatch" | "layout-pageframe-non-deterministic-value" | "layout-pageframe-unextractable-value" | "motion-duration-not-allowed" | "motion-timing-not-allowed" | "descriptor-flows-missing" | "flow-required-missing" | "flow-steps-min" | "flow-steps-required" | "flow-transition-required" | "flow-terminal-invalid" | "descriptor-missing" | "descriptor-unused" | "shell-owned-primitive-emitted"; +export type DriftViolationType = "unknown-surface" | "missing-section" | "unknown-section" | "font-not-allowed" | "color-not-allowed" | "icon-source-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-minwidth-mismatch" | "layout-pageframe-padding-mismatch" | "layout-pageframe-non-deterministic-value" | "layout-pageframe-unextractable-value" | "landing-pattern-signal-missing" | "landing-pattern-top-level-missing" | "landing-pattern-section-order" | "landing-pattern-section-nested" | "landing-pattern-background-mode" | "motion-duration-not-allowed" | "motion-timing-not-allowed" | "descriptor-flows-missing" | "flow-required-missing" | "flow-steps-min" | "flow-steps-required" | "flow-transition-required" | "flow-terminal-invalid" | "descriptor-missing" | "descriptor-unused" | "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 fdfca1c..acad1c8 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 9d10dc8..4d64d8c 100644 --- a/packages/interfacectl-validator/src/index.ts +++ b/packages/interfacectl-validator/src/index.ts @@ -286,6 +286,121 @@ function validateFlowPolicy( } } +function validateLandingPattern( + surface: ContractSurface, + descriptor: SurfaceDescriptor, + violations: DriftViolation[], +): void { + const landingPattern = surface.layout.landingPattern; + if (!landingPattern || landingPattern.policy === "off") { + return; + } + + const descriptorPattern = descriptor.layout.landingPattern; + if (!descriptorPattern) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "landing-pattern-signal-missing", + message: `Landing pattern signals are missing for surface "${descriptor.surfaceId}".`, + details: { + policy: landingPattern.policy, + }, + }); + return; + } + + const topLevelSections = new Set(descriptorPattern.topLevelSections); + const nestedSections = new Set(descriptorPattern.nestedSections); + const orderedSections = descriptorPattern.sectionOrder; + + const requiredTopLevel = landingPattern.requireTopLevelSections ?? []; + const missingTopLevel = requiredTopLevel.filter( + (sectionId) => !topLevelSections.has(sectionId), + ); + if (missingTopLevel.length > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "landing-pattern-top-level-missing", + message: `Landing sections must appear as top-level blocks for surface "${descriptor.surfaceId}": ${missingTopLevel.join(", ")}.`, + details: { + policy: landingPattern.policy, + expectedTopLevelSections: requiredTopLevel, + missingTopLevelSections: missingTopLevel, + source: descriptorPattern.source, + }, + }); + } + + const disallowedNestedSections = requiredTopLevel.filter((sectionId) => + nestedSections.has(sectionId), + ); + if (disallowedNestedSections.length > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "landing-pattern-section-nested", + message: `Landing sections must not be nested inside other contract sections for surface "${descriptor.surfaceId}": ${disallowedNestedSections.join(", ")}.`, + details: { + policy: landingPattern.policy, + nestedSections: disallowedNestedSections, + source: descriptorPattern.source, + }, + }); + } + + const expectedOrder = landingPattern.sectionOrder ?? []; + if (expectedOrder.length > 1) { + let lastIndex = -1; + const outOfOrderSections: string[] = []; + + for (const sectionId of expectedOrder) { + const sectionIndex = orderedSections.indexOf(sectionId); + if (sectionIndex === -1) { + continue; + } + if (sectionIndex < lastIndex) { + outOfOrderSections.push(sectionId); + } else { + lastIndex = sectionIndex; + } + } + + if (outOfOrderSections.length > 0) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "landing-pattern-section-order", + message: `Landing section order does not match the shared pattern for surface "${descriptor.surfaceId}".`, + details: { + policy: landingPattern.policy, + expectedSectionOrder: expectedOrder, + foundSectionOrder: orderedSections, + outOfOrderSections, + source: descriptorPattern.source, + }, + }); + } + } + + const expectedBackgroundMode = landingPattern.pageBackgroundMode; + if ( + expectedBackgroundMode && + descriptorPattern.pageBackgroundMode && + descriptorPattern.pageBackgroundMode !== "unknown" && + descriptorPattern.pageBackgroundMode !== expectedBackgroundMode + ) { + violations.push({ + surfaceId: descriptor.surfaceId, + type: "landing-pattern-background-mode", + message: `Landing page background treatment for surface "${descriptor.surfaceId}" must be "${expectedBackgroundMode}".`, + details: { + policy: landingPattern.policy, + expectedBackgroundMode, + actualBackgroundMode: descriptorPattern.pageBackgroundMode, + source: descriptorPattern.source, + }, + }); + } +} + export function evaluateSurfaceCompliance( contract: InterfaceContract, descriptor: SurfaceDescriptor, @@ -423,6 +538,7 @@ export function evaluateSurfaceCompliance( validateColorPolicy(contract, descriptor, violations); validateIconPolicy(surface, descriptor, violations); validateFlowPolicy(surface, descriptor, violations); + validateLandingPattern(surface, descriptor, violations); const reportedWidth = descriptor.layout.maxContentWidth; if (reportedWidth === null || reportedWidth === undefined) { @@ -822,6 +938,7 @@ export type { SurfaceMotionDescriptor, SurfaceLayoutDescriptor, PageFrameLayoutDescriptor, + LandingPatternDescriptor, SurfacePrimitiveDescriptor, SurfaceReport, DriftViolation, @@ -841,6 +958,7 @@ export type { FlowPolicy, FlowRequirement, FlowTransitionRequirement, + LandingPatternPolicy, SurfaceFlowDescriptor, SurfaceFlowStepDescriptor, SurfaceFlowTransitionDescriptor, 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 9385014..f710cdf 100644 --- a/packages/interfacectl-validator/src/schema/web.surface.contract.schema.json +++ b/packages/interfacectl-validator/src/schema/web.surface.contract.schema.json @@ -158,6 +158,9 @@ }, "chromePolicy": { "$ref": "#/$defs/chromePolicy" + }, + "landingPattern": { + "$ref": "#/$defs/landingPattern" } } }, @@ -392,6 +395,46 @@ } } }, + "landingPattern": { + "type": "object", + "additionalProperties": false, + "required": [ + "policy" + ], + "properties": { + "policy": { + "type": "string", + "enum": [ + "off", + "warn", + "strict" + ] + }, + "requireTopLevelSections": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9]+(\\.[a-z0-9]+)*$" + }, + "uniqueItems": true + }, + "sectionOrder": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9]+(\\.[a-z0-9]+)*$" + }, + "uniqueItems": true + }, + "pageBackgroundMode": { + "type": "string", + "enum": [ + "solid", + "custom" + ] + } + } + }, "flowPolicy": { "type": "object", "additionalProperties": false, diff --git a/packages/interfacectl-validator/src/types.ts b/packages/interfacectl-validator/src/types.ts index f7c5a47..bc4b820 100644 --- a/packages/interfacectl-validator/src/types.ts +++ b/packages/interfacectl-validator/src/types.ts @@ -22,6 +22,13 @@ export interface ChromePolicy { allowInsetShadow: boolean; } +export interface LandingPatternPolicy { + policy: "off" | "warn" | "strict"; + requireTopLevelSections?: string[]; + sectionOrder?: string[]; + pageBackgroundMode?: "solid" | "custom"; +} + export interface FlowTransitionRequirement { from: string; to: string; @@ -51,6 +58,7 @@ export interface ContractSurface { requiredContainers?: string[]; pageFrame?: PageFrameLayout; chromePolicy?: ChromePolicy; + landingPattern?: LandingPatternPolicy; }; icons?: IconPolicy; flows?: FlowPolicy; @@ -165,12 +173,21 @@ export interface PageFrameLayoutDescriptor { paddingHasClampCalc?: boolean; } +export interface LandingPatternDescriptor { + sectionOrder: string[]; + topLevelSections: string[]; + nestedSections: string[]; + pageBackgroundMode?: "solid" | "custom" | "unknown"; + source?: string; +} + export interface SurfaceLayoutDescriptor { maxContentWidth?: number | null; containers?: string[]; containerSources?: string[]; source?: string; pageFrame?: PageFrameLayoutDescriptor; + landingPattern?: LandingPatternDescriptor; } export interface SurfaceDescriptor { @@ -203,6 +220,11 @@ export type DriftViolationType = | "layout-pageframe-padding-mismatch" | "layout-pageframe-non-deterministic-value" | "layout-pageframe-unextractable-value" + | "landing-pattern-signal-missing" + | "landing-pattern-top-level-missing" + | "landing-pattern-section-order" + | "landing-pattern-section-nested" + | "landing-pattern-background-mode" | "motion-duration-not-allowed" | "motion-timing-not-allowed" | "descriptor-flows-missing" diff --git a/packages/interfacectl-validator/test/evaluate-contract.test.mjs b/packages/interfacectl-validator/test/evaluate-contract.test.mjs index 81ffe18..8a4ed56 100644 --- a/packages/interfacectl-validator/test/evaluate-contract.test.mjs +++ b/packages/interfacectl-validator/test/evaluate-contract.test.mjs @@ -724,3 +724,119 @@ test("flow policy emits flow-terminal-invalid when terminal step has outgoing tr { from: "review", to: "done" }, ]); }); + +test("reports landing pattern violations for nested sections and custom background", () => { + const contract = { + ...baseContract, + sections: [ + { id: "landing.hero", intent: "hero", description: "Hero section" }, + { id: "landing.guidance", intent: "guidance", description: "Guidance section" }, + { id: "landing.actions", intent: "actions", description: "Actions section" }, + ], + surfaces: [ + { + id: "surface-landing-pattern", + displayName: "Surface Landing Pattern", + type: "web", + requiredSections: ["landing.hero", "landing.guidance", "landing.actions"], + allowedFonts: ["var(--font-landing)"], + layout: { + maxContentWidth: 1120, + landingPattern: { + policy: "strict", + requireTopLevelSections: ["landing.hero", "landing.guidance", "landing.actions"], + sectionOrder: ["landing.hero", "landing.guidance", "landing.actions"], + pageBackgroundMode: "solid", + }, + }, + }, + ], + }; + + const descriptor = { + surfaceId: "surface-landing-pattern", + sections: [ + { id: "landing.hero" }, + { id: "landing.guidance" }, + { id: "landing.actions" }, + ], + fonts: [{ value: "var(--font-landing)" }], + colors: [{ value: "var(--color-primary)" }], + layout: { + maxContentWidth: 1000, + containers: ["contract-container"], + landingPattern: { + sectionOrder: ["landing.hero", "landing.guidance", "landing.actions"], + topLevelSections: ["landing.hero"], + nestedSections: ["landing.guidance", "landing.actions"], + pageBackgroundMode: "custom", + source: "apps/surface/app/page.tsx", + }, + }, + motion: [], + }; + + const report = evaluateSurfaceCompliance(contract, descriptor); + const violationTypes = report.violations.map((violation) => violation.type); + assert.ok(violationTypes.includes("landing-pattern-top-level-missing")); + assert.ok(violationTypes.includes("landing-pattern-section-nested")); + assert.ok(violationTypes.includes("landing-pattern-background-mode")); +}); + +test("passes landing pattern validation when the shared structure is preserved", () => { + const contract = { + ...baseContract, + sections: [ + { id: "landing.hero", intent: "hero", description: "Hero section" }, + { id: "landing.guidance", intent: "guidance", description: "Guidance section" }, + { id: "landing.actions", intent: "actions", description: "Actions section" }, + ], + surfaces: [ + { + id: "surface-landing-pass", + displayName: "Surface Landing Pass", + type: "web", + requiredSections: ["landing.hero", "landing.guidance", "landing.actions"], + allowedFonts: ["var(--font-landing)"], + layout: { + maxContentWidth: 1120, + landingPattern: { + policy: "strict", + requireTopLevelSections: ["landing.hero", "landing.guidance", "landing.actions"], + sectionOrder: ["landing.hero", "landing.guidance", "landing.actions"], + pageBackgroundMode: "solid", + }, + }, + }, + ], + }; + + const descriptor = { + surfaceId: "surface-landing-pass", + sections: [ + { id: "landing.hero" }, + { id: "landing.guidance" }, + { id: "landing.actions" }, + ], + fonts: [{ value: "var(--font-landing)" }], + colors: [{ value: "var(--color-primary)" }], + layout: { + maxContentWidth: 1000, + containers: ["contract-container"], + landingPattern: { + sectionOrder: ["landing.hero", "landing.guidance", "landing.actions"], + topLevelSections: ["landing.hero", "landing.guidance", "landing.actions"], + nestedSections: [], + pageBackgroundMode: "solid", + source: "apps/surface/app/page.tsx", + }, + }, + motion: [], + }; + + const report = evaluateSurfaceCompliance(contract, descriptor); + const landingViolations = report.violations.filter((violation) => + violation.type.startsWith("landing-pattern-"), + ); + assert.equal(landingViolations.length, 0); +});