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 splitWCP-ADR-002: headless composition packageWCP-ADR-010: explicit v0 composition policyWCP-ADR-014: one pipeline, two facadesWCP-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 })CompositionPolicypolicySchemaVersion- contribution descriptor categories
- module runtime activation rules after host resolution
- composer ordering
ComposeWorkspaceResultWorkspaceCompositionstructural 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 Kind | Default 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 surfaces | not 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
matchedStatusequal to their declared status - built-in fallback surfaces with
anyNonReadydo not materialize because there is no resolved module status to match - module contributions use explicit
activeWhenwhen provided, otherwise the kind default disabledomits every non-fallback module contribution regardless ofactiveWhen- fallback surfaces do not accept
activeWhen - module fallback surfaces are active only when their
statusmatches the resolved module status oranyNonReady - module
anyNonReadyfallback surfaces materialize exactly once for a non-ready module and record the actual runtime status inmatchedStatus - module concrete fallback surfaces and
anyNonReadyfallback surfaces may both materialize for the same runtime status - active entries from
readOnlymodules 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
policySchemaVersionis a structural error - policy targets use canonical contribution keys, never package IDs, labels, or paths
rendererOverridetargets 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
hideoperation for the same target - more than one
reorderoperation for the same target - more than one
replaceoperation for the same target - more than one
pathAliasoperation for the same route target - more than one
rendererOverrideoperation for the same renderer claim hideandreplacetargeting the same contributionreplaceoperations that create a replacement cyclereorderoperations 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:
| Reason | Applies When | field | expected | received |
|---|---|---|---|---|
invalid-input-shape | top-level compose input is not an object | input | ComposeWorkspaceInput | received type |
invalid-collection | builtins, modules, or a contribution collection is not an array | field path | array | received type |
missing-required-field | required descriptor or source field is absent | field path | expected field type | undefined |
missing-runtime | module instance has no resolved runtime | modules[].runtime | ResolvedModuleRuntime | undefined |
invalid-runtime-status | runtime status is not a v0 ModuleRuntimeStatus value | runtime.status | sorted status values | received 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:
| Reason | Applies When | policyId | target |
|---|---|---|---|
duplicate-operation-id | two or more operations share an ID | duplicated operation ID | operation:<id> |
duplicate-hide-target | more than one hide targets the same contribution | second operation ID by lexical order | canonical contribution key |
duplicate-reorder-target | more than one reorder targets the same contribution | second operation ID by lexical order | canonical contribution key |
duplicate-replace-target | more than one replace targets the same contribution | second operation ID by lexical order | canonical contribution key |
duplicate-path-alias-target | more than one pathAlias targets the same route | second operation ID by lexical order | canonical contribution key |
duplicate-renderer-override-claim | more than one rendererOverride targets the same renderer claim | second operation ID by lexical order | renderer claim |
hide-replace-same-target | one contribution is both hidden and replaced | first operation ID by lexical order | canonical contribution key |
replace-cycle | replacement graph contains a cycle | first operation ID by lexical order in the cycle | canonical contribution key |
reorder-cycle | reorder graph contains a cycle | first operation ID by lexical order in the cycle | canonical contribution key |
missing-target | policy target does not exist in declarations | operation ID | canonical contribution key or renderer claim |
inactive-target | policy target exists but is inactive after runtime gating | operation ID | canonical contribution key or renderer claim |
missing-use | rendererOverride.use does not exist in declarations | operation ID | renderer claim |
inactive-use | rendererOverride.use exists but is inactive after runtime gating | operation ID | renderer claim |
invalid-use-kind | rendererOverride.use is not a renderer contribution for the targeted claim | operation ID | renderer claim |
missing-render-handle | replace.replacement or rendererOverride.use selects a renderable contribution without an opaque render handle | operation ID | canonical contribution key or renderer claim |
invalid-kind | operation targets an unsupported contribution kind | operation ID | canonical contribution key or renderer claim |
policy-created-dangling-reference | simulated policy output creates a broken cross-reference | operation ID that caused removal or replacement | dangling canonical contribution key |
default-renderer-protected | policy attempts to override default user or assistant text rendering | operation ID | renderer 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
replacedoes not copy route context, grants, labels, or render handles from the targetreplaceis 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, andactivityRendererscontain only the effective renderer for each claim - overridden renderer contributions are omitted from renderer arrays,
rendererClaims, andrequiredRenderHandles - the selected composed renderer has the override policy ID in
policyIds - the
rendererOverridepolicy effect records the selected renderer key and every overridden renderer key overriddenkeys are sorted by canonical contribution key
Composer Algorithm
The kernel follows this deterministic order:
- Validate kernel input shape and supported schema versions, including
compositionSchemaVersionand any providedpolicySchemaVersion. - Normalize built-ins and modules into
CompositionSource[]. - Validate module runtime status is present for every module.
- Derive canonical keys for all declared contributions.
- Validate namespace, local IDs, route paths, renderer claims, and version fields through WCP-003 rules.
- Normalize omitted policy to an empty v0 policy.
- Gate module contributions according to resolved runtime status and descriptor activation.
- Materialize fallback-surface candidates for non-ready statuses.
- Validate policy operation IDs, active policy references, predicted cross-references, and policy conflict rules.
- Apply
hideandreplace. - Apply
pathAlias. - Detect effective route path conflicts.
- Resolve renderer claims with
rendererOverride. - Detect unresolved renderer claim conflicts.
- Validate non-policy cross-references, including sidebar route targets and replacement targets.
- Apply
reorderconstraints. - Collect required render handles.
- 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 Status | Contribution Behavior |
|---|---|
ready | normal contributions active; fallback surfaces inactive |
readOnly | contributions active only when descriptor activation includes readOnly; active entries carry read-only runtime metadata |
needsConfiguration | contributions active only when descriptor activation includes needsConfiguration; matching fallback surfaces are active |
unauthorized | contributions active only when descriptor activation includes unauthorized; normal route and renderer defaults exclude this status |
disabled | every non-fallback module contribution omitted; matching fallback surfaces are active |
error | contributions 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 keymodules:instanceIdroutes:effectivePath, then contribution keysidebarItems: reorder result, then contribution keyconfigurationSurfaces: reorder result, then contribution keyintegrationSurfaces: reorder result, then contribution keymessageRenderers:claim, then contribution keymessageAppenders: reorder result, thentargetMessageType,position, contribution keytoolRenderers:claim, then contribution keyactivityRenderers:claim, then contribution keyfallbackSurfaces: matched status order, fallback declared status order, reorder result, then contribution keyrendererClaims:claim, thenrendererKeyrequiredRenderHandles: handle keyappliedPolicyandpolicyEffects: policy kind order, then target or claim, thenpolicyIddiagnostics: diagnostic stable key; the humanmessageis 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
requiredRenderHandlesis a unique manifest- each manifest entry is deduped by owner key plus render handle identity
usedBylists 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
requiredRenderHandlesis 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 ID | Kernel Assertion |
|---|---|
fixture-happy-single-module | returns ok: true with one module route, one sidebar item, and one required render handle |
fixture-happy-multi-instance | same package with two instance IDs composes independently |
fixture-same-local-id-across-instances | same local IDs across instances produce distinct canonical keys |
fixture-duplicate-instance-id | duplicate instance ID blocks composition |
fixture-duplicate-canonical-key | duplicate canonical key blocks composition |
fixture-invalid-local-id | invalid local ID blocks composition |
fixture-duplicate-route-path | duplicate effective route path blocks composition |
fixture-invalid-route-path | invalid route path blocks composition |
fixture-route-path-trailing-slash-rejected | non-root trailing slash is invalid |
fixture-route-path-alias | explicit path alias changes effective route path |
fixture-route-path-alias-creates-conflict | alias-created route conflict blocks composition |
fixture-duplicate-message-renderer-claim | duplicate custom message renderer claim blocks composition |
fixture-duplicate-tool-renderer-claim | duplicate tool renderer claim blocks composition |
fixture-duplicate-activity-renderer-claim | duplicate activity renderer claim blocks composition |
fixture-message-appenders-do-not-conflict | multiple appenders targeting same message and position order deterministically |
fixture-renderer-override | explicit renderer override selects one renderer owner |
fixture-invalid-policy-target | invalid policy target emits policy diagnostic and blocks composition |
fixture-unsupported-policy-schema-version | unsupported policy schema version blocks composition |
fixture-policy-hide-route | hide removes only the targeted route and reports dangling references |
fixture-policy-reorder-sidebar | reorder constraints sort sidebar deterministically |
fixture-policy-replace-descriptor | replace swaps active descriptor ownership without copying target metadata |
fixture-policy-conflict | duplicate or conflicting policy operations block composition before effects apply |
fixture-policy-reorder-cycle | reorder cycles emit policy diagnostics |
fixture-policy-hide-excludes-required-handle | hidden surfaces do not contribute required handles |
fixture-policy-replace-updates-required-handles | replacement updates required handle manifest |
fixture-runtime-ready | ready modules contribute normally |
fixture-runtime-read-only | read-only modules keep allowed surfaces and read-only metadata |
fixture-runtime-needs-configuration | needs-configuration modules show config and fallback surfaces only |
fixture-runtime-unauthorized | unauthorized modules omit default route and renderer contributions and expose matching fallback surfaces |
fixture-runtime-disabled | disabled modules omit normal contributions and expose matching fallback surfaces |
fixture-runtime-error | errored modules omit default route and renderer contributions while exposing default integration surfaces and matching fallback surfaces |
fixture-kernel-builtins-and-modules-same-pipeline | built-ins and modules use identical merge, policy, route, renderer, and diagnostic paths |
fixture-claude-builtins-default-composition | default Claude built-ins compose without module-specific special cases |
fixture-builtin-module-route-conflict-diagnostic | built-in/module route conflicts expose both owners in diagnostics |
fixture-builtin-module-renderer-conflict-diagnostic | built-in/module renderer conflicts expose both owners in diagnostics |
fixture-builtin-owner-shape-diagnostics | malformed built-in owner shape is rejected before composition |
fixture-kernel-required-render-handles | required handles are deterministic and include all active render surfaces |
fixture-required-render-handles-runtime-fallback | selected 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.