export type SurfaceDemandKind = 'route' | 'control' | 'request'; export interface SurfaceCandidate { kind: SurfaceDemandKind; /** Stable identity — the same surface must produce the same id across runs. */ id: string; /** * The host's own identity for the surface, carried through untouched: the * route id discovery reported, for a route candidate. A diagnostic repeats * it so the host joins back to a route name or a source path by key rather * than by parsing the candidate id. */ sourceId?: string; /** Frame the candidate was found in. Absent for route candidates. */ frameId?: string; /** * Frame-independent form of the same surface, so one verdict can answer it * everywhere it appears. * * An application shell repeats: the same send button, the same sidebar toggle, * the same navigation tab are in most frames. Demanding a per-frame verdict * for each would mean writing the same product fact tens of times over, which * is the kind of tax that gets a gate switched off rather than answered. A * frame-scoped verdict still wins when the answer genuinely differs there. */ matchId?: string; /** Description of what was found, for the panel and the gate message. */ detail: string; } /** * The host's answer. `covered` names the declaration that handles the surface * so a stale answer breaks loudly; `excluded` carries the reason instead. */ export type SurfaceVerdict = { verdict: 'covered'; by: string; } | { verdict: 'excluded'; reason: string; }; export type SurfaceClassification = SurfaceVerdict & { id: string; }; /** * What went wrong, as a value a gate can branch on. * * - `duplicate-candidate` — an enumerator reported one surface twice. * - `stale-verdict` — a verdict names a surface no enumerator reports. * - `duplicate-verdict` — two verdicts answer one surface. * - `exclusion-reason` — an exclusion's reason is below the floor. * - `empty-target` — a covered verdict names nothing. * - `unknown-target` — a covered verdict names a declaration that does not * exist among the known targets the caller supplied. * - `unclassified` — a surface has no verdict at all. */ export type SurfaceDemandDiagnosticCode = 'duplicate-candidate' | 'stale-verdict' | 'duplicate-verdict' | 'exclusion-reason' | 'empty-target' | 'unknown-target' | 'unclassified'; export interface SurfaceDemandDiagnostic { severity: 'error' | 'warning'; code: SurfaceDemandDiagnosticCode; kind: SurfaceDemandKind | 'unknown'; candidateId: string; /** The candidate's host-side identity, when the enumerator carried one. */ sourceId?: string; /** Frame the candidate was found in, when it was found in one. */ frameId?: string; /** The coverage target the verdict named, for target diagnostics. */ target?: string; message: string; } export interface SurfaceDemandOptions { /** * Every declaration a `covered` verdict may name. * * Without it a verdict passes as long as it names something, so a screen * that was deleted keeps covering its route through the verdict that named * it. With it, `by` must be one of these ids; `declaredCoverageTargets` * builds the set from the host's declarations. */ knownTargets?: readonly string[] | ReadonlySet; } /** Reasons must say something; the scenario ledger uses the same floor. */ export declare const MIN_SURFACE_REASON_LENGTH = 10; /** * The ids a `covered` verdict may name, built from what the host declared. * * A screen is named by its own id — the same string a coverage claim's * `screenId` uses, prefix and all. A registered component is named * `component:`, since a bare component name and a screen id could * collide. Anything else the host counts as a coverage target (an * interactive axis, a card slot) is passed through `targets` as written. A * host with its own vocabulary builds the set itself and hands it to * `knownTargets` directly. */ export interface CoverageTargetDeclarations { /** Declared screens — the cases handed to `createDesignScreenCollection` qualify. */ screens?: readonly { id: string; }[]; /** Registered component names, or the registry object itself. */ components?: readonly string[] | ReadonlySet | Readonly>; /** Further target ids, verbatim. */ targets?: readonly string[]; } export declare function declaredCoverageTargets(declarations: CoverageTargetDeclarations): Set; /** * Compares what the editor found against what the host answered. * * Errors, not warnings, on every branch: a warning would let the build stay * green while a surface goes unreviewed, which is the state this module exists * to end. Callers that need a migration window hold a baseline of known ids * rather than downgrading the severity. * * Pass `knownTargets` to also require that every `covered` verdict names a * declaration that exists. Without it the target is only required to be * nonempty, which is what let a deleted screen keep covering its route. */ export declare function validateSurfaceDemands(candidates: readonly SurfaceCandidate[], classifications: readonly SurfaceClassification[], options?: SurfaceDemandOptions): SurfaceDemandDiagnostic[]; /** * Counts by outcome. Four buckets and nothing moves between them: a surface is * covered only when a valid verdict names a valid target, an exclusion is * visible in its own count rather than folded into coverage, and a verdict * that failed validation counts as invalid — never as covered. */ export interface SurfaceCoverageSummary { total: number; covered: number; excluded: number; /** Surfaces whose verdict failed validation. */ invalid: number; unclassified: number; excludedIds: string[]; invalidIds: string[]; unclassifiedIds: string[]; } export declare function summarizeSurfaceDemands(candidates: readonly SurfaceCandidate[], classifications: readonly SurfaceClassification[], options?: SurfaceDemandOptions): SurfaceCoverageSummary; /** Candidates with no verdict yet — what the panel shows on a frame. */ export declare function unclassifiedSurfaces(candidates: readonly SurfaceCandidate[], classifications: readonly SurfaceClassification[]): SurfaceCandidate[]; /** * A route as the host's discovery reported it. * * `path` is the canonical pattern in whatever syntax the host normalized to * (`/items/:id`, or `/items/[id]` left as the framework wrote it); `id` is the * host's stable identity for it, which the candidate carries as `sourceId`. * Either alone is enough: a route with only an id is treated as a path. */ export interface DiscoveredRoute { path?: string; id?: string; } /** * What a declared screen says about source routes. * * `route` is the concrete address the screen boots; it represents a * discovered pattern when it satisfies that pattern segment for segment. * `coverageRoutes` is the explicit form: the source routes this screen stands * for, matched by the discovered route's `id` or by its normalized path, * exactly. It is how a screen that renders a registered component * (`componentName`) covers a route without inventing a navigation URL, and * how a screen booted at one concrete address claims a dynamic pattern * without the core guessing at parameters. A `DesignScreenCase` satisfies * this shape as written. */ export interface DeclaredScreenRoute { id: string; route?: string; componentName?: string; coverageRoutes?: readonly string[]; } export interface RouteEnumerationInput { /** Routes discovered in the codebase. */ routes: readonly DiscoveredRoute[]; /** Screen cases the host declared, with the route or component each one renders. */ declaredScreens: readonly DeclaredScreenRoute[]; /** * Registered component names, or the registry itself. When given, a screen * whose `componentName` is not among them represents nothing: the fixture * it names is gone, and the routes it mapped stay unrepresented. */ registeredComponents?: readonly string[] | ReadonlySet | Readonly>; } /** How a declared screen came to represent a discovered route. */ export type RouteRepresentationKind = /** The screen's concrete `route` satisfies the discovered pattern. */ 'preview' /** The screen lists the route in `coverageRoutes` and boots a route of its own. */ | 'mapping' /** The screen lists the route in `coverageRoutes` and renders a registered component. */ | 'fixture'; export interface RouteRepresentation { /** The candidate id the route would have had, `route:`. */ candidateId: string; path: string; sourceId?: string; screenId: string; via: RouteRepresentationKind; componentName?: string; } /** * - `unknown-route` — a `coverageRoutes` entry matches no discovered route: * the source route was removed or renamed, and the mapping is stale. * - `unregistered-component` — the screen's `componentName` is not among * `registeredComponents`: its fixture registration is gone. * - `empty-mapping` — a `coverageRoutes` entry is blank. * - `missing-render-source` — a mapped screen has neither a nonempty preview * route nor a component fixture to render. */ export type RouteCoverageDiagnosticCode = 'unknown-route' | 'unregistered-component' | 'empty-mapping' | 'missing-render-source'; export interface RouteCoverageDiagnostic { severity: 'error'; code: RouteCoverageDiagnosticCode; screenId: string; /** The `coverageRoutes` entry as the host wrote it. */ route?: string; componentName?: string; message: string; } export interface RouteCoverageAudit { /** Discovered routes nothing represents — each still owes a verdict. */ candidates: SurfaceCandidate[]; /** Every (route, screen) pair that holds, so a host can join routes to screens. */ represented: RouteRepresentation[]; /** Mappings that name something missing. Separate from the candidates on purpose. */ diagnostics: RouteCoverageDiagnostic[]; } /** * Which declared screens represent each discovered route, and which routes * nothing represents. * * A discovered route with no screen case is the plainest hole there is: the * codebase can render it and the storyboard never shows it. Resolution today * is silent — `resolveStoryboardScreenCases` maps declared pages and never * looks at the discovered route list. * * Two ways a screen represents a route, checked in this order: * * 1. Explicitly, by listing the route in `coverageRoutes`. The entry must * equal the discovered route's `id` or normalize to its path — no * parameter substitution, no prefix. A screen that renders a registered * component covers the route this way without a navigation URL; a screen * booted at `/items/fixture-1` covers `/items/[id]` this way without the * core parsing bracket syntax. * 2. Implicitly, by booting a concrete `route` that satisfies the pattern * segment for segment (`:param` and `*` accept any nonempty segment). * * A mapping that names a missing route or an unregistered component is a * diagnostic, not a representation: a stale mapping must not keep covering, * and the route it named is reported among the candidates as well. */ export declare function auditRouteCoverage(input: RouteEnumerationInput): RouteCoverageAudit; /** The unrepresented routes of `auditRouteCoverage`, for callers that only need the holes. */ export declare function enumerateRouteCandidates(input: RouteEnumerationInput): SurfaceCandidate[]; export interface ControlNodeLike { id: string; domTag?: string; domAttributes?: Readonly>; hidden?: boolean; } /** * Enumerates the operable elements of one frame's imported tree. * * The host declares the axes it thought of, which is why axis coverage has * always been a memory exercise. The tree is already imported for editing, so * the elements are right there — asking about each one makes a forgotten axis * impossible rather than unlikely. * * Elements with no identifying attribute are skipped: there would be no stable * id to attach a verdict to, and a positional id would churn on every layout * edit. Hosts that want those asked about mark them. */ export declare function enumerateControlCandidates(frameId: string, nodes: readonly ControlNodeLike[]): SurfaceCandidate[]; export interface ObservedRequest { /** Absolute or app-relative URL as the page requested it. */ url: string; method?: string; } /** * Collapses a URL to one endpoint shape. * * Numeric and uuid path segments become `:id` so a list of fixture rows asks * one question instead of one per row, and the query string is dropped for the * same reason. */ export declare function requestEndpointShape(url: string): string; /** * Every request a frame makes is a state the product can be in and usually is * not shown: the failure, the empty result, the slow wait. Declaring one used * to mean inventing a host flag and a mock branch per state, so hosts declared * almost none. The core now supplies the mechanism * (`StoryboardEnvironment.network`), which is what makes asking fair. */ export declare function enumerateRequestCandidates(frameId: string, requests: readonly ObservedRequest[]): SurfaceCandidate[]; export interface FrameSurfaceDemands { candidates: SurfaceCandidate[]; unclassified: SurfaceCandidate[]; } /** One editor's declared surface classifications and the requests its frames observed. */ export declare function createSurfaceDemands(): { setSurfaceClassifications: (classifications: readonly SurfaceClassification[] | undefined) => void; getSurfaceClassifications: () => readonly SurfaceClassification[]; setObservedRequests: (frameId: string, requests: readonly ObservedRequest[]) => void; getObservedRequests: (frameId: string) => readonly ObservedRequest[]; frameSurfaceDemands: (frameId: string, root?: (ControlNodeLike & { children?: readonly ControlNodeLike[]; }) | null, classifications?: readonly SurfaceClassification[]) => FrameSurfaceDemands; }; export type SurfaceDemands = ReturnType;