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 handlesWCP-ADR-004: scoped React render runtimeWCP-ADR-005: module instance is the unitWCP-ADR-006: module contributions are namespacedWCP-ADR-007: modules do not own persistent stateWCP-ADR-008: grants are UI activation contractsWCP-ADR-009: build-time trusted React modulesWCP-ADR-015: diagnostics are product inputsWCP-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, andinstanceIdresponsibilities- 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:
| Export | Required When | Purpose |
|---|---|---|
. | always | headless factory, metadata, compatibility, and types |
./fixtures | always | authoring conformance fixtures |
./package.json | always | static tooling metadata |
./render-pack | module declares render handles for React UI | scoped React render pack factory |
./styles.css | module ships CSS | explicit 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.csssubentry 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:
| Field | Owner | Meaning |
|---|---|---|
packageId | package author | code origin |
packageVersion | package author | artifact version |
moduleApiVersion | package author | module declaration compatibility |
instanceId | host integrator | enabled 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:
ModuleRequirementIduseslower-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.missingcontains requirement IDs for required contract entries the host did not satisfyruntime.grantscontains 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.actionsthat resolve a missing requirement should use IDs such asconfigure:<requirementId>,authorize:<requirementId>, orretry:<requirementId>needsConfigurationis used when required configuration is missing or invalidunauthorizedis used when required permissions or grants are denied by user or tenant policyreadOnlyis used when read UI can run but write activation is missingdisabledis used when host policy disables the instance regardless of satisfiable requirementserroris used for backend health or runtime failures that are not represented by a missing requirement
Contributions
Modules may declare these v0 contribution categories:
routessidebarItemsconfigurationSurfacesintegrationSurfacesmessageRenderersmessageAppenderstoolRenderersactivityRenderersfallbackSurfaces
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:
renderHandleVersionmust be explicitkindmust match the contribution surface typeidshould be stable across patch releases unless the rendered surface contract changes- handle
datamust be JSON-serializable and non-secret - handle matching happens in the render runtime through an explicitly supplied render pack
Handle kind mapping:
| Contribution Array | Required Handle Kind |
|---|---|
routes | route |
configurationSurfaces | configuration-surface |
integrationSurfaces | integration-surface |
messageRenderers | message-renderer |
messageAppenders | message-appender |
toolRenderers | tool-renderer |
activityRenderers | activity-renderer |
fallbackSurfaces | fallback-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
controlleris passed to every component render asReactRenderProps.controller - a render runtime must not share one pack controller across different
instanceIdvalues - 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-packmust 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
@importor 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-uiare host singletons and must be peer dependencies, not bundleddependencies- ordinary private libraries may be
dependencieswhen 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-exportsfixture-module-authoring-root-headlessfixture-module-authoring-compatibility-metadatafixture-module-authoring-render-pack-coveragefixture-module-authoring-css-scopefixture-module-authoring-multi-instance
Fixture rules:
- fixture inputs use public factories
- multi-instance fixtures use the same package with distinct
instanceIdvalues - 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 ID | Decision IDs | Verifier Command | Stable Failure Names | Failure Fixture IDs |
|---|---|---|---|---|
exports.explicit-subentries | WCP-ADR-006, WCP-ADR-015, WCP-ADR-016 | pnpm verify:sample-module, pnpm verify:sample-module:conformance | package.exports, package.sideEffects, package.peerDependencies.includesComposition, package.peerDependencies.includesUi, package.peerDependencies.includesReact, fixtures.includes.fixture-module-authoring-package-exports | sample-module-fail-missing-fixtures-export |
manifest.compatibility-metadata | WCP-ADR-006, WCP-ADR-015 | pnpm verify:sample-module | package.mayaModule.packageId, WCP-021 sample module metadata mirrors package metadata, fixtures.includes.fixture-module-authoring-compatibility-metadata | none yet |
namespace.instance-and-claims | WCP-ADR-006, WCP-ADR-015 | pnpm verify:sample-module | WCP-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-instance | none yet |
state.host-owned-boundary | WCP-ADR-007, WCP-ADR-015 | pnpm verify:sample-module, pnpm verify:sample-module:conformance | source.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-fallback | sample-module-fail-headless-react-import, sample-module-fail-forbidden-platform-api, sample-module-fail-doc-controller-runtime |
render-pack.explicit-scoped-runtime | WCP-ADR-006, WCP-ADR-015 | pnpm verify:sample-module, pnpm verify:sample-module:conformance | source.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-owner | sample-module-fail-missing-render-provider |
css.aui-root-scope | WCP-ADR-016, WCP-ADR-015 | pnpm verify:sample-module, pnpm verify:sample-module:conformance | styles.no-forbidden-global-css, styles.all-selectors-scoped, styles.uses-aui-tokens, dist.styles-not-older-than-source, fixtures.includes.fixture-module-authoring-css-scope | sample-module-fail-unscoped-css, sample-module-fail-stale-css-dist |
diagnostics.missing-render-pack | WCP-ADR-015 | pnpm verify:sample-module, pnpm verify:sample-module:conformance | runtime.missing-pack-diagnostic, WCP-021 sample module missing render pack produces handle diagnostics without rendering | sample-module-fail-missing-render-provider |
fixtures.public-factory-evidence | WCP-ADR-006, WCP-ADR-015 | pnpm verify:sample-module, pnpm verify:sample-module:conformance | WCP-021 sample module fixtures are generated from public factories, fixtures.includes.fixture-module-authoring-root-headless, fixtures.includes.fixture-module-authoring-multi-instance | sample-module-fail-fixture-id-drift |
gates.package-boundary-wiring | WCP-ADR-015 | pnpm verify:module-conformance, pnpm verify:sample-module:conformance | gate.packageBoundaryBuilt.sampleVerifier, gate.packageBoundaryBuilt.sampleConformance | sample-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.jsonmayaModule, 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-workspacefrom 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.