Goal: show how to consume the current interface contract at Runtime (edge) for adaptive UI gating and how that runtime policy lines up with canonical browser-observed validation.
Terminology follows docs/taxonomy.md:
- This guide covers
Runtime (edge)only. Generation timeandCI/CD timeare separate contexts.
- Source contract:
contracts/surfaces.web.contract.json - Compile a per-surface bundle:
interfacectl compile --contract ./contracts/surfaces.web.contract.json --out ./artifacts/runtime-bundle- Resolve one adapter-ready runtime payload:
interfacectl prepare-runtime \
--bundle-root ./artifacts/runtime-bundle \
--surface surfaces-web \
--out ./artifacts/runtime-inputs/surfaces-web.json- The compiled bundle includes
surfaces/<surfaceId>/runtime.json. A representative runtime slice looks like:
{
"provenance": {
"contractId": "surfaces.web",
"contractVersion": "0.1.0"
},
"identity": {
"surfaceId": "surfaces-web",
"displayName": "Surfaces Web",
"type": "web"
},
"runtime": {
"policy": "strict",
"policySeverities": {
"interaction": "warn",
"runtime": "strict",
"structure": "strict"
},
"mutationEnvelope": {
"mode": "slot-bound"
},
"contexts": [
{ "id": "launch", "policy": "warn" },
{ "id": "error", "kind": "error", "requiredRecoveryActions": ["retry"] }
],
"feedbackRecovery": {
"policy": "warn",
"requiredStateKinds": ["loading", "empty", "error", "success"]
},
"structure": {
"requiredSections": ["landing.hero"],
"flowSummary": {
"policy": "warn",
"flowIds": ["signup"],
"requirementCount": 1
}
},
"interaction": {
"targetAcquisition": {
"policy": "warn",
"minHitAreaPx": 44,
"minGapPx": 8,
"minEdgeInsetPx": 8,
"destructiveGapPx": 16
}
}
}
}Always attach provenance to decisions: provenance.contractId, provenance.contractVersion, and identity.surfaceId.
prepare-runtime turns that slice into one deterministic payload with provenance, governance, summary checklist items, and evidence refs. Runtime consumers can read runtime.json directly if they want, but prepare-runtime is the canonical adapter-ready handoff.
strict: block changewarn: allow and emit violationoff: skip color check
{
"surfaceId": "surfaces-web",
"contractId": "surfaces.web",
"version": "0.1.0",
"rule": "target.hit-area-too-small",
"policy": "warn",
"evidence": {
"targetId": "primary-cta",
"measuredHitAreaPx": { "width": 36, "height": 36 },
"requiredMinHitAreaPx": 44
},
"action": "allow"
}The same shape applies to browser-observed flow.* and feedback.* findings. Consumer repos may remap these into dashboards or repair guidance, but the rule codes and their meanings stay canonical in interfacectl.
Browser-observed runtime validation uses interfacectl validate --remote-url ... and the same contract semantics as compiled runtime consumers.
Runtime observation measures rendered controls and can emit:
target.hit-area-too-smalltarget.gap-too-tighttarget.edge-inset-too-smalltarget.destructive-too-closetarget.unobservable
Runtime observation reads data-contract-flow-* markers and can emit:
flow.steps.requiredflow.transition.requiredflow.terminal.invalidflow.unobservable
Runtime observation reads data-contract-state-* and data-contract-recovery-action markers and can emit:
feedback.recovery-action-missingfeedback.pending-action-not-blockedfeedback.last-good-content-missingfeedback.unobservable
See: docs/examples/edge/validate-edge.js
import manifest from "./manifest.json" assert { type: "json" };
export function validateEdgeChange(change) {
const violations = [];
if (!change.sectionId) {
violations.push(makeViolation("sections.required", "strict", { sectionId: change.sectionId }));
}
const colorPolicy = manifest.color.policy;
const background = change.proposedStyles?.background;
if (colorPolicy !== "off" && isDisallowedColor(background, manifest.color.allowedValues)) {
violations.push(makeViolation("color.allowedValues", colorPolicy, {
property: "background",
value: background,
expected: manifest.color.allowedValues,
}));
}
const blocking = violations.find((v) => v.policy === "strict");
return {
allowed: !blocking,
violations,
contract: {
id: manifest.contractId,
version: manifest.version,
surfaceId: manifest.surfaceId,
},
};
}
function isDisallowedColor(value = "", allowedValues = []) {
const normalized = normalizeColorValue(value);
const allowed = new Set(allowedValues.map(normalizeColorValue));
return normalized.length > 0 && !allowed.has(normalized);
}
function normalizeColorValue(value = "") {
return String(value)
.trim()
.replace(/\s+/g, " ")
.replace(/\s*([(),/:])\s*/g, "$1")
.replace(/\s*\-\-\s*/g, "--");
}
function makeViolation(rule, policy, evidence) {
return {
rule,
policy,
severity: policy === "strict" ? "error" : "warn",
evidence,
};
}Swift:
func isDisallowedColor(_ value: String, allowedValues: [String]) -> Bool {
let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines)
if normalized.isEmpty { return false }
return !allowedValues.contains(normalized)
}Kotlin:
fun isDisallowedColor(value: String, allowedValues: List<String>): Boolean {
val normalized = value.trim()
if (normalized.isEmpty()) return false
return !allowedValues.contains(normalized)
}- Keep manifest small and per-surface.
- Avoid network fetches in hot paths.
- Emit violations with provenance for observability and incident correlation.
- Preserve the same rule codes and evidence semantics whether findings come from a compiled runtime consumer or from
validate --remote-url. - Consumer repos may wrap or visualize these findings, but canonical meaning stays in
interfacectl.