Skip to main content

Workspace Composition Kernel

This document is the canonical v0 spec for WCP-005: Write composition kernel spec v0.

It defines the headless composition kernel contract for @maya/assistant-composition.

Primary linked decisions:

  • WCP-ADR-001: responsibility split
  • WCP-ADR-002: headless composition package
  • WCP-ADR-010: explicit v0 composition policy
  • WCP-ADR-014: one pipeline, two facades
  • WCP-ADR-015: diagnostics are product inputs

Inherited constraints:

  • WCP-003 owns namespace grammar, route path grammar, renderer claim grammar, diagnostic codes, version field semantics, and diagnostic detail shapes.
  • WCP-004 owns trust, grant redaction, and security enforcement policy.
  • WCP-006 owns React render runtime behavior.

Scope

This spec owns:

  • composeWorkspace({ builtins, modules, policy })
  • CompositionPolicy
  • policySchemaVersion
  • contribution descriptor categories
  • module runtime activation rules after host resolution
  • composer ordering
  • ComposeWorkspaceResult
  • WorkspaceComposition structural output
  • fallback entries derived from resolved module runtime status
  • required render handle collection
  • kernel fixture map

This spec does not own:

  • React components, JSX, lazy imports, DOM, CSS, or render pack discovery
  • grant or runtime resolution
  • tenant policy, user authorization, backend health checks, fetch, storage, or localStorage
  • namespace grammar, diagnostic code registry, or version field semantics already owned by WCP-003
  • marketplace, remote ESM, iframe sandboxing, or runtime untrusted installs
  • local patching inside built-in pages

Shared Terms

These aliases are intentionally narrow. Their grammar and validation semantics come from WCP-003.

type CompositionSchemaVersion = '0.1';
type PolicySchemaVersion = '0.1';
type ModuleApiVersion = '0.1';
type RenderHandleVersion = '0.1';

type CanonicalContributionKey = string;
type RoutePath = string;
type RendererClaim = string;
type CompositionJsonPrimitive = string | number | boolean | null;
type CompositionJsonValue = CompositionJsonPrimitive | readonly CompositionJsonValue[] | CompositionJsonObject;
type CompositionJsonObject = { readonly [key: string]: CompositionJsonValue };

CanonicalContributionKey, RoutePath, and RendererClaim must be validated through WCP-003 rules before output is considered renderable.

The composer accepts opaque render handles as JSON values. It must validate the version field, but it never imports, resolves, or executes the handle.

type RenderHandle = {
renderHandleVersion: RenderHandleVersion;
kind: string;
id: string;
data?: CompositionJsonObject;
};

Render handle identity is the stable JSON serialization of renderHandleVersion, kind, id, and data with object keys sorted recursively.

Kernel Input

composeWorkspace() consumes built-ins, already resolved module instances, and explicit policy.

function composeWorkspace(input: ComposeWorkspaceInput): ComposeWorkspaceResult;

type ComposeWorkspaceInput = {
compositionSchemaVersion?: CompositionSchemaVersion;
builtins: BuiltinContributionSet[];
modules: ResolvedModuleInstance[];
policy?: CompositionPolicy;
supportedVersions?: SupportedCompositionVersions;
};

type SupportedCompositionVersions = {
compositionSchemaVersions: CompositionSchemaVersion[];
moduleApiVersions: ModuleApiVersion[];
renderHandleVersions: RenderHandleVersion[];
policySchemaVersions: PolicySchemaVersion[];
};

If supportedVersions is omitted, the composer supports only v0.1 schema versions.

If compositionSchemaVersion is omitted, the composer treats it as '0.1'.

Built-ins and modules enter one pipeline. Built-ins are not special merge cases.

If policy is omitted, the composer uses an empty v0 policy:

{ policySchemaVersion: '0.1', operations: [] }
composeWorkspace({
builtins: [claudeChatBuiltins, claudeProjectsBuiltins],
modules: [resolvedDataLabModule],
policy,
});

Contribution Sources

The composer normalizes every input into a CompositionSource.

type ContributionOwner = {
ownerKind: 'module' | 'builtin';
ownerId: string;
packageId?: string;
packageVersion?: string;
};

type BuiltinContributionSet = {
owner: ContributionOwner & { ownerKind: 'builtin' };
contributions: WorkspaceContributions;
};

type ResolvedModuleInstance = {
packageId: string;
packageVersion?: string;
moduleApiVersion: ModuleApiVersion;
instanceId: string;
runtime: ResolvedModuleRuntime;
contributions: WorkspaceContributions;
};

type ModuleRuntimeStatus =
| 'ready'
| 'readOnly'
| 'needsConfiguration'
| 'unauthorized'
| 'disabled'
| 'error';

type RuntimeAction = {
id: string;
label: string;
kind: 'configure' | 'authorize' | 'retry' | 'open' | 'custom';
data?: CompositionJsonObject;
};

type ResolvedModuleRuntime = {
instanceId: string;
status: ModuleRuntimeStatus;
grants: string[];
missing: string[];
actions: RuntimeAction[];
message?: string;
data?: CompositionJsonObject;
};

type CompositionSource = {
owner: ContributionOwner;
runtime?: ResolvedModuleRuntime;
contributions: WorkspaceContributions;
};

Built-ins use ownerKind: 'builtin' and reserved owner ids such as builtin-claude-workspace.

Modules use ownerKind: 'module' and ownerId equal to instanceId.

All canonical keys are derived by WCP-003 rules.

The host is responsible for redacting runtime.message, runtime.actions, and runtime.data before composition. The composer copies only the provided runtime summary; it does not perform security redaction.

Contribution Descriptors

The kernel accepts serializable descriptors only. Descriptors may contain render handles, but never React components or dynamic imports.

type WorkspaceContributions = {
routes?: RouteContribution[];
sidebarItems?: SidebarItemContribution[];
configurationSurfaces?: SurfaceContribution[];
integrationSurfaces?: SurfaceContribution[];
messageRenderers?: RendererContribution[];
messageAppenders?: MessageAppenderContribution[];
toolRenderers?: RendererContribution[];
activityRenderers?: RendererContribution[];
fallbackSurfaces?: FallbackSurfaceContribution[];
};

type ContributionKind =
| 'routes'
| 'sidebar-items'
| 'configuration-surfaces'
| 'integration-surfaces'
| 'message-renderers'
| 'message-appenders'
| 'tool-renderers'
| 'activity-renderers'
| 'fallback-surfaces';

type BaseContribution = {
id: string;
label?: string;
activeWhen?: ModuleRuntimeStatus[];
metadata?: CompositionJsonObject;
};

type RouteContribution = BaseContribution & {
path: RoutePath;
renderHandle: RenderHandle;
};

type SidebarItemContribution = BaseContribution & {
label: string;
section?: string;
routeKey?: CanonicalContributionKey;
};

type SurfaceContribution = BaseContribution & {
purpose: string;
renderHandle: RenderHandle;
};

type RendererContribution = BaseContribution & {
claim: RendererClaim;
renderHandle: RenderHandle;
};

type MessageAppenderContribution = BaseContribution & {
targetMessageType: string;
position: 'before' | 'after';
renderHandle: RenderHandle;
};

type FallbackSurfaceContribution = Omit<BaseContribution, 'activeWhen'> & {
status: Exclude<ModuleRuntimeStatus, 'ready'> | 'anyNonReady';
renderHandle: RenderHandle;
};

Descriptor requirements:

  • every descriptor has a local id
  • every descriptor becomes one canonical contribution key
  • route descriptors carry a declared route path and a render handle
  • sidebar descriptors target canonical route keys, not route paths
  • renderer descriptors carry explicit claims and render handles
  • message appenders carry target message type, position, and render handles
  • fallback surfaces carry a runtime status target and render handle

activeWhen is a UI activation hint for an already resolved runtime status. It is not a permission check and does not grant data access.

Activation defaults:

Contribution KindDefault activeWhen
routes['ready']
sidebar items['ready']
message renderers['ready']
message appenders['ready']
tool renderers['ready']
activity renderers['ready']
configuration surfaces['ready', 'readOnly', 'needsConfiguration']
integration surfaces['ready', 'readOnly', 'needsConfiguration', 'unauthorized', 'error']
fallback surfacesnot controlled by activeWhen; matched only by status

Runtime gating rules:

  • built-ins have no module runtime and are active unless policy removes them
  • built-in fallback surfaces with a concrete status are active product fallback entry points and record matchedStatus equal to their declared status
  • built-in fallback surfaces with anyNonReady do not materialize because there is no resolved module status to match
  • module contributions use explicit activeWhen when provided, otherwise the kind default
  • disabled omits every non-fallback module contribution regardless of activeWhen
  • fallback surfaces do not accept activeWhen
  • module fallback surfaces are active only when their status matches the resolved module status or anyNonReady
  • module anyNonReady fallback surfaces materialize exactly once for a non-ready module and record the actual runtime status in matchedStatus
  • module concrete fallback surfaces and anyNonReady fallback surfaces may both materialize for the same runtime status
  • active entries from readOnly modules carry read-only runtime metadata in the composed output

Composition Policy v0

type CompositionPolicy = {
policySchemaVersion: PolicySchemaVersion;
operations: CompositionPolicyOperation[];
};

type CompositionPolicyOperation =
| { id: string; kind: 'hide'; target: CanonicalContributionKey }
| { id: string; kind: 'reorder'; target: CanonicalContributionKey; position: 'first' | 'last' }
| { id: string; kind: 'reorder'; target: CanonicalContributionKey; position: 'before' | 'after'; anchor: CanonicalContributionKey }
| { id: string; kind: 'replace'; target: CanonicalContributionKey; replacement: CanonicalContributionKey }
| { id: string; kind: 'pathAlias'; target: CanonicalContributionKey; path: RoutePath }
| { id: string; kind: 'rendererOverride'; claim: RendererClaim; use: CanonicalContributionKey };

Policy rules:

  • every operation must have a stable id
  • policy operation IDs must be unique
  • an omitted policy object defaults to the empty v0 policy
  • a provided policy object with a missing or unsupported policySchemaVersion is a structural error
  • policy targets use canonical contribution keys, never package IDs, labels, or paths
  • rendererOverride targets a renderer claim and selects one canonical renderer contribution
  • policy array order does not resolve conflicts
  • invalid policy produces structured diagnostics

Policy Conflict Rules

The composer rejects ambiguous policy instead of using array order as a tie-breaker.

Invalid v0 policy conflicts:

  • duplicate operation IDs
  • more than one hide operation for the same target
  • more than one reorder operation for the same target
  • more than one replace operation for the same target
  • more than one pathAlias operation for the same route target
  • more than one rendererOverride operation for the same renderer claim
  • hide and replace targeting the same contribution
  • replace operations that create a replacement cycle
  • reorder operations that create an ordering cycle

Policy references are validated against the active candidate graph produced after runtime gating and fallback materialization, before any policy operation is applied.

A policy operation cannot target or select a contribution that is inactive in that candidate graph. The composer also simulates policy removals and replacements before applying them; any policy-created dangling reference is invalid policy. Operations that would reference a contribution removed or replaced by the same policy are invalid.

Policy conflicts produce policy diagnostics with a reason that identifies the rejected rule. WCP-003 owns the concrete diagnostic code.

No policy operation is applied until all policy operation IDs, references, versions, predicted cross-references, and conflict rules validate. If policy validation blocks composition, policyEffects is empty.

Structural Diagnostic Reasons

Input shape validation produces structure diagnostics before any policy effects are applied.

Stable structure diagnostic reasons:

ReasonApplies Whenfieldexpectedreceived
invalid-input-shapetop-level compose input is not an objectinputComposeWorkspaceInputreceived type
invalid-collectionbuiltins, modules, or a contribution collection is not an arrayfield patharrayreceived type
missing-required-fieldrequired descriptor or source field is absentfield pathexpected field typeundefined
missing-runtimemodule instance has no resolved runtimemodules[].runtimeResolvedModuleRuntimeundefined
invalid-runtime-statusruntime status is not a v0 ModuleRuntimeStatus valueruntime.statussorted status valuesreceived value

When the malformed value belongs to a module or contribution, the diagnostic subject includes the best available owner and contribution fields. When the malformed value is top-level input, the diagnostic subject uses ownerKind: 'host'.

Policy Diagnostic Reasons

Policy validation produces policy diagnostics before any policy effects are applied.

Policy diagnostic details use these stable reason codes:

ReasonApplies WhenpolicyIdtarget
duplicate-operation-idtwo or more operations share an IDduplicated operation IDoperation:<id>
duplicate-hide-targetmore than one hide targets the same contributionsecond operation ID by lexical ordercanonical contribution key
duplicate-reorder-targetmore than one reorder targets the same contributionsecond operation ID by lexical ordercanonical contribution key
duplicate-replace-targetmore than one replace targets the same contributionsecond operation ID by lexical ordercanonical contribution key
duplicate-path-alias-targetmore than one pathAlias targets the same routesecond operation ID by lexical ordercanonical contribution key
duplicate-renderer-override-claimmore than one rendererOverride targets the same renderer claimsecond operation ID by lexical orderrenderer claim
hide-replace-same-targetone contribution is both hidden and replacedfirst operation ID by lexical ordercanonical contribution key
replace-cyclereplacement graph contains a cyclefirst operation ID by lexical order in the cyclecanonical contribution key
reorder-cyclereorder graph contains a cyclefirst operation ID by lexical order in the cyclecanonical contribution key
missing-targetpolicy target does not exist in declarationsoperation IDcanonical contribution key or renderer claim
inactive-targetpolicy target exists but is inactive after runtime gatingoperation IDcanonical contribution key or renderer claim
missing-userendererOverride.use does not exist in declarationsoperation IDrenderer claim
inactive-userendererOverride.use exists but is inactive after runtime gatingoperation IDrenderer claim
invalid-use-kindrendererOverride.use is not a renderer contribution for the targeted claimoperation IDrenderer claim
missing-render-handlereplace.replacement or rendererOverride.use selects a renderable contribution without an opaque render handleoperation IDcanonical contribution key or renderer claim
invalid-kindoperation targets an unsupported contribution kindoperation IDcanonical contribution key or renderer claim
policy-created-dangling-referencesimulated policy output creates a broken cross-referenceoperation ID that caused removal or replacementdangling canonical contribution key
default-renderer-protectedpolicy attempts to override default user or assistant text renderingoperation IDrenderer claim

For rendererOverride, target is the renderer claim and policyId is the override operation ID. Invalid use failures include details.use with the requested canonical renderer key. If the requested renderer exists, the diagnostic subjects include it as a contribution subject.

Policy Semantics

hide

hide removes exactly one active contribution.

It does not cascade. If a sidebar item points at a hidden route, the composer emits a policy diagnostic instead of silently hiding the sidebar item.

reorder

reorder applies only to ordered collections:

  • sidebar items
  • configuration surfaces
  • integration surfaces
  • message appenders
  • fallback surface display groups

The composer resolves all reorder constraints with a deterministic topological sort. Canonical key order is the tie-breaker.

replace

replace suppresses target and promotes replacement.

Rules:

  • both keys must exist
  • both contributions must be active before replacement
  • both contributions must have the same contribution kind
  • replacement keeps its own owner, render handles, label, route path, claims, and metadata
  • replace does not copy route context, grants, labels, or render handles from the target
  • replace is descriptor-level ownership replacement, not built-in page patching

pathAlias

pathAlias applies only to routes/* contributions.

It changes the effective route path used for conflict checks and final route table output. It does not change the canonical contribution key or declared route path.

rendererOverride

rendererOverride resolves duplicate custom message, tool, or activity renderer claims by selecting one active renderer contribution.

It cannot override ordinary user or assistant text rendering, cannot select an inactive contribution, and cannot create a missing renderer.

Output rules:

  • final messageRenderers, toolRenderers, and activityRenderers contain only the effective renderer for each claim
  • overridden renderer contributions are omitted from renderer arrays, rendererClaims, and requiredRenderHandles
  • the selected composed renderer has the override policy ID in policyIds
  • the rendererOverride policy effect records the selected renderer key and every overridden renderer key
  • overridden keys are sorted by canonical contribution key

Composer Algorithm

The kernel follows this deterministic order:

  1. Validate kernel input shape and supported schema versions, including compositionSchemaVersion and any provided policySchemaVersion.
  2. Normalize built-ins and modules into CompositionSource[].
  3. Validate module runtime status is present for every module.
  4. Derive canonical keys for all declared contributions.
  5. Validate namespace, local IDs, route paths, renderer claims, and version fields through WCP-003 rules.
  6. Normalize omitted policy to an empty v0 policy.
  7. Gate module contributions according to resolved runtime status and descriptor activation.
  8. Materialize fallback-surface candidates for non-ready statuses.
  9. Validate policy operation IDs, active policy references, predicted cross-references, and policy conflict rules.
  10. Apply hide and replace.
  11. Apply pathAlias.
  12. Detect effective route path conflicts.
  13. Resolve renderer claims with rendererOverride.
  14. Detect unresolved renderer claim conflicts.
  15. Validate non-policy cross-references, including sidebar route targets and replacement targets.
  16. Apply reorder constraints.
  17. Collect required render handles.
  18. Emit WorkspaceComposition, diagnostics, and policy effects.

Structural errors produce ok: false and no renderable composition.

Warnings and info diagnostics may coexist with ok: true.

Runtime Status And Fallbacks

The host resolves module runtime before composition. The composer only adapts already resolved status into structure.

Runtime StatusContribution Behavior
readynormal contributions active; fallback surfaces inactive
readOnlycontributions active only when descriptor activation includes readOnly; active entries carry read-only runtime metadata
needsConfigurationcontributions active only when descriptor activation includes needsConfiguration; matching fallback surfaces are active
unauthorizedcontributions active only when descriptor activation includes unauthorized; normal route and renderer defaults exclude this status
disabledevery non-fallback module contribution omitted; matching fallback surfaces are active
errorcontributions active only when descriptor activation includes error; integration surfaces are active by default and matching fallback surfaces receive the host-redacted runtime summary

Invalid descriptors still produce diagnostics even when a module is disabled or unauthorized. Runtime status must not become a way to hide broken declarations from tests.

Fallback surfaces are explicit contributions such as:

data-lab-prod:fallback-surfaces/needs-configuration

Built-in product fallback surfaces are explicit contributions too, for example:

builtin-claude-workspace:fallback-surfaces/unauthorized

The composer never invents fallback UI.

Result Object

type ComposeWorkspaceResult =
| {
ok: true;
composition: WorkspaceComposition;
diagnostics: WorkspaceDiagnostic[];
policyEffects: CompositionPolicyEffect[];
}
| {
ok: false;
composition: null;
diagnostics: WorkspaceDiagnostic[];
policyEffects: CompositionPolicyEffect[];
};
type WorkspaceComposition = {
compositionSchemaVersion: CompositionSchemaVersion;
policySchemaVersion: PolicySchemaVersion;
owners: ContributionOwner[];
modules: WorkspaceCompositionModule[];
routes: ComposedRoute[];
sidebarItems: ComposedSidebarItem[];
configurationSurfaces: ComposedSurface[];
integrationSurfaces: ComposedSurface[];
messageRenderers: ComposedRenderer[];
messageAppenders: ComposedMessageAppender[];
toolRenderers: ComposedRenderer[];
activityRenderers: ComposedRenderer[];
fallbackSurfaces: ComposedFallbackSurface[];
rendererClaims: ComposedRendererClaimBinding[];
requiredRenderHandles: RequiredRenderHandle[];
appliedPolicy: CompositionPolicyEffect[];
diagnostics: WorkspaceDiagnostic[];
};

diagnostics is included in both the result and composition so hosts can inspect failures before render and product UIs can show nonfatal diagnostics after render.

policyEffects and composition.appliedPolicy use the same schema. When ok: true, composition.appliedPolicy equals result.policyEffects. When ok: false, composition is null and result.policyEffects contains only effects that were applied before the blocking diagnostic.

Composed Output Types

type WorkspaceCompositionModule = {
owner: ContributionOwner & { ownerKind: 'module' };
instanceId: string;
packageId: string;
packageVersion?: string;
moduleApiVersion: ModuleApiVersion;
runtime: ComposedRuntimeSummary;
};

type ComposedRuntimeSummary = {
status: ModuleRuntimeStatus;
grants: string[];
missing: string[];
actions: RuntimeAction[];
message?: string;
data?: CompositionJsonObject;
readOnly: boolean;
};

type BaseComposedContribution = {
key: CanonicalContributionKey;
kind: ContributionKind;
owner: ContributionOwner;
localId: string;
label?: string;
metadata?: CompositionJsonObject;
runtime?: ComposedRuntimeSummary;
policyIds: string[];
};

type ComposedRoute = BaseComposedContribution & {
kind: 'routes';
declaredPath: RoutePath;
effectivePath: RoutePath;
renderHandle: RenderHandle;
};

type ComposedSidebarItem = BaseComposedContribution & {
kind: 'sidebar-items';
label: string;
section?: string;
routeKey?: CanonicalContributionKey;
};

type ComposedSurface = BaseComposedContribution & {
kind: 'configuration-surfaces' | 'integration-surfaces';
purpose: string;
renderHandle: RenderHandle;
};

type ComposedRenderer = BaseComposedContribution & {
kind: 'message-renderers' | 'tool-renderers' | 'activity-renderers';
claim: RendererClaim;
renderHandle: RenderHandle;
};

type ComposedMessageAppender = BaseComposedContribution & {
kind: 'message-appenders';
targetMessageType: string;
position: 'before' | 'after';
renderHandle: RenderHandle;
};

type ComposedFallbackSurface = BaseComposedContribution & {
kind: 'fallback-surfaces';
status: Exclude<ModuleRuntimeStatus, 'ready'> | 'anyNonReady';
matchedStatus: Exclude<ModuleRuntimeStatus, 'ready'>;
renderHandle: RenderHandle;
};

type ComposedRendererClaimBinding = {
claim: RendererClaim;
rendererKind: 'message' | 'tool' | 'activity';
rendererKey: CanonicalContributionKey;
owner: ContributionOwner;
renderHandle: RenderHandle;
policyId?: string;
};

type RequiredRenderHandle = {
owner: ContributionOwner;
handle: RenderHandle;
usedBy: CanonicalContributionKey[];
};

runtime is attached only to module-owned composed contributions. Built-in contributions omit it.

policyIds is sorted and contains the policy operations that affected the composed contribution. Unaffected entries use an empty array.

Policy Effects

type CompositionPolicyEffect =
| { policyId: string; kind: 'hide'; target: CanonicalContributionKey; removed: CanonicalContributionKey }
| { policyId: string; kind: 'replace'; target: CanonicalContributionKey; replacement: CanonicalContributionKey }
| { policyId: string; kind: 'pathAlias'; target: CanonicalContributionKey; originalPath: RoutePath; effectivePath: RoutePath }
| { policyId: string; kind: 'rendererOverride'; claim: RendererClaim; selected: CanonicalContributionKey; overridden: CanonicalContributionKey[] }
| { policyId: string; kind: 'reorder'; target: CanonicalContributionKey; collection: ContributionKind };

Invalid policy operations never produce policy effects.

If later non-policy validation fails after valid policy effects have been applied, the failed result includes those effects for debugging.

Ordering Contract

All output arrays are deterministic.

Sort helpers:

  • owner key: ${ownerKind}:${ownerId}
  • contribution key: canonical contribution key
  • handle key: ${ownerKey}:${stableRenderHandleIdentity}
  • status order: readOnly, needsConfiguration, unauthorized, disabled, error
  • fallback declared status order: concrete status before anyNonReady; concrete statuses use status order
  • severity order: error, warning, info
  • policy kind order: hide, replace, pathAlias, rendererOverride, reorder
  • diagnostic stable key: severity, category, diagnostic code string, normalized details JSON, normalized subjects JSON, docs anchor

Output ordering:

  • owners: owner key
  • modules: instanceId
  • routes: effectivePath, then contribution key
  • sidebarItems: reorder result, then contribution key
  • configurationSurfaces: reorder result, then contribution key
  • integrationSurfaces: reorder result, then contribution key
  • messageRenderers: claim, then contribution key
  • messageAppenders: reorder result, then targetMessageType, position, contribution key
  • toolRenderers: claim, then contribution key
  • activityRenderers: claim, then contribution key
  • fallbackSurfaces: matched status order, fallback declared status order, reorder result, then contribution key
  • rendererClaims: claim, then rendererKey
  • requiredRenderHandles: handle key
  • appliedPolicy and policyEffects: policy kind order, then target or claim, then policyId
  • diagnostics: diagnostic stable key; the human message is excluded from sorting

The policy array order is retained only as authoring metadata. It never resolves merge conflicts.

Required Render Handles

The kernel collects opaque render handles from active output entries.

Rules:

  • no handle is executed by the kernel
  • duplicate handle usage is allowed
  • requiredRenderHandles is a unique manifest
  • each manifest entry is deduped by owner key plus render handle identity
  • usedBy lists every active composed contribution key using that owner and handle, sorted by contribution key
  • missing handle providers are checked by render runtime, not by the kernel
  • only active and effective output surfaces contribute required handles
  • hidden, replaced, disabled, and omitted contributions do not contribute required handles
  • requiredRenderHandles is deterministic and sorted by handle key

Golden Fixture Contract

Kernel fixtures are JSON files consumed by WCP-011 and WCP-013 tests.

Each fixture has this shape:

type KernelFixture = {
id: string;
input: ComposeWorkspaceInput;
expect: {
ok: boolean;
composition: WorkspaceComposition | null;
diagnostics: WorkspaceDiagnostic[];
policyEffects: CompositionPolicyEffect[];
};
};

Core WCP-011 fixtures assert complete JSON for successful compositions. WCP-013 product-default fixtures use focused composition snapshots so the descriptor inventory can evolve without duplicating every composed field in the architecture doc. Diagnostic fixtures must assert composition: null and the WCP-003 diagnostic object including the registered code, subjects, details, decision IDs, and docs anchor.

The minimum happy-path fixture must include:

{
"id": "fixture-happy-single-module",
"input": {
"builtins": [],
"modules": [
{
"packageId": "@maya/data-lab-module",
"packageVersion": "0.1.0",
"moduleApiVersion": "0.1",
"instanceId": "data-lab-prod",
"runtime": {
"instanceId": "data-lab-prod",
"status": "ready",
"grants": ["dataLab.query"],
"missing": [],
"actions": []
},
"contributions": {
"routes": [
{
"id": "home",
"path": "/data-lab",
"renderHandle": {
"renderHandleVersion": "0.1",
"kind": "route",
"id": "data-lab.home"
}
}
],
"sidebarItems": [
{
"id": "home",
"label": "Data Lab",
"routeKey": "data-lab-prod:routes/home"
}
]
}
}
]
},
"expect": {
"ok": true,
"composition": {
"compositionSchemaVersion": "0.1",
"policySchemaVersion": "0.1",
"owners": [
{
"ownerKind": "module",
"ownerId": "data-lab-prod",
"packageId": "@maya/data-lab-module",
"packageVersion": "0.1.0"
}
],
"modules": [
{
"owner": {
"ownerKind": "module",
"ownerId": "data-lab-prod",
"packageId": "@maya/data-lab-module",
"packageVersion": "0.1.0"
},
"instanceId": "data-lab-prod",
"packageId": "@maya/data-lab-module",
"packageVersion": "0.1.0",
"moduleApiVersion": "0.1",
"runtime": {
"status": "ready",
"grants": ["dataLab.query"],
"missing": [],
"actions": [],
"readOnly": false
}
}
],
"routes": [
{
"key": "data-lab-prod:routes/home",
"kind": "routes",
"owner": {
"ownerKind": "module",
"ownerId": "data-lab-prod",
"packageId": "@maya/data-lab-module",
"packageVersion": "0.1.0"
},
"localId": "home",
"runtime": {
"status": "ready",
"grants": ["dataLab.query"],
"missing": [],
"actions": [],
"readOnly": false
},
"policyIds": [],
"declaredPath": "/data-lab",
"effectivePath": "/data-lab",
"renderHandle": {
"renderHandleVersion": "0.1",
"kind": "route",
"id": "data-lab.home"
}
}
],
"sidebarItems": [
{
"key": "data-lab-prod:sidebar-items/home",
"kind": "sidebar-items",
"owner": {
"ownerKind": "module",
"ownerId": "data-lab-prod",
"packageId": "@maya/data-lab-module",
"packageVersion": "0.1.0"
},
"localId": "home",
"label": "Data Lab",
"routeKey": "data-lab-prod:routes/home",
"runtime": {
"status": "ready",
"grants": ["dataLab.query"],
"missing": [],
"actions": [],
"readOnly": false
},
"policyIds": []
}
],
"configurationSurfaces": [],
"integrationSurfaces": [],
"messageRenderers": [],
"messageAppenders": [],
"toolRenderers": [],
"activityRenderers": [],
"fallbackSurfaces": [],
"rendererClaims": [],
"requiredRenderHandles": [
{
"owner": {
"ownerKind": "module",
"ownerId": "data-lab-prod",
"packageId": "@maya/data-lab-module",
"packageVersion": "0.1.0"
},
"handle": {
"renderHandleVersion": "0.1",
"kind": "route",
"id": "data-lab.home"
},
"usedBy": ["data-lab-prod:routes/home"]
}
],
"appliedPolicy": [],
"diagnostics": []
},
"diagnostics": [],
"policyEffects": []
}
}

Golden Fixture Map

This map names kernel assertions that WCP-005 adds on top of the WCP-003 golden fixture inventory. WCP-003 owns the cross-platform inventory and expected diagnostic codes. WCP-005 may add kernel-specific fixture IDs for composition behavior, but it must not remove or rename WCP-003 fixture IDs.

Fixture IDKernel Assertion
fixture-happy-single-modulereturns ok: true with one module route, one sidebar item, and one required render handle
fixture-happy-multi-instancesame package with two instance IDs composes independently
fixture-same-local-id-across-instancessame local IDs across instances produce distinct canonical keys
fixture-duplicate-instance-idduplicate instance ID blocks composition
fixture-duplicate-canonical-keyduplicate canonical key blocks composition
fixture-invalid-local-idinvalid local ID blocks composition
fixture-duplicate-route-pathduplicate effective route path blocks composition
fixture-invalid-route-pathinvalid route path blocks composition
fixture-route-path-trailing-slash-rejectednon-root trailing slash is invalid
fixture-route-path-aliasexplicit path alias changes effective route path
fixture-route-path-alias-creates-conflictalias-created route conflict blocks composition
fixture-duplicate-message-renderer-claimduplicate custom message renderer claim blocks composition
fixture-duplicate-tool-renderer-claimduplicate tool renderer claim blocks composition
fixture-duplicate-activity-renderer-claimduplicate activity renderer claim blocks composition
fixture-message-appenders-do-not-conflictmultiple appenders targeting same message and position order deterministically
fixture-renderer-overrideexplicit renderer override selects one renderer owner
fixture-invalid-policy-targetinvalid policy target emits policy diagnostic and blocks composition
fixture-unsupported-policy-schema-versionunsupported policy schema version blocks composition
fixture-policy-hide-routehide removes only the targeted route and reports dangling references
fixture-policy-reorder-sidebarreorder constraints sort sidebar deterministically
fixture-policy-replace-descriptorreplace swaps active descriptor ownership without copying target metadata
fixture-policy-conflictduplicate or conflicting policy operations block composition before effects apply
fixture-policy-reorder-cyclereorder cycles emit policy diagnostics
fixture-policy-hide-excludes-required-handlehidden surfaces do not contribute required handles
fixture-policy-replace-updates-required-handlesreplacement updates required handle manifest
fixture-runtime-readyready modules contribute normally
fixture-runtime-read-onlyread-only modules keep allowed surfaces and read-only metadata
fixture-runtime-needs-configurationneeds-configuration modules show config and fallback surfaces only
fixture-runtime-unauthorizedunauthorized modules omit default route and renderer contributions and expose matching fallback surfaces
fixture-runtime-disableddisabled modules omit normal contributions and expose matching fallback surfaces
fixture-runtime-errorerrored modules omit default route and renderer contributions while exposing default integration surfaces and matching fallback surfaces
fixture-kernel-builtins-and-modules-same-pipelinebuilt-ins and modules use identical merge, policy, route, renderer, and diagnostic paths
fixture-claude-builtins-default-compositiondefault Claude built-ins compose without module-specific special cases
fixture-builtin-module-route-conflict-diagnosticbuilt-in/module route conflicts expose both owners in diagnostics
fixture-builtin-module-renderer-conflict-diagnosticbuilt-in/module renderer conflicts expose both owners in diagnostics
fixture-builtin-owner-shape-diagnosticsmalformed built-in owner shape is rejected before composition
fixture-kernel-required-render-handlesrequired handles are deterministic and include all active render surfaces
fixture-required-render-handles-runtime-fallbackselected fallback surfaces contribute required handles

Duplicate Truth Boundary

This document must not redefine WCP-003 grammar, diagnostic code registry, or version field semantics.

It may reference WCP-003 fixture IDs and diagnostic categories, but it must not copy the diagnostic registry.

WCP-005 owns composition order, policy semantics, result shape, and fallback structural behavior only.