Skip to main content

Workspace Composition Module Authoring

This document is the canonical v0 spec for WCP-002: Write module authoring spec v0.

It defines the package, factory, contract, render pack, CSS, fixture, and conformance rules for developer-installed npm React modules.

Primary linked decisions:

  • WCP-ADR-003: opaque render handles
  • WCP-ADR-004: scoped React render runtime
  • WCP-ADR-005: module instance is the unit
  • WCP-ADR-006: module contributions are namespaced
  • WCP-ADR-007: modules do not own persistent state
  • WCP-ADR-008: grants are UI activation contracts
  • WCP-ADR-009: build-time trusted React modules
  • WCP-ADR-015: diagnostics are product inputs
  • WCP-ADR-016: CSS is scoped to assistant UI

Inherited constraints:

  • WCP-003 owns namespace grammar, route path grammar, renderer claim grammar, diagnostic codes, version field semantics, and cross-platform fixture inventory.
  • WCP-005 owns WorkspaceContributions, render handle identity, runtime activation, composition output, ordering, and policy semantics.
  • WCP-006 owns exact React render runtime execution behavior.

Scope

This spec owns the module authoring contract:

  • npm package export shape
  • root module declaration factory
  • module package metadata and compatibility metadata
  • packageId, packageVersion, and instanceId responsibilities
  • module contract declaration shape
  • authoring rules for contributions and render handles
  • render pack subentry expectations
  • CSS subentry expectations
  • host-owned controller and runtime boundary
  • authoring fixtures and conformance checks

This spec does not own:

  • composition merge behavior
  • route, namespace, renderer claim, or diagnostic code grammar
  • React render runtime implementation details
  • marketplace metadata, online install, remote ESM, iframe sandboxing, or untrusted runtime plugins
  • backend authorization, tenant policy, storage implementation, credentials, or network transport
  • built-in page patching or default user/assistant text message replacement

Authoring Model

v0 modules are trusted build-time npm packages imported explicitly by the host.

The authoring split is:

Module package declares.
Host resolves runtime.
Composer composes structure.
Render runtime executes UI.

The module root export is headless. It returns serializable declarations only: metadata, contract, contributions, and opaque render handles.

Executable React UI belongs to the render pack subentry. CSS belongs to the explicit CSS subentry. Nothing registers itself at import time.

The implemented WCP-021 sample package is documented at .docs/packages/data-lab-module.md; this spec stays canonical, and the package guide is the concrete conformance example.

Package Exports

Every module package must expose root, fixtures, and package metadata. Render pack and CSS exports are conditional.

The following full-featured module example includes React UI and CSS:

{
"name": "@maya/data-lab-module",
"version": "0.1.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./render-pack": {
"types": "./dist/render-pack.d.ts",
"import": "./dist/render-pack.js"
},
"./styles.css": "./dist/styles.css",
"./fixtures": {
"types": "./dist/fixtures.d.ts",
"import": "./dist/fixtures.js"
},
"./package.json": "./package.json"
},
"sideEffects": ["**/*.css"]
}

Export requirements:

ExportRequired WhenPurpose
.alwaysheadless factory, metadata, compatibility, and types
./fixturesalwaysauthoring conformance fixtures
./package.jsonalwaysstatic tooling metadata
./render-packmodule declares render handles for React UIscoped React render pack factory
./styles.cssmodule ships CSSexplicit host-imported styles

Root export rules:

  • export named factories, metadata, compatibility, and types
  • do not use a default export
  • do not import React
  • do not import CSS
  • do not create render packs
  • do not register modules, renderers, routes, controllers, styles, or policy
  • do not read environment, browser storage, cookies, URL state, or network state at import time

Render pack export rules:

  • export named render pack factories
  • accept host-owned per-instance controllers
  • bind handles to React components without global registration
  • do not mutate global render runtime state

CSS export rules:

  • expose one explicit ./styles.css subentry when CSS is shipped
  • do not inject CSS from JavaScript import side effects

All JavaScript subentry imports must be inert. Importing root, render pack, or fixtures must not read environment, browser storage, cookies, URL state, network state, timers, global listeners, DOM state, or mutate ambient host/runtime state.

Package Metadata

packageId identifies code origin. It is normally the npm package name.

packageVersion identifies the package artifact version. It is useful for diagnostics and audit logs, but it does not replace Maya schema version checks.

instanceId identifies one enabled module instance in one composed workspace. It is provided or approved by the host, and it is the namespace owner for contributions.

Responsibilities:

FieldOwnerMeaning
packageIdpackage authorcode origin
packageVersionpackage authorartifact version
moduleApiVersionpackage authormodule declaration compatibility
instanceIdhost integratorenabled instance and namespace owner

Modules must not derive instanceId from package name, random suffixes, user-visible labels, or import order.

Author-Facing Types

These are the v0 authoring types. They intentionally expose only the declaration layer needed by module authors.

type ModulePackageMetadata = {
packageId: string;
packageVersion?: string;
moduleApiVersion: '0.1';
};

type ModuleCompatibility = {
packageId: string;
packageVersion?: string;
moduleApiVersions: readonly ['0.1'];
renderHandleVersions: readonly ['0.1'];
compositionSchemaVersions: readonly ['0.1'];
diagnosticSchemaVersions: readonly ['0.1'];
};

type CreateModuleInstanceOptions = {
instanceId: string;
label?: string;
};

type CompositionJsonObject = import('@maya/assistant-composition').CompositionJsonObject;

type ModuleFactory<TOptions extends CreateModuleInstanceOptions> = (
options: TOptions,
) => ModuleInstance;

type ModuleInstance = {
packageId: string;
packageVersion?: string;
moduleApiVersion: '0.1';
instanceId: string;
label?: string;
contract?: ModuleContract;
contributions: WorkspaceContributions;
metadata?: CompositionJsonObject;
};

WorkspaceContributions and RenderHandle are the WCP-005 descriptor types. This spec does not redefine their merge behavior.

Compatibility Metadata

Each module root should export machine-readable compatibility:

export const dataLabModuleCompatibility = {
packageId: '@maya/data-lab-module',
packageVersion: '0.1.0',
moduleApiVersions: ['0.1'],
renderHandleVersions: ['0.1'],
compositionSchemaVersions: ['0.1'],
diagnosticSchemaVersions: ['0.1'],
} as const;

The same compatibility information should be mirrored in package.json. packageVersion is the top-level version; it is not repeated inside mayaModule.

{
"mayaModule": {
"packageId": "@maya/data-lab-module",
"moduleApiVersions": ["0.1"],
"renderHandleVersions": ["0.1"],
"compositionSchemaVersions": ["0.1"],
"diagnosticSchemaVersions": ["0.1"]
}
}

npm semver constrains installation. Maya schema compatibility is decided by explicit supported-version sets.

Conformance must verify that root compatibility metadata, package.json mayaModule, and the top-level package name and version do not drift.

Module Factory

The declaration factory returns an unresolved module instance:

export function createDataLabModule(options: {
instanceId: string;
label?: string;
routeBasePath?: string;
}): ModuleInstance {
return {
packageId: '@maya/data-lab-module',
packageVersion: '0.1.0',
moduleApiVersion: '0.1',
instanceId: options.instanceId,
label: options.label,
contract: dataLabContract,
contributions: createDataLabContributions(options),
};
}

Factory rules:

  • require explicit instanceId
  • return serializable declarations
  • do not return a resolved runtime
  • do not accept credentials, storage adapters, backend clients, global controller objects, or raw fetch functions
  • do not close over persistent state
  • do not create React components inside descriptors
  • do not auto-import CSS or render packs

Host-owned controllers are wired through render pack factories or host controller maps, not through serializable composition descriptors.

Module Contract

A module contract declares what the host must resolve before a module can be activated.

type ModuleContract = {
capabilities?: {
required?: ModuleRequirement[];
optional?: ModuleRequirement[];
};
grants?: {
required?: ModuleRequirement[];
optional?: ModuleRequirement[];
};
permissions?: ModulePermissionRequirement[];
configuration?: ModuleConfigurationRequirement[];
hostStorage?: ModuleHostStorageRequirement[];
runtime?: {
supports: ModuleRuntimeStatus[];
};
fallback?: ModuleFallbackSemantics;
};

type ModuleRequirement = {
id: ModuleRequirementId;
label?: string;
description?: string;
};

type ModuleRequirementId = string;

type ModulePermissionRequirement = ModuleRequirement & {
scope: string;
};

type ModuleConfigurationRequirement = ModuleRequirement & {
kind: 'api-key' | 'base-url' | 'model' | 'tenant-policy' | 'custom';
secret?: boolean;
};

type ModuleHostStorageRequirement = ModuleRequirement & {
owner: 'host';
durability: 'session' | 'persistent';
};

type ModuleFallbackSemantics = {
needsConfiguration?: string;
unauthorized?: string;
readOnly?: string;
disabled?: string;
error?: string;
};

Contract rules:

  • contracts are inputs to host runtime resolution
  • contracts do not grant security authority
  • frontend grants are UI activation contracts, not backend authorization
  • secrets and credentials stay host-owned
  • host storage declarations are requests for host-owned persistence, not module-owned storage
  • backend health, tenant policy, user permission, and configuration state are resolved by the host

Requirement ID rules:

  • ModuleRequirementId uses lower-alnum *( lower-alnum | "-" | "." )
  • IDs are unique across all contract arrays in one module package
  • IDs are stable across patch releases unless the requirement meaning changes
  • runtime.missing contains requirement IDs for required contract entries the host did not satisfy
  • runtime.grants contains granted UI activation IDs, and declared grant/capability grants use their requirement IDs
  • host-specific extra grants should use package-prefixed IDs such as data-lab.query
  • runtime.actions that resolve a missing requirement should use IDs such as configure:<requirementId>, authorize:<requirementId>, or retry:<requirementId>
  • needsConfiguration is used when required configuration is missing or invalid
  • unauthorized is used when required permissions or grants are denied by user or tenant policy
  • readOnly is used when read UI can run but write activation is missing
  • disabled is used when host policy disables the instance regardless of satisfiable requirements
  • error is used for backend health or runtime failures that are not represented by a missing requirement

Contributions

Modules may declare these v0 contribution categories:

  • routes
  • sidebarItems
  • configurationSurfaces
  • integrationSurfaces
  • messageRenderers
  • messageAppenders
  • toolRenderers
  • activityRenderers
  • fallbackSurfaces

Authoring rules:

  • local IDs must follow WCP-003 grammar
  • route paths must follow WCP-003 route path grammar
  • renderer claims must follow WCP-003 renderer claim grammar
  • descriptors carry opaque render handles, not React components
  • runtime activation defaults are defined by WCP-005
  • module settings and integration UI are semantic surfaces, not hard-coded host UI locations
  • default user and assistant text message renderers are not replaceable in v0

Example:

function createDataLabContributions(options: {
instanceId: string;
routeBasePath?: string;
}): WorkspaceContributions {
const routeBasePath = options.routeBasePath ?? '/data-lab';

return {
routes: [
{
id: 'home',
path: routeBasePath,
renderHandle: {
renderHandleVersion: '0.1',
kind: 'route',
id: 'data-lab.home',
},
},
],
sidebarItems: [
{
id: 'home',
label: 'Data Lab',
routeKey: `${options.instanceId}:routes/home`,
},
],
configurationSurfaces: [
{
id: 'connection',
purpose: 'connection',
renderHandle: {
renderHandleVersion: '0.1',
kind: 'configuration-surface',
id: 'data-lab.connection',
},
},
],
toolRenderers: [
{
id: 'query-card',
claim: 'tool:data_lab.query',
renderHandle: {
renderHandleVersion: '0.1',
kind: 'tool-renderer',
id: 'data-lab.query-card',
},
},
],
fallbackSurfaces: [
{
id: 'needs-configuration',
status: 'needsConfiguration',
renderHandle: {
renderHandleVersion: '0.1',
kind: 'fallback-surface',
id: 'data-lab.needs-configuration',
},
},
],
};
}

Render Handles

Render handles are serializable identifiers. They are not imports, callbacks, React components, lazy functions, or registry side effects.

const routeHandle = {
renderHandleVersion: '0.1',
kind: 'route',
id: 'data-lab.home',
};

Handle rules:

  • renderHandleVersion must be explicit
  • kind must match the contribution surface type
  • id should be stable across patch releases unless the rendered surface contract changes
  • handle data must be JSON-serializable and non-secret
  • handle matching happens in the render runtime through an explicitly supplied render pack

Handle kind mapping:

Contribution ArrayRequired Handle Kind
routesroute
configurationSurfacesconfiguration-surface
integrationSurfacesintegration-surface
messageRenderersmessage-renderer
messageAppendersmessage-appender
toolRendererstool-renderer
activityRenderersactivity-renderer
fallbackSurfacesfallback-surface

sidebarItems do not carry render handles.

Render Pack Subentry

Modules with React UI export a render pack factory from ./render-pack.

type ReactRenderPack<TController = unknown> = {
renderRuntimeSchemaVersion: '0.1';
owner: ContributionOwner;
packageId: string;
packageVersion?: string;
controller: TController;
trustTier?: 'core' | 'trusted' | 'external';
providers: readonly ReactRenderProvider<TController>[];
};

type ReactRenderProvider<TController = unknown> = {
handle: RenderHandle;
load:
| { kind: 'eager'; component: ReactRenderComponent<TController> }
| { kind: 'lazy'; load: () => Promise<ReactRenderModule<TController>>; moduleId: string };
preloadHint?: 'eager' | 'route' | 'visible' | 'intent' | 'manual' | 'never';
};

type ReactRenderComponent<TController = unknown> = (props: ReactRenderProps<TController>) => React.ReactNode;

type ReactRenderProps<TController = unknown> = {
contribution: ReactRenderableContribution;
owner: ContributionOwner;
contributionKey: string;
contributionKind: ContributionKind;
renderHandle: RenderHandle;
runtime?: ComposedRuntimeSummary;
metadata?: CompositionJsonObject;
controller: TController;
};

export function createDataLabRenderPack(options: {
instanceId: string;
controller: DataLabController;
}): ReactRenderPack {
return {
renderRuntimeSchemaVersion: '0.1',
owner: {
ownerKind: 'module',
ownerId: options.instanceId,
packageId: '@maya/data-lab-module',
packageVersion: '0.1.0',
},
packageId: '@maya/data-lab-module',
packageVersion: '0.1.0',
controller: options.controller,
trustTier: 'trusted',
providers: [
{
handle: {
renderHandleVersion: '0.1',
kind: 'route',
id: 'data-lab.home',
},
load: { kind: 'eager', component: DataLabHomeRoute },
preloadHint: 'route',
},
{
handle: {
renderHandleVersion: '0.1',
kind: 'configuration-surface',
id: 'data-lab.connection',
},
load: { kind: 'eager', component: DataLabConnectionSurface },
preloadHint: 'visible',
},
],
};
}

Render pack rules:

  • host imports the render pack explicitly
  • host passes the render pack into a scoped render runtime explicitly
  • render pack providers are matched by module instance owner plus stable render handle identity
  • render pack factories may receive host-owned controllers
  • the pack-level controller is passed to every component render as ReactRenderProps.controller
  • a render runtime must not share one pack controller across different instanceId values
  • render pack factories must not register globally
  • root module factory imports must not create render packs
  • missing handle coverage is reported by render runtime diagnostics
  • importing ./render-pack must not read environment, browser storage, cookies, URL state, network state, timers, global listeners, DOM state, or mutate ambient host/runtime state

Controller example:

type DataLabController = {
instanceId: string;
connection: {
baseUrlLabel: string;
apiKeyConfigured: boolean;
health: 'ready' | 'degraded' | 'offline';
};
metrics: {
datasetCount: number;
lastRefreshLabel: string;
};
actions: {
runQuery?(input: QueryInput): Promise<QueryResult> | QueryResult | void;
openConfiguration?(): void;
retryHealthCheck?(): void;
openIntegration?(): void;
onRuntimeAction?(action: RuntimeAction): void;
};
};

Controller rules:

  • controller is per instance
  • controller is host-owned
  • controller data is redacted before it reaches module UI
  • server-side or backend authorization is enforced by the host and backend, not by frontend grant checks
  • controllers should expose intentional actions, not raw platform access

CSS Subentry

Modules that ship CSS expose one explicit CSS subentry:

import '@maya/data-lab-module/styles.css';

CSS rules:

  • every selector must be scoped under .aui-root
  • module selectors should use a module-specific prefix such as .maya-data-lab-*
  • use --aui-* tokens where possible
  • define module variables only under the module scope
  • do not target unscoped html, body, :root, *, button, input, or host app selectors
  • do not ship global resets
  • do not use remote @import or remote fonts
  • do not rely on automatic CSS injection from render pack imports
  • keep z-index, focus, motion, and theme behavior inside assistant UI token conventions

Dependency Boundary

Module packages use peer dependencies for Maya and React coupling.

{
"peerDependencies": {
"@maya/assistant-composition": ">=0.1.0 <0.2.0",
"@maya/assistant-ui": ">=0.1.0 <0.2.0",
"react": ">=18 <20",
"react-dom": ">=18 <20"
},
"peerDependenciesMeta": {
"@maya/assistant-ui": { "optional": true },
"react": { "optional": true },
"react-dom": { "optional": true }
},
"devDependencies": {
"@maya/assistant-composition": "workspace:*",
"@maya/assistant-ui": "workspace:*"
}
}

Rules:

  • generic modules must not depend on @maya/claude-workspace
  • modules without React UI may omit React peers
  • modules with render packs should declare React peers
  • root factory exports must remain headless even when the package has React peers
  • react, react-dom, @maya/assistant-composition, and @maya/assistant-ui are host singletons and must be peer dependencies, not bundled dependencies
  • ordinary private libraries may be dependencies when they are not host singletons and do not create forbidden runtime side effects

Host Integration Path

The host imports module pieces explicitly. The current executable path is headless composition plus a scoped React render runtime. The Claude workspace React facade plus Claude built-in render-pack and CSS exports remain future product-kit convenience work; current module authoring examples import module render packs and CSS explicitly, then wire composition and render-runtime directly.

import { composeWorkspace } from '@maya/assistant-composition';
import { createReactRenderRuntime } from '@maya/assistant-ui/render-runtime';
import { bindDataLabModuleRuntime, createDataLabModule } from '@maya/data-lab-module';
import { createDataLabRenderPack, type DataLabController } from '@maya/data-lab-module/render-pack';

const dataLab = createDataLabModule({
instanceId: 'data-lab-prod',
label: 'Data Lab',
routeBasePath: '/data-lab',
});

const dataLabRuntime = {
instanceId: 'data-lab-prod',
status: 'ready',
grants: ['data-lab.query'],
missing: [],
actions: [],
} as const;

const dataLabController = {
instanceId: 'data-lab-prod',
connection: {
baseUrlLabel: 'host-owned.example',
apiKeyConfigured: true,
health: 'ready',
},
metrics: {
datasetCount: 3,
lastRefreshLabel: 'just now',
},
actions: {
runQuery: async (input) => ({
title: input.query,
summary: `Host executed ${input.query}`,
rows: [],
}),
},
} satisfies DataLabController;

const dataLabRenderPack = createDataLabRenderPack({
instanceId: 'data-lab-prod',
controller: dataLabController,
});

const resolvedDataLab = bindDataLabModuleRuntime(dataLab, dataLabRuntime);
const composed = composeWorkspace({ builtins: [], modules: [resolvedDataLab] });
if (!composed.ok) throw new Error(composed.diagnostics.map((diagnostic) => diagnostic.message).join('\n'));

const renderRuntime = createReactRenderRuntime({ packs: [dataLabRenderPack] });
const coverage = renderRuntime.validateCoverage(composed.composition);
if (!coverage.ok) throw new Error(coverage.diagnostics.map((diagnostic) => diagnostic.message).join('\n'));

Multi-instance example:

const staging = createDataLabModule({
instanceId: 'data-lab-staging',
label: 'Data Lab Staging',
routeBasePath: '/data-lab-staging',
toolClaim: 'tool:data_lab.query.staging',
});

const production = createDataLabModule({
instanceId: 'data-lab-prod',
label: 'Data Lab',
routeBasePath: '/data-lab',
toolClaim: 'tool:data_lab.query',
});

const stagingRuntime = {
instanceId: 'data-lab-staging',
status: 'readOnly',
grants: ['data-lab.query'],
missing: ['data-lab.write'],
actions: [],
} as const;

const productionRuntime = {
instanceId: 'data-lab-prod',
status: 'ready',
grants: ['data-lab.query', 'data-lab.write'],
missing: [],
actions: [],
} as const;

const stagingController = {
instanceId: 'data-lab-staging',
connection: {
baseUrlLabel: 'staging.host-owned.example',
apiKeyConfigured: true,
health: 'ready',
},
metrics: {
datasetCount: 1,
lastRefreshLabel: 'staging snapshot',
},
actions: {
runQuery: async (input) => ({
title: input.query,
summary: `Staging host executed ${input.query}`,
rows: [],
}),
},
} satisfies DataLabController;

const productionController = {
instanceId: 'data-lab-prod',
connection: {
baseUrlLabel: 'prod.host-owned.example',
apiKeyConfigured: true,
health: 'ready',
},
metrics: {
datasetCount: 3,
lastRefreshLabel: 'production snapshot',
},
actions: {
runQuery: async (input) => ({
title: input.query,
summary: `Production host executed ${input.query}`,
rows: [],
}),
},
} satisfies DataLabController;

const stagingRenderPack = createDataLabRenderPack({
instanceId: 'data-lab-staging',
controller: stagingController,
});

const productionRenderPack = createDataLabRenderPack({
instanceId: 'data-lab-prod',
controller: productionController,
});

const composed = composeWorkspace({
builtins: [],
modules: [
bindDataLabModuleRuntime(staging, stagingRuntime),
bindDataLabModuleRuntime(production, productionRuntime),
],
});
if (!composed.ok) throw new Error(composed.diagnostics.map((diagnostic) => diagnostic.message).join('\n'));

const renderRuntime = createReactRenderRuntime({ packs: [stagingRenderPack, productionRenderPack] });
const coverage = renderRuntime.validateCoverage(composed.composition);
if (!coverage.ok) throw new Error(coverage.diagnostics.map((diagnostic) => diagnostic.message).join('\n'));

The same package may be enabled multiple times. Each instance has separate instanceId, runtime, controller, route paths, and render pack providers.

Host-Owned State Boundary

Modules may hold transient UI state inside React components:

  • selected tab
  • expanded panel
  • popover open state
  • draft text before submit
  • hover and focus state

Host-owned state includes:

  • credentials and secrets
  • API keys and base URLs
  • tenant policy
  • user permissions
  • backend health
  • audit logs
  • synchronized data
  • cross-refresh state
  • storage and persistence
  • network clients and transport
  • runtime status

Prohibited module-owned patterns:

  • localStorage, sessionStorage, IndexedDB, cookies, or service workers for module persistence
  • credential stores inside modules
  • raw auth tokens in module props, metadata, diagnostics, runtime data, route params, or CSS content
  • raw fetch, XMLHttpRequest, WebSocket, or database clients as generic module platform access
  • hidden package-level singleton state shared across instances

Fixtures

Each module package must export authoring fixtures from ./fixtures.

Fixtures must be generated from the public factory and render pack exports. They must not create a parallel declaration shape.

Importing ./fixtures must not read environment, browser storage, cookies, URL state, network state, timers, global listeners, DOM state, or mutate ambient host/runtime state.

Authoring fixtures should cover the WCP-003 and WCP-005 named cases that apply to the module's declared contributions and runtime states.

WCP-002-owned fixture IDs use the fixture-module-authoring-* prefix:

  • fixture-module-authoring-package-exports
  • fixture-module-authoring-root-headless
  • fixture-module-authoring-compatibility-metadata
  • fixture-module-authoring-render-pack-coverage
  • fixture-module-authoring-css-scope
  • fixture-module-authoring-multi-instance

Fixture rules:

  • fixture inputs use public factories
  • multi-instance fixtures use the same package with distinct instanceId values
  • descriptor fixture expectations validate through WCP-003 and WCP-005
  • render pack coverage fixtures validate through WCP-006
  • fixtures must not assert human diagnostic message text as the compatibility contract

Conformance Checks

WCP-022 Module Conformance Checklist

pnpm verify:module-conformance is the v0 authoring checklist index. It uses the first-party Data Lab package as the passing fixture, checks that the checklist is wired into package-boundary gates, and maps each check to its decision IDs. pnpm verify:sample-module:conformance runs black-box negative fixtures against the real pnpm verify:sample-module verifier. Failure names are stable conformance check names, not user-facing diagnostic codes.

Check IDDecision IDsVerifier CommandStable Failure NamesFailure Fixture IDs
exports.explicit-subentriesWCP-ADR-006, WCP-ADR-015, WCP-ADR-016pnpm verify:sample-module, pnpm verify:sample-module:conformancepackage.exports, package.sideEffects, package.peerDependencies.includesComposition, package.peerDependencies.includesUi, package.peerDependencies.includesReact, fixtures.includes.fixture-module-authoring-package-exportssample-module-fail-missing-fixtures-export
manifest.compatibility-metadataWCP-ADR-006, WCP-ADR-015pnpm verify:sample-modulepackage.mayaModule.packageId, WCP-021 sample module metadata mirrors package metadata, fixtures.includes.fixture-module-authoring-compatibility-metadatanone yet
namespace.instance-and-claimsWCP-ADR-006, WCP-ADR-015pnpm verify:sample-moduleWCP-021 sample module factory requires explicit valid instance identity, runtime.multi-instance-composes, WCP-021 sample module supports same package enabled twice with distinct instance IDs, fixtures.includes.fixture-module-authoring-multi-instancenone yet
state.host-owned-boundaryWCP-ADR-007, WCP-ADR-015pnpm verify:sample-module, pnpm verify:sample-module:conformancesource.headless-root-and-fixtures, source.no-forbidden-platform-drift, source.contract-no-host-storage, docs.package.noPersistentState, docs.controller.no-type-runtime-field, docs.controller.no-controller-runtime-fallbacksample-module-fail-headless-react-import, sample-module-fail-forbidden-platform-api, sample-module-fail-doc-controller-runtime
render-pack.explicit-scoped-runtimeWCP-ADR-006, WCP-ADR-015pnpm verify:sample-module, pnpm verify:sample-module:conformancesource.render-pack-explicit-factory, source.render-pack-no-global-registration, runtime.explicit-render-packs-cover-handles, runtime.coverage-no-missing, runtime.one-pack-does-not-cover-other-instance, docs.render-pack.current-schema, docs.render-pack.current-owner, docs.render-pack.no-instance-ownersample-module-fail-missing-render-provider
css.aui-root-scopeWCP-ADR-016, WCP-ADR-015pnpm verify:sample-module, pnpm verify:sample-module:conformancestyles.no-forbidden-global-css, styles.all-selectors-scoped, styles.uses-aui-tokens, dist.styles-not-older-than-source, fixtures.includes.fixture-module-authoring-css-scopesample-module-fail-unscoped-css, sample-module-fail-stale-css-dist
diagnostics.missing-render-packWCP-ADR-015pnpm verify:sample-module, pnpm verify:sample-module:conformanceruntime.missing-pack-diagnostic, WCP-021 sample module missing render pack produces handle diagnostics without renderingsample-module-fail-missing-render-provider
fixtures.public-factory-evidenceWCP-ADR-006, WCP-ADR-015pnpm verify:sample-module, pnpm verify:sample-module:conformanceWCP-021 sample module fixtures are generated from public factories, fixtures.includes.fixture-module-authoring-root-headless, fixtures.includes.fixture-module-authoring-multi-instancesample-module-fail-fixture-id-drift
gates.package-boundary-wiringWCP-ADR-015pnpm verify:module-conformance, pnpm verify:sample-module:conformancegate.packageBoundaryBuilt.sampleVerifier, gate.packageBoundaryBuilt.sampleConformancesample-module-fail-gate-unwired

v0 conformance must check:

  • package exports include root, ./fixtures, and ./package.json
  • packages with React UI export ./render-pack
  • packages with CSS export ./styles.css
  • root export has no React import, CSS import, registration side effect, or default export
  • all JavaScript subentries avoid import-time ambient reads and mutations
  • factory requires explicit instanceId
  • root factory returns serializable declarations
  • factory inputs do not include credentials, storage adapters, backend clients, global controllers, or raw fetch functions
  • factory implementation does not close over persistent state
  • contribution descriptors contain render handles, not React components
  • render handle kinds match the contribution array mapping
  • route paths, renderer claims, local IDs, and schema versions validate against WCP-003
  • package metadata includes explicit compatibility version sets
  • root compatibility metadata, package.json mayaModule, and top-level package metadata do not drift
  • render pack coverage matches required render handles
  • render pack providers are scoped by instance owner plus render handle identity
  • CSS selectors are scoped under .aui-root
  • CSS avoids forbidden global selectors and remote imports
  • fixtures cover multi-instance and runtime status cases
  • docs and API comments use grant language as UI activation, not frontend authorization
  • diagnostics, runtime summaries, metadata, labels, and actions contain no secrets
  • code avoids module-owned persistence, raw platform clients, package-level runtime singletons, global render registries, automatic discovery, remote code loading, and dependency on @maya/claude-workspace

Forbidden Drift

Do not add these to v0 module authoring:

  • marketplace metadata
  • online install
  • remote ESM
  • iframe sandbox
  • untrusted third-party plugin model
  • automatic module discovery
  • global render registry
  • import-time registration
  • render pack auto-discovery
  • module-owned persistent storage
  • module-owned credentials
  • package-level singleton runtime state
  • built-in page DOM patching
  • default user or assistant text renderer replacement
  • dependency on @maya/claude-workspace from generic modules

Duplicate Truth Boundary

This document is the canonical source for module package authoring shape.

It must not redefine WCP-003 grammar, WCP-003 diagnostic codes, WCP-005 composition output, or WCP-006 render runtime execution semantics.

It may reference those specs and define authoring conformance checks that call into them.