@forgeax/engine-skinning 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../types/src/asset-errors.ts","../../types/src/result.ts","../../types/src/asset-evidence.ts","../../types/src/handle.ts","../../types/src/material/asset.ts","../../types/src/material/color-space.ts","../../types/src/material/errors.ts","../../types/src/material/resolve.ts","../../types/src/runtime-scope.ts","../../types/src/derive-paramschema.ts","../../types/src/asset-producer.ts","../../types/src/catalog.ts","../../types/src/import.ts","../../types/src/index.ts","../src/assets/skin-decoder.ts","../src/errors.ts","../src/skin.ts","../src/plugin.ts","../src/resolve-skin-joints.ts"],"sourcesContent":["/** Closed structured error contracts for Pack v2 and asset evidence. */\n\nexport type AssetLoadErrorCode =\n | 'asset-guid-invalid'\n | 'asset-kind-mismatch'\n | 'asset-not-found'\n | 'asset-not-ready'\n | 'catalog-unavailable'\n | 'catalog-discontinuous'\n | 'asset-fetch-failed'\n | 'asset-integrity-failed'\n | 'asset-package-invalid'\n | 'asset-decoder-missing'\n | 'asset-decode-failed'\n | 'asset-dependency-failed'\n | 'asset-superseded'\n | 'asset-load-cancelled'\n | 'asset-runtime-disposed';\n\nexport const ASSET_LOAD_ERROR_HINTS: Readonly<Record<AssetLoadErrorCode, string>> = {\n 'asset-guid-invalid': 'provide a valid asset GUID and retry the current publication',\n 'asset-kind-mismatch': 'pass the Catalog kind or the matching custom AssetKind token',\n 'asset-not-found': 'inspect the producer Catalog and rebuild the missing publication',\n 'asset-not-ready': 'wait for the current publication or inspect its producer lifecycle',\n 'catalog-unavailable': 'inspect the scope and create a fresh Registry for a new scope',\n 'catalog-discontinuous': 'reconcile the Catalog baseline before loading the current row',\n 'asset-fetch-failed': 'verify the package locator and republish the Pack',\n 'asset-integrity-failed': 'verify the artifact digest and recook the Pack',\n 'asset-package-invalid': 'validate the Pack v2 envelope and recook invalid output',\n 'asset-decoder-missing': 'install the owner decoder lease for this kind',\n 'asset-decode-failed': 'inspect the structured decoder detail and repair the owner output',\n 'asset-dependency-failed': 'repair the dependency publication named in detail and retry',\n 'asset-superseded': 'load the current publication instead of the superseded ticket',\n 'asset-load-cancelled': 'retry with a live AbortSignal when the request is still needed',\n 'asset-runtime-disposed': 'obtain a new Registry from the current realm',\n};\n\ntype AssetLoadErrorBase<C extends AssetLoadErrorCode, D> = {\n readonly code: C;\n readonly expected: string;\n readonly hint: string;\n readonly detail: D;\n};\n\nexport type AssetLoadError =\n | AssetLoadErrorBase<'asset-guid-invalid', { readonly guid: string }>\n | AssetLoadErrorBase<\n 'asset-kind-mismatch',\n { readonly guid: string; readonly expectedKind: string; readonly actualKind: string }\n >\n | AssetLoadErrorBase<'asset-not-found', { readonly guid: string }>\n | AssetLoadErrorBase<'asset-not-ready', { readonly guid: string; readonly generation: number }>\n | AssetLoadErrorBase<'catalog-unavailable', { readonly scopeId: string }>\n | AssetLoadErrorBase<\n 'catalog-discontinuous',\n {\n readonly scopeId: string;\n readonly expectedGeneration: number;\n readonly actualGeneration: number;\n }\n >\n | AssetLoadErrorBase<'asset-fetch-failed', { readonly guid: string; readonly packageUrl: string }>\n | AssetLoadErrorBase<\n 'asset-integrity-failed',\n {\n readonly guid: string;\n readonly artifactKey: string;\n readonly expectedDigest: string;\n readonly actualDigest: string;\n }\n >\n | AssetLoadErrorBase<'asset-package-invalid', { readonly guid: string; readonly reason: string }>\n | AssetLoadErrorBase<'asset-decoder-missing', { readonly kind: string }>\n | AssetLoadErrorBase<'asset-decode-failed', { readonly guid: string; readonly kind: string }>\n | AssetLoadErrorBase<\n 'asset-dependency-failed',\n { readonly guid: string; readonly dependencyGuid: string }\n >\n | AssetLoadErrorBase<'asset-superseded', { readonly guid: string; readonly generation: number }>\n | AssetLoadErrorBase<'asset-load-cancelled', { readonly guid: string }>\n | AssetLoadErrorBase<'asset-runtime-disposed', { readonly scopeId: string }>;\n\nexport type AssetArtifactErrorCode =\n | 'asset-artifact-path-invalid'\n | 'asset-artifact-missing'\n | 'asset-artifact-integrity-mismatch'\n | 'asset-artifact-media-unsupported'\n | 'asset-artifact-codec-unsupported'\n | 'asset-artifact-encoding-unsupported'\n | 'asset-artifact-decode-failed';\n\nexport type AssetArtifactErrorDetail =\n | {\n readonly guid: string;\n readonly artifactKey: string;\n readonly observed: string;\n readonly expected: string;\n }\n | {\n readonly guid: string;\n readonly artifactKey: string;\n readonly observed: string;\n readonly expected: string;\n readonly path: string;\n };\n\nexport type AssetArtifactError =\n | {\n readonly code: 'asset-artifact-path-invalid';\n readonly expected: string;\n readonly hint: string;\n readonly detail: Extract<AssetArtifactErrorDetail, { readonly artifactKey: string }>;\n }\n | {\n readonly code: 'asset-artifact-missing';\n readonly expected: string;\n readonly hint: string;\n readonly detail: Extract<AssetArtifactErrorDetail, { readonly path: string }>;\n }\n | {\n readonly code: Exclude<\n AssetArtifactErrorCode,\n 'asset-artifact-path-invalid' | 'asset-artifact-missing'\n >;\n readonly expected: string;\n readonly hint: string;\n readonly detail: Extract<AssetArtifactErrorDetail, { readonly artifactKey: string }>;\n };\n\nexport type PackV2ErrorCode =\n | 'pack-v2-version-unsupported'\n | 'pack-v2-envelope-invalid'\n | 'pack-v2-duplicate-guid'\n | 'pack-v2-duplicate-artifact-key'\n | 'pack-v2-artifact-descriptor-invalid';\n\nexport type PackV2ErrorDetail =\n | { readonly observed: string; readonly expected: string }\n | { readonly guid: string; readonly paths: readonly string[] }\n | { readonly guid: string; readonly artifactKey: string }\n | { readonly guid: string; readonly artifactKey: string; readonly field: string };\n\nexport interface PackV2Error {\n readonly code: PackV2ErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail: PackV2ErrorDetail;\n}\n\nexport type AssetEvidenceErrorDetail =\n | { readonly capability: string; readonly stage: string }\n | { readonly guid: string; readonly observed: string; readonly expected: string };\n\nexport interface AssetEvidenceError {\n readonly code: AssetEvidenceErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail: AssetEvidenceErrorDetail;\n}\n\nexport const ASSET_EVIDENCE_ERROR_HINTS = {\n 'asset-evidence-capability-missing':\n 'provide the missing evidence capability, then rerun asset lookup or verify',\n 'asset-evidence-source-conflict':\n 'keep one source declaration per GUID and rerun the offline evidence projection',\n 'asset-evidence-locator-conflict':\n 'keep one packageUrl per GUID and rebuild the catalog before verifying the asset',\n 'asset-evidence-receipt-conflict':\n 'keep one producer-owned receipt per GUID and rerun cook before verifying the asset',\n 'asset-evidence-digest-mismatch':\n 'recook the source or restore the package bytes, then rerun artifact verification',\n} satisfies Readonly<Record<string, string>>;\n\nexport type AssetEvidenceErrorCode = keyof typeof ASSET_EVIDENCE_ERROR_HINTS;\n\n/** Ordered author-to-runtime stages used by structured recovery errors. */\nexport type AssetErrorStage =\n | 'author-validation'\n | 'external-declaration'\n | 'import'\n | 'native-cook'\n | 'ddc-validation'\n | 'runtime-parse'\n | 'editor-capability';\n\n/** AI-readable next action; it does not grant a cache or runtime write authority. */\nexport interface AssetStageRecovery {\n readonly action: string;\n readonly command?: string;\n readonly retryable: boolean;\n}\n\nexport type AssetStageErrorDetail =\n | { readonly authoringPath: string; readonly rule: string }\n | { readonly sourceKey: string; readonly sourceIndex?: number }\n | { readonly sourcePath: string; readonly importer?: string }\n | { readonly guid: string; readonly producer: string }\n | { readonly guid: string; readonly observedDigest: string; readonly expectedDigest: string }\n | { readonly guid: string; readonly packageUrl: string }\n | { readonly capability: string; readonly assetKind: string };\n\nexport interface AssetStageErrorBase<S extends AssetErrorStage, C extends string> {\n readonly stage: S;\n readonly code: C;\n readonly expected: string;\n readonly hint: string;\n readonly detail: AssetStageErrorDetail;\n readonly recovery: AssetStageRecovery;\n}\n\ntype AssetStageErrorWithDetail<\n S extends AssetErrorStage,\n C extends string,\n D extends AssetStageErrorDetail,\n> = Omit<AssetStageErrorBase<S, C>, 'detail'> & { readonly detail: D };\n\nexport type AuthorValidationError = AssetStageErrorWithDetail<\n 'author-validation',\n 'author-validation-failed',\n Extract<AssetStageErrorDetail, { readonly authoringPath: string }>\n>;\nexport type ExternalDeclarationError = AssetStageErrorWithDetail<\n 'external-declaration',\n 'external-declaration-invalid',\n Extract<AssetStageErrorDetail, { readonly sourceKey: string }>\n>;\nexport type ImportStageError = AssetStageErrorWithDetail<\n 'import',\n 'import-failed',\n Extract<AssetStageErrorDetail, { readonly sourcePath: string }>\n>;\nexport type NativeCookError = AssetStageErrorWithDetail<\n 'native-cook',\n 'native-cook-failed',\n Extract<AssetStageErrorDetail, { readonly guid: string; readonly producer: string }>\n>;\nexport type DdcValidationError = AssetStageErrorWithDetail<\n 'ddc-validation',\n 'ddc-validation-failed',\n Extract<AssetStageErrorDetail, { readonly observedDigest: string }>\n>;\nexport type RuntimeParseError = AssetStageErrorWithDetail<\n 'runtime-parse',\n 'runtime-parse-failed',\n Extract<AssetStageErrorDetail, { readonly packageUrl: string }>\n>;\nexport type EditorCapabilityError = AssetStageErrorWithDetail<\n 'editor-capability',\n 'editor-capability-unavailable',\n Extract<AssetStageErrorDetail, { readonly capability: string }>\n>;\n\nexport type AssetStageError =\n | AuthorValidationError\n | ExternalDeclarationError\n | ImportStageError\n | NativeCookError\n | DdcValidationError\n | RuntimeParseError\n | EditorCapabilityError;\n\nexport type AssetStageErrorCode = AssetStageError['code'];\n\nexport const ASSET_STAGE_ERROR_HINTS: Readonly<Record<AssetStageErrorCode, string>> = {\n 'author-validation-failed': 'read the authoring rule and apply the suggested recovery',\n 'external-declaration-invalid': 'repair the sourceKey declaration and retry recovery',\n 'import-failed': 'fix the importer input or registration, then retry recovery',\n 'native-cook-failed': 'fix the native producer and rerun recovery',\n 'ddc-validation-failed': 'repair the cooked artifact and rerun recovery',\n 'runtime-parse-failed': 'repair the package payload and rerun recovery',\n 'editor-capability-unavailable': 'register the capability and retry recovery',\n};\n","// @forgeax/engine-types — Result<T, E> SSOT (tweak-20260612-result-into-types).\n//\n// `Result<T, E>` is the project-wide binary success/failure carrier. It used to\n// live as TWO byte-aligned copies (`packages/rhi/src/errors.ts` +\n// `packages/ecs/src/result.ts`) — that \"byte-for-byte aligned\" prose was a\n// declaration, not a mechanism, and silently drifted. Consolidating here:\n// - One physical source for the discriminated union + `ok`/`err` factories.\n// - rhi / ecs each keep their typed error class (RhiError / EcsError union)\n// and just re-export the Result shape from this module.\n// - Generic parameter is intentionally NOT defaulted — each consumer narrows\n// the error parameter at its own boundary (`Result<T, RhiError>`,\n// `Result<T, EcsError>`).\n//\n// Charter mapping: P5 consistent abstraction (single Result idiom across rhi\n// and ecs); SSOT data layer (one authoritative carrier, derive don't duplicate).\n\n// ────────────────────────────────────────────────────────────────────────────\n// Narrow shape — boolean discriminant `.ok` + `.value` / `.error` branches\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * Success branch — plain field access (`.ok === true`, `.value: T`) plus\n * method chain (`.unwrap()` / `.unwrapOr(default)`).\n *\n * On the ok branch `unwrap()` returns `.value`; `unwrapOr(d)` ignores the\n * default and returns `.value` (charter proposition 4 explicit-failure: the\n * throwing path lives only on the err branch).\n */\nexport interface ResultOk<T> {\n readonly ok: true;\n readonly value: T;\n /** Return the wrapped value. Never throws on the ok branch. */\n unwrap(): T;\n /** Return the wrapped value; the default argument is unused on the ok branch. */\n unwrapOr(defaultValue: T): T;\n}\n\n/**\n * Failure branch — plain field access (`.ok === false`, `.error: E`) plus\n * method chain (`.unwrap()` / `.unwrapOr(default)`).\n *\n * On the err branch `unwrap()` throws the underlying `E` (not wrapped in a\n * fresh `Error`) so AI consumers preserve `.code` / `.expected` / `.hint` for\n * programmatic recovery (charter proposition 4 + AGENTS.md \"Errors are\n * structured. Return Result, never throw for expected failures.\" — `.unwrap()`\n * is the explicit Layer 3 ErrorHandler boundary, not a hidden throw).\n *\n * `unwrapOr(d)` returns the default; the original error is silently dropped.\n * Use `if (!r.ok) ...` plus `r.error` if you need to inspect the failure.\n */\nexport interface ResultErr<E> {\n readonly ok: false;\n readonly error: E;\n /** Throw the underlying `error` (preserved without rewrapping). */\n unwrap(): never;\n /** Return the supplied default value (the original error is silently dropped). */\n unwrapOr<T>(defaultValue: T): T;\n}\n\n/**\n * Discriminated union: either `ResultOk<T>` or `ResultErr<E>`.\n *\n * Use `ok(value)` / `err(error)` factories to create instances.\n * Use `if (r.ok) ...` / `if (!r.ok) ...` to narrow.\n *\n * Width-assignment friendly: a `ResultErr<E>` returned by `err(...)` typed as\n * `Result<never, E>` satisfies any `Result<X, E>` after a `if (!r.ok)` narrow,\n * so `return r;` propagates without a cast.\n */\nexport type Result<T, E> = ResultOk<T> | ResultErr<E>;\n\n// ────────────────────────────────────────────────────────────────────────────\n// Prototype objects (shared methods — dimorphic, V8 friendly)\n// ────────────────────────────────────────────────────────────────────────────\n\nconst OK_PROTO = {\n unwrap(this: ResultOk<unknown>): unknown {\n return this.value;\n },\n unwrapOr(this: ResultOk<unknown>, _defaultValue: unknown): unknown {\n return this.value;\n },\n};\n\nconst ERR_PROTO = {\n unwrap(this: ResultErr<unknown>): never {\n // Throw the ORIGINAL error — NOT wrapped in new Error()\n throw this.error;\n },\n unwrapOr<T>(this: ResultErr<unknown>, defaultValue: T): T {\n return defaultValue;\n },\n};\n\n// ────────────────────────────────────────────────────────────────────────────\n// Factory functions\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * Construct a success branch.\n *\n * Returns the narrow `ResultOk<T>` shape so direct `.value` access works\n * without a `if (r.ok)` narrow at the call site (a `ResultOk<T>` widens to\n * `Result<T, E>` for any `E` by structural assignment).\n */\nexport function ok<T>(value: T): ResultOk<T> {\n const r = Object.create(OK_PROTO) as { ok: true; value: T };\n r.ok = true;\n r.value = value;\n return r as ResultOk<T>;\n}\n\n/**\n * Construct a failure branch.\n *\n * Returns the narrow `ResultErr<E>` shape so direct `.error` access works\n * without a `if (!r.ok)` narrow at the call site. After a `if (!r.ok) return\n * r;` narrow, `ResultErr<E>` widens structurally to any `Result<X, E>` so\n * `return r;` propagates without a cast.\n */\nexport function err<E>(error: E): ResultErr<E> {\n const r = Object.create(ERR_PROTO) as { ok: false; error: E };\n r.ok = false;\n r.error = error;\n return r as ResultErr<E>;\n}\n","import type {\n ArtifactDescriptor,\n ArtifactVerificationStatus,\n ContentEncoding,\n CookFreshness,\n CookOrigin,\n CookReceiptStatus,\n CookStatus,\n Integrity,\n RuntimeEvidenceStatus,\n} from './asset.js';\nimport {\n ASSET_EVIDENCE_ERROR_HINTS,\n type AssetEvidenceError,\n type AssetEvidenceErrorCode,\n} from './asset-errors.js';\nimport { err, ok, type Result } from './result.js';\n\nexport type {\n AssetEvidenceError,\n AssetEvidenceErrorCode,\n AssetEvidenceErrorDetail,\n} from './asset-errors.js';\n\n/** Producer-owned cook attempt; a receipt is evidence only for its own input fingerprint. */\nexport interface CookReceipt {\n readonly guid: string;\n readonly origin: CookOrigin;\n readonly status: CookReceiptStatus;\n readonly inputFingerprint: string;\n readonly outputDigest?: string;\n readonly error?: {\n readonly code: string;\n readonly expected: string;\n readonly hint: string;\n readonly detail?: unknown;\n };\n}\n\nexport type { CookProduct } from './asset.js';\n\n/** Source-side declaration used to compare current input with a producer receipt. */\nexport interface SourceDeclarationEvidence {\n readonly origin: CookOrigin;\n readonly sourcePath?: string;\n readonly inputFingerprint?: string;\n}\n\n/** Verification state for the Pack v2 package envelope, independent of cook freshness. */\nexport interface PackageVerificationEvidence {\n readonly status: ArtifactVerificationStatus;\n readonly digest?: string;\n}\n\n/** Descriptor plus the explicit verification result for one package artifact. */\nexport interface AssetEvidenceArtifact {\n readonly descriptor: ArtifactDescriptor;\n readonly verification: ArtifactVerificationStatus;\n}\n\nexport type AssetEvidenceStatus = 'passed' | 'notChecked' | 'failed' | 'unknown';\n\n/** Derived GUID evidence; use the closed states literally and follow error hints. */\nexport interface AssetEvidence {\n readonly guid: string;\n readonly packageUrl?: string;\n readonly cookReceiptUrl?: string;\n readonly source?: SourceDeclarationEvidence;\n readonly cook: {\n readonly status: CookStatus;\n readonly freshness: CookFreshness;\n readonly receipt?: CookReceipt;\n };\n readonly package?: PackageVerificationEvidence;\n readonly artifacts: Readonly<Record<string, AssetEvidenceArtifact>>;\n readonly runtime: {\n readonly status: RuntimeEvidenceStatus;\n };\n}\n\n/** Catalog navigation only; the URLs locate evidence but do not prove readiness. */\nexport interface AssetEvidenceLocator {\n readonly packageUrl: string;\n readonly cookReceiptUrl?: string;\n}\n\nexport interface AssetEvidencePackageInput {\n readonly guid: string;\n readonly digest?: string;\n readonly artifacts: Readonly<\n Record<\n string,\n {\n readonly descriptor: ArtifactDescriptor;\n readonly verification?: ArtifactVerificationStatus;\n }\n >\n >;\n}\n\n/** Inputs accepted by the pure projector; producers supply facts, not inferred status. */\nexport interface AssetEvidenceInputs {\n readonly guid: string;\n readonly source?: SourceDeclarationEvidence;\n readonly sources?: readonly SourceDeclarationEvidence[];\n readonly locator?: AssetEvidenceLocator;\n readonly locators?: readonly AssetEvidenceLocator[];\n readonly receipt?: CookReceipt;\n readonly receipts?: readonly CookReceipt[];\n readonly packageVerification?: PackageVerificationEvidence;\n readonly package?: AssetEvidencePackageInput;\n readonly artifacts?: Readonly<Record<string, ArtifactDescriptor>>;\n readonly artifactVerification?: Readonly<Record<string, ArtifactVerificationStatus>>;\n readonly runtime?: { readonly status: RuntimeEvidenceStatus };\n}\n\n/** Project the one public producer product into the shared evidence view. */\nexport function projectCookProductEvidence(\n product: import('./asset.js').CookProduct,\n locator?: AssetEvidenceLocator,\n): Result<AssetEvidence, AssetEvidenceError> {\n if (product.receipt.guid !== product.guid) {\n return conflict(\n 'asset-evidence-receipt-conflict',\n product.guid,\n product.receipt.guid,\n product.guid,\n );\n }\n if (product.receipt.outputDigest !== product.digest) {\n return conflict(\n 'asset-evidence-digest-mismatch',\n product.guid,\n product.digest,\n product.receipt.outputDigest ?? 'missing receipt digest',\n );\n }\n return projectAssetEvidence({\n guid: product.guid,\n source: {\n origin: product.receipt.origin,\n inputFingerprint: product.receipt.inputFingerprint,\n },\n ...(locator === undefined ? {} : { locator }),\n receipt: product.receipt,\n package: {\n guid: product.guid,\n digest: product.digest,\n artifacts: Object.fromEntries(\n Object.entries(product.artifacts).map(([key, descriptor]) => [\n key,\n { descriptor, verification: 'passed' as const },\n ]),\n ),\n },\n });\n}\n\nexport { ASSET_EVIDENCE_ERROR_HINTS } from './asset-errors.js';\n\nfunction conflict(\n code: AssetEvidenceErrorCode,\n guid: string,\n observed: string,\n expected: string,\n): Result<never, AssetEvidenceError> {\n return err({\n code,\n expected,\n hint: ASSET_EVIDENCE_ERROR_HINTS[code],\n detail: { guid, observed, expected },\n });\n}\n\nfunction distinct<T>(values: readonly T[], key: (value: T) => string): readonly T[] {\n const result: T[] = [];\n const seen = new Set<string>();\n for (const value of values) {\n const identity = key(value);\n if (!seen.has(identity)) {\n seen.add(identity);\n result.push(value);\n }\n }\n return result;\n}\n\nfunction chooseSource(\n inputs: AssetEvidenceInputs,\n): Result<SourceDeclarationEvidence | undefined, AssetEvidenceError> {\n const values = distinct(\n [inputs.source, ...(inputs.sources ?? [])].filter(\n (value): value is SourceDeclarationEvidence => value !== undefined,\n ),\n (value) => JSON.stringify(value),\n );\n if (values.length > 1) {\n return conflict(\n 'asset-evidence-source-conflict',\n inputs.guid,\n JSON.stringify(values),\n 'one source declaration per GUID',\n );\n }\n return ok(values[0]);\n}\n\nfunction chooseLocator(\n inputs: AssetEvidenceInputs,\n): Result<AssetEvidenceLocator | undefined, AssetEvidenceError> {\n const values = distinct(\n [inputs.locator, ...(inputs.locators ?? [])].filter(\n (value): value is AssetEvidenceLocator => value !== undefined,\n ),\n (value) => JSON.stringify(value),\n );\n if (values.length > 1) {\n return conflict(\n 'asset-evidence-locator-conflict',\n inputs.guid,\n JSON.stringify(values),\n 'one package locator per GUID',\n );\n }\n return ok(values[0]);\n}\n\nfunction chooseReceipt(\n inputs: AssetEvidenceInputs,\n): Result<CookReceipt | undefined, AssetEvidenceError> {\n const values = distinct(\n [inputs.receipt, ...(inputs.receipts ?? [])].filter(\n (value): value is CookReceipt => value !== undefined,\n ),\n (value) => JSON.stringify(value),\n );\n if (values.length > 1) {\n return conflict(\n 'asset-evidence-receipt-conflict',\n inputs.guid,\n JSON.stringify(values),\n 'one cook receipt per GUID',\n );\n }\n const receipt = values[0];\n if (receipt !== undefined && receipt.guid.toLowerCase() !== inputs.guid.toLowerCase()) {\n return conflict(\n 'asset-evidence-receipt-conflict',\n inputs.guid,\n receipt.guid,\n `receipt GUID ${inputs.guid}`,\n );\n }\n return ok(receipt);\n}\n\nfunction packageEvidence(inputs: AssetEvidenceInputs): PackageVerificationEvidence | undefined {\n if (inputs.packageVerification !== undefined) return inputs.packageVerification;\n if (inputs.package === undefined) return undefined;\n const statuses = Object.values(inputs.package.artifacts).map(\n (artifact) => artifact.verification ?? 'notChecked',\n );\n const status = statuses.some((value) => value === 'failed')\n ? 'failed'\n : statuses.length > 0 && statuses.every((value) => value === 'passed')\n ? 'passed'\n : 'notChecked';\n return {\n status,\n ...(inputs.package.digest !== undefined ? { digest: inputs.package.digest } : {}),\n };\n}\n\nfunction artifactsEvidence(\n inputs: AssetEvidenceInputs,\n): Readonly<Record<string, AssetEvidenceArtifact>> {\n const descriptors =\n inputs.artifacts ??\n Object.fromEntries(\n Object.entries(inputs.package?.artifacts ?? {}).map(([key, value]) => [\n key,\n value.descriptor,\n ]),\n );\n return Object.fromEntries(\n Object.entries(descriptors).map(([key, descriptor]) => [\n key,\n {\n descriptor,\n verification:\n inputs.artifactVerification?.[key] ??\n inputs.package?.artifacts[key]?.verification ??\n 'notChecked',\n },\n ]),\n );\n}\n\nfunction freshness(\n source: SourceDeclarationEvidence | undefined,\n receipt: CookReceipt | undefined,\n): CookFreshness {\n if (source?.origin === 'authoredPack') return 'notApplicable';\n if (receipt === undefined) return 'unknown';\n if (source?.inputFingerprint === undefined) return 'unknown';\n return source.inputFingerprint === receipt.inputFingerprint ? 'current' : 'stale';\n}\n\nfunction cookStatus(\n source: SourceDeclarationEvidence | undefined,\n receipt: CookReceipt | undefined,\n): CookStatus {\n if (source?.origin === 'authoredPack') return 'notRequired';\n if (receipt?.status === 'failed') return 'failed';\n if (receipt?.status === 'succeeded') return 'ready';\n if (source?.origin === 'sourceMeta') return 'notCooked';\n return 'unknown';\n}\n\n/** Join source, locator, receipt, package, artifact, and runtime facts into one view. */\nexport function projectAssetEvidence(\n inputs: AssetEvidenceInputs,\n): Result<AssetEvidence, AssetEvidenceError> {\n const sourceResult = chooseSource(inputs);\n if (!sourceResult.ok) return sourceResult;\n const locatorResult = chooseLocator(inputs);\n if (!locatorResult.ok) return locatorResult;\n const receiptResult = chooseReceipt(inputs);\n if (!receiptResult.ok) return receiptResult;\n\n const source = sourceResult.value;\n const locator = locatorResult.value;\n const receipt = receiptResult.value;\n const packageVerification = packageEvidence(inputs);\n if (\n receipt?.outputDigest !== undefined &&\n packageVerification?.digest !== undefined &&\n receipt.outputDigest !== packageVerification.digest\n ) {\n return conflict(\n 'asset-evidence-digest-mismatch',\n inputs.guid,\n packageVerification.digest,\n receipt.outputDigest,\n );\n }\n\n return ok({\n guid: inputs.guid,\n ...(locator?.packageUrl !== undefined ? { packageUrl: locator.packageUrl } : {}),\n ...(locator?.cookReceiptUrl !== undefined ? { cookReceiptUrl: locator.cookReceiptUrl } : {}),\n ...(source !== undefined ? { source } : {}),\n cook: {\n status: cookStatus(source, receipt),\n freshness: freshness(source, receipt),\n ...(receipt !== undefined ? { receipt } : {}),\n },\n ...(packageVerification !== undefined ? { package: packageVerification } : {}),\n artifacts: artifactsEvidence(inputs),\n runtime: { status: inputs.runtime?.status ?? 'unknown' },\n });\n}\n\nexport type {\n ArtifactVerificationStatus,\n CookFreshness,\n CookStatus,\n RuntimeEvidenceStatus,\n} from './asset.js';\nexport type { ArtifactDescriptor, ContentEncoding, Integrity };\n","// @forgeax/engine-types - Handle<T,M> brand + AssetTagMap + TagOf + 3 helpers SSOT.\n//\n// Single physical source-of-truth (feat-20260517-handle-type-unify M1 / D-2 / D-4 / D-7).\n// The package barrel `index.ts` re-exports this file; AI users import the\n// brand and helpers via `@forgeax/engine-types`, and IDE hover lands on this\n// file (charter F1 single-entry indexability).\n//\n// Contents (charter P4 consistent abstraction - 5 co-located building blocks):\n// - type Handle<T extends string, M extends 'unique' | 'shared'>\n// (double-axis phantom brand on top of `number`)\n// - type UniqueHandle<T> / SharedHandle<T> (mode-pinned aliases)\n// - interface AssetTagMap (14-member closed map mesh/texture/cube-texture/sampler/material/scene/audio/skin/skeleton/animation-clip/shader/font/render-pipeline/tileset/video)\n// - type TagOf<T extends Asset> (distributive conditional - 14+1 never tail)\n// - function toUnique<T>(raw) / toShared<T>(raw) (brand creation factories)\n// - function unwrapHandle<T,M>(h) (brand removal helper - cast inverse)\n//\n// The single `as Handle<T, M>` cast inside each factory is the brand-creation\n// structural cast (D-7 + AC-01 exemption); all other call sites must route\n// through these factories or `unwrapHandle` so that no `as unknown as Handle`\n// or `as unknown as number` literal survives anywhere outside this file\n// (AC-01 grep gate, M3 / M4 cleanup).\n//\n// Charter mapping: F1 (single-entry IDE autocomplete from\n// `@forgeax/engine-types`) + P3 (cross-mode rejection is a TS compile-time\n// failure red line) + P4 (consistent abstraction: brand + map + 3 factories\n// + 1 distributive conditional all co-located in this 1 file).\n\nimport type { Asset } from './index';\n\n/**\n * Phantom-branded Handle: a `number` carrying two type tags.\n *\n * @typeParam T - asset target tag (string literal, e.g. `'MeshAsset'`)\n * @typeParam M - release mode: `'unique'` (ECS-tracked via UniqueRefStore)\n * or `'shared'` (external owner, e.g. `AssetRegistry`)\n *\n * Runtime representation is a u32 number so the GPU upload path\n * (`GPUBuffer.writeBuffer(slot, ...)`) keeps zero-cost passthrough; only the\n * TS layer enforces non-assignability across modes / targets via the\n * `__handle` phantom field. The `__handle` field is type-only - runtime\n * objects never carry it (charter P4 zero-overhead abstraction).\n *\n * Cross-tag rejection: `Handle<'MeshAsset', M>` is not assignable to\n * `Handle<'TextureAsset', M>` and vice versa (the brand `target` field\n * differs).\n *\n * Cross-mode rejection: `Handle<T, 'unique'>` is not assignable to\n * `Handle<T, 'shared'>` and vice versa (the brand `mode` field differs);\n * this is the TS compile-time wall that prevents accidentally feeding a\n * unique-mode handle to a registry that owns its own release lifecycle (charter\n * P3 explicit failure red line; tests live in\n * `packages/types/src/__tests__/handle-brand.test-d.ts` and\n * `packages/ecs/src/__tests__/handle.test-d.ts`).\n *\n * AI users do not write `as Handle<...>` - handles come from registry\n * factories (`engine.assets.register<T>(asset).unwrap()` produces\n * `Handle<TagOf<T>, 'shared'>`; `world.uniqueRefs.alloc<T>(value)`\n * produces `Handle<T, 'unique'>` after M2). The only `as Handle` literal in the\n * codebase is the brand-creation cast inside `toUnique` / `toShared`\n * below (AC-01 exemption).\n */\nexport type Handle<T extends string, M extends 'unique' | 'shared'> = number & {\n readonly __handle: { readonly target: T; readonly mode: M };\n};\n\n/**\n * Convenience alias - unique-mode handle for asset target `T`.\n *\n * Intended for ECS-internal consumption (column slot read sites in the\n * unique-ref store, M2 rename). The `@forgeax/engine-ecs` barrel does NOT\n * re-export this alias name (AC-15 grep gate - keeps the AI-facing surface\n * narrow); callers outside ecs continue to write `Handle<T, 'unique'>`\n * literally.\n *\n * Schema vocab `'unique<T>'` derives the column field type to this alias via\n * `FieldValueType<T>` conditional inference (see\n * `packages/ecs/src/component.ts`).\n */\nexport type UniqueHandle<T extends string> = Handle<T, 'unique'>;\n\n/**\n * Convenience alias - shared-mode handle for asset target `T`.\n *\n * Mirrors `UniqueHandle<T>` for the refcounted-owner side; surfaces on\n * `AssetRegistry.register<T>` return signatures and `MeshFilter.assetHandle`\n * column type. Re-exported by the `@forgeax/engine-ecs` barrel (alongside\n * `Handle`) so AI users importing from ecs see the alias - this remains\n * subordinate to writing `Handle<T, 'shared'>` literally.\n *\n * Schema vocab `'shared<T>'` derives the column field type to this alias\n * via `FieldValueType<T>` conditional inference (feat-20260614 M5).\n */\nexport type SharedHandle<T extends string> = Handle<T, 'shared'>;\n\n/**\n * Asset.kind tag map - 13-member closed map keying each Asset variant\n * `kind` literal to its TS type name string literal (D-1 path (a)).\n *\n * Used by `TagOf<T>` distributive conditional below to derive the brand\n * `target` tag from an Asset variant TS type at register / inference time;\n * AI users adding a new Asset variant minor-add the corresponding\n * `kind -> 'XxxAsset'` row here so that `register<NewVariant>(asset)` returns\n * the correct `Handle<'XxxAsset', 'shared'>` automatically (this map is\n * the single must-edit point per Asset addition - charter F1 single-entry\n * indexability).\n *\n * The 13 members align byte-for-byte with the closed `Asset` union; adding\n * a new member to `Asset` without adding a row here surfaces as a\n * `TagOf<NewAsset>` resolving to `never` (charter P3 explicit failure -\n * downstream `register<NewAsset>` calls fail to compile).\n */\nexport interface AssetTagMap {\n mesh: 'MeshAsset';\n texture: 'TextureAsset';\n equirect: 'EquirectAsset';\n sampler: 'SamplerAsset';\n material: 'MaterialAsset';\n scene: 'SceneAsset';\n audio: 'AudioClipAsset';\n /** feat-20260523-skin-skeleton-animation M0 */\n skin: 'SkinAsset';\n /** feat-20260523-skin-skeleton-animation M0 */\n skeleton: 'SkeletonAsset';\n /** feat-20260523-skin-skeleton-animation M0 */\n 'animation-clip': 'AnimationClip';\n /** feat-20260713-animation-state-machine-plugin M2 / w13 */\n 'animation-graph': 'AnimationGraph';\n /** feat-20260528-material-shader-registration-unification M1 / w1 */\n /** feat-20260531-world-space-msdf-text-rendering M2 / w5 */\n font: 'FontAsset';\n /** feat-20260601-customizable-render-pipeline-seam-and-dogfood-rend M1 / w5 */\n 'render-pipeline': 'RenderPipelineAsset';\n /** feat-20260608-tilemap-object-layer-rendering M0 baseline rebuild */\n tileset: 'TilesetAsset';\n /** feat-20260623-world-space-video-asset M1 / w2 */\n video: 'VideoAsset';\n /** feat-20260728-wave1-vfx-contract-and-asset-cook M1 / m1-i1 */\n 'particle-effect': 'ParticleEffectAsset';\n}\n\n/**\n * Distributive conditional - maps an Asset variant TS type to its brand\n * `target` tag string literal (D-1 path (a)).\n *\n * `TagOf<MeshAsset>` resolves to `'MeshAsset'`; `TagOf<MaterialAsset>`\n * resolves to `'MaterialAsset'` even though `MaterialAsset` is itself the\n * pass-based single interface (MaterialAsset) - the\n * distributive conditional resolves to 'MaterialAsset' via kind: 'material'\n * collapse onto `'MaterialAsset'` because both share `kind: 'material'`\n * (research Finding 2).\n *\n * Asset variants without a matching `AssetTagMap` row (or a `kind` literal\n * outside the 5 closed values) resolve to `never`, surfacing the missing\n * row at every downstream `register<T>` consumer site (charter P3 explicit\n * failure).\n */\nexport type TagOf<T extends Asset> = T extends { kind: infer K }\n ? K extends keyof AssetTagMap\n ? AssetTagMap[K]\n : never\n : never;\n\n/**\n * Construct a `Handle<T, 'unique'>` from a raw u32. Brand-creation\n * structural cast - the `as Handle<T, 'unique'>` literal here is the\n * AC-01 exemption single point of brand creation (D-7); all other call\n * sites must route through this factory.\n *\n * Used by `World.allocUniqueRef<T>(value)` (M2 rename of allocUniqueRef)\n * to brand fresh handles that the ECS will track via the per-row release\n * loop. AI users typically\n * do not call this directly - it is the brand-creation primitive that the\n * ecs / runtime layers wrap.\n */\nexport function toUnique<T extends string>(raw: number): Handle<T, 'unique'> {\n return raw as Handle<T, 'unique'>;\n}\n\n/**\n * Construct a `Handle<T, 'shared'>` from a raw u32. Brand-creation\n * structural cast - the `as Handle<T, 'shared'>` literal here is the\n * AC-01 exemption single point of brand creation (D-7).\n *\n * Used by `AssetRegistry.register<T>(asset).unwrap()` to brand the returned handle\n * with `Handle<TagOf<T>, 'shared'>`, and by builtin handle constants\n * (`HANDLE_CUBE` / `HANDLE_TRIANGLE` / `HANDLE_ROOM_CUBE` / `BUILTIN_HANDLE_*`)\n * to brand compile-time u32 literals without caller-side `as unknown as`\n * casts (AC-05).\n */\nexport function toShared<T extends string>(raw: number): Handle<T, 'shared'> {\n return raw as Handle<T, 'shared'>;\n}\n\n/**\n * Remove the `Handle<T, M>` brand and recover the raw u32 carried inside.\n * Brand-removal helper - the inverse of `toUnique` / `toShared`.\n *\n * Runtime is identity (the brand `__handle` field is type-only; the\n * underlying number value is unchanged); the helper exists purely to\n * collapse all `as unknown as number` cast sites into a single function so\n * that AC-01 grep can surface stragglers (D-7 / D-8 cast collapse plan).\n *\n * Public on the types barrel (parallel to `toUnique` / `toShared`):\n * column read sites in unique-ref-store (M2 rename) / scene-instance-container,\n * AssetRegistry internal `Map<number, ...>` key reads, and any AI-user\n * code that needs to bridge a branded handle to a numeric ABI all call\n * this. AI users on the typical spawn-site / register-site surface\n * usually do not need it (charter P1 progressive disclosure — handle\n * stays branded end-to-end), but when a numeric escape is required this\n * is the single sanctioned escape.\n */\nexport function unwrapHandle<T extends string, M extends 'unique' | 'shared'>(\n h: Handle<T, M>,\n): number {\n return h;\n}\n\n/**\n * Slot boundary between the builtin tier and the user tier (feat-20260614 M6\n * D-15 / D-16). Builtin asset handles (the 5 process-static meshes:\n * HANDLE_CUBE=1 .. HANDLE_NINESLICE_QUAD=5) occupy slots `[1, BUILTIN_BASE)`;\n * user-tier handles minted by `World.sharedRefs.alloc` start at `BUILTIN_BASE`.\n *\n * Defined here in `@forgeax/engine-types` — the single dependency shared by\n * both `@forgeax/engine-ecs` (SharedRefStore `nextSlot` init + builtin-slot\n * fail-fast) and `@forgeax/engine-runtime` (BuiltinAssetRegistry resolve\n * dispatch + AssetRegistry index) — so the boundary is one named constant with\n * no cross-package circular dependency. Value 1024 is the historic\n * `FIRST_USER_HANDLE` literal, promoted to the shared SSOT.\n */\nexport const BUILTIN_BASE = 1024;\n\n// === Gen-slot codec SSOT (feat-20260623-asset-handle-generation M1 / w2) ==========\n//\n// Domain-agnostic pure bit operations — max-slot, max-gen, pack, unpack-slot,\n// unpack-gen, and the retire predicate (isRetiredSlot: gen > MAX_GEN, so gen\n// 255 is usable and a slot retires only when its bump would reach 256). This\n// is the single definition point\n// for the `(gen << 24) | slot` layout across entity and asset handles (D-1,\n// AC-15). Callers (ecs / runtime / ref stores) import from\n// @forgeax/engine-types; entity-side overflow throw and sentinel stay in ecs\n// (D-1). No domain concepts live here — just mask and shift.\n//\n// Bit layout (OOS-3: 32-bit number, 24-bit slot + 8-bit gen):\n// - slot: bits [0, 23], max value (1 << 24) - 1 = 16_777_215\n// - gen: bits [24, 31], max value 0xff = 255\n//\n// pack(slot, gen) fixed to (((gen & 0xff) << 24) | (slot & 0xffffff)) >>> 0\n// per D-7 hard constraint — the `>>> 0` prevents ToInt32 negative when\n// gen >= 128 (entity-handle.ts:73 comment documents this trap).\n\n/** Maximum slot index (2^24 - 1 = 16_777_215). */\nexport const MAX_SLOT = (1 << 24) - 1;\n\n/** Maximum generation value (2^8 - 1 = 255). */\nexport const MAX_GEN = 0xff;\n\n/**\n * Pack (slot, gen) into a u32 handle.\n *\n * The caller is responsible for ensuring slot does not exceed MAX_SLOT;\n * values above 24 bits are masked by `slot & 0xffffff`. Generation is\n * masked to 8 bits via `gen & 0xff`. The `>>> 0` forces unsigned u32\n * representation — without it, gen >= 128 produces a signed-negative\n * ToInt32 (D-7 hard constraint, entity-handle.ts:73).\n */\nexport function pack(slot: number, gen: number): number {\n return (((gen & 0xff) << 24) | (slot & 0xffffff)) >>> 0;\n}\n\n/** Extract the low 24 bits (slot) from a packed u32 handle. */\nexport function unpackSlot(v: number): number {\n return v & 0xffffff;\n}\n\n/** Extract the high 8 bits (generation) from a packed u32 handle. */\nexport function unpackGen(v: number): number {\n return (v >>> 24) & 0xff;\n}\n\n/**\n * Retire-when-gen-exceeds-MAX_GEN semantic: returns `true` when gen has\n * exceeded MAX_GEN (255), i.e. gen 255 is still a usable handle; a slot\n * retires only when its bumped generation reaches 256. A retired slot\n * never returns to the free list (AC-07).\n */\nexport function isRetiredSlot(gen: number): boolean {\n return gen > MAX_GEN;\n}\n\n// === Handle inspection helpers (feat-20260623-asset-handle-generation M1 / w2) ====\n//\n// Thin wrappers over the shared codec for Handle<T, M> consumers. handleSlot\n// and handleGeneration internally call unpackSlot / unpackGen — these are the\n// migration targets for unwrapHandle sites that only need the slot or gen\n// (decision q4). unwrapHandle stays as-is for sites that need the full\n// encoded value (D-5 excluded round-trip sites).\n\n/**\n * Extract the slot (low 24 bits) from a branded Handle.\n *\n * This is the runtime identity of the handle — the slot index that maps to\n * store payloads / GPU resource keys. Call sites that currently use\n * `unwrapHandle(h)` as a Map key should migrate here so the key stays stable\n * when gen > 0 (AC-09).\n */\nexport function handleSlot<T extends string, M extends 'unique' | 'shared'>(\n h: Handle<T, M>,\n): number {\n return unpackSlot(h as unknown as number);\n}\n\n/**\n * Extract the generation (high 8 bits) from a branded Handle.\n *\n * Used by store-level gen comparisons (resolve/retain/release) to detect\n * stale handles — the gen embedded during alloc is compared against the\n * store's current gen for the same slot.\n */\nexport function handleGeneration<T extends string, M extends 'unique' | 'shared'>(\n h: Handle<T, M>,\n): number {\n return unpackGen(h as unknown as number);\n}\n","import type { AssetGuid } from '../index.js';\nimport type { MaterialColorSpace } from './color-space.js';\n\nexport type MaterialParameterType =\n | 'bool'\n | 'f32'\n | 'i32'\n | 'u32'\n | 'vec2'\n | 'vec3'\n | 'vec4'\n | 'color'\n | 'texture';\n\nexport interface MaterialParameter {\n readonly name: string;\n readonly type: MaterialParameterType;\n /**\n * Asset-side transfer function for an authored color value. `color`\n * parameters default to `srgb`; numeric vectors default to linear unless\n * explicitly tagged. An asset-level `colorSpace` overrides this schema\n * default. Runtime material values are always linear.\n */\n readonly colorSpace?: MaterialColorSpace;\n readonly default?: MaterialValue;\n readonly optional?: boolean;\n readonly static?: boolean;\n}\n\nexport interface MaterialTextureCoordinates {\n readonly set?: number;\n /** Producer-owned logical-to-physical texture extent correction. */\n readonly physicalUvScale?: readonly [number, number];\n readonly transform?: {\n readonly offset?: readonly [number, number];\n readonly scale?: readonly [number, number];\n readonly rotation?: number;\n };\n}\n\nexport interface ResolvedMaterialTextureCoordinates {\n readonly set: number;\n readonly transform: {\n readonly offset: readonly [number, number];\n readonly scale: readonly [number, number];\n readonly rotation: number;\n };\n}\n\nexport function resolveMaterialTextureCoordinates(\n coordinates?: MaterialTextureCoordinates,\n): ResolvedMaterialTextureCoordinates {\n return {\n set: coordinates?.set ?? 0,\n transform: {\n offset: coordinates?.transform?.offset ?? [0, 0],\n scale: coordinates?.transform?.scale ?? [1, 1],\n rotation: coordinates?.transform?.rotation ?? 0,\n },\n };\n}\n\nexport type MaterialTextureReference = AssetGuid | number | string;\n\nexport interface MaterialTextureValue {\n readonly texture: MaterialTextureReference;\n readonly sampler?: MaterialTextureReference;\n readonly coordinates?: MaterialTextureCoordinates;\n readonly normalScale?: number;\n readonly occlusionStrength?: number;\n}\n\nexport type MaterialValue = boolean | number | readonly number[] | string | MaterialTextureValue;\n\nexport interface MaterialProgram {\n readonly module: string;\n readonly vertexEntry?: string;\n readonly fragmentEntry?: string;\n readonly moduleSlots?: Readonly<Record<string, string>>;\n}\n\nexport interface MaterialPass {\n readonly name: string;\n readonly program: MaterialProgram;\n readonly renderState?: Readonly<Record<string, unknown>>;\n}\n\nexport type MaterialPassList = readonly [MaterialPass, ...MaterialPass[]];\n\nexport interface MaterialAsset {\n readonly kind: 'material';\n /**\n * Transfer function override for all authored color parameters. Omitted\n * parameters ultimately default to sRGB.\n * Explicit `linear` is reserved for physical/imported data such as glTF\n * factors. Numeric values are never rewritten when this metadata changes.\n */\n readonly colorSpace?: MaterialColorSpace;\n readonly parent?: AssetGuid;\n readonly passes?: MaterialPassList;\n readonly parameters?: readonly MaterialParameter[];\n readonly values?: Readonly<Record<string, MaterialValue | null>>;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Validate JSON-authored material descriptors at their loading boundary. */\nexport function assertMaterialAsset(\n value: unknown,\n context = 'material',\n): asserts value is MaterialAsset {\n if (!isRecord(value) || value.kind !== 'material') {\n throw new Error(`${context}: expected a material asset`);\n }\n if (\n value.colorSpace !== undefined &&\n value.colorSpace !== 'srgb' &&\n value.colorSpace !== 'linear'\n ) {\n throw new Error(`${context}: invalid colorSpace`);\n }\n if (value.passes !== undefined) {\n if (!Array.isArray(value.passes) || value.passes.length === 0) {\n throw new Error(`${context}: passes must be a non-empty array`);\n }\n for (const [index, pass] of value.passes.entries()) {\n if (!isRecord(pass) || typeof pass.name !== 'string' || !isRecord(pass.program)) {\n throw new Error(`${context}: pass ${index} is malformed`);\n }\n if (typeof pass.program.module !== 'string' || pass.program.module.length === 0) {\n throw new Error(`${context}: pass ${index} has no module identity`);\n }\n if (\n (pass.program.vertexEntry !== undefined && typeof pass.program.vertexEntry !== 'string') ||\n (pass.program.fragmentEntry !== undefined && typeof pass.program.fragmentEntry !== 'string')\n ) {\n throw new Error(`${context}: pass ${index} has malformed entry points`);\n }\n if (pass.program.moduleSlots !== undefined) {\n if (!isRecord(pass.program.moduleSlots)) {\n throw new Error(`${context}: pass ${index} has malformed module slots`);\n }\n for (const [name, slot] of Object.entries(pass.program.moduleSlots)) {\n if (typeof slot !== 'string')\n throw new Error(`${context}: module slot ${name} is not a string`);\n }\n }\n }\n }\n if (value.parameters !== undefined) {\n if (!Array.isArray(value.parameters))\n throw new Error(`${context}: parameters must be an array`);\n for (const [index, parameter] of value.parameters.entries()) {\n if (\n !isRecord(parameter) ||\n typeof parameter.name !== 'string' ||\n typeof parameter.type !== 'string'\n ) {\n throw new Error(`${context}: parameter ${index} is malformed`);\n }\n if (\n parameter.colorSpace !== undefined &&\n parameter.colorSpace !== 'srgb' &&\n parameter.colorSpace !== 'linear'\n ) {\n throw new Error(`${context}: parameter ${index} has invalid colorSpace`);\n }\n }\n }\n}\n","import type { MaterialValue } from './asset.js';\n\n/** Transfer function attached to an authored material color. */\nexport type MaterialColorSpace = 'srgb' | 'linear';\n\n/** Minimal schema shape needed to identify authored color values. */\nexport interface MaterialColorParameterSchema {\n readonly name: string;\n readonly type: string;\n readonly colorSpace?: MaterialColorSpace;\n}\n\n/** IEC 61966-2-1 sRGB electro-optical transfer function. */\nexport function srgbChannelToLinear(value: number): number {\n return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;\n}\n\n/** IEC 61966-2-1 inverse transfer function. */\nexport function linearChannelToSrgb(value: number): number {\n return value <= 0.0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - 0.055;\n}\n\n/**\n * Decode an authored RGB/RGBA value for linear runtime use.\n *\n * Only the first three color channels are transformed. Alpha and any\n * additional lanes are data and pass through unchanged.\n */\nexport function authoredColorToLinear(\n value: readonly number[],\n colorSpace: MaterialColorSpace = 'srgb',\n): number[] {\n if (colorSpace === 'linear') return [...value];\n return value.map((channel, index) => (index < 3 ? srgbChannelToLinear(channel) : channel));\n}\n\n/**\n * Resolve a material parameter's asset-side transfer-function contract.\n * `color` is an authored color and therefore defaults to sRGB. Numeric\n * vectors remain linear unless their schema explicitly marks them as colors.\n */\nexport function materialParameterColorSpace(\n parameter: MaterialColorParameterSchema,\n assetColorSpace?: MaterialColorSpace,\n): MaterialColorSpace | undefined {\n const isColor = parameter.type === 'color' || parameter.colorSpace !== undefined;\n if (!isColor) return undefined;\n return assetColorSpace ?? parameter.colorSpace ?? 'srgb';\n}\n\n/**\n * Project authored MaterialAsset values into a fresh runtime value map.\n * Asset values are never mutated, so repeated extraction cannot compound the\n * transfer function.\n */\nexport function materialValuesToLinearRuntime(\n values: Readonly<Record<string, MaterialValue | null>> | undefined,\n parameters: readonly MaterialColorParameterSchema[],\n assetColorSpace?: MaterialColorSpace,\n): Readonly<Record<string, MaterialValue | null>> {\n if (values === undefined) return {};\n const colorSpaces = new Map<string, MaterialColorSpace>();\n for (const parameter of parameters) {\n const colorSpace = materialParameterColorSpace(parameter, assetColorSpace);\n if (colorSpace !== undefined) colorSpaces.set(parameter.name, colorSpace);\n }\n\n const runtimeValues: Record<string, MaterialValue | null> = {};\n for (const [name, value] of Object.entries(values)) {\n const colorSpace = colorSpaces.get(name);\n runtimeValues[name] =\n colorSpace !== undefined && Array.isArray(value)\n ? authoredColorToLinear(value, colorSpace)\n : value;\n }\n return runtimeValues;\n}\n","export const MATERIAL_ERROR_CODES = [\n 'material-parent-not-found',\n 'material-circular-inheritance',\n 'material-no-effective-pass',\n 'material-value-unknown',\n 'material-value-type-mismatch',\n 'material-contract-program-mismatch',\n 'shader-module-id-missing',\n 'shader-module-id-duplicate',\n 'shader-module-not-found',\n 'shader-module-namespace-reserved',\n 'material-reflection-binding-mismatch',\n 'material-specialization-not-cooked',\n 'material-specialization-stale-generation',\n 'gltf-material-uv-set-missing',\n 'material-derived-interface-mismatch',\n 'material-texture-coordinate-invalid',\n 'material-payload-bounds',\n] as const;\n\nexport type MaterialErrorCode = (typeof MATERIAL_ERROR_CODES)[number];\n\nexport interface MaterialGenerationVector {\n readonly dependencies: Readonly<Record<string, number>>;\n}\n\nexport interface MaterialParentNotFoundDetail {\n readonly code: 'material-parent-not-found';\n readonly leaf: string;\n readonly missingParent: string;\n readonly chain: readonly string[];\n}\n\nexport interface MaterialCircularInheritanceDetail {\n readonly code: 'material-circular-inheritance';\n readonly leaf: string;\n readonly chain: readonly string[];\n}\n\nexport interface MaterialNoEffectivePassDetail {\n readonly code: 'material-no-effective-pass';\n readonly material: string;\n}\n\nexport interface MaterialValueUnknownDetail {\n readonly code: 'material-value-unknown';\n readonly material: string;\n readonly parameter: string;\n}\n\nexport interface MaterialValueTypeMismatchDetail {\n readonly code: 'material-value-type-mismatch';\n readonly material: string;\n readonly parameter: string;\n readonly expectedType: string;\n readonly actualType: string;\n}\n\nexport interface MaterialContractProgramMismatchDetail {\n readonly code: 'material-contract-program-mismatch';\n readonly material: string;\n readonly pass: string;\n readonly program: string;\n readonly expectedProgram: string;\n}\n\nexport interface ShaderModuleIdMissingDetail {\n readonly code: 'shader-module-id-missing';\n readonly source: string;\n}\n\nexport interface ShaderModuleIdDuplicateDetail {\n readonly code: 'shader-module-id-duplicate';\n readonly module: string;\n readonly sources: readonly string[];\n}\n\nexport interface ShaderModuleNotFoundDetail {\n readonly code: 'shader-module-not-found';\n readonly module: string;\n readonly source: string;\n}\n\nexport interface ShaderModuleNamespaceReservedDetail {\n readonly code: 'shader-module-namespace-reserved';\n readonly module: string;\n readonly namespace: string;\n}\n\nexport interface MaterialReflectionBindingMismatchDetail {\n readonly code: 'material-reflection-binding-mismatch';\n readonly material: string;\n readonly pass: string;\n readonly parameter: string;\n readonly expected: string;\n readonly actual: string;\n}\n\nexport interface MaterialSpecializationNotCookedDetail {\n readonly code: 'material-specialization-not-cooked';\n readonly material: string;\n readonly staticSelection: readonly string[];\n}\n\nexport interface MaterialSpecializationStaleGenerationDetail {\n readonly code: 'material-specialization-stale-generation';\n readonly material: string;\n readonly dependencies: readonly string[];\n readonly observed: MaterialGenerationVector;\n readonly current: MaterialGenerationVector;\n}\n\nexport interface GltfMaterialUvSetMissingDetail {\n readonly material: string;\n readonly primitive: string;\n readonly slot: string;\n readonly requestedSet: number;\n readonly availableSets: readonly number[];\n}\n\nexport interface MaterialDerivedInterfaceMismatchDetail {\n readonly code: 'material-derived-interface-mismatch';\n readonly stage: 'compile' | 'cook' | 'extract' | 'record';\n readonly material: string;\n readonly layoutIdentity: string;\n readonly expectedIdentity?: string;\n readonly actualIdentity?: string;\n readonly parameter?: string;\n readonly action: 'recook';\n}\n\nexport interface MaterialTextureCoordinateInvalidDetail {\n readonly code: 'material-texture-coordinate-invalid';\n readonly stage: 'compile' | 'cook' | 'extract' | 'record';\n readonly material: string;\n readonly layoutIdentity: string;\n readonly parameter: string;\n readonly slot: string;\n readonly reason: 'missing' | 'non-finite' | 'shape';\n readonly action: 'recook';\n}\n\nexport interface MaterialPayloadBoundsDetail {\n readonly code: 'material-payload-bounds';\n readonly stage: 'compile' | 'cook' | 'extract' | 'record';\n readonly material: string;\n readonly layoutIdentity: string;\n readonly slot: string;\n readonly byteOffset: number;\n readonly byteLength: number;\n readonly payloadBytes: number;\n readonly action: 'stop-draw';\n}\n\ninterface MaterialErrorDetailByCode {\n readonly 'material-parent-not-found': MaterialParentNotFoundDetail;\n readonly 'material-circular-inheritance': MaterialCircularInheritanceDetail;\n readonly 'material-no-effective-pass': MaterialNoEffectivePassDetail;\n readonly 'material-value-unknown': MaterialValueUnknownDetail;\n readonly 'material-value-type-mismatch': MaterialValueTypeMismatchDetail;\n readonly 'material-contract-program-mismatch': MaterialContractProgramMismatchDetail;\n readonly 'shader-module-id-missing': ShaderModuleIdMissingDetail;\n readonly 'shader-module-id-duplicate': ShaderModuleIdDuplicateDetail;\n readonly 'shader-module-not-found': ShaderModuleNotFoundDetail;\n readonly 'shader-module-namespace-reserved': ShaderModuleNamespaceReservedDetail;\n readonly 'material-reflection-binding-mismatch': MaterialReflectionBindingMismatchDetail;\n readonly 'material-specialization-not-cooked': MaterialSpecializationNotCookedDetail;\n readonly 'material-specialization-stale-generation': MaterialSpecializationStaleGenerationDetail;\n readonly 'gltf-material-uv-set-missing': GltfMaterialUvSetMissingDetail;\n readonly 'material-derived-interface-mismatch': MaterialDerivedInterfaceMismatchDetail;\n readonly 'material-texture-coordinate-invalid': MaterialTextureCoordinateInvalidDetail;\n readonly 'material-payload-bounds': MaterialPayloadBoundsDetail;\n}\n\nexport type MaterialErrorDetail = MaterialErrorDetailByCode[MaterialErrorCode];\n\nexport type MaterialErrorFor<C extends MaterialErrorCode> = {\n readonly code: C;\n readonly expected: string;\n readonly hint: string;\n readonly detail: MaterialErrorDetailByCode[C];\n readonly message: string;\n};\n\nexport type MaterialError = {\n [C in MaterialErrorCode]: MaterialErrorFor<C>;\n}[MaterialErrorCode];\n\nconst MATERIAL_ERROR_POLICY = {\n 'material-parent-not-found': {\n expected: 'every parent GUID resolves to a MaterialAsset',\n hint: 'fix the parent GUID and resolve the material again',\n },\n 'material-circular-inheritance': {\n expected: 'the parent chain is acyclic',\n hint: 'remove the repeated GUID from the parent chain',\n },\n 'material-no-effective-pass': {\n expected: 'the resolved material has at least one pass',\n hint: 'add a pass to the root material or an inherited parent',\n },\n 'material-value-unknown': {\n expected: 'every value name is declared by the effective contract',\n hint: 'remove the value or declare the parameter in the root contract',\n },\n 'material-value-type-mismatch': {\n expected: 'each value matches its declared parameter type',\n hint: 'change the value to the declared parameter type',\n },\n 'material-contract-program-mismatch': {\n expected: 'the program satisfies the material contract',\n hint: 'align the program entries with the root contract',\n },\n 'shader-module-id-missing': {\n expected: 'each WGSL source declares a module ID',\n hint: 'add a compiler-native module ID declaration to the WGSL source',\n },\n 'shader-module-id-duplicate': {\n expected: 'each module ID has one source provenance',\n hint: 'rename one module or remove the duplicate source',\n },\n 'shader-module-not-found': {\n expected: 'every referenced module exists in the source catalog',\n hint: 'add the module to the source catalog or fix the reference',\n },\n 'shader-module-namespace-reserved': {\n expected: 'user modules use a non-reserved namespace',\n hint: 'choose a module ID outside the reserved namespace',\n },\n 'material-reflection-binding-mismatch': {\n expected: 'reflection matches the material contract bindings',\n hint: 'update the contract or WGSL binding and cook again',\n },\n 'material-specialization-not-cooked': {\n expected: 'the requested specialization has a cooked artifact',\n hint: 'run the build or development cook path for this selection',\n },\n 'material-specialization-stale-generation': {\n expected: 'all specialization dependencies share one generation',\n hint: 'retry after dependent assets and sources settle',\n },\n 'gltf-material-uv-set-missing': {\n expected: 'each texture slot references an available primitive UV set',\n hint: 'add the requested UV set to the primitive and re-import it',\n },\n 'material-derived-interface-mismatch': {\n expected: 'the generated material interface matches the derived schema interface',\n hint: 'repair the schema or WGSL producer and recook the material',\n },\n 'material-texture-coordinate-invalid': {\n expected: 'every texture coordinate record is finite and complete',\n hint: 'repair the texture metadata or coordinates and recook the material',\n },\n 'material-payload-bounds': {\n expected: 'every material payload write stays within the derived payload',\n hint: 'repair the derived payload owner before submitting the draw',\n },\n} satisfies {\n readonly [C in MaterialErrorCode]: {\n readonly expected: string;\n readonly hint: string;\n };\n};\n\nexport const MATERIAL_ERROR_EXPECTED: Readonly<Record<MaterialErrorCode, string>> =\n Object.fromEntries(\n MATERIAL_ERROR_CODES.map((code) => [code, MATERIAL_ERROR_POLICY[code].expected]),\n ) as Readonly<Record<MaterialErrorCode, string>>;\n\nexport const MATERIAL_ERROR_HINTS: Readonly<Record<MaterialErrorCode, string>> = Object.fromEntries(\n MATERIAL_ERROR_CODES.map((code) => [code, MATERIAL_ERROR_POLICY[code].hint]),\n) as Readonly<Record<MaterialErrorCode, string>>;\n\nexport function createMaterialError<C extends MaterialErrorCode>(\n code: C,\n detail: MaterialErrorDetailByCode[C],\n message = `${code}: ${MATERIAL_ERROR_EXPECTED[code]}`,\n): MaterialErrorFor<C> {\n return {\n code,\n expected: MATERIAL_ERROR_EXPECTED[code],\n hint: MATERIAL_ERROR_HINTS[code],\n detail,\n message,\n };\n}\n","import { err, ok, type Result } from '../result.js';\nimport type { MaterialAsset, MaterialParameter, MaterialPass, MaterialValue } from './asset.js';\nimport type { MaterialError } from './errors.js';\nimport { createMaterialError } from './errors.js';\n\nexport type MaterialTable = Readonly<Record<string, MaterialAsset>>;\n\nexport interface ResolvedMaterial {\n readonly leaf: string;\n readonly chain: readonly string[];\n readonly asset: MaterialAsset;\n}\n\nexport function materialGuidText(value: string | Uint8Array): string {\n return typeof value === 'string'\n ? value\n : Array.from(value, (byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nfunction valueType(value: MaterialValue): string {\n if (typeof value === 'boolean') return 'bool';\n if (typeof value === 'number') return 'number';\n if (typeof value === 'string') return 'string';\n if (Array.isArray(value)) return `vec${value.length}`;\n return 'texture';\n}\n\nfunction parameterTypeMatches(parameter: MaterialParameter, value: MaterialValue): boolean {\n switch (parameter.type) {\n case 'bool':\n return typeof value === 'boolean';\n case 'f32':\n case 'i32':\n case 'u32':\n return typeof value === 'number';\n case 'vec2':\n return Array.isArray(value) && value.length === 2;\n case 'vec3':\n return Array.isArray(value) && value.length === 3;\n case 'vec4':\n case 'color':\n return Array.isArray(value) && value.length === 4;\n case 'texture':\n // Runtime asset handles are branded numbers at the type level. The\n // brand is erased before a material reaches the resolver, so numeric\n // handles must remain valid texture values alongside structured\n // texture descriptors.\n return (\n typeof value === 'string' ||\n (typeof value === 'number' && Number.isInteger(value) && value >= 0) ||\n (typeof value === 'object' && !Array.isArray(value))\n );\n }\n}\n\nfunction validateValues(\n material: string,\n values: Readonly<Record<string, MaterialValue | null>>,\n parameters: readonly MaterialParameter[] | undefined,\n): Result<true, MaterialError> {\n if (parameters === undefined) return ok(true);\n const declarations = new Map(parameters.map((parameter) => [parameter.name, parameter]));\n for (const [name, value] of Object.entries(values)) {\n const parameter = declarations.get(name);\n if (parameter === undefined) {\n return err(\n createMaterialError('material-value-unknown', {\n code: 'material-value-unknown',\n material,\n parameter: name,\n }),\n );\n }\n if (value === null) {\n if (!parameter.optional) {\n return err(\n createMaterialError('material-value-type-mismatch', {\n code: 'material-value-type-mismatch',\n material,\n parameter: name,\n expectedType: parameter.type,\n actualType: 'null',\n }),\n );\n }\n continue;\n }\n if (!parameterTypeMatches(parameter, value)) {\n return err(\n createMaterialError('material-value-type-mismatch', {\n code: 'material-value-type-mismatch',\n material,\n parameter: name,\n expectedType: parameter.type,\n actualType: valueType(value),\n }),\n );\n }\n }\n return ok(true);\n}\n\nfunction mergePasses(\n inherited: readonly MaterialPass[] | undefined,\n override: readonly MaterialPass[] | undefined,\n): readonly MaterialPass[] | undefined {\n if (inherited === undefined && override === undefined) return undefined;\n const passes = [...(inherited ?? [])];\n for (const next of override ?? []) {\n const index = passes.findIndex((current) => current.name === next.name);\n if (index === -1) passes.push(next);\n else passes[index] = next;\n }\n return passes;\n}\n\nfunction mergeMaterial(parent: MaterialAsset, child: MaterialAsset): MaterialAsset {\n const values: Record<string, MaterialValue> = {};\n for (const [name, value] of Object.entries(parent.values ?? {})) {\n if (value !== null) values[name] = value;\n }\n for (const [name, value] of Object.entries(child.values ?? {})) {\n if (value === null) delete values[name];\n else values[name] = value;\n }\n const passes = mergePasses(parent.passes, child.passes);\n return {\n kind: 'material',\n ...((child.colorSpace ?? parent.colorSpace) !== undefined\n ? { colorSpace: child.colorSpace ?? parent.colorSpace }\n : {}),\n ...(passes !== undefined && passes.length > 0\n ? { passes: passes as NonNullable<MaterialAsset['passes']> }\n : {}),\n ...(parent.parameters !== undefined ? { parameters: parent.parameters } : {}),\n ...(Object.keys(values).length > 0 ? { values } : {}),\n };\n}\n\nfunction resolveChain(\n id: string,\n leaf: string,\n table: MaterialTable,\n stack: readonly string[],\n): Result<ResolvedMaterial, MaterialError> {\n if (stack.includes(id)) {\n return err(\n createMaterialError('material-circular-inheritance', {\n code: 'material-circular-inheritance',\n leaf,\n chain: [...stack, id],\n }),\n );\n }\n const current = table[id];\n if (current === undefined) {\n return err(\n createMaterialError('material-parent-not-found', {\n code: 'material-parent-not-found',\n leaf,\n missingParent: id,\n chain: [...stack, id],\n }),\n );\n }\n const parent = current.parent === undefined ? undefined : materialGuidText(current.parent);\n if (parent === undefined) {\n if (current.passes === undefined || current.passes.length === 0) {\n return err(\n createMaterialError('material-no-effective-pass', {\n code: 'material-no-effective-pass',\n material: leaf,\n }),\n );\n }\n const values: Record<string, MaterialValue> = {};\n for (const [name, value] of Object.entries(current.values ?? {})) {\n if (value !== null) values[name] = value;\n }\n const valid = validateValues(id, values, current.parameters);\n if (!valid.ok) return valid;\n return ok({ leaf, chain: [id], asset: { ...current, values } });\n }\n\n const parentResult = resolveChain(parent, leaf, table, [...stack, id]);\n if (!parentResult.ok) return parentResult;\n const valid = validateValues(id, current.values ?? {}, parentResult.value.asset.parameters);\n if (!valid.ok) return valid;\n const merged = mergeMaterial(parentResult.value.asset, current);\n if (merged.passes === undefined || merged.passes.length === 0) {\n return err(\n createMaterialError('material-no-effective-pass', {\n code: 'material-no-effective-pass',\n material: leaf,\n }),\n );\n }\n return ok({ leaf, chain: [...parentResult.value.chain, id], asset: merged });\n}\n\nexport function resolveMaterialAsset(\n leaf: string,\n table: MaterialTable,\n): Result<ResolvedMaterial, MaterialError> {\n return resolveChain(leaf, leaf, table, []);\n}\n","import type { CatalogDiagnostic } from './asset-producer.js';\nimport type { CatalogEntry } from './catalog.js';\n\n/** Versioned wire shape shared by the asset producer and browser consumers. */\nexport const RUNTIME_ASSET_BINDING_SCHEMA = 'runtime-asset-binding-v1' as const;\nexport const RUNTIME_CATALOG_SNAPSHOT_SCHEMA = 'runtime-catalog-snapshot-v1' as const;\n\nexport type RuntimeScopeStatus = 'unbound' | 'transitioning' | 'ready' | 'degraded' | 'unavailable';\n\n/**\n * Logical projection of one package-declared asset root into the runtime\n * catalog coordinate space. This carries no filesystem path across the wire.\n */\nexport interface RuntimeCatalogRoot {\n readonly root: string;\n readonly catalogPrefix: string;\n}\n\n/**\n * The only identity a browser-side asset consumer may use for a dev realm.\n * `scopeId` describes ownership; `generation` rejects stale browser work.\n * Filesystem roots intentionally do not cross this wire contract; catalogRoots\n * is only the logical declaration-to-catalog projection used by browser views.\n */\nexport interface RuntimeAssetBinding {\n readonly schemaVersion: typeof RUNTIME_ASSET_BINDING_SCHEMA;\n readonly gameId: string;\n readonly scopeId: string;\n readonly generation: number;\n readonly status: RuntimeScopeStatus;\n readonly catalogUrl: string;\n readonly importUrlBase: string;\n readonly packageUrlBase: string;\n readonly catalogRoots?: readonly RuntimeCatalogRoot[];\n readonly authority?: 'authoritative' | 'degraded';\n readonly diagnostics?: readonly CatalogDiagnostic[];\n}\n\nexport function isRuntimeCatalogRoots(value: unknown): value is readonly RuntimeCatalogRoot[] {\n return (\n Array.isArray(value) &&\n value.every(\n (root) =>\n root !== null &&\n typeof root === 'object' &&\n typeof (root as { root?: unknown }).root === 'string' &&\n typeof (root as { catalogPrefix?: unknown }).catalogPrefix === 'string',\n )\n );\n}\n\n/** Authority-bearing catalog response for one runtime scope. */\nexport interface RuntimeCatalogSnapshot {\n readonly schemaVersion: typeof RUNTIME_CATALOG_SNAPSHOT_SCHEMA;\n readonly scopeId: string;\n readonly generation: number;\n readonly authority: 'authoritative' | 'degraded';\n readonly entries: readonly CatalogEntry[];\n readonly diagnostics: readonly CatalogDiagnostic[];\n}\n\n/**\n * Make a route below the engine's scoped runtime namespace. The helper is\n * deliberately pure so hosts and tests cannot drift on URL construction.\n */\nexport function runtimeScopePath(\n binding: Pick<RuntimeAssetBinding, 'scopeId' | 'generation'>,\n suffix = '',\n): string {\n const normalized = suffix.length === 0 ? '' : `/${suffix.replace(/^\\/+/, '')}`;\n return `/__pack/scopes/${encodeURIComponent(binding.scopeId)}/${binding.generation}${normalized}`;\n}\n\n/**\n * Create the fixed binding used by a standalone Vite game host.\n *\n * A standalone host still has one explicit realm: its game owns the roots and\n * the browser endpoints are derived from the same scope/generation pair. The\n * optional scope override is only for a host-level test server that mounts\n * several standalone entrypoints behind one deliberately shared test realm;\n * production hosts should leave it unset.\n */\nexport function createStandaloneRuntimeAssetBinding(\n gameId: string,\n scopeId = gameId,\n basePath = '',\n): RuntimeAssetBinding {\n const normalizedBase = basePath === '/' ? '' : basePath.replace(/\\/+$/, '');\n const identity = { scopeId, generation: 1 } as const;\n const scopedPath = runtimeScopePath(identity);\n const hostPrefix = normalizedBase.startsWith('/') ? normalizedBase : `/${normalizedBase}`;\n const prefix = normalizedBase.length === 0 ? '' : hostPrefix;\n return {\n schemaVersion: RUNTIME_ASSET_BINDING_SCHEMA,\n gameId,\n scopeId,\n generation: identity.generation,\n status: 'ready',\n catalogUrl: `${prefix}${scopedPath}/catalog.json`,\n importUrlBase: `${prefix}${scopedPath}/import`,\n packageUrlBase: prefix,\n };\n}\n\nexport function runtimeScopeMatches(\n binding: Pick<RuntimeAssetBinding, 'scopeId' | 'generation'> | undefined,\n scopeId: string,\n generation: number,\n): boolean {\n return binding?.scopeId === scopeId && binding.generation === generation;\n}\n","// derive(schema) — paramSchema -> one DerivedMaterialInterface.\n// feat-20260613-material-paramschema-driven-binding M1 / w3\n//\n// Decision anchors (plan-strategy §2):\n// - D-2 single pure function, no side effect; one signature consumed by 3\n// downstream paths (BGL build / UBO record / loader-extract).\n// - D-3 consecutive numeric entries are run-merged into one UBO entry at\n// one binding slot (uniform buffer); std140-aligned offsets.\n// - D-4 every texture* family entry auto-pairs a filtering sampler at\n// binding-1 (sampler emitted FIRST, then texture); sampler /\n// sampler_comparison stay user-declared.\n// - D-7 type set is the 14-literal MaterialParamType union.\n// - D-12 empty schema is graceful: bglEntries=[] / totalBytes=0 / fields empty.\n//\n// std140 alignment table (WGSL uniform):\n// - f32 / i32 / u32 size 4 align 4\n// - vec2<f32> size 8 align 8\n// - vec3<f32> size 12 align 16\n// - vec4 / color (rgba) size 16 align 16\n// - struct round-up: totalBytes is rounded up to 16-byte alignment.\n\nimport type {\n BindGroupLayoutEntry,\n MaterialParamType,\n NumericParamType,\n ParamSchemaEntry,\n TextureBindingParamType,\n} from './index.js';\n\nconst FRAGMENT = 0x2 as GPUShaderStageFlags;\n\nconst NUMERIC_TYPES: ReadonlySet<MaterialParamType> = new Set<MaterialParamType>([\n 'f32',\n 'i32',\n 'u32',\n 'vec2',\n 'vec3',\n 'vec4',\n 'color',\n]);\n\nconst TEXTURE_VIEW_TYPES: ReadonlySet<MaterialParamType> = new Set<MaterialParamType>([\n 'texture2d',\n 'texture_cube',\n 'texture_depth_2d',\n 'texture_cube_array',\n]);\n\nconst SAMPLER_TYPES: ReadonlySet<MaterialParamType> = new Set<MaterialParamType>([\n 'sampler',\n 'sampler_comparison',\n]);\n\nconst ALL_TYPES: ReadonlySet<MaterialParamType> = new Set<MaterialParamType>([\n ...NUMERIC_TYPES,\n ...TEXTURE_VIEW_TYPES,\n ...SAMPLER_TYPES,\n 'storage_buffer',\n]);\n\ninterface NumericFootprint {\n readonly size: number;\n readonly align: number;\n}\n\nfunction numericFootprint(t: NumericParamType): NumericFootprint {\n switch (t) {\n case 'f32':\n case 'i32':\n case 'u32':\n return { size: 4, align: 4 };\n case 'vec2':\n return { size: 8, align: 8 };\n case 'vec3':\n return { size: 12, align: 16 };\n case 'vec4':\n case 'color':\n return { size: 16, align: 16 };\n }\n}\n\nfunction alignUp(value: number, alignment: number): number {\n return (value + alignment - 1) & ~(alignment - 1);\n}\n\ninterface TextureBglDescriptor {\n readonly sampleType: GPUTextureSampleType;\n readonly viewDimension: GPUTextureViewDimension;\n}\n\nfunction textureBglDescriptor(t: TextureBindingParamType): TextureBglDescriptor {\n switch (t) {\n case 'texture2d':\n return { sampleType: 'float', viewDimension: '2d' };\n case 'texture_cube':\n return { sampleType: 'float', viewDimension: 'cube' };\n case 'texture_depth_2d':\n return { sampleType: 'depth', viewDimension: '2d' };\n case 'texture_cube_array':\n return { sampleType: 'float', viewDimension: 'cube-array' };\n case 'sampler':\n case 'sampler_comparison':\n // Not a texture view — caller must dispatch separately. Falling through\n // here is a defensive guard; numericFootprint / sampler handler covers\n // these branches before this function is reached.\n throw new Error(`derive: textureBglDescriptor called on sampler-family type '${t}'`);\n }\n}\n\nfunction samplerBindingType(t: 'sampler' | 'sampler_comparison'): GPUSamplerBindingType {\n return t === 'sampler' ? 'filtering' : 'comparison';\n}\n\n/** Single UBO sub-entry — one merged std140 slot. */\nexport interface UboFieldLayout {\n readonly name: string;\n readonly offset: number;\n readonly size: number;\n readonly type: NumericParamType;\n}\n\nexport interface DerivedNumericMember extends UboFieldLayout {\n readonly alignment: number;\n}\n\nexport interface MaterialCoordinateRecordLayout {\n readonly parameter: string;\n readonly offset: number;\n readonly size: 32;\n readonly alignment: 16;\n readonly transformMember: string;\n readonly metadataMember: string;\n}\n\nexport type MaterialResourceKind = 'sampler' | 'texture' | 'storage-buffer';\n\nexport interface MaterialResourceBindingLayout {\n readonly name: string;\n readonly parameter?: string;\n readonly kind: MaterialResourceKind;\n readonly binding: number;\n}\n\nexport interface MaterialParameterResourceProjection {\n readonly kind: 'sampler' | 'storage-buffer';\n readonly name: string;\n readonly type: 'sampler' | 'sampler_comparison' | 'storage_buffer';\n readonly resource: MaterialResourceBindingLayout;\n readonly coordinates?: never;\n}\n\nexport interface MaterialParameterTextureProjection {\n readonly kind: 'texture';\n readonly name: string;\n readonly type: TextureBindingParamType;\n readonly coordinates: MaterialCoordinateRecordLayout;\n readonly resource: {\n readonly parameter: string;\n readonly texture: MaterialResourceBindingLayout;\n readonly sampler: MaterialResourceBindingLayout;\n };\n}\n\nexport interface MaterialParameterNumericProjection {\n readonly kind: 'numeric';\n readonly name: string;\n readonly type: NumericParamType;\n readonly member: DerivedNumericMember;\n readonly coordinates?: never;\n readonly resource?: never;\n}\n\nexport type MaterialParameterProjection =\n | MaterialParameterNumericProjection\n | MaterialParameterTextureProjection\n | MaterialParameterResourceProjection;\n\nexport interface DerivedMaterialInterface {\n readonly bglEntries: readonly BindGroupLayoutEntry[];\n readonly uboLayout: UboLayout;\n readonly numericMembers: readonly DerivedNumericMember[];\n readonly coordinateRecords: readonly MaterialCoordinateRecordLayout[];\n readonly resourceBindings: readonly MaterialResourceBindingLayout[];\n readonly totalBytes: number;\n readonly layoutIdentity: string;\n readonly textureFieldNames: ReadonlySet<string>;\n readonly samplerForTexture: ReadonlyMap<string, string>;\n readonly userRegionBindingEnd: number;\n}\n\nexport interface UboLayout {\n readonly entries: readonly UboFieldLayout[];\n readonly totalBytes: number;\n}\n\nexport type DeriveOutput = DerivedMaterialInterface;\n\nexport interface ImmutableParamSchemaProjection {\n readonly ownerId: string;\n readonly revision: number;\n readonly schema: readonly ParamSchemaEntry[];\n readonly derivedInterface: DerivedMaterialInterface;\n}\n\nexport interface ParamSchemaProjectionOwnerStats {\n readonly admissions: number;\n readonly derivations: number;\n readonly projections: number;\n}\n\nexport type ParamSchemaDeriveObservationKind =\n | 'admitted-identity-hit'\n | 'unregistered-fallback-derive'\n | 'fallback-layout-sha';\n\nexport interface ParamSchemaDeriveObserver {\n readonly enabled: boolean;\n readonly observe: (event: {\n readonly kind: ParamSchemaDeriveObservationKind;\n readonly site: string;\n }) => void;\n}\n\ninterface OwnedParamSchemaProjection extends ImmutableParamSchemaProjection {\n readonly canonicalSchema: string;\n}\n\n// Runtime consumers can still receive the schema array through older API\n// surfaces. Admission binds that immutable array identity back to the one\n// owner projection, so those compatibility calls return the admitted result\n// without deriving or hashing again. Unregistered schemas keep the pure\n// offline/compiler behavior.\nconst ADMITTED_PARAM_SCHEMA_PROJECTIONS = new WeakMap<\n readonly ParamSchemaEntry[],\n DerivedMaterialInterface\n>();\n\n/**\n * Immutable/revision owner for runtime ParamSchema projections.\n *\n * A new `(ownerId, revision)` performs exactly one pure derivation. Repeating\n * the same admission is idempotent when the schema bytes match and fails loud\n * when a revision is reused for different schema content. The admitted schema\n * is cloned and frozen so caller-side mutation cannot alter a published\n * revision.\n */\nexport class ParamSchemaProjectionOwner {\n readonly #projections = new Map<string, OwnedParamSchemaProjection>();\n #admissions = 0;\n #derivations = 0;\n\n admit(args: {\n readonly ownerId: string;\n readonly revision: number;\n readonly schema: readonly ParamSchemaEntry[];\n }): ImmutableParamSchemaProjection {\n if (args.ownerId.length === 0) {\n throw new Error('ParamSchemaProjectionOwner: ownerId must be non-empty');\n }\n if (!Number.isSafeInteger(args.revision) || args.revision < 1) {\n throw new Error('ParamSchemaProjectionOwner: revision must be a positive safe integer');\n }\n this.#admissions += 1;\n const key = `${args.ownerId}\\u0000${args.revision}`;\n const canonicalSchema = JSON.stringify(args.schema);\n const existing = this.#projections.get(key);\n if (existing !== undefined) {\n if (existing.canonicalSchema !== canonicalSchema) {\n throw new Error(\n `ParamSchemaProjectionOwner: ${args.ownerId}@${args.revision} reused with different schema content`,\n );\n }\n return existing;\n }\n\n const schema = freezeParamSchema(args.schema);\n const derivedInterface = freezeDerivedMaterialInterface(derivePure(schema));\n this.#derivations += 1;\n ADMITTED_PARAM_SCHEMA_PROJECTIONS.set(schema, derivedInterface);\n const projection = Object.freeze({\n ownerId: args.ownerId,\n revision: args.revision,\n schema,\n derivedInterface,\n canonicalSchema,\n });\n this.#projections.set(key, projection);\n return projection;\n }\n\n get(ownerId: string, revision: number): ImmutableParamSchemaProjection | undefined {\n return this.#projections.get(`${ownerId}\\u0000${revision}`);\n }\n\n stats(): ParamSchemaProjectionOwnerStats {\n return Object.freeze({\n admissions: this.#admissions,\n derivations: this.#derivations,\n projections: this.#projections.size,\n });\n }\n}\n\n/**\n * Pure derivation: paramSchema -> BGL entries + UBO byte layout + field maps.\n *\n * The function has no side effects and is the SSOT for BGL / UBO / loader\n * lookup tables (D-2). Runtime / vite-plugin-shader / loader all call into\n * this single entry point; in particular, `userRegionBindingEnd` is the\n * post-user-region binding index that engine-injected groups (shadow / IBL\n * / lightmap, see D-6) must start from.\n *\n * Throws on schema authoring errors:\n * - duplicate entry name (numeric or non-numeric)\n * - unrecognised type literal (not in the 14-member union)\n * - empty entry name\n * - user-declared name collides with the auto-paired `<tex>_sampler`\n */\nexport function derive(schema: readonly ParamSchemaEntry[]): DeriveOutput {\n const admitted = ADMITTED_PARAM_SCHEMA_PROJECTIONS.get(schema);\n if (admitted !== undefined) return admitted;\n return derivePure(schema);\n}\n\n/**\n * Debug-only observation seam for runtime compatibility consumers.\n *\n * This wrapper deliberately does not cache or change derivation semantics. It\n * only records whether the supplied schema identity was admitted by the\n * ParamSchemaProjectionOwner before delegating to the existing pure `derive`\n * function. Keeping the observer out of the normal `derive` signature leaves\n * production callers on the existing zero-argument path.\n */\nexport function deriveObserved(\n schema: readonly ParamSchemaEntry[],\n observer: ParamSchemaDeriveObserver,\n site: string,\n): DeriveOutput {\n const admitted = ADMITTED_PARAM_SCHEMA_PROJECTIONS.has(schema);\n const output = derive(schema);\n if (observer.enabled) {\n observer.observe({\n kind: admitted ? 'admitted-identity-hit' : 'unregistered-fallback-derive',\n site,\n });\n if (!admitted) observer.observe({ kind: 'fallback-layout-sha', site });\n }\n return output;\n}\n\nfunction derivePure(schema: readonly ParamSchemaEntry[]): DeriveOutput {\n const bglEntries: BindGroupLayoutEntry[] = [];\n const uboFields: UboFieldLayout[] = [];\n const numericMembers: DerivedNumericMember[] = [];\n const coordinateRecords: MaterialCoordinateRecordLayout[] = [];\n const resourceBindings: MaterialResourceBindingLayout[] = [];\n const textureFieldNames = new Set<string>();\n const samplerForTexture = new Map<string, string>();\n const seenNames = new Set<string>();\n const reservedSamplerNames = new Set<string>();\n\n let nextBinding = 0;\n let uboCursor = 0;\n let uboBinding: number | null = null;\n\n const ensureUboBinding = (): number => {\n if (uboBinding !== null) return uboBinding;\n uboBinding = nextBinding;\n nextBinding += 1;\n bglEntries.push({\n binding: uboBinding,\n visibility: FRAGMENT,\n buffer: { type: 'uniform' },\n });\n return uboBinding;\n };\n\n for (const rawEntry of schema) {\n const entry = rawEntry as ParamSchemaEntry;\n if (entry.name.length === 0) {\n throw new Error('derive: schema entry name must be non-empty');\n }\n if (!ALL_TYPES.has(entry.type)) {\n throw new Error(`derive: unrecognised paramSchema type literal '${entry.type}'`);\n }\n if (seenNames.has(entry.name)) {\n throw new Error(`derive: duplicate paramSchema entry name '${entry.name}'`);\n }\n if (reservedSamplerNames.has(entry.name)) {\n throw new Error(\n `derive: paramSchema entry '${entry.name}' collides with auto-paired sampler name`,\n );\n }\n seenNames.add(entry.name);\n\n if (NUMERIC_TYPES.has(entry.type)) {\n const numericType = entry.type as NumericParamType;\n const { size, align } = numericFootprint(numericType);\n ensureUboBinding();\n const offset = alignUp(uboCursor, align);\n uboFields.push({ name: entry.name, offset, size, type: numericType });\n numericMembers.push({ name: entry.name, offset, size, alignment: align, type: numericType });\n uboCursor = offset + size;\n continue;\n }\n\n if (TEXTURE_VIEW_TYPES.has(entry.type)) {\n const texType = entry.type as TextureBindingParamType;\n ensureUboBinding();\n const coordinateOffset = alignUp(uboCursor, 16);\n const coordinates: MaterialCoordinateRecordLayout = {\n parameter: entry.name,\n offset: coordinateOffset,\n size: 32,\n alignment: 16,\n transformMember: `${entry.name}CoordinatesTransform`,\n metadataMember: `${entry.name}CoordinatesMetadata`,\n };\n coordinateRecords.push(coordinates);\n uboCursor = coordinateOffset + coordinates.size;\n const { sampleType, viewDimension } = textureBglDescriptor(texType);\n // Sampler-first per plan §D-4: emit auto-paired filtering sampler at\n // binding N, then the texture view at binding N+1. Matches the actual\n // WGSL @binding declaration order in the 5 built-in shaders (sampler\n // declared on the odd binding, texture on the even+1 binding).\n const samplerName = `${entry.name}_sampler`;\n if (seenNames.has(samplerName)) {\n throw new Error(\n `derive: auto-paired sampler name '${samplerName}' collides with existing entry`,\n );\n }\n reservedSamplerNames.add(samplerName);\n samplerForTexture.set(entry.name, samplerName);\n const samplerBinding = nextBinding;\n nextBinding += 1;\n bglEntries.push({\n binding: samplerBinding,\n visibility: FRAGMENT,\n sampler: { type: 'filtering' },\n });\n resourceBindings.push({\n name: samplerName,\n parameter: entry.name,\n kind: 'sampler',\n binding: samplerBinding,\n });\n\n const texBinding = nextBinding;\n nextBinding += 1;\n bglEntries.push({\n binding: texBinding,\n visibility: FRAGMENT,\n texture: { sampleType, viewDimension, multisampled: false },\n });\n resourceBindings.push({\n name: entry.name,\n parameter: entry.name,\n kind: 'texture',\n binding: texBinding,\n });\n textureFieldNames.add(entry.name);\n continue;\n }\n\n if (SAMPLER_TYPES.has(entry.type)) {\n const samplerType = entry.type as 'sampler' | 'sampler_comparison';\n const samplerBinding = nextBinding;\n nextBinding += 1;\n bglEntries.push({\n binding: samplerBinding,\n visibility: FRAGMENT,\n sampler: { type: samplerBindingType(samplerType) },\n });\n resourceBindings.push({\n name: entry.name,\n parameter: entry.name,\n kind: 'sampler',\n binding: samplerBinding,\n });\n continue;\n }\n\n // entry.type === 'storage_buffer'\n const storageBinding = nextBinding;\n nextBinding += 1;\n bglEntries.push({\n binding: storageBinding,\n visibility: FRAGMENT,\n buffer: { type: 'read-only-storage' },\n });\n resourceBindings.push({\n name: entry.name,\n parameter: entry.name,\n kind: 'storage-buffer',\n binding: storageBinding,\n });\n }\n\n const totalBytes = uboCursor === 0 ? 0 : alignUp(uboCursor, 16);\n const layoutIdentity = sha256LayoutIdentity({\n numericMembers,\n coordinateRecords,\n resourceBindings,\n totalBytes,\n });\n\n return {\n bglEntries,\n uboLayout: { entries: uboFields, totalBytes },\n numericMembers,\n coordinateRecords,\n resourceBindings,\n totalBytes,\n layoutIdentity,\n textureFieldNames,\n samplerForTexture,\n userRegionBindingEnd: nextBinding,\n };\n}\n\nfunction freezeParamSchema(schema: readonly ParamSchemaEntry[]): readonly ParamSchemaEntry[] {\n return Object.freeze(\n schema.map((entry) => {\n const defaultValue = cloneAndFreezeSchemaValue(entry.default);\n return Object.freeze({\n ...entry,\n ...(entry.default === undefined ? {} : { default: defaultValue }),\n }) as ParamSchemaEntry;\n }),\n );\n}\n\nfunction cloneAndFreezeSchemaValue(value: unknown): unknown {\n if (Array.isArray(value)) {\n return Object.freeze(value.map((item) => cloneAndFreezeSchemaValue(item)));\n }\n if (typeof value === 'object' && value !== null) {\n return Object.freeze(\n Object.fromEntries(\n Object.entries(value).map(([key, item]) => [key, cloneAndFreezeSchemaValue(item)]),\n ),\n );\n }\n return value;\n}\n\nfunction freezeDerivedMaterialInterface(\n derived: DerivedMaterialInterface,\n): DerivedMaterialInterface {\n deepFreeze(derived.bglEntries);\n deepFreeze(derived.uboLayout);\n deepFreeze(derived.numericMembers);\n deepFreeze(derived.coordinateRecords);\n deepFreeze(derived.resourceBindings);\n return Object.freeze({\n ...derived,\n textureFieldNames: new ImmutableSetView(derived.textureFieldNames),\n samplerForTexture: new ImmutableMapView(derived.samplerForTexture),\n });\n}\n\nfunction deepFreeze<T>(value: T): T {\n if (typeof value !== 'object' || value === null || Object.isFrozen(value)) return value;\n for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child);\n return Object.freeze(value);\n}\n\nclass ImmutableSetView<T> implements ReadonlySet<T> {\n readonly #values: Set<T>;\n\n constructor(values: Iterable<T>) {\n this.#values = new Set(values);\n Object.freeze(this);\n }\n\n get size(): number {\n return this.#values.size;\n }\n\n has(value: T): boolean {\n return this.#values.has(value);\n }\n\n entries(): SetIterator<[T, T]> {\n return this.#values.entries();\n }\n\n keys(): SetIterator<T> {\n return this.#values.keys();\n }\n\n values(): SetIterator<T> {\n return this.#values.values();\n }\n\n forEach(callbackfn: (value: T, value2: T, set: ReadonlySet<T>) => void, thisArg?: unknown): void {\n for (const value of this.#values) callbackfn.call(thisArg, value, value, this);\n }\n\n [Symbol.iterator](): SetIterator<T> {\n return this.#values[Symbol.iterator]();\n }\n}\n\nclass ImmutableMapView<K, V> implements ReadonlyMap<K, V> {\n readonly #values: Map<K, V>;\n\n constructor(values: Iterable<readonly [K, V]>) {\n this.#values = new Map(values);\n Object.freeze(this);\n }\n\n get size(): number {\n return this.#values.size;\n }\n\n get(key: K): V | undefined {\n return this.#values.get(key);\n }\n\n has(key: K): boolean {\n return this.#values.has(key);\n }\n\n entries(): MapIterator<[K, V]> {\n return this.#values.entries();\n }\n\n keys(): MapIterator<K> {\n return this.#values.keys();\n }\n\n values(): MapIterator<V> {\n return this.#values.values();\n }\n\n forEach(callbackfn: (value: V, key: K, map: ReadonlyMap<K, V>) => void, thisArg?: unknown): void {\n for (const [key, value] of this.#values) callbackfn.call(thisArg, value, key, this);\n }\n\n [Symbol.iterator](): MapIterator<[K, V]> {\n return this.#values[Symbol.iterator]();\n }\n}\n\nexport function inferMaterialParameterKind(\n entry: ParamSchemaEntry,\n): MaterialParameterProjection['kind'] {\n if (NUMERIC_TYPES.has(entry.type)) return 'numeric';\n if (TEXTURE_VIEW_TYPES.has(entry.type)) return 'texture';\n if (entry.type === 'storage_buffer') return 'storage-buffer';\n return 'sampler';\n}\n\nfunction sha256LayoutIdentity(value: {\n readonly numericMembers: readonly DerivedNumericMember[];\n readonly coordinateRecords: readonly MaterialCoordinateRecordLayout[];\n readonly resourceBindings: readonly MaterialResourceBindingLayout[];\n readonly totalBytes: number;\n}): string {\n const canonical = JSON.stringify({\n version: 1,\n numericMembers: value.numericMembers,\n coordinateRecords: value.coordinateRecords,\n resourceBindings: value.resourceBindings,\n totalBytes: value.totalBytes,\n });\n return `sha256-${sha256(canonical)}`;\n}\n\nconst SHA256_K = [\n 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,\n 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,\n 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,\n 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,\n 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,\n 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,\n 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,\n 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,\n] as const;\n\nfunction sha256(input: string): string {\n const bytes = new TextEncoder().encode(input);\n const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;\n const padded = new Uint8Array(paddedLength);\n padded.set(bytes);\n padded[bytes.length] = 0x80;\n const view = new DataView(padded.buffer);\n view.setUint32(paddedLength - 4, bytes.length * 8, false);\n let hash = new Uint32Array([\n 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,\n ]);\n for (let block = 0; block < padded.length; block += 64) {\n const words = new Uint32Array(64);\n for (let index = 0; index < 16; index += 1)\n words[index] = view.getUint32(block + index * 4, false);\n for (let index = 16; index < 64; index += 1) {\n const a = words[index - 15] ?? 0;\n const b = words[index - 2] ?? 0;\n words[index] =\n (smallSigma1(b) + (words[index - 7] ?? 0) + smallSigma0(a) + (words[index - 16] ?? 0)) >>>\n 0;\n }\n let a = hash[0] ?? 0;\n let b = hash[1] ?? 0;\n let c = hash[2] ?? 0;\n let d = hash[3] ?? 0;\n let e = hash[4] ?? 0;\n let f = hash[5] ?? 0;\n let g = hash[6] ?? 0;\n let h = hash[7] ?? 0;\n for (let index = 0; index < 64; index += 1) {\n const choose = (e & f) ^ (~e & g);\n const majority = (a & b) ^ (a & c) ^ (b & c);\n const t1 = (h + bigSigma1(e) + choose + (SHA256_K[index] ?? 0) + (words[index] ?? 0)) >>> 0;\n const t2 = (bigSigma0(a) + majority) >>> 0;\n [h, g, f, e, d, c, b, a] = [g, f, e, (d + t1) >>> 0, c, b, a, (t1 + t2) >>> 0];\n }\n const state = [a, b, c, d, e, f, g, h];\n hash = new Uint32Array(hash.map((value, index) => ((value + (state[index] ?? 0)) >>> 0) >>> 0));\n }\n return Array.from(hash, (word) => word.toString(16).padStart(8, '0')).join('');\n}\n\nfunction rotateRight(value: number, shift: number): number {\n return (value >>> shift) | (value << (32 - shift));\n}\n\nfunction bigSigma0(value: number): number {\n return rotateRight(value, 2) ^ rotateRight(value, 13) ^ rotateRight(value, 22);\n}\n\nfunction bigSigma1(value: number): number {\n return rotateRight(value, 6) ^ rotateRight(value, 11) ^ rotateRight(value, 25);\n}\n\nfunction smallSigma0(value: number): number {\n return rotateRight(value, 7) ^ rotateRight(value, 18) ^ (value >>> 3);\n}\n\nfunction smallSigma1(value: number): number {\n return rotateRight(value, 17) ^ rotateRight(value, 19) ^ (value >>> 10);\n}\n\n/**\n * The three user-region material texture fields whose handles flow through the\n * paramSchema-driven extract filter (`validateTextureHandle`). They map to the\n * fixed `@group(1) @binding(2/4/6)` slots in the standard material BGL.\n *\n * `emissiveTexture` / `occlusionTexture` are deliberately EXCLUDED: they live\n * in the engine-managed lightmap injection region (`appendInjection`,\n * bindings 14..17), are sampled by `default-standard-pbr` without a schema\n * entry, and are never filtered by `validateTextureHandle`. Including them\n * would false-positive the engine's own PBR shader.\n */\nconst USER_REGION_TEXTURE_FIELDS: readonly string[] = [\n 'baseColorTexture',\n 'metallicRoughnessTexture',\n 'normalTexture',\n];\n\n/** Strip `//` line comments and block comments before scanning WGSL source. */\nfunction stripWgslComments(source: string): string {\n return source.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/\\/\\/[^\\n]*/g, '');\n}\n\n/**\n * Detect user-region material textures a shader actually `textureSample`s but\n * its paramSchema fails to declare as a texture entry.\n *\n * This is the runtime (register-time) counterpart of the build-time superset\n * gate (`compareParamSchemaSuperset`): user shaders registered directly via\n * `ShaderRegistry.installMaterialArtifact` bypass the vite-plugin-shader\n * reflection path, so an under-declared schema would otherwise let the extract\n * stage's `validateTextureHandle` silently drop the sampled texture's handle\n * and fall back to the default white texture (charter P3 violation — the bug\n * that turned the LearnOpenGL 4.3 blending demo's grass + windows opaque white;\n * see docs/handover/2026-06-19-blending-transparency-regression-bisect.md).\n *\n * Returns the field names that are sampled-but-undeclared (empty = consistent).\n * Scan is name-based on the WGSL var passed as the first `textureSample*`\n * argument; comments are stripped first so a commented-out sample never trips\n * the check. Only the three `USER_REGION_TEXTURE_FIELDS` participate, so a\n * shader that merely *declares* the standard binding layout without sampling it\n * (e.g. an outline / depth-viz shader reusing the PBR BGL) is not flagged.\n */\nexport function findUndeclaredSampledTextures(\n wgslSource: string,\n schema: readonly ParamSchemaEntry[],\n): readonly string[] {\n const declared = derive(schema).textureFieldNames;\n const clean = stripWgslComments(wgslSource);\n const sampleRe = /textureSample[A-Za-z]*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)/g;\n const sampled = new Set<string>();\n for (let m = sampleRe.exec(clean); m !== null; m = sampleRe.exec(clean)) {\n const name = m[1];\n if (name !== undefined) sampled.add(name);\n }\n return USER_REGION_TEXTURE_FIELDS.filter((f) => sampled.has(f) && !declared.has(f));\n}\n","// @forgeax/engine-types - producer-owned asset catalog contract.\n//\n// This is the shared POD boundary between an engine asset producer and any\n// consumer. Consumers must use these facts instead of deriving origin from a\n// DDC URL, filename suffix, or catalog position.\n\nexport type AssetSubjectType = 'asset' | 'package' | 'resource';\n\n/** The producer-owned subject behind a catalog row. */\nexport type CatalogSubject = 'internal-asset' | 'imported-output';\n\n/** Whether the runtime projection is validated directly or cooked. */\nexport type CookExecution = 'direct' | 'cooked';\n\n/** Derived lifecycle states exposed by the catalog. */\nexport type CatalogLifecycle = 'missing' | 'cooking' | 'current' | 'stale' | 'failed';\n\n/** Usage derived from producer refs and build-time content reads. */\nexport type AssetPublicationEvidenceUsage = 'reference' | 'content' | 'both';\n\n/** One external dependency observed while publishing a source package. */\nexport interface AssetPublicationExternalEvidence {\n readonly guid: string;\n readonly usage: AssetPublicationEvidenceUsage;\n readonly generation?: number;\n readonly digest?: string;\n}\n\n/** One ordinary asset output in an atomic source-package publication. */\nexport interface AssetPublicationOutput {\n readonly guid: string;\n readonly sourceKey: string;\n readonly kind: string;\n readonly digest: string;\n /** GUIDs emitted by the owning ordinary producer, in stable order. */\n readonly refs: readonly string[];\n}\n\n/** Receipt facts that prove the complete output set and dependency closure. */\nexport interface AssetPublicationReceipt {\n readonly schemaVersion: 'asset-publication-receipt/1';\n readonly sourcePath: string;\n readonly sourceRevision: string;\n readonly inputFingerprint: string;\n readonly outputDigest: string;\n readonly outputSetDigest: string;\n readonly externalEvidence: readonly AssetPublicationExternalEvidence[];\n}\n\n/** Stable locator for current or last-known-good publication recovery. */\nexport interface AssetPublicationLocator {\n readonly generation: number;\n readonly digest: string;\n readonly outputSetDigest: string;\n readonly packageUrl: string;\n readonly receiptKey: string;\n}\n\nexport type AssetPublicationFailureStage =\n | 'source'\n | 'output'\n | 'receipt'\n | 'route'\n | 'catalog'\n | 'cancelled';\n\n/** Machine-readable failure and recovery facts; messages are not a protocol. */\nexport interface AssetPublicationFailure {\n readonly code: string;\n readonly stage: AssetPublicationFailureStage;\n readonly sourcePath: string;\n readonly sourceRevision?: string;\n readonly generation?: number;\n readonly outputGuid?: string;\n readonly reason: string;\n}\n\n/** Actions a consumer can execute after a candidate publication is rejected. */\nexport interface AssetPublicationRecovery {\n readonly retryable: boolean;\n readonly preserveCurrent: boolean;\n readonly useLastKnownGood: boolean;\n readonly actions: readonly string[];\n}\n\n/**\n * Engine-owned publication SSOT shared by pack, import, Catalog, and consumers.\n * A publication is valid only when receipt and every output belong to one\n * source revision, generation, digest, and output-set digest tuple.\n */\nexport interface AssetPublicationEnvelope {\n readonly schemaVersion: 'asset-publication/1';\n readonly sourcePath: string;\n readonly sourceRevision: string;\n readonly generation: number;\n readonly digest: string;\n readonly outputSetDigest: string;\n readonly outputs: readonly AssetPublicationOutput[];\n readonly receipt: AssetPublicationReceipt;\n readonly externalEvidence: readonly AssetPublicationExternalEvidence[];\n readonly failureStage?: AssetPublicationFailureStage;\n readonly failure?: AssetPublicationFailure;\n readonly current?: AssetPublicationLocator;\n readonly lastKnownGood?: AssetPublicationLocator;\n readonly recovery?: AssetPublicationRecovery;\n}\n\n/**\n * Immutable source-level identity carried by an authored generated Scene mount.\n * A mount is consumable only when every member resolves from this complete\n * publication tuple; no single output GUID or generation number is sufficient.\n */\nexport interface ScenePublicationFence {\n readonly schemaVersion: 'scene-publication-fence/1';\n readonly sourcePath: string;\n readonly sourceRevision: string;\n readonly publicationGeneration: number;\n readonly outputDigest: string;\n readonly outputSetDigest: string;\n readonly receiptIdentity: string;\n}\n\nexport type CatalogOperationName =\n | 'preview'\n | 'save'\n | 'rebuild'\n | 'sourceOverride'\n | 'instanceOverride'\n | 'promote';\n\nexport interface CatalogOperationDescriptor {\n readonly operation: CatalogOperationName;\n readonly enabled: boolean;\n readonly reason?: string;\n}\n\nexport type CatalogOperations = Readonly<Record<CatalogOperationName, CatalogOperationDescriptor>>;\n\nexport interface CatalogProjectionInput {\n readonly subject: CatalogSubject;\n readonly execution: CookExecution;\n readonly lifecycle: CatalogLifecycle;\n}\n\n/** The explicit three-axis projection consumed by AI-facing catalog clients. */\nexport interface CatalogProjection extends CatalogProjectionInput {\n readonly operations: CatalogOperations;\n readonly lastKnownGood?: {\n readonly packageUrl: string;\n readonly receiptUrl?: string;\n };\n}\n\n/**\n * Derive operation descriptors from catalog facts only.\n *\n * `kind`, paths, and diagnostic messages are intentionally absent from this\n * function: a consumer receives a complete operation matrix and can branch\n * on `enabled` without reimplementing producer policy.\n */\nexport function catalogOperationsFor(input: CatalogProjectionInput): CatalogOperations {\n const imported = input.subject === 'imported-output';\n const current = input.lifecycle === 'current';\n const ready = current && (input.execution === 'direct' || input.execution === 'cooked');\n const canRebuild = input.execution === 'cooked';\n const canPreview = input.execution === 'cooked' && input.lifecycle !== 'missing';\n const operation = (name: CatalogOperationName, enabled: boolean, reason?: string) => ({\n operation: name,\n enabled,\n ...(reason === undefined ? {} : { reason }),\n });\n\n return {\n preview: operation('preview', canPreview, canPreview ? undefined : 'no projection to preview'),\n save: operation(\n 'save',\n !imported && input.execution === 'direct' && ready,\n imported ? 'imported output is read-only' : 'direct projection is not current',\n ),\n rebuild: operation(\n 'rebuild',\n canRebuild,\n canRebuild ? undefined : 'direct assets do not require a cook',\n ),\n sourceOverride: operation(\n 'sourceOverride',\n imported && canRebuild,\n imported\n ? canRebuild\n ? undefined\n : 'cooked projection is not available'\n : 'only imported output has a source override',\n ),\n instanceOverride: operation(\n 'instanceOverride',\n imported && current,\n imported\n ? current\n ? undefined\n : 'projection is not current'\n : 'only imported output has an instance override',\n ),\n promote: operation(\n 'promote',\n imported && current,\n imported\n ? current\n ? undefined\n : 'projection is not current'\n : 'internal assets are already authored',\n ),\n };\n}\n\n/** Reject impossible axis combinations before a catalog row is published. */\nexport function isCatalogProjectionValid(input: CatalogProjection): boolean {\n if (input.execution === 'direct' && input.lifecycle !== 'current') return false;\n if (input.subject === 'imported-output' && input.execution !== 'cooked') return false;\n return Object.entries(input.operations).every(\n ([name, descriptor]) => name === descriptor.operation,\n );\n}\n\nexport interface AssetAuthoringUnavailableReason {\n readonly code: 'unsupported-asset-kind' | 'missing-producer-capability';\n readonly hint: string;\n}\n\n/** Engine-facing operation shape exposed by a producer-owned catalog row. */\nexport type AssetPlacementCapability =\n | { readonly operation: 'spawnEntity' }\n | { readonly operation: 'addSceneAssetToScene' }\n | { readonly operation: 'unavailable'; readonly reason: AssetAuthoringUnavailableReason };\n\nexport interface AssetBindingTarget {\n readonly component: string;\n readonly field: string;\n readonly assetType: string;\n readonly cardinality: 'single' | 'array';\n}\n\n/**\n * One projection exposed by the producer-owned UI authoring contract.\n *\n * `supported` describes a real engine seam; `unavailable` is deliberately\n * structured so an editor or AI client cannot silently invent a consumer-side\n * implementation for a missing producer capability.\n */\nexport type UiAuthoringProjection =\n | {\n readonly status: 'supported';\n readonly operation:\n | 'createUiPreviewSession'\n | 'mountUi'\n | 'gameProjection'\n | 'dom-native'\n | 'ui-artifact-companion';\n readonly contractVersion: '1';\n }\n | {\n readonly status: 'unavailable';\n readonly reason: AssetAuthoringUnavailableReason;\n };\n\n/**\n * Versioned UI authoring facts published beside every `kind: 'ui'` catalog\n * row. This is a protocol descriptor, not a promise that the editor may reach\n * into a game world: runtime state/action/read semantics remain owned by the\n * game projection registrar and the UI mount/preview seams remain owned by the\n * engine UI package.\n */\nexport interface UiAuthoringCapability {\n readonly contractVersion: '1';\n readonly profileVersion: '1';\n readonly preview: {\n readonly operation: 'createUiPreviewSession';\n readonly lifecycle: 'open-rebuild-retry-dispose';\n };\n readonly mount: {\n readonly operation: 'mountUi';\n readonly lifecycle: 'mount-dispose';\n readonly actionPort: 'onAction';\n };\n readonly state: UiAuthoringProjection;\n readonly actions: UiAuthoringProjection;\n readonly reads: UiAuthoringProjection;\n readonly input: UiAuthoringProjection;\n readonly navigation: UiAuthoringProjection;\n readonly font: UiAuthoringProjection;\n readonly localization: UiAuthoringProjection;\n}\n\nexport type AssetBindingCapability =\n | {\n readonly operation: 'bindAssetRef' | 'createMaterialThenBindAssetRef';\n readonly target: AssetBindingTarget;\n readonly requiredSlots: 1;\n }\n | { readonly operation: 'unavailable'; readonly reason: AssetAuthoringUnavailableReason };\n\n/** Producer-owned placement and binding facts for one catalog asset. */\nexport interface AssetAuthoringCapability {\n readonly placement: AssetPlacementCapability;\n readonly binding: AssetBindingCapability;\n /** Present for producer-owned UI assets; absent for unrelated kinds. */\n readonly ui?: UiAuthoringCapability;\n readonly sourceOverrides?: readonly SourceOverrideDescriptor[];\n}\n\nconst UI_AUTHORING_CAPABILITY: UiAuthoringCapability = {\n contractVersion: '1',\n profileVersion: '1',\n preview: {\n operation: 'createUiPreviewSession',\n lifecycle: 'open-rebuild-retry-dispose',\n },\n mount: {\n operation: 'mountUi',\n lifecycle: 'mount-dispose',\n actionPort: 'onAction',\n },\n state: { status: 'supported', operation: 'gameProjection', contractVersion: '1' },\n actions: { status: 'supported', operation: 'gameProjection', contractVersion: '1' },\n reads: { status: 'supported', operation: 'gameProjection', contractVersion: '1' },\n input: { status: 'supported', operation: 'dom-native', contractVersion: '1' },\n navigation: { status: 'supported', operation: 'dom-native', contractVersion: '1' },\n font: { status: 'supported', operation: 'ui-artifact-companion', contractVersion: '1' },\n localization: {\n status: 'unavailable',\n reason: {\n code: 'missing-producer-capability',\n hint: 'UI localization resources are not yet published through the UI authoring contract.',\n },\n },\n};\n\n/** Built-in defaults for legacy rows that do not carry an explicit override. */\nexport function authoringCapabilityForAssetKind(kind: string): AssetAuthoringCapability {\n switch (kind) {\n case 'ui':\n return {\n placement: {\n operation: 'unavailable',\n reason: {\n code: 'unsupported-asset-kind',\n hint: 'UI assets mount through the UI runtime and are not ECS scene placements.',\n },\n },\n binding: {\n operation: 'unavailable',\n reason: {\n code: 'unsupported-asset-kind',\n hint: 'UI assets bind through their producer-owned UI runtime contract.',\n },\n },\n ui: UI_AUTHORING_CAPABILITY,\n };\n case 'scene':\n return {\n placement: { operation: 'addSceneAssetToScene' },\n binding: {\n operation: 'unavailable',\n reason: {\n code: 'unsupported-asset-kind',\n hint: 'Scene assets are placed as a scene mount.',\n },\n },\n };\n case 'mesh':\n return {\n placement: { operation: 'spawnEntity' },\n binding: {\n operation: 'bindAssetRef',\n target: {\n component: 'MeshFilter',\n field: 'assetHandle',\n assetType: 'MeshAsset',\n cardinality: 'single',\n },\n requiredSlots: 1,\n },\n };\n case 'material':\n return {\n placement: { operation: 'spawnEntity' },\n binding: {\n operation: 'bindAssetRef',\n target: {\n component: 'MeshRenderer',\n field: 'materials',\n assetType: 'MaterialAsset',\n cardinality: 'array',\n },\n requiredSlots: 1,\n },\n };\n case 'texture':\n return {\n placement: { operation: 'spawnEntity' },\n binding: {\n operation: 'createMaterialThenBindAssetRef',\n target: {\n component: 'MeshRenderer',\n field: 'materials',\n assetType: 'MaterialAsset',\n cardinality: 'array',\n },\n requiredSlots: 1,\n },\n };\n case 'particle-effect':\n return {\n placement: { operation: 'spawnEntity' },\n binding: {\n operation: 'bindAssetRef',\n target: {\n component: 'ParticleEffectPlayer',\n field: 'effect',\n assetType: 'ParticleEffectAsset',\n cardinality: 'single',\n },\n requiredSlots: 1,\n },\n };\n default:\n return {\n placement: {\n operation: 'unavailable',\n reason: {\n code: 'unsupported-asset-kind',\n hint: `No placement capability is published for asset kind '${kind}'.`,\n },\n },\n binding: {\n operation: 'unavailable',\n reason: {\n code: 'unsupported-asset-kind',\n hint: `No binding capability is published for asset kind '${kind}'.`,\n },\n },\n };\n }\n}\n\n/** Stable producer subject identity used by relations and diagnostics. */\nexport interface AssetSubjectRef {\n readonly type: AssetSubjectType;\n readonly id: string;\n}\n\n/** Provider identity carried with producer-owned facts; not a catalog locator. */\nexport interface ProviderProvenance {\n readonly provider: string;\n readonly version: string;\n readonly source?: string;\n}\n\n/** Producer-owned JSON payload for one stable imported-output source key. */\nexport type SourceOverridePayload = Readonly<Record<string, unknown>>;\n\n/** Optional Meta author facts keyed by the producer's stable sourceKey. */\nexport type SourceOverrideMap = Readonly<Record<string, SourceOverridePayload>>;\n\nexport interface SourceOverrideDescriptor {\n readonly sourceKey: string;\n /** Producer-owned semantic used by authoring hosts for catalog-aware validation. */\n readonly semantic?: 'mesh-material-slot-defaults';\n readonly payloadSchema?: unknown;\n}\n\n/** Shared payload contract explicitly published by Mesh-producing importers. */\nexport const MESH_MATERIAL_SLOT_SOURCE_OVERRIDE_PAYLOAD_SCHEMA = {\n type: 'object',\n properties: {\n materialSlots: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n slotName: { type: 'string', minLength: 1 },\n sourceKey: { type: 'string', minLength: 1 },\n defaultMaterialGuid: { type: 'string' },\n },\n required: ['slotName'],\n additionalProperties: true,\n },\n },\n materialSlotDefaultOverrides: {\n type: 'object',\n additionalProperties: { type: 'string', nullable: true },\n },\n },\n additionalProperties: true,\n} as const;\n\nexport type SourceOverrideErrorCode =\n | 'unknown-source-key'\n | 'duplicate-source-key'\n | 'invalid-source-overrides'\n | 'invalid-source-override-payload';\n\nexport interface SourceOverrideDiagnostic {\n readonly code: SourceOverrideErrorCode;\n readonly expected: string;\n readonly actual?: string;\n readonly hint: string;\n}\n\nexport type SourceOverrideValidationResult =\n | { readonly ok: true; readonly value: SourceOverrideMap | undefined }\n | { readonly ok: false; readonly error: SourceOverrideDiagnostic };\n\nfunction sourceOverrideError(\n code: SourceOverrideErrorCode,\n expected: string,\n hint: string,\n actual?: string,\n): SourceOverrideValidationResult {\n return {\n ok: false,\n error: { code, expected, hint, ...(actual === undefined ? {} : { actual }) },\n };\n}\n\nfunction isSourceOverridePayload(value: unknown): value is SourceOverridePayload {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Canonicalize legacy/empty override maps without changing producer payloads. */\nexport function canonicalizeSourceOverrides(value: unknown): SourceOverrideMap | undefined {\n if (value === undefined) return undefined;\n if (!isSourceOverridePayload(value)) return undefined;\n const keys = Object.keys(value);\n if (keys.length === 0) return undefined;\n return Object.fromEntries(keys.sort().map((key) => [key, value[key]])) as SourceOverrideMap;\n}\n\nfunction validateSourceOverrideEntry(\n sourceKey: string,\n payload: unknown,\n declared: ReadonlySet<string>,\n seen: Set<string>,\n): SourceOverrideDiagnostic | undefined {\n if (seen.has(sourceKey)) {\n return {\n code: 'duplicate-source-key',\n expected: 'sourceKey values to be unique within sourceOverrides',\n hint: 'remove the duplicate source override',\n actual: sourceKey,\n };\n }\n seen.add(sourceKey);\n if (!declared.has(sourceKey)) {\n return {\n code: 'unknown-source-key',\n expected: 'sourceKey to be declared by the producer topology',\n hint: 'request a fresh Catalog topology before writing Meta',\n actual: sourceKey,\n };\n }\n if (!isSourceOverridePayload(payload)) {\n return {\n code: 'invalid-source-override-payload',\n expected: 'each source override payload to be a producer-owned object',\n hint: 'validate the payload with the producer schema',\n actual: sourceKey,\n };\n }\n return undefined;\n}\n\nfunction validateSourceOverrideEntries(\n entries: readonly (readonly [string, unknown])[],\n declared: ReadonlySet<string>,\n): SourceOverrideValidationResult {\n const seen = new Set<string>();\n for (const [sourceKey, payload] of entries) {\n const error = validateSourceOverrideEntry(sourceKey, payload, declared, seen);\n if (error !== undefined) return { ok: false, error };\n }\n return { ok: true, value: canonicalizeSourceOverrides(Object.fromEntries(entries)) };\n}\n\n/** Validate source override identity while leaving payload interpretation to the producer. */\nexport function validateSourceOverrideMap(\n value: unknown,\n declaredSourceKeys: readonly string[],\n): SourceOverrideValidationResult {\n const declared = new Set<string>();\n for (const sourceKey of declaredSourceKeys) {\n if (declared.has(sourceKey)) {\n return sourceOverrideError(\n 'duplicate-source-key',\n 'sourceKey values declared by a producer to be unique',\n 'repair the producer topology before publishing Meta',\n sourceKey,\n );\n }\n declared.add(sourceKey);\n }\n if (value === undefined) return { ok: true, value: undefined };\n if (Array.isArray(value)) {\n const entries: (readonly [string, unknown])[] = [];\n for (const item of value) {\n if (!Array.isArray(item) || item.length !== 2 || typeof item[0] !== 'string') {\n return sourceOverrideError(\n 'invalid-source-overrides',\n 'sourceOverrides to be an object keyed by sourceKey',\n 'pass a producer-owned source override map',\n );\n }\n entries.push([item[0], item[1]]);\n }\n return validateSourceOverrideEntries(entries, declared);\n }\n if (!isSourceOverridePayload(value)) {\n return sourceOverrideError(\n 'invalid-source-overrides',\n 'sourceOverrides to be an object keyed by sourceKey',\n 'pass a producer-owned source override map',\n );\n }\n return validateSourceOverrideEntries(Object.entries(value), declared);\n}\n\n/** Monotonic producer observation used to validate catalog continuity. */\nexport interface ResourceRevision {\n readonly digest: string;\n readonly observedAt: number;\n readonly rootId: string;\n}\n\nexport type AssetRelationType =\n | 'references'\n | 'reads'\n | 'depends-on'\n | 'owns'\n | 'contains'\n | 'produces'\n | 'materialized-as'\n | (string & {});\n\nexport interface AssetRelationPolicy {\n readonly ownership?: 'owned' | 'shared';\n readonly lifecycle?: 'authored' | 'derived';\n readonly strength?: 'required' | 'optional';\n}\n\n/** Structured graph edge emitted by a producer; consumers must preserve its fields. */\nexport interface AssetRelation {\n readonly from: AssetSubjectRef;\n readonly to: AssetSubjectRef;\n readonly type: AssetRelationType;\n readonly policy?: AssetRelationPolicy;\n readonly provenance: ProviderProvenance;\n}\n\nexport type CatalogDiagnosticSeverity = 'info' | 'warning' | 'blocking';\n\n/** Machine-readable catalog problem; consumers branch on fields, never message text. */\nexport interface CatalogDiagnostic {\n readonly code: string;\n readonly severity: CatalogDiagnosticSeverity;\n readonly message?: string;\n readonly subject?: AssetSubjectRef;\n readonly expected?: string;\n readonly actual?: string;\n readonly hint?: string;\n readonly authority?: 'producer' | 'pack' | 'catalog';\n readonly evidence?: readonly AssetSubjectRef[];\n readonly recoveryIntents?: readonly string[];\n}\n\n/** Closed set of contract failures returned by producer validation. */\nexport type ProducerContractErrorCode =\n | SourceOverrideErrorCode\n | TopologyConflictReason\n | 'invalid-source-key'\n | 'invalid-source-index'\n | 'invalid-producer-fact';\n\n/** Structured producer validation failure with its owning authority. */\nexport interface ProducerContractDiagnostic {\n readonly code: ProducerContractErrorCode;\n readonly subject: AssetSubjectRef;\n readonly expected: string;\n readonly actual?: string;\n readonly hint: string;\n readonly authority: 'producer' | 'pack';\n}\n\n/** Result boundary for producer validation; success and failure are discriminated by `ok`. */\nexport type ProducerContractResult<T> =\n | { readonly ok: true; readonly value: T }\n | { readonly ok: false; readonly error: ProducerContractDiagnostic };\n\n/** Canonical producer output declaration used for topology matching and recovery. */\nexport interface ImportedOutputDeclaration {\n readonly guid: string;\n readonly sourceKey?: string;\n readonly sourceIndex: number;\n readonly kind: string;\n readonly name?: string;\n /** New kinds that may reuse this output's prior GUID. */\n readonly compatiblePreviousKinds?: readonly string[];\n}\n\nexport type ProposedOutput = ImportedOutputDeclaration;\n\nexport type ExistingOutput = ImportedOutputDeclaration;\n\nexport interface KindChange {\n readonly guid: string;\n readonly oldKind: string;\n readonly newKind: string;\n readonly sourceKey?: string;\n readonly action: 'remove-add' | 'preserve-guid';\n}\n\nexport type TopologyConflictReason =\n | 'duplicate-source-key'\n | 'missing-source-key'\n | 'source-index-ambiguous';\n\nexport interface MatchConflict {\n readonly reason: TopologyConflictReason;\n readonly sourceKey?: string;\n readonly previous: readonly ExistingOutput[];\n readonly next: readonly ProposedOutput[];\n}\n\nexport interface TopologyPreserved {\n readonly guid: string;\n readonly oldKey: string;\n readonly newKey: string;\n}\n\nexport interface TopologyDiff {\n readonly preserved: readonly TopologyPreserved[];\n readonly added: readonly ProposedOutput[];\n readonly removed: readonly ExistingOutput[];\n readonly changedKind: readonly KindChange[];\n readonly ambiguous: readonly MatchConflict[];\n}\n","import type { AssetPublicationTuple } from './asset.js';\nimport type {\n AssetAuthoringCapability,\n AssetPublicationEnvelope,\n AssetRelation,\n CatalogDiagnostic,\n CatalogLifecycle,\n CatalogProjection,\n CatalogSubject,\n CookExecution,\n ProviderProvenance,\n ResourceRevision,\n SourceOverrideDescriptor,\n SourceOverrideMap,\n TopologyDiff,\n} from './asset-producer';\nimport { err, ok, type Result } from './result';\n\nexport type {\n AssetPublicationEnvelope,\n AssetPublicationEvidenceUsage,\n AssetPublicationExternalEvidence,\n AssetPublicationFailure,\n AssetPublicationFailureStage,\n AssetPublicationLocator,\n AssetPublicationOutput,\n AssetPublicationReceipt,\n AssetPublicationRecovery,\n CatalogLifecycle,\n CatalogProjection,\n CatalogSubject,\n CookExecution,\n} from './asset-producer';\n\n/**\n * Strict contract row for the Catalog projection.\n *\n * This is evidence for an AI consumer, not authoring authority: a Catalog row\n * projects producer facts and runtime navigation while preserving lifecycle,\n * execution, and sourceKey distinctions.\n */\nexport interface CatalogEntryV2 extends AssetPublicationTuple {\n readonly guid: string;\n readonly packageUrl: string;\n readonly kind: string;\n readonly sourcePath: string;\n readonly subject: CatalogSubject;\n readonly execution: CookExecution;\n readonly lifecycle: CatalogLifecycle;\n readonly projection: CatalogProjection;\n}\n/** One producer revision point in a catalog continuity window. */\nexport interface CatalogRevisionPoint {\n readonly rootId: string;\n readonly revision: number;\n}\n\n/** Baseline/current revision sets used to reject stale or partial updates. */\nexport interface CatalogRevisionWindow {\n readonly baseline: readonly CatalogRevisionPoint[];\n readonly current: readonly CatalogRevisionPoint[];\n}\n\n/** One stable row from a development or build catalog snapshot. */\nexport interface CatalogEntry {\n readonly guid: string;\n /** GUID-to-pack navigation only; artifact paths live inside Pack v2. */\n readonly packageUrl: string;\n readonly kind: string;\n /** Producer-owned placement/binding facts; absent only on legacy rows. */\n readonly authoring?: AssetAuthoringCapability;\n /** Source declaration navigation for diagnostics, not runtime content. */\n readonly sourcePath: string;\n /** Stable package identity; path is a locator, never the package identity. */\n readonly packageId?: string;\n /** Producer-owned importer/provider identity and version. */\n readonly provenance?: ProviderProvenance;\n /** Producer-owned resource/package revision used for conflict checks. */\n readonly revision?: ResourceRevision;\n /** Stable producer key for imported-output topology matching; never infer it from sourceIndex. */\n readonly sourceKey?: string;\n /** Producer-declared output position; never used as identity when sourceKey exists. */\n readonly sourceIndex?: number;\n /** Producer-owned author facts carried through the catalog without interpretation. */\n readonly sourceOverrides?: SourceOverrideMap;\n readonly sourceOverrideDescriptors?: readonly SourceOverrideDescriptor[];\n /** Typed graph edges emitted by the producer. */\n readonly relations?: readonly AssetRelation[];\n /** Structured producer diagnostics; consumers must not parse messages. */\n readonly diagnostics?: readonly CatalogDiagnostic[];\n readonly name?: string;\n /** Optional navigation to the producer-owned cook receipt. */\n readonly cookReceiptUrl?: string;\n readonly refs?: readonly string[];\n /** Explicit producer-owned runtime projection axes. */\n readonly subject?: CatalogSubject;\n readonly execution?: CookExecution;\n readonly lifecycle?: CatalogLifecycle;\n readonly projection?: CatalogProjection;\n /** Complete Engine publication tuple, when this row came from a source package. */\n readonly publication?: AssetPublicationEnvelope;\n}\n\n/**\n * A folded, neutral set of catalog-row changes keyed by stable GUID.\n *\n * `authority` and `diagnostics` tell AI-readable consumers whether the delta\n * is safe to apply; a degraded delta carries no identity-bearing changes.\n */\nexport interface CatalogDelta {\n /** Runtime realm identity for dev publications; absent for immutable legacy builds. */\n readonly scopeId?: string;\n readonly generation?: number;\n readonly added: readonly CatalogEntry[];\n readonly changed: readonly CatalogEntry[];\n readonly removed: readonly CatalogEntry['guid'][];\n /** Optional topology evidence for imported-output changes in this delta. */\n readonly topology?: readonly TopologyDiff[];\n /** Present when a watch revision was supplied for continuity validation. */\n readonly authority?: 'authoritative' | 'degraded';\n /** Machine-readable continuity or topology diagnostics. */\n readonly diagnostics?: readonly CatalogDiagnostic[];\n readonly revisions?: CatalogRevisionWindow;\n}\n\nexport interface CatalogDeltaValidationError {\n readonly code: 'catalog-delta-invalid';\n readonly expected: string;\n readonly hint: string;\n readonly detail: { readonly field: string };\n}\n\nfunction catalogInvalid(field: string): Result<never, CatalogDeltaValidationError> {\n return err({\n code: 'catalog-delta-invalid',\n expected: 'a CatalogDelta with complete row identity and string removals',\n hint: 'discard the delta and enumerate a verified catalog snapshot',\n detail: { field },\n });\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\nfunction isStringArray(value: unknown): value is readonly string[] {\n return Array.isArray(value) && value.every(isNonEmptyString);\n}\n\nfunction isSubjectRef(value: unknown): boolean {\n return (\n isRecord(value) &&\n (value.type === 'asset' || value.type === 'package' || value.type === 'resource') &&\n isNonEmptyString(value.id)\n );\n}\n\nfunction isRevision(value: unknown): value is ResourceRevision {\n if (!isRecord(value)) return false;\n const observedAt = value.observedAt;\n return (\n isNonEmptyString(value.digest) &&\n typeof observedAt === 'number' &&\n Number.isSafeInteger(observedAt) &&\n observedAt >= 0 &&\n isNonEmptyString(value.rootId)\n );\n}\n\nfunction isDiagnostic(value: unknown): value is CatalogDiagnostic {\n return (\n isRecord(value) &&\n isNonEmptyString(value.code) &&\n (value.severity === 'info' || value.severity === 'warning' || value.severity === 'blocking') &&\n (value.message === undefined || typeof value.message === 'string') &&\n (value.subject === undefined || isSubjectRef(value.subject)) &&\n (value.expected === undefined || typeof value.expected === 'string') &&\n (value.actual === undefined || typeof value.actual === 'string') &&\n (value.hint === undefined || typeof value.hint === 'string') &&\n (value.authority === undefined ||\n value.authority === 'producer' ||\n value.authority === 'pack' ||\n value.authority === 'catalog') &&\n (value.evidence === undefined ||\n (Array.isArray(value.evidence) && value.evidence.every(isSubjectRef))) &&\n (value.recoveryIntents === undefined || isStringArray(value.recoveryIntents))\n );\n}\n\nfunction isRevisionWindow(value: unknown): value is CatalogRevisionWindow {\n if (!isRecord(value) || !Array.isArray(value.baseline) || !Array.isArray(value.current)) {\n return false;\n }\n const isPoint = (point: unknown): boolean =>\n isRecord(point) &&\n isNonEmptyString(point.rootId) &&\n typeof point.revision === 'number' &&\n Number.isSafeInteger(point.revision) &&\n point.revision >= 0;\n return value.baseline.every(isPoint) && value.current.every(isPoint);\n}\n\nfunction isTopologyDiff(value: unknown): boolean {\n if (!isRecord(value)) return false;\n return ['preserved', 'added', 'removed', 'changedKind', 'ambiguous'].every((field) =>\n Array.isArray(value[field]),\n );\n}\n\nfunction isCatalogEntry(value: unknown): value is CatalogEntry {\n if (!isRecord(value)) return false;\n if (\n !isNonEmptyString(value.guid) ||\n !isNonEmptyString(value.packageUrl) ||\n !isNonEmptyString(value.kind) ||\n !isNonEmptyString(value.sourcePath)\n ) {\n return false;\n }\n if (value.authoring !== undefined && !isRecord(value.authoring)) return false;\n if (value.packageId !== undefined && !isNonEmptyString(value.packageId)) return false;\n if (value.provenance !== undefined) {\n if (!isRecord(value.provenance)) return false;\n if (\n !isNonEmptyString(value.provenance.provider) ||\n !isNonEmptyString(value.provenance.version)\n ) {\n return false;\n }\n if (value.provenance.source !== undefined && !isNonEmptyString(value.provenance.source)) {\n return false;\n }\n }\n if (value.revision !== undefined && !isRevision(value.revision)) return false;\n if (value.sourceKey !== undefined && !isNonEmptyString(value.sourceKey)) return false;\n const sourceIndex = value.sourceIndex;\n if (\n sourceIndex !== undefined &&\n (typeof sourceIndex !== 'number' || !Number.isSafeInteger(sourceIndex) || sourceIndex < 0)\n )\n return false;\n if (value.sourceOverrides !== undefined && !isRecord(value.sourceOverrides)) return false;\n if (\n value.sourceOverrideDescriptors !== undefined &&\n !Array.isArray(value.sourceOverrideDescriptors)\n ) {\n return false;\n }\n if (value.relations !== undefined && !Array.isArray(value.relations)) return false;\n if (\n value.diagnostics !== undefined &&\n (!Array.isArray(value.diagnostics) || !value.diagnostics.every(isDiagnostic))\n )\n return false;\n if (value.name !== undefined && !isNonEmptyString(value.name)) return false;\n if (value.cookReceiptUrl !== undefined && !isNonEmptyString(value.cookReceiptUrl)) return false;\n if (value.refs !== undefined && !isStringArray(value.refs)) return false;\n if (\n value.subject !== undefined &&\n value.subject !== 'internal-asset' &&\n value.subject !== 'imported-output'\n ) {\n return false;\n }\n if (\n value.execution !== undefined &&\n value.execution !== 'direct' &&\n value.execution !== 'cooked'\n ) {\n return false;\n }\n if (\n value.lifecycle !== undefined &&\n !['missing', 'cooking', 'current', 'stale', 'failed'].includes(value.lifecycle as string)\n ) {\n return false;\n }\n if (value.projection !== undefined && !isRecord(value.projection)) return false;\n return value.publication === undefined || isRecord(value.publication);\n}\n\nexport function validateCatalogDelta(\n value: unknown,\n): Result<CatalogDelta, CatalogDeltaValidationError> {\n if (!isRecord(value)) return catalogInvalid('delta');\n if (!Array.isArray(value.added)) return catalogInvalid('added');\n if (!Array.isArray(value.changed)) return catalogInvalid('changed');\n if (!Array.isArray(value.removed)) return catalogInvalid('removed');\n if (!value.added.every(isCatalogEntry)) return catalogInvalid('added.entry');\n if (!value.changed.every(isCatalogEntry)) return catalogInvalid('changed.entry');\n if (!value.removed.every((guid): guid is string => typeof guid === 'string' && guid.length > 0)) {\n return catalogInvalid('removed.guid');\n }\n const generation = value.generation;\n if (\n (value.scopeId !== undefined && !isNonEmptyString(value.scopeId)) ||\n (generation !== undefined &&\n (typeof generation !== 'number' || !Number.isSafeInteger(generation) || generation < 1))\n ) {\n return catalogInvalid('scope');\n }\n if ((value.scopeId === undefined) !== (value.generation === undefined)) {\n return catalogInvalid('scope.generation');\n }\n if (\n value.authority !== undefined &&\n value.authority !== 'authoritative' &&\n value.authority !== 'degraded'\n ) {\n return catalogInvalid('authority');\n }\n if (\n value.diagnostics !== undefined &&\n (!Array.isArray(value.diagnostics) || !value.diagnostics.every(isDiagnostic))\n ) {\n return catalogInvalid('diagnostics');\n }\n if (value.revisions !== undefined && !isRevisionWindow(value.revisions)) {\n return catalogInvalid('revisions');\n }\n if (\n value.topology !== undefined &&\n (!Array.isArray(value.topology) || !value.topology.every(isTopologyDiff))\n ) {\n return catalogInvalid('topology');\n }\n const identityKeys = [...value.added, ...value.changed].map((entry) => entry.guid.toLowerCase());\n if (new Set(identityKeys).size !== identityKeys.length) return catalogInvalid('duplicate.guid');\n const removedKeys = value.removed.map((guid) => guid.toLowerCase());\n if (new Set(removedKeys).size !== removedKeys.length) return catalogInvalid('duplicate.removed');\n if (value.authority === 'degraded' && identityKeys.length > 0) {\n return catalogInvalid('degraded.identity');\n }\n return ok(value as unknown as CatalogDelta);\n}\n\nfunction canonicalCatalogValue(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonicalCatalogValue).join(',')}]`;\n if (isRecord(value)) {\n return `{${Object.entries(value)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, child]) => `${JSON.stringify(key)}:${canonicalCatalogValue(child)}`)\n .join(',')}}`;\n }\n return JSON.stringify(value) ?? 'null';\n}\n\nfunction digestPart(value: string, seed: bigint): string {\n let hash = seed;\n for (let index = 0; index < value.length; index += 1) {\n hash ^= BigInt(value.charCodeAt(index));\n hash = BigInt.asUintN(64, hash * 0x100000001b3n);\n }\n return hash.toString(16).padStart(16, '0');\n}\n\nconst CATALOG_DIGEST_SEEDS = [\n 0xcbf29ce484222325n,\n 0x84222325cbf29ce4n,\n 0x9e3779b185ebca87n,\n 0x517cc1b727220a95n,\n];\n\nfunction catalogDigest(value: unknown): string {\n const canonical = canonicalCatalogValue(value);\n return `sha256:${CATALOG_DIGEST_SEEDS.map((seed) => digestPart(canonical, seed)).join('')}`;\n}\n\n/** Canonical semantic identity for one Catalog row, independent of object key order. */\nexport function catalogEntryDigest(entry: CatalogEntry): string {\n return catalogDigest({ ...entry, guid: entry.guid.toLowerCase() });\n}\n\nexport function catalogDeltaDigest(delta: CatalogDelta): string {\n return catalogDigest({\n ...delta,\n added: [...delta.added].sort((left, right) => left.guid.localeCompare(right.guid)),\n changed: [...delta.changed].sort((left, right) => left.guid.localeCompare(right.guid)),\n removed: [...delta.removed].sort(),\n });\n}\n\nexport type PackIndexEntry = CatalogEntry;\n","import type { AssetStageErrorBase } from './asset-errors.js';\nimport type { SourceOverrideErrorCode, SourceOverrideMap } from './asset-producer.js';\nimport type { PackIndexEntry } from './catalog.js';\nimport type { Asset, AssetCodec, AssetRef, ImageError, TextureAsset } from './index.js';\n\nexport type ImportStageContractError = AssetStageErrorBase<'import', 'import-failed'>;\n\n// === Import contract SSOT (feat-20260603-asset-import-loader-injection M2 / w12) ===\n//\n// Decision anchors:\n// - requirements AC-07 (Importer dispatched by `meta.importer` string key) +\n// AC-08 (`importer-not-registered` fail-fast) + AC-09 (GUID import-stable\n// iron law: `guid-mismatch` / `import-produced-no-assets`) + AC-10\n// (`ImportErrorCode` is a closed union with exhaustive switch without default)\n// - plan-strategy D-6 (error model add-only: `ImportErrorCode` is an\n// independent closed union, not folded into `AssetErrorCode`) + D-1\n// (`ImporterRegistry` register/get/fail-fast mirrors LoaderRegistry) + D-4\n// (import runner skips the reserved `importer: 'shader'` key)\n// - research Finding 8 (`ImportTransport` interface slot; HTTP adapter is\n// OOS-2, landed in M4) + Finding 9 (`PackError` four-field shape is the\n// structural template)\n// - charter P3 (structured failure: `.code` / `.expected` / `.hint` /\n// `.detail`; AI users consume via property access, never `.message`\n// parsing) + P4 (consistent abstraction, structurally parallel to\n// `PackError` / `AssetError`)\n//\n// The import side is the build-time half of the import/load split. An\n// `Importer` turns an external source (a `.gltf` / `.png` / `.ttf` on disk)\n// plus its `*.meta.json` GUID declarations into in-memory `ImportedAsset[]`\n// (internal `Asset` PODs stamped with the meta-declared GUIDs). The import\n// runner then materializes those into the DDC (`.pack.json` / `.bin`). The\n// `Importer` itself stays pure of disk write + GUID minting — it consumes the\n// meta-declared GUIDs (GUID import-stable iron law) and emits PODs only.\n\n/**\n * Closed `ImportErrorCode` union for build-time import failures.\n * Used exclusively by the build-time `@forgeax/engine-import` runner +\n * `ImporterRegistry` fail-fast chain. Domain-separated from the runtime\n * `AssetErrorCode` (the `loadByGuid` / `get` surface) and the disk-scanner\n * `PackErrorCode` — disjoint lifecycle phases. Counts evolve; see\n * AGENTS.md §Error model for the live roster.\n *\n * Exhaustive `switch (err.code)` needs no `default:` — TypeScript guards\n * union completeness at compile time (charter P2 machine-readable union >\n * prose + P3 explicit failure).\n *\n * | code | trigger |\n * |:--|:--|\n * | `'importer-not-registered'` | the import runner read `meta.importer` but the injected `ImporterRegistry` has no importer for that key; `.detail.importer` is the missing key and `.detail.registeredImporters` lists the keys currently wired (charter P3 — AI users read `.detail.registeredImporters` to know what to inject). |\n * | `'source-read-failed'` | the source file referenced by `meta.source` could not be read (missing / unreadable); `.detail.source` is the path and `.detail.reason` the underlying error string. |\n * | `'import-produced-no-assets'` | the importer returned an empty `ImportedAsset[]`, or omitted a GUID that `meta.subAssets[]` declared (the produced GUID set is not a superset of the declared set); `.detail.missingGuids` lists the declared GUIDs the importer failed to produce. |\n * | `'guid-mismatch'` | the importer produced a GUID that `meta.subAssets[]` never declared (violates the GUID import-stable iron law); `.detail.unexpectedGuids` lists the produced GUIDs absent from the declared set. |\n * | `'import-internal-error'` | the importer failed at runtime. Two sub-cases ride `.detail`: a build-time module-LOAD failure surfaces `.detail.loadError`, while a conversion THROW surfaces `.detail.reason`. |\n * | `'source-validation-failed'` | the source was readable but failed an authoring rule; `.detail.diagnostics` contains source-located, machine-readable findings. |\n */\nexport type ImportErrorCode =\n | SourceOverrideErrorCode\n | 'importer-not-registered'\n | 'source-read-failed'\n | 'import-produced-no-assets'\n | 'guid-mismatch'\n | 'mesh-material-slot-topology-change'\n | 'import-internal-error'\n | 'source-validation-failed';\n\n/** A source range shared by all import diagnostics. */\nexport interface ImportSourceRange {\n readonly start: number;\n readonly end: number;\n readonly line: number;\n readonly column: number;\n}\n\n/** A related source location for a cross-file import diagnostic. */\nexport interface ImportDiagnosticLocation {\n readonly sourcePath: string;\n readonly sourceRange: ImportSourceRange;\n}\n\n/** Machine-readable provenance for one blocking or quality import finding. */\nexport interface ImportDiagnostic {\n readonly code: string;\n readonly severity: 'error' | 'warning';\n readonly sourcePath: string;\n readonly sourceRange: ImportSourceRange;\n readonly rule: string;\n readonly expected: string;\n readonly actual: string;\n readonly hint: string;\n readonly relatedLocations?: readonly ImportDiagnosticLocation[];\n}\n\n/**\n * Discriminated detail union for {@link ImportError} — narrowed per\n * `ImportError.code`. AI users access `err.detail.<field>` directly after\n * `switch (err.code)` narrows the variant. Structurally parallel to\n * `PackErrorDetail` (the `code` field is intentionally absent from each\n * variant; identify via the top-level `ImportError.code`).\n */\nexport type ImportErrorDetail =\n | {\n /** The `meta.importer` key with no registered importer. */\n readonly importer: string;\n /** The importer keys currently wired into the registry (insertion order). */\n readonly registeredImporters: readonly string[];\n }\n | {\n /** The `meta.source` path that could not be read. */\n readonly source: string;\n /** The underlying read error message. */\n readonly reason: string;\n }\n | {\n /** Declared sub-asset GUIDs the importer failed to produce (empty when the importer produced nothing at all). */\n readonly missingGuids: readonly string[];\n }\n | {\n /** Produced GUIDs absent from the `meta.subAssets[]` declared set. */\n readonly unexpectedGuids: readonly string[];\n }\n | {\n /** The original thrown error message (importer loaded but its conversion threw). */\n readonly reason: string;\n }\n | {\n /**\n * The module-load failure message (feat-20260629 D-5): the importer\n * module / native addon could not be loaded at build time (e.g.\n * module-not-found, native-addon-not-built). Distinguishes a LOAD\n * failure from a conversion THROW (`reason`) under the same\n * `import-internal-error` code without growing the closed ImportErrorCode\n * union. AI users branch on `'loadError' in err.detail`.\n */\n readonly loadError: string;\n }\n | {\n /** Source-located authoring findings retained across the import boundary. */\n readonly diagnostics: readonly ImportDiagnostic[];\n }\n | {\n /** Source override identity that failed Meta topology validation. */\n readonly sourceKey?: string;\n readonly declaredSourceKeys: readonly string[];\n readonly reason?: string;\n }\n | {\n readonly meshGuid: string;\n readonly meshSourceKey?: string;\n readonly previousIndices: readonly number[];\n readonly nextIndices: readonly number[];\n };\n\n/**\n * Structured import error — four-field surface (`.code` / `.expected` /\n * `.hint` / `.detail`) structurally parallel to `PackError` / `AssetError`\n * (charter P4 consistent abstraction). `.detail` is narrowed per `.code` via\n * {@link ImportErrorDetail}.\n *\n * AI users consume the structured surface via property access:\n * `switch (err.code) { case 'guid-mismatch': ... err.detail.unexpectedGuids ... }`\n * — never by parsing `.message` (charter P3 red line).\n */\nexport class ImportError extends Error {\n readonly code: ImportErrorCode;\n readonly expected: string;\n readonly actual?: string;\n readonly hint: string;\n readonly detail: ImportErrorDetail;\n\n constructor(args: {\n code: ImportErrorCode;\n expected: string;\n actual?: string;\n hint: string;\n detail: ImportErrorDetail;\n }) {\n super(`[ImportError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);\n this.name = 'ImportError';\n this.code = args.code;\n this.expected = args.expected;\n if (args.actual !== undefined) this.actual = args.actual;\n this.hint = args.hint;\n this.detail = args.detail;\n }\n}\n\n/**\n * Per-code `.hint` string literals SSOT. `Record<ImportErrorCode, string>`\n * makes a new closed-union member a compile-time error here as well\n * (reinforces charter P3 explicit failure). Consumed by the import runner +\n * tests so the producer and the fixtures share one source of truth.\n */\nexport const IMPORT_ERROR_HINTS: Readonly<Record<ImportErrorCode, string>> = {\n 'importer-not-registered':\n 'no importer registered for this meta.importer key; register one via importers.register(importer) (the importer carries its own key, e.g. gltfImporter / imageImporter); err.detail.registeredImporters lists the keys currently wired',\n 'source-read-failed':\n 'the file at meta.source could not be read; check the path is correct relative to the sidecar and the process has read access',\n 'import-produced-no-assets':\n 'the importer produced no assets, or omitted a GUID that meta.subAssets[] declared; the produced GUID set must be a superset of the declared set (GUID import-stable iron law); err.detail.missingGuids lists the declared GUIDs not produced',\n 'guid-mismatch':\n 'the importer produced a GUID that meta.subAssets[] never declared (violates the GUID import-stable iron law: GUIDs come from the external meta, never minted by the importer); err.detail.unexpectedGuids lists the offending GUIDs',\n 'mesh-material-slot-topology-change':\n 'the importer could not match previous and current Mesh material slots without ambiguity; name source materials uniquely or repair their stable sourceKey values before reimport',\n 'import-internal-error':\n 'the importer failed at runtime; branch on err.detail: a conversion THROW carries err.detail.reason (the loaded importer threw while converting the source — an importer bug, not a meta / source problem), while a build-time module-LOAD failure carries err.detail.loadError (the host importer module / native addon could not be imported)',\n 'source-validation-failed':\n 'the source violates an import authoring rule; inspect err.detail.diagnostics fields (code, sourcePath, sourceRange, rule, expected, actual, hint, and relatedLocations) and fix the referenced source',\n 'unknown-source-key':\n 'sourceOverrides contains a key absent from meta.subAssets[]; refresh the producer topology and use one of err.detail.declaredSourceKeys',\n 'duplicate-source-key':\n 'the producer declared the same sourceKey more than once; repair the Meta topology before importing',\n 'invalid-source-overrides':\n 'sourceOverrides must be an object keyed by producer-owned sourceKey values',\n 'invalid-source-override-payload':\n 'each sourceOverrides value must be a producer-owned object validated by the importer',\n};\n\n/**\n * One asset produced by an {@link Importer}: the meta-declared `guid`, the\n * in-memory `Asset` POD, and its outbound GUID cross-references (`refs`). The\n * import runner folds these into the DDC `.pack.json` `assets[]` rows (one\n * `ImportedAsset` -> one `{ guid, kind, payload, refs }` row).\n *\n * The `guid` always comes from `meta.subAssets[].guid` (GUID import-stable\n * iron law) — the importer never mints it; it reads the declared GUID off the\n * meta and stamps it here. `kind` mirrors the `Asset.kind` discriminant so the\n * DDC row and the runtime loader dispatch on the same string.\n */\nexport interface ImportedAsset<P = Asset> {\n readonly guid: string;\n readonly kind: string;\n readonly name?: string;\n readonly payload: P;\n readonly refs: readonly AssetRef[];\n readonly artifacts: Readonly<Record<string, ImportedArtifactBody>>;\n}\n\n/**\n * One declared sub-asset entry the import runner hands to an\n * {@link Importer.import} call — the meta-declared `guid` + its `sourceIndex`\n * + `kind`. Mirrors the `meta.subAssets[]` rows so the importer can map a\n * source object index to the GUID it must stamp (GUID import-stable iron law).\n */\nexport interface ImportSubAsset {\n readonly guid: string;\n readonly sourceIndex: number;\n /** Producer-owned semantic identity used to look up sourceOverrides. */\n readonly sourceKey?: string;\n readonly kind: string;\n}\n\n/**\n * Capabilities + declarations the import runner wires into an\n * {@link Importer.import} call. The importer reads the source bytes via\n * `readSource`, the GUID declarations via `subAssets`, and the free-form\n * importer settings via `importSettings`. It stays pure of disk write +\n * registry bookkeeping (pipeline isolation, architecture-principles #4).\n *\n * - `source` — the `meta.source` path (relative to the sidecar), for\n * diagnostics + the importer's own external-resource resolution base.\n * - `readSource()` — fetch the raw source bytes (the runner has already\n * resolved the path); a structured failure here surfaces as\n * `source-read-failed`.\n * - `subAssets` — the `meta.subAssets[]` GUID declarations the importer must\n * honour (GUID import-stable iron law).\n * - `importSettings` — free-form importer settings copied verbatim from the\n * sidecar.\n * - `readSibling(uri)` — fetch raw bytes of a file co-located with the\n * primary source (e.g. an `.gltf` referencing an external `.bin` /\n * `.png` via relative URI). Failures surface as `source-read-failed`\n * (C-6 — no specialised error code; the URI is forensic detail). Used\n * by gltfImporter to resolve `images[].uri` external references at\n * import time.\n * - `decodeImage(bytes, mimeType, importSettings)` — decode raw image\n * bytes (PNG / JPEG) into a `TextureAsset` POD plus a `bytes` copy\n * suitable for `<guid>.bin` emission. When the host applies an offline\n * delivery codec, it also returns the artifact media type and codec so\n * gltfImporter can preserve those facts in the Pack v2 envelope. The seam\n * keeps gltfImporter\n * out of `@forgeax/engine-image` (D-1: zero static `from\n * '@forgeax/engine-image'` edge in `packages/gltf/src`). The\n * concrete implementation (parseImage + format derivation) lives\n * behind `@forgeax/engine-image/image-importer`; the build-time\n * orchestrator (vite-plugin-pack / cli-gltf / tests) binds the\n * callback when constructing the runner's `ImportRunnerFs`.\n */\nexport interface ImportContext {\n readonly source: string;\n readSource(): Promise<\n | { readonly ok: true; readonly value: Uint8Array }\n | { readonly ok: false; readonly error: unknown }\n >;\n readSibling(\n uri: string,\n ): Promise<\n | { readonly ok: true; readonly value: Uint8Array }\n | { readonly ok: false; readonly error: ImportError }\n >;\n decodeImage(\n bytes: Uint8Array,\n mimeType: 'image/png' | 'image/jpeg' | 'image/x-tga',\n importSettings: Readonly<Record<string, unknown>>,\n ): Promise<\n | {\n readonly ok: true;\n readonly value: {\n readonly texture: TextureAsset;\n readonly bytes: Uint8Array;\n readonly mediaType?: string;\n readonly assetCodec?: AssetCodec;\n };\n }\n | { readonly ok: false; readonly error: ImageError }\n >;\n readonly subAssets: readonly ImportSubAsset[];\n readonly importSettings: Readonly<Record<string, unknown>>;\n /** Optional Meta author facts passed through without importer-kind branching. */\n readonly sourceOverrides?: SourceOverrideMap;\n}\n\n/**\n * Build-time importer injected into the `ImporterRegistry`. One importer per\n * `meta.importer` key; the import runner dispatches on the key.\n *\n * `import` is pure of disk write + GUID minting: it reads the source via\n * `ctx.readSource()`, honours the `ctx.subAssets[]` GUID declarations, and\n * returns the produced `ImportedAsset[]`. The runner validates the produced\n * GUID set against the declared set (GUID import-stable iron law) and writes\n * the DDC. A thrown error is wrapped by the runner into\n * `import-internal-error` (charter P3) — importers may throw, but should\n * prefer returning a partial / empty result so the runner can attribute the\n * failure precisely.\n */\nexport interface ImportProductFinalizeArtifact {\n readonly path: string;\n readonly mimeType: string;\n readonly bytes: Uint8Array;\n}\n\nexport interface ImportProductFinalizeOptions {\n readonly artifactUrl: (artifact: ImportProductFinalizeArtifact) => string;\n}\n\nexport type ImportProductFinalizeResult =\n | {\n readonly ok: true;\n readonly value: {\n readonly asset: unknown;\n readonly artifacts: readonly { readonly path: string; readonly mimeType: string }[];\n };\n }\n | {\n readonly ok: false;\n readonly error: {\n readonly code: string;\n readonly expected: string;\n readonly hint: string;\n readonly detail: unknown;\n };\n };\n\n/** Optional producer capability exposed to the generic import runner. */\nexport interface ImporterCapabilities {\n readonly decodeImage?: ImportContext['decodeImage'];\n /** Producer-owned Catalog visibility for declarations before materialization. */\n readonly catalog?: {\n readonly publish?: (input: {\n readonly importSettings: Readonly<Record<string, unknown>>;\n readonly subAssets: readonly ImportSubAsset[];\n }) => boolean;\n };\n}\n\nexport interface Importer {\n readonly key: string;\n // biome-ignore lint/suspicious/noExplicitAny: pending downstream importer migration keeps old consumers source-compatible\n import(ctx: ImportContext): Promise<any> | any;\n readonly capabilities?: ImporterCapabilities;\n /** Optional owner projection for transport artifacts produced by this importer. */\n finalize?: (\n product: ImportProduct<unknown>,\n options: ImportProductFinalizeOptions,\n ) => ImportProductFinalizeResult;\n}\n/**\n * Interface slot for the M4 lazy-import transport (OOS-2). A runtime\n * `ImportTransport` fetches a missing DDC artefact on demand (the shipped form\n * never falls back to a runtime import; see `AssetErrorCode 'asset-not-imported'`).\n * Declared here as a contract seam only — the HTTP adapter lands in M4 w31;\n * M2 does not implement it (plan-strategy D-6 / research Finding 8).\n */\nexport interface ImportTransport {\n /**\n * Trigger an on-demand DDC import for a GUID at runtime. On success the\n * transport returns the freshly imported catalog rows for the GUID (and any\n * sub-asset siblings produced by the same import) so the caller patches just\n * those rows into its catalog cache -- per-asset incremental, never a\n * whole-catalog re-fetch (the four-verb redesign, 2026-06-06). `entries` may\n * be empty when the transport imported the artefact but does not surface the\n * rows; the caller then re-resolves the GUID from its (possibly stale) cache.\n * `ok: false` means the import did not produce an artefact and the caller\n * surfaces `asset-not-imported`.\n */\n fetchPack(\n guid: string,\n scope?: Pick<\n import('./runtime-scope.js').RuntimeAssetBinding,\n 'scopeId' | 'generation' | 'status'\n >,\n ): Promise<\n { readonly ok: true; readonly entries?: readonly PackIndexEntry[] } | { readonly ok: false }\n >;\n}\n\n/** A normalized filesystem path observed by an importer read attempt. */\nexport type SourceDependency = string;\n\n/** Logical artifact content emitted by one imported asset. */\nexport interface ImportedArtifactBody {\n readonly mediaType: string;\n readonly assetCodec?: AssetCodec;\n readonly bytes: Uint8Array;\n}\n\n/** Generic import output shared by every importer and transport. */\nexport interface ImportProduct<P = Asset> {\n readonly assets: readonly ImportedAsset<P>[];\n readonly sourceDependencies: readonly SourceDependency[];\n}\n\n/** Structured result returned by an importer. */\nexport type ImportResult<P = Asset> =\n | { readonly ok: true; readonly value: ImportProduct<P> }\n | { readonly ok: false; readonly error: ImportError };\n\n/* ImportTransport is intentionally kept with the build-time contract. */\n","// @forgeax/engine-types — POD types, union aliases, and cross-package primitives SSOT.\n//\n// Proposition: this package is the single source of truth for shared shapes that\n// must NOT diverge across @forgeax/engine-rhi / ecs / naga / image / gltf / console /\n// render-graph / shader / future renderer packages.\n//\n// Scope:\n// - POD types & union aliases — Asset / MaterialAsset / RenderQueue / PassKind /\n// FontAsset / RenderPipelineAsset / SceneAsset /\n// PackErrorCode / ImageErrorCode / AudioErrorCode / PhysicsErrorCode etc.\n// - Structured error classes — AssetError / FontError / TextError / AudioError /\n// PhysicsError (carry .code / .expected / .hint surface).\n// - Project-wide Result<T, E> + ok / err factories (tweak-20260612-result-into-types\n// consolidated 5 byte-aligned-by-prose copies into this single module).\n// - GPUFlagsConstant aliases for @webgpu/types numeric runtime constants — the\n// global objects (e.g. GPUBufferUsage.MAP_READ) are still consumed directly by\n// upstream callers; only the literal type aliases live here (decision S-6 /\n// research F-2 option (b)).\n//\n// Shape rules:\n// - math-free (no vec / mat / quat dependency).\n// - Single-source policy — fields already exported by @webgpu/types are re-exported\n// verbatim as one-line aliases; never duplicated here.\n//\n// Anchors: requirements §AC AC-01 + MVP-1.5; plan-strategy §2 S-6 + §7.6 propositions\n// 4 / 5; research §F-2 (`GPUFlagsConstant = number` alias).\n\n/// <reference types=\"@webgpu/types\" />\n\n// === Handle SSOT barrel re-export (feat-20260517-handle-type-unify M2 / D-2) ====\n//\n// `./handle` carries the unique double-axis Handle<T extends string, M> brand\n// + AssetTagMap + TagOf + 3 factories (toUnique / toShared / unwrapHandle).\n// The legacy 1-arg form that lived here at M1 has been physically deleted\n// (M2 t10) so the package surface exposes a single Handle shape (charter F1\n// single-entry indexability; AC-03 grep gate).\n\nimport type { AnimationTargetIdValue } from './animation-target';\nimport type { AudioClipAsset } from './asset.js';\nimport type { MaterialAsset } from './material/asset.js';\nimport type { ParticleEffectAsset } from './vfx';\n\nexport type { AnimationTargetIdValue } from './animation-target';\nexport * from './asset.js';\nexport * from './asset-errors.js';\nexport type {\n AssetArtifactReader,\n AssetDecoder,\n AssetDecoderContribution,\n AssetDecoderContributionRef,\n AssetDecoderInput,\n AssetDecoderLease,\n AssetDecoderResult,\n AssetKind,\n AssetKindPayload,\n AssetRegistryAction,\n AssetRuntimeApiGroups,\n BuiltinAssetKind,\n BuiltinAssetKindToken,\n BuiltinAssetPayload,\n} from './asset-runtime.js';\nexport * from './handle';\nexport * from './material/index.js';\n\n// === Result<T, E> SSOT (tweak-20260612-result-into-types) ======================\n//\n// Single physical source of `Result<T, E>` + `ok(...)` / `err(...)`. Replaces\n// the prior dual copies in packages/rhi/src/errors.ts + packages/ecs/src/result.ts\n// (those copies were \"byte-for-byte aligned\" by prose, not by mechanism).\n// AI users import the binary success/failure carrier via `@forgeax/engine-types`\n// (or via the rhi/ecs package barrels which re-export this same module).\nexport * from './result';\nexport * from './runtime-scope';\nexport * from './vfx';\n\n// === Sub-asset POD SSOT (feat-20260615-fbx-importer-via-sdk M1 / t9) ===========\n//\n// Importer-independent pure-data IR types shared across glTF / FBX / future\n// format importers. These are pre-kind, pre-guid data carriers — each importer\n// writes these Pods from its format-specific JSON POD, and `to-asset-pack`\n// promotes them to registry-ready Asset handles.\n//\n// Design axioms:\n// - SSOT (architecture-principles #1): defined once in @forgeax/engine-types,\n// consumed via import by gltf / fbx / future importer packages.\n// - Derive, don't duplicate (#2): gltf/fbx drop their local MeshIr/MeshRecord\n// and import MeshPod — no per-package copy.\n// - No format prefix (plan-strategy section 8): Pod types are named after the\n// *asset kind* they represent, not the source format. AI users write code\n// that reads MeshPod regardless of whether the source was FBX or glTF.\n// - Pre-kind data (charter P4): Pods carry raw geometric/material data without\n// `kind` discriminant fields. The importer bridge layer adds `kind` when\n// converting Pod -> Asset handle.\n//\n// Pod roster (AC-01..AC-07):\n// MeshPod — vertices/indices/attributes/submeshes\n// MaterialPod — PBR parameter values (baseColor/metallic/roughness)\n// ScenePod — entity hierarchy + mount points\n// TexturePod — external file path (cross-platform normalized)\n// SkeletonPod — joint count + inverse bind matrices\n// SkinPod — skeleton reference + joint paths\n// AnimationClipPod — duration + channels + samplers\n\n// === AC-01: MeshPod — pure geometric data ===\n\n/** Per-submesh descriptor within a MeshPod. */\nexport interface MeshSubmeshPod {\n /** Vertex count for this submesh (draw count when non-indexed). */\n readonly vertexCount: number;\n /** Index count when indexed; 0 for non-indexed geometry. */\n readonly indexCount: number;\n /** Byte offset into the shared indices buffer (0-based). */\n readonly indexOffset: number;\n /** Material binding index into the parent document's materials array. */\n readonly materialIndex: number | null;\n /** Primitive topology. */\n readonly topology: 'triangle-list' | 'line-list' | 'line-strip' | 'point-list';\n}\n\n/** MeshPod: pre-kind geometric data IR shared across importers. */\nexport interface MeshPod {\n /** Optional debug name from source document. */\n readonly name?: string;\n /** Packed vertex positions (Float32Array, 3 floats per vertex). */\n readonly vertices: Float32Array;\n /** Packed triangle indices (Uint16Array or Uint32Array). Absent when non-indexed. */\n readonly indices?: Uint16Array | Uint32Array;\n /** Per-vertex attributes keyed by semantic (POSITION/NORMAL/TEXCOORD_0/JOINTS_0/WEIGHTS_0). */\n readonly attributes: Record<string, Float32Array | Uint16Array | Uint32Array>;\n /** Optional additive morph deltas, one entry per target. */\n readonly morphTargets?: readonly MorphTarget[];\n /** Optional default morph weights, one value per target. */\n readonly morphWeights?: Float32Array;\n /** Per-submesh descriptors (>=1). */\n readonly submeshes: readonly MeshSubmeshPod[];\n /** Source mesh index within the original document (for diagnostic mapping). */\n readonly sourceIndex: number;\n}\n\n// === AC-02: MaterialPod — PBR parameter values ===\n\n/** MaterialPod: pre-kind PBR material data IR shared across importers. */\nexport const MATERIAL_TEXTURE_SLOTS = [\n 'baseColorTexture',\n 'normalTexture',\n 'specularTintTexture',\n 'metallicRoughnessTexture',\n 'emissiveTexture',\n 'occlusionTexture',\n] as const;\n\nexport type MaterialTextureSlot = (typeof MATERIAL_TEXTURE_SLOTS)[number];\n\nexport interface MaterialTextureBindingPod {\n /** Standard material value slot receiving the texture reference. */\n readonly slot: MaterialTextureSlot;\n /** Index into the parent document's textures array. */\n readonly textureIndex: number;\n /** Optional source UV set. */\n readonly texCoord?: number;\n}\n\nexport interface MaterialPod {\n /** Optional debug name from source document. */\n readonly name?: string;\n /** RGBA base color factor (linear space). */\n readonly baseColorFactor: readonly [number, number, number, number];\n /** Metallic factor (0..1). */\n readonly metallicFactor: number;\n /** Roughness factor (0..1). */\n readonly roughnessFactor: number;\n /** Index into the parent document's textures array for base color map. */\n readonly baseColorTextureIndex?: number;\n /** Index for metallic-roughness packed texture. */\n readonly metallicRoughnessTextureIndex?: number;\n /** Index for normal map. */\n readonly normalTextureIndex?: number;\n /** Index for occlusion map. */\n readonly occlusionTextureIndex?: number;\n /** Index for emissive map. */\n readonly emissiveTextureIndex?: number;\n /** Index for a specular tint map. */\n readonly specularTintTextureIndex?: number;\n /** Engine-owned semantic texture bindings from the producer. */\n readonly textureBindings?: readonly MaterialTextureBindingPod[];\n}\n\n// === AC-03: ScenePod — entity hierarchy ===\n\n/** A single entity node within a ScenePod hierarchy. */\nexport interface SceneEntityPod {\n /** Entity name (for Name component attachment). */\n readonly name: string;\n /** Decomposed local transform (TRS). */\n readonly transform: {\n readonly translation: readonly [number, number, number];\n readonly rotation: readonly [number, number, number, number];\n readonly scale: readonly [number, number, number];\n };\n /** Index into the parent document's meshes array. Null when not a mesh node. */\n readonly meshIndex: number | null;\n /** Children entity indices in the flattened entities array. */\n readonly children: readonly number[];\n}\n\n/** ScenePod: entity hierarchy IR shared across importers. */\nexport interface ScenePod {\n /** Optional scene name. */\n readonly name?: string;\n /** Flattened entity list (topological order, parents before children). */\n readonly entities: readonly SceneEntityPod[];\n /** Index of the default/root scene entity. */\n readonly rootEntityIndex: number;\n}\n\n// === AC-04: TexturePod — external file path ===\n\n/** TexturePod: external texture reference IR shared across importers. */\nexport interface TexturePod {\n /** Optional texture name. */\n readonly name?: string;\n /** Filesystem path relative to the source document, with '/' separators. */\n readonly filePath: string;\n /** The producer-declared relative path before host resolution. */\n readonly relativeFilePath?: string;\n /** Parse-scope absolute hint; never persist this into project metadata. */\n readonly absoluteFilePath?: string;\n /** Embedded encoded image bytes, when the FBX contains the texture payload. */\n readonly embeddedBytes?: Uint8Array;\n /** Producer texture kind; only file/embedded textures are importable. */\n readonly type?: 'file' | 'layered' | 'procedural' | 'shader' | 'unknown';\n /** Source texture index within the original document. */\n readonly sourceIndex: number;\n}\n\n// === AC-05: SkeletonPod — joint hierarchy ===\n\n/** SkeletonPod: skeleton joint data IR shared across importers. */\nexport interface SkeletonPod {\n /** Number of joints. */\n readonly jointCount: number;\n /** Inverse bind matrices, Float32Array of length jointCount * 16. */\n readonly inverseBindMatrices: Float32Array;\n /** Per-joint name path from scene root (parallel to joints array). */\n readonly jointPaths: readonly string[];\n}\n\n// === AC-06: SkinPod — vertex skinning data ===\n\n/** Per-vertex joint influence descriptor. */\nexport interface SkinVertexInfluencePod {\n /** 4 joint indices (Uint16Array), always padded to 4 entries. */\n readonly jointIndices: Uint16Array;\n /** 4 joint weights (Float32Array), always padded to 4 entries. */\n readonly jointWeights: Float32Array;\n}\n\n/** SkinPod: vertex skinning data IR shared across importers. */\nexport interface SkinPod {\n /** GUID-like identifier for the associated SkeletonAsset (resolved at bridge time). */\n readonly skeletonGuid: string;\n /** Joint name paths (same as SkeletonPod.jointPaths for cross-reference). */\n readonly jointPaths: readonly string[];\n /** Number of influenced vertices. */\n readonly vertexCount: number;\n /** Per-vertex joint influences (4 joints per vertex). */\n readonly influences: readonly SkinVertexInfluencePod[];\n}\n\n// === AC-07: AnimationClipPod — keyframe animation ===\n\n/** Animation sampler (keyframe data for one property). */\nexport interface AnimationSamplerPod {\n /** Keyframe timestamps (ascending, seconds). */\n readonly input: Float32Array;\n /** Keyframe values (packed per-element stride). */\n readonly output: Float32Array;\n /** Interpolation mode. */\n readonly interpolation: 'LINEAR' | 'STEP';\n}\n\n/** Animation channel (one target-property pair). */\nexport interface AnimationChannelPod {\n /** Stable animation target identity. */\n readonly targetId: AnimationTargetIdValue;\n /** Target property: 'translation' | 'rotation' | 'scale' | 'weights'. */\n readonly property: 'translation' | 'rotation' | 'scale' | 'weights';\n /** Sampler driving this channel. */\n readonly sampler: AnimationSamplerPod;\n}\n\n/** AnimationClipPod: keyframe animation clip IR shared across importers. */\nexport interface AnimationClipPod {\n /** Optional clip name. */\n readonly name?: string;\n /** Clip duration in seconds (max sampler.input[last]). */\n readonly duration: number;\n /** Per-animation-target-property channels. */\n readonly channels: readonly AnimationChannelPod[];\n}\n\n// === Asset system v1 SSOT (feat-20260511-asset-system-v1) ======================\n//\n// Decision anchors:\n// - requirements §G7 + §2 row 8 + AC-09 / AC-15 / AC-21 (4-variant Asset\n// discriminated union, 14-key VertexAttributeMap closed set,\n// AssetErrorCode 4-member closed union elevated to TS alias)\n// - plan-strategy §2 D-P1 (@forgeax/engine-types single-file SSOT for\n// Asset union + AssetErrorCode; 4-member AssetErrorCode independent from\n// RhiErrorCode, AI users discover through one-line import)\n// - plan-strategy §7.2 (lowercase key alignment with Three.js r184\n// BufferGeometry mental migration; D-P5 preserves segments 6 params)\n// - plan-strategy §7.3 (AssetError .hint strings per error code, verbatim)\n// - charter proposition 1 (single-entry IDE autocomplete via\n// `@forgeax/engine-types`) + proposition 3 (machine-readable union >\n// prose) + proposition 4 (explicit failure - exhaustive switch needs no\n// default fallback) + proposition 5 (consistent abstraction - structurally\n// parallel to RhiError / InspectorError / MetricError 4-field surface)\n// - architecture-principles #1 SSOT (4 literals + shape live here once;\n// @forgeax/engine-runtime AssetRegistry / tests / AGENTS.md Error model\n// table all reference this module)\n\n/**\n * Mesh asset POD shape aligned with Three.js r184 BufferGeometry mental\n * model (plan-strategy §7.2 naming convention). `vertices` is the interleaved\n * or primary position buffer; `indices` narrows to `Uint16Array | Uint32Array`\n * per WebGPU spec index format; `attributes` is the VertexAttributeMap\n * lowercase-key closed set.\n *\n * Canonical lowercase keys include position, normal, uv, tangent, skinIndex,\n * skinWeight, uv1..uv7 and optional linear RGBA color.\n *\n * Designed for M3 GLTF loader single-layer mapping\n * (`POSITION -> position` / `TEXCOORD_0 -> uv` etc.) without runtime rename.\n */\nexport interface MeshAsset {\n readonly kind: 'mesh';\n readonly vertices: Float32Array;\n /**\n * Index buffer. Optional: vertex-only meshes (point-list / line-list with no\n * shared vertices) omit it, and the engine takes a non-indexed draw path\n * (`pass.draw(vertexCount)` instead of `pass.drawIndexed`). When present the\n * indexed path is byte-for-byte unchanged.\n */\n readonly indices?: Uint16Array | Uint32Array;\n readonly attributes: VertexAttributeMap;\n /**\n * Axis-aligned bounding box in local space: 6 floats [minX, minY, minZ, maxX, maxY, maxZ].\n *\n * Producer obligation: every MeshAsset MUST carry a computed `aabb` from its\n * position attribute. Built-in producers (glTF, FBX, geometry factories)\n * fill it automatically via `box3.fromPositions` -- the single\n * authoritative implementation in @forgeax/engine-math.\n *\n * When `aabb` is `undefined`, the pick() broad-phase and frustum culling\n * silently skip the mesh -- pick() returns `undefined` for every ray query\n * against it with no diagnostic signal. AI users hand-writing MeshAsset\n * must self-check that `aabb` is populated.\n *\n * Empty / degenerate position input (0 vertices) produces an\n * inverted-infinity empty box (min = +Infinity, max = -Infinity).\n * Consumers reject this for picking (min.x > max.x) and may skip it\n * for culling.\n *\n * The bare Float32Array keeps engine-types math-free (no Box3 branded\n * type dependency). Consumers narrow to the math-layer Box3 via\n * `as Box3Like`.\n */\n readonly aabb?: Float32Array;\n /**\n * Submeshes partition the index/vertex range into independent draw calls,\n * each with its own topology (one of the 5 WebGPU primitives:\n * 'point-list' | 'line-list' | 'line-strip' | 'triangle-list' | 'triangle-strip').\n *\n * Every mesh must declare at least one submesh. The engine draws one\n * `drawIndexed` (or `draw` for vertex-only) per submesh entry, and pairs\n * them with `MeshRenderer.materials[]` by index position.\n *\n * Must be non-empty: an empty array triggers a `mesh-asset-submeshes-empty`\n * AssetError at register-time (fail-fast, charter P3 explicit failure).\n */\n readonly submeshes: readonly Submesh[];\n /** Stable, mesh-owned material entry points shared by every instance. */\n readonly materialSlots: readonly MeshMaterialSlot[];\n /** Target-major dense morph deltas. */\n readonly morphTargets?: readonly MorphTarget[];\n /** Authored default weights, one value per morph target when present. */\n readonly morphWeights?: Float32Array;\n}\n\nexport interface MorphTarget {\n readonly position?: Float32Array;\n readonly normal?: Float32Array;\n readonly tangent?: Float32Array;\n}\n\n/** Mesh-owned default binding for one stable material slot. */\nexport interface MeshMaterialSlot {\n /** Unique, non-empty display/tooling name within the mesh. */\n readonly slotName: string;\n /** Producer-owned stable identity used to preserve slot indices on reimport. */\n readonly sourceKey?: string;\n /** Missing means the slot intentionally inherits the neutral engine material. */\n readonly defaultMaterial?: AssetGuid;\n}\n\n/** JSON-safe producer topology persisted beside an imported Mesh output. */\nexport interface MeshMaterialSlotTopologyEntry {\n readonly slotName: string;\n readonly sourceKey?: string;\n readonly defaultMaterialGuid?: string;\n /** Persisted removed-slot identity; never participates in cooked bindings. */\n readonly tombstone?: true;\n}\n\n/** Resolve the persisted source/authoring layers to the one runtime Mesh default. */\nexport function resolveMeshMaterialSlotDefaultGuid(\n slot: MeshMaterialSlotTopologyEntry,\n authoredDefaultMaterialGuid?: string | null,\n): string | undefined {\n if (authoredDefaultMaterialGuid !== undefined) {\n return authoredDefaultMaterialGuid === null ? undefined : authoredDefaultMaterialGuid;\n }\n return slot.defaultMaterialGuid;\n}\n\nexport interface MeshMaterialSlotTopologyChange {\n readonly code: 'mesh-material-slot-topology-change';\n readonly previousIndices: readonly number[];\n readonly nextIndices: readonly number[];\n readonly hint: string;\n}\n\nexport type MeshMaterialSlotReconcileResult =\n | {\n readonly ok: true;\n readonly slots: readonly MeshMaterialSlotTopologyEntry[];\n /** New source-order slot index -> stable persisted slot index. */\n readonly currentToStableSlot: readonly number[];\n }\n | { readonly ok: false; readonly error: MeshMaterialSlotTopologyChange };\n\n/**\n * Preserve positional renderer overrides across source reimport.\n *\n * Stable sourceKey wins, then a unique slotName. A single remaining pair is\n * the only unambiguous source-order fallback. Removed slots remain as\n * defaultless tombstones; new slots append, so an old index never silently\n * starts naming a different source material.\n */\nexport function reconcileMeshMaterialSlotTopology(\n current: readonly MeshMaterialSlotTopologyEntry[],\n previous: readonly MeshMaterialSlotTopologyEntry[] = [],\n): MeshMaterialSlotReconcileResult {\n if (previous.length === 0) {\n return { ok: true, slots: [...current], currentToStableSlot: current.map((_, index) => index) };\n }\n\n const stable: MeshMaterialSlotTopologyEntry[] = previous.map((slot) => ({\n slotName: slot.slotName,\n ...(slot.sourceKey === undefined ? {} : { sourceKey: slot.sourceKey }),\n tombstone: true,\n }));\n const mapping = new Array<number>(current.length).fill(-1);\n const usedPrevious = new Set<number>();\n\n const uniqueIndex = (\n slots: readonly MeshMaterialSlotTopologyEntry[],\n read: (slot: MeshMaterialSlotTopologyEntry) => string | undefined,\n ): Map<string, number> => {\n const first = new Map<string, number>();\n const duplicates = new Set<string>();\n slots.forEach((slot, index) => {\n const key = read(slot)?.trim();\n if (!key) return;\n if (first.has(key)) duplicates.add(key);\n else first.set(key, index);\n });\n for (const duplicate of duplicates) first.delete(duplicate);\n return first;\n };\n\n const match = (read: (slot: MeshMaterialSlotTopologyEntry) => string | undefined): void => {\n const oldByKey = uniqueIndex(previous, read);\n const nextByKey = uniqueIndex(current, read);\n for (const [key, nextIndex] of nextByKey) {\n if (mapping[nextIndex] !== -1) continue;\n const oldIndex = oldByKey.get(key);\n if (oldIndex === undefined || usedPrevious.has(oldIndex)) continue;\n mapping[nextIndex] = oldIndex;\n usedPrevious.add(oldIndex);\n }\n };\n\n match((slot) => slot.sourceKey);\n match((slot) => slot.slotName);\n\n const unmatchedCurrent = mapping\n .map((oldIndex, index) => (oldIndex === -1 ? index : -1))\n .filter((index) => index !== -1);\n const unmatchedPrevious = previous\n .map((_, index) => (usedPrevious.has(index) ? -1 : index))\n .filter((index) => index !== -1);\n if (\n unmatchedCurrent.length === 1 &&\n unmatchedPrevious.length === 1 &&\n current[unmatchedCurrent[0] as number]?.sourceKey === undefined &&\n previous[unmatchedPrevious[0] as number]?.sourceKey === undefined\n ) {\n mapping[unmatchedCurrent[0] as number] = unmatchedPrevious[0] as number;\n usedPrevious.add(unmatchedPrevious[0] as number);\n unmatchedCurrent.length = 0;\n unmatchedPrevious.length = 0;\n }\n const identityInsufficient =\n unmatchedCurrent.some((index) => current[index]?.sourceKey === undefined) &&\n unmatchedPrevious.some((index) => previous[index]?.sourceKey === undefined);\n if (unmatchedCurrent.length > 0 && unmatchedPrevious.length > 0 && identityInsufficient) {\n return {\n ok: false,\n error: {\n code: 'mesh-material-slot-topology-change',\n previousIndices: unmatchedPrevious,\n nextIndices: unmatchedCurrent,\n hint: 'name source materials uniquely or provide stable sourceKey values before reimport',\n },\n };\n }\n\n for (let currentIndex = 0; currentIndex < current.length; currentIndex++) {\n let stableIndex = mapping[currentIndex] as number;\n if (stableIndex === -1) {\n stableIndex = stable.length;\n mapping[currentIndex] = stableIndex;\n }\n stable[stableIndex] = current[currentIndex] as MeshMaterialSlotTopologyEntry;\n }\n return { ok: true, slots: stable, currentToStableSlot: mapping };\n}\n\nexport interface MeshMaterialOverrideMigrationContext {\n readonly meshGuid: string;\n readonly sceneGuid: string;\n readonly entityId: number;\n}\n\nexport interface MeshMaterialOverrideConflict extends MeshMaterialOverrideMigrationContext {\n readonly code: 'mesh-material-slot-override-conflict';\n readonly materialSlot: number;\n readonly submeshIndices: readonly number[];\n readonly overrideHandles?: readonly number[];\n readonly overrideGuids?: readonly string[];\n readonly hint: string;\n}\n\nexport type MeshMaterialOverrideMigrationResult<T extends number | string = number> =\n | { readonly ok: true; readonly overrides: readonly T[] }\n | { readonly ok: false; readonly error: MeshMaterialOverrideConflict };\n\n/**\n * Collapse legacy per-submesh overrides into v3 per-slot overrides.\n * Conflicting section overrides fail atomically with full dependency context.\n */\nexport function migrateLegacyMeshMaterialOverrides<T extends number | string>(\n legacyOverrides: readonly T[],\n submeshes: readonly Pick<Submesh, 'materialSlot'>[],\n materialSlotCount: number,\n context: MeshMaterialOverrideMigrationContext,\n): MeshMaterialOverrideMigrationResult<T> {\n const inherited = (typeof legacyOverrides[0] === 'string' ? '' : 0) as T;\n const overrides = new Array<T>(materialSlotCount).fill(inherited);\n const sectionsBySlot = new Map<number, number[]>();\n for (let submeshIndex = 0; submeshIndex < submeshes.length; submeshIndex++) {\n const materialSlot = submeshes[submeshIndex]?.materialSlot;\n if (materialSlot === undefined || materialSlot < 0 || materialSlot >= materialSlotCount)\n continue;\n const sections = sectionsBySlot.get(materialSlot) ?? [];\n sections.push(submeshIndex);\n sectionsBySlot.set(materialSlot, sections);\n }\n for (const [materialSlot, submeshIndices] of sectionsBySlot) {\n const handles: T[] = submeshIndices.map((index) => legacyOverrides[index] ?? inherited);\n const distinct = [...new Set(handles)];\n if (distinct.length > 1) {\n return {\n ok: false,\n error: {\n code: 'mesh-material-slot-override-conflict',\n ...context,\n materialSlot,\n submeshIndices,\n ...(typeof handles[0] === 'string'\n ? { overrideGuids: handles as string[] }\n : { overrideHandles: handles as number[] }),\n hint: 'split the source slot or choose one override explicitly before v2 to v3 recook',\n },\n };\n }\n overrides[materialSlot] = distinct[0] ?? inherited;\n }\n return { ok: true, overrides };\n}\n\n/**\n * Submesh partitions a mesh's index/vertex range into an independent draw\n * call with its own primitive topology.\n *\n * Every field is required -- there is no default topology; the caller\n * must state the intended primitive type explicitly (charter P3 explicit\n * failure: silent default would mask topology mistakes).\n *\n * Naming aligns with Unity `SubMeshDescriptor` (without firstVertex /\n * baseVertex / bounds which are out of scope per OOS-3/OOS-4).\n *\n * | field | description |\n * |:--|:--|\n * | `indexOffset` | Start offset into the parent mesh's index buffer (in elements, not bytes). For vertex-only (non-indexed) submeshes, set to 0. |\n * | `indexCount` | Number of indices consumed from the index buffer starting at `indexOffset`. For vertex-only submeshes, set to 0. |\n * | `vertexCount` | Number of vertices spanned by this submesh range (used for the non-indexed draw path and for index-range OOB validation). |\n * | `topology` | GPU primitive topology for this submesh (one of the 5 WebGPU primitives: 'point-list' \\| 'line-list' \\| 'line-strip' \\| 'triangle-list' \\| 'triangle-strip'). |\n */\nexport interface Submesh {\n readonly indexOffset: number;\n readonly indexCount: number;\n readonly vertexCount: number;\n readonly topology: PrimitiveTopology;\n /** Index into the owning MeshAsset.materialSlots table. */\n readonly materialSlot: number;\n}\n\n/**\n * Texture asset POD shape aligned with `@webgpu/types ^0.1.69`\n * `GPUTextureDescriptor` subset (plan-strategy D-P1; RHI form rule\n * \"spec-aligned\"). Carries the decoded pixel bytes ready for\n * `GPUQueue.writeTexture` / `copyExternalImageToTexture` upload.\n *\n * `format` is the `GPUTextureFormat` string-literal union; `data` holds the\n * CPU-side decoded pixels (tight-packed, srgb-premultiplied-alpha-false per\n * D-P9). Optional `mipLevelCount` / `sampleCount` default to 1 at upload\n * time; v1 registers only 2D textures (depth / 3D / cube array are future\n * spinoffs).\n *\n * feat-20260515-learn-render-getting-started M3 / T-M3-03 minor-add (Asset\n * closed-union member count unchanged at 5; plan-strategy section 2.5 D Open\n * Q-4 selection (c)):\n * - `colorSpace: 'srgb' | 'linear'` -- AI-user-semantic SSOT (charter P4\n * consistent abstraction); `format='*-srgb'` family <-> `colorSpace='srgb'`\n * enforced by `AssetRegistry.uploadTexture` consistency assertion.\n * - `mipmap: boolean` -- `true` enables runtime mipmap-generator blit chain\n * (research F-1 SSOT three-source convergence; plan-strategy section 2.6\n * D Open Q-5 (a) independent file). `false` ships the single mip level\n * authored in `data`.\n *\n * Both fields are required (no default value) so consumers always make the\n * decision explicit at register-time (charter P3 explicit failure: silent\n * default would mask sRGB encode mistakes).\n */\nexport interface TextureAsset {\n readonly kind: 'texture';\n readonly width: number;\n readonly height: number;\n readonly format: GPUTextureFormat;\n readonly data: Uint8Array | Uint8ClampedArray;\n readonly colorSpace: 'srgb' | 'linear';\n readonly mipmap: boolean;\n readonly mipLevelCount?: number;\n readonly sampleCount?: number;\n}\n\n/**\n * Equirectangular environment-map asset POD shape -- a single 2D HDR image in\n * latitude-longitude projection (feat-20260630-equirect-kind-internalized-ibl-\n * declarative-skyligh M1, replacing the prior `CubeTextureAsset`).\n *\n * `kind:'equirect'` is the build-time-imported `.hdr` artefact: a single\n * `rgba16float` 2D image whose pixels live in `data` (tight-packed, build .bin).\n * Unlike the retired cube-texture, an equirect HAS a single 2D representation,\n * so it folds to a build-time `.bin` like `TextureAsset` (the cube-to-cube IBL\n * projection is a GPU-side pass driven internally by the render-system record\n * arm; AI users declare `Skylight{equirect}` rather than calling an upload).\n *\n * Fields mirror the `TextureAsset` 2D-image surface (charter P4 consistent\n * abstraction): `width` / `height` / `format` / `data` / `colorSpace`. No\n * `mipmap` chain field -- the IBL prefilter mip chain is a GPU-side pass, not a\n * CPU-authored level set.\n */\nexport interface EquirectAsset {\n readonly kind: 'equirect';\n readonly width: number;\n readonly height: number;\n readonly format: GPUTextureFormat;\n readonly data: Uint8Array | Uint8ClampedArray;\n readonly colorSpace: 'srgb' | 'linear';\n}\n\n/**\n * Sampler asset POD shape aligned with `@webgpu/types ^0.1.69`\n * `GPUSamplerDescriptor` subset. AI users supply filter + address modes;\n * the RHI layer materialises the GPU-side sampler on upload.\n */\nexport interface SamplerAsset {\n readonly kind: 'sampler';\n readonly magFilter?: GPUFilterMode;\n readonly minFilter?: GPUFilterMode;\n readonly mipmapFilter?: GPUMipmapFilterMode;\n readonly addressModeU?: GPUAddressMode;\n readonly addressModeV?: GPUAddressMode;\n readonly addressModeW?: GPUAddressMode;\n readonly lodMinClamp?: number;\n readonly lodMaxClamp?: number;\n readonly compare?: GPUCompareFunction;\n readonly maxAnisotropy?: number;\n}\n\n// feat-20260613 fix-issue-4: MATERIAL_PARAM_TYPES_V1 (9-Set) deleted —\n// MATERIAL_PARAM_TYPES (14-tuple, declared below) is the single SSOT.\n// §Change stance forbids v1/v2 dual-paths; the 9 v1 literals are a strict\n// subset of the 14-tuple, so all consumers (buildMaterialAssetValidator,\n// scanner.ts) migrate to the 14-tuple in one cut.\n\n// === MaterialParamType v2 (feat-20260613-material-paramschema-driven-binding M1 / w2) ===\n//\n// Decision anchors:\n// - plan-strategy D-7 paramSchema type set v2 (9 v1 + 5 new = 14 literals)\n// - research finding F-1 the union of all binding types used by the 5 built-in\n// shaders (standard-pbr / pbr-skin / unlit / sprite / shadow-caster) is exactly\n// these 14 entries; CSM (texture_depth_2d) / point shadow (texture_cube_array) /\n// IBL (texture_cube) / sampler_comparison / storage_buffer (skin palette) all\n// already exist downstream\n// - charter P3 explicit failure: closed unions guard exhaustive switching with\n// no default arm; TS verifies completeness\n//\n// Shape:\n// - `MATERIAL_PARAM_TYPES` : 14-element readonly tuple, the SSOT whitelist\n// - `MaterialParamType` : string-literal union derived from the tuple\n// - `NumericParamType` : 7 numeric literals (run-merged into one UBO entry)\n// - `TextureBindingParamType`: 6 literals — texture* + sampler*\n// (per D-4 each texture* auto-pairs a filtering sampler in derive output;\n// sampler / sampler_comparison are user-declared schema entries)\n// - `StorageBindingParamType`: storage_buffer (independent binding)\n// - `ParamSchemaEntry` : discriminated union over the three families\n// (Numeric / TextureBinding / StorageBinding) — exhaustive switching\n// on `entry.type` is closed across the 14 literals.\n\n/** 7 numeric WGSL types — std140-packed into one merged UBO entry (D-3). */\nexport type NumericParamType = 'f32' | 'i32' | 'u32' | 'vec2' | 'vec3' | 'vec4' | 'color';\n\n/** 6 texture-binding-family WGSL types: 4 texture views + 2 sampler kinds. */\nexport type TextureBindingParamType =\n | 'texture2d'\n | 'texture_cube'\n | 'texture_depth_2d'\n | 'texture_cube_array'\n | 'sampler'\n | 'sampler_comparison';\n\n/** 1 storage-binding type (e.g. skin palette buffer). */\nexport type StorageBindingParamType = 'storage_buffer';\n\n/**\n * Closed union of WGSL material-parameter type literals (14 members).\n * Every paramSchema entry's `type` field MUST be a member of this union.\n */\nexport type MaterialParamType =\n | NumericParamType\n | TextureBindingParamType\n | StorageBindingParamType;\n\n/**\n * v2 material parameter type whitelist — 14 ordered literal tuple (D-7).\n * Order is significant only as a stable enumeration source for tests\n * and discoverability; consumers should treat membership as a Set.\n */\nexport const MATERIAL_PARAM_TYPES = [\n 'f32',\n 'i32',\n 'u32',\n 'vec2',\n 'vec3',\n 'vec4',\n 'color',\n 'texture2d',\n 'texture_cube',\n 'texture_depth_2d',\n 'texture_cube_array',\n 'sampler',\n 'sampler_comparison',\n 'storage_buffer',\n] as const satisfies readonly MaterialParamType[];\n\n// Numeric-family schema entry (run-merged into a single UBO entry by derive).\n// `default` is optional; when present, values may omit the key.\n// - scalar numeric (f32 / i32 / u32) defaults to a single number\n// - vector + color types default to a length-N number tuple\nexport interface NumericParamSchemaEntry {\n readonly name: string;\n readonly type: NumericParamType;\n /** Asset-side color transfer function; runtime UBO values are always linear. */\n readonly colorSpace?: 'srgb' | 'linear';\n readonly default?: number | readonly number[];\n}\n\n// Texture-binding-family schema entry (texture* / sampler*).\n// `default` is kept optional for backward compatibility with existing\n// schema fixtures that carry stray defaults; derive ignores it for the\n// non-numeric families (D-4 auto-pair rule resolves samplers; textures\n// resolve via Handle<TextureAsset>).\nexport interface TextureBindingParamSchemaEntry {\n readonly name: string;\n readonly type: TextureBindingParamType;\n readonly default?: unknown;\n}\n\n// Storage-binding-family schema entry (storage_buffer).\n// Always an independent binding (not merged into the UBO run); `default`\n// is optional and ignored by derive (backward compatibility shim).\nexport interface StorageBindingParamSchemaEntry {\n readonly name: string;\n readonly type: StorageBindingParamType;\n readonly default?: unknown;\n}\n\n// Re-export derive(schema) and its output shapes alongside the schema\n// type union so all downstream consumers (runtime / vite-plugin-shader /\n// shader-compiler) reach the SSOT through a single import surface (D-2).\nexport type {\n DerivedMaterialInterface,\n DerivedNumericMember,\n DeriveOutput,\n ImmutableParamSchemaProjection,\n MaterialCoordinateRecordLayout,\n MaterialParameterProjection,\n MaterialParameterResourceProjection,\n MaterialParameterTextureProjection,\n MaterialResourceBindingLayout,\n MaterialResourceKind,\n ParamSchemaDeriveObservationKind,\n ParamSchemaDeriveObserver,\n ParamSchemaProjectionOwnerStats,\n UboFieldLayout,\n UboLayout,\n} from './derive-paramschema.js';\nexport {\n derive,\n deriveObserved,\n findUndeclaredSampledTextures,\n inferMaterialParameterKind,\n ParamSchemaProjectionOwner,\n} from './derive-paramschema.js';\n\n/**\n * Single material parameter schema entry — discriminated union over the\n * three families (Numeric / TextureBinding / StorageBinding).\n *\n * `name` is the parameter identifier matching a WGSL binding name.\n * `type` is the discriminator — exhaustive `switch (entry.type)` covers the\n * 14 literals without a `default` arm; TS guards completeness (charter P3).\n */\nexport type ParamSchemaEntry =\n | NumericParamSchemaEntry\n | TextureBindingParamSchemaEntry\n | StorageBindingParamSchemaEntry;\n\n// === RenderQueue namespace constants (feat-20260526-material-asset-multipass-renderstate M1 / w3) ===\n//\n// Decision anchors:\n// - requirements AC-04 (5 standard queue values: Background=1000 / Geometry=2000 /\n// AlphaTest=2450 / Transparent=3000 / Overlay=4000)\n// - plan-strategy D-3 (queue values replace three-bucket dispatch)\n// - research F-5 (Utopia queue model; Transparent=3000 gives gap between\n// AlphaTest=2450 and Transparent=3000 for user custom queues)\n// - charter P1 (progressive disclosure: RenderQueue.Geometry autocomplete\n// exposes the value; AI users never need to memorize bare numbers)\n\n/**\n * Standard render queue constants (AC-04). AI users access via IDE autocomplete\n * (`RenderQueue.`) — no bare numbers to memorize (charter P1 progressive disclosure).\n *\n * Queue order (ascending):\n * Background(1000) -> Geometry(2000) -> AlphaTest(2450) -> Transparent(3000) -> Overlay(4000)\n *\n * The gap between AlphaTest(2450) and Transparent(3000) allows user-inserted\n * custom queues without colliding with either boundary (research F-5).\n */\nexport const RenderQueue = {\n /** Skybox / backdrop draw, processed first. */\n Background: 1000,\n /** Opaque geometry draw — default queue for solid surfaces. */\n Geometry: 2000,\n /** Alpha-tested geometry (clip/discard in fragment shader) — drawn after opaque,\n * before transparent to avoid overdraw. */\n AlphaTest: 2450,\n /** Transparent / alpha-blended geometry — drawn back-to-front after opaque pass. */\n Transparent: 3000,\n /** Overlay / UI / debug lines — drawn last. */\n Overlay: 4000,\n} as const;\n\n/** Type alias for the 5-member RenderQueue value union (1000 | 2000 | 2450 | 3000 | 4000). */\nexport type RenderQueue = (typeof RenderQueue)[keyof typeof RenderQueue];\n\n// === MaterialRenderState POD interface (feat-20260526-material-asset-multipass-renderstate M1 / w1) ===\n//\n// Decision anchors:\n// - requirements AC-03 (all fields optional; engine applies known defaults)\n// - plan-strategy D-2 (MaterialRenderState fields optional + engine static defaults)\n// - research F-3 (current hardcoded values become the defaults)\n// - research F-6 (Three.js taxonomy proves this subset is sufficient for LO 4.x)\n// - charter P1 (all fields optional reduces boilerplate; JSDoc on each field exposes\n// the default so AI users discover via IDE hover without reading prose)\n\n/**\n * Stencil face state sub-interface — mirrors `@webgpu/types.GPUStencilFaceState`\n * field-by-field (spec-aligned per RHI form rules). All fields optional so\n * consumers declare only the stencil behavior they need.\n */\nexport interface StencilFaceState {\n /** Default: `'never'`. */\n readonly compare?: GPUCompareFunction;\n /** Default: `'keep'`. */\n readonly failOp?: GPUStencilOperation;\n /** Default: `'keep'`. */\n readonly depthFailOp?: GPUStencilOperation;\n /** Default: `'keep'`. */\n readonly passOp?: GPUStencilOperation;\n}\n\n/**\n * Material render-state POD interface — all fields optional (AC-03).\n *\n * When a field is `undefined`, the pipeline-builder falls back to the\n * engine default value noted in each field's JSDoc. AI users only override\n * the fields that differ from the defaults (charter P1 progressive disclosure).\n *\n * The defaults are:\n * - `cullMode='back'` (back-face culling per WebGPU convention)\n * - `depthCompare='less'` (standard depth testing)\n * - `depthWriteEnabled=true` (write depth for opaque surfaces)\n * - `blend` undefined (no blending — opaque pass)\n * - `stencil` undefined (no stencil operations)\n * - `stencilReadMask` undefined (WebGPU default 0xFFFFFFFF)\n * - `stencilWriteMask` undefined (WebGPU default 0xFFFFFFFF)\n * - `frontFace` undefined (default 'ccw')\n */\nexport interface MaterialRenderState {\n /** Face culling mode. Default: `'back'` (cull back faces). */\n readonly cullMode?: 'none' | 'front' | 'back';\n /** Depth comparison function. Default: `'less'`. */\n readonly depthCompare?: GPUCompareFunction;\n /** Whether depth writes are enabled. Default: `true`. */\n readonly depthWriteEnabled?: boolean;\n /** Blend state descriptor (spec-aligned with `GPUBlendState`). Default: undefined (no blending). */\n readonly blend?: GPUBlendState;\n /** Enable alpha-to-coverage when the active camera uses MSAA. Default: `false`. */\n readonly alphaToCoverageEnabled?: boolean;\n /** Stencil face state. Default: undefined (no stencil operations). */\n readonly stencil?: StencilFaceState;\n /**\n * Stencil read mask (mirrors GPUDepthStencilState.stencilReadMask top-level).\n * Default: undefined (WebGPU default 0xFFFFFFFF).\n */\n readonly stencilReadMask?: number;\n /**\n * Stencil write mask (mirrors GPUDepthStencilState.stencilWriteMask top-level).\n * Default: undefined (WebGPU default 0xFFFFFFFF).\n */\n readonly stencilWriteMask?: number;\n /**\n * Front-face winding (mirrors GPUPrimitiveState.frontFace).\n * Default: `'ccw'`.\n */\n readonly frontFace?: 'ccw' | 'cw';\n}\n\n// === PassKind as open string + KNOWN_PASS_KINDS (feat-20260615-pipeline-spec-ssot D-10) ===\n//\n// Decision anchors:\n// - plan-strategy D-10 (PassKind opened from closed union to string; KNOWN_PASS_KINDS\n// is a discoverable documentation constant)\n// - requirements AC-09 (PassKind: open string; unknown passKind -> PipelineSpecError\n// code='unknown-pass-kind')\n// - charter P3 (fail-fast on unknown passKind via PipelineSpecError, not silent route)\n\n/**\n * Render-pass kind -- open string, no longer a closed union.\n *\n * `KNOWN_PASS_KINDS` (below) documents the engine-shipped pass kinds; user-defined\n * pass kinds are supported through {@link ShaderRegistry} registration. An unknown\n * pass kind triggers {@link PipelineSpecError} with code `'unknown-pass-kind'`\n * at pipeline-spec build time (charter P3 explicit failure).\n *\n * @see {@link KNOWN_PASS_KINDS} for the discoverable pass-kind catalogue\n * @see plan-strategy D-10 (PassKind opened from closed union)\n */\nexport type PassKind = string;\n\n/**\n * Engine-shipped pass kinds -- discoverable constant catalogue (D-10).\n *\n * Consumers iterate or lookup against this set to validate pass kinds before\n * submitting to `getOrBuildPipeline`. An unknown pass kind still triggers a\n * structured `PipelineSpecError` with code `'unknown-pass-kind'` carrying\n * `.detail.expected = KNOWN_PASS_KINDS` and `.detail.actual`.\n */\nexport const KNOWN_PASS_KINDS: readonly string[] = [\n 'forward',\n 'deferred',\n 'lighting',\n 'shadow-caster',\n 'point-shadow-caster',\n 'post-process',\n 'skybox',\n] as const;\n\n// === PassSelector type (feat-20260526-material-asset-multipass-renderstate M1 / w4) ===\n//\n// Decision anchors:\n// - requirements AC-05 (tags + PassSelector matching: all selector keys must\n// exist in pass tags with value in the selector's value list)\n// - plan-strategy D-4 (Tags free Record + PassSelector Record<string, string[]>;\n// enum-based categories rejected — adding a pipeline stage should not require\n// editing the types package)\n// - charter P1 (type signature itself is the match-rule documentation;\n// `Record<string, string[]>` is self-describing)\n\n/**\n * Pass selector — maps tag keys to allowed value lists (AC-05).\n *\n * A pass matches the selector when **every** key in the selector exists in the\n * pass's `tags` and the pass's tag value is in the selector's value list for that key.\n * An empty selector matches every pass (no key constraints).\n *\n * The type signature itself is the API docs (charter P1): each entry maps a\n * tag key (string) to its allowed values (string array).\n *\n * @example Match passes tagged with `RenderType: 'Opaque'` or `RenderType: 'Transparent'`\n * ```ts\n * const selector: PassSelector = { RenderType: ['Opaque', 'Transparent'] };\n * ```\n */\nexport type PassSelector = Record<string, readonly string[]>;\n\n// === FontAsset POD shape (feat-20260531-world-space-msdf-text-rendering M2 / w5) ===\n//\n// Decision anchors:\n// - plan-strategy D-6 (FontAsset data shape: atlas Handle<TextureAsset> +\n// sampler Handle<SamplerAsset> + glyphs Record<codepoint, GlyphMetric> +\n// common block + optional notdef fallback; POD, math-free, fields 1:1\n// mirror toolchain wiki §4 BMFont char mapping)\n// - requirements AC-04 (FontAsset enters Asset closed union, 11->12)\n// - AGENTS.md §Component naming (single-semantic components drop the\n// Component suffix; FontAsset is a data asset, not an ECS component)\n//\n// GlyphMetric fields mirror the BMFont char block layout (toolchain wiki §4):\n// advance <- xadvance (horizontal distance to next glyph)\n// bearingX <- xoffset (horizontal offset from cursor)\n// bearingY <- yoffset (vertical offset from baseline)\n// size.{w,h} <- width/height (glyph quad size in layout space, before atlas\n// scale)\n// region.{x,y,w,h} <- atlas UV region in pixels (x/y = top-left corner\n// relative to atlas origin)\n\n/**\n * Per-glyph metric layout — 1:1 mirror of BMFont char block fields\n * (toolchain wiki §4). POD, math-free.\n */\nexport interface GlyphMetric {\n /** Horizontal distance to the next glyph (xadvance). */\n readonly advance: number;\n /** Horizontal offset from cursor (xoffset). */\n readonly bearingX: number;\n /** Vertical offset from baseline (yoffset). */\n readonly bearingY: number;\n /** Glyph quad size in layout space. */\n readonly size: { readonly w: number; readonly h: number };\n /** Atlas UV region in pixels (top-left origin). */\n readonly region: {\n readonly x: number;\n readonly y: number;\n readonly w: number;\n readonly h: number;\n };\n}\n\n/**\n * Font asset POD — atlas texture handle + sampler handle + per-codepoint\n * glyph metrics + common layout block.\n *\n * AI users obtain a FontAsset via `assets.loadByGuid<FontAsset>(guid)` and\n * hand the resulting `Handle<FontAsset>` to `GlyphText.fontHandle`. The atlas\n * texture and sampler are resolved through the handle chain by the glyph\n * layout system; the per-glyph metrics drive the quad-position-and-UV baking\n * (plan-strategy D-6).\n *\n * | Field | Purpose |\n * |:--|:--|\n * | `atlas` | Handle to the baked MSDF atlas `TextureAsset` |\n * | `sampler` | Handle to the `SamplerAsset` for atlas sampling |\n * | `glyphs` | `Record<codepoint, GlyphMetric>` — O(1) codepoint lookup |\n * | `common` | Common layout block (lineHeight / base / distanceRange / pxRange / atlas width/height) |\n * | `notdef` | Optional fallback glyph metric for missing codepoints (TOFU, AC-14) |\n */\nexport interface FontAsset {\n readonly kind: 'font';\n /**\n * Atlas texture GUID (D-19). Payload-internal sub-asset ref stored as an\n * AssetGuid, not a handle — the loadByGuid recursion mints no handle; the\n * World-holding consumer (glyph-text-layout) resolves it once at read time.\n */\n readonly atlas: AssetGuid;\n /** Sampler GUID (D-19). Same GUID-identity contract as `atlas`. */\n readonly sampler: AssetGuid;\n readonly glyphs: Record<number, GlyphMetric>;\n readonly common: {\n readonly lineHeight: number;\n readonly base: number;\n readonly distanceRange: number;\n readonly pxRange: number;\n readonly atlasWidth: number;\n readonly atlasHeight: number;\n };\n readonly notdef?: GlyphMetric;\n}\n\n// === RenderPipelineAsset POD shape =============================================\n//\n// feat-20260601-customizable-render-pipeline-seam-and-dogfood-rend M1 / w5.\n// Asset-layer descriptor that binds a registered render-pipeline logic id to an\n// installable Handle (the MaterialAsset { materialShaderId, params } pattern, scaled to\n// the pipeline layer). `installPipeline(handle)` resolves this POD off the AssetRegistry,\n// looks up `pipelineId` in the pipeline registry, and swaps the per-frame graph.\n//\n// M2 stage (w10): `config.passCount` is the FIRST real config key. The standard pipeline\n// runs with config undefined (default frame byte-identical, AC-01); a custom pipeline can\n// size its declared pass chain from `config.passCount` so its topology varies observably\n// via `renderer.perFramePassNames` (AC-03 / plan-strategy D-C).\n//\n// feat-20260608-cluster-lighting M2 / w8: `config.clusterGrid` is the HDRP cluster grid\n// dimensions config (default {x:16, y:9, z:24}). `pipelineId` is narrowed to a literal\n// union of `'forgeax::urp' | 'forgeax::hdrp' | (string & {})` so TS narrowing on\n// `pipelineId === 'forgeax::hdrp'` narrows `config.clusterGrid`.\n//\n// feat-20260612-hdrp-ssao M4 / w19: `config.ssao` is the SSAO configuration\n// (enabled+radius+bias+intensity). Same shared-config pattern as clusterGrid;\n// HDRP consumes it, URP ignores it at runtime.\n/**\n * Asset-layer descriptor for an installable render pipeline.\n *\n * `pipelineId` references a logic registered via `renderer.registerPipeline(id, impl)`\n * (engine builtins use the `forgeax::` prefix, e.g. `'forgeax::urp'`; user pipelines use\n * `<package>::<id>`). `config` is the per-install tuning the pipeline logic reads at\n * `buildGraph` time; `passCount` is the first real key (a custom pipeline declares that\n * many passes). The URP ignores `config` (its topology is fixed).\n *\n * `config.clusterGrid` is the HDRP cluster grid dimensions (x/y/z each an integer in\n * [1, 64]; default {16, 9, 24}). It is ignored by URP.\n *\n * `config.ssao` enables SSAO (Screen-Space Ambient Occlusion) for the HDRP\n * deferred path. Ignored by URP.\n */\nexport interface RenderPipelineAsset {\n readonly kind: 'render-pipeline';\n readonly pipelineId: 'forgeax::urp' | 'forgeax::hdrp' | (string & {});\n readonly config?: {\n readonly passCount?: number;\n readonly clusterGrid?: { readonly x: number; readonly y: number; readonly z: number };\n readonly ssao?: {\n readonly enabled: boolean;\n readonly radius?: number | undefined;\n readonly bias?: number | undefined;\n readonly intensity?: number | undefined;\n };\n /**\n * feat-20260621 M4': ordered registered post-process shader ids the built-in\n * pipelines composite over the FINAL swap-chain image, after the fxaa pass\n * and before the debug overlay. Each id must be registered via\n * `renderer.postProcess.register(id, entry)` first. The effects run in array\n * order; each samples the current swap-chain (copy) and writes it back, so a\n * chain composes left-to-right. This is the AUGMENT path: the built-in 9-pass\n * chain (shadow cascades, tonemap, bloom, fxaa) renders unchanged and the\n * effects layer on top — unlike installing a wholly custom pipeline, which\n * REPLACES the built-in graph (and would drop its shadow passes). `undefined`\n * or `[]` adds zero passes (default frame byte-identical).\n *\n * WebGPU backend only: each effect reads the mid-frame swap-chain (copy +\n * non-srgb storage-view write), which the WebGL2 fallback swap-chain does not\n * support (no COPY_SRC, no non-srgb reinterpret view) — same constraint the\n * built-in FXAA pass already carries. On a non-WebGPU device leave this empty.\n */\n readonly postEffects?: readonly string[];\n };\n}\n\n/**\n * Asset discriminated union - 13 variants keyed on `.kind`.\n *\n * Variant history:\n * - feat-20260513-instanced-mesh M1 introduced a 5th `'instanced-buffer-asset'`\n * variant carrying packed mat4 transforms + a `version` dirty flag.\n * - feat-20260514-ecs-children-instances-managed-buffer-array M3 (w15)\n * retired that variant: per-entity instanced transforms are now stored\n * directly inside the ECS via the `Instances { transforms: 'array<f32>' }`\n * component (managed by the BufferPool slot column + sidecar count\n * column). Asset closed-union shrinks 5 -> 4 (evolution major rename\n * per AGENTS.md `Change stance`); existing exhaustive `switch\n * (asset.kind)` consumers drop the now-unreachable arm in the same PR.\n * - feat-20260514-scene-as-world-blueprint w3 added `'scene'` variant\n * (4 -> 5, minor add per AGENTS.md `Evolution contract`); declarative\n * SceneEntity list, no overrides at the asset layer.\n * - feat-20260531-world-space-msdf-text-rendering w5 added `'font'` variant\n * (11 -> 12, minor add per AGENTS.md `Evolution contract`);\n * FontAsset with atlas handle + sampler handle + glyph metrics.\n * - feat-20260601-customizable-render-pipeline-seam-and-dogfood-rend w5 added\n * `'render-pipeline'` variant (12 -> 13, minor add per AGENTS.md\n * `Evolution contract`); RenderPipelineAsset with pipelineId + config.\n * - feat-20260623-world-space-video-asset M1 added `'video'` variant\n * (14 -> 15, minor add per AGENTS.md `Evolution contract`);\n * VideoAsset with `{ url }` descriptor, no width/height/duration.\n *\n * Exhaustive `switch (asset.kind)` type-guards against future additions\n * without default fallback (charter proposition 4 + proposition 3).\n *\n * | kind | variant |\n * |:--|:--|\n * | `'mesh'` | `MeshAsset` |\n * | `'texture'` | `TextureAsset` |\n * | `'equirect'` | `EquirectAsset` (single 2D HDR lat-long env map for IBL) |\n * | `'sampler'` | `SamplerAsset` |\n * | `'material'` | `MaterialAsset` (further narrows on `.passes`) |\n * | `'scene'` | `SceneAsset` (declarative SceneEntity list, no overrides) |\n * | `'font'` | `FontAsset` (MSDF atlas handle + glyph metrics) |\n * | `'render-pipeline'` | `RenderPipelineAsset` (installable pipeline logic id + config) |\n * | `'video'` | `VideoAsset` (runtime-only `{ url }` descriptor, no width/height/duration) |\n * | `'animation-graph'` | `AnimationGraph` (Clip/Blend/Add DAG + per-node static weights) |\n */\nexport type Asset =\n | MeshAsset\n | TextureAsset\n | EquirectAsset\n | SamplerAsset\n | MaterialAsset\n | SceneAsset\n | SkeletonAsset\n | SkinAsset\n | AnimationClip\n // === 1 new variant (feat-20260713-animation-state-machine-plugin M2 / w13) ===\n // AnimationGraph POD (Clip/Blend/Add DAG); GUID-addressable, serializable\n // (AC-14 foundation). Closed node union (clip/blend/add), no reserved FSM/Mask\n // fields (OOS-1..4).\n | AnimationGraph\n | AudioClipAsset\n | FontAsset\n | RenderPipelineAsset\n // === 1 new variant (feat-20260608-tilemap-object-layer-rendering M0 baseline rebuild) ===\n // Direct atlases[] form (plan-strategy D-7 one-cut); no intermediate single-`atlas` shape.\n | TilesetAsset\n // === 1 new variant (feat-20260623-world-space-video-asset M1) ===\n // runtime-only { url } descriptor; no width/height/duration in payload.\n | VideoAsset\n | ParticleEffectAsset;\n\n// === Tileset asset POD shape (feat-20260608 M0 baseline rebuild) =================\n//\n// Decision anchors:\n// - requirements §AC-01/03/04/05 (TilesetAsset 9 fields; atlases plural composite;\n// M0 TilesetTileEntry single-field shape with M1 adding 5 optional + collider).\n// - plan-strategy §D-5 (M0 baseline rebuild after main reverted feat-20260604).\n// - plan-strategy §D-7 (`atlases: readonly Handle<TextureAsset,managed>[]` one-cut\n// rename; no `atlas` single-form alias or dual-path).\n// - plan-strategy §D-6 (AssetErrorCode count restoration -- M0 reintroduces\n// `tileset-region-index-out-of-range`; M1 adds `tileset-tile-entry-malformed`).\n// - charter F1 (AI users discover the schema via IDE autocomplete on the closed\n// `Asset` union + `Handle<TilesetAsset>` returns from `AssetRegistry.register`).\n// - charter P4 (atlases plural composite mirrors `MaterialAsset.passes[]` shape).\n\n/**\n * Closed `TilesetTileCollider` union -- per-tile collider schema (M1\n * extension; feat-20260608-tilemap-object-layer-rendering M1 / m1-t2).\n *\n * Three discriminant variants (closed enum, charter P3):\n *\n * - `{ type: 'none' }` -- no collider for this tile.\n * - `{ type: 'rect', rect: readonly [x, y, w, h] }` -- axis-aligned\n * rectangle in normalized cell coordinates `[0, 1]^2`. `w > 0`, `h > 0`,\n * `x + w <= 1`, `y + h <= 1` are enforced by `validateTilesetPayload`\n * (R-6 first-error path).\n * - `{ type: 'polygon', points: readonly [x, y][] }` -- convex/concave\n * polygon in normalized cell coordinates. `points.length >= 3` and\n * each point lies in `[0, 1]^2`.\n *\n * The engine validates this schema at register-time but does NOT consume\n * it (plan-strategy §D-4 -- schema landed, consumer deferred to a future\n * `feat-tilemap-physics-bridge` closed loop). AI users with a physics\n * sidecar consume the schema directly via `tileset.tiles[i].collider`\n * after `assets.register<TilesetAsset>(...)` resolves the handle.\n *\n * Exhaustive switch:\n * ```ts\n * switch (collider.type) {\n * case 'none': return null;\n * case 'rect': return collider.rect;\n * case 'polygon': return collider.points;\n * // No default branch -- TS guards completeness (charter P3).\n * }\n * ```\n */\nexport type TilesetTileCollider =\n | { readonly type: 'none' }\n | { readonly type: 'rect'; readonly rect: readonly [number, number, number, number] }\n | { readonly type: 'polygon'; readonly points: readonly (readonly [number, number])[] };\n\n/**\n * Rectangular sub-region within a tileset atlas (M1 schema extension on\n * top of M0 baseline rebuild).\n *\n * Four required fields define the atlas-space rectangle in pixels:\n * `x` / `y` top-left corner; `width` / `height` extent. `width + x` and\n * `height + y` MUST stay within the parent atlas extent or\n * `validateTilesetPayload` returns\n * `AssetError { code: 'tileset-region-index-out-of-range' }`\n * (charter P3 explicit failure at register-time).\n *\n * Optional `atlasIndex?: number` (M1; default 0) routes the region into\n * `TilesetAsset.atlases[atlasIndex]` for multi-atlas tilesets. Out-of-range\n * `atlasIndex` (`>= atlases.length` or negative) surfaces\n * `AssetError { code: 'tileset-tile-entry-malformed', detail: { field: 'atlasIndex', scope: 'tileset-asset' } }`\n * at register-time (plan-strategy §D-7 three-hop routing; R-6 first-error\n * order places atlasIndex check between region rect bounds and per-tile\n * entry field checks).\n */\nexport interface TilesetRegion {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n readonly atlasIndex?: number;\n}\n\n/**\n * Per-tile entry in `TilesetAsset.tiles[]` (M1 schema extension on top of\n * M0 baseline rebuild).\n *\n * Required `regionIndex` points into the parent `TilesetAsset.regions[]`\n * array. M1 adds five optional fields for the variable-size + custom-pivot\n * object-layer story (plan-strategy §M1):\n *\n * - `widthCells?: number` -- multi-cell width in `Tilemap.cols/rows`\n * coordinate units (default 1; range `(0, 64]`). Anchored at the\n * cell that hosts the non-zero tileId entry; cells inside the\n * `widthCells x heightCells` footprint must stay 0 in `TileLayer.tiles[]`\n * (anchor convention, not enforced at register time).\n * - `heightCells?: number` -- multi-cell height (default 1; range `(0, 64]`).\n * - `pivotX?: number` -- normalized horizontal pivot in `[0, 1]` (default 0.5).\n * The pivot is the world-space anchor: `pivot_world_X = (cellX + pivotX) * tileSizeX`.\n * Quad center is offset by `(0.5 - pivotX) * widthCells * tileSizeX`\n * (plan-strategy §D-2 first-line geometry; M2 implementation).\n * - `pivotY?: number` -- normalized vertical pivot in `[0, 1]` (default 0.5).\n * For asi_world `.tsj` extension: `pivotY = 1.0` means quad bottom\n * anchors the cell, `pivotY = 0.0` means quad top (per-asset convention,\n * not Tiled native). The engine uses `effectivePivotY` for Y-sort.\n * - `collider?: TilesetTileCollider` -- 3-variant closed union schema\n * (charter P3). Engine validates the schema at register-time but does\n * NOT consume it (plan-strategy §D-4).\n *\n * All five fields are optional so unit-cell call sites `{ regionIndex: N }`\n * remain backward compatible (charter F1).\n *\n * Out-of-range `regionIndex` (>= regions.length, or negative) surfaces\n * `AssetError { code: 'tileset-region-index-out-of-range' }`. Out-of-range\n * `widthCells / heightCells / pivotX / pivotY / collider` surface\n * `AssetError { code: 'tileset-tile-entry-malformed', detail: { field, scope: 'tile-entry', tileEntryIndex } }`\n * (plan-strategy §D-6 closed 7-variant `.detail.field` enum).\n */\nexport interface TilesetTileEntry {\n readonly regionIndex: number;\n readonly widthCells?: number;\n readonly heightCells?: number;\n readonly pivotX?: number;\n readonly pivotY?: number;\n readonly collider?: TilesetTileCollider;\n}\n\n/**\n * Tileset asset (M0 baseline rebuild on origin/main).\n *\n * Nine fields:\n * - `kind` -- discriminator literal `'tileset'`.\n * - `atlases` -- one or more durable GUIDs for atlas textures.\n * `atlases.length >= 1` enforced at register time.\n * M0 reads `atlases[0]` exclusively (single-atlas form);\n * M1 adds `regions[].atlasIndex` for multi-atlas routing.\n * - `tileWidth`/`tileHeight` -- per-cell pixel size (atlas grid stride).\n * - `columns`/`rows` -- atlas grid layout (informational metadata; used\n * as fallback when `atlasSizes` is absent to infer\n * atlas pixel extent as `columns * tileWidth`).\n * - `atlasSizes` -- optional per-atlas pixel dimensions. When present,\n * `atlasSizes[i]` gives the exact pixel size of\n * `atlases[i]` and overrides `columns`/`rows` for UV\n * normalisation in the chunk-extract system. Required when\n * the tileset contains multiple atlases with different\n * pixel sizes (e.g. a terrain + object composite tileset).\n * Each entry carries `{ pixelWidth, pixelHeight }`.\n * - `regions` -- array of atlas sub-rectangles (TilesetRegion).\n * - `tiles` -- per-tile entries (TilesetTileEntry), 1-indexed via tile id\n * sentinel where 0 means \"empty\" in `TileLayer.tiles`.\n *\n * Plural composite `atlases` (not single `atlas`) is the one-cut breaking\n * rename versus the old feat-20260604 form (plan-strategy §D-7 + AGENTS.md\n * §Change stance \"optimal > compatible\"); no deprecation alias survives.\n *\n * @example Register a tileset and spawn a Tilemap + TileLayer pair:\n * ```ts\n * const atlasGuid = 'world/object_atlas';\n * const tileset = registry.register<TilesetAsset>({\n * kind: 'tileset',\n * atlases: [atlasGuid],\n * tileWidth: 16,\n * tileHeight: 16,\n * columns: 8,\n * rows: 8,\n * regions: [{ x: 0, y: 0, width: 16, height: 16 }],\n * tiles: [{ regionIndex: 0 }],\n * }).unwrap();\n * ```\n */\nexport interface TilesetAtlasSize {\n readonly pixelWidth: number;\n readonly pixelHeight: number;\n}\n\nexport interface TilesetAsset {\n readonly kind: 'tileset';\n readonly atlases: readonly string[];\n readonly tileWidth: number;\n readonly tileHeight: number;\n readonly columns: number;\n readonly rows: number;\n /** Per-atlas pixel dimensions. `atlasSizes[i]` overrides `columns`/`rows`\n * for UV normalisation of regions whose `atlasIndex === i`. Required when\n * atlases have different pixel sizes. */\n readonly atlasSizes?: readonly TilesetAtlasSize[];\n readonly regions: readonly TilesetRegion[];\n readonly tiles: readonly TilesetTileEntry[];\n}\n\n// === AssetRef + AssetEnvelope (feat-20260622-asset-ref-graph-protocol-unification-refs-as-ssot M1 / w1) ===\n//\n// Decision anchors:\n// - plan-strategy D-1: single envelope type AssetEnvelope = { guid, kind, name?,\n// payload, refs } — ImportedAsset is upgraded to this shape; assetCatalog\n// value type changes from Map<string, Asset> to Map<string, AssetEnvelope>.\n// - plan-strategy D-2: scene refs are importer flat superset (mesh U material U\n// texture U skeleton U skin); texture edges have sourceField=undefined (no\n// per-entity origin).\n// - plan-strategy D-3: sourceField is structured triple { componentName,\n// fieldName, arrayIndex? } — consumers read via property access, not string\n// parse (charter P3).\n// - plan-strategy D-10: edge metadata (AssetRef) does NOT sink into Loader.load\n// refs param — loader still receives GUID string projection.\n// - plan-strategy OOS-1: Asset closed union unchanged; AssetEnvelope wraps it.\n// - plan-strategy OOS-4: AssetErrorCode member set unchanged (21 members).\n// - charter F1: single-entry indexability — refs field name cross-layer\n// consistent (ImportedAsset.refs / AssetEnvelope.refs / pack-index refs).\n\n/**\n * Structured edge metadata carried in an asset envelope's `refs[]`. Each entry\n * records a GUID-level reference plus optional provenance: which scene entity\n * field originated the reference (``sourceField``) and the entity's local id\n * (``sceneEntityId``). Texture edges and other transitive references have no\n * per-entity origin — ``sourceField`` is ``undefined`` for those (D-2).\n *\n * AI users consume ``sourceField`` via property access (``ref.sourceField?.componentName``\n * / ``ref.sourceField?.fieldName`` / ``ref.sourceField?.arrayIndex``), never by\n * parsing a concatenated string (charter P3).\n */\nexport interface AssetRef {\n readonly guid: string;\n readonly sourceField?: {\n readonly componentName?: string;\n readonly fieldName: string;\n readonly arrayIndex?: number;\n };\n readonly sceneEntityId?: number;\n}\n\n/**\n * Self-contained asset envelope — the single shape through which assets flow\n * from import to catalog to recursive load (plan-strategy D-1).\n *\n * ``payload`` carries the closed ``Asset`` union member (mesh / texture / scene /\n * material / etc.). ``refs`` is the authoritative reference graph — every GUID\n * this asset transitively depends on, with optional edge metadata\n * (``sourceField`` / ``sceneEntityId``). ``name`` is the per-asset display name\n * (may be undefined; ``resolveName`` derives the final name via a three-argument\n * XOR that also considers the package path).\n */\nexport interface AssetEnvelope<P = Asset> {\n readonly guid: string;\n readonly kind: string;\n // Per-GUID stored display name -- the `storedName` argument resolveName feeds\n // to deriveAssetName (the single home for the explicit name, replacing the\n // retired storedNameOf side table). `undefined` = no explicit name (resolveName\n // then applies the multi-asset basename fallback / no-package '' branch).\n readonly name?: string;\n readonly payload: P;\n readonly refs: readonly AssetRef[];\n}\n\n// === Package interface (feat-20260618-asset-and-pack-name-fields M1 / w2) ======\n//\n// Decision anchors:\n// - plan-strategy D-7 (Package interface in @forgeax/engine-types, same layer\n// as Asset union, for multi-package consumer discoverability per charter F1)\n// - architecture-principles #2 (Derive, Don't Duplicate): assetCount is\n// derived from assetGuids.size, never stored independently\n// - plan-strategy D-5 (builtin assets -> null Package, not a synthetic path)\n// - Package does not carry a `name` field — resolved names flow through\n// resolveName (D-6), not stored on Package\n//\n// AI users discover Package via IDE autocomplete on @forgeax/engine-types;\n// the runtime AssetRegistry.packageOf Map carries Package | null per guid.\n\n/**\n * Runtime view of one import-source package -- the grouping unit for\n * the two-segment asset identity (`<packagePath>.<name>`).\n *\n * `path` is the import file path (e.g. `'assets/hero.glb'`). Multiple\n * assets imported from the same source file share one `Package`.\n *\n * `assetGuids` lists every GUID that belongs to this package. The\n * runtime keeps it in sync with `registerPackage` insertions.\n *\n * `assetCount` is a derived view (`assetGuids.size`); it is **not**\n * stored as a standalone field (Derive axiom #2).\n */\nexport interface Package {\n readonly path: string;\n readonly assetGuids: ReadonlySet<string>;\n readonly assetCount: number;\n}\n\n// === Scene asset POD shape (feat-20260514-scene-as-world-blueprint w2) ==========\n//\n// Decision anchors:\n// - requirements §AC-01 (AssetUnion 6 elements; SceneAsset top level only\n// `kind` + `entities`, no overrides field at the asset layer)\n// - requirements §AC-02 (LocalEntityId branded number; cross-brand assignment\n// to / from Entity is a TS compile-time error)\n// - charter proposition 1 (single-entry IDE autocomplete from\n// `@forgeax/engine-types`) + proposition 4 (explicit failure: brand\n// phantom rejects untagged number) + proposition 5 (consistent\n// abstraction, structurally parallel to Handle<T> brand)\n//\n// The unique-symbol brand stays private to this module so the brand\n// identity is anchored exactly here; consumers refer to LocalEntityId\n// as opaque number subtypes.\n\ndeclare const LocalEntityIdBrand: unique symbol;\n\n/**\n * Scene-local entity index brand (u32).\n *\n * Authored as `0..entities.length-1` inside a SceneAsset; runtime storage stays\n * a plain JS number (the phantom `[LocalEntityIdBrand]` is erased at runtime\n * but rejects cross-brand assignment with `Entity` at\n * the TS layer).\n *\n * AI users obtain LocalEntityId values from `SceneEntity.localId` accessors and\n * hand them back to `SceneEntity` manipulation methods; plain\n * `number` is not assignable to `LocalEntityId` by design — see\n * `packages/types/src/__tests__/scene-brand.test-d.ts` for the negative\n * assertions.\n */\nexport type LocalEntityId = number & { readonly [LocalEntityIdBrand]: void };\n\n/**\n * Open map shape from component name to the per-component value record\n * authored on a `SceneEntity` (feat-20260514 w2).\n *\n * The map is keyed by component-token name (`'Transform' | 'MeshFilter' |\n * 'ChildOf' | ...`) and each per-component record is a free-form\n * `Record<string, unknown>` POD shape; the precise field types live in the\n * ecs `defineComponent(...)` schema (one layer up). This package stays\n * math-free + ecs-free; the layered alignment with the ecs schema vocab is\n * documented in plan-strategy §3.1 types_pkg sub-graph and tested by w3 /\n * w22 at the ecs / runtime layer.\n *\n * Open shape is intentional: components evolve via add-only minor in their\n * own packages; locking this map to a closed union here would force an edit\n * in @forgeax/engine-types every time a new component appears (charter\n * proposition 5 consistent abstraction — registration discipline owned by\n * each component's defineComponent site).\n */\nexport type ComponentValuesMap = {\n readonly [componentName: string]: Readonly<Record<string, unknown>>;\n};\n\n/**\n * Single SceneEntity POD shape (feat-20260514 w2).\n *\n * Carries one `localId` (LocalEntityId brand) plus a partial map of explicit\n * component field values. The partial keying lets layer 1 (explicit) leave\n * any component absent so layer 2 (component-level defaults) and layer 3\n * (TS type defaults) can fill in the residual fields at instantiate time\n * (plan-strategy §default-values 4-layer fallback table; AC-07 / AC-11 /\n * AC-12 sites).\n */\nexport interface SceneEntity {\n readonly localId: LocalEntityId;\n readonly components: Partial<ComponentValuesMap>;\n}\n\n/**\n * One per-member mount-time modification (add-or-patch) applied to a\n * mounted scene member (feat-20260608-scene-nesting-ecs-fication M1 / w7;\n * feat-20260713-mount-override-component-add-and-shared-ref-round M1 / w2;\n * AC-01, AC-19).\n *\n * `localId` selects a member entity inside the mounted SceneAsset,\n * counted against the mount's `memberFirst` window. `comp` names the\n * target component. `field` is a component-granular discriminant:\n * - present -> PATCH one field: `value` is that single field's value,\n * written over the member's existing component at instantiate time;\n * - absent -> ADD/UPSERT the whole component: `value` is the per-field\n * value map for `comp`, merged onto (or creating) the member's\n * component.\n * `value` stays `unknown` because the per-component schema vocab lives one\n * layer up — runtime fail-fast via 'scene-override-type-mismatch' catches\n * type drift (plan-strategy D-9 / D-1). The discriminant is carried by the\n * shape itself (field present / absent), never a separate `op` tag, so no\n * consumer branches on a `switch (op)` (requirements: consumers do not\n * encode variant knowledge).\n *\n * Boundary notes: add is upsert (an existing component is merged, not\n * rejected); a NULL-sentinel entity value in the payload follows the same\n * `entity` remap rules as SceneEntity.components; overrides apply in\n * `mounts[].overrides[]` array order after the member's own authored\n * components.\n *\n * Charter mapping: proposition 1 (single-entry import surface, three-tier\n * progressive disclosure) + proposition 3 (machine-readable union,\n * MountOverride is a closed POD shape) + proposition 4 (explicit failure:\n * runtime apply path returns Result with structured error code).\n */\nexport interface MountOverride {\n readonly localId: LocalEntityId;\n readonly comp: string;\n readonly field?: string;\n readonly value: unknown;\n}\n\n/**\n * One mount instance authored on a parent SceneAsset\n * (feat-20260608-scene-nesting-ecs-fication M1 / w7; AC-01).\n *\n * A mount embeds another SceneAsset (referenced by `source`, a dual-carrier:\n * `number` = at-rest refs[] index; `string` = post-parse / post-collect GUID\n * string) into the parent's namespace. The mount reserves a contiguous\n * LocalEntityId window\n * `[memberFirst, memberFirst + memberCount)` for the embedded scene's\n * member entities so the parent SceneAsset's namespace invariant\n * `totalSlots = entities.length + mounts.length + sum(memberCount)`\n * holds (plan-strategy §6.3 + requirements §S-1).\n *\n * Optional fields:\n * - `parent`: LocalEntityId in the *parent* scene to which the mount\n * attaches (defaults to the parent scene's outermost root,\n * requirements-decisions §D-4);\n * - `components`: per-component value overlay applied to the mount\n * entity itself (mirrors SceneEntity.components shape; carries the\n * same Partial<ComponentValuesMap> typing);\n * - `overrides`: an array of MountOverride records that further\n * specialise individual member entities at mount-time (AC-19).\n *\n * Charter mapping: proposition 1 (single-entry import surface;\n * SceneInstanceMount sits next to SceneEntity / SceneAsset) +\n * proposition 5 (consistent abstraction: the field set mirrors\n * SceneEntity for AI-user discoverability).\n */\nexport interface SceneInstanceMount {\n readonly localId: LocalEntityId;\n readonly source: number | string;\n readonly memberFirst: LocalEntityId;\n readonly memberCount: number;\n readonly parent?: LocalEntityId;\n readonly components?: Partial<ComponentValuesMap>;\n readonly overrides?: readonly MountOverride[];\n /** Engine-owned publication identity for generated Scene mounts. */\n readonly publicationFence?: import('./asset-producer').ScenePublicationFence;\n}\n\n/**\n * Scene asset POD shape (feat-20260514 w2; sixth member of the closed\n * `Asset` union).\n *\n * Three top-level fields — `kind: 'scene'` discriminator,\n * `entities: readonly SceneEntity[]`, and the optional `mounts:\n * readonly SceneInstanceMount[]` (feat-20260608-scene-nesting-\n * ecs-fication M1 / w7; AC-01). Per-instance overrides at the\n * asset layer are still absent (charter proposition 5: ECS write\n * paths and prefab override paths are explicitly disjoint,\n * plan-strategy §3.2 sequence B); the `mounts[]` window is an\n * authoring-time graph edge, not a write path.\n *\n * Back-compat: `mounts` is optional so legacy SceneAsset values\n * remain assignable; missing `mounts` is semantically equivalent to\n * `mounts: []` (plan-strategy §6.3, ajv default `[]`).\n */\nexport interface SceneAsset {\n readonly kind: 'scene';\n readonly entities: readonly SceneEntity[];\n readonly mounts?: readonly SceneInstanceMount[];\n /**\n * GUIDs of `SkinAsset`s the scene's skinned entities reference (one per\n * SkeletonAsset bound by a `Skin: { skeleton }` component). SkinAssets are\n * not reachable through any `handle<*>` field on a SceneEntity component\n * (`Skin.skeleton` carries the SkeletonAsset GUID; the SkinAsset itself is\n * a sibling identified by matching `skeletonGuid`), so the scene's pack\n * load chain has to surface them explicitly. Without this list the\n * browser-async-pack-fetch path would never load SkinAssets, leaving\n * `postSpawnResolveJoints` unable to populate `Skin.joints[]` and the\n * extract pass fail-fasting on `Skin.joints.length=0` every frame\n * (feat-20260612-skin-palette-per-frame-upload M2 fixup).\n *\n * On disk: refs[] indices, mirror of `mounts[].source`.\n * Post-parseScenePayload: GUID strings (resolved via refs[]).\n * Enumerated in the scene envelope's `refs[]` (the recursion source) so\n * `loadByGuid<SceneAsset>` recursively pulls each SkinAsset before\n * `instantiate`.\n */\n readonly skinGuids?: readonly string[];\n}\n\n// === Skeleton / Skin / AnimationClip asset POD shapes (feat-20260523-skin-skeleton-animation M0) ===\n//\n// Decision anchors:\n// - requirements AC-01 (SkeletonAsset shape: kind, guid, inverseBindMatrices Float32Array, jointCount)\n// - requirements AC-04 (Skin sub-asset shape: kind, guid, skeletonGuid, jointPaths)\n// - requirements AC-07 (AnimationClip shape: kind, guid, duration, channels)\n// - plan-strategy D-1 (3-asset separation: IBM/Skin bindings/AnimationClip curves physically independent)\n// - charter P3 (explicit failure: all shape carry typed fields, no loose Record<string,unknown> payloads)\n//\n// AnimationChannel / AnimationSampler sub-types are inline here since they are\n// exclusively consumed by AnimationClip; no other asset or component references them.\n\n/** Stable target identity linking a sampler to a scene entity. */\nexport interface AnimationChannel {\n /** Stable animation target identity. */\n readonly targetId: AnimationTargetIdValue;\n /** Target transform property: 'translation' | 'rotation' | 'scale' | 'weights'. */\n readonly property: 'translation' | 'rotation' | 'scale' | 'weights';\n /** Sampler driving this channel. */\n readonly sampler: AnimationSampler;\n}\n\n/**\n * Animation sampler — keyframe curve for a single animation-target-property pair.\n *\n * `input` and `output` are Float32Arrays of equal length\n * (`output.length = input.length * elementCount`) where elementCount is:\n * - 3 for 'translation' / 'scale' (vec3)\n * - 4 for 'rotation' (quat)\n * - targetCount for 'weights' (morph weights)\n *\n * `interpolation` is restricted to LINEAR and STEP per D-1 scope;\n * CUBICSPLINE is deferred to OOS-skin-cubicspline (fail-fast at importer).\n */\nexport interface AnimationSampler {\n readonly input: Float32Array;\n readonly output: Float32Array;\n readonly interpolation: 'LINEAR' | 'STEP';\n}\n\n/**\n * Animation clip asset POD shape.\n *\n * `duration` is max(sampler.input[last]) across all channels — the\n * longest channel defines the clip length. Each channel targets one\n * animation-target property, resolved during playback by `targetId`.\n */\nexport interface AnimationClip {\n readonly kind: 'animation-clip';\n readonly duration: number;\n readonly channels: readonly AnimationChannel[];\n}\n\n// === AnimationGraph asset POD + node union (feat-20260713 M2 / w13) ==============\n//\n// Decision anchors:\n// - requirements AC-02 (declarative Clip/Blend/Add + nesting graph carried as\n// a shared<AnimationGraph> asset handle, multi-entity shared).\n// - requirements AC-14 (AnimationGraph joins the closed `Asset` union, owns a\n// GUID, and serializes into pack/scene round-trip — foundation landed here,\n// the serialize/deserialize mechanism itself is M4).\n// - requirements OOS-1/OOS-2/OOS-3/OOS-4 (node union is CLOSED at three\n// variants — clip / blend / add. No FSM state/transition, no bone Mask, no\n// built-in transition layer, no BlendSpace fields are reserved; deferred\n// features add nodes in a future closed loop, not speculative fields now —\n// charter F4 \"no unvalidated abstraction\").\n// - requirements OOS-7 (POD carries only the topology of an engine-authored\n// defineAnimationGraph graph; no DCC import metadata).\n// - plan-strategy D-4 (POD + node union land in types/index.ts, the single-file\n// SSOT for every Asset POD, alongside AnimationClip).\n//\n// The POD mirrors AnimationClip: no inline `guid` field — the GUID is assigned by\n// the AssetRegistry / shared-handle system when the graph is registered (like\n// AnimationClip / MaterialAsset / VideoAsset). Nodes are stored flat in `nodes[]`\n// and referenced by index; `root` is the index of the output node. Clip leaves\n// carry a durable GUID string; the runtime consumer resolves that GUID to a\n// World-local transient shared handle only at evaluation time.\n\n/**\n * Clip leaf node — samples a single `shared<AnimationClip>` at the node's\n * runtime seek-time (M3 evaluation). `weight` is the node's STATIC weight; the\n * effective weight is `runtime weight x static weight` (requirements AC-07\n * orthogonal product).\n */\nexport interface AnimationGraphClipNode {\n readonly type: 'clip';\n readonly clip: string;\n readonly weight: number;\n}\n\n/**\n * Blend node — normalizing lerp over its children (requirements AC-04). Child\n * effective weights are normalized so they sum to 1 at evaluation. `children`\n * are indices into the parent {@link AnimationGraph.nodes} array.\n */\nexport interface AnimationGraphBlendNode {\n readonly type: 'blend';\n readonly children: readonly number[];\n readonly weight: number;\n}\n\n/**\n * Add node — non-normalizing additive stack (requirements AC-05). The `base`\n * child contributes its effective weight unchanged; each `additive` layer is\n * added on top WITHOUT normalization (total may exceed 1). `base` and\n * `additive` are indices into {@link AnimationGraph.nodes}.\n */\nexport interface AnimationGraphAddNode {\n readonly type: 'add';\n readonly base: number;\n readonly additive: readonly number[];\n readonly weight: number;\n}\n\n/**\n * Closed node union — exactly three variants (clip / blend / add). AI users\n * exhaustive `switch (node.type)` without default; TS guards completeness\n * (charter P3). No FSM / Mask / transition / BlendSpace variants are reserved\n * (OOS-1..4).\n */\nexport type AnimationGraphNode =\n | AnimationGraphClipNode\n | AnimationGraphBlendNode\n | AnimationGraphAddNode;\n\n/**\n * AnimationGraph asset POD — a Clip/Blend/Add DAG with per-node static weights.\n *\n * Joins the closed `Asset` union with `kind: 'animation-graph'` (AC-14); minted\n * into a `Handle<'AnimationGraph', 'shared'>` via `world.allocSharedRef` /\n * `AssetRegistry`, shared across multiple entities. Constructed via\n * `defineAnimationGraph` (runtime), which validates topology (no out-of-range\n * refs / cycles / invalid weights / empty graph) before a handle is minted.\n */\nexport interface AnimationGraph {\n readonly kind: 'animation-graph';\n readonly nodes: readonly AnimationGraphNode[];\n readonly root: number;\n}\n\n// === VideoAsset POD shape (feat-20260623-world-space-video-asset M1) ==========\n//\n// Decision anchors:\n// - requirements AC-01 (VideoAsset is Asset closed-union 15th member;\n// kind discriminator 'video'; payload { url: string }, no width/height/duration).\n// - requirements constraint: payload must not inline video bytes, only a URL descriptor.\n// - plan-strategy D-4 (VideoAsset descriptor naming aligns with TextureAsset/AudioClipAsset).\n// - charter F1 (AI users discover the schema via IDE autocomplete on the closed\n// `Asset` union + `Handle<VideoAsset>` returns from `AssetRegistry.register`).\n// - plan-strategy D-5 (resolveTexLike identifies video kind via `payload.kind === 'video'`;\n// video does not masquerade as 'texture').\n//\n// `refs` is always empty (isolated leaf) — VideoAsset carries no sub-asset\n// references (plan-strategy S6.3). The `url` field points to an external video\n// file; the runtime resolves it into an HTMLVideoElement via the host-provided\n// `VideoElementProvider` World Resource (plan-strategy D-1).\n\n/**\n * Video asset POD shape -- pure `{url}` descriptor.\n *\n * `VideoAsset` is a runtime-only asset kind (OOS-1: no import/cook pipeline).\n * The `url` field points to an external video file (e.g. `*.webm` / `*.mp4`);\n * the engine does NOT decode video bytes -- it delegates to the host-side\n * `HTMLVideoElement` via `VideoElementProvider` (plan-strategy D-1).\n *\n * `width` / `height` / `duration` are deliberately absent from the POD:\n * the runtime reads them from `HTMLVideoElement.videoWidth` /\n * `videoHeight` / `duration` after `loadedmetadata` fires (requirements\n * constraint \"payload must not inline video bytes\").\n *\n * Consumers reference a VideoAsset via a material texture value\n * fields (e.g. `baseColorTexture`), sharing the same `texture2d` slot with\n * static textures (charter P4 consistent abstraction). The extraction layer\n * (render-system-extract `resolveTexLike`) identifies the video kind and\n * routes to the per-frame transient texture pathway instead of the static\n * `GpuResourceStore.ensureResident` cache (plan-strategy D-5).\n */\nexport interface VideoAsset {\n readonly kind: 'video';\n readonly url: string;\n}\n\n/**\n * Skeleton asset POD shape — pure rig data, no mesh attachment.\n *\n * `inverseBindMatrices` is a Float32Array of length jointCount * 16\n * (column-major mat4 per joint). Missing IBM in source glTF is filled\n * with identity mat4 at importer time.\n *\n * `jointCount` is the number of joints (= IBM array length / 16).\n * Keys off the glTF skin's `joints[]` array length; validated against\n * MAX_JOINTS (256) at importer time.\n */\nexport interface SkeletonAsset {\n readonly kind: 'skeleton';\n readonly inverseBindMatrices: Float32Array;\n readonly jointCount: number;\n}\n\n/**\n * Skin sub-asset — the binding between a skeleton and a scene node hierarchy.\n *\n * `skeletonGuid` references a SkeletonAsset by GUID (string form).\n * `jointPaths` is a parallel array to the skeleton's joints; each entry\n * is a Name-component path from scene root to the joint entity, used\n * at post-spawn time to populate Skin.joints: Entity[].\n *\n * Zero-Entity-reference at the asset layer (AC-06): no Entity or\n * LocalEntityId fields — the binding is name-based, resolved at instantiate time.\n */\nexport interface SkinAsset {\n readonly kind: 'skin';\n readonly skeletonGuid: string;\n readonly jointPaths: readonly string[];\n}\n\n/**\n * VertexAttributeMap — 14-key closed set (feat-20260823 vertex-color asset closure).\n *\n * Canonical interleaved order (plan-strategy F-1, must match bridge + layout layers):\n * position / normal / uv / tangent / skinIndex / skinWeight / uv1..uv7 / color\n *\n * @location mapping per plan-strategy D-4:\n * position@0 normal@1 uv@2 tangent@3 skinIndex@4 skinWeight@5\n * uv1@6 uv2@7 uv3@8 uv4@9 uv5@10 uv6@11 uv7@12 color@13\n *\n * Keys align with Three.js r184 `BufferGeometry.attributes` naming (D-P1 +\n * plan-strategy §7.2 mental migration stance). Importers rename at ingest\n * (`POSITION -> position` / `TEXCOORD_0 -> uv` / `JOINTS_0 -> skinIndex` /\n * `WEIGHTS_0 -> skinWeight`) so the runtime key space remains lowercase.\n *\n * All 14 keys are optional; a mesh with only `position` (static unlit) is\n * valid. Values accept the three common binary shapes:\n * `ArrayBuffer | Float32Array | Uint16Array` (extend only via minor add per\n * the closed-union evolution contract).\n *\n * AC-15 narrowing: consumer sites writing\n * `for (const [key, buffer] of Object.entries(attributes))` observe `key`\n * typed as the 14-member literal union without `as` casts; any typo (e.g.\n * `'POSITION'`) is a TS compile-time error.\n */\nexport interface VertexAttributeMap {\n position?: ArrayBuffer | Float32Array | Uint16Array;\n normal?: ArrayBuffer | Float32Array | Uint16Array;\n uv?: ArrayBuffer | Float32Array | Uint16Array;\n tangent?: ArrayBuffer | Float32Array | Uint16Array;\n skinIndex?: ArrayBuffer | Float32Array | Uint16Array;\n skinWeight?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 1 (feat-20260629-multi-uv-set-support m3-w3, pre-added by M1 for bridge typecheck). */\n uv1?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 2 */\n uv2?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 3 */\n uv3?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 4 */\n uv4?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 5 */\n uv5?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 6 */\n uv6?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 7 */\n uv7?: ArrayBuffer | Float32Array | Uint16Array;\n /** Optional per-vertex linear RGBA color, exactly four finite floats per vertex. */\n color?: Float32Array;\n}\n\n/** Closed storage vocabulary used by the geometry pack boundary. */\nexport type VertexAttributeStorage = 'array-buffer' | 'float32' | 'uint16' | 'other';\n\n/** Lossless detail for one rejected canonical vertex attribute pack. */\nexport type VertexAttributePackDetail =\n | {\n readonly field: 'vertexCount';\n readonly reason: 'vertex-count-invalid';\n readonly actual: number;\n }\n | {\n readonly field: 'attributes';\n readonly reason: 'attributes-empty';\n readonly actualCount: 0;\n }\n | {\n readonly field: keyof VertexAttributeMap;\n readonly reason: 'attribute-storage-invalid';\n readonly expectedStorage: 'float32' | 'uint16';\n readonly actualStorage: VertexAttributeStorage;\n }\n | {\n readonly field: keyof VertexAttributeMap;\n readonly reason: 'attribute-cardinality-mismatch';\n readonly vertexCount: number;\n readonly componentsPerVertex: number;\n readonly expectedLength: number;\n readonly actualLength: number;\n }\n | {\n readonly field: keyof VertexAttributeMap;\n readonly reason: 'attribute-non-finite';\n readonly elementIndex: number;\n readonly actual: 'nan' | 'positive-infinity' | 'negative-infinity';\n };\n\n/**\n * Canonical UV attribute key order (set 0 = `uv`, sets 1..7 = `uv1..uv7`),\n * matching the VertexAttributeMap declaration + @location numbering (D-4).\n * SSOT for \"how many UV sets does this attribute map carry\" so the import,\n * gpu-resource, and layout-derivation layers count identically (no drift).\n */\nexport const UV_ATTRIBUTE_KEYS = ['uv', 'uv1', 'uv2', 'uv3', 'uv4', 'uv5', 'uv6', 'uv7'] as const;\n\n/**\n * Structural input for the UV-set counters: any object that may carry the\n * canonical UV attribute keys. Both the loosely-typed mesh-attribute record\n * (`Record<string, unknown>`) and the closed `VertexAttributeMap` satisfy it,\n * so the import / gpu-resource / layout layers all call one counter.\n */\nexport type UvAttributeSource = Partial<Record<(typeof UV_ATTRIBUTE_KEYS)[number], unknown>>;\n\n/**\n * Total number of UV sets present in an attribute map: the highest populated\n * `uv`/`uv1..uv7` index + 1, or 0 when none are present. A populated key is one\n * whose value is a typed array / array buffer / number array (the binary shapes\n * a vertex attribute can take). This is the single counter all multi-UV layers\n * derive from (feat-20260629 F-4 DRY collapse).\n */\nexport function countUvSets(attrs: UvAttributeSource | undefined): number {\n if (attrs === undefined) return 0;\n for (let i = UV_ATTRIBUTE_KEYS.length - 1; i >= 0; i--) {\n // biome-ignore lint/style/noNonNullAssertion: bounded index on const tuple\n const v = attrs[UV_ATTRIBUTE_KEYS[i]!];\n if (\n v instanceof Float32Array ||\n v instanceof Uint16Array ||\n v instanceof ArrayBuffer ||\n Array.isArray(v)\n ) {\n return i + 1;\n }\n }\n return 0;\n}\n\n/**\n * Extra UV sets beyond set 0 (= `countUvSets - 1`, floored at 0). The\n * gpu-resource + register layers size the dynamic vertex stride from this.\n */\nexport function countExtraUvSets(attrs: UvAttributeSource | undefined): number {\n const total = countUvSets(attrs);\n return total > 0 ? total - 1 : 0;\n}\n\n// === Asset error model SSOT (feat-20260511-asset-system-v1 / D-P1 / w3) =========\n//\n// Decision anchors:\n// - requirements §G3 + AC-03 + AC-10 + AC-21 + §1 callout row 9 (4-member closed\n// `AssetErrorCode` independent from `RhiErrorCode`; `AssetError` class with\n// `.code / .expected / .hint / .message` four-field surface structurally\n// parallel to `RhiError` / `InspectorError` / `MetricError`)\n// - plan-strategy §2 D-P1 (`@forgeax/engine-types` single-file SSOT for\n// AssetErrorCode; independent closed union aligned with\n// MetricErrorCode / InspectorErrorCode precedent)\n// - plan-strategy §7.3 (per-code `.hint` string literals locked verbatim\n// below; any drift updates both this module and the test fixtures)\n// - charter proposition 3 (machine-readable union > prose) + proposition 4\n// (explicit failure — `switch (err.code)` exhaustive without `default:`) +\n// proposition 5 (consistent abstraction — structurally parallel to\n// `@forgeax/engine-rhi` `RhiError` surface)\n// - architecture-principles #1 SSOT (the 4 literals + class shape live here\n// once; M4 AssetRegistry / M3 Geometry factories / AGENTS.md §Error model\n// row all reference this module)\n\n/**\n * Closed `AssetErrorCode` union — 23 members (D-P1 + feat-20260518 D-1 minor\n * evolution + feat-20260520-skylight-ibl-cubemap 5 members +\n * feat-20260523 mesh-upload-fix 1 member +\n * feat-20260523-shader-template-instance-split M1-T02 1 member +\n * feat-20260526-material-asset-multipass-renderstate M1 1 member +\n * feat-20260603-asset-import-loader-injection M1 2 members +\n * feat-20260604-hdr-equirect-cube-importer-loader M2 1 member +\n * feat-20260608-mesh-multi-section-primitive-multi-material-slot M1 3 members +\n * feat-20260621-asset-registry-robustness-invalidate-inflight-cach M2 1 member +\n * feat-20260707-texture-block-compression M5 1 member\n * (mipgen-unsupported-compressed-format);\n * requirements §G3 + AC-03 + AC-21 +\n * feat-20260518 AC-02 + bug-20260523 AC-01. The runtime-guard SSOT for the\n * current member count is the ASSET_ERROR_HINTS key-count test, not this prose.)\n * Exhaustive `switch (err.code)` needs no default fallback — TypeScript guards\n * union completeness at compile time (charter F2/P2 machine-readable union >\n * prose + P3 explicit failure).\n *\n * Domain-separated from `RhiErrorCode 'asset-not-registered'` (which is a\n * render-time registry lookup miss, 18-member closed union in\n * `@forgeax/engine-rhi/src/errors.ts`). The two unions cover disjoint\n * lifecycle phases — AI users face only these 22 alternatives on the\n * `engine.assets.loadByGuid(guid)` / `engine.assets.get(handle)` /\n * `engine.assets.register(payload).unwrap()` surface.\n *\n * | code | trigger |\n * |:--|:--|\n * | `'asset-not-found'` | `AssetRegistry.get(handle)` returned no entry (handle never registered or registry was reset); charter P3 explicit failure. |\n * | `'asset-parse-failed'` | decoded bytes are not a valid image (PNG / JPG header corruption on the load path; dimensions <= 0 / segments < 1 on the procedural geometry constructor path — double semantics locked by requirements §9 \"constructor path\" extension). |\n * | `'asset-format-unsupported'` | URL content-type / magic bytes are neither PNG nor JPG (v1 scope; KTX2 / Basis / GLTF embedded textures deferred to M3+). |\n * | `'asset-fetch-failed'` | `fetch(url)` returned non-2xx, threw, or the URL was otherwise unreachable (404 / network / CORS surface). |\n * | `'asset-invalid-value'` | `register<MaterialAsset>(payload)` value validation fails the 3-tier validator (type-mismatch / extra-key / missing-required) — fail-fast at register entry. |\n * | `'cubemap-handle-missing'` | internal equirect-to-cubemap projection has no live cubemap for a `Skylight.equirect` handle; `.hint` points to loading an EquirectAsset + caps.rgba16floatRenderable. |\n * | `'invalid-source-format'` | image importer path when `.hdr` decode needs rgba16float / rgba32float. |\n * | `'load-failed'` | `loadByGuid` when guid entry exists in catalog but file is inaccessible. |\n * | `'device-unsupported'` | GPU capability gate: `device.caps.rgba16floatRenderable` missing. |\n * | `'ibl-precompute-not-dispatched'` | `IblPipelineCache` when counter increments but `queue.submit` hasn't been called. |\n * | `'mesh-vertex-stride-mismatch'` | `register({ kind: 'mesh', ... })` vertices buffer is not evenly divisible by 12 floats per vertex (position vec3 + normal vec3 + uv vec2 + tangent vec4) or `maxIndex+1 !== vertexCount` — fail-fast at register entry (charter P3 structured failure; `.detail` carries `vertexCount` / `floatsPerVertex`). |\n| `'material-circular-inheritance'` | material resolve detected a cycle in the parent chain; `.hint` carries the full cycle path (e.g. \"A -> B -> A\") via `err.detail.cycle`. |\n * | `'loader-not-registered'` | `loadByGuid` dispatched on `asset.kind` but the injected `LoaderRegistry` has no loader for that kind; `.detail.kind` is the missing kind and `.detail.registeredKinds` lists the kinds currently wired (feat-20260603-asset-import-loader-injection M1; charter P3 — AI users read `.detail.registeredKinds` to know what to inject). |\n * | `'asset-not-imported'` | `loadByGuid` found the GUID in the catalog but its DDC is absent and no `ImportTransport` is wired (shipped form); `.hint` points back to build-time pre-import rather than a runtime workaround (feat-20260603-asset-import-loader-injection M4; logic wired in M4 w31). |\n * | `'texture-source-not-imported'` | `loadTextureAsset` received an uncooked source locator instead of a Pack v2 artifact; the runtime carries no source decoder. |\n */\nexport type AssetErrorCode =\n | 'asset-not-found'\n | 'asset-parse-failed'\n | 'asset-format-unsupported'\n | 'asset-fetch-failed'\n | 'catalog-source-unconfigured'\n | 'asset-invalid-value'\n | 'cubemap-handle-missing'\n | 'invalid-source-format'\n | 'load-failed'\n | 'device-unsupported'\n | 'ibl-precompute-not-dispatched'\n | 'mesh-vertex-stride-mismatch'\n // === 1 new code (feat-20260523-shader-template-instance-split M1-T02) ===\n | 'material-shader-ref-broken'\n // === 1 new code (feat-20260526-material-asset-multipass-renderstate M1 / w6) ===\n | 'material-circular-inheritance'\n // === 2 new codes (feat-20260603-asset-import-loader-injection M1 / w1) ===\n | 'loader-not-registered'\n | 'asset-not-imported'\n // === 1 new code (feat-20260604-hdr-equirect-cube-importer-loader M2 / w4) ===\n | 'texture-source-not-imported'\n // === 1 new code (perf-20260706-raw-container-failfast) ===\n // A mesh/material/scene/skeleton/skin/animation-clip catalog row whose\n // packageUrl is still a raw source container (.glb/.gltf/.fbx), not an\n // importer-produced artifact (.bin/.pack.json). Like texture-source-not-imported\n // this is transport-eligible: the studio form lazily imports via the injected\n // ImportTransport; the shipped form fails fast. Distinct from the generic\n // asset-not-imported so it never masks the parent-missing breadcrumb.\n | 'source-not-imported'\n // === 3 new codes (feat-20260608-mesh-multi-section-primitive-multi-material-slot M1 / w2) ===\n | 'mesh-renderer-material-override-invalid'\n | 'mesh-renderer-material-override-overflow'\n | 'mesh-asset-submeshes-empty'\n | 'mesh-asset-material-slot-index-out-of-range'\n | 'mesh-submesh-index-range-out-of-bounds'\n // === 1 new code (feat-20260608-tilemap-object-layer-rendering M0 baseline rebuild) ===\n // Tileset region rectangle out of atlas extent OR tile entry regionIndex out of\n // regions array bounds (single closed code per plan-strategy §D-6 first-error\n // ordering). 19 -> 20 baseline-restored.\n | 'tileset-region-index-out-of-range'\n // === 1 new code (feat-20260629-multi-uv-set-support M2 / m2-w5) ===\n // mesh-bin header v2 contract violation: version unknown, uvSetCount out of\n // [0,8], or stride/floatsPerVertex self-consistency check failed at encode\n // exit or decode entry (Fail Fast). Carries detail { version, uvSetCount,\n // stride }. .hint = 're-cook the asset via importer'.\n | 'mesh-bin-contract-violation'\n // === 1 new code (feat-20260608-tilemap-object-layer-rendering M1 schema extension) ===\n // Tile entry optional field (widthCells / heightCells / pivotX / pivotY /\n // collider) or top-level atlases / region.atlasIndex schema invariant\n // breached at register time. `.detail.field` carries the closed 7-variant\n // enum + `.scope?` is 'tile-entry' | 'tileset-asset' (plan-strategy §D-6;\n // charter P3 closed enum + AI-grep affordance). 20 -> 21 M1 net add.\n | 'tileset-tile-entry-malformed'\n // === 1 new code (feat-20260621-asset-registry-robustness-invalidate-inflight-cach M2 / w4) ===\n | 'asset-invalidated'\n // === 1 new code (feat-20260707-texture-block-compression M5 / w35, D-9) ===\n // deriveRenderDataTexture fail-fast: a block-compressed `format` requested\n // RUNTIME mip generation (`mipmap:true` with no offline `mipLevelCount>1`\n // chain). Compressed formats are not render targets, so the mipmap blit\n // pipeline cannot generate their mips (F-7); the chain must be baked offline.\n // `.hint` carries the self-recovery (bake offline mips, or set the sidecar\n // `compressionMode:'none'`). A compressed texture whose mips are ALREADY in\n // `data` (mipLevelCount>1 from a KTX2 level chain) does NOT trip this gate.\n | 'mipgen-unsupported-compressed-format';\n\n/**\n * Structured asset error -- four-field surface (`.code` / `.expected` /\n * `.hint` / `.message`) structurally parallel to `@forgeax/engine-rhi`\n * `RhiError` + `@forgeax/engine-remote` `InspectorError` + `MetricError`\n * (charter proposition 5 consistent abstraction; AGENTS.md \"Errors are\n * structured. Return Result, never throw for expected failures.\").\n *\n * AI users consume the structured triple via property access:\n * `switch (err.code) { case 'asset-fetch-failed': ... err.hint ... }`\n * -- never by parsing `.message` (charter proposition 4 explicit failure\n * red line).\n *\n * The `.message` field is auto-composed for human stack traces and carries\n * the same content as `.code` + `.expected` + `.hint`; AI users prefer\n * field access on the structured triple.\n *\n * @example AI-user exhaustive switch on the 22 members (no default fallback)\n * ```ts\n * import { AssetError, type AssetErrorCode } from '@forgeax/engine-types';\n *\n * function recover(code: AssetErrorCode): string {\n * switch (code) {\n * case 'asset-not-found': return 'ensure handle was registered before get()';\n * case 'asset-parse-failed': return 'check file integrity or geometry dimensions';\n * case 'asset-format-unsupported': return 'convert to PNG or JPG';\n * case 'asset-fetch-failed': return 'check url path or dev server';\n * case 'asset-invalid-value': return 'read err.hint / err.detail for the case-specific fix';\n * }\n * }\n * ```\n */\nexport class AssetError extends Error {\n readonly code: AssetErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail?: Readonly<AssetErrorDetail>;\n\n constructor(args: {\n code: AssetErrorCode;\n expected: string;\n hint: string;\n detail?: Readonly<AssetErrorDetail>;\n }) {\n super(`[AssetError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);\n this.name = 'AssetError';\n this.code = args.code;\n this.expected = args.expected;\n this.hint = args.hint;\n if (args.detail !== undefined) {\n this.detail = args.detail;\n }\n }\n}\n\n/**\n * Per-code `.hint` string literals (plan-strategy §7.3 lock-in). Exported\n * so M3 Geometry factories / M4 AssetRegistry / tests consume the same\n * SSOT — any drift here updates both producer call sites and the\n * AGENTS.md §Error model table.\n *\n * The shape is a `Record<AssetErrorCode, string>` so future additions to\n * the closed union are a compile-time error here as well (reinforces\n * charter proposition 4 explicit failure).\n */\nexport const ASSET_ERROR_HINTS: Readonly<Record<AssetErrorCode, string>> = {\n 'asset-fetch-failed':\n 'check url path; verify dev server is running; in tests use data: URL fixture (data:image/png;base64,...)',\n 'catalog-source-unconfigured':\n 'call AssetRegistry.setCatalogSource(source) before enumerateCatalog(), then retry the operation',\n 'asset-parse-failed':\n 'check file bytes are not corrupted; for procedural geometry: verify all dimensions > 0 and segments >= 1',\n 'asset-format-unsupported':\n 'v1 supports png/jpg only; convert .bmp/.webp etc. via image tooling; gltf/glb supported via @forgeax/engine-gltf importer (forgeax-engine-remote-gltf import <gltf-or-glb>)',\n 'asset-not-found':\n 'handle id not in registry; verify register() was called before get(); inspect() returns all live handles',\n 'asset-invalid-value':\n 'a register-time value failed validation; read err.hint for the case-specific fix (e.g. clamp a MaterialAsset param to [0,1], or give a strip-topology MeshAsset an index buffer) and err.detail for the offending field/value',\n 'cubemap-handle-missing':\n 'the equirect-to-cubemap projection (internal to the render-system record arm) has no live cubemap for this Skylight; ensure Skylight.equirect references a loaded EquirectAsset handle and caps.rgba16floatRenderable is true',\n 'invalid-source-format':\n 'decode .hdr via @forgeax/engine-image first; supported formats are rgba16float and rgba32float',\n 'load-failed':\n 'source asset could not be loaded; check GUID validity and file accessibility in the pack-index catalog',\n 'device-unsupported':\n 'GPU device lacks required capability; check device.caps for rgba16float renderable feature',\n 'ibl-precompute-not-dispatched':\n 'check IblPipelineCache.runIblPrecompute is called inside the internal GpuResourceStore equirect-to-cubemap projection; counters must not increment before queue.submit (plan D-7 / N-3 AC-20 invariant)',\n 'mesh-vertex-stride-mismatch':\n 'use meshFromInterleaved (packages/runtime/src/geometry/box.ts) or expand vertices buffer to canonical 12F layout (position vec3 + normal vec3 + uv vec2 + tangent vec4)',\n // === 1 new hint (feat-20260523-shader-template-instance-split M1-T02) ===\n 'material-shader-ref-broken':\n 'the materialShader identifier (path or GUID) resolves to no registered shader; check ShaderRegistry for path identifiers or AssetRegistry for GUID sub-assets',\n // === 1 new hint (feat-20260526-material-asset-multipass-renderstate M1 / w6) ===\n 'material-circular-inheritance':\n 'circular parent chain detected; inspect parent handles — use err.detail.cycle to see the full path (e.g. \"A -> B -> A\")',\n // === 2 new hints (feat-20260603-asset-import-loader-injection M1 / w1) ===\n 'loader-not-registered':\n 'no loader registered for this asset kind; register it via engine.assets.loaders.register(loader) (the loader carries its own kind); err.detail.registeredKinds lists the kinds currently wired',\n 'asset-not-imported':\n 'GUID is in the catalog but its DDC artefact is missing and no ImportTransport is wired (shipped form never falls back to a runtime import); add the asset to the build-time pre-import step instead of importing at runtime',\n // === 1 new hint (feat-20260604-hdr-equirect-cube-importer-loader M2 / w4) ===\n 'texture-source-not-imported':\n 'texture source not imported yet; wire createDevImportTransport() in the studio form for dev lazy-import, or pre-import via the build-time pipeline',\n // === 1 new hint (perf-20260706-raw-container-failfast) ===\n 'source-not-imported':\n 'this mesh/material/scene sub-asset row still points at the raw source container (.glb/.gltf/.fbx); wire createDevImportTransport() for dev lazy-import (POST /__import), or pre-import via the build-time pipeline. The runtime does not parse raw containers at load time.',\n // === 3 new hints (feat-20260608-mesh-multi-section-primitive-multi-material-slot M1 / w2) ===\n 'mesh-renderer-material-override-invalid':\n 'a MeshRenderer.materials slot is stale or does not resolve to a MaterialAsset; the renderer inherited the MeshAsset default for that slot',\n 'mesh-renderer-material-override-overflow':\n 'MeshRenderer.materials contains entries beyond MeshAsset.materialSlots; extra overrides are ignored',\n 'mesh-asset-submeshes-empty':\n 'MeshAsset.submeshes must have at least one entry; every mesh must declare at least one submesh; check MeshAsset registration payload for empty submeshes array',\n 'mesh-asset-material-slot-index-out-of-range':\n 'MeshAsset submesh materialSlot must index MeshAsset.materialSlots; re-cook the mesh and inspect the offending submesh/slot topology',\n 'mesh-submesh-index-range-out-of-bounds':\n 'submesh indexOffset + indexCount exceeds the parent mesh index buffer length; check submesh index range bounds against MeshAsset.indices and MeshAsset.vertices; err.detail carries submeshIndex, indexOffset, indexCount, indexBufferLength, and meshAssetGuid',\n // === 1 new hint (feat-20260608-tilemap-object-layer-rendering M0 baseline rebuild) ===\n 'tileset-region-index-out-of-range':\n 'a TilesetAsset.regions[] rectangle escapes the atlas extent OR a TilesetAsset.tiles[].regionIndex points past TilesetAsset.regions.length; check regions[i] (x + width <= atlasWidth, y + height <= atlasHeight) and tiles[i].regionIndex in [0, regions.length); err.detail carries tilesetGuid, tileId, regionIndex, regionCount',\n // === 1 new hint (feat-20260608-tilemap-object-layer-rendering M1 schema extension) ===\n 'tileset-tile-entry-malformed':\n 'a TilesetTileEntry optional field is out of range (widthCells / heightCells in (0, 64], pivotX / pivotY in [0, 1], collider rect/polygon in normalized [0,1]^2 with rect.length === 4 and polygon.points.length >= 3) OR a top-level field is out of range (atlases.length >= 1, region.atlasIndex in [0, atlases.length)); engine fail-fast at register-time. read err.detail.field (closed enum) + err.detail.scope (tile-entry | tileset-asset) + err.detail.tileEntryIndex to locate the offending entry; switch (err.detail.field) covers the 7 variants exhaustively without default',\n // === 1 new hint (feat-20260621-asset-registry-robustness-invalidate-inflight-cach M2 / w4) ===\n 'asset-invalidated':\n 'The asset was invalidated during load; call loadByGuid(guid) again to retry with a fresh fetch',\n // === 1 new hint (feat-20260629-multi-uv-set-support M2 / m2-w5) ===\n 'mesh-bin-contract-violation':\n 're-cook the asset via importer; the .bin sidecar v4 contract is violated — inspect err.detail.reason and its expected/actual projection, stride, cardinality, and byte-length facts',\n // === 1 new hint (feat-20260707-texture-block-compression M5 / w35, D-9) ===\n 'mipgen-unsupported-compressed-format':\n 'compressed-texture mips must be baked offline (the GPU cannot generate mips for a non-render-target block format); re-cook with an offline mip chain, or set the sidecar .meta.json compressionMode:\"none\" (or mipmap:false) to keep runtime mip generation on an uncompressed texture',\n};\n\n// === Font error model SSOT (feat-20260531-world-space-msdf-text-rendering M2 / w6) ===\n//\n// Decision anchors:\n// - plan-strategy D-11 (two closed unions: FontErrorCode = build/load phase,\n// TextErrorCode = runtime layout phase; structured .code/.expected/.hint/.detail;\n// TOFU is rendering behaviour not an error — AC-14)\n// - requirements AC-15 (non-TTF -> FontErrorCode 'unsupported-font-format')\n// - requirements AC-16 (both unions in types/src/index.ts; exhaustive\n// switch(err.code) without default compiles)\n// - requirements AC-20 (font concurrency > 8 -> TextErrorCode\n// 'font-concurrency-exceeded')\n// - charter P3 (explicit failure: structured error > silent behaviour,\n// D-8 rejects silent LRU eviction for concurrency violation)\n//\n// Domain separation: FontErrorCode covers build-time bake failures and\n// load-time atlas/sampler resolution; TextErrorCode covers runtime glyph\n// layout and text rendering failures.\n\n/**\n * Closed `FontErrorCode` union — build-time bake + load-time resolution\n * errors (plan-strategy D-11).\n *\n * | code | trigger |\n * |:--|:--|\n * | `'unsupported-font-format'` | bake receives non-TTF input (OTF / WOFF2); `.expected: 'ttf'` (AC-15) |\n * | `'font-atlas-missing'` | loadByGuid font handle has missing/empty atlas texture GUID |\n * | `'font-atlas-corrupted'` | sidecar JSON parse failed or glyph metrics shape invalid |\n * | `'bake-failed'` | @zappar/msdf-generator call threw (wasm unavailable / internal error) |\n */\nexport type FontErrorCode =\n | 'unsupported-font-format'\n | 'font-atlas-missing'\n | 'font-atlas-corrupted'\n | 'bake-failed';\n\n/**\n * Closed `TextErrorCode` union — runtime glyph layout and text rendering\n * errors (plan-strategy D-11).\n *\n * | code | trigger |\n * |:--|:--|\n * | `'font-concurrency-exceeded'` | > 8 distinct FontAsset handles active in one frame; `.expected: 8` (AC-20) |\n * | `'font-atlas-missing'` | glyph layout system resolved fontHandle but atlas texture is not yet uploaded |\n * | `'glyph-layout-failed'` | layout computation encountered unexpected state (empty common block, etc.) |\n */\nexport type TextErrorCode =\n | 'font-concurrency-exceeded'\n | 'font-atlas-missing'\n | 'glyph-layout-failed';\n\n/**\n * Structured font error — four-field surface (`.code` / `.expected` /\n * `.hint` / `.message`) in the style of {@link AssetError}.\n *\n * AI users consume via property access:\n * `switch (err.code) { case 'unsupported-font-format': ... err.expected ... }`\n * — never by parsing `.message`.\n */\nexport class FontError extends Error {\n readonly code: FontErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail?: Readonly<Record<string, unknown>>;\n\n constructor(args: {\n code: FontErrorCode;\n expected: string;\n hint: string;\n detail?: Readonly<Record<string, unknown>>;\n }) {\n super(`[FontError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);\n this.name = 'FontError';\n this.code = args.code;\n this.expected = args.expected;\n this.hint = args.hint;\n if (args.detail !== undefined) {\n this.detail = args.detail;\n }\n }\n}\n\n/**\n * Structured text error — four-field surface (`.code` / `.expected` /\n * `.hint` / `.message`) in the style of {@link AssetError}.\n *\n * AI users consume via property access:\n * `switch (err.code) { case 'font-concurrency-exceeded': ... err.hint ... }`\n * — never by parsing `.message`.\n */\nexport class TextError extends Error {\n readonly code: TextErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail?: Readonly<Record<string, unknown>>;\n\n constructor(args: {\n code: TextErrorCode;\n expected: string;\n hint: string;\n detail?: Readonly<Record<string, unknown>>;\n }) {\n super(`[TextError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);\n this.name = 'TextError';\n this.code = args.code;\n this.expected = args.expected;\n this.hint = args.hint;\n if (args.detail !== undefined) {\n this.detail = args.detail;\n }\n }\n}\n\n// === AssetErrorDetail discriminated union (feat-20260523-shader-template-instance-split M1-T02) ===\n//\n// Introduced to type-narrow the AssetError.detail field for the new\n// 'material-shader-ref-broken' variant. Existing AssetErrorCode members\n// keep their Record<string, unknown> detail shapes; the union is\n// backward-compatible because the detail field on AssetError is optional.\n\n/**\n * Detail for `material-shader-ref-broken` — materialShader identifier\n * (path or GUID) could not be resolved to a registered shader.\n */\nexport interface AssetMaterialShaderRefBrokenDetail {\n readonly code: 'material-shader-ref-broken';\n readonly materialAssetGuid: string;\n readonly missingShaderId: string;\n readonly materialShaderPath?: string;\n}\n\n/**\n * Detail for `tileset-region-index-out-of-range` (feat-20260608 M0 baseline rebuild).\n *\n * Carries the offending tileset GUID + tile-entry index + the rejected\n * `regionIndex` + the live `regionCount` so AI consumers can pinpoint\n * the malformed payload field via property access (charter P3 / P4).\n *\n * Surfaced by `validateTilesetPayload` along two paths:\n * - region rectangle escapes the parent atlas extent\n * (regionIndex == the offending rectangle index).\n * - `tiles[i].regionIndex` >= `regions.length` (or negative)\n * (tileId encodes which entry; regionIndex carries the rejected value).\n */\nexport interface AssetTilesetRegionIndexOutOfRangeDetail {\n readonly code: 'tileset-region-index-out-of-range';\n readonly tilesetGuid: string;\n readonly tileId: number;\n readonly regionIndex: number;\n readonly regionCount: number;\n}\n\n/**\n * Detail for `tileset-tile-entry-malformed` (feat-20260608 M1 schema\n * extension; plan-strategy §D-6).\n *\n * Closed 7-variant `.field` enum locks the AI-grep affordance: switch\n * (detail.field) over the union compiles without default (charter P3).\n *\n * - `widthCells` -- `tiles[i].widthCells` out of `(0, 64]`.\n * - `heightCells` -- `tiles[i].heightCells` out of `(0, 64]`.\n * - `pivotX` -- `tiles[i].pivotX` out of `[0, 1]`.\n * - `pivotY` -- `tiles[i].pivotY` out of `[0, 1]`.\n * - `collider` -- `tiles[i].collider` schema invariant (rect.length !==\n * 4 / rect dimension out of `[0, 1]^2` / polygon.points.length < 3 /\n * any point out of `[0, 1]^2` / type discriminator outside the closed\n * 3-variant enum).\n * - `atlases` -- top-level `atlases.length < 1` (empty atlas list).\n * - `atlasIndex` -- `regions[i].atlasIndex` outside `[0, atlases.length)`.\n *\n * `.scope?` is `'tile-entry'` when the violation is in `tiles[i].*` (in\n * which case `.tileEntryIndex` carries the offending `tiles[]` index) and\n * `'tileset-asset'` when the violation is at the top level (atlases /\n * region atlasIndex).\n */\nexport interface AssetTilesetTileEntryMalformedDetail {\n readonly code: 'tileset-tile-entry-malformed';\n readonly field:\n | 'widthCells'\n | 'heightCells'\n | 'pivotX'\n | 'pivotY'\n | 'collider'\n | 'atlases'\n | 'atlasIndex';\n readonly scope?: 'tileset-asset' | 'tile-entry';\n readonly tileEntryIndex?: number;\n readonly tilesetGuid: string;\n readonly expected?: string;\n readonly hint?: string;\n}\n\n/**\n * Detail for `mesh-bin-contract-violation`.\n *\n * The mesh-bin header is a projection of the canonical geometry layout. Keep\n * the complete wire facts in the structured detail so recovery never depends\n * on parsing the human-facing `expected` / `actual` strings. `sourceKey` and\n * `reason` identify the owning payload and the failed invariant; the nested\n * snapshots preserve the lossless expected/actual cardinality facts.\n */\nexport type AssetMeshBinContractViolationReason =\n | 'header-truncated'\n | 'version-unsupported'\n | 'header-invalid'\n | 'projection-mismatch'\n | 'payload-length-mismatch'\n | 'metadata-invalid'\n | 'attribute-invalid'\n | 'payload-non-finite';\n\nexport interface AssetMeshBinContractFacts {\n readonly field?:\n | 'byteLength'\n | 'version'\n | 'projectionVersion'\n | 'mask'\n | 'stride'\n | 'digest'\n | 'vertexBytes'\n | 'indexBytes'\n | 'jsonBytes'\n | 'metadata'\n | 'attribute';\n readonly attribute?: keyof VertexAttributeMap;\n readonly elementIndex?: number;\n readonly expectedLength?: number;\n readonly actualLength?: number;\n readonly actualValue?: 'nan' | 'positive-infinity' | 'negative-infinity';\n readonly version?: number;\n readonly projectionVersion?: number;\n readonly mask?: number;\n readonly digest?: string;\n readonly stride?: number;\n readonly vertexCount?: number;\n readonly vertexBytes?: number;\n readonly indexCount?: number;\n readonly indexWidth?: number;\n readonly indexBytes?: number;\n readonly jsonBytes?: number;\n readonly byteLength?: number;\n}\n\nexport interface AssetMeshBinContractViolationDetail {\n readonly code: 'mesh-bin-contract-violation';\n readonly sourceKey: string;\n readonly reason: AssetMeshBinContractViolationReason;\n readonly expected: Readonly<AssetMeshBinContractFacts>;\n readonly actual: Readonly<AssetMeshBinContractFacts>;\n}\n\n/**\n * Discriminated detail union for AssetError, narrowed per AssetErrorCode.\n *\n * Variants:\n * - `material-shader-ref-broken` -- materialShader identifier (path or GUID)\n * could not be resolved to a registered shader; carries materialAssetGuid +\n * missingShaderId.\n * - `asset-invalid-value` -- a register-time value failed validation;\n * carries `{ field: string; got: unknown }`.\n * - `mesh-renderer-material-override-overflow` -- override entries exceed\n * slot count; carries `{ expectedCount, actualCount, meshAssetGuid }`.\n * - `mesh-asset-submeshes-empty` -- MeshAsset.submeshes is empty array;\n * carries `{ meshAssetGuid }`.\n * - `mesh-submesh-index-range-out-of-bounds` -- submesh index range exceeds\n * parent mesh index buffer; carries `{ submeshIndex, indexOffset,\n * indexCount, indexBufferLength, meshAssetGuid }`.\n */\nexport type AssetErrorDetail =\n | import('./asset.js').AssetCodecFailureDetail\n | AssetMaterialShaderRefBrokenDetail\n | AssetTilesetRegionIndexOutOfRangeDetail\n | AssetTilesetTileEntryMalformedDetail\n | AssetMeshBinContractViolationDetail\n | VertexAttributePackDetail\n | { readonly field: string; readonly got: unknown }\n | { readonly field: string; readonly value: unknown; readonly reason: string }\n | { readonly expectedCount: number; readonly actualCount: number; readonly meshAssetGuid: string }\n | { readonly meshAssetGuid: string; readonly slotIndex: number; readonly handle: number }\n | {\n readonly meshAssetGuid: string;\n readonly slotIndex: number;\n readonly slotName: string;\n readonly defaultMaterialGuid: string;\n readonly actualKind: string;\n }\n | MeshMaterialOverrideConflict\n | {\n readonly meshAssetGuid: string;\n readonly submeshIndex: number;\n readonly materialSlot: number;\n readonly materialSlotCount: number;\n }\n | { readonly meshAssetGuid: string }\n | {\n readonly submeshIndex: number;\n readonly indexOffset: number;\n readonly indexCount: number;\n readonly indexBufferLength: number;\n readonly meshAssetGuid: string;\n }\n // Pre-existing detail shapes used by AssetRegistry / loaders / pipeline-builder\n // (added in M5 / w27 alongside the count-mismatch tightening so the union\n // accommodates every current call site without losing structural narrowing).\n | { readonly sourcePath: string }\n | { readonly kind: string; readonly registeredKinds?: readonly string[] }\n | { readonly key: string; readonly legalPattern: string }\n | { readonly passCount: number }\n | {\n readonly passIndex: number;\n readonly shaderKey: string;\n readonly cause: string;\n }\n | { readonly paramName: string; readonly expectedType: string; readonly got: unknown }\n | { readonly paramName: string; readonly got: unknown }\n | { readonly missingParams: readonly string[] }\n | { readonly cycle: string }\n | {\n readonly localId: number;\n readonly component: string;\n readonly field: string;\n readonly index: number;\n readonly refsLength: number;\n }\n | { readonly vertexCount: number; readonly floatsPerVertex: number }\n // feat-20260622 verify r1: sub-asset load-failure breadcrumb in structured\n // form. The recursive loader composes the same provenance into the `.hint`\n // string; this variant additionally exposes it for property access so AI\n // users locate the broken edge without parsing the hint (charter P3,\n // requirements section error-self-recovery). `sourceField`/`sceneEntityId`\n // mirror the originating AssetRef edge; both undefined for transitive\n // (texture) edges with no per-entity origin (D-2).\n | {\n readonly referencedByGuid: string;\n readonly referencedByKind: string;\n readonly subAssetGuid: string;\n readonly sceneEntityId?: number;\n readonly sourceField?: {\n readonly componentName?: string;\n readonly fieldName: string;\n readonly arrayIndex?: number;\n };\n };\n\n// === Image importer error model SSOT ===\n// ImageErrorDetailByCode owns the closed vocabulary and payload shapes. The\n// envelope and runtime constructors are derived from it so `.code` and\n// `.detail` stay correlated for consumers.\n\n/** Closed code union, derived from the detail map below. */\ninterface ImageErrorDetailByCode {\n 'image-decode-failed': {\n readonly reason: string;\n readonly path?: string;\n };\n 'image-format-unsupported': {\n readonly actualMime: string;\n readonly path?: string;\n readonly formatColorSpaceConflict?: {\n readonly format: string;\n readonly colorSpace: 'srgb' | 'linear';\n readonly expected: 'srgb' | 'linear';\n };\n };\n 'image-dimension-out-of-bounds': {\n readonly requested: { readonly width: number; readonly height: number };\n readonly limit: number;\n };\n 'image-meta-missing': {\n readonly sourcePath: string;\n readonly expectedSidecarPath: string;\n };\n 'image-hdr-decode-failed': {\n readonly reason: string;\n readonly path?: string;\n };\n 'atlas-empty-input': {\n readonly receivedCount: number;\n };\n 'atlas-size-exceeded': {\n readonly name: string;\n readonly width: number;\n readonly height: number;\n readonly maxAtlasSize: number;\n };\n 'atlas-region-mismatch': {\n readonly name: string;\n readonly regionsTotalPixels: number;\n readonly atlasPixels: number;\n };\n}\n\nexport type ImageErrorCode = keyof ImageErrorDetailByCode;\n\n/** Detail union projected from one code-to-payload map. */\nexport type ImageErrorDetailFor<C extends ImageErrorCode> = Readonly<{ code: C }> &\n ImageErrorDetailByCode[C];\n\nexport type ImageErrorDetail = {\n [C in ImageErrorCode]: ImageErrorDetailFor<C>;\n}[ImageErrorCode];\n\n/**\n * Correlated error envelope. `ImageErrorFor<C>` is the producer-facing\n * generic; `ImageError` is its closed union for exhaustive consumer switches.\n */\nexport type ImageErrorFor<C extends ImageErrorCode> = Error & {\n readonly code: C;\n readonly expected: string;\n readonly hint: string;\n readonly detail: ImageErrorDetailFor<C>;\n};\n\nexport type ImageError = {\n [C in ImageErrorCode]: ImageErrorFor<C>;\n}[ImageErrorCode];\n\n/**\n * Per-code `.hint` string literals SSOT (plan-strategy section 2.3 / Tier 2\n * documentation). `Record<ImageErrorCode, string>` ensures compile-time\n * completeness; any future minor add to `ImageErrorCode` raises a TS error\n * on this map until the matching hint is supplied (charter proposition 4\n * explicit failure -- producer/consumer + reviewer all see the missing arm).\n *\n * Each hint embeds an executable command so AI users self-recover by\n * copy-pasting the hint into the shell (plan-strategy Tier 2 \"hint must\n * carry forgeax-engine-remote-image import <path>\" or similar; the image\n * plugin bin lands in feat-future-console-plugin-image — until then the\n * hint references the in-package importer surface).\n */\nexport const IMAGE_ERROR_HINTS: Readonly<Record<ImageErrorCode, string>> = {\n 'image-decode-failed':\n 'check file integrity; re-export from DCC tool (Photoshop / GIMP / Aseprite); dimensions > 0 + valid PNG / JPG header bytes',\n 'image-format-unsupported':\n 'supports PNG / JPG / TGA true-color sources; convert unsupported formats with: magick convert <input> <output>.png; check importSettings.colorSpace consistency with format family if formatColorSpaceConflict present',\n 'image-dimension-out-of-bounds':\n 'downscale source under device caps (typical maxTextureDimension2D = 8192 / 16384); use mipmap chain instead of larger source if lod is the goal',\n 'image-meta-missing': 'run: forgeax-engine-remote-asset import <path>',\n 'image-hdr-decode-failed':\n 'check .hdr file integrity; verify Radiance RGBE header magic (#?RADIANCE) and FORMAT=32-bit_rle_rgbe header field; ensure file was not truncated',\n // feat-20260521-sprite-atlas-animation M1 T-02 — atlas hook hint strings\n // (plan-strategy section 2 D-2). Each hint embeds an executable recovery\n // path so AI users self-repair by copy-pasting the hint into the shell\n // or into the build config (charter P3 explicit failure + AGENTS.md\n // Error model \"hint must carry executable recovery\").\n 'atlas-empty-input':\n 'verify forgeax-engine-remote-asset atlas --input <glob> --name <prefix> --output <dir> matches at least 1 PNG on disk; run `ls <glob>` to inspect the resolved file set; add the missing sprite source or fix the glob pattern',\n 'atlas-size-exceeded':\n 'downscale the source PNG so width * height <= maxAtlasSize^2 (default 4096); or split sprites across multiple atlas runs (forgeax-engine-remote-asset atlas --input <subset-glob> --name <other-prefix> --output <dir>); or raise the cap via --max-atlas-size 8192 if device caps allow it',\n 'atlas-region-mismatch':\n 'shelfPack returned regions exceeding atlas footprint — packer safety net; file a forgeax-engine bug; rerun forgeax-engine-remote-asset atlas with a smaller input set or lower --max-atlas-size as temporary recovery',\n};\n\n/**\n * Image color-space discriminator (plan-strategy section 2.5 D Open Q-4 (c)).\n *\n * `'srgb'` -- baseColor / albedo authored in sRGB display space; uploaded\n * with `format='*-srgb'` so hardware applies the gamma decode automatically\n * (research F-3 spec guarantee for mipmap blits).\n *\n * `'linear'` -- normal / metallic / roughness / data textures authored in\n * linear color space; uploaded with `format='*-unorm'` (no gamma transform).\n */\nexport type ImageColorSpace = 'srgb' | 'linear';\n\n/**\n * Image importer settings POD (plan-strategy section 2.2 D-4 image disk\n * schema; AC-26). 5-field free-form object persisted into the `*.meta.json`\n * `importSettings` field; the GUID lives at the top of the POD so consumers\n * (importer + runtime + console asset import) share one schema (charter\n * proposition 5 consistent abstraction).\n *\n * Fields:\n * - `guid` -- string-form RFC 4122 dash-form UUID identifying the single\n * image sub-asset (image disk schema is currently single-sub-asset by\n * design; cubemap face / array layer reserved for future feat).\n * - `colorSpace` -- `'srgb' | 'linear'` (drives uploadTexture format\n * selection; plan-strategy section 2.5).\n * - `mipmap` -- `'auto' | 'none'` (`'auto'` enables runtime mipmap-generator\n * blit chain; `'none'` ships a single mip level).\n * - `addressMode` -- WGPU address mode for sampler (passed through to\n * uploadTexture's sampler descriptor).\n * - `filterMode` -- magFilter / minFilter selector.\n *\n * The shape stays free-form `Record<string, unknown>` compatible at the\n * `*.meta.json` `importSettings` slot (research F-9 -- meta.schema.json\n * does not lock importSettings sub-shape; minor add of new fields is\n * non-breaking; plan-strategy R5 risk-free).\n */\nexport interface ImageMeta {\n readonly guid: string;\n readonly colorSpace: ImageColorSpace;\n readonly mipmap: 'auto' | 'none';\n readonly addressMode: 'repeat' | 'clamp-to-edge' | 'mirror-repeat';\n readonly filterMode: 'nearest' | 'linear';\n /** Optional asset-owned cooked-payload target dimension. */\n readonly downscaleMaxDimension?: number;\n}\n\n/**\n * Decoded image POD (plan-strategy section 2.2 + section 3.3; AC-26).\n * Producer: `@forgeax/engine-image` parseImage / decodeImageFromFile.\n * Consumer: `@forgeax/engine-runtime` AssetRegistry.uploadTexture (M3).\n *\n * Six-field tight-packed shape:\n * - `bytes` -- decoded pixel buffer (RGBA tight-packed; 4 bytes per pixel).\n * Producer guarantees `bytes.length === width * height * 4`.\n * - `width` / `height` -- pixel dimensions (must satisfy device caps\n * `maxTextureDimension2D` floor; surfaced as `image-dimension-out-of-bounds`\n * when exceeded).\n * - `mime` -- discriminator over the supported set (`'image/jpeg' | 'image/png' | 'image/x-tga'`);\n * keeps the runtime side from sniffing magic bytes.\n * - `colorSpace` -- carried over from `ImageMeta.colorSpace`; uploadTexture\n * asserts `format <-> colorSpace` consistency at the GPU upload entry\n * (plan-strategy section 2.5 D Open Q-4 (c)).\n * - `mipmap` -- boolean derived from `ImageMeta.mipmap === 'auto'`; the\n * runtime mipmap-generator skips the blit chain when false.\n *\n * Math-free POD; no Float32Array / branded handle on this shape (charter\n * proposition 5 consistent abstraction with TextureAsset POD).\n */\nexport interface DecodedImage {\n readonly bytes: Uint8Array;\n readonly width: number;\n readonly height: number;\n readonly mime: 'image/jpeg' | 'image/png' | 'image/x-tga';\n readonly colorSpace: ImageColorSpace;\n readonly mipmap: boolean;\n}\n\n// === AssetGuid — disk-layer GUID brand type (feat-20260513-guid-asset-package-system) ===========\n//\n// Type-only declaration. Implementation (parse / format / equals / random) lives in\n// @forgeax/engine-pack/guid. The brand field is a phantom string literal that prevents\n// accidental assignment from plain Uint8Array or string at compile time.\n\n/** 16-byte UUID brand for disk-layer asset identification. RFC 4122 UUIDv7 wire form. */\nexport type AssetGuid = Uint8Array & { readonly __guidBrand: 'AssetGuid' };\n\n// === PackErrorCode / PackErrorDetail — disk-layer error SSOT (feat-20260513-guid-asset-package-system w15) ===\n//\n// Decision anchors:\n// - requirements §6.1 (13-member closed union literal set SSOT; widened from\n// the original 8 by feat-20260523-shader-template-instance-split (+1) and\n// feat-20260608-scene-nesting-ecs-fication M1 / w8 (+4))\n// - requirements §6.2 AC-05/07 (per-code discriminated detail)\n// - plan-strategy §D-5 (PackError 4-field surface + check-pack-error-detail-narrowed.mjs guard)\n// - AGENTS.md §Error model (structurally parallel to AssetErrorCode / InspectorErrorCode)\n\n/**\n * Closed PackErrorCode union — 15 members.\n * Used exclusively by the @forgeax/engine-pack scanner fail-fast chain.\n *\n * | code | trigger |\n * |:--|:--|\n * | `'pack-malformed-meta'` | .meta.json fails ajv schema validation |\n * | `'pack-malformed-pack'` | .pack.json fails ajv schema validation |\n * | `'pack-guid-malformed'` | a GUID field is not a valid 36-char RFC 4122 dash-form string |\n * | `'pack-orphan-meta'` | .meta.json exists but the corresponding source file does not |\n * | `'pack-meta-missing'` | source file exists but no .meta.json (strict mode) |\n * | `'pack-guid-collision'` | two .pack.json files declare the same GUID |\n * | `'pack-cyclic-reference'` | asset refs[] (incl. mount.source) form a cycle |\n * | `'pack-subasset-index-out-of-range'` | subAsset.sourceIndex >= source count |\n * | `'payload-schema-mismatch'` | material payload fails materialShader / paramSchema schema |\n * | `'pack-mount-localid-overlap'` | mount memberFirst windows overlap or collide with entities[] |\n * | `'pack-mount-count-mismatch'` | mount memberCount disagrees with referenced child SceneAsset |\n * | `'pack-mount-override-localid-out-of-range'` | override.localId outside the mount's member window |\n * | `'pack-mount-override-unknown-field'` | override.comp / override.field unknown to the schema vocab |\n * | `'pack-unknown-path'` | @name references a name not declared in package.json#forgeax.assets.paths |\n * | `'pack-malformed-path-ref'` | source starts with @ but does not match @<name>/<rest> format, or resolves to a path outside the declared directory |\n *\n * Membership history: 8 -> 9 added 'payload-schema-mismatch' (feat-20260523\n * shader-template-instance-split); 9 -> 13 adds the four mount-* codes\n * (feat-20260608-scene-nesting-ecs-fication M1 / w8, plan-strategy D-8 literals\n * locked); 13 -> 15 adds 'pack-unknown-path' + 'pack-malformed-path-ref'\n * (feat-20260625-asset-meta-source-mount-prefix M1 / w1).\n */\nexport type PackErrorCode =\n | 'pack-malformed-meta'\n | 'pack-malformed-pack'\n | 'pack-guid-malformed'\n | 'pack-orphan-meta'\n | 'pack-meta-missing'\n | 'pack-guid-collision'\n | 'pack-cyclic-reference'\n | 'pack-subasset-index-out-of-range'\n // === 1 new code (feat-20260523-shader-template-instance-split M1-T02) ===\n | 'payload-schema-mismatch'\n // === 4 new codes (feat-20260608-scene-nesting-ecs-fication M1 / w8;\n // plan-strategy D-8 literals locked) ===\n | 'pack-mount-localid-overlap'\n | 'pack-mount-count-mismatch'\n | 'pack-mount-override-localid-out-of-range'\n | 'pack-mount-override-unknown-field'\n // === 2 new codes (feat-20260625-asset-meta-source-mount-prefix M1 / w1) ===\n | 'pack-unknown-path'\n | 'pack-malformed-path-ref';\n\n/**\n * Discriminated detail union for PackError — narrowed per PackError.code.\n * AI users access `err.detail.<field>` directly after switch (err.code) narrows\n * the variant. Variants without an own `code` field are narrowed exclusively by\n * the top-level `PackError.code` discriminant (the legacy 7 variants below);\n * variants that carry a `code` field (`payload-schema-mismatch`, the evolved\n * `pack-cyclic-reference`, and the four mount-* additions) double-narrow via\n * `Extract<PackErrorDetail, { code: ... }>` at the type layer (R10).\n *\n * Structurally parallel to RhiErrorDetail / MetricErrorDetail.\n */\nexport type PackErrorDetail =\n | {\n /** Absolute or relative path to the malformed .meta.json file. */\n readonly path: string;\n /** ajv validation errors produced by validateMeta(). */\n readonly ajvErrors: readonly { readonly instancePath: string; readonly message: string }[];\n }\n | {\n /** Absolute or relative path to the malformed .pack.json file. */\n readonly path: string;\n /** ajv validation errors produced by validatePack(). */\n readonly ajvErrors: readonly { readonly instancePath: string; readonly message: string }[];\n /**\n * Optional human-readable reason category. Set to a fixed literal for\n * the runtime instantiate-path SceneEntity field-name typo route:\n * `'unknown component field'` (requirements §AC-08(b)). Absent on\n * scanner-path ajv-validation failures (where the structural\n * ajvErrors[].message string already carries the diagnostic).\n */\n readonly reason?: string;\n }\n | {\n /** The raw string value that failed UUID validation. */\n readonly raw: string;\n /** Human-readable reason (e.g. 'expected 36-char RFC 4122 dash-form UUID'). */\n readonly reason: string;\n }\n | {\n /** Path of the .meta.json that has no corresponding source file. */\n readonly metaPath: string;\n /** Path that was expected to exist as a source file. */\n readonly expectedFile: string;\n }\n | {\n /** Path of the source file that has no accompanying .meta.json. */\n readonly filePath: string;\n }\n | {\n /** The two .pack.json paths that both declare the same GUID (tuple, always length 2). */\n readonly paths: readonly [string, string];\n /** The colliding GUID dash-form string. */\n readonly guid: string;\n }\n // === Evolved variant (feat-20260608-scene-nesting-ecs-fication M1 / w8;\n // plan-strategy R10): pack-cyclic-reference now carries `code` + `kind` so\n // the build-time scanner (kind: 'mount-asset', cycle: GUID list) and the\n // runtime ChildOf detector (kind: 'childof', cycle: LocalEntityId list) stay\n // narrowable from a single error code. ===\n | {\n readonly code: 'pack-cyclic-reference';\n /**\n * Cycle origin tag: 'childof' for runtime ChildOf relationship cycles\n * (LocalEntityId stringified), 'mount-asset' for build-time\n * SceneAsset.mounts[].source GUID cycles (D-1).\n */\n readonly kind: 'childof' | 'mount-asset';\n /** Cycle path as ordered identifier strings; first === last. */\n readonly cycle: readonly string[];\n }\n | {\n /** Path of the .meta.json declaring the out-of-range sourceIndex. */\n readonly metaPath: string;\n /** The declared sourceIndex value. */\n readonly sourceIndex: number;\n /** The maximum valid sourceIndex (exclusive upper bound = source count). */\n readonly max: number;\n }\n // === 1 new variant (feat-20260523-shader-template-instance-split M1-T02) ===\n | {\n /** Discriminated code for material payload schema mismatch. */\n readonly code: 'payload-schema-mismatch';\n /** GUID of the offending material asset. */\n readonly guid: string;\n /** ajv validation errors for the material payload. */\n readonly errors: readonly { readonly instancePath: string; readonly message: string }[];\n }\n // === 4 new variants (feat-20260608-scene-nesting-ecs-fication M1 / w8;\n // plan-strategy D-8 literals locked; AC-04 / AC-05 / AC-06 / AC-07) ===\n | {\n readonly code: 'pack-mount-localid-overlap';\n /** Overlapping LocalEntityId values (sorted ascending). */\n readonly overlapping: readonly number[];\n /**\n * Source labels for the conflicting windows. Each entry is a\n * human-readable origin string (`mount[<localId>]`,\n * `entities[<localId>]`, etc.) of length matching `overlapping[]`.\n */\n readonly sources: readonly string[];\n }\n | {\n readonly code: 'pack-mount-count-mismatch';\n /** localId of the offending mount within its parent SceneAsset. */\n readonly mountLocalId: number;\n /** memberCount declared on the mount. */\n readonly declared: number;\n /** Actual entities[].length resolved from the referenced child SceneAsset. */\n readonly actual: number;\n }\n | {\n readonly code: 'pack-mount-override-localid-out-of-range';\n /** override.localId that fell outside the mount's member window. */\n readonly overrideLocalId: number;\n /** localId of the parent mount. */\n readonly mountLocalId: number;\n /** memberCount of the parent mount (window upper bound, exclusive). */\n readonly memberCount: number;\n }\n | {\n readonly code: 'pack-mount-override-unknown-field';\n /** Component name on which the override was authored. */\n readonly comp: string;\n /** Unknown field name. */\n readonly field: string;\n /** localId of the parent mount carrying the override. */\n readonly mountLocalId: number;\n }\n // === 2 new variants (feat-20260625-asset-meta-source-mount-prefix M1 / w1) ===\n | {\n readonly code: 'pack-unknown-path';\n /** The @name that was not found in package.json#forgeax.assets.paths. */\n readonly pathName: string;\n /** All known path names declared in package.json#forgeax.assets.paths. */\n readonly knownNames: readonly string[];\n }\n | {\n readonly code: 'pack-malformed-path-ref';\n /**\n * Which malformation occurred, so an AI user can branch on a property\n * instead of parsing the human-facing .hint:\n * - 'format': source starts with @ but does not match @<name>/<rest>\n * - 'escape': rest segment resolves outside the declared path directory\n */\n readonly reason: 'format' | 'escape';\n /** The raw source string as written in the .meta.json. */\n readonly rawSource: string;\n /** The expected format description for self-correction. */\n readonly expectedFormat: string;\n };\n\n/**\n * Per-code .hint string literals SSOT.\n * Record<PackErrorCode, string> ensures compile-time completeness.\n */\nexport const PACK_ERROR_HINTS: Readonly<Record<PackErrorCode, string>> = {\n 'pack-malformed-meta':\n 'check guid is a valid RFC 4122 UUID; validate with: ajv validate -s schema/meta.schema.json -d <file>',\n 'pack-malformed-pack':\n 'check all asset guid and refs[] fields are 36-char dash-form UUIDs; validate with pack.schema.json',\n 'pack-guid-malformed':\n 'use AssetGuid.random() or a UUIDv7 generator; all GUID fields must be 36-char RFC 4122 dash-form',\n 'pack-orphan-meta': 'remove the orphan .meta.json or add the missing source file next to it',\n 'pack-meta-missing':\n 'run forgeax-engine-remote-asset scan --roots <dir> to list source files without .meta.json',\n 'pack-guid-collision':\n 'run forgeax-engine-remote-asset verify to list all GUID collisions; each GUID must be globally unique',\n 'pack-cyclic-reference':\n 'run forgeax-engine-remote-asset verify to print the cycle path; break the cycle by removing a refs[] entry',\n 'pack-subasset-index-out-of-range':\n 'check subAssets[].sourceIndex does not exceed the actual sub-image count in the source file',\n // === 1 new hint (feat-20260523-shader-template-instance-split M1-T02) ===\n 'payload-schema-mismatch':\n 'material asset payload failed schema validation; check paramSchema entries all use valid types from MATERIAL_PARAM_TYPES and materialShader is a non-empty string',\n // === 4 new hints (feat-20260608-scene-nesting-ecs-fication M1 / w8;\n // plan-strategy D-8) ===\n 'pack-mount-localid-overlap':\n 'check parent SceneAsset.mounts[].memberFirst windows do not overlap with each other or with entities[].localId; rebuild mount sidecar after the child SceneAsset reimport',\n 'pack-mount-count-mismatch':\n 'mount.memberCount must equal the referenced child SceneAsset totalSlots (entities.length + sum(mounts[].memberCount) + mounts.length); rebuild mount sidecar via forgeax-engine-remote-asset verify <dir> after the child SceneAsset reimport',\n 'pack-mount-override-localid-out-of-range':\n 'override.localId must be in [0, mount.memberCount); shrink the override or extend memberCount to match the child SceneAsset',\n 'pack-mount-override-unknown-field':\n 'override.comp / override.field must match a defined component schema; check defineComponent registry or rebuild mount sidecar after the child SceneAsset reimport',\n // === 2 new hints (feat-20260625-asset-meta-source-mount-prefix M1 / w1) ===\n 'pack-unknown-path':\n 'the @name in source is not declared in package.json#forgeax.assets.paths; add it there or use a known name from the list in error.detail.knownNames',\n 'pack-malformed-path-ref':\n 'source must be @<name>/<rest> where name is a key in package.json#forgeax.assets.paths; the resolved path must not escape the declared directory',\n};\n\n// === AudioErrorCode / AudioError / AudioErrorDetail -- audio error SSOT (feat-20260527-audio-system M1 / w4) ===\n//\n// Decision anchors:\n// - requirements S-8 (5-member independent closed union: context-creation-failed /\n// decode-failed / context-suspended / invalid-clip-handle / bus-not-found)\n// - requirements AC-13 (AudioErrorCode closed union switch exhaustiveness)\n// - plan-strategy D-7 (AudioErrorCode SSOT in engine-types, parallel to\n// ImageErrorCode / GltfErrorCode / AssetErrorCode)\n// - plan-strategy section 8 AI User Affordance (structured 4-field surface:\n// .code / .expected / .hint / .detail)\n// - charter P3 (explicit failure: switch (err.code) exhaustive without default;\n// .hint provides concrete recovery action)\n// - charter P4 (consistent abstraction: structurally parallel to AssetError,\n// ImageError, GltfError same 4-field shape)\n// - architecture-principles #1 SSOT (the 5 literals + class shape + hints table\n// live here once; engine-audio package references this module)\n\n/**\n * Closed `AudioErrorCode` union -- 5 members (plan-strategy D-7;\n * requirements S-8). Exhaustive `switch (err.code)` needs no default\n * fallback -- TypeScript guards union completeness at compile time\n * (charter P3 explicit failure).\n *\n * Domain-separated from `AssetErrorCode` (runtime registry surface, 12 members)\n * and `GltfErrorCode` (importer surface, 13 members). AI users face these 5\n * alternatives at the audio engine surface (`@forgeax/engine-audio`\n * AudioError + `@forgeax/engine-audio-webaudio` backend).\n *\n * | code | trigger |\n * |:--|:--|\n * | `'context-creation-failed'` | `new AudioContext()` threw or returned null (privacy browser / no audio device) |\n * | `'decode-failed'` | `decodeAudioData(arrayBuffer)` rejected (corrupt file / unsupported codec) |\n * | `'context-suspended'` | `play()` called while AudioContext.state is `'suspended'` and gesture listener failed to resume |\n * | `'invalid-clip-handle'` | AudioSource.clip handle is dangling or refers to an unregistered asset |\n * | `'bus-not-found'` | AudioSource.bus refers to a string literal outside the `'sfx' | 'music'` closed set |\n */\nexport type AudioErrorCode =\n | 'context-creation-failed'\n | 'decode-failed'\n | 'context-suspended'\n | 'invalid-clip-handle'\n | 'bus-not-found';\n\n/**\n * Per-code `AudioError` detail shapes -- discriminated payloads narrowed\n * by `AudioError.code` so AI users writing `switch (err.code)` get\n * control-flow-tightened access to the relevant detail fields\n * (charter P3 explicit failure).\n */\n\n/** `context-creation-failed` payload: carries the original error reason. */\nexport interface AudioCtxCreationFailedDetail {\n readonly code: 'context-creation-failed';\n readonly reason: string;\n}\n\n/** `decode-failed` payload: carries the original decode error reason. */\nexport interface AudioDecodeFailedDetail {\n readonly code: 'decode-failed';\n readonly reason: string;\n}\n\n/** `context-suspended` payload: empty marker detail (no extra fields). */\nexport interface AudioCtxSuspendedDetail {\n readonly code: 'context-suspended';\n}\n\n/** `invalid-clip-handle` payload: carries the dangling handle identifier. */\nexport interface AudioInvalidClipHandleDetail {\n readonly code: 'invalid-clip-handle';\n readonly clipHandleId: number;\n}\n\n/** `bus-not-found` payload: carries the invalid bus name attempted. */\nexport interface AudioBusNotFoundDetail {\n readonly code: 'bus-not-found';\n readonly attemptedBus: string;\n}\n\n/**\n * Discriminated detail union for `AudioError`, narrowed per `AudioError.code`.\n * AI users obtain the concrete detail shape via `switch (err.code)` without\n * needing a fallback `as` cast (charter P3).\n */\nexport type AudioErrorDetail =\n | AudioCtxCreationFailedDetail\n | AudioDecodeFailedDetail\n | AudioCtxSuspendedDetail\n | AudioInvalidClipHandleDetail\n | AudioBusNotFoundDetail;\n\n/**\n * Structured audio error -- four-field surface (`.code` / `.expected` /\n * `.hint` / `.detail`) structurally parallel to `@forgeax/engine-types`\n * `AssetError` + `ImageError` + `GltfError` same-shape errors\n * (charter P4 consistent abstraction; AGENTS.md \"Errors are structured.\n * Return Result, never throw for expected failures.\").\n *\n * AI users consume the structured triple via property access:\n * `switch (err.code) { case 'decode-failed': ... err.hint ... }`\n * -- never by parsing `.message` (charter P3 explicit failure red line).\n *\n * The `.message` field is auto-composed for human stack traces and carries\n * the same content as `.code` + `.expected` + `.hint`; AI users prefer\n * field access on the structured triple.\n *\n * @example AI-user exhaustive switch on the 5 members (no default fallback)\n * ```ts\n * import { AudioError, type AudioErrorCode } from '@forgeax/engine-types';\n *\n * function recover(code: AudioErrorCode): string {\n * switch (code) {\n * case 'context-creation-failed': return 'check browser supports AudioContext';\n * case 'decode-failed': return 'ensure audio file is a valid wav/mp3/ogg/flac';\n * case 'context-suspended': return 'call play after user gesture to trigger resume';\n * case 'invalid-clip-handle': return 'verify clip was registered via AssetRegistry';\n * case 'bus-not-found': return 'use sfx or music bus literal';\n * }\n * }\n * ```\n */\nexport class AudioError extends Error {\n readonly code: AudioErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail?: AudioErrorDetail;\n\n constructor(args: {\n code: AudioErrorCode;\n expected: string;\n hint: string;\n detail?: AudioErrorDetail;\n }) {\n super(`[AudioError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);\n this.name = 'AudioError';\n this.code = args.code;\n this.expected = args.expected;\n this.hint = args.hint;\n if (args.detail !== undefined) {\n this.detail = args.detail;\n }\n }\n}\n\n/**\n * Per-code `.hint` string literals SSOT (plan-strategy D-7 lock-in).\n * Exported so engine-audio error helpers and tests consume the same SSOT\n * -- any drift here updates both producer call sites and the AGENTS.md\n * Error model table.\n *\n * The shape is a `Record<AudioErrorCode, string>` so future additions to\n * the closed union are a compile-time error here as well (reinforces\n * charter P3 explicit failure). Each hint embeds an executable recovery\n * action so AI users self-repair (charter P3).\n */\nexport const AUDIO_ERROR_HINTS: Readonly<Record<AudioErrorCode, string>> = {\n 'context-creation-failed':\n 'check browser supports AudioContext; verify no privacy extension blocks audio; try reloading the page after user gesture',\n 'decode-failed':\n 'ensure audio file is a valid wav/mp3/ogg/flac at the GUID path; check file integrity (truncated or empty bytes)',\n 'context-suspended':\n 'call play after a user gesture (click/tap/keydown) to trigger AudioContext.resume(); if in iframe check sandbox attribute',\n 'invalid-clip-handle':\n 'verify clip was registered via AssetRegistry.register() before spawning AudioSource; inspect active handles via assetRegistry.inspect()',\n 'bus-not-found':\n \"use 'sfx' or 'music' bus literal; custom bus names are not supported in v1 (OOS-2)\",\n};\n\n// === PhysicsErrorCode / PhysicsError / PhysicsErrorDetail -- physics error SSOT (feat-20260528-rapier-physics-2d-3d M1 / t6; extended feat-20260617-kinematic M1) ===\n//\n// Decision anchors:\n// - requirements AC-11 (PhysicsErrorCode closed union registration + AGENTS.md update)\n// - plan-strategy D-5 (PhysicsErrorCode 9 members / PhysicsError 4-field surface / PhysicsErrorDetail discriminated)\n// - charter P3 (explicit failure: exhaustive switch without default; .hint provides recovery)\n// - charter P4 (consistent abstraction: structurally parallel to AssetError / AudioError / GltfError)\n// - architecture-principles #1 SSOT (the 9 literals + class + hints table live here once;\n// engine-physics package re-exports from here)\n\n/**\n * Closed `PhysicsErrorCode` union -- 9 members (plan-strategy D-5;\n * requirements AC-11). Exhaustive `switch (err.code)` needs no default\n * fallback -- TypeScript guards union completeness at compile time\n * (charter P3 explicit failure).\n *\n * Domain-separated from `AssetErrorCode` (runtime registry, 13 members)\n * and `AudioErrorCode` (audio engine, 5 members). AI users face these 9\n * alternatives at the physics engine surface.\n *\n * | code | trigger |\n * |:--|:--|\n * | `'wasm-load-failed'` | dynamic import() of Rapier WASM rejected (network / file not found). |\n * | `'wasm-simd-unsupported'` | WebAssembly.validate returned false for SIMD test module; compat fallback also unavailable. |\n * | `'step-failed'` | Rapier World.step threw a WASM trap (invalid body parameters / NaN values). |\n * | `'invalid-body-config'` | mass <= 0 for dynamic bodies, or other validation failure. |\n * | `'body-not-found'` | entity handle resolved to no Rapier rigid body (no RigidBody spawned or handle was freed). |\n * | `'collider-not-found'` | entity handle resolved to no Rapier collider (no Collider spawned or handle was freed). |\n * | `'backend-not-registered'` | PhysicsWorld resource missing from World; use createApp(canvas, { plugins: [physicsPlugin('rapier-3d')] }) or manual registration. |\n * | `'teleport-invalid-body-type'` | teleport() called on a static or kinematic body (only dynamic allowed). |\n * | `'controller-requires-kinematic'` | moveAndSlide() called on a non-kinematic body. |\n */\nexport type PhysicsErrorCode =\n | 'wasm-load-failed'\n | 'wasm-simd-unsupported'\n | 'step-failed'\n | 'invalid-body-config'\n | 'body-not-found'\n | 'collider-not-found'\n | 'backend-not-registered'\n | 'teleport-invalid-body-type'\n | 'controller-requires-kinematic';\n\n/**\n * Per-code `PhysicsError` detail shapes -- discriminated payloads narrowed\n * by `PhysicsError.code` so AI users writing `switch (err.code)` get\n * control-flow-tightened access to the relevant detail fields (charter P3).\n */\n\n/** `wasm-load-failed` payload: carries the original error reason. */\nexport interface PhysicsWasmLoadFailedDetail {\n readonly code: 'wasm-load-failed';\n readonly reason: string;\n}\n\n/** `wasm-simd-unsupported` payload: carries the detection failure reason. */\nexport interface PhysicsWasmSimdUnsupportedDetail {\n readonly code: 'wasm-simd-unsupported';\n readonly reason: string;\n}\n\n/** `step-failed` payload: carries the WASM trap reason. */\nexport interface PhysicsStepFailedDetail {\n readonly code: 'step-failed';\n readonly reason: string;\n}\n\n/** `invalid-body-config` payload: carries the violating field + value. */\nexport interface PhysicsInvalidBodyConfigDetail {\n readonly code: 'invalid-body-config';\n readonly field: string;\n readonly value: unknown;\n}\n\n/** `body-not-found` payload: carries the entity that was not found. */\nexport interface PhysicsBodyNotFoundDetail {\n readonly code: 'body-not-found';\n readonly entity: number;\n}\n\n/** `collider-not-found` payload: carries the entity that was not found. */\nexport interface PhysicsColliderNotFoundDetail {\n readonly code: 'collider-not-found';\n readonly entity: number;\n}\n\n/** `backend-not-registered` payload: carries the attempted backend name. */\nexport interface PhysicsBackendNotRegisteredDetail {\n readonly code: 'backend-not-registered';\n readonly attemptedBackend: string;\n}\n\n/** `teleport-invalid-body-type` payload: carries the entity + disallowed body type. */\nexport interface PhysicsTeleportInvalidBodyTypeDetail {\n readonly code: 'teleport-invalid-body-type';\n readonly entity: number;\n readonly bodyType: string;\n}\n\n/** `controller-requires-kinematic` payload: carries the entity + actual body type. */\nexport interface PhysicsControllerRequiresKinematicDetail {\n readonly code: 'controller-requires-kinematic';\n readonly entity: number;\n readonly bodyType: string;\n}\n\n/**\n * Discriminated detail union for `PhysicsError`, narrowed per `PhysicsError.code`.\n * AI users obtain the concrete detail shape via `switch (err.code)` without\n * needing a fallback `as` cast (charter P3).\n */\nexport type PhysicsErrorDetail =\n | PhysicsWasmLoadFailedDetail\n | PhysicsWasmSimdUnsupportedDetail\n | PhysicsStepFailedDetail\n | PhysicsInvalidBodyConfigDetail\n | PhysicsBodyNotFoundDetail\n | PhysicsColliderNotFoundDetail\n | PhysicsBackendNotRegisteredDetail\n | PhysicsTeleportInvalidBodyTypeDetail\n | PhysicsControllerRequiresKinematicDetail;\n\n/**\n * Structured physics error -- four-field surface (`.code` / `.expected` /\n * `.hint` / `.detail`) structurally parallel to `@forgeax/engine-types`\n * `AssetError` + `AudioError` + `GltfError` (charter P4 consistent abstraction).\n *\n * AI users consume the structured triple via property access:\n * `switch (err.code) { case 'wasm-load-failed': ... err.hint ... }`\n * -- never by parsing `.message` (charter P3 explicit failure red line).\n *\n * @example AI-user exhaustive switch on the 9 members (no default fallback)\n * ```ts\n * import { PhysicsError, type PhysicsErrorCode } from '@forgeax/engine-types';\n *\n * function recover(code: PhysicsErrorCode): string {\n * switch (code) {\n * case 'wasm-load-failed': return 'check network and @dimforge/rapier3d-compat';\n * case 'wasm-simd-unsupported': return 'check browser supports WASM SIMD';\n * case 'step-failed': return 'check for NaN values in transforms';\n * case 'invalid-body-config': return 'ensure mass > 0 for dynamic bodies';\n * case 'body-not-found': return 'ensure RigidBody was spawned before use';\n * case 'collider-not-found': return 'ensure Collider was spawned before use';\n * case 'backend-not-registered': return 'use createApp(canvas, { plugins: [physicsPlugin(...)] })';\n * case 'teleport-invalid-body-type': return 'only dynamic bodies can be teleported';\n * case 'controller-requires-kinematic': return 'set RigidBody.type to kinematic';\n * }\n * }\n * ```\n */\nexport class PhysicsError extends Error {\n readonly code: PhysicsErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail?: PhysicsErrorDetail;\n\n constructor(args: {\n code: PhysicsErrorCode;\n expected: string;\n hint: string;\n detail?: PhysicsErrorDetail;\n }) {\n super(`[PhysicsError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);\n this.name = 'PhysicsError';\n this.code = args.code;\n this.expected = args.expected;\n this.hint = args.hint;\n if (args.detail !== undefined) {\n this.detail = args.detail;\n }\n }\n}\n\n/**\n * Per-code `.hint` string literals SSOT (plan-strategy D-5 lock-in).\n * Exported so engine-physics error helpers and tests consume the same SSOT.\n *\n * The shape is a `Record<PhysicsErrorCode, string>` so future additions to\n * the closed union are a compile-time error here as well (reinforces\n * charter P3 explicit failure).\n */\nexport const PHYSICS_ERROR_HINTS: Readonly<Record<PhysicsErrorCode, string>> = {\n 'wasm-load-failed':\n 'dynamic import() of Rapier WASM rejected; check network, file path, and that @dimforge/rapier3d-compat is installed',\n 'wasm-simd-unsupported':\n 'WebAssembly.validate returned false for the SIMD test module; ensure browser supports WASM SIMD (Chrome 91+, Firefox 89+, Safari 16.4+)',\n 'step-failed':\n 'Rapier World.step threw a WASM trap; check for invalid body parameters or NaN values in transforms',\n 'invalid-body-config':\n 'check mass > 0 for dynamic bodies and valid shape parameters; see PhysicsError.detail.field',\n 'body-not-found':\n 'the entity handle did not resolve to a Rapier rigid body; ensure RigidBody was spawned before calling physics APIs',\n 'collider-not-found':\n 'the entity handle did not resolve to a Rapier collider; ensure Collider was spawned before calling physics APIs',\n 'backend-not-registered':\n \"PhysicsWorld resource not found; use createApp(canvas, { plugins: [physicsPlugin('rapier-3d')] }) or manually register a backend\",\n 'teleport-invalid-body-type':\n 'teleport is only valid for dynamic bodies; static and kinematic bodies have their position managed differently',\n 'controller-requires-kinematic':\n \"moveAndSlide requires a kinematic RigidBody; set the entity's RigidBody.type to 'kinematic'\",\n};\n\n// === RuntimeErrorCode - runtime-layer error code SSOT (feat-20260523-skin-skeleton-animation M0) ===\n//\n// Closed union of runtime-layer error code literals. Defined here as the\n// single source of truth for code-string discovery (charter F1: AI users\n// grep '@forgeax/engine-types' for all error code families). The error\n// classes that carry these codes live in @forgeax/engine-runtime.\n//\n// Decision anchors:\n// - requirements AC-29 (RuntimeErrorCode +6: skin-joint-count-exceeded /\n// skin-joint-despawned / skin-joint-path-unresolved /\n// skin-instances-coexist-forbidden / vertex-storage-buffer-unavailable /\n// skin-palette-overflow)\n// - plan-strategy D-12 (kebab-case + closed union)\n// - charter P3 (explicit failure: exhaustive switch without default)\n\n/** Closed union of runtime-layer error codes. */\nexport type RuntimeErrorCode =\n | 'shadow-invalid-config'\n | 'skin-joint-count-exceeded'\n | 'skin-joint-despawned'\n | 'skin-joint-path-unresolved'\n | 'skin-instances-coexist-forbidden'\n | 'vertex-storage-buffer-unavailable'\n | 'skin-palette-overflow'\n | 'material-resolved-empty-passes'\n | 'equirect-projection-failed'\n | 'mesh-ssbo-capacity-exceeded'\n | 'mesh-ssbo-ceiling-reached'\n | 'hdrp-caps-insufficient'\n | 'hdrp-light-budget-exceeded'\n | 'hdrp-index-list-overflow';\n\n// === GPUFlagsConstant namespace numeric aliases (5 *Flags + 8 Size/Index/Offset/SampleMask) ===\n//\n// One-to-one with the W3C CR §3.6 `unsigned long` definitions; runtime values are\n// surfaced by the global objects (GPUBufferUsage / GPUColorWrite / GPUMapMode /\n// GPUShaderStage / GPUTextureUsage).\n\n/** GPU buffer usage bit flags (OR combination of GPUBufferUsage.MAP_READ / COPY_SRC / ...). */\nexport type BufferUsageFlags = GPUBufferUsageFlags;\n\n/** GPU color write mask bit flags (GPUColorWrite.RED / GREEN / BLUE / ALPHA / ALL). */\nexport type ColorWriteFlags = GPUColorWriteFlags;\n\n/** GPU buffer map mode bit flags (GPUMapMode.READ / WRITE). */\nexport type MapModeFlags = GPUMapModeFlags;\n\n/** GPU shader stage bit flags (GPUShaderStage.VERTEX / FRAGMENT / COMPUTE). */\nexport type ShaderStageFlags = GPUShaderStageFlags;\n\n/** GPU texture usage bit flags (GPUTextureUsage.COPY_SRC / COPY_DST / TEXTURE_BINDING / ...). */\nexport type TextureUsageFlags = GPUTextureUsageFlags;\n\n/** GPU 32-bit unsigned size. */\nexport type Size32 = GPUSize32;\n\n/** GPU 64-bit unsigned size. */\nexport type Size64 = GPUSize64;\n\n/** GPU 32-bit unsigned index. */\nexport type Index32 = GPUIndex32;\n\n/** GPU 32-bit signed offset. */\nexport type SignedOffset32 = GPUSignedOffset32;\n\n/** GPU integer coordinate (used for texture / viewport extents). */\nexport type IntegerCoordinate = GPUIntegerCoordinate;\n\n/** GPU sample mask bit pattern. */\nexport type SampleMask = GPUSampleMask;\n\n/** GPU buffer dynamic offset. */\nexport type BufferDynamicOffset = GPUBufferDynamicOffset;\n\n/** GPU stencil value (reference / read mask / write mask). */\nexport type StencilValue = GPUStencilValue;\n\n// === String literal enum re-exports (already exported by @webgpu/types; we only alias) ===\n\n/** GPU texture format enum (e.g. 'rgba8unorm' / 'depth24plus' / ...). */\nexport type TextureFormat = GPUTextureFormat;\n\n/** GPU texture dimension ('1d' / '2d' / '3d'). */\nexport type TextureDimension = GPUTextureDimension;\n\n/** GPU texture view dimension ('1d' / '2d' / '2d-array' / 'cube' / 'cube-array' / '3d'). */\nexport type TextureViewDimension = GPUTextureViewDimension;\n\n/** GPU compare function ('never' / 'less' / 'equal' / 'less-equal' / 'greater' / 'not-equal' / 'greater-equal' / 'always'). */\nexport type CompareFunction = GPUCompareFunction;\n\n/** GPU filter mode ('nearest' / 'linear'). */\nexport type FilterMode = GPUFilterMode;\n\n/** GPU address mode ('clamp-to-edge' / 'repeat' / 'mirror-repeat'). */\nexport type AddressMode = GPUAddressMode;\n\n/** GPU vertex format enum ('float32' / 'float32x2' / ... 32 variants in total). */\nexport type VertexFormat = GPUVertexFormat;\n\n/** GPU vertex step mode ('vertex' / 'instance'). */\nexport type VertexStepMode = GPUVertexStepMode;\n\n/** GPU index format ('uint16' / 'uint32'). */\nexport type IndexFormat = GPUIndexFormat;\n\n/** GPU primitive topology ('point-list' / 'line-list' / 'line-strip' / 'triangle-list' / 'triangle-strip'). */\nexport type PrimitiveTopology = GPUPrimitiveTopology;\n\n/** GPU triangle cull mode ('none' / 'front' / 'back'). */\nexport type CullMode = GPUCullMode;\n\n/** GPU triangle front-face winding ('ccw' / 'cw'). */\nexport type FrontFace = GPUFrontFace;\n\n/** GPU stencil operation ('keep' / 'zero' / 'replace' / 'invert' / 'increment-clamp' / 'decrement-clamp' / 'increment-wrap' / 'decrement-wrap'). */\nexport type StencilOperation = GPUStencilOperation;\n\n/** GPU blend factor ('zero' / 'one' / 'src' / 'one-minus-src' / ...). */\nexport type BlendFactor = GPUBlendFactor;\n\n/** GPU blend operation ('add' / 'subtract' / 'reverse-subtract' / 'min' / 'max'). */\nexport type BlendOperation = GPUBlendOperation;\n\n/** GPU load op ('load' / 'clear'). */\nexport type LoadOp = GPULoadOp;\n\n/** GPU store op ('store' / 'discard'). */\nexport type StoreOp = GPUStoreOp;\n\n// === Shader pipeline trio SSOT (feat-20260508-shader-pipeline-mvp) =================\n//\n// Decision anchors:\n// - plan-strategy §S-7 + §S-9 (fully-explicit reflection + ShaderError 5-field top level)\n// - requirements §AC-04 (manifest 4 fields) + MVP-2.6 (manifest schema TS SSOT)\n// - research Finding 2 (reflection JSON field-mapping oracle, 9 boundary cases)\n// - charter proposition 4 (explicit failure) + proposition 5 (consistent abstraction:\n// dev-time and runtime errors share one shape)\n\n/**\n * Single shader manifest entry — trio + 4-field manifest SSOT (AC-04).\n *\n * | Field | Shape | Notes |\n * |:--|:--|:--|\n * | `hash` | `string` | content-addressable fingerprint (the on-disk key written by the plugin's `generateBundle`) |\n * | `wgsl` | `string` | WGSL source: relative path or inline literal (the plugin chooses; schema does not constrain) |\n * | `glsl` | `string \\| undefined` | GLSL placeholder (empty string or undefined within M1 scope; reserved for the non-WebGL fallback path) |\n * | `bindings` | `string` | `BindGroupLayoutDescriptor[]` serialized as a JSON string (output derived from reflection) |\n *\n * Written by `@forgeax/engine-shader-compiler`, persisted by `@forgeax/engine-vite-plugin-shader`,\n * loaded and consumed by `@forgeax/engine-shader` — the schema's single source of truth lives\n * in this package across all three sides (charter proposition 5: consistent abstraction).\n */\nexport interface ManifestEntry {\n readonly hash: string;\n readonly wgsl: string;\n readonly glsl: string | undefined;\n readonly bindings: string;\n}\n\n/**\n * Shader compile-time error-code closed union — 7 members\n * (feat-20260512-naga-oil-composition-hmr M3 T-09 extension; D-R7 / S-7 /\n * OQ-2 legacy 4 + D-08 new 3 for naga_oil composition).\n *\n * Symmetric in shape with `@forgeax/engine-rhi`'s `RhiErrorCode` closed union\n * (AGENTS.md error model); exhaustive `switch` needs no default fallback —\n * TypeScript guards union completeness at compile time (charter proposition 4\n * explicit failure / proposition 3 machine-readable union > prose).\n *\n * Evolution: minor-add (requirements §AC-08). The 4 legacy positions remain\n * byte-for-byte at the top (AGENTS.md `Evolution contract`: members can be\n * added only — no rename / delete / reorder). The 3 new members appear at the\n * bottom:\n * - `shader-import-not-found` — naga_oil `ImportNotFound` variant surfaces\n * when `#import <moduleId>::<symbol>` cannot bind to any module registered\n * through `options.imports` (plan-strategy D-08 + D-12 offset passthrough).\n * - `shader-circular-import` — TS-layer DFS (T-11 `detectCycle`) catches\n * `a -> b -> a` style import cycles before calling into the wasm composer\n * (plan-strategy D-03 path A + D-04 cycle first/last repetition form).\n * - `shader-define-conflict` — TS-layer pre-scan (T-12 `scanDefineConflicts`)\n * rejects the same `#define NAME` appearing in >=2 modules (plan-strategy\n * D-07; prevents naga_oil HashMap silent override from research R-07).\n *\n * | code | Trigger |\n * |:--|:--|\n * | `'shader-compile-failed'` | naga `parse_str` / `Validator` failed; also the fallback for any non-ImportNotFound naga_oil ComposerError variant (plan-strategy D-05 non-boolean #define value goes here, never a new 8th member). |\n * | `'compiler-init-failed'` | wasm load / `init()` failed (cold start / missing wasm artifact). |\n * | `'manifest-malformed'` | manifest.json schema validation failed (4 fields missing or JSON unparseable). |\n * | `'shader-not-found'` | `ShaderRegistry.get(hash)` hash miss. |\n * | `'shader-import-not-found'` | `#import <moduleId>` target absent from `options.imports` (or lacks `#define_import_path` header). `err.detail.importPath` + `err.detail.fromModuleId` narrow after the switch. |\n * | `'shader-circular-import'` | import dependency graph contains a cycle; `err.detail.cycle` carries the full chain with first/last repeated (D-04). |\n * | `'shader-define-conflict'` | same `#define NAME` declared in multiple modules; `err.detail.sites[]` lists each offending moduleId. |\n */\nexport type ShaderErrorCode =\n | 'shader-compile-failed'\n | 'compiler-init-failed'\n | 'manifest-malformed'\n | 'shader-not-found'\n | 'shader-import-not-found'\n | 'shader-circular-import'\n | 'shader-define-conflict'\n // === 5 new material-* codes (feat-20260523-shader-template-instance-split M1-T02) ===\n | 'material-schema-mismatch'\n | 'material-shader-not-found'\n | 'material-param-type-mismatch'\n | 'material-param-unknown'\n | 'material-param-missing-required'\n // === build-time superset gate (feat-20260613-material-paramschema-driven-binding M2 / w9) ===\n | 'material-shader-binding-mismatch';\n\n// === Shader error detail discriminated union (feat-20260512 M3 T-09 / D-08) =====\n//\n// Decision anchors:\n// - plan-strategy §2 D-08 (3 new typed variants keyed on `code`, structurally\n// parallel to `RhiErrorDetail` in `packages/rhi/src/errors.ts` lines\n// 165-189; 4 legacy members stay as prose detail for backwards compat).\n// - plan-strategy §2 D-04 (cycle first/last repetition form: ['a','b','a']).\n// - plan-strategy §2 D-12 (ImportNotFound offset passthrough when naga_oil\n// carries a source position on the inner variant).\n// - requirements §AC-08 (AGENTS.md §Error model ShaderErrorDetail row 3\n// variants) + §AC-15 (property access over string parsing).\n// - charter proposition 3 (machine-readable union > prose) + proposition 4\n// (explicit failure — narrow via `switch (err.detail.code)` after the\n// `switch (err.code)` tier).\n// - architecture-principles #1 SSOT (3 typed variants live here once;\n// producer site `packages/shader-compiler/src/error-mapper.ts` constructs\n// them verbatim; AGENTS.md §Error model table references this module).\n\n/**\n * Detail for the `shader-import-not-found` path (D-08 + D-12).\n *\n * `importPath` mirrors the bare `#import` target string (`'forgeax_pbr::brdf'`).\n * `fromModuleId` identifies the entry module that issued the unresolved\n * import; when the caller omitted `options.id`, this carries the\n * `<anonymous-entry-<hash8>>` placeholder (plan-strategy D-11). Optional\n * `offset` passes through the naga_oil inner-variant byte offset when present\n * (D-12); AI users surface this in error logs for IDE jump-to-source.\n */\nexport interface ShaderImportNotFoundDetail {\n readonly code: 'shader-import-not-found';\n readonly importPath: string;\n readonly fromModuleId: string;\n readonly offset?: number;\n}\n\n/**\n * Detail for the `shader-circular-import` path (D-08 + D-04).\n *\n * `cycle` lists the full import chain with the first and last element\n * repeated so consumers can visualise the loop at a glance\n * (`['a','b','c','a']`). The array is `readonly` so copy-out sites cannot\n * mutate the structure post-emit (charter proposition 4 explicit failure).\n */\nexport interface ShaderCircularImportDetail {\n readonly code: 'shader-circular-import';\n readonly cycle: readonly string[];\n}\n\n/**\n * Detail for the `shader-define-conflict` path (D-08 + D-07).\n *\n * `defineName` names the offending `#define NAME` literal; `sites` lists each\n * moduleId that declared it so the AI user can navigate to every duplicate\n * without re-scanning the source set (charter proposition 3 machine-readable\n * > prose).\n */\nexport interface ShaderDefineConflictDetail {\n readonly code: 'shader-define-conflict';\n readonly defineName: string;\n readonly sites: readonly { readonly moduleId: string }[];\n}\n\n/**\n * Detail for the `shader-compile-failed` path\n * (feat-small-20260513-dx-docs-types-cleanup D-9 / requirements §3.1.7 (A)).\n *\n * `compilerMessages` forwards the full 6 fields of `GPUCompilationMessage`\n * from `@webgpu/types ^0.1.69` (`message` / `type` / `lineNum` / `linePos` /\n * `offset` / `length`); the array is `readonly` so copy-out sites cannot\n * mutate the structure post-emit (charter proposition 4 explicit failure).\n * Optional `reason` carries a prose supplement when the wasm side surfaces a\n * higher-level summary alongside the raw compiler frame.\n *\n * @see RhiShaderCompileDetail in @forgeax/engine-rhi for the RhiError parallel\n * (R-7 namespace separation: `ShaderError.detail` vs `RhiError.detail` cover\n * disjoint lifecycle phases — compile-time vs async runtime dispatch — and\n * AI users distinguish them by import path).\n */\nexport interface ShaderCompileFailedDetail {\n readonly code: 'shader-compile-failed';\n readonly compilerMessages: readonly GPUCompilationMessage[];\n readonly reason?: string;\n}\n\n/**\n * Detail for the `compiler-init-failed` path\n * (feat-small-20260513-dx-docs-types-cleanup D-9 / requirements §3.1.7 (A)).\n *\n * Constructed by `@forgeax/engine-naga` when the wasm cold start fails\n * (`ensureReady()` rejects, the artefact is missing, or `init()` itself\n * throws). The `code` literal narrows `.detail` after the top-level\n * `switch (err.code)`; optional `reason` carries the wasm-side error message\n * when available (charter proposition 3 machine-readable union > prose).\n */\nexport interface ShaderInitFailedDetail {\n readonly code: 'compiler-init-failed';\n readonly reason?: string;\n}\n\n/**\n * Detail for the `manifest-malformed` path\n * (feat-small-20260513-dx-docs-types-cleanup D-9 / requirements §3.1.7 (A)).\n *\n * Constructed by `@forgeax/engine-naga` / `@forgeax/engine-shader-compiler`\n * when the shader manifest fails the 4-field schema (`{hash, wgsl, glsl,\n * bindings}`) or the JSON itself is unparseable. Optional `reason` carries\n * the schema validator or `JSON.parse` error message when available\n * (charter proposition 4 explicit failure: typed `.reason` access never\n * requires parsing `.message`).\n */\nexport interface ShaderManifestMalformedDetail {\n readonly code: 'manifest-malformed';\n readonly reason?: string;\n}\n\n// === 5 new material-* ShaderErrorDetail variants (feat-20260523-shader-template-instance-split M1-T02) ===\n//\n// Decision anchors:\n// - plan-strategy D-NewErrorCodes-Anchor (5 ShaderErrorCode + 5 detail variants in types SSOT)\n// - plan-strategy F-6 round 2 (material-schema-mismatch.mismatchKind is 4-element union:\n// schema-extra | shader-extra | type-mismatch | bg-overflow)\n// - requirements AC-12 (each new error code has structured detail)\n\n/**\n * Detail for `material-schema-mismatch` — paramSchema vs BGL mismatch at build-time.\n *\n * `mismatchKind` narrows on the 4-way mismatch category (F-6 round 2):\n * - 'schema-extra': paramSchema declares a name not in BGL\n * - 'shader-extra': BGL has a binding not in paramSchema\n * - 'type-mismatch': param type differs from BGL entry type\n * - 'bg-overflow': binding group count exceeds maxBindGroups (4) — AC-07\n *\n * Optional `expectedParam` / `actualBinding` carry the specific mismatch detail\n * for schema-extra / shader-extra / type-mismatch variants. `actualCount` /\n * `maxAllowed` populated for bg-overflow.\n */\nexport interface MaterialSchemaMismatchDetail {\n readonly code: 'material-schema-mismatch';\n readonly mismatchKind: 'schema-extra' | 'shader-extra' | 'type-mismatch' | 'bg-overflow';\n readonly materialShaderPath: string;\n readonly expectedParam?: string;\n readonly actualBinding?: number;\n readonly actualCount?: number;\n readonly maxAllowed?: number;\n}\n\n/**\n * Detail for `material-shader-not-found` — ShaderRegistry lookup miss.\n */\nexport interface MaterialShaderNotFoundDetail {\n readonly code: 'material-shader-not-found';\n readonly identifier: string;\n}\n\n/**\n * Detail for `material-param-type-mismatch` — a material value does not match\n * paramSchema expected type at runtime register.\n */\nexport interface MaterialParamTypeMismatchDetail {\n readonly code: 'material-param-type-mismatch';\n readonly paramName: string;\n readonly expectedType: string;\n readonly actualValue: unknown;\n}\n\n/**\n * Detail for `material-param-unknown` — material values contain a key not in\n * paramSchema.\n */\nexport interface MaterialParamUnknownDetail {\n readonly code: 'material-param-unknown';\n readonly paramName: string;\n}\n\n/**\n * Detail for `material-param-missing-required` — material values miss a key\n * that paramSchema declares without a default.\n */\nexport interface MaterialParamMissingRequiredDetail {\n readonly code: 'material-param-missing-required';\n readonly paramName: string;\n}\n\n/**\n * Detail for `material-shader-binding-mismatch` — vite-plugin-shader build-time\n * single-direction superset gate (feat-20260613-material-paramschema-driven-\n * binding M2 / D-9 / D-10).\n *\n * The actual reflected BGL must contain every binding emitted by\n * derive(schema); otherwise the build fails with this code. Extra bindings on\n * the actual side are tolerated (engine-injection placeholders such as shadow\n * / IBL / lightmap bind groups land at register-time).\n *\n * `expected` is the BGL entry derive(schema) emitted (the binding number +\n * resource layout the shader source must declare). `actual` is the entry the\n * reflector found at the same binding number, or `undefined` when the binding\n * is absent altogether. `expectedParam` names the paramSchema entry that\n * produced `expected` so AI users can grep the sidecar quickly. `mismatchKind`\n * narrows the failure category for AI-side branching.\n */\nexport interface MaterialShaderBindingMismatchDetail {\n readonly code: 'material-shader-binding-mismatch';\n readonly mismatchKind: 'binding-missing' | 'binding-type-mismatch';\n readonly materialShaderPath: string;\n readonly expected: BindGroupLayoutEntry;\n readonly actual?: BindGroupLayoutEntry;\n readonly expectedParam: string;\n}\n\n/**\n * Discriminated union of the 6 typed `.detail` variants keyed on `code`\n * (D-08 legacy 3 variants + feat-small-20260513-dx-docs-types-cleanup D-9\n * minor-add 3 variants; parallel to `RhiErrorDetail` lines 165-189 of\n * `packages/rhi/src/errors.ts`).\n *\n * AI users narrow to the per-code shape after the top-level\n * `switch (err.code)` via the nested `if (err.detail?.code === '<literal>')`\n * guard — `err.detail.compilerMessages` / `err.detail.importPath` /\n * `err.detail.reason` etc. are then typed property accesses with full IDE\n * autocomplete (charter proposition 3 machine-readable union > prose +\n * proposition 4 explicit failure).\n *\n * The 7th member `'shader-not-found'` has no typed detail variant — the naga\n * `shaderNotFound` factory leaves `.detail` undefined because the surface\n * carries no per-instance payload (the `hash` is already embedded in\n * `.message` / `.expected`; OOS-11 deferring a typed variant).\n *\n * Listed in the same order as the corresponding `ShaderErrorCode` members so\n * a reviewer can grep the two unions vertically for drift\n * (T-09 acceptance check ties `ShaderErrorDetail` grep hit to this layout).\n */\nexport type ShaderErrorDetail =\n | ShaderImportNotFoundDetail\n | ShaderCircularImportDetail\n | ShaderDefineConflictDetail\n | ShaderCompileFailedDetail\n | ShaderInitFailedDetail\n | ShaderManifestMalformedDetail\n // === 5 new material-* detail variants (feat-20260523-shader-template-instance-split M1-T02) ===\n | MaterialSchemaMismatchDetail\n | MaterialShaderNotFoundDetail\n | MaterialParamTypeMismatchDetail\n | MaterialParamUnknownDetail\n | MaterialParamMissingRequiredDetail\n // === build-time superset gate (feat-20260613-material-paramschema-driven-binding M2 / w9) ===\n | MaterialShaderBindingMismatchDetail;\n\n/**\n * Bind group layout descriptor — shape-aligned with\n * `Pick<GPUBindGroupLayoutDescriptor, 'entries' | 'label'>` (S-9 / AC-04).\n *\n * **Shape rules**:\n * - `entries` is narrowed here to a concrete `readonly BindGroupLayoutEntry[]` (the\n * spec uses `Iterable<...>`; reflection-derived output is always an array shape).\n * - All optional fields are uniformly `?: T | undefined` (guarded by\n * exactOptionalPropertyTypes).\n * - Field names match `@webgpu/types` exactly, character for character (spec-alignment rule).\n *\n * **Fully-explicit reflection JSON constraint** (plan-strategy §S-9 / D-R9):\n * the `bindings` JSON emitted by `@forgeax/engine-shader-compiler` must populate every default\n * field defined in W3C spec §5 (e.g. `hasDynamicOffset: false` / `minBindingSize: 0`);\n * `visibility` is output as the `GPUShaderStage` integer bitmask (VERTEX=0x1 /\n * FRAGMENT=0x2 / COMPUTE=0x4 OR-ed together) — string-array form is **forbidden**.\n * This type only describes the schema shape; full explicitness is enforced on the\n * producer side.\n */\nexport interface BindGroupLayoutDescriptor {\n readonly label?: string | undefined;\n readonly entries: readonly BindGroupLayoutEntry[];\n}\n\n/**\n * Single bind group layout entry — shape-aligned with\n * `@webgpu/types.GPUBindGroupLayoutEntry`.\n *\n * `binding` / `visibility` are required; the four resource layouts (buffer / sampler /\n * texture / storageTexture) form the \"exactly one set\" constraint per W3C spec §5\n * (`externalTexture` is out of scope for the forgeax MVP and is not surfaced here yet).\n */\nexport interface BindGroupLayoutEntry {\n readonly binding: GPUIndex32;\n readonly visibility: GPUShaderStageFlags;\n readonly buffer?: GPUBufferBindingLayout | undefined;\n readonly sampler?: GPUSamplerBindingLayout | undefined;\n readonly texture?: GPUTextureBindingLayout | undefined;\n readonly storageTexture?: GPUStorageTextureBindingLayout | undefined;\n}\n\n// === RemoteHandle (feat-20260629-inspector-two-layer-model M4 / w17) ========\n//\n// Decision anchors:\n// - plan-strategy secondary D-6: RemoteHandle defined in @forgeax/engine-types\n// (neutral package, no temporal coupling to @forgeax/engine-remote)\n// - requirements AC-11: app.remote typed as RemoteHandle | undefined,\n// exposed on the createApp return value for host inspection\n//\n// Shape:\n// port — number, the listen port (determined by the server on startup)\n// close — Promise<void>, tear down the server (Surface Plugin pattern\n// from startServer's returned ConsoleHandle)\n\n/**\n * Handle for a running remote eval server (feat-20260629-inspector-two-layer-model M4).\n *\n * AI users access `app.remote.port` for WS connection / status, and call\n * `await app.remote.close()` to tear down. The field is `undefined` when the\n * server is not started (production build or headless without opt-in).\n *\n * @see {@link startServer} in @forgeax/engine-remote for the producer side\n */\nexport interface RemoteHandle {\n /** Server listen port (number). Non-zero when the server is running. */\n readonly port: number;\n /** Tear down the server. Returns a Promise that resolves once the WS\n * server has closed all connections. */\n close(): Promise<void>;\n}\n\n// === Remote error model SSOT (feat-20260629-inspector-two-layer-model) ====\n//\n// Decision anchors:\n// - requirements §10.1 + §10.2 + AC-05 (`RemoteErrorCode` 5-member\n// closed union + structured `RemoteError` shape independent from RhiError /\n// ShaderError)\n// - plan-strategy §2 D-5 (rename InspectorErrorCode -> RemoteErrorCode,\n// delete inspector-write-denied, delete script-timeout, rename\n// console-* -> server-*)\n// - charter proposition 3 (machine-readable union > prose) +\n// proposition 4 (explicit failure — `switch (err.code)` is exhaustive\n// without default fallback) + proposition 5 (consistent abstraction —\n// structurally aligned with @forgeax/engine-rhi's RhiError surface)\n// - architecture-principles #1 SSOT (the 5 string literals + structured\n// structural shape live here once; @forgeax/engine-remote's runtime `RemoteError`\n// class implements this interface; consumers import the type\n// alias without dragging the runtime class through static deps —\n// parallel to the existing ShaderErrorCode pattern)\n\n/**\n * Closed `RemoteErrorCode` union — 5 members (feat-20260629-inspector-two-layer-model\n * D-5; requirements AC-05). Exhaustive `switch` needs no default\n * fallback — TypeScript guards union completeness at compile time\n * (charter proposition 4 explicit failure + proposition 3 machine-readable\n * union > prose).\n *\n * | code | trigger |\n * |:--|:--|\n * | `'script-syntax-error'` | Script body is not parseable JavaScript (SyntaxError from eval). |\n * | `'script-runtime-error'` | Script threw a non-syntax exception during execution (e.g. ReferenceError / TypeError). |\n * | `'server-startup-failed'` | The remote eval server failed to come up: WebSocketServer raised 'error' (EADDRINUSE / other listen failure), dynamic-import resolution failed, or the target package lacks the `startServer` factory. |\n * | `'server-not-running'` | CLI client's `new WebSocket('ws://localhost:<port>/inspector')` failed to connect (server not started; `app.remote` not wired in the demo). |\n * | `'eval-result-not-serializable'` | A successful eval result cannot cross the JSON-RPC wire, such as a BigInt or cyclic object. |\n *\n * **Independence from `RhiError | ShaderError` union** — `RemoteErrorCode`\n * is **not** merged into the GPU / asset error union (charter proposition 5 +\n * architecture-principles #1 SSOT). Engine-side errors stream is OOS-1\n * (errors.subscribe v2 spinoff); remote callers only face these 5\n * alternatives.\n */\nexport type RemoteErrorCode =\n | 'script-syntax-error'\n | 'script-runtime-error'\n | 'server-startup-failed'\n | 'server-not-running'\n | 'eval-result-not-serializable';\n\n/**\n * Structural shape of a forgeax remote error (feat-20260629-inspector-two-layer-model\n * D-5). Structured surface mirroring `@forgeax/engine-rhi` `RhiError`\n * (charter proposition 5 consistent abstraction; AGENTS.md \"Errors are\n * structured\"):\n *\n * - `.code` closed union member (L1 key signal; switch-able).\n * - `.expected` expected-state description (L2 detail).\n * - `.hint` actionable recovery guidance (L2 detail).\n * - `.message` auto-composed string for human stack traces (AI users\n * prefer property access on `.code` / `.expected` / `.hint`).\n * - `.name` Error name marker (`'RemoteError'`) for cross-realm\n * dispatch under JSON-RPC transport.\n *\n * This interface intentionally extends `Error` so a runtime `RemoteError`\n * **class** (defined in `@forgeax/engine-remote/errors`) satisfies the contract\n * without re-declaring the inherited `name` / `message` slots.\n *\n * AI users consume the structured triple via property access — never by\n * parsing `.message` (charter proposition 4 explicit failure red line).\n */\nexport interface RemoteError extends Error {\n readonly code: RemoteErrorCode;\n readonly expected: string;\n readonly hint: string;\n /**\n * Optional discriminated detail payload (feat-20260517 D-7). Per-code\n * variant carries structured provenance that would otherwise pollute the\n * single-line `.hint` copy. AI users narrow via `switch (err.code)`; the\n * `.detail` slot is `undefined` for codes whose discriminator has no\n * payload (charter P4 explicit failure: signal absence by type).\n */\n readonly detail?: RemoteErrorDetail;\n}\n\n/**\n * Discriminated detail union for {@link RemoteError} (feat-20260517 D-7).\n * Each variant pairs a {@link RemoteErrorCode} member with the\n * structured payload AI users need to act on the error without grepping\n * prose. Variants without payload are intentionally absent — the\n * `RemoteError.detail` slot is `undefined` for those codes.\n *\n * The `server-startup-failed` variant carries bounded startup provenance.\n */\nexport type RemoteErrorDetail = ServerStartupFailedDetail | EvalResultNotSerializableDetail;\n\n/**\n * `server-startup-failed` discriminator variant with bounded provenance.\n */\nexport interface ServerStartupFailedDetail {\n readonly code: 'server-startup-failed';\n readonly removedAt: string;\n readonly docAnchor: string;\n}\n\n/**\n * SSOT for the legacy-inspect routing hint template. Keep the recovery copy\n * executable and byte-stable for CLI and remote consumers.\n */\nexport function legacyInspectHint(legacyInspectTarget: string): string {\n return `did you mean 'forgeax-engine-remote-ecs ${legacyInspectTarget}'?`;\n}\n\n/**\n * `eval-result-not-serializable` discriminator variant. The shape is a\n * bounded classification only; it carries no part of the returned object\n * so failed transport cannot leak arbitrary engine state.\n */\nexport interface EvalResultNotSerializableDetail {\n readonly code: 'eval-result-not-serializable';\n readonly shape: 'bigint' | 'cyclic-object' | 'unsupported';\n}\n\n// === Metric registry error model SSOT (feat-20260512-threejs-pixel-parity-bench) ===\n//\n// Decision anchors:\n// - requirements §3.5 + AC-04 + AC-05 + AC-11 (`MetricErrorCode` 4-member closed\n// union elevated to TS alias; B-1 regression-prevention callout — exhaustive\n// `switch (err.code)` without `default:` must compile under tsc strict)\n// - plan-strategy §2 D-P3 (MetricErrorCode TS alias goes first in the topology;\n// M1 T-001 ships only the 4 legacy members verbatim from AGENTS.md Error\n// model table)\n// - research Finding 9 (`MetricErrorCode` currently has zero TS alias = direct\n// B-1 regression risk; §6 g9 checklist item 1: introduce\n// `export type MetricErrorCode = ...` in `packages/types/src/index.ts`,\n// structurally parallel to ShaderErrorCode / RemoteErrorCode)\n// - charter proposition 3 (machine-readable union > prose) + proposition 4\n// (explicit failure — closed-union exhaustive switch needs no default fallback;\n// tsc strict mode guards completeness) + proposition 5 (consistent abstraction —\n// structurally aligned with @forgeax/engine-rhi RhiError and RemoteError)\n// - architecture-principles #1 SSOT (the 4 string literals live here once;\n// `scripts/check-metrics-declared.mjs` / `scripts/metrics/run-all.mjs` /\n// `scripts/metrics/run-fps.mjs` are .mjs producer sites that emit the same\n// literals at throw points; parallel to ShaderErrorCode pattern)\n\n/**\n * Closed `MetricErrorCode` union — 4 members (M1 T-001 elevation of the\n * pre-existing 4 `.mjs` producer literals to a TS alias; research Finding 9\n * §6 g9 checklist item 1). Exhaustive `switch` needs no default fallback —\n * TypeScript guards union completeness at compile time (charter proposition 4\n * explicit failure + proposition 3 machine-readable union > prose).\n *\n * | code | trigger |\n * |:--|:--|\n * | `'metric-not-declared'` | a workspace member lacks `package.json#forgeax.metrics` or the declaration is not a plain object; emitted by `scripts/check-metrics-declared.mjs` + `scripts/metrics/run-all.mjs`. |\n * | `'metric-kind-unknown'` | `forgeax.metrics` contains a key not in the closed `MetricKind` union (`bundle-size` / `fps` / `bench` / `gate` / `spike-report`); typo guard via ajv `additionalProperties: false`. |\n * | `'metric-status-not-ok'` | dispatcher (bundle-size / bench / gate / fps / spike-report) returned `status !== 'ok'`; the offending `report/<package>/<kind>.json` carries the value-vs-threshold detail. |\n * | `'metric-schema-malformed'` | `forgeax-metrics.schema.json` failed to parse / compile as JSON Schema 2020-12; precondition failure surfaced by both `check-metrics-declared.mjs` and `run-all.mjs`. |\n *\n * **B-1 regression prevention** (requirements AC-05 + AC-11): an alias without\n * a TS consumer site cannot be exhaustively switched; M1 T-002 adds type-level\n * tests against this alias, and M2 evaluator + M2 runner CLI add the two\n * non-test exhaustive `switch (err.code)` consumer sites (D-P9 plan-strategy\n * decision).\n *\n * Per-feat extension to 6 members (M1 T-002, D-P3): `'pixel-parity-threshold-exceeded'`\n * + `'pixel-parity-capture-failed'` extend the alias at the bottom; AGENTS.md\n * Error model table flips from `(4)` to `(6)` in lockstep. The two new members\n * encode the double-gate of the pixel-parity bench (research Finding 10 +\n * plan-strategy D-P2): Layer A per-pixel YIQ tolerance ` perPixelThreshold` is\n * pixelmatch-internal and never raises on its own; Layer B aggregate cap\n * `threshold` raises `'pixel-parity-threshold-exceeded'`; any capture-side\n * failure (chromium launch / vite preview / readPixels / size mismatch /\n * pixelmatch internal throw) collapses into `'pixel-parity-capture-failed'`\n * with a `.detail.stage` discriminator (charter proposition 5 consistent\n * abstraction — pixelmatch internal exception does NOT get a third member;\n * see D-P3 decision rationale).\n */\nexport type MetricErrorCode =\n | 'metric-not-declared'\n | 'metric-kind-unknown'\n | 'metric-status-not-ok'\n | 'metric-schema-malformed'\n | 'pixel-parity-threshold-exceeded'\n | 'pixel-parity-capture-failed';\n\n/**\n * Per-code detail shape for the four legacy `MetricErrorCode` members\n * (`'metric-not-declared'` / `'metric-kind-unknown'` / `'metric-status-not-ok'`\n * / `'metric-schema-malformed'`).\n *\n * The four legacy `.mjs` producer sites (`scripts/check-metrics-declared.mjs`,\n * `scripts/metrics/run-all.mjs`, `scripts/metrics/run-fps.mjs`) emit textual\n * `[reason] / [hint]` lines and never carry a structured payload — they live\n * in CI-only scripts and exit 1 directly. The `.detail` slot is therefore left\n * `undefined` so AI consumers do not waste a narrowing step looking for a\n * non-existent payload (charter proposition 4 explicit failure: signal absence\n * by type).\n */\nexport interface MetricLegacyDetail {\n readonly stage?: undefined;\n}\n\n/**\n * Detail shape exclusive to the `'pixel-parity-threshold-exceeded'` path\n * (M1 T-002 / D-P11). Carries the full numeric verdict so AI users can\n * surface the value-vs-threshold delta in stderr / sticky-comment renderings\n * without parsing `.message`.\n *\n * | Field | Meaning |\n * |:--|:--|\n * | `diffPixelCount` | Aggregate count from `pixelmatch(left, right, ...)` (Layer B reading). |\n * | `diffPercent` | `diffPixelCount / (width * height)` rendered as a 0..1 float for sticky-comment formatting. |\n * | `maxChannelDelta` | Maximum per-channel uint8 delta across all differing pixels (0..255). Helps disambiguate \"many tiny diffs\" from \"few big diffs\". |\n * | `threshold` | The declared Layer B integer cap (`package.json#forgeax.metrics.bench.pixelDiff.threshold`). |\n * | `perPixelThreshold` | The Layer A `pixelmatch` per-pixel YIQ float threshold actually used; equals the declared value or the `0.1` fallback (D-P2 default semantics). |\n *\n * The exhaustive discriminator is `code === 'pixel-parity-threshold-exceeded'`\n * — AI users access `.detail.diffPixelCount` directly after the type guard\n * with full IDE autocomplete (charter proposition 3 machine-readable union >\n * prose; AI-user review F-1 IDE autocomplete affordance).\n */\nexport interface ParityThresholdDetail {\n readonly diffPixelCount: number;\n readonly diffPercent: number;\n readonly maxChannelDelta: number;\n readonly threshold: number;\n readonly perPixelThreshold: number;\n}\n\n/**\n * Detail shape exclusive to the `'pixel-parity-capture-failed'` path (M1 T-002\n * / D-P11). Carries a discriminator `.stage` that pinpoints which step of the\n * capture pipeline collapsed (charter proposition 5 consistent abstraction:\n * pixelmatch-internal throw becomes `.stage='diff'` rather than a third\n * `MetricErrorCode` member — plan-strategy D-P3 decision).\n *\n * | `.stage` | trigger |\n * |:--|:--|\n * | `'chromium-launch'` | `chromium.launch({...})` threw (research Finding 6: `--enable-unsafe-webgpu` flag still rejected on the host). |\n * | `'vite-preview'` | spawned vite preview never reached `wait-on tcp 30s` (research Finding 4 cleanup pattern). |\n * | `'pixel-readback'` | `gl.readPixels(...)` or `commandEncoder.copyTextureToBuffer(...)` failed, or `window.__captureLeft/Right` was missing. |\n * | `'size-mismatch'` | left and right `Uint8Array.length` differ; `leftSize` / `rightSize` carry the actual byte counts. |\n * | `'diff'` | `pixelmatch(left, right, ...)` itself threw (charter proposition 4 explicit failure: no silent catch; EC-06). |\n *\n * Optional `leftSize` / `rightSize` are populated for the `'size-mismatch'`\n * stage; they are absent for the other stages because the failure happened\n * before any byte count was known.\n */\nexport interface ParityCaptureDetail {\n readonly stage: 'chromium-launch' | 'vite-preview' | 'pixel-readback' | 'size-mismatch' | 'diff';\n readonly leftSize?: number;\n readonly rightSize?: number;\n /**\n * Optional human-readable cause string for the failure (typically the\n * caught `Error.message` text or an inferred reason). Aligned with ECMA\n * 2022 `Error.cause` naming convention so IDE hover invokes the same mental\n * model. Filled by `scripts/bench/pixel-parity.mjs` at every stage that\n * surfaces a non-empty message; absent when the failure is purely\n * structural (e.g. `'size-mismatch'` where `leftSize` / `rightSize` carry\n * the diagnostic payload instead).\n */\n readonly cause?: string;\n}\n\n/**\n * Non-optional detail projection carried by `MetricError`.\n *\n * `MetricError` owns the complete code-to-detail relation. `NonNullable` removes\n * only the four legacy absence markers; parity payloads and the legacy detail\n * shape remain in the public family without a second manually maintained list.\n */\nexport type MetricErrorDetail = NonNullable<MetricError['detail']>;\n\n/**\n * Structural shape of a forgeax metric error (feat-20260512 T-002).\n *\n * Three-field surface (`.code` / `.expected` / `.hint`) plus per-code-narrowed\n * `.detail`, structurally aligned with `@forgeax/engine-rhi` `RhiError` and\n * `InspectorError` (charter proposition 5 consistent abstraction; AGENTS.md\n * \"Errors are structured. Return Result, never throw for expected failures\").\n *\n * `MetricError` is a TypeScript discriminated union of 6 per-code interfaces;\n * each variant narrows `.detail` to the corresponding `MetricErrorDetail`\n * branch. AI users perform a single `switch (err.code)` and pick up\n * `.detail.diffPixelCount` (threshold-exceeded path) or `.detail.stage`\n * (capture-failed path) with full IDE autocomplete (AI-user review F-1\n * affordance; D-P11).\n *\n * - `.code` closed union member (L1 key signal; switch-able).\n * - `.expected` expected-state description (L2 detail; mirrors the `[reason]`\n * line emitted by `failStructured(...)` in the three `.mjs`\n * producer sites).\n * - `.hint` actionable recovery guidance (L2 detail; mirrors the `[hint]`\n * line in `failStructured(...)`).\n * - `.detail` path-specific structured payload narrowed per `.code`.\n *\n * AI users consume the structured triple via property access — never by\n * parsing `.message` (charter proposition 4 explicit failure red line).\n */\nexport type MetricError =\n | (MetricErrorBase & {\n readonly code: 'metric-not-declared';\n readonly detail?: MetricLegacyDetail | undefined;\n })\n | (MetricErrorBase & {\n readonly code: 'metric-kind-unknown';\n readonly detail?: MetricLegacyDetail | undefined;\n })\n | (MetricErrorBase & {\n readonly code: 'metric-status-not-ok';\n readonly detail?: MetricLegacyDetail | undefined;\n })\n | (MetricErrorBase & {\n readonly code: 'metric-schema-malformed';\n readonly detail?: MetricLegacyDetail | undefined;\n })\n | (MetricErrorBase & {\n readonly code: 'pixel-parity-threshold-exceeded';\n readonly detail: ParityThresholdDetail;\n })\n | (MetricErrorBase & {\n readonly code: 'pixel-parity-capture-failed';\n readonly detail: ParityCaptureDetail;\n });\n\n/**\n * Common base of every `MetricError` variant (D-P11 internal helper —\n * never instantiated on its own, only intersected into the per-code\n * branches of `MetricError`).\n */\ninterface MetricErrorBase {\n readonly code: MetricErrorCode;\n readonly expected: string;\n readonly hint: string;\n}\n\n// === Pack-index catalog entry POD (feat-20260517-vite-plugin-image-build-time-cook D-2) ===\n//\n// PackIndexEntry is the in-memory shape of one row in `pack-index.json` (build\n// path) and `/__pack/index` JSON response (dev path). It is the SSOT contract\n// between the build-time catalog builder (`@forgeax/engine-vite-plugin-pack`)\n// and the runtime asset loader (`@forgeax/engine-runtime` `parseAssetPayload`).\n//\n// Decision anchors:\n// - plan-strategy D-2 (5-field metadata sub-structure: width / height /\n// format / colorSpace / mipmap, mirrors TextureAsset POD field names so\n// `metadata.colorSpace` greps to the same surface across catalog / POD /\n// sidecar).\n// - plan-strategy D-5 (sidecar `mipmap: 'auto' | 'none'` is mapped to the\n// `boolean` form by the catalog builder; runtime is unaware of the\n// string token).\n// - charter P1 (progressive disclosure -- core 4 fields stay flat,\n// image-only metadata sinks into a sub-structure that texture-arm\n// consumers narrow into).\n// - charter P4 (consistent abstraction -- `metadata` field-by-field\n// mirrors `TextureAsset` POD field names; `width` / `height` / `format`\n// / `colorSpace` / `mipmap` align byte-for-byte).\n//\n// Backward compatibility (D-2 'minor' evolution):\n// - `metadata` is `?: ImageMetadata | undefined` -- legacy 4-field entries\n// emitted by older builds (or future non-texture kinds: 'mesh' / 'scene' /\n// 'material') stay valid; runtime consumers narrow on `entry.metadata !==\n// undefined` before accessing fields.\n// - The interface stays open over `kind` (string) so future 'audio' /\n// 'video' arms can join without re-typing PackIndexEntry; the texture\n// arm narrows via `entry.kind === 'texture'` + `entry.metadata`\n// existence in `parseAssetPayload`.\n\n/**\n * Metadata sub-structure carried by `PackIndexEntry` rows of `kind: 'texture'`.\n *\n * Five fields mirror `TextureAsset` POD field names (`width` / `height` /\n * `format` / `colorSpace` / `mipmap`) so AI users can grep one identifier and\n * see the same surface in catalog rows, sidecar `*.meta.json`\n * `importSettings`, and the runtime `TextureAsset` POD (charter P4 consistent\n * abstraction).\n *\n * `width` / `height` are optional because dev-mode catalog rows folded from a\n * `*.meta.json` sidecar may lack pixel dimensions until `parseImage`\n * decodes the JPG bytes; build-mode (import) rows always have them filled\n * because the import step has already run `parseImage` to produce the RGBA\n * bytes.\n *\n * `format` is `GPUTextureFormat` to align with the `TextureAsset.format`\n * field (math-free, spec-aligned with `@webgpu/types ^0.1.70`).\n *\n * `colorSpace` and `mipmap` are required because the sidecar\n * `importSettings` always carries them (D-5: `'auto'` / `'none'` string\n * tokens are mapped to `true` / `false` at the catalog builder; runtime never\n * sees the string form).\n *\n * `compression` is the build-time compression level this image artefact\n * was stored with. Loop 1 supports `'none'` (passthrough) and `'zstd'`.\n * Loop 2 may add members like `'basis-uastc'`. Absent for legacy rows.\n */\n\n/**\n * Asset compression strategy — closed literal union (SSOT, D-3 / D-9).\n *\n * Five flat, mutually-exclusive members describing how an artefact is stored:\n * - `'none'` — pass-through (uncompressed bytes)\n * - `'zstd'` — generic zstd container compression (Loop 1)\n * - `'basis-etc1s'` / `'basis-uastc'` / `'basis-uastc-hdr'` — a Basis-encoded\n * KTX2 texture (Loop 2). The `basis-*` members fully describe the delivered\n * encoding: the KTX2 container carries its own supercompression (self-\n * described by the KTX2 header) and does NOT stack an outer `'zstd'` layer\n * (mutual exclusion by construction, D-3). The `basis-` kebab prefix is\n * visually distinct from GPU texture-format literals (naming rule, §8).\n *\n * Add-only-minor: Loop 2 appends the three `basis-*` members without repainting\n * `'none'` / `'zstd'` semantics (AC-11a). A missing / `undefined` field means a\n * legacy uncompressed artefact (E1 backward-compat).\n */\nexport type AssetCompression = 'none' | 'zstd' | 'basis-etc1s' | 'basis-uastc' | 'basis-uastc-hdr';\n\nexport interface ImageMetadata {\n readonly kind: 'texture';\n readonly width?: number;\n readonly height?: number;\n readonly format: GPUTextureFormat;\n readonly colorSpace: 'srgb' | 'linear';\n readonly mipmap: boolean;\n /** Build-time compression strategy used for this image artefact. `undefined` for legacy assets. */\n readonly compression?: AssetCompression;\n /**\n * Sidecar control-plane request for the offline texture encoder (D-12).\n * `'auto'` derives the delivery encoding from `colorSpace` + HDR source;\n * `'etc1s'` / `'uastc'` force a Basis encoding; `'none'` keeps the\n * uncompressed `.bin` path. Aligns with the mipmap sidecar tri-state idiom.\n * The `'auto'` default semantics activate in M5; M3 keeps the default `'none'`.\n */\n readonly compressionMode?: 'auto' | 'etc1s' | 'uastc' | 'none';\n /** Optional asset-owned cooked-payload target dimension. */\n readonly downscaleMaxDimension?: number;\n}\n\nexport type {\n AssetAuthoringCapability,\n AssetAuthoringUnavailableReason,\n AssetBindingCapability,\n AssetBindingTarget,\n AssetPlacementCapability,\n AssetRelation,\n AssetRelationPolicy,\n AssetRelationType,\n AssetSubjectRef,\n AssetSubjectType,\n CatalogDiagnostic,\n CatalogDiagnosticSeverity,\n CatalogLifecycle,\n CatalogOperationDescriptor,\n CatalogOperationName,\n CatalogOperations,\n CatalogProjection,\n CatalogProjectionInput,\n CatalogSubject,\n CookExecution,\n ExistingOutput,\n ImportedOutputDeclaration,\n KindChange,\n MatchConflict,\n ProducerContractDiagnostic,\n ProducerContractErrorCode,\n ProducerContractResult,\n ProposedOutput,\n ProviderProvenance,\n ResourceRevision,\n ScenePublicationFence,\n SourceOverrideDescriptor,\n SourceOverrideDiagnostic,\n SourceOverrideErrorCode,\n SourceOverrideMap,\n SourceOverridePayload,\n SourceOverrideValidationResult,\n TopologyConflictReason,\n TopologyDiff,\n TopologyPreserved,\n UiAuthoringCapability,\n UiAuthoringProjection,\n} from './asset-producer';\nexport {\n authoringCapabilityForAssetKind,\n canonicalizeSourceOverrides,\n catalogOperationsFor,\n isCatalogProjectionValid,\n MESH_MATERIAL_SLOT_SOURCE_OVERRIDE_PAYLOAD_SCHEMA,\n validateSourceOverrideMap,\n} from './asset-producer';\n/**\n * One row in the pack-index catalog (`pack-index.json` for build path,\n * `/__pack/index` JSON response for dev path).\n *\n * Core fields (4) stay flat for AI users to grep one identifier:\n * - `guid`: UUIDv5/v7 lowercase string (asset identity SSOT)\n * - `packageUrl`: cooked Pack v2 package navigation URL.\n * - `kind`: closed-string discriminator (`'texture'` / `'mesh'` / `'scene'`\n * / `'material'` / future arms); narrowed by runtime `parseAssetPayload`\n * via exhaustive switch.\n * - `sourcePath`: relative path to the on-disk source artefact for\n * debugging + grep (dev: source JPG path; build: same source JPG path\n * even though `packageUrl` points to the cooked package).\n *\n * Optional 5th field:\n * - `metadata`: `ImageMetadata | undefined` -- present when `kind ===\n * 'texture'`; absent for non-texture kinds (legacy `.pack.json` entries\n * emit 4-field rows). Runtime consumers narrow with `entry.metadata !==\n * undefined` before consumption (D-2 backward-compat strategy).\n */\nexport type {\n AssetPublicationEnvelope,\n AssetPublicationEvidenceUsage,\n AssetPublicationExternalEvidence,\n AssetPublicationFailure,\n AssetPublicationFailureStage,\n AssetPublicationLocator,\n AssetPublicationOutput,\n AssetPublicationReceipt,\n AssetPublicationRecovery,\n CatalogDelta,\n CatalogDeltaValidationError,\n CatalogEntry,\n CatalogEntry as PackIndexEntry,\n CatalogEntryV2,\n CatalogRevisionPoint,\n CatalogRevisionWindow,\n} from './catalog';\nexport { catalogDeltaDigest, catalogEntryDigest, validateCatalogDelta } from './catalog';\n\n// === InspectEntry / InspectSnapshot (feat-20260618-asset-and-pack-name-fields M1 / w3) ===\n//\n// Decision anchors:\n// - plan-strategy D-9 (InspectEntry.name: string via resolveName, non-optional\n// with empty string as legal value; relocated from runtime private to types\n// for single-entry discoverability per charter F1)\n// - requirements AC-12 (inspector assets root carries resolved name per entry)\n//\n// These types were originally private interfaces in asset-registry.ts.\n// They are promoted to @forgeax/engine-types so console + future inspector\n// consumers import them from a single entry point (charter F1).\n\n/** One row in the inspector's `assets[]` snapshot (JSON-RPC over WS). */\nexport interface InspectEntry {\n readonly guid: string;\n /** Asset kind discriminant string (e.g. `'mesh'`, `'texture'`, `'scene'`). */\n readonly kind: string;\n /** Display name resolved by resolveName (empty string is legal). */\n readonly name: string;\n}\n\n/** Snapshot returned by `AssetRegistry.inspect()` -- the inspector root. */\nexport interface InspectSnapshot {\n readonly assets: ReadonlyArray<InspectEntry>;\n}\n\n// === Loader contract SSOT (feat-20260603-asset-import-loader-injection M1 / w3) ===\n//\n// Decision anchors:\n// - plan-strategy D-1 (runtime LoaderRegistry dispatches on `asset.kind`;\n// host injects loaders via `wireDefaultLoaders`, mirroring Console\n// `wireDefaultInspectors`) + D-2 (contract SSOT lives here in\n// `@forgeax/engine-types`, math-free, so `@forgeax/engine-runtime` only\n// depends on the interface, never reverse-imports a concrete loader)\n// - requirements core principle (third DIP instance after RHI / Console)\n// - charter P3 (structured failure) + P4 (consistent abstraction)\n//\n// A `Loader` is the runtime-side half of the import/load split: it turns an\n// already-imported internal artefact (a `.pack.json` payload, or fetched\n// bytes for texture / font) into an in-memory `Asset` POD. It stays pure of\n// the registry's bookkeeping — `registerWithGuid` is the AssetRegistry's job,\n// never the loader's (plan-strategy D-2).\n//\n// Two dispatch shapes share this one contract (the asymmetry is intentional,\n// matching the two pre-existing AssetRegistry load paths the M1 refactor\n// converges; research Finding 1 + Finding 2):\n// (a) inline pack-payload kinds (mesh / scene / material /\n// skeleton / skin / animation-clip) parse synchronously and return\n// `Asset | undefined` (`undefined` = parse rejected, the caller maps it\n// to a structured `AssetError`).\n// (b) upstream-branch kinds (texture / font / equirect) fetch + decode\n// asynchronously and return a `Promise<LoaderAsyncResult>` carrying either\n// the produced `Asset` POD or a structured error.\n\n/**\n * Result envelope returned by the async branch of {@link Loader.load}\n * (texture / font). Mirrors the `Result<T, E>` shape used across the engine\n * (`.ok` discriminant) but is declared math-free here so\n * `@forgeax/engine-types` need not import `@forgeax/engine-rhi`. The error is\n * left as `unknown` so the runtime can surface its own\n * `AssetError | ImageError | RhiError` union without leaking those classes\n * into the types package (charter P4 — the runtime narrows; types stays\n * dependency-free).\n */\nexport type LoaderAsyncResult<P = Asset> =\n | { readonly ok: true; readonly value: P }\n | { readonly ok: false; readonly error: unknown };\n\n/**\n * Output of {@link Loader.load}. The synchronous arm returns `Asset` (parse\n * succeeded) or `undefined` (parse rejected); the asynchronous arm returns a\n * `Promise<LoaderAsyncResult>`.\n */\nexport type LoaderOutput<P = Asset> =\n | P\n | undefined\n | { readonly ok: false; readonly error: ParseErrorDetail }\n | Promise<LoaderAsyncResult<P>>;\n\n/**\n * Capabilities the host wires into a {@link Loader} at load time. A loader\n * receives this context so it never reaches back into AssetRegistry\n * internals (pipeline isolation, architecture-principles #4).\n *\n * Exactly three capabilities (plan-strategy D-3 rationale):\n * - `fetchBinary(url)` — fetch raw bytes for the artefact (texture import\n * `.bin`, `.hdr`, source image, font pack JSON).\n * - `resolveRef(guid)` — recursively resolve a referenced sub-asset GUID to\n * its registered handle id (font atlas / sampler). Returns the raw handle\n * number so the loader can stamp it into the produced POD; the runtime\n * performs the recursive `loadByGuid` + registration underneath.\n * - `device` — opaque GPU device slot, present for future GPU-touching\n * loaders; current loaders register CPU PODs only and never touch it\n * (research Finding 3 — texture GPU upload is decoupled from load time via\n * the pull-model `GpuResourceStore`). Typed `unknown` so types stays\n * RHI-free.\n *\n * F21 (feat-20260621): the error-contextualization callback has been removed.\n */\nexport interface ParseErrorDetail {\n readonly localId: number;\n readonly component: string;\n readonly field: string;\n readonly index: number;\n readonly refsLength: number;\n}\n\n/**\n * Device texture-compression capabilities the transcode target selector reads\n * (feat-20260707 M5 / D-8, D-11).\n *\n * Three independent booleans mirror the WebGPU `texture-compression-{bc,etc2,\n * astc}` device features (and the `RhiCaps.textureCompression{Bc,Etc2,Astc}`\n * triple they are projected from — createRenderer does the one-line RhiCaps ->\n * TranscodeCaps projection). This shape is structurally identical to the codec\n * package's own `TranscodeCaps` (`@forgeax/engine-codec`): the codec keeps a\n * LOCAL copy on purpose (D-8 — codec is a pure, dependency-light transcode\n * library and must not take a `@forgeax/engine-types` edge just to name its\n * pure-function input). The runtime passes a value of this type straight into\n * `selectTranscodeTarget` by structural compatibility; there is exactly one\n * value threaded through `LoadContext`, so no fact is duplicated at runtime.\n */\nexport interface TranscodeCaps {\n readonly bc: boolean;\n readonly etc2: boolean;\n readonly astc: boolean;\n}\n\nexport interface LoadContext {\n /**\n * Fetch raw bytes for an asset artefact, with optional decompression.\n *\n * feat-20260706 M3 / w19: extended signature per D-2 — a `compression`\n * opt triggers the decompression gate inside the closure\n * (`@forgeax/engine-codec` lazy-init). `undefined` / `'none'` = E1\n * pass-through (backward-compat for legacy catalog rows).\n */\n fetchBinary(\n url: string,\n opts?: { readonly compression?: AssetCompression },\n ): Promise<\n | { readonly ok: true; readonly value: Uint8Array }\n | { readonly ok: false; readonly error: unknown }\n >;\n resolveRef(\n guid: string,\n ): Promise<\n { readonly ok: true; readonly value: number } | { readonly ok: false; readonly error: unknown }\n >;\n /**\n * feat-20260613-material-paramschema-driven-binding M4 / w22 (D-5 graceful):\n * derive(paramSchema).textureFieldNames for the given material-shader id,\n * built from the registered shader's paramSchema. Used by materialLoader\n * to know which material value fields carry refs[] indices vs scalar values\n * (replacing the deleted hardcoded texture-field allowlist Set per AC-03).\n *\n * Returns `undefined` when the shader is not yet registered (the cross-\n * worktree shader-late-register path of plan R-4): the loader then falls\n * back to a graceful \"try every int paramValue as a refs index\" walk that\n * may misclassify scalar f32 fields whose value happens to land in\n * [0, refs.length); the extract layer (M4 / w23) catches mis-typed\n * handles via paramSchema validation and falls back to MISSING_TEXTURE_HANDLE.\n */\n getMaterialShaderTextureFieldNames?(shaderId: string): ReadonlySet<string> | undefined;\n /**\n * feat-20260707 M5 / w33 (D-11): device compression caps the texture / equirect\n * Basis arms feed to `selectTranscodeTarget` to pick a transcode target. Wired\n * by `createRenderer` from `RhiCaps` (D-8 one-line projection); a bare\n * AssetRegistry (test / headless path) defaults to all-false, which drives the\n * uncompressed `rgba8unorm` / `rgba16float` fallback (section 8 P3, AC-04).\n * Extends the single ctx input face rather than opening a new loader channel\n * (Pipeline Isolation — inputs declared explicitly).\n */\n readonly transcodeCaps: TranscodeCaps;\n readonly device: unknown;\n}\n\n/**\n * Runtime-side loader injected into the `LoaderRegistry`. One loader per\n * `asset.kind`; the registry dispatches `loadByGuid` on the kind.\n *\n * `load` is pure of registry bookkeeping (no `registerWithGuid`); it only\n * produces the `Asset` POD (or a structured error / `undefined`). See the\n * module comment above for the sync vs async dispatch asymmetry.\n */\nexport interface Loader<P = Asset> {\n readonly kind: string;\n /** Optional Pack v2 dispatch that retains asset-local artifact bytes. */\n readonly loadPack?: (\n input: {\n readonly guid: string;\n readonly kind: string;\n readonly payload: Record<string, unknown>;\n readonly refs: readonly string[];\n readonly artifacts: Readonly<\n Record<\n string,\n {\n readonly descriptor: {\n readonly path: string;\n readonly mediaType: string;\n readonly assetCodec?: {\n readonly name: string;\n readonly container?: 'ktx2' | 'basis';\n readonly profile?: string;\n readonly version?: string;\n };\n };\n readonly bytes: Uint8Array;\n }\n >\n >;\n },\n ctx: LoadContext,\n ) => LoaderOutput<P>;\n load(\n payload: Record<string, unknown>,\n refs: readonly string[] | undefined,\n ctx: LoadContext,\n ): LoaderOutput<P>;\n}\n\nexport type {\n ImportContext,\n ImportDiagnostic,\n ImportDiagnosticLocation,\n ImportErrorCode,\n ImportErrorDetail,\n ImportedArtifactBody,\n ImportedAsset,\n Importer,\n ImporterCapabilities,\n ImportProduct,\n ImportProductFinalizeArtifact,\n ImportProductFinalizeOptions,\n ImportProductFinalizeResult,\n ImportResult,\n ImportSourceRange,\n ImportSubAsset,\n ImportTransport,\n SourceDependency,\n} from './import.js';\nexport {\n IMPORT_ERROR_HINTS,\n ImportError,\n} from './import.js';\n// === EngineMetrics contract SSOT (feat-20260705-runtime-tier2-decomposition M1 / w2, D-3) ===\n//\n// Decision anchors:\n// - plan-strategy D-3 (the 3-method EngineMetrics interface has zero type\n// dependencies; sinking it into @forgeax/engine-types makes types the SSOT\n// leaf. EngineMetricsImpl + createEngineMetrics stay in runtime.)\n// - research F5 (asset-registry.ts `import type { EngineMetrics }` was the\n// third reverse edge from assets cluster to runtime; type-only but the\n// project-reference layer still had to resolve it — relocating the contract\n// to the leaf removes the edge).\n//\n// The interface was originally defined in runtime/src/engine-metrics.ts. It is\n// promoted here so both @forgeax/engine-runtime and @forgeax/engine-assets-runtime\n// import the contract from a single entry point (charter F1).\n\n/**\n * Per-Renderer metrics counter API. Backed by a `Map<string, number>`; reads\n * return a frozen plain object so external mutation never leaks back into the\n * registry (D-5 + R-2 mutation-resistance).\n *\n * Three methods cover the full surface:\n *\n * const r = await createRenderer(canvas);\n * // ... renderer hits some nineslice runtime soft-warn\n * r.metrics.snapshot()['nineslice.scale-too-small']; // -> number | undefined\n *\n * r.metrics.increment('nineslice.scale-too-small'); // mutate counter\n * r.metrics.reset(); // drop all counters\n *\n * Callers can `for (const k in renderer.metrics.snapshot())` to enumerate\n * fired events without knowing the namespace ahead of time.\n *\n * @remarks Closed namespace (charter P5):\n * - `nineslice.scale-too-small`\n * - `nineslice.tile-needs-repeat-sampler`\n * - `render.instancing.foldedDraws`\n */\nexport interface EngineMetrics {\n /**\n * Bump the counter for `name` by 1. Counters start at 0 implicitly; the\n * first `increment(name)` lands a 1 in the snapshot. Names are free-form\n * strings (no prior registration), so feat-local namespaces (e.g.\n * `nineslice.*`) coexist without coordination.\n */\n increment(name: string): void;\n /**\n * Read all counters as an immutable `Readonly<Record<string, number>>`.\n * The returned object is frozen — external mutation throws in strict mode\n * and is silently ignored otherwise. Snapshot-then-mutate is decoupled\n * from the registry: a later `increment` does not retroactively alter the\n * already-returned snapshot.\n */\n snapshot(): Readonly<Record<string, number>>;\n /**\n * Drop every counter back to 0 (counter rows physically removed). Provided\n * for test isolation and Inspector reset workflows; production code on\n * the hot path never calls this.\n */\n reset(): void;\n}\n\n// inspector-client is Node-only (imports 'ws'); consume via\n// import { ... } from '@forgeax/engine-types/inspector-client'\n","import {\n type AssetDecoderContribution,\n type AssetKind,\n err,\n ok,\n type SkeletonAsset,\n type SkinAsset,\n} from '@forgeax/engine-types';\n\nfunction floatArray(value: unknown): Float32Array | undefined {\n if (value instanceof Float32Array) return value;\n if (Array.isArray(value) && value.every((item) => typeof item === 'number')) {\n return Float32Array.from(value);\n }\n return undefined;\n}\n\nexport const skinContribution: AssetDecoderContribution<SkinAsset, 'skin'> = {\n kind: { kind: 'skin' } as AssetKind<SkinAsset, 'skin'>,\n consumer: 'resolveSkinJoints',\n decoder: {\n async decode({ envelope }) {\n const payload = envelope.payload;\n if (\n payload.kind === 'skin' &&\n payload.skeletonGuid.length > 0 &&\n payload.jointPaths.length > 0\n ) {\n return ok(payload);\n }\n return err({\n code: 'asset-package-invalid',\n expected: 'a skin payload with a skeleton GUID and joint paths',\n hint: 'recook the skin binding and publish its skeleton reference',\n detail: { guid: envelope.guid, reason: 'skin owner validation failed' },\n });\n },\n },\n};\n\nexport const skeletonContribution: AssetDecoderContribution<SkeletonAsset, 'skeleton'> = {\n kind: { kind: 'skeleton' } as AssetKind<SkeletonAsset, 'skeleton'>,\n consumer: 'resolveSkinJoints',\n decoder: {\n async decode({ envelope }) {\n const payload = envelope.payload as unknown;\n if (payload !== null && typeof payload === 'object') {\n const source = payload as Record<string, unknown>;\n const inverseBindMatrices = floatArray(source.inverseBindMatrices);\n const jointCount = source.jointCount;\n if (\n source.kind === 'skeleton' &&\n inverseBindMatrices !== undefined &&\n Number.isSafeInteger(jointCount) &&\n (jointCount as number) >= 0 &&\n inverseBindMatrices.length === (jointCount as number) * 16\n ) {\n return ok({ kind: 'skeleton', inverseBindMatrices, jointCount: jointCount as number });\n }\n }\n return err({\n code: 'asset-package-invalid',\n expected: 'a skeleton payload with one inverse-bind matrix per joint',\n hint: 'recook the skeleton and publish its complete joint data',\n detail: { guid: envelope.guid, reason: 'skeleton owner validation failed' },\n });\n },\n },\n};\n","// @forgeax/engine-runtime -- skin cluster error classes.\n//\n// feat-20260704-runtime-tier1-decomposition M2 / w8 (D-3): skin / skeleton\n// animation cluster -- joint count / despawn / path / coexistence and\n// extract-stage binding failures. Palette/material/GPU failures stay in the\n// render error union because render owns the emitting frame stages. Binding\n// class names, .code literals, and .detail shapes are preserved byte-for-byte\n// (OOS-4).\n//\n// SkinExtractErrorCode (the 3-member extract-stage subset union) is kept as a\n// named export and folded into SkinErrorCode, preserving the pre-existing\n// public symbol (OOS-4).\n\n// ── SkinExtractErrorCode subset union ───────────────────────────────────────\n\n/**\n * feat-20260612-skin-palette-per-frame-upload M2 / m2-5 subset union.\n *\n * Covers the three new fail-fast extract-stage errors that fire from\n * `render-system-extract.ts` `hasSkin` segment when the per-frame palette\n * upload pipeline cannot resolve a slice for an entity. Single-entity\n * `continue` semantics: the entity is skipped, sibling entities in the\n * same frame keep extracting (plan-strategy D-5).\n *\n * | code | class | trigger |\n * |:--|:--|:--|\n * | `'skeleton-resolve-failed'` | `SkeletonResolveFailedError` | `assets.get<SkeletonAsset>(skin.skeleton)` returns null/undefined |\n * | `'joint-count-mismatch'` | `JointCountMismatchError` | `Skin.joints.length !== SkeletonAsset.jointCount` |\n * | `'joint-entity-dangling'` | `JointEntityDanglingError` | `Skin.joints[i]` Entity is despawned (Transform.world view undefined) |\n *\n * AI users discriminate via `switch (err.code)` over `RuntimeErrorCode`;\n * each member narrows to its `*Error` class with structured `.detail`.\n *\n * NOTE: distinct from the pre-existing `'skin-joint-despawned'` /\n * `'skin-joint-path-unresolved'` / `'skin-joint-count-exceeded'`\n * (advanceAnimationPlayer + post-spawn jointPath resolution); plan-strategy\n * D-4 forbids reusing those codes for the new extract-stage triggers.\n */\nexport type SkinExtractErrorCode =\n | 'skeleton-resolve-failed'\n | 'joint-count-mismatch'\n | 'joint-entity-dangling';\n\n// ── SkinJointCountExceededError ────────────────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'skin-joint-count-exceeded'`.\n *\n * Emitted when a glTF skin has more than MAX_JOINTS (256) joints.\n */\nexport interface SkinJointCountExceededDetail {\n readonly jointCount: number;\n readonly max: number;\n}\n\n/**\n * Structured error for skin joint count exceeding the engine cap.\n *\n * Emitted during skin import/validation. Four-field surface:\n * - `.code = 'skin-joint-count-exceeded'`\n * - `.expected` — max allowed (256)\n * - `.hint` — reduce joint count in the source asset\n * - `.detail = { jointCount, max }` — actual vs limit\n */\nexport class SkinJointCountExceededError extends Error {\n readonly code = 'skin-joint-count-exceeded' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: SkinJointCountExceededDetail;\n\n constructor(jointCount: number, max = 256) {\n const expected = `jointCount <= ${max}`;\n const hint = `skin has ${jointCount} joints (max ${max}); reduce joint count in the source glTF asset (OOS-skin-many-joints)`;\n super(`skin joint count ${jointCount} exceeds max ${max}`);\n this.name = 'SkinJointCountExceededError';\n this.expected = expected;\n this.hint = hint;\n this.detail = { jointCount, max };\n }\n}\n\n// ── SkinJointDespawnedError ─────────────────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'skin-joint-despawned'`.\n *\n * Emitted at extract time when a Skin.joints[i] Entity has been despawned.\n */\nexport interface SkinJointDespawnedDetail {\n readonly meshEntity: number;\n readonly jointIndex: number;\n}\n\n/**\n * Structured error for despawned skin joint Entity.\n *\n * Emitted at extract time; the mesh draw is fully skipped.\n * - `.code = 'skin-joint-despawned'`\n * - `.expected` — all Skin.joints alive\n * - `.hint` — remove the Skin component or re-spawn joints\n * - `.detail = { meshEntity, jointIndex }`\n */\nexport class SkinJointDespawnedError extends Error {\n readonly code = 'skin-joint-despawned' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: SkinJointDespawnedDetail;\n\n constructor(meshEntity: number, jointIndex: number) {\n const expected = `Skin.joints[${jointIndex}] references a live entity`;\n const hint = `joint[${jointIndex}] of entity ${meshEntity} has been despawned; remove Skin component or re-spawn the joint entity (OOS-skin-joint-respawn)`;\n super(`skin joint[${jointIndex}] despawned for entity ${meshEntity}`);\n this.name = 'SkinJointDespawnedError';\n this.expected = expected;\n this.hint = hint;\n this.detail = { meshEntity, jointIndex };\n }\n}\n\n// ── SkinJointPathUnresolvedError ────────────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'skin-joint-path-unresolved'`.\n *\n * Emitted at post-spawn time when a jointPath leaf name cannot be found.\n */\nexport interface SkinJointPathUnresolvedDetail {\n readonly skinEntity: number;\n readonly path: readonly string[];\n readonly failedAtIndex: number;\n}\n\n/**\n * Structured error for unresolved jointPath post-spawn.\n *\n * Emitted by resolveSkinJoints when Name lookup fails.\n * - `.code = 'skin-joint-path-unresolved'`\n * - `.expected` — Name-bearing entity exists for each jointPath leaf\n * - `.hint` — verify glTF node Name preservation in the importer\n * - `.detail = { skinEntity, path, failedAtIndex }`\n */\nexport class SkinJointPathUnresolvedError extends Error {\n readonly code = 'skin-joint-path-unresolved' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: SkinJointPathUnresolvedDetail;\n\n constructor(skinEntity: number, path: readonly string[], failedAtIndex: number) {\n const leafName = path[failedAtIndex] ?? '<unknown>';\n const expected = `joint entity with Name=\"${leafName}\" exists in the world`;\n const hint = `joint path \"${path.join('/')}\" for skin entity ${skinEntity} could not be resolved; verify glTF node names are preserved`;\n super(\n `joint path \"${path.join('/')}\" unresolved at index ${failedAtIndex} for entity ${skinEntity}`,\n );\n this.name = 'SkinJointPathUnresolvedError';\n this.expected = expected;\n this.hint = hint;\n this.detail = { skinEntity, path, failedAtIndex };\n }\n}\n\n// ── SkinInstancesCoexistForbiddenError ──────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'skin-instances-coexist-forbidden'`.\n *\n * Emitted at extract time when Skin + Instances coexist on the same entity.\n */\nexport interface SkinInstancesCoexistForbiddenDetail {\n readonly entity: number;\n}\n\n/**\n * Structured error for Skin + Instances coexistence on same entity.\n *\n * Emitted at extract time; the entity draw is skipped.\n * - `.code = 'skin-instances-coexist-forbidden'`\n * - `.expected` — Skin and Instances on separate entities\n * - `.hint` — split skinned meshes from instanced meshes into separate entities (OOS-skin-instances-coexist)\n * - `.detail = { entity }`\n */\nexport class SkinInstancesCoexistForbiddenError extends Error {\n readonly code = 'skin-instances-coexist-forbidden' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: SkinInstancesCoexistForbiddenDetail;\n\n constructor(entity: number) {\n const expected = 'Skin and Instances must not coexist on the same entity';\n const hint = `entity ${entity} has both Skin and Instances; split skinned meshes from instanced meshes into separate entities (OOS-skin-instances-coexist)`;\n super(`Skin + Instances coexistence forbidden on entity ${entity}`);\n this.name = 'SkinInstancesCoexistForbiddenError';\n this.expected = expected;\n this.hint = hint;\n this.detail = { entity };\n }\n}\n\n// ── SkeletonResolveFailedError ─────────────────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'skeleton-resolve-failed'`.\n *\n * Emitted at extract time when `assets.get<SkeletonAsset>(skin.skeleton)`\n * returns `null` / `undefined`. The skeleton handle is non-zero (the entity\n * declared a Skin) but the asset is not registered (importer drift /\n * AssetRegistry not warmed).\n */\nexport interface SkeletonResolveFailedDetail {\n readonly entity: number;\n readonly skeletonHandle: number;\n}\n\n/**\n * Structured error for unresolved SkeletonAsset handle at extract time\n * (feat-20260612-skin-palette-per-frame-upload M2 / m2-5).\n *\n * Emitted at extract time; the entity draw is skipped (continue), other\n * entities in the same frame keep extracting.\n * - `.code = 'skeleton-resolve-failed'`\n * - `.expected` — Skin.skeleton handle resolves to a registered SkeletonAsset\n * - `.hint` — verify SkeletonAsset is imported into pack-index AND registered\n * via AssetRegistry.register(handle, asset) before extractFrame\n * - `.detail = { entity, skeletonHandle }`\n */\nexport class SkeletonResolveFailedError extends Error {\n readonly code = 'skeleton-resolve-failed' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: SkeletonResolveFailedDetail;\n\n constructor(entity: number, skeletonHandle: number) {\n const expected = `Skin.skeleton handle ${skeletonHandle} resolves to a registered SkeletonAsset`;\n const hint = `entity ${entity} Skin.skeleton handle ${skeletonHandle} is not registered; check that the SkeletonAsset went through the gltf importer into pack-index AND that AssetRegistry.register was called for the handle before extractFrame runs`;\n super(`Skin skeleton resolve failed on entity ${entity}: handle ${skeletonHandle}`);\n this.name = 'SkeletonResolveFailedError';\n this.expected = expected;\n this.hint = hint;\n this.detail = { entity, skeletonHandle };\n }\n}\n\n// ── JointCountMismatchError ────────────────────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'joint-count-mismatch'`.\n *\n * Emitted at extract time when `Skin.joints.length !== SkeletonAsset.jointCount`.\n * `expected` is the SkeletonAsset's jointCount (the source of truth);\n * `actual` is the entity's `Skin.joints.length` (the runtime entity reference\n * list materialized at post-spawn time).\n */\nexport interface JointCountMismatchDetail {\n readonly entity: number;\n readonly expected: number;\n readonly actual: number;\n}\n\n/**\n * Structured error for SkinAsset.joints[] vs SkeletonAsset.jointCount disagreement\n * (feat-20260612-skin-palette-per-frame-upload M2 / m2-5).\n *\n * Emitted at extract time; the entity draw is skipped (continue).\n * - `.code = 'joint-count-mismatch'`\n * - `.expected` — Skin.joints.length === SkeletonAsset.jointCount\n * - `.hint` — verify SkinAsset.joints[] and SkeletonAsset jointPaths[]\n * come from the same glTF skin node\n * - `.detail = { entity, expected, actual }`\n */\nexport class JointCountMismatchError extends Error {\n readonly code = 'joint-count-mismatch' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: JointCountMismatchDetail;\n\n constructor(entity: number, expected: number, actual: number) {\n const expectedStr = `Skin.joints.length === SkeletonAsset.jointCount (=${expected})`;\n const hint = `entity ${entity}: Skin.joints.length=${actual} disagrees with SkeletonAsset.jointCount=${expected}; verify SkinAsset.joints[] and SkeletonAsset jointPaths[] come from the same glTF skin node`;\n super(\n `joint count mismatch on entity ${entity}: SkeletonAsset.jointCount=${expected}, Skin.joints.length=${actual}`,\n );\n this.name = 'JointCountMismatchError';\n this.expected = expectedStr;\n this.hint = hint;\n this.detail = { entity, expected, actual };\n }\n}\n\n// ── JointEntityDanglingError ──────────────────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'joint-entity-dangling'`.\n *\n * Emitted at extract time when `Skin.joints[i]` points at an Entity that has\n * been despawned (or lost its Transform component) so\n * `worldInternal._getArrayView(jointEntity, Transform, 'world')` returns\n * undefined. `jointIndex` is the position within `Skin.joints[]`.\n */\nexport interface JointEntityDanglingDetail {\n readonly entity: number;\n readonly jointIndex: number;\n}\n\n/**\n * Structured error for despawned (or Transform-less) joint Entity at extract\n * time (feat-20260612-skin-palette-per-frame-upload M2 / m2-5).\n *\n * Distinct from the pre-existing `SkinJointDespawnedError` which fires from\n * advanceAnimationPlayer (animation-stage); this one fires from extractFrame\n * (palette-upload stage) when the per-joint world mat4 view is missing.\n *\n * Emitted at extract time; the entity draw is skipped (continue).\n * - `.code = 'joint-entity-dangling'`\n * - `.expected` — Skin.joints[i] references a live Entity with Transform\n * - `.hint` — sync Skin.joints[] when joint entities are despawned, or\n * re-import the scene through the gltf importer to refresh Entity refs\n * - `.detail = { entity, jointIndex }`\n */\nexport class JointEntityDanglingError extends Error {\n readonly code = 'joint-entity-dangling' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: JointEntityDanglingDetail;\n\n constructor(entity: number, jointIndex: number) {\n const expected = `Skin.joints[${jointIndex}] references a live Entity with Transform`;\n const hint = `entity ${entity} Skin.joints[${jointIndex}] points at a despawned (or Transform-less) Entity; sync Skin.joints[] when joint entities are despawned, or re-import the scene through the gltf importer to refresh Entity references`;\n super(`joint entity dangling on entity ${entity} at jointIndex ${jointIndex}`);\n this.name = 'JointEntityDanglingError';\n this.expected = expected;\n this.hint = hint;\n this.detail = { entity, jointIndex };\n }\n}\n\n// -- SkinErrorCode / SkinError closed unions ------------------------------------\n\n/**\n * Closed union of skin-cluster error codes derived from the correlated error\n * union. AI users perform exhaustive `switch (err.code)` without default; TS\n * guards completeness.\n */\nexport type SkinErrorCode = SkinError['code'];\n\n/**\n * Closed union of the skin-cluster structured error classes, each carrying a\n * `SkinErrorCode` discriminant on `.code`.\n */\nexport type SkinError =\n | SkinJointCountExceededError\n | SkinJointDespawnedError\n | SkinJointPathUnresolvedError\n | SkinInstancesCoexistForbiddenError\n | SkeletonResolveFailedError\n | JointCountMismatchError\n | JointEntityDanglingError;\n","// @forgeax/engine-runtime - Skin component (skeleton handle + joint Entity slots).\n//\n// Schema: { skeleton: 'shared<SkeletonAsset>', joints: 'array<entity>' }.\n//\n// `skeleton` carries the immutable SkeletonAsset handle (IBM + jointCount);\n// `joints` carries the live Entity[] resolved at post-spawn time from the\n// SkinAsset.jointPaths via Name-component BFS/DFS lookup. The joint list is\n// consumed by advanceAnimationPlayer (write target) and render-system-extract\n// (CPU palette pre-multiply source).\n//\n// Naming: single-semantic component drops the 'Component' suffix\n// (AGENTS.md §Component naming rule #1). `joints` field takes the holder's\n// perspective (AGENTS.md §Component naming rule #3).\n//\n// Component registered alongside MeshFilter / MeshRenderer / Transform as\n// a sibling on the same entity (AC-13 / AC-37). Skin + Instances coexistence\n// on the same entity is forbidden (M2 fail-fast 'skin-instances-coexist-forbidden').\n//\n// Decision anchors:\n// - requirements AC-13 (Skin sibling to MeshFilter / MeshRenderer)\n// - requirements AC-15 (joint Entity slots, no marker component)\n// - requirements AC-37 (no Component suffix)\n// - plan-strategy D-10 (SkinPaletteSlice naming + Skin x Instances fail-fast)\n// - charter P3 (explicit failure: joint despawn fail-fast)\n// - schema vocab 'shared<SkeletonAsset>' v1 missing item #4 alignment\n//\n// ## Transform contract (post-bug-20260615 fix)\n//\n// **Old (buggy) implicit contract (pre-bug-20260615):** The Skin entity's\n// Transform.world was double-applied during skinning -- the shader computed\n// `world = meshes[0].worldFromLocal x palette x pos`, so any non-identity\n// Transform on the Skin entity (or its non-joint ancestors) caused doubled\n// motion (translation 2x, rotation 2x). Holders had to manually pin the Skin\n// entity's Transform to identity to avoid doubled motion. This contract was\n// undocumented and easy to violate.\n//\n// **New explicit contract (post-bug-20260615 fix):** An entity carrying `Skin`\n// has its own `Transform` ignored at render time; the world transform is\n// determined entirely by the joints' world matrices fed through the palette:\n//\n// palette[i] = jointWorld_i x IBM_i\n// shader: world = palette[i] x pos\n//\n// No additional left-multiply by `meshes[0].worldFromLocal` or `instanceLocal`.\n// To move the rig, parent the joint root (or any common ancestor of the joints\n// in `Skin.joints[]`) to your driving entity -- moving the Skin entity itself\n// has no rendering effect. This aligns with glTF 2.0 SSkins Implementation\n// Note: \"the transform of the node that the mesh is attached to must be\n// ignored when performing skinning.\"\n//\n// Full pipeline documentation: packages/runtime/README.md\n// SSkinPaletteAllocator.\n//\n// Fix commits:\n// - M0 (red): 15425c2b -- parented skin double-transform unit test\n// - M1 (green): 2ad509b7 -- shader Plan A: drop meshes[0] left-multiply\n// - M2 (cleanup): 4118e463 -- extract.ts joint read -> world.get API\n// - M3 (demo): 94d7db66 -- hello-skin parented Fox under non-identity rig\n// - M4 (baseline): b6ddf46d -- palette-hash counter-proof + submodule pointer\n\nimport { defineComponent } from '@forgeax/engine-ecs';\n\nexport const Skin = defineComponent('Skin', {\n // The renderer owns the live skeleton asset/palette binding; joint entity\n // relationships remain the portable simulation-side pose contract.\n skeleton: { type: 'shared<SkeletonAsset>' },\n joints: { type: 'array<entity>' },\n});\n","import type { Component, World } from '@forgeax/engine-ecs';\nimport type { Plugin } from '@forgeax/engine-plugin';\nimport { Skin } from './skin';\n\nconst SKINNING_COMPONENTS: readonly Component[] = [Skin];\n\nfunction registerSkinningComponents(world: World): () => void {\n const leases = SKINNING_COMPONENTS.map((component) =>\n world.components.register(component).unwrap(),\n );\n return () => {\n for (let index = leases.length - 1; index >= 0; index -= 1) leases[index]?.dispose();\n };\n}\n\n/** Install skeletal binding components in a World that consumes skinned scenes. */\nexport function skinningPlugin(): Plugin {\n return {\n name: 'skinning',\n inject: ['world'],\n apply(ctx) {\n ctx.effect(() => registerSkinningComponents(ctx.world), 'skinning/components');\n },\n };\n}\n","import type { EntityHandle } from '@forgeax/engine-ecs';\nimport { type SkinError, SkinJointPathUnresolvedError } from './errors.js';\n\nexport function resolveSkinJoints(\n jointPaths: readonly string[],\n names: ReadonlyMap<string, EntityHandle>,\n skinEntity: EntityHandle,\n): { ok: true; value: Uint32Array } | { ok: false; error: SkinError } {\n const joints: number[] = [];\n for (const path of jointPaths) {\n const segments = path.split('/').filter(Boolean);\n if (segments.length === 0) continue;\n const failedAtIndex = segments.length - 1;\n const entity = names.get(segments[failedAtIndex] ?? '');\n if (entity === undefined) {\n return {\n ok: false,\n error: new SkinJointPathUnresolvedError(skinEntity as number, segments, failedAtIndex),\n };\n }\n joints.push(entity as number);\n }\n return { ok: true, value: new Uint32Array(joints) };\n}\n"],"mappings":";AC2EA,IAAM,WAAW;EACf,SAAyC;AACvC,WAAO,KAAK;EACd;EACA,SAAkC,eAAiC;AACjE,WAAO,KAAK;EACd;AACF;AAEA,IAAM,YAAY;EAChB,SAAwC;AAEtC,UAAM,KAAK;EACb;EACA,SAAsC,cAAoB;AACxD,WAAO;EACT;AACF;AAaO,SAAS,GAAM,OAAuB;AAC3C,QAAM,IAAI,OAAO,OAAO,QAAQ;AAChC,IAAE,KAAK;AACP,IAAE,QAAQ;AACV,SAAO;AACT;AAUO,SAAS,IAAO,OAAwB;AAC7C,QAAM,IAAI,OAAO,OAAO,SAAS;AACjC,IAAE,KAAK;AACP,IAAE,QAAQ;AACV,SAAO;AACT;AE+HO,IAAM,YAAY,KAAK,MAAM;AG5P7B,IAAM,uBAAuB;EAClC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AACF;AA0KA,IAAM,wBAAwB;EAC5B,6BAA6B;IAC3B,UAAU;IACV,MAAM;EAAA;EAER,iCAAiC;IAC/B,UAAU;IACV,MAAM;EAAA;EAER,8BAA8B;IAC5B,UAAU;IACV,MAAM;EAAA;EAER,0BAA0B;IACxB,UAAU;IACV,MAAM;EAAA;EAER,gCAAgC;IAC9B,UAAU;IACV,MAAM;EAAA;EAER,sCAAsC;IACpC,UAAU;IACV,MAAM;EAAA;EAER,4BAA4B;IAC1B,UAAU;IACV,MAAM;EAAA;EAER,8BAA8B;IAC5B,UAAU;IACV,MAAM;EAAA;EAER,2BAA2B;IACzB,UAAU;IACV,MAAM;EAAA;EAER,oCAAoC;IAClC,UAAU;IACV,MAAM;EAAA;EAER,wCAAwC;IACtC,UAAU;IACV,MAAM;EAAA;EAER,sCAAsC;IACpC,UAAU;IACV,MAAM;EAAA;EAER,4CAA4C;IAC1C,UAAU;IACV,MAAM;EAAA;EAER,gCAAgC;IAC9B,UAAU;IACV,MAAM;EAAA;EAER,uCAAuC;IACrC,UAAU;IACV,MAAM;EAAA;EAER,uCAAuC;IACrC,UAAU;IACV,MAAM;EAAA;EAER,2BAA2B;IACzB,UAAU;IACV,MAAM;EAAA;AAEV;AAOO,IAAM,0BACX,OAAO;EACL,qBAAqB,IAAI,CAAC,SAAS,CAAC,MAAM,sBAAsB,IAAI,EAAE,QAAQ,CAAC;AACjF;AAEK,IAAM,uBAAoE,OAAO;EACtF,qBAAqB,IAAI,CAAC,SAAS,CAAC,MAAM,sBAAsB,IAAI,EAAE,IAAI,CAAC;AAC7E;AGhPA,IAAM,gBAAA,oBAAoD,IAAuB;EAC/E;EACA;EACA;EACA;EACA;EACA;EACA;AACF,CAAC;AAED,IAAM,qBAAA,oBAAyD,IAAuB;EACpF;EACA;EACA;EACA;AACF,CAAC;AAED,IAAM,gBAAA,oBAAoD,IAAuB;EAC/E;EACA;AACF,CAAC;AAED,IAAM,YAAA,oBAAgD,IAAuB;EAC3E,GAAG;EACH,GAAG;EACH,GAAG;EACH;AACF,CAAC;;;AKjDD,SAAS,WAAW,OAA0C;AAC5D,MAAI,iBAAiB,aAAc,QAAO;AAC1C,MAAI,MAAM,QAAQ,KAAK,KAAK,MAAM,MAAM,CAAC,SAAS,OAAO,SAAS,QAAQ,GAAG;AAC3E,WAAO,aAAa,KAAK,KAAK;AAAA,EAChC;AACA,SAAO;AACT;AAEO,IAAM,mBAAgE;AAAA,EAC3E,MAAM,EAAE,MAAM,OAAO;AAAA,EACrB,UAAU;AAAA,EACV,SAAS;AAAA,IACP,MAAM,OAAO,EAAE,SAAS,GAAG;AACzB,YAAM,UAAU,SAAS;AACzB,UACE,QAAQ,SAAS,UACjB,QAAQ,aAAa,SAAS,KAC9B,QAAQ,WAAW,SAAS,GAC5B;AACA,eAAO,GAAG,OAAO;AAAA,MACnB;AACA,aAAO,IAAI;AAAA,QACT,MAAM;AAAA,QACN,UAAU;AAAA,QACV,MAAM;AAAA,QACN,QAAQ,EAAE,MAAM,SAAS,MAAM,QAAQ,+BAA+B;AAAA,MACxE,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEO,IAAM,uBAA4E;AAAA,EACvF,MAAM,EAAE,MAAM,WAAW;AAAA,EACzB,UAAU;AAAA,EACV,SAAS;AAAA,IACP,MAAM,OAAO,EAAE,SAAS,GAAG;AACzB,YAAM,UAAU,SAAS;AACzB,UAAI,YAAY,QAAQ,OAAO,YAAY,UAAU;AACnD,cAAM,SAAS;AACf,cAAM,sBAAsB,WAAW,OAAO,mBAAmB;AACjE,cAAM,aAAa,OAAO;AAC1B,YACE,OAAO,SAAS,cAChB,wBAAwB,UACxB,OAAO,cAAc,UAAU,KAC9B,cAAyB,KAC1B,oBAAoB,WAAY,aAAwB,IACxD;AACA,iBAAO,GAAG,EAAE,MAAM,YAAY,qBAAqB,WAAiC,CAAC;AAAA,QACvF;AAAA,MACF;AACA,aAAO,IAAI;AAAA,QACT,MAAM;AAAA,QACN,UAAU;AAAA,QACV,MAAM;AAAA,QACN,QAAQ,EAAE,MAAM,SAAS,MAAM,QAAQ,mCAAmC;AAAA,MAC5E,CAAC;AAAA,IACH;AAAA,EACF;AACF;;;ACJO,IAAM,8BAAN,cAA0C,MAAM;AAAA,EAC5C,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,YAAoB,MAAM,KAAK;AACzC,UAAM,WAAW,iBAAiB,GAAG;AACrC,UAAM,OAAO,YAAY,UAAU,gBAAgB,GAAG;AACtD,UAAM,oBAAoB,UAAU,gBAAgB,GAAG,EAAE;AACzD,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,YAAY,IAAI;AAAA,EAClC;AACF;AAuBO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EACxC,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,YAAoB,YAAoB;AAClD,UAAM,WAAW,eAAe,UAAU;AAC1C,UAAM,OAAO,SAAS,UAAU,eAAe,UAAU;AACzD,UAAM,cAAc,UAAU,0BAA0B,UAAU,EAAE;AACpE,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,YAAY,WAAW;AAAA,EACzC;AACF;AAwBO,IAAM,+BAAN,cAA2C,MAAM;AAAA,EAC7C,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,YAAoB,MAAyB,eAAuB;AAC9E,UAAM,WAAW,KAAK,aAAa,KAAK;AACxC,UAAM,WAAW,2BAA2B,QAAQ;AACpD,UAAM,OAAO,eAAe,KAAK,KAAK,GAAG,CAAC,qBAAqB,UAAU;AACzE;AAAA,MACE,eAAe,KAAK,KAAK,GAAG,CAAC,yBAAyB,aAAa,eAAe,UAAU;AAAA,IAC9F;AACA,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,YAAY,MAAM,cAAc;AAAA,EAClD;AACF;AAsBO,IAAM,qCAAN,cAAiD,MAAM;AAAA,EACnD,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,QAAgB;AAC1B,UAAM,WAAW;AACjB,UAAM,OAAO,UAAU,MAAM;AAC7B,UAAM,oDAAoD,MAAM,EAAE;AAClE,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,OAAO;AAAA,EACzB;AACF;AA6BO,IAAM,6BAAN,cAAyC,MAAM;AAAA,EAC3C,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,QAAgB,gBAAwB;AAClD,UAAM,WAAW,wBAAwB,cAAc;AACvD,UAAM,OAAO,UAAU,MAAM,yBAAyB,cAAc;AACpE,UAAM,0CAA0C,MAAM,YAAY,cAAc,EAAE;AAClF,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,QAAQ,eAAe;AAAA,EACzC;AACF;AA6BO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EACxC,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,QAAgB,UAAkB,QAAgB;AAC5D,UAAM,cAAc,qDAAqD,QAAQ;AACjF,UAAM,OAAO,UAAU,MAAM,wBAAwB,MAAM,4CAA4C,QAAQ;AAC/G;AAAA,MACE,kCAAkC,MAAM,8BAA8B,QAAQ,wBAAwB,MAAM;AAAA,IAC9G;AACA,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,QAAQ,UAAU,OAAO;AAAA,EAC3C;AACF;AAgCO,IAAM,2BAAN,cAAuC,MAAM;AAAA,EACzC,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,QAAgB,YAAoB;AAC9C,UAAM,WAAW,eAAe,UAAU;AAC1C,UAAM,OAAO,UAAU,MAAM,gBAAgB,UAAU;AACvD,UAAM,mCAAmC,MAAM,kBAAkB,UAAU,EAAE;AAC7E,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,QAAQ,WAAW;AAAA,EACrC;AACF;;;ACjRA,SAAS,uBAAuB;AAEzB,IAAM,OAAO,gBAAgB,QAAQ;AAAA;AAAA;AAAA,EAG1C,UAAU,EAAE,MAAM,wBAAwB;AAAA,EAC1C,QAAQ,EAAE,MAAM,gBAAgB;AAClC,CAAC;;;AC/DD,IAAM,sBAA4C,CAAC,IAAI;AAEvD,SAAS,2BAA2B,OAA0B;AAC5D,QAAM,SAAS,oBAAoB;AAAA,IAAI,CAAC,cACtC,MAAM,WAAW,SAAS,SAAS,EAAE,OAAO;AAAA,EAC9C;AACA,SAAO,MAAM;AACX,aAAS,QAAQ,OAAO,SAAS,GAAG,SAAS,GAAG,SAAS,EAAG,QAAO,KAAK,GAAG,QAAQ;AAAA,EACrF;AACF;AAGO,SAAS,iBAAyB;AACvC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ,CAAC,OAAO;AAAA,IAChB,MAAM,KAAK;AACT,UAAI,OAAO,MAAM,2BAA2B,IAAI,KAAK,GAAG,qBAAqB;AAAA,IAC/E;AAAA,EACF;AACF;;;ACrBO,SAAS,kBACd,YACA,OACA,YACoE;AACpE,QAAM,SAAmB,CAAC;AAC1B,aAAW,QAAQ,YAAY;AAC7B,UAAM,WAAW,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAC/C,QAAI,SAAS,WAAW,EAAG;AAC3B,UAAM,gBAAgB,SAAS,SAAS;AACxC,UAAM,SAAS,MAAM,IAAI,SAAS,aAAa,KAAK,EAAE;AACtD,QAAI,WAAW,QAAW;AACxB,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,OAAO,IAAI,6BAA6B,YAAsB,UAAU,aAAa;AAAA,MACvF;AAAA,IACF;AACA,WAAO,KAAK,MAAgB;AAAA,EAC9B;AACA,SAAO,EAAE,IAAI,MAAM,OAAO,IAAI,YAAY,MAAM,EAAE;AACpD;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../types/src/asset-errors.ts","../../types/src/result.ts","../../types/src/asset-evidence.ts","../../types/src/handle.ts","../../types/src/material/asset.ts","../../types/src/material/color-space.ts","../../types/src/material/errors.ts","../../types/src/material/resolve.ts","../../types/src/runtime-scope.ts","../../types/src/derive-paramschema.ts","../../types/src/asset-producer.ts","../../types/src/catalog.ts","../../types/src/import.ts","../../types/src/index.ts","../src/assets/skin-decoder.ts","../src/errors.ts","../src/skin.ts","../src/plugin.ts","../src/resolve-skin-joints.ts"],"sourcesContent":["/** Closed structured error contracts for Pack v2 and asset evidence. */\n\nexport type AssetLoadErrorCode =\n | 'asset-guid-invalid'\n | 'asset-kind-mismatch'\n | 'asset-not-found'\n | 'asset-not-ready'\n | 'catalog-unavailable'\n | 'catalog-discontinuous'\n | 'asset-fetch-failed'\n | 'asset-integrity-failed'\n | 'asset-package-invalid'\n | 'asset-decoder-missing'\n | 'asset-decode-failed'\n | 'asset-dependency-failed'\n | 'asset-superseded'\n | 'asset-load-cancelled'\n | 'asset-runtime-disposed';\n\nexport const ASSET_LOAD_ERROR_HINTS: Readonly<Record<AssetLoadErrorCode, string>> = {\n 'asset-guid-invalid': 'provide a valid asset GUID and retry the current publication',\n 'asset-kind-mismatch': 'pass the Catalog kind or the matching custom AssetKind token',\n 'asset-not-found': 'inspect the producer Catalog and rebuild the missing publication',\n 'asset-not-ready': 'wait for the current publication or inspect its producer lifecycle',\n 'catalog-unavailable': 'inspect the scope and create a fresh Registry for a new scope',\n 'catalog-discontinuous': 'reconcile the Catalog baseline before loading the current row',\n 'asset-fetch-failed': 'verify the package locator and republish the Pack',\n 'asset-integrity-failed': 'verify the artifact digest and recook the Pack',\n 'asset-package-invalid': 'validate the Pack v2 envelope and recook invalid output',\n 'asset-decoder-missing': 'install the owner decoder lease for this kind',\n 'asset-decode-failed': 'inspect the structured decoder detail and repair the owner output',\n 'asset-dependency-failed': 'repair the dependency publication named in detail and retry',\n 'asset-superseded': 'load the current publication instead of the superseded ticket',\n 'asset-load-cancelled': 'retry with a live AbortSignal when the request is still needed',\n 'asset-runtime-disposed': 'obtain a new Registry from the current realm',\n};\n\ntype AssetLoadErrorBase<C extends AssetLoadErrorCode, D> = {\n readonly code: C;\n readonly expected: string;\n readonly hint: string;\n readonly detail: D;\n};\n\nexport type AssetLoadError =\n | AssetLoadErrorBase<'asset-guid-invalid', { readonly guid: string }>\n | AssetLoadErrorBase<\n 'asset-kind-mismatch',\n { readonly guid: string; readonly expectedKind: string; readonly actualKind: string }\n >\n | AssetLoadErrorBase<'asset-not-found', { readonly guid: string }>\n | AssetLoadErrorBase<'asset-not-ready', { readonly guid: string; readonly generation: number }>\n | AssetLoadErrorBase<'catalog-unavailable', { readonly scopeId: string }>\n | AssetLoadErrorBase<\n 'catalog-discontinuous',\n {\n readonly scopeId: string;\n readonly expectedGeneration: number;\n readonly actualGeneration: number;\n }\n >\n | AssetLoadErrorBase<'asset-fetch-failed', { readonly guid: string; readonly packageUrl: string }>\n | AssetLoadErrorBase<\n 'asset-integrity-failed',\n {\n readonly guid: string;\n readonly artifactKey: string;\n readonly expectedDigest: string;\n readonly actualDigest: string;\n }\n >\n | AssetLoadErrorBase<'asset-package-invalid', { readonly guid: string; readonly reason: string }>\n | AssetLoadErrorBase<'asset-decoder-missing', { readonly kind: string }>\n | AssetLoadErrorBase<'asset-decode-failed', { readonly guid: string; readonly kind: string }>\n | AssetLoadErrorBase<\n 'asset-dependency-failed',\n { readonly guid: string; readonly dependencyGuid: string }\n >\n | AssetLoadErrorBase<'asset-superseded', { readonly guid: string; readonly generation: number }>\n | AssetLoadErrorBase<'asset-load-cancelled', { readonly guid: string }>\n | AssetLoadErrorBase<'asset-runtime-disposed', { readonly scopeId: string }>;\n\nexport type AssetArtifactErrorCode =\n | 'asset-artifact-path-invalid'\n | 'asset-artifact-missing'\n | 'asset-artifact-integrity-mismatch'\n | 'asset-artifact-media-unsupported'\n | 'asset-artifact-codec-unsupported'\n | 'asset-artifact-encoding-unsupported'\n | 'asset-artifact-decode-failed';\n\nexport type AssetArtifactErrorDetail =\n | {\n readonly guid: string;\n readonly artifactKey: string;\n readonly observed: string;\n readonly expected: string;\n }\n | {\n readonly guid: string;\n readonly artifactKey: string;\n readonly observed: string;\n readonly expected: string;\n readonly path: string;\n };\n\nexport type AssetArtifactError =\n | {\n readonly code: 'asset-artifact-path-invalid';\n readonly expected: string;\n readonly hint: string;\n readonly detail: Extract<AssetArtifactErrorDetail, { readonly artifactKey: string }>;\n }\n | {\n readonly code: 'asset-artifact-missing';\n readonly expected: string;\n readonly hint: string;\n readonly detail: Extract<AssetArtifactErrorDetail, { readonly path: string }>;\n }\n | {\n readonly code: Exclude<\n AssetArtifactErrorCode,\n 'asset-artifact-path-invalid' | 'asset-artifact-missing'\n >;\n readonly expected: string;\n readonly hint: string;\n readonly detail: Extract<AssetArtifactErrorDetail, { readonly artifactKey: string }>;\n };\n\nexport type PackV2ErrorCode =\n | 'pack-v2-version-unsupported'\n | 'pack-v2-envelope-invalid'\n | 'pack-v2-duplicate-guid'\n | 'pack-v2-duplicate-artifact-key'\n | 'pack-v2-artifact-descriptor-invalid';\n\nexport type PackV2ErrorDetail =\n | { readonly observed: string; readonly expected: string }\n | { readonly guid: string; readonly paths: readonly string[] }\n | { readonly guid: string; readonly artifactKey: string }\n | { readonly guid: string; readonly artifactKey: string; readonly field: string };\n\nexport interface PackV2Error {\n readonly code: PackV2ErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail: PackV2ErrorDetail;\n}\n\nexport type AssetEvidenceErrorDetail =\n | { readonly capability: string; readonly stage: string }\n | { readonly guid: string; readonly observed: string; readonly expected: string };\n\nexport interface AssetEvidenceError {\n readonly code: AssetEvidenceErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail: AssetEvidenceErrorDetail;\n}\n\nexport const ASSET_EVIDENCE_ERROR_HINTS = {\n 'asset-evidence-capability-missing':\n 'provide the missing evidence capability, then rerun asset lookup or verify',\n 'asset-evidence-source-conflict':\n 'keep one source declaration per GUID and rerun the offline evidence projection',\n 'asset-evidence-locator-conflict':\n 'keep one packageUrl per GUID and rebuild the catalog before verifying the asset',\n 'asset-evidence-receipt-conflict':\n 'keep one producer-owned receipt per GUID and rerun cook before verifying the asset',\n 'asset-evidence-digest-mismatch':\n 'recook the source or restore the package bytes, then rerun artifact verification',\n} satisfies Readonly<Record<string, string>>;\n\nexport type AssetEvidenceErrorCode = keyof typeof ASSET_EVIDENCE_ERROR_HINTS;\n\n/** Ordered author-to-runtime stages used by structured recovery errors. */\nexport type AssetErrorStage =\n | 'author-validation'\n | 'external-declaration'\n | 'import'\n | 'native-cook'\n | 'ddc-validation'\n | 'runtime-parse'\n | 'editor-capability';\n\n/** AI-readable next action; it does not grant a cache or runtime write authority. */\nexport interface AssetStageRecovery {\n readonly action: string;\n readonly command?: string;\n readonly retryable: boolean;\n}\n\nexport type AssetStageErrorDetail =\n | { readonly authoringPath: string; readonly rule: string }\n | { readonly sourceKey: string; readonly sourceIndex?: number }\n | { readonly sourcePath: string; readonly importer?: string }\n | { readonly guid: string; readonly producer: string }\n | { readonly guid: string; readonly observedDigest: string; readonly expectedDigest: string }\n | { readonly guid: string; readonly packageUrl: string }\n | { readonly capability: string; readonly assetKind: string };\n\nexport interface AssetStageErrorBase<S extends AssetErrorStage, C extends string> {\n readonly stage: S;\n readonly code: C;\n readonly expected: string;\n readonly hint: string;\n readonly detail: AssetStageErrorDetail;\n readonly recovery: AssetStageRecovery;\n}\n\ntype AssetStageErrorWithDetail<\n S extends AssetErrorStage,\n C extends string,\n D extends AssetStageErrorDetail,\n> = Omit<AssetStageErrorBase<S, C>, 'detail'> & { readonly detail: D };\n\nexport type AuthorValidationError = AssetStageErrorWithDetail<\n 'author-validation',\n 'author-validation-failed',\n Extract<AssetStageErrorDetail, { readonly authoringPath: string }>\n>;\nexport type ExternalDeclarationError = AssetStageErrorWithDetail<\n 'external-declaration',\n 'external-declaration-invalid',\n Extract<AssetStageErrorDetail, { readonly sourceKey: string }>\n>;\nexport type ImportStageError = AssetStageErrorWithDetail<\n 'import',\n 'import-failed',\n Extract<AssetStageErrorDetail, { readonly sourcePath: string }>\n>;\nexport type NativeCookError = AssetStageErrorWithDetail<\n 'native-cook',\n 'native-cook-failed',\n Extract<AssetStageErrorDetail, { readonly guid: string; readonly producer: string }>\n>;\nexport type DdcValidationError = AssetStageErrorWithDetail<\n 'ddc-validation',\n 'ddc-validation-failed',\n Extract<AssetStageErrorDetail, { readonly observedDigest: string }>\n>;\nexport type RuntimeParseError = AssetStageErrorWithDetail<\n 'runtime-parse',\n 'runtime-parse-failed',\n Extract<AssetStageErrorDetail, { readonly packageUrl: string }>\n>;\nexport type EditorCapabilityError = AssetStageErrorWithDetail<\n 'editor-capability',\n 'editor-capability-unavailable',\n Extract<AssetStageErrorDetail, { readonly capability: string }>\n>;\n\nexport type AssetStageError =\n | AuthorValidationError\n | ExternalDeclarationError\n | ImportStageError\n | NativeCookError\n | DdcValidationError\n | RuntimeParseError\n | EditorCapabilityError;\n\nexport type AssetStageErrorCode = AssetStageError['code'];\n\nexport const ASSET_STAGE_ERROR_HINTS: Readonly<Record<AssetStageErrorCode, string>> = {\n 'author-validation-failed': 'read the authoring rule and apply the suggested recovery',\n 'external-declaration-invalid': 'repair the sourceKey declaration and retry recovery',\n 'import-failed': 'fix the importer input or registration, then retry recovery',\n 'native-cook-failed': 'fix the native producer and rerun recovery',\n 'ddc-validation-failed': 'repair the cooked artifact and rerun recovery',\n 'runtime-parse-failed': 'repair the package payload and rerun recovery',\n 'editor-capability-unavailable': 'register the capability and retry recovery',\n};\n","// @forgeax/engine-types — Result<T, E> SSOT (tweak-20260612-result-into-types).\n//\n// `Result<T, E>` is the project-wide binary success/failure carrier. It used to\n// live as TWO byte-aligned copies (`packages/rhi/src/errors.ts` +\n// `packages/ecs/src/result.ts`) — that \"byte-for-byte aligned\" prose was a\n// declaration, not a mechanism, and silently drifted. Consolidating here:\n// - One physical source for the discriminated union + `ok`/`err` factories.\n// - rhi / ecs each keep their typed error class (RhiError / EcsError union)\n// and just re-export the Result shape from this module.\n// - Generic parameter is intentionally NOT defaulted — each consumer narrows\n// the error parameter at its own boundary (`Result<T, RhiError>`,\n// `Result<T, EcsError>`).\n//\n// Charter mapping: P5 consistent abstraction (single Result idiom across rhi\n// and ecs); SSOT data layer (one authoritative carrier, derive don't duplicate).\n\n// ────────────────────────────────────────────────────────────────────────────\n// Narrow shape — boolean discriminant `.ok` + `.value` / `.error` branches\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * Success branch — plain field access (`.ok === true`, `.value: T`) plus\n * method chain (`.unwrap()` / `.unwrapOr(default)`).\n *\n * On the ok branch `unwrap()` returns `.value`; `unwrapOr(d)` ignores the\n * default and returns `.value` (charter proposition 4 explicit-failure: the\n * throwing path lives only on the err branch).\n */\nexport interface ResultOk<T> {\n readonly ok: true;\n readonly value: T;\n /** Return the wrapped value. Never throws on the ok branch. */\n unwrap(): T;\n /** Return the wrapped value; the default argument is unused on the ok branch. */\n unwrapOr(defaultValue: T): T;\n}\n\n/**\n * Failure branch — plain field access (`.ok === false`, `.error: E`) plus\n * method chain (`.unwrap()` / `.unwrapOr(default)`).\n *\n * On the err branch `unwrap()` throws the underlying `E` (not wrapped in a\n * fresh `Error`) so AI consumers preserve `.code` / `.expected` / `.hint` for\n * programmatic recovery (charter proposition 4 + AGENTS.md \"Errors are\n * structured. Return Result, never throw for expected failures.\" — `.unwrap()`\n * is the explicit Layer 3 ErrorHandler boundary, not a hidden throw).\n *\n * `unwrapOr(d)` returns the default; the original error is silently dropped.\n * Use `if (!r.ok) ...` plus `r.error` if you need to inspect the failure.\n */\nexport interface ResultErr<E> {\n readonly ok: false;\n readonly error: E;\n /** Throw the underlying `error` (preserved without rewrapping). */\n unwrap(): never;\n /** Return the supplied default value (the original error is silently dropped). */\n unwrapOr<T>(defaultValue: T): T;\n}\n\n/**\n * Discriminated union: either `ResultOk<T>` or `ResultErr<E>`.\n *\n * Use `ok(value)` / `err(error)` factories to create instances.\n * Use `if (r.ok) ...` / `if (!r.ok) ...` to narrow.\n *\n * Width-assignment friendly: a `ResultErr<E>` returned by `err(...)` typed as\n * `Result<never, E>` satisfies any `Result<X, E>` after a `if (!r.ok)` narrow,\n * so `return r;` propagates without a cast.\n */\nexport type Result<T, E> = ResultOk<T> | ResultErr<E>;\n\n// ────────────────────────────────────────────────────────────────────────────\n// Prototype objects (shared methods — dimorphic, V8 friendly)\n// ────────────────────────────────────────────────────────────────────────────\n\nconst OK_PROTO = {\n unwrap(this: ResultOk<unknown>): unknown {\n return this.value;\n },\n unwrapOr(this: ResultOk<unknown>, _defaultValue: unknown): unknown {\n return this.value;\n },\n};\n\nconst ERR_PROTO = {\n unwrap(this: ResultErr<unknown>): never {\n // Throw the ORIGINAL error — NOT wrapped in new Error()\n throw this.error;\n },\n unwrapOr<T>(this: ResultErr<unknown>, defaultValue: T): T {\n return defaultValue;\n },\n};\n\n// ────────────────────────────────────────────────────────────────────────────\n// Factory functions\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * Construct a success branch.\n *\n * Returns the narrow `ResultOk<T>` shape so direct `.value` access works\n * without a `if (r.ok)` narrow at the call site (a `ResultOk<T>` widens to\n * `Result<T, E>` for any `E` by structural assignment).\n */\nexport function ok<T>(value: T): ResultOk<T> {\n const r = Object.create(OK_PROTO) as { ok: true; value: T };\n r.ok = true;\n r.value = value;\n return r as ResultOk<T>;\n}\n\n/**\n * Construct a failure branch.\n *\n * Returns the narrow `ResultErr<E>` shape so direct `.error` access works\n * without a `if (!r.ok)` narrow at the call site. After a `if (!r.ok) return\n * r;` narrow, `ResultErr<E>` widens structurally to any `Result<X, E>` so\n * `return r;` propagates without a cast.\n */\nexport function err<E>(error: E): ResultErr<E> {\n const r = Object.create(ERR_PROTO) as { ok: false; error: E };\n r.ok = false;\n r.error = error;\n return r as ResultErr<E>;\n}\n","import type {\n ArtifactDescriptor,\n ArtifactVerificationStatus,\n ContentEncoding,\n CookFreshness,\n CookOrigin,\n CookReceiptStatus,\n CookStatus,\n Integrity,\n RuntimeEvidenceStatus,\n} from './asset.js';\nimport {\n ASSET_EVIDENCE_ERROR_HINTS,\n type AssetEvidenceError,\n type AssetEvidenceErrorCode,\n} from './asset-errors.js';\nimport { err, ok, type Result } from './result.js';\n\nexport type {\n AssetEvidenceError,\n AssetEvidenceErrorCode,\n AssetEvidenceErrorDetail,\n} from './asset-errors.js';\n\n/** Producer-owned cook attempt; a receipt is evidence only for its own input fingerprint. */\nexport interface CookReceipt {\n readonly guid: string;\n readonly origin: CookOrigin;\n readonly status: CookReceiptStatus;\n readonly inputFingerprint: string;\n readonly outputDigest?: string;\n readonly error?: {\n readonly code: string;\n readonly expected: string;\n readonly hint: string;\n readonly detail?: unknown;\n };\n}\n\nexport type { CookProduct } from './asset.js';\n\n/** Source-side declaration used to compare current input with a producer receipt. */\nexport interface SourceDeclarationEvidence {\n readonly origin: CookOrigin;\n readonly sourcePath?: string;\n readonly inputFingerprint?: string;\n}\n\n/** Verification state for the Pack v2 package envelope, independent of cook freshness. */\nexport interface PackageVerificationEvidence {\n readonly status: ArtifactVerificationStatus;\n readonly digest?: string;\n}\n\n/** Descriptor plus the explicit verification result for one package artifact. */\nexport interface AssetEvidenceArtifact {\n readonly descriptor: ArtifactDescriptor;\n readonly verification: ArtifactVerificationStatus;\n}\n\nexport type AssetEvidenceStatus = 'passed' | 'notChecked' | 'failed' | 'unknown';\n\n/** Derived GUID evidence; use the closed states literally and follow error hints. */\nexport interface AssetEvidence {\n readonly guid: string;\n readonly packageUrl?: string;\n readonly cookReceiptUrl?: string;\n readonly source?: SourceDeclarationEvidence;\n readonly cook: {\n readonly status: CookStatus;\n readonly freshness: CookFreshness;\n readonly receipt?: CookReceipt;\n };\n readonly package?: PackageVerificationEvidence;\n readonly artifacts: Readonly<Record<string, AssetEvidenceArtifact>>;\n readonly runtime: {\n readonly status: RuntimeEvidenceStatus;\n };\n}\n\n/** Catalog navigation only; the URLs locate evidence but do not prove readiness. */\nexport interface AssetEvidenceLocator {\n readonly packageUrl: string;\n readonly cookReceiptUrl?: string;\n}\n\nexport interface AssetEvidencePackageInput {\n readonly guid: string;\n readonly digest?: string;\n readonly artifacts: Readonly<\n Record<\n string,\n {\n readonly descriptor: ArtifactDescriptor;\n readonly verification?: ArtifactVerificationStatus;\n }\n >\n >;\n}\n\n/** Inputs accepted by the pure projector; producers supply facts, not inferred status. */\nexport interface AssetEvidenceInputs {\n readonly guid: string;\n readonly source?: SourceDeclarationEvidence;\n readonly sources?: readonly SourceDeclarationEvidence[];\n readonly locator?: AssetEvidenceLocator;\n readonly locators?: readonly AssetEvidenceLocator[];\n readonly receipt?: CookReceipt;\n readonly receipts?: readonly CookReceipt[];\n readonly packageVerification?: PackageVerificationEvidence;\n readonly package?: AssetEvidencePackageInput;\n readonly artifacts?: Readonly<Record<string, ArtifactDescriptor>>;\n readonly artifactVerification?: Readonly<Record<string, ArtifactVerificationStatus>>;\n readonly runtime?: { readonly status: RuntimeEvidenceStatus };\n}\n\n/** Project the one public producer product into the shared evidence view. */\nexport function projectCookProductEvidence(\n product: import('./asset.js').CookProduct,\n locator?: AssetEvidenceLocator,\n): Result<AssetEvidence, AssetEvidenceError> {\n if (product.receipt.guid !== product.guid) {\n return conflict(\n 'asset-evidence-receipt-conflict',\n product.guid,\n product.receipt.guid,\n product.guid,\n );\n }\n if (product.receipt.outputDigest !== product.digest) {\n return conflict(\n 'asset-evidence-digest-mismatch',\n product.guid,\n product.digest,\n product.receipt.outputDigest ?? 'missing receipt digest',\n );\n }\n return projectAssetEvidence({\n guid: product.guid,\n source: {\n origin: product.receipt.origin,\n inputFingerprint: product.receipt.inputFingerprint,\n },\n ...(locator === undefined ? {} : { locator }),\n receipt: product.receipt,\n package: {\n guid: product.guid,\n digest: product.digest,\n artifacts: Object.fromEntries(\n Object.entries(product.artifacts).map(([key, descriptor]) => [\n key,\n { descriptor, verification: 'passed' as const },\n ]),\n ),\n },\n });\n}\n\nexport { ASSET_EVIDENCE_ERROR_HINTS } from './asset-errors.js';\n\nfunction conflict(\n code: AssetEvidenceErrorCode,\n guid: string,\n observed: string,\n expected: string,\n): Result<never, AssetEvidenceError> {\n return err({\n code,\n expected,\n hint: ASSET_EVIDENCE_ERROR_HINTS[code],\n detail: { guid, observed, expected },\n });\n}\n\nfunction distinct<T>(values: readonly T[], key: (value: T) => string): readonly T[] {\n const result: T[] = [];\n const seen = new Set<string>();\n for (const value of values) {\n const identity = key(value);\n if (!seen.has(identity)) {\n seen.add(identity);\n result.push(value);\n }\n }\n return result;\n}\n\nfunction chooseSource(\n inputs: AssetEvidenceInputs,\n): Result<SourceDeclarationEvidence | undefined, AssetEvidenceError> {\n const values = distinct(\n [inputs.source, ...(inputs.sources ?? [])].filter(\n (value): value is SourceDeclarationEvidence => value !== undefined,\n ),\n (value) => JSON.stringify(value),\n );\n if (values.length > 1) {\n return conflict(\n 'asset-evidence-source-conflict',\n inputs.guid,\n JSON.stringify(values),\n 'one source declaration per GUID',\n );\n }\n return ok(values[0]);\n}\n\nfunction chooseLocator(\n inputs: AssetEvidenceInputs,\n): Result<AssetEvidenceLocator | undefined, AssetEvidenceError> {\n const values = distinct(\n [inputs.locator, ...(inputs.locators ?? [])].filter(\n (value): value is AssetEvidenceLocator => value !== undefined,\n ),\n (value) => JSON.stringify(value),\n );\n if (values.length > 1) {\n return conflict(\n 'asset-evidence-locator-conflict',\n inputs.guid,\n JSON.stringify(values),\n 'one package locator per GUID',\n );\n }\n return ok(values[0]);\n}\n\nfunction chooseReceipt(\n inputs: AssetEvidenceInputs,\n): Result<CookReceipt | undefined, AssetEvidenceError> {\n const values = distinct(\n [inputs.receipt, ...(inputs.receipts ?? [])].filter(\n (value): value is CookReceipt => value !== undefined,\n ),\n (value) => JSON.stringify(value),\n );\n if (values.length > 1) {\n return conflict(\n 'asset-evidence-receipt-conflict',\n inputs.guid,\n JSON.stringify(values),\n 'one cook receipt per GUID',\n );\n }\n const receipt = values[0];\n if (receipt !== undefined && receipt.guid.toLowerCase() !== inputs.guid.toLowerCase()) {\n return conflict(\n 'asset-evidence-receipt-conflict',\n inputs.guid,\n receipt.guid,\n `receipt GUID ${inputs.guid}`,\n );\n }\n return ok(receipt);\n}\n\nfunction packageEvidence(inputs: AssetEvidenceInputs): PackageVerificationEvidence | undefined {\n if (inputs.packageVerification !== undefined) return inputs.packageVerification;\n if (inputs.package === undefined) return undefined;\n const statuses = Object.values(inputs.package.artifacts).map(\n (artifact) => artifact.verification ?? 'notChecked',\n );\n const status = statuses.some((value) => value === 'failed')\n ? 'failed'\n : statuses.length > 0 && statuses.every((value) => value === 'passed')\n ? 'passed'\n : 'notChecked';\n return {\n status,\n ...(inputs.package.digest !== undefined ? { digest: inputs.package.digest } : {}),\n };\n}\n\nfunction artifactsEvidence(\n inputs: AssetEvidenceInputs,\n): Readonly<Record<string, AssetEvidenceArtifact>> {\n const descriptors =\n inputs.artifacts ??\n Object.fromEntries(\n Object.entries(inputs.package?.artifacts ?? {}).map(([key, value]) => [\n key,\n value.descriptor,\n ]),\n );\n return Object.fromEntries(\n Object.entries(descriptors).map(([key, descriptor]) => [\n key,\n {\n descriptor,\n verification:\n inputs.artifactVerification?.[key] ??\n inputs.package?.artifacts[key]?.verification ??\n 'notChecked',\n },\n ]),\n );\n}\n\nfunction freshness(\n source: SourceDeclarationEvidence | undefined,\n receipt: CookReceipt | undefined,\n): CookFreshness {\n if (source?.origin === 'authoredPack') return 'notApplicable';\n if (receipt === undefined) return 'unknown';\n if (source?.inputFingerprint === undefined) return 'unknown';\n return source.inputFingerprint === receipt.inputFingerprint ? 'current' : 'stale';\n}\n\nfunction cookStatus(\n source: SourceDeclarationEvidence | undefined,\n receipt: CookReceipt | undefined,\n): CookStatus {\n if (source?.origin === 'authoredPack') return 'notRequired';\n if (receipt?.status === 'failed') return 'failed';\n if (receipt?.status === 'succeeded') return 'ready';\n if (source?.origin === 'sourceMeta') return 'notCooked';\n return 'unknown';\n}\n\n/** Join source, locator, receipt, package, artifact, and runtime facts into one view. */\nexport function projectAssetEvidence(\n inputs: AssetEvidenceInputs,\n): Result<AssetEvidence, AssetEvidenceError> {\n const sourceResult = chooseSource(inputs);\n if (!sourceResult.ok) return sourceResult;\n const locatorResult = chooseLocator(inputs);\n if (!locatorResult.ok) return locatorResult;\n const receiptResult = chooseReceipt(inputs);\n if (!receiptResult.ok) return receiptResult;\n\n const source = sourceResult.value;\n const locator = locatorResult.value;\n const receipt = receiptResult.value;\n const packageVerification = packageEvidence(inputs);\n if (\n receipt?.outputDigest !== undefined &&\n packageVerification?.digest !== undefined &&\n receipt.outputDigest !== packageVerification.digest\n ) {\n return conflict(\n 'asset-evidence-digest-mismatch',\n inputs.guid,\n packageVerification.digest,\n receipt.outputDigest,\n );\n }\n\n return ok({\n guid: inputs.guid,\n ...(locator?.packageUrl !== undefined ? { packageUrl: locator.packageUrl } : {}),\n ...(locator?.cookReceiptUrl !== undefined ? { cookReceiptUrl: locator.cookReceiptUrl } : {}),\n ...(source !== undefined ? { source } : {}),\n cook: {\n status: cookStatus(source, receipt),\n freshness: freshness(source, receipt),\n ...(receipt !== undefined ? { receipt } : {}),\n },\n ...(packageVerification !== undefined ? { package: packageVerification } : {}),\n artifacts: artifactsEvidence(inputs),\n runtime: { status: inputs.runtime?.status ?? 'unknown' },\n });\n}\n\nexport type {\n ArtifactVerificationStatus,\n CookFreshness,\n CookStatus,\n RuntimeEvidenceStatus,\n} from './asset.js';\nexport type { ArtifactDescriptor, ContentEncoding, Integrity };\n","// @forgeax/engine-types - Handle<T,M> brand + AssetTagMap + TagOf + 3 helpers SSOT.\n//\n// Single physical source-of-truth (feat-20260517-handle-type-unify M1 / D-2 / D-4 / D-7).\n// The package barrel `index.ts` re-exports this file; AI users import the\n// brand and helpers via `@forgeax/engine-types`, and IDE hover lands on this\n// file (charter F1 single-entry indexability).\n//\n// Contents (charter P4 consistent abstraction - 5 co-located building blocks):\n// - type Handle<T extends string, M extends 'unique' | 'shared'>\n// (double-axis phantom brand on top of `number`)\n// - type UniqueHandle<T> / SharedHandle<T> (mode-pinned aliases)\n// - interface AssetTagMap (14-member closed map mesh/texture/cube-texture/sampler/material/scene/audio/skin/skeleton/animation-clip/shader/font/render-pipeline/tileset/video)\n// - type TagOf<T extends Asset> (distributive conditional - 14+1 never tail)\n// - function toUnique<T>(raw) / toShared<T>(raw) (brand creation factories)\n// - function unwrapHandle<T,M>(h) (brand removal helper - cast inverse)\n//\n// The single `as Handle<T, M>` cast inside each factory is the brand-creation\n// structural cast (D-7 + AC-01 exemption); all other call sites must route\n// through these factories or `unwrapHandle` so that no `as unknown as Handle`\n// or `as unknown as number` literal survives anywhere outside this file\n// (AC-01 grep gate, M3 / M4 cleanup).\n//\n// Charter mapping: F1 (single-entry IDE autocomplete from\n// `@forgeax/engine-types`) + P3 (cross-mode rejection is a TS compile-time\n// failure red line) + P4 (consistent abstraction: brand + map + 3 factories\n// + 1 distributive conditional all co-located in this 1 file).\n\nimport type { Asset } from './index';\n\n/**\n * Phantom-branded Handle: a `number` carrying two type tags.\n *\n * @typeParam T - asset target tag (string literal, e.g. `'MeshAsset'`)\n * @typeParam M - release mode: `'unique'` (ECS-tracked via UniqueRefStore)\n * or `'shared'` (external owner, e.g. `AssetRegistry`)\n *\n * Runtime representation is a u32 number so the GPU upload path\n * (`GPUBuffer.writeBuffer(slot, ...)`) keeps zero-cost passthrough; only the\n * TS layer enforces non-assignability across modes / targets via the\n * `__handle` phantom field. The `__handle` field is type-only - runtime\n * objects never carry it (charter P4 zero-overhead abstraction).\n *\n * Cross-tag rejection: `Handle<'MeshAsset', M>` is not assignable to\n * `Handle<'TextureAsset', M>` and vice versa (the brand `target` field\n * differs).\n *\n * Cross-mode rejection: `Handle<T, 'unique'>` is not assignable to\n * `Handle<T, 'shared'>` and vice versa (the brand `mode` field differs);\n * this is the TS compile-time wall that prevents accidentally feeding a\n * unique-mode handle to a registry that owns its own release lifecycle (charter\n * P3 explicit failure red line; tests live in\n * `packages/types/src/__tests__/handle-brand.test-d.ts` and\n * `packages/ecs/src/__tests__/handle.test-d.ts`).\n *\n * AI users do not write `as Handle<...>` - handles come from registry\n * factories (`engine.assets.register<T>(asset).unwrap()` produces\n * `Handle<TagOf<T>, 'shared'>`; `world.uniqueRefs.alloc<T>(value)`\n * produces `Handle<T, 'unique'>` after M2). The only `as Handle` literal in the\n * codebase is the brand-creation cast inside `toUnique` / `toShared`\n * below (AC-01 exemption).\n */\nexport type Handle<T extends string, M extends 'unique' | 'shared'> = number & {\n readonly __handle: { readonly target: T; readonly mode: M };\n};\n\n/**\n * Convenience alias - unique-mode handle for asset target `T`.\n *\n * Intended for ECS-internal consumption (column slot read sites in the\n * unique-ref store, M2 rename). The `@forgeax/engine-ecs` barrel does NOT\n * re-export this alias name (AC-15 grep gate - keeps the AI-facing surface\n * narrow); callers outside ecs continue to write `Handle<T, 'unique'>`\n * literally.\n *\n * Schema vocab `'unique<T>'` derives the column field type to this alias via\n * `FieldValueType<T>` conditional inference (see\n * `packages/ecs/src/component.ts`).\n */\nexport type UniqueHandle<T extends string> = Handle<T, 'unique'>;\n\n/**\n * Convenience alias - shared-mode handle for asset target `T`.\n *\n * Mirrors `UniqueHandle<T>` for the refcounted-owner side; surfaces on\n * `AssetRegistry.register<T>` return signatures and `MeshFilter.assetHandle`\n * column type. Re-exported by the `@forgeax/engine-ecs` barrel (alongside\n * `Handle`) so AI users importing from ecs see the alias - this remains\n * subordinate to writing `Handle<T, 'shared'>` literally.\n *\n * Schema vocab `'shared<T>'` derives the column field type to this alias\n * via `FieldValueType<T>` conditional inference (feat-20260614 M5).\n */\nexport type SharedHandle<T extends string> = Handle<T, 'shared'>;\n\n/**\n * Asset.kind tag map - 13-member closed map keying each Asset variant\n * `kind` literal to its TS type name string literal (D-1 path (a)).\n *\n * Used by `TagOf<T>` distributive conditional below to derive the brand\n * `target` tag from an Asset variant TS type at register / inference time;\n * AI users adding a new Asset variant minor-add the corresponding\n * `kind -> 'XxxAsset'` row here so that `register<NewVariant>(asset)` returns\n * the correct `Handle<'XxxAsset', 'shared'>` automatically (this map is\n * the single must-edit point per Asset addition - charter F1 single-entry\n * indexability).\n *\n * The 13 members align byte-for-byte with the closed `Asset` union; adding\n * a new member to `Asset` without adding a row here surfaces as a\n * `TagOf<NewAsset>` resolving to `never` (charter P3 explicit failure -\n * downstream `register<NewAsset>` calls fail to compile).\n */\nexport interface AssetTagMap {\n mesh: 'MeshAsset';\n texture: 'TextureAsset';\n equirect: 'EquirectAsset';\n sampler: 'SamplerAsset';\n material: 'MaterialAsset';\n scene: 'SceneAsset';\n audio: 'AudioClipAsset';\n /** feat-20260523-skin-skeleton-animation M0 */\n skin: 'SkinAsset';\n /** feat-20260523-skin-skeleton-animation M0 */\n skeleton: 'SkeletonAsset';\n /** feat-20260523-skin-skeleton-animation M0 */\n 'animation-clip': 'AnimationClip';\n /** feat-20260713-animation-state-machine-plugin M2 / w13 */\n 'animation-graph': 'AnimationGraph';\n /** feat-20260528-material-shader-registration-unification M1 / w1 */\n /** feat-20260531-world-space-msdf-text-rendering M2 / w5 */\n font: 'FontAsset';\n /** feat-20260601-customizable-render-pipeline-seam-and-dogfood-rend M1 / w5 */\n 'render-pipeline': 'RenderPipelineAsset';\n /** feat-20260608-tilemap-object-layer-rendering M0 baseline rebuild */\n tileset: 'TilesetAsset';\n /** feat-20260623-world-space-video-asset M1 / w2 */\n video: 'VideoAsset';\n /** feat-20260728-wave1-vfx-contract-and-asset-cook M1 / m1-i1 */\n 'particle-effect': 'ParticleEffectAsset';\n}\n\n/**\n * Distributive conditional - maps an Asset variant TS type to its brand\n * `target` tag string literal (D-1 path (a)).\n *\n * `TagOf<MeshAsset>` resolves to `'MeshAsset'`; `TagOf<MaterialAsset>`\n * resolves to `'MaterialAsset'` even though `MaterialAsset` is itself the\n * pass-based single interface (MaterialAsset) - the\n * distributive conditional resolves to 'MaterialAsset' via kind: 'material'\n * collapse onto `'MaterialAsset'` because both share `kind: 'material'`\n * (research Finding 2).\n *\n * Asset variants without a matching `AssetTagMap` row (or a `kind` literal\n * outside the 5 closed values) resolve to `never`, surfacing the missing\n * row at every downstream `register<T>` consumer site (charter P3 explicit\n * failure).\n */\nexport type TagOf<T extends Asset> = T extends { kind: infer K }\n ? K extends keyof AssetTagMap\n ? AssetTagMap[K]\n : never\n : never;\n\n/**\n * Construct a `Handle<T, 'unique'>` from a raw u32. Brand-creation\n * structural cast - the `as Handle<T, 'unique'>` literal here is the\n * AC-01 exemption single point of brand creation (D-7); all other call\n * sites must route through this factory.\n *\n * Used by `World.allocUniqueRef<T>(value)` (M2 rename of allocUniqueRef)\n * to brand fresh handles that the ECS will track via the per-row release\n * loop. AI users typically\n * do not call this directly - it is the brand-creation primitive that the\n * ecs / runtime layers wrap.\n */\nexport function toUnique<T extends string>(raw: number): Handle<T, 'unique'> {\n return raw as Handle<T, 'unique'>;\n}\n\n/**\n * Construct a `Handle<T, 'shared'>` from a raw u32. Brand-creation\n * structural cast - the `as Handle<T, 'shared'>` literal here is the\n * AC-01 exemption single point of brand creation (D-7).\n *\n * Used by `AssetRegistry.register<T>(asset).unwrap()` to brand the returned handle\n * with `Handle<TagOf<T>, 'shared'>`, and by builtin handle constants\n * (`HANDLE_CUBE` / `HANDLE_TRIANGLE` / `HANDLE_ROOM_CUBE` / `BUILTIN_HANDLE_*`)\n * to brand compile-time u32 literals without caller-side `as unknown as`\n * casts (AC-05).\n */\nexport function toShared<T extends string>(raw: number): Handle<T, 'shared'> {\n return raw as Handle<T, 'shared'>;\n}\n\n/**\n * Remove the `Handle<T, M>` brand and recover the raw u32 carried inside.\n * Brand-removal helper - the inverse of `toUnique` / `toShared`.\n *\n * Runtime is identity (the brand `__handle` field is type-only; the\n * underlying number value is unchanged); the helper exists purely to\n * collapse all `as unknown as number` cast sites into a single function so\n * that AC-01 grep can surface stragglers (D-7 / D-8 cast collapse plan).\n *\n * Public on the types barrel (parallel to `toUnique` / `toShared`):\n * column read sites in unique-ref-store (M2 rename) / scene-instance-container,\n * AssetRegistry internal `Map<number, ...>` key reads, and any AI-user\n * code that needs to bridge a branded handle to a numeric ABI all call\n * this. AI users on the typical spawn-site / register-site surface\n * usually do not need it (charter P1 progressive disclosure — handle\n * stays branded end-to-end), but when a numeric escape is required this\n * is the single sanctioned escape.\n */\nexport function unwrapHandle<T extends string, M extends 'unique' | 'shared'>(\n h: Handle<T, M>,\n): number {\n return h;\n}\n\n/**\n * Slot boundary between the builtin tier and the user tier (feat-20260614 M6\n * D-15 / D-16). Builtin asset handles (the 5 process-static meshes:\n * HANDLE_CUBE=1 .. HANDLE_NINESLICE_QUAD=5) occupy slots `[1, BUILTIN_BASE)`;\n * user-tier handles minted by `World.sharedRefs.alloc` start at `BUILTIN_BASE`.\n *\n * Defined here in `@forgeax/engine-types` — the single dependency shared by\n * both `@forgeax/engine-ecs` (SharedRefStore `nextSlot` init + builtin-slot\n * fail-fast) and `@forgeax/engine-runtime` (BuiltinAssetRegistry resolve\n * dispatch + AssetRegistry index) — so the boundary is one named constant with\n * no cross-package circular dependency. Value 1024 is the historic\n * `FIRST_USER_HANDLE` literal, promoted to the shared SSOT.\n */\nexport const BUILTIN_BASE = 1024;\n\n// === Gen-slot codec SSOT (feat-20260623-asset-handle-generation M1 / w2) ==========\n//\n// Domain-agnostic pure bit operations — max-slot, max-gen, pack, unpack-slot,\n// unpack-gen, and the retire predicate (isRetiredSlot: gen > MAX_GEN, so gen\n// 255 is usable and a slot retires only when its bump would reach 256). This\n// is the single definition point\n// for the `(gen << 24) | slot` layout across entity and asset handles (D-1,\n// AC-15). Callers (ecs / runtime / ref stores) import from\n// @forgeax/engine-types; entity-side overflow throw and sentinel stay in ecs\n// (D-1). No domain concepts live here — just mask and shift.\n//\n// Bit layout (OOS-3: 32-bit number, 24-bit slot + 8-bit gen):\n// - slot: bits [0, 23], max value (1 << 24) - 1 = 16_777_215\n// - gen: bits [24, 31], max value 0xff = 255\n//\n// pack(slot, gen) fixed to (((gen & 0xff) << 24) | (slot & 0xffffff)) >>> 0\n// per D-7 hard constraint — the `>>> 0` prevents ToInt32 negative when\n// gen >= 128 (entity-handle.ts:73 comment documents this trap).\n\n/** Maximum slot index (2^24 - 1 = 16_777_215). */\nexport const MAX_SLOT = (1 << 24) - 1;\n\n/** Maximum generation value (2^8 - 1 = 255). */\nexport const MAX_GEN = 0xff;\n\n/**\n * Pack (slot, gen) into a u32 handle.\n *\n * The caller is responsible for ensuring slot does not exceed MAX_SLOT;\n * values above 24 bits are masked by `slot & 0xffffff`. Generation is\n * masked to 8 bits via `gen & 0xff`. The `>>> 0` forces unsigned u32\n * representation — without it, gen >= 128 produces a signed-negative\n * ToInt32 (D-7 hard constraint, entity-handle.ts:73).\n */\nexport function pack(slot: number, gen: number): number {\n return (((gen & 0xff) << 24) | (slot & 0xffffff)) >>> 0;\n}\n\n/** Extract the low 24 bits (slot) from a packed u32 handle. */\nexport function unpackSlot(v: number): number {\n return v & 0xffffff;\n}\n\n/** Extract the high 8 bits (generation) from a packed u32 handle. */\nexport function unpackGen(v: number): number {\n return (v >>> 24) & 0xff;\n}\n\n/**\n * Retire-when-gen-exceeds-MAX_GEN semantic: returns `true` when gen has\n * exceeded MAX_GEN (255), i.e. gen 255 is still a usable handle; a slot\n * retires only when its bumped generation reaches 256. A retired slot\n * never returns to the free list (AC-07).\n */\nexport function isRetiredSlot(gen: number): boolean {\n return gen > MAX_GEN;\n}\n\n// === Handle inspection helpers (feat-20260623-asset-handle-generation M1 / w2) ====\n//\n// Thin wrappers over the shared codec for Handle<T, M> consumers. handleSlot\n// and handleGeneration internally call unpackSlot / unpackGen — these are the\n// migration targets for unwrapHandle sites that only need the slot or gen\n// (decision q4). unwrapHandle stays as-is for sites that need the full\n// encoded value (D-5 excluded round-trip sites).\n\n/**\n * Extract the slot (low 24 bits) from a branded Handle.\n *\n * This is the runtime identity of the handle — the slot index that maps to\n * store payloads / GPU resource keys. Call sites that currently use\n * `unwrapHandle(h)` as a Map key should migrate here so the key stays stable\n * when gen > 0 (AC-09).\n */\nexport function handleSlot<T extends string, M extends 'unique' | 'shared'>(\n h: Handle<T, M>,\n): number {\n return unpackSlot(h as unknown as number);\n}\n\n/**\n * Extract the generation (high 8 bits) from a branded Handle.\n *\n * Used by store-level gen comparisons (resolve/retain/release) to detect\n * stale handles — the gen embedded during alloc is compared against the\n * store's current gen for the same slot.\n */\nexport function handleGeneration<T extends string, M extends 'unique' | 'shared'>(\n h: Handle<T, M>,\n): number {\n return unpackGen(h as unknown as number);\n}\n","import type { AssetGuid } from '../index.js';\nimport type { MaterialColorSpace } from './color-space.js';\n\nexport type MaterialParameterType =\n | 'bool'\n | 'f32'\n | 'i32'\n | 'u32'\n | 'vec2'\n | 'vec3'\n | 'vec4'\n | 'color'\n | 'texture';\n\nexport interface MaterialParameter {\n readonly name: string;\n readonly type: MaterialParameterType;\n /**\n * Asset-side transfer function for an authored color value. `color`\n * parameters default to `srgb`; numeric vectors default to linear unless\n * explicitly tagged. An asset-level `colorSpace` overrides this schema\n * default. Runtime material values are always linear.\n */\n readonly colorSpace?: MaterialColorSpace;\n readonly default?: MaterialValue;\n readonly optional?: boolean;\n}\n\nexport interface MaterialTextureCoordinates {\n readonly set?: number;\n /** Producer-owned logical-to-physical texture extent correction. */\n readonly physicalUvScale?: readonly [number, number];\n readonly transform?: {\n readonly offset?: readonly [number, number];\n readonly scale?: readonly [number, number];\n readonly rotation?: number;\n };\n}\n\nexport interface ResolvedMaterialTextureCoordinates {\n readonly set: number;\n readonly transform: {\n readonly offset: readonly [number, number];\n readonly scale: readonly [number, number];\n readonly rotation: number;\n };\n}\n\nexport function resolveMaterialTextureCoordinates(\n coordinates?: MaterialTextureCoordinates,\n): ResolvedMaterialTextureCoordinates {\n return {\n set: coordinates?.set ?? 0,\n transform: {\n offset: coordinates?.transform?.offset ?? [0, 0],\n scale: coordinates?.transform?.scale ?? [1, 1],\n rotation: coordinates?.transform?.rotation ?? 0,\n },\n };\n}\n\nexport type MaterialTextureReference = AssetGuid | number | string;\n\nexport interface MaterialTextureValue {\n readonly texture: MaterialTextureReference;\n readonly sampler?: MaterialTextureReference;\n readonly coordinates?: MaterialTextureCoordinates;\n readonly normalScale?: number;\n readonly occlusionStrength?: number;\n}\n\nexport type MaterialValue = boolean | number | readonly number[] | string | MaterialTextureValue;\n\nexport interface MaterialProgram {\n readonly module: string;\n readonly vertexEntry?: string;\n readonly fragmentEntry?: string;\n readonly moduleSlots?: Readonly<Record<string, string>>;\n}\n\nexport interface MaterialPass {\n readonly name: string;\n readonly program: MaterialProgram;\n readonly renderState?: Readonly<Record<string, unknown>>;\n}\n\nexport type MaterialPassList = readonly [MaterialPass, ...MaterialPass[]];\n\n/**\n * One authored material subject. Pack publishes the resolved contract;\n * runtime and render consume read-only projections. Values and module slots\n * are runtime data with a closed compiler context; compiler macros and\n * feature defines are not part of this contract.\n */\nexport interface MaterialAsset {\n readonly kind: 'material';\n /**\n * Transfer function override for all authored color parameters. Omitted\n * parameters ultimately default to sRGB.\n * Explicit `linear` is reserved for physical/imported data such as glTF\n * factors. Numeric values are never rewritten when this metadata changes.\n */\n readonly colorSpace?: MaterialColorSpace;\n readonly parent?: AssetGuid;\n readonly passes?: MaterialPassList;\n readonly parameters?: readonly MaterialParameter[];\n readonly values?: Readonly<Record<string, MaterialValue | null>>;\n}\n\nexport interface MaterialAuthoringErrorDetail {\n readonly code: 'material-authoring-field-forbidden';\n readonly owner: 'material-runtime' | 'material-authoring';\n readonly field: string;\n readonly actual: unknown;\n readonly action: 'remove-field';\n}\n\nexport class MaterialAssetContractError extends Error {\n readonly code = 'material-authoring-field-forbidden' as const;\n readonly expected =\n 'material authoring contains only runtime values and source-owned module selection';\n readonly hint =\n 'remove the compiler macro field and use a runtime value, module slot, or compiler context';\n readonly detail: MaterialAuthoringErrorDetail;\n\n constructor(detail: Omit<MaterialAuthoringErrorDetail, 'code'>) {\n super(`${detail.field}: material compiler macro fields are not supported`);\n this.name = 'MaterialAssetContractError';\n this.detail = { code: 'material-authoring-field-forbidden', ...detail };\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Validate JSON-authored material descriptors at their loading boundary. */\nexport function assertMaterialAsset(\n value: unknown,\n context = 'material',\n): asserts value is MaterialAsset {\n if (!isRecord(value) || value.kind !== 'material') {\n throw new Error(`${context}: expected a material asset`);\n }\n for (const field of Object.keys(value)) {\n if (!['kind', 'colorSpace', 'parent', 'passes', 'parameters', 'values'].includes(field)) {\n throw new MaterialAssetContractError({\n owner:\n field === 'features' || field === 'defines' ? 'material-authoring' : 'material-runtime',\n field,\n actual: value[field],\n action: 'remove-field',\n });\n }\n }\n if (\n value.colorSpace !== undefined &&\n value.colorSpace !== 'srgb' &&\n value.colorSpace !== 'linear'\n ) {\n throw new Error(`${context}: invalid colorSpace`);\n }\n if (value.passes !== undefined) {\n if (!Array.isArray(value.passes) || value.passes.length === 0) {\n throw new Error(`${context}: passes must be a non-empty array`);\n }\n for (const [index, pass] of value.passes.entries()) {\n if (!isRecord(pass) || typeof pass.name !== 'string' || !isRecord(pass.program)) {\n throw new Error(`${context}: pass ${index} is malformed`);\n }\n if (typeof pass.program.module !== 'string' || pass.program.module.length === 0) {\n throw new Error(`${context}: pass ${index} has no module identity`);\n }\n if (\n (pass.program.vertexEntry !== undefined && typeof pass.program.vertexEntry !== 'string') ||\n (pass.program.fragmentEntry !== undefined && typeof pass.program.fragmentEntry !== 'string')\n ) {\n throw new Error(`${context}: pass ${index} has malformed entry points`);\n }\n if (pass.program.moduleSlots !== undefined) {\n if (!isRecord(pass.program.moduleSlots)) {\n throw new Error(`${context}: pass ${index} has malformed module slots`);\n }\n for (const [name, slot] of Object.entries(pass.program.moduleSlots)) {\n if (typeof slot !== 'string')\n throw new Error(`${context}: module slot ${name} is not a string`);\n }\n }\n }\n }\n if (value.parameters !== undefined) {\n if (!Array.isArray(value.parameters))\n throw new Error(`${context}: parameters must be an array`);\n for (const [index, parameter] of value.parameters.entries()) {\n if (\n !isRecord(parameter) ||\n typeof parameter.name !== 'string' ||\n typeof parameter.type !== 'string'\n ) {\n throw new Error(`${context}: parameter ${index} is malformed`);\n }\n if (\n parameter.colorSpace !== undefined &&\n parameter.colorSpace !== 'srgb' &&\n parameter.colorSpace !== 'linear'\n ) {\n throw new Error(`${context}: parameter ${index} has invalid colorSpace`);\n }\n if ('static' in parameter) {\n throw new MaterialAssetContractError({\n owner: 'material-authoring',\n field: 'parameters.static',\n actual: parameter.static,\n action: 'remove-field',\n });\n }\n }\n }\n}\n","import type { MaterialValue } from './asset.js';\n\n/** Transfer function attached to an authored material color. */\nexport type MaterialColorSpace = 'srgb' | 'linear';\n\n/** Minimal schema shape needed to identify authored color values. */\nexport interface MaterialColorParameterSchema {\n readonly name: string;\n readonly type: string;\n readonly colorSpace?: MaterialColorSpace;\n}\n\n/** IEC 61966-2-1 sRGB electro-optical transfer function. */\nexport function srgbChannelToLinear(value: number): number {\n return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;\n}\n\n/** IEC 61966-2-1 inverse transfer function. */\nexport function linearChannelToSrgb(value: number): number {\n return value <= 0.0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - 0.055;\n}\n\n/**\n * Decode an authored RGB/RGBA value for linear runtime use.\n *\n * Only the first three color channels are transformed. Alpha and any\n * additional lanes are data and pass through unchanged.\n */\nexport function authoredColorToLinear(\n value: readonly number[],\n colorSpace: MaterialColorSpace = 'srgb',\n): number[] {\n if (colorSpace === 'linear') return [...value];\n return value.map((channel, index) => (index < 3 ? srgbChannelToLinear(channel) : channel));\n}\n\n/**\n * Resolve a material parameter's asset-side transfer-function contract.\n * `color` is an authored color and therefore defaults to sRGB. Numeric\n * vectors remain linear unless their schema explicitly marks them as colors.\n */\nexport function materialParameterColorSpace(\n parameter: MaterialColorParameterSchema,\n assetColorSpace?: MaterialColorSpace,\n): MaterialColorSpace | undefined {\n const isColor = parameter.type === 'color' || parameter.colorSpace !== undefined;\n if (!isColor) return undefined;\n return assetColorSpace ?? parameter.colorSpace ?? 'srgb';\n}\n\n/**\n * Project authored MaterialAsset values into a fresh runtime value map.\n * Asset values are never mutated, so repeated extraction cannot compound the\n * transfer function.\n */\nexport function materialValuesToLinearRuntime(\n values: Readonly<Record<string, MaterialValue | null>> | undefined,\n parameters: readonly MaterialColorParameterSchema[],\n assetColorSpace?: MaterialColorSpace,\n): Readonly<Record<string, MaterialValue | null>> {\n if (values === undefined) return {};\n const colorSpaces = new Map<string, MaterialColorSpace>();\n for (const parameter of parameters) {\n const colorSpace = materialParameterColorSpace(parameter, assetColorSpace);\n if (colorSpace !== undefined) colorSpaces.set(parameter.name, colorSpace);\n }\n\n const runtimeValues: Record<string, MaterialValue | null> = {};\n for (const [name, value] of Object.entries(values)) {\n const colorSpace = colorSpaces.get(name);\n runtimeValues[name] =\n colorSpace !== undefined && Array.isArray(value)\n ? authoredColorToLinear(value, colorSpace)\n : value;\n }\n return runtimeValues;\n}\n","export const MATERIAL_ERROR_CODES = [\n 'material-parent-not-found',\n 'material-circular-inheritance',\n 'material-no-effective-pass',\n 'material-value-unknown',\n 'material-value-type-mismatch',\n 'material-contract-program-mismatch',\n 'shader-module-id-missing',\n 'shader-module-id-duplicate',\n 'shader-module-not-found',\n 'shader-module-namespace-reserved',\n 'material-reflection-binding-mismatch',\n 'material-specialization-not-cooked',\n 'material-specialization-stale-generation',\n 'gltf-material-uv-set-missing',\n 'material-derived-interface-mismatch',\n 'material-texture-coordinate-invalid',\n 'material-payload-bounds',\n] as const;\n\nexport type MaterialErrorCode = (typeof MATERIAL_ERROR_CODES)[number];\n\nexport interface MaterialGenerationVector {\n readonly dependencies: Readonly<Record<string, number>>;\n}\n\nexport interface MaterialParentNotFoundDetail {\n readonly code: 'material-parent-not-found';\n readonly leaf: string;\n readonly missingParent: string;\n readonly chain: readonly string[];\n}\n\nexport interface MaterialCircularInheritanceDetail {\n readonly code: 'material-circular-inheritance';\n readonly leaf: string;\n readonly chain: readonly string[];\n}\n\nexport interface MaterialNoEffectivePassDetail {\n readonly code: 'material-no-effective-pass';\n readonly material: string;\n}\n\nexport interface MaterialValueUnknownDetail {\n readonly code: 'material-value-unknown';\n readonly material: string;\n readonly parameter: string;\n}\n\nexport interface MaterialValueTypeMismatchDetail {\n readonly code: 'material-value-type-mismatch';\n readonly material: string;\n readonly parameter: string;\n readonly expectedType: string;\n readonly actualType: string;\n}\n\nexport interface MaterialContractProgramMismatchDetail {\n readonly code: 'material-contract-program-mismatch';\n readonly material: string;\n readonly pass: string;\n readonly program: string;\n readonly expectedProgram: string;\n}\n\nexport interface ShaderModuleIdMissingDetail {\n readonly code: 'shader-module-id-missing';\n readonly source: string;\n}\n\nexport interface ShaderModuleIdDuplicateDetail {\n readonly code: 'shader-module-id-duplicate';\n readonly module: string;\n readonly sources: readonly string[];\n}\n\nexport interface ShaderModuleNotFoundDetail {\n readonly code: 'shader-module-not-found';\n readonly module: string;\n readonly source: string;\n}\n\nexport interface ShaderModuleNamespaceReservedDetail {\n readonly code: 'shader-module-namespace-reserved';\n readonly module: string;\n readonly namespace: string;\n}\n\nexport interface MaterialReflectionBindingMismatchDetail {\n readonly code: 'material-reflection-binding-mismatch';\n readonly material: string;\n readonly pass: string;\n readonly parameter: string;\n readonly expected: string;\n readonly actual: string;\n}\n\nexport interface MaterialSpecializationNotCookedDetail {\n readonly code: 'material-specialization-not-cooked';\n readonly material: string;\n readonly staticSelection: readonly string[];\n}\n\nexport interface MaterialSpecializationStaleGenerationDetail {\n readonly code: 'material-specialization-stale-generation';\n readonly material: string;\n readonly dependencies: readonly string[];\n readonly observed: MaterialGenerationVector;\n readonly current: MaterialGenerationVector;\n}\n\nexport interface GltfMaterialUvSetMissingDetail {\n readonly material: string;\n readonly primitive: string;\n readonly slot: string;\n readonly requestedSet: number;\n readonly availableSets: readonly number[];\n}\n\nexport interface MaterialDerivedInterfaceMismatchDetail {\n readonly code: 'material-derived-interface-mismatch';\n readonly stage: 'compile' | 'cook' | 'extract' | 'record';\n readonly material: string;\n readonly layoutIdentity: string;\n readonly expectedIdentity?: string;\n readonly actualIdentity?: string;\n readonly parameter?: string;\n readonly action: 'recook';\n}\n\nexport interface MaterialTextureCoordinateInvalidDetail {\n readonly code: 'material-texture-coordinate-invalid';\n readonly stage: 'compile' | 'cook' | 'extract' | 'record';\n readonly material: string;\n readonly layoutIdentity: string;\n readonly parameter: string;\n readonly slot: string;\n readonly reason: 'missing' | 'non-finite' | 'shape';\n readonly action: 'recook';\n}\n\nexport interface MaterialPayloadBoundsDetail {\n readonly code: 'material-payload-bounds';\n readonly stage: 'compile' | 'cook' | 'extract' | 'record';\n readonly material: string;\n readonly layoutIdentity: string;\n readonly slot: string;\n readonly byteOffset: number;\n readonly byteLength: number;\n readonly payloadBytes: number;\n readonly action: 'stop-draw';\n}\n\ninterface MaterialErrorDetailByCode {\n readonly 'material-parent-not-found': MaterialParentNotFoundDetail;\n readonly 'material-circular-inheritance': MaterialCircularInheritanceDetail;\n readonly 'material-no-effective-pass': MaterialNoEffectivePassDetail;\n readonly 'material-value-unknown': MaterialValueUnknownDetail;\n readonly 'material-value-type-mismatch': MaterialValueTypeMismatchDetail;\n readonly 'material-contract-program-mismatch': MaterialContractProgramMismatchDetail;\n readonly 'shader-module-id-missing': ShaderModuleIdMissingDetail;\n readonly 'shader-module-id-duplicate': ShaderModuleIdDuplicateDetail;\n readonly 'shader-module-not-found': ShaderModuleNotFoundDetail;\n readonly 'shader-module-namespace-reserved': ShaderModuleNamespaceReservedDetail;\n readonly 'material-reflection-binding-mismatch': MaterialReflectionBindingMismatchDetail;\n readonly 'material-specialization-not-cooked': MaterialSpecializationNotCookedDetail;\n readonly 'material-specialization-stale-generation': MaterialSpecializationStaleGenerationDetail;\n readonly 'gltf-material-uv-set-missing': GltfMaterialUvSetMissingDetail;\n readonly 'material-derived-interface-mismatch': MaterialDerivedInterfaceMismatchDetail;\n readonly 'material-texture-coordinate-invalid': MaterialTextureCoordinateInvalidDetail;\n readonly 'material-payload-bounds': MaterialPayloadBoundsDetail;\n}\n\nexport type MaterialErrorDetail = MaterialErrorDetailByCode[MaterialErrorCode];\n\nexport type MaterialErrorFor<C extends MaterialErrorCode> = {\n readonly code: C;\n readonly expected: string;\n readonly hint: string;\n readonly detail: MaterialErrorDetailByCode[C];\n readonly message: string;\n};\n\nexport type MaterialError = {\n [C in MaterialErrorCode]: MaterialErrorFor<C>;\n}[MaterialErrorCode];\n\nconst MATERIAL_ERROR_POLICY = {\n 'material-parent-not-found': {\n expected: 'every parent GUID resolves to a MaterialAsset',\n hint: 'fix the parent GUID and resolve the material again',\n },\n 'material-circular-inheritance': {\n expected: 'the parent chain is acyclic',\n hint: 'remove the repeated GUID from the parent chain',\n },\n 'material-no-effective-pass': {\n expected: 'the resolved material has at least one pass',\n hint: 'add a pass to the root material or an inherited parent',\n },\n 'material-value-unknown': {\n expected: 'every value name is declared by the effective contract',\n hint: 'remove the value or declare the parameter in the root contract',\n },\n 'material-value-type-mismatch': {\n expected: 'each value matches its declared parameter type',\n hint: 'change the value to the declared parameter type',\n },\n 'material-contract-program-mismatch': {\n expected: 'the program satisfies the material contract',\n hint: 'align the program entries with the root contract',\n },\n 'shader-module-id-missing': {\n expected: 'each WGSL source declares a module ID',\n hint: 'add a compiler-native module ID declaration to the WGSL source',\n },\n 'shader-module-id-duplicate': {\n expected: 'each module ID has one source provenance',\n hint: 'rename one module or remove the duplicate source',\n },\n 'shader-module-not-found': {\n expected: 'every referenced module exists in the source catalog',\n hint: 'add the module to the source catalog or fix the reference',\n },\n 'shader-module-namespace-reserved': {\n expected: 'user modules use a non-reserved namespace',\n hint: 'choose a module ID outside the reserved namespace',\n },\n 'material-reflection-binding-mismatch': {\n expected: 'reflection matches the material contract bindings',\n hint: 'update the contract or WGSL binding and cook again',\n },\n 'material-specialization-not-cooked': {\n expected: 'the requested specialization has a cooked artifact',\n hint: 'run the build or development cook path for this selection',\n },\n 'material-specialization-stale-generation': {\n expected: 'all specialization dependencies share one generation',\n hint: 'retry after dependent assets and sources settle',\n },\n 'gltf-material-uv-set-missing': {\n expected: 'each texture slot references an available primitive UV set',\n hint: 'add the requested UV set to the primitive and re-import it',\n },\n 'material-derived-interface-mismatch': {\n expected: 'the generated material interface matches the derived schema interface',\n hint: 'repair the schema or WGSL producer and recook the material',\n },\n 'material-texture-coordinate-invalid': {\n expected: 'every texture coordinate record is finite and complete',\n hint: 'repair the texture metadata or coordinates and recook the material',\n },\n 'material-payload-bounds': {\n expected: 'every material payload write stays within the derived payload',\n hint: 'repair the derived payload owner before submitting the draw',\n },\n} satisfies {\n readonly [C in MaterialErrorCode]: {\n readonly expected: string;\n readonly hint: string;\n };\n};\n\nexport const MATERIAL_ERROR_EXPECTED: Readonly<Record<MaterialErrorCode, string>> =\n Object.fromEntries(\n MATERIAL_ERROR_CODES.map((code) => [code, MATERIAL_ERROR_POLICY[code].expected]),\n ) as Readonly<Record<MaterialErrorCode, string>>;\n\nexport const MATERIAL_ERROR_HINTS: Readonly<Record<MaterialErrorCode, string>> = Object.fromEntries(\n MATERIAL_ERROR_CODES.map((code) => [code, MATERIAL_ERROR_POLICY[code].hint]),\n) as Readonly<Record<MaterialErrorCode, string>>;\n\nexport function createMaterialError<C extends MaterialErrorCode>(\n code: C,\n detail: MaterialErrorDetailByCode[C],\n message = `${code}: ${MATERIAL_ERROR_EXPECTED[code]}`,\n): MaterialErrorFor<C> {\n return {\n code,\n expected: MATERIAL_ERROR_EXPECTED[code],\n hint: MATERIAL_ERROR_HINTS[code],\n detail,\n message,\n };\n}\n","import { err, ok, type Result } from '../result.js';\nimport type { MaterialAsset, MaterialParameter, MaterialPass, MaterialValue } from './asset.js';\nimport type { MaterialError } from './errors.js';\nimport { createMaterialError } from './errors.js';\n\nexport type MaterialTable = Readonly<Record<string, MaterialAsset>>;\n\nexport interface ResolvedMaterial {\n readonly leaf: string;\n readonly chain: readonly string[];\n readonly asset: MaterialAsset;\n}\n\nexport function materialGuidText(value: string | Uint8Array): string {\n return typeof value === 'string'\n ? value\n : Array.from(value, (byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nfunction valueType(value: MaterialValue): string {\n if (typeof value === 'boolean') return 'bool';\n if (typeof value === 'number') return 'number';\n if (typeof value === 'string') return 'string';\n if (Array.isArray(value)) return `vec${value.length}`;\n return 'texture';\n}\n\nfunction parameterTypeMatches(parameter: MaterialParameter, value: MaterialValue): boolean {\n switch (parameter.type) {\n case 'bool':\n return typeof value === 'boolean';\n case 'f32':\n case 'i32':\n case 'u32':\n return typeof value === 'number';\n case 'vec2':\n return Array.isArray(value) && value.length === 2;\n case 'vec3':\n return Array.isArray(value) && value.length === 3;\n case 'vec4':\n case 'color':\n return Array.isArray(value) && value.length === 4;\n case 'texture':\n // Runtime asset handles are branded numbers at the type level. The\n // brand is erased before a material reaches the resolver, so numeric\n // handles must remain valid texture values alongside structured\n // texture descriptors.\n return (\n typeof value === 'string' ||\n (typeof value === 'number' && Number.isInteger(value) && value >= 0) ||\n (typeof value === 'object' && !Array.isArray(value))\n );\n }\n}\n\nfunction validateValues(\n material: string,\n values: Readonly<Record<string, MaterialValue | null>>,\n parameters: readonly MaterialParameter[] | undefined,\n): Result<true, MaterialError> {\n if (parameters === undefined) return ok(true);\n const declarations = new Map(parameters.map((parameter) => [parameter.name, parameter]));\n for (const [name, value] of Object.entries(values)) {\n const parameter = declarations.get(name);\n if (parameter === undefined) {\n return err(\n createMaterialError('material-value-unknown', {\n code: 'material-value-unknown',\n material,\n parameter: name,\n }),\n );\n }\n if (value === null) {\n if (!parameter.optional) {\n return err(\n createMaterialError('material-value-type-mismatch', {\n code: 'material-value-type-mismatch',\n material,\n parameter: name,\n expectedType: parameter.type,\n actualType: 'null',\n }),\n );\n }\n continue;\n }\n if (!parameterTypeMatches(parameter, value)) {\n return err(\n createMaterialError('material-value-type-mismatch', {\n code: 'material-value-type-mismatch',\n material,\n parameter: name,\n expectedType: parameter.type,\n actualType: valueType(value),\n }),\n );\n }\n }\n return ok(true);\n}\n\nfunction mergePasses(\n inherited: readonly MaterialPass[] | undefined,\n override: readonly MaterialPass[] | undefined,\n): readonly MaterialPass[] | undefined {\n if (inherited === undefined && override === undefined) return undefined;\n const passes = [...(inherited ?? [])];\n for (const next of override ?? []) {\n const index = passes.findIndex((current) => current.name === next.name);\n if (index === -1) passes.push(next);\n else passes[index] = next;\n }\n return passes;\n}\n\nfunction mergeMaterial(parent: MaterialAsset, child: MaterialAsset): MaterialAsset {\n const values: Record<string, MaterialValue> = {};\n for (const [name, value] of Object.entries(parent.values ?? {})) {\n if (value !== null) values[name] = value;\n }\n for (const [name, value] of Object.entries(child.values ?? {})) {\n if (value === null) delete values[name];\n else values[name] = value;\n }\n const passes = mergePasses(parent.passes, child.passes);\n return {\n kind: 'material',\n ...((child.colorSpace ?? parent.colorSpace) !== undefined\n ? { colorSpace: child.colorSpace ?? parent.colorSpace }\n : {}),\n ...(passes !== undefined && passes.length > 0\n ? { passes: passes as NonNullable<MaterialAsset['passes']> }\n : {}),\n ...(parent.parameters !== undefined ? { parameters: parent.parameters } : {}),\n ...(Object.keys(values).length > 0 ? { values } : {}),\n };\n}\n\nfunction resolveChain(\n id: string,\n leaf: string,\n table: MaterialTable,\n stack: readonly string[],\n): Result<ResolvedMaterial, MaterialError> {\n if (stack.includes(id)) {\n return err(\n createMaterialError('material-circular-inheritance', {\n code: 'material-circular-inheritance',\n leaf,\n chain: [...stack, id],\n }),\n );\n }\n const current = table[id];\n if (current === undefined) {\n return err(\n createMaterialError('material-parent-not-found', {\n code: 'material-parent-not-found',\n leaf,\n missingParent: id,\n chain: [...stack, id],\n }),\n );\n }\n const parent = current.parent === undefined ? undefined : materialGuidText(current.parent);\n if (parent === undefined) {\n if (current.passes === undefined || current.passes.length === 0) {\n return err(\n createMaterialError('material-no-effective-pass', {\n code: 'material-no-effective-pass',\n material: leaf,\n }),\n );\n }\n const values: Record<string, MaterialValue> = {};\n for (const [name, value] of Object.entries(current.values ?? {})) {\n if (value !== null) values[name] = value;\n }\n const valid = validateValues(id, values, current.parameters);\n if (!valid.ok) return valid;\n return ok({ leaf, chain: [id], asset: { ...current, values } });\n }\n\n const parentResult = resolveChain(parent, leaf, table, [...stack, id]);\n if (!parentResult.ok) return parentResult;\n const valid = validateValues(id, current.values ?? {}, parentResult.value.asset.parameters);\n if (!valid.ok) return valid;\n const merged = mergeMaterial(parentResult.value.asset, current);\n if (merged.passes === undefined || merged.passes.length === 0) {\n return err(\n createMaterialError('material-no-effective-pass', {\n code: 'material-no-effective-pass',\n material: leaf,\n }),\n );\n }\n return ok({ leaf, chain: [...parentResult.value.chain, id], asset: merged });\n}\n\nexport function resolveMaterialAsset(\n leaf: string,\n table: MaterialTable,\n): Result<ResolvedMaterial, MaterialError> {\n return resolveChain(leaf, leaf, table, []);\n}\n","import type { CatalogDiagnostic } from './asset-producer.js';\nimport type { CatalogEntry } from './catalog.js';\n\n/** Versioned wire shape shared by the asset producer and browser consumers. */\nexport const RUNTIME_ASSET_BINDING_SCHEMA = 'runtime-asset-binding-v1' as const;\nexport const RUNTIME_CATALOG_SNAPSHOT_SCHEMA = 'runtime-catalog-snapshot-v1' as const;\n\nexport type RuntimeScopeStatus = 'unbound' | 'transitioning' | 'ready' | 'degraded' | 'unavailable';\n\n/**\n * Logical projection of one package-declared asset root into the runtime\n * catalog coordinate space. This carries no filesystem path across the wire.\n */\nexport interface RuntimeCatalogRoot {\n readonly root: string;\n readonly catalogPrefix: string;\n}\n\n/**\n * The only identity a browser-side asset consumer may use for a dev realm.\n * `scopeId` describes ownership; `generation` rejects stale browser work.\n * Filesystem roots intentionally do not cross this wire contract; catalogRoots\n * is only the logical declaration-to-catalog projection used by browser views.\n */\nexport interface RuntimeAssetBinding {\n readonly schemaVersion: typeof RUNTIME_ASSET_BINDING_SCHEMA;\n readonly gameId: string;\n readonly scopeId: string;\n readonly generation: number;\n readonly status: RuntimeScopeStatus;\n readonly catalogUrl: string;\n readonly importUrlBase: string;\n readonly packageUrlBase: string;\n readonly catalogRoots?: readonly RuntimeCatalogRoot[];\n readonly authority?: 'authoritative' | 'degraded';\n readonly diagnostics?: readonly CatalogDiagnostic[];\n}\n\nexport function isRuntimeCatalogRoots(value: unknown): value is readonly RuntimeCatalogRoot[] {\n return (\n Array.isArray(value) &&\n value.every(\n (root) =>\n root !== null &&\n typeof root === 'object' &&\n typeof (root as { root?: unknown }).root === 'string' &&\n typeof (root as { catalogPrefix?: unknown }).catalogPrefix === 'string',\n )\n );\n}\n\n/** Authority-bearing catalog response for one runtime scope. */\nexport interface RuntimeCatalogSnapshot {\n readonly schemaVersion: typeof RUNTIME_CATALOG_SNAPSHOT_SCHEMA;\n readonly scopeId: string;\n readonly generation: number;\n readonly authority: 'authoritative' | 'degraded';\n readonly entries: readonly CatalogEntry[];\n readonly diagnostics: readonly CatalogDiagnostic[];\n}\n\n/**\n * Make a route below the engine's scoped runtime namespace. The helper is\n * deliberately pure so hosts and tests cannot drift on URL construction.\n */\nexport function runtimeScopePath(\n binding: Pick<RuntimeAssetBinding, 'scopeId' | 'generation'>,\n suffix = '',\n): string {\n const normalized = suffix.length === 0 ? '' : `/${suffix.replace(/^\\/+/, '')}`;\n return `/__pack/scopes/${encodeURIComponent(binding.scopeId)}/${binding.generation}${normalized}`;\n}\n\n/**\n * Create the fixed binding used by a standalone Vite game host.\n *\n * A standalone host still has one explicit realm: its game owns the roots and\n * the browser endpoints are derived from the same scope/generation pair. The\n * optional scope override is only for a host-level test server that mounts\n * several standalone entrypoints behind one deliberately shared test realm;\n * production hosts should leave it unset.\n */\nexport function createStandaloneRuntimeAssetBinding(\n gameId: string,\n scopeId = gameId,\n basePath = '',\n): RuntimeAssetBinding {\n const normalizedBase = basePath === '/' ? '' : basePath.replace(/\\/+$/, '');\n const identity = { scopeId, generation: 1 } as const;\n const scopedPath = runtimeScopePath(identity);\n const hostPrefix = normalizedBase.startsWith('/') ? normalizedBase : `/${normalizedBase}`;\n const prefix = normalizedBase.length === 0 ? '' : hostPrefix;\n return {\n schemaVersion: RUNTIME_ASSET_BINDING_SCHEMA,\n gameId,\n scopeId,\n generation: identity.generation,\n status: 'ready',\n catalogUrl: `${prefix}${scopedPath}/catalog.json`,\n importUrlBase: `${prefix}${scopedPath}/import`,\n packageUrlBase: prefix,\n };\n}\n\nexport function runtimeScopeMatches(\n binding: Pick<RuntimeAssetBinding, 'scopeId' | 'generation'> | undefined,\n scopeId: string,\n generation: number,\n): boolean {\n return binding?.scopeId === scopeId && binding.generation === generation;\n}\n","// derive(schema) — paramSchema -> one DerivedMaterialInterface.\n// feat-20260613-material-paramschema-driven-binding M1 / w3\n//\n// Decision anchors (plan-strategy §2):\n// - D-2 single pure function, no side effect; one signature consumed by 3\n// downstream paths (BGL build / UBO record / loader-extract).\n// - D-3 consecutive numeric entries are run-merged into one UBO entry at\n// one binding slot (uniform buffer); std140-aligned offsets.\n// - D-4 every texture* family entry auto-pairs a filtering sampler at\n// binding-1 (sampler emitted FIRST, then texture); sampler /\n// sampler_comparison stay user-declared.\n// - D-7 type set is the 14-literal MaterialParamType union.\n// - D-12 empty schema is graceful: bglEntries=[] / totalBytes=0 / fields empty.\n//\n// std140 alignment table (WGSL uniform):\n// - f32 / i32 / u32 size 4 align 4\n// - vec2<f32> size 8 align 8\n// - vec3<f32> size 12 align 16\n// - vec4 / color (rgba) size 16 align 16\n// - struct round-up: totalBytes is rounded up to 16-byte alignment.\n\nimport type {\n BindGroupLayoutEntry,\n MaterialParamType,\n NumericParamType,\n ParamSchemaEntry,\n TextureBindingParamType,\n} from './index.js';\n\nconst FRAGMENT = 0x2 as GPUShaderStageFlags;\n\nconst NUMERIC_TYPES: ReadonlySet<MaterialParamType> = new Set<MaterialParamType>([\n 'f32',\n 'i32',\n 'u32',\n 'vec2',\n 'vec3',\n 'vec4',\n 'color',\n]);\n\nconst TEXTURE_VIEW_TYPES: ReadonlySet<MaterialParamType> = new Set<MaterialParamType>([\n 'texture2d',\n 'texture_cube',\n 'texture_depth_2d',\n 'texture_cube_array',\n]);\n\nconst SAMPLER_TYPES: ReadonlySet<MaterialParamType> = new Set<MaterialParamType>([\n 'sampler',\n 'sampler_comparison',\n]);\n\nconst ALL_TYPES: ReadonlySet<MaterialParamType> = new Set<MaterialParamType>([\n ...NUMERIC_TYPES,\n ...TEXTURE_VIEW_TYPES,\n ...SAMPLER_TYPES,\n 'storage_buffer',\n]);\n\ninterface NumericFootprint {\n readonly size: number;\n readonly align: number;\n}\n\nfunction numericFootprint(t: NumericParamType): NumericFootprint {\n switch (t) {\n case 'f32':\n case 'i32':\n case 'u32':\n return { size: 4, align: 4 };\n case 'vec2':\n return { size: 8, align: 8 };\n case 'vec3':\n return { size: 12, align: 16 };\n case 'vec4':\n case 'color':\n return { size: 16, align: 16 };\n }\n}\n\nfunction alignUp(value: number, alignment: number): number {\n return (value + alignment - 1) & ~(alignment - 1);\n}\n\ninterface TextureBglDescriptor {\n readonly sampleType: GPUTextureSampleType;\n readonly viewDimension: GPUTextureViewDimension;\n}\n\nfunction textureBglDescriptor(t: TextureBindingParamType): TextureBglDescriptor {\n switch (t) {\n case 'texture2d':\n return { sampleType: 'float', viewDimension: '2d' };\n case 'texture_cube':\n return { sampleType: 'float', viewDimension: 'cube' };\n case 'texture_depth_2d':\n return { sampleType: 'depth', viewDimension: '2d' };\n case 'texture_cube_array':\n return { sampleType: 'float', viewDimension: 'cube-array' };\n case 'sampler':\n case 'sampler_comparison':\n // Not a texture view — caller must dispatch separately. Falling through\n // here is a defensive guard; numericFootprint / sampler handler covers\n // these branches before this function is reached.\n throw new Error(`derive: textureBglDescriptor called on sampler-family type '${t}'`);\n }\n}\n\nfunction samplerBindingType(t: 'sampler' | 'sampler_comparison'): GPUSamplerBindingType {\n return t === 'sampler' ? 'filtering' : 'comparison';\n}\n\n/** Single UBO sub-entry — one merged std140 slot. */\nexport interface UboFieldLayout {\n readonly name: string;\n readonly offset: number;\n readonly size: number;\n readonly type: NumericParamType;\n}\n\nexport interface DerivedNumericMember extends UboFieldLayout {\n readonly alignment: number;\n}\n\nexport interface MaterialCoordinateRecordLayout {\n readonly parameter: string;\n readonly offset: number;\n readonly size: 32;\n readonly alignment: 16;\n readonly transformMember: string;\n readonly metadataMember: string;\n}\n\nexport type MaterialResourceKind = 'sampler' | 'texture' | 'storage-buffer';\n\nexport interface MaterialResourceBindingLayout {\n readonly name: string;\n readonly parameter?: string;\n readonly kind: MaterialResourceKind;\n readonly binding: number;\n}\n\nexport interface MaterialBindingSpan {\n readonly group: number;\n readonly binding: number;\n readonly start: number;\n readonly end: number;\n}\n\nexport interface MaterialUserRegion {\n readonly group: number;\n readonly bindingStart: number;\n readonly bindingEnd: number;\n}\n\nexport interface MaterialParameterResourceProjection {\n readonly kind: 'sampler' | 'storage-buffer';\n readonly name: string;\n readonly type: 'sampler' | 'sampler_comparison' | 'storage_buffer';\n readonly resource: MaterialResourceBindingLayout;\n readonly coordinates?: never;\n}\n\nexport interface MaterialParameterTextureProjection {\n readonly kind: 'texture';\n readonly name: string;\n readonly type: TextureBindingParamType;\n readonly coordinates: MaterialCoordinateRecordLayout;\n readonly resource: {\n readonly parameter: string;\n readonly texture: MaterialResourceBindingLayout;\n readonly sampler: MaterialResourceBindingLayout;\n };\n}\n\nexport interface MaterialParameterNumericProjection {\n readonly kind: 'numeric';\n readonly name: string;\n readonly type: NumericParamType;\n readonly member: DerivedNumericMember;\n readonly coordinates?: never;\n readonly resource?: never;\n}\n\nexport type MaterialParameterProjection =\n | MaterialParameterNumericProjection\n | MaterialParameterTextureProjection\n | MaterialParameterResourceProjection;\n\nexport interface DerivedMaterialInterface {\n readonly schemaVersion: 'material-abi/1';\n readonly group: number;\n readonly visibility: GPUShaderStageFlags;\n readonly bglEntries: readonly BindGroupLayoutEntry[];\n readonly uboLayout: UboLayout;\n readonly numericMembers: readonly DerivedNumericMember[];\n readonly coordinateRecords: readonly MaterialCoordinateRecordLayout[];\n readonly resourceBindings: readonly MaterialResourceBindingLayout[];\n readonly totalBytes: number;\n readonly layoutIdentity: string;\n readonly textureFieldNames: ReadonlySet<string>;\n readonly samplerForTexture: ReadonlyMap<string, string>;\n readonly userRegionBindingEnd: number;\n readonly bindingSpans: readonly MaterialBindingSpan[];\n readonly userRegion: MaterialUserRegion;\n}\n\nexport interface UboLayout {\n readonly entries: readonly UboFieldLayout[];\n readonly totalBytes: number;\n}\n\nexport type DeriveOutput = DerivedMaterialInterface;\n\nexport interface ImmutableParamSchemaProjection {\n readonly ownerId: string;\n readonly revision: number;\n readonly schema: readonly ParamSchemaEntry[];\n readonly derivedInterface: DerivedMaterialInterface;\n}\n\nexport interface ParamSchemaProjectionOwnerStats {\n readonly admissions: number;\n readonly derivations: number;\n readonly projections: number;\n}\n\nexport type ParamSchemaDeriveObservationKind =\n | 'admitted-identity-hit'\n | 'unregistered-fallback-derive'\n | 'fallback-layout-sha';\n\nexport interface ParamSchemaDeriveObserver {\n readonly enabled: boolean;\n readonly observe: (event: {\n readonly kind: ParamSchemaDeriveObservationKind;\n readonly site: string;\n }) => void;\n}\n\ninterface OwnedParamSchemaProjection extends ImmutableParamSchemaProjection {\n readonly canonicalSchema: string;\n}\n\n// Runtime consumers can still receive the schema array through older API\n// surfaces. Admission binds that immutable array identity back to the one\n// owner projection, so those compatibility calls return the admitted result\n// without deriving or hashing again. Unregistered schemas keep the pure\n// offline/compiler behavior.\nconst ADMITTED_PARAM_SCHEMA_PROJECTIONS = new WeakMap<\n readonly ParamSchemaEntry[],\n DerivedMaterialInterface\n>();\n\n/**\n * Immutable/revision owner for runtime ParamSchema projections.\n *\n * A new `(ownerId, revision)` performs exactly one pure derivation. Repeating\n * the same admission is idempotent when the schema bytes match and fails loud\n * when a revision is reused for different schema content. The admitted schema\n * is cloned and frozen so caller-side mutation cannot alter a published\n * revision.\n */\nexport class ParamSchemaProjectionOwner {\n readonly #projections = new Map<string, OwnedParamSchemaProjection>();\n #admissions = 0;\n #derivations = 0;\n\n admit(args: {\n readonly ownerId: string;\n readonly revision: number;\n readonly schema: readonly ParamSchemaEntry[];\n }): ImmutableParamSchemaProjection {\n if (args.ownerId.length === 0) {\n throw new Error('ParamSchemaProjectionOwner: ownerId must be non-empty');\n }\n if (!Number.isSafeInteger(args.revision) || args.revision < 1) {\n throw new Error('ParamSchemaProjectionOwner: revision must be a positive safe integer');\n }\n this.#admissions += 1;\n const key = `${args.ownerId}\\u0000${args.revision}`;\n const canonicalSchema = JSON.stringify(args.schema);\n const existing = this.#projections.get(key);\n if (existing !== undefined) {\n if (existing.canonicalSchema !== canonicalSchema) {\n throw new Error(\n `ParamSchemaProjectionOwner: ${args.ownerId}@${args.revision} reused with different schema content`,\n );\n }\n return existing;\n }\n\n const schema = freezeParamSchema(args.schema);\n const derivedInterface = freezeDerivedMaterialInterface(derivePure(schema));\n this.#derivations += 1;\n ADMITTED_PARAM_SCHEMA_PROJECTIONS.set(schema, derivedInterface);\n const projection = Object.freeze({\n ownerId: args.ownerId,\n revision: args.revision,\n schema,\n derivedInterface,\n canonicalSchema,\n });\n this.#projections.set(key, projection);\n return projection;\n }\n\n get(ownerId: string, revision: number): ImmutableParamSchemaProjection | undefined {\n return this.#projections.get(`${ownerId}\\u0000${revision}`);\n }\n\n stats(): ParamSchemaProjectionOwnerStats {\n return Object.freeze({\n admissions: this.#admissions,\n derivations: this.#derivations,\n projections: this.#projections.size,\n });\n }\n}\n\n/**\n * Pure derivation: paramSchema -> BGL entries + UBO byte layout + field maps.\n *\n * The function has no side effects and is the SSOT for BGL / UBO / loader\n * lookup tables (D-2). Runtime / vite-plugin-shader / loader all call into\n * this single entry point; in particular, `userRegionBindingEnd` is the\n * post-user-region binding index that engine-injected groups (shadow / IBL\n * / lightmap, see D-6) must start from.\n *\n * Throws on schema authoring errors:\n * - duplicate entry name (numeric or non-numeric)\n * - unrecognised type literal (not in the 14-member union)\n * - empty entry name\n * - user-declared name collides with the auto-paired `<tex>_sampler`\n */\nexport function derive(schema: readonly ParamSchemaEntry[]): DeriveOutput {\n const admitted = ADMITTED_PARAM_SCHEMA_PROJECTIONS.get(schema);\n if (admitted !== undefined) return admitted;\n return derivePure(schema);\n}\n\n/**\n * Debug-only observation seam for runtime compatibility consumers.\n *\n * This wrapper deliberately does not cache or change derivation semantics. It\n * only records whether the supplied schema identity was admitted by the\n * ParamSchemaProjectionOwner before delegating to the existing pure `derive`\n * function. Keeping the observer out of the normal `derive` signature leaves\n * production callers on the existing zero-argument path.\n */\nexport function deriveObserved(\n schema: readonly ParamSchemaEntry[],\n observer: ParamSchemaDeriveObserver,\n site: string,\n): DeriveOutput {\n const admitted = ADMITTED_PARAM_SCHEMA_PROJECTIONS.has(schema);\n const output = derive(schema);\n if (observer.enabled) {\n observer.observe({\n kind: admitted ? 'admitted-identity-hit' : 'unregistered-fallback-derive',\n site,\n });\n if (!admitted) observer.observe({ kind: 'fallback-layout-sha', site });\n }\n return output;\n}\n\nfunction derivePure(schema: readonly ParamSchemaEntry[]): DeriveOutput {\n const bglEntries: BindGroupLayoutEntry[] = [];\n const uboFields: UboFieldLayout[] = [];\n const numericMembers: DerivedNumericMember[] = [];\n const coordinateRecords: MaterialCoordinateRecordLayout[] = [];\n const resourceBindings: MaterialResourceBindingLayout[] = [];\n const bindingSpans: MaterialBindingSpan[] = [];\n const textureFieldNames = new Set<string>();\n const samplerForTexture = new Map<string, string>();\n const seenNames = new Set<string>();\n const reservedSamplerNames = new Set<string>();\n\n let nextBinding = 0;\n let uboCursor = 0;\n let uboBinding: number | null = null;\n\n const ensureUboBinding = (): number => {\n if (uboBinding !== null) return uboBinding;\n uboBinding = nextBinding;\n nextBinding += 1;\n bglEntries.push({\n binding: uboBinding,\n visibility: FRAGMENT,\n buffer: { type: 'uniform' },\n });\n bindingSpans.push({ group: 1, binding: uboBinding, start: 0, end: 0 });\n return uboBinding;\n };\n\n for (const rawEntry of schema) {\n const entry = rawEntry as ParamSchemaEntry;\n if (entry.name.length === 0) {\n throw new Error('derive: schema entry name must be non-empty');\n }\n if (!ALL_TYPES.has(entry.type)) {\n throw new Error(`derive: unrecognised paramSchema type literal '${entry.type}'`);\n }\n if (seenNames.has(entry.name)) {\n throw new Error(`derive: duplicate paramSchema entry name '${entry.name}'`);\n }\n if (reservedSamplerNames.has(entry.name)) {\n throw new Error(\n `derive: paramSchema entry '${entry.name}' collides with auto-paired sampler name`,\n );\n }\n seenNames.add(entry.name);\n\n if (NUMERIC_TYPES.has(entry.type)) {\n const numericType = entry.type as NumericParamType;\n const { size, align } = numericFootprint(numericType);\n ensureUboBinding();\n const offset = alignUp(uboCursor, align);\n uboFields.push({ name: entry.name, offset, size, type: numericType });\n numericMembers.push({ name: entry.name, offset, size, alignment: align, type: numericType });\n uboCursor = offset + size;\n continue;\n }\n\n if (TEXTURE_VIEW_TYPES.has(entry.type)) {\n const texType = entry.type as TextureBindingParamType;\n ensureUboBinding();\n const coordinateOffset = alignUp(uboCursor, 16);\n const coordinates: MaterialCoordinateRecordLayout = {\n parameter: entry.name,\n offset: coordinateOffset,\n size: 32,\n alignment: 16,\n transformMember: `${entry.name}CoordinatesTransform`,\n metadataMember: `${entry.name}CoordinatesMetadata`,\n };\n coordinateRecords.push(coordinates);\n uboCursor = coordinateOffset + coordinates.size;\n const { sampleType, viewDimension } = textureBglDescriptor(texType);\n // Sampler-first per plan §D-4: emit auto-paired filtering sampler at\n // binding N, then the texture view at binding N+1. Matches the actual\n // WGSL @binding declaration order in the 5 built-in shaders (sampler\n // declared on the odd binding, texture on the even+1 binding).\n const samplerName = `${entry.name}_sampler`;\n if (seenNames.has(samplerName)) {\n throw new Error(\n `derive: auto-paired sampler name '${samplerName}' collides with existing entry`,\n );\n }\n reservedSamplerNames.add(samplerName);\n samplerForTexture.set(entry.name, samplerName);\n const samplerBinding = nextBinding;\n nextBinding += 1;\n bglEntries.push({\n binding: samplerBinding,\n visibility: FRAGMENT,\n sampler: { type: 'filtering' },\n });\n bindingSpans.push({ group: 1, binding: samplerBinding, start: 0, end: 0 });\n resourceBindings.push({\n name: samplerName,\n parameter: entry.name,\n kind: 'sampler',\n binding: samplerBinding,\n });\n\n const texBinding = nextBinding;\n nextBinding += 1;\n bglEntries.push({\n binding: texBinding,\n visibility: FRAGMENT,\n texture: { sampleType, viewDimension, multisampled: false },\n });\n bindingSpans.push({ group: 1, binding: texBinding, start: 0, end: 0 });\n resourceBindings.push({\n name: entry.name,\n parameter: entry.name,\n kind: 'texture',\n binding: texBinding,\n });\n textureFieldNames.add(entry.name);\n continue;\n }\n\n if (SAMPLER_TYPES.has(entry.type)) {\n const samplerType = entry.type as 'sampler' | 'sampler_comparison';\n const samplerBinding = nextBinding;\n nextBinding += 1;\n bglEntries.push({\n binding: samplerBinding,\n visibility: FRAGMENT,\n sampler: { type: samplerBindingType(samplerType) },\n });\n bindingSpans.push({ group: 1, binding: samplerBinding, start: 0, end: 0 });\n resourceBindings.push({\n name: entry.name,\n parameter: entry.name,\n kind: 'sampler',\n binding: samplerBinding,\n });\n continue;\n }\n\n // entry.type === 'storage_buffer'\n const storageBinding = nextBinding;\n nextBinding += 1;\n bglEntries.push({\n binding: storageBinding,\n visibility: FRAGMENT,\n buffer: { type: 'read-only-storage' },\n });\n bindingSpans.push({ group: 1, binding: storageBinding, start: 0, end: 0 });\n resourceBindings.push({\n name: entry.name,\n parameter: entry.name,\n kind: 'storage-buffer',\n binding: storageBinding,\n });\n }\n\n const totalBytes = uboCursor === 0 ? 0 : alignUp(uboCursor, 16);\n const uniformSpan = bindingSpans.find((span) => span.binding === uboBinding);\n if (uniformSpan !== undefined) {\n const index = bindingSpans.indexOf(uniformSpan);\n bindingSpans[index] = { ...uniformSpan, end: totalBytes };\n }\n const userRegion: MaterialUserRegion = { group: 1, bindingStart: 0, bindingEnd: nextBinding };\n const layoutIdentity = sha256LayoutIdentity({\n numericMembers,\n coordinateRecords,\n resourceBindings,\n totalBytes,\n bindingSpans,\n userRegion,\n });\n\n return {\n schemaVersion: 'material-abi/1',\n group: 1,\n visibility: FRAGMENT,\n bglEntries,\n uboLayout: { entries: uboFields, totalBytes },\n numericMembers,\n coordinateRecords,\n resourceBindings,\n totalBytes,\n layoutIdentity,\n textureFieldNames,\n samplerForTexture,\n userRegionBindingEnd: nextBinding,\n bindingSpans,\n userRegion,\n };\n}\n\nfunction freezeParamSchema(schema: readonly ParamSchemaEntry[]): readonly ParamSchemaEntry[] {\n return Object.freeze(\n schema.map((entry) => {\n const defaultValue = cloneAndFreezeSchemaValue(entry.default);\n return Object.freeze({\n ...entry,\n ...(entry.default === undefined ? {} : { default: defaultValue }),\n }) as ParamSchemaEntry;\n }),\n );\n}\n\nfunction cloneAndFreezeSchemaValue(value: unknown): unknown {\n if (Array.isArray(value)) {\n return Object.freeze(value.map((item) => cloneAndFreezeSchemaValue(item)));\n }\n if (typeof value === 'object' && value !== null) {\n return Object.freeze(\n Object.fromEntries(\n Object.entries(value).map(([key, item]) => [key, cloneAndFreezeSchemaValue(item)]),\n ),\n );\n }\n return value;\n}\n\nfunction freezeDerivedMaterialInterface(\n derived: DerivedMaterialInterface,\n): DerivedMaterialInterface {\n deepFreeze(derived.bglEntries);\n deepFreeze(derived.uboLayout);\n deepFreeze(derived.numericMembers);\n deepFreeze(derived.coordinateRecords);\n deepFreeze(derived.resourceBindings);\n deepFreeze(derived.bindingSpans);\n deepFreeze(derived.userRegion);\n return Object.freeze({\n ...derived,\n textureFieldNames: new ImmutableSetView(derived.textureFieldNames),\n samplerForTexture: new ImmutableMapView(derived.samplerForTexture),\n });\n}\n\nfunction deepFreeze<T>(value: T): T {\n if (typeof value !== 'object' || value === null || Object.isFrozen(value)) return value;\n for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child);\n return Object.freeze(value);\n}\n\nclass ImmutableSetView<T> implements ReadonlySet<T> {\n readonly #values: Set<T>;\n\n constructor(values: Iterable<T>) {\n this.#values = new Set(values);\n Object.freeze(this);\n }\n\n get size(): number {\n return this.#values.size;\n }\n\n has(value: T): boolean {\n return this.#values.has(value);\n }\n\n entries(): SetIterator<[T, T]> {\n return this.#values.entries();\n }\n\n keys(): SetIterator<T> {\n return this.#values.keys();\n }\n\n values(): SetIterator<T> {\n return this.#values.values();\n }\n\n forEach(callbackfn: (value: T, value2: T, set: ReadonlySet<T>) => void, thisArg?: unknown): void {\n for (const value of this.#values) callbackfn.call(thisArg, value, value, this);\n }\n\n [Symbol.iterator](): SetIterator<T> {\n return this.#values[Symbol.iterator]();\n }\n}\n\nclass ImmutableMapView<K, V> implements ReadonlyMap<K, V> {\n readonly #values: Map<K, V>;\n\n constructor(values: Iterable<readonly [K, V]>) {\n this.#values = new Map(values);\n Object.freeze(this);\n }\n\n get size(): number {\n return this.#values.size;\n }\n\n get(key: K): V | undefined {\n return this.#values.get(key);\n }\n\n has(key: K): boolean {\n return this.#values.has(key);\n }\n\n entries(): MapIterator<[K, V]> {\n return this.#values.entries();\n }\n\n keys(): MapIterator<K> {\n return this.#values.keys();\n }\n\n values(): MapIterator<V> {\n return this.#values.values();\n }\n\n forEach(callbackfn: (value: V, key: K, map: ReadonlyMap<K, V>) => void, thisArg?: unknown): void {\n for (const [key, value] of this.#values) callbackfn.call(thisArg, value, key, this);\n }\n\n [Symbol.iterator](): MapIterator<[K, V]> {\n return this.#values[Symbol.iterator]();\n }\n}\n\nexport function inferMaterialParameterKind(\n entry: ParamSchemaEntry,\n): MaterialParameterProjection['kind'] {\n if (NUMERIC_TYPES.has(entry.type)) return 'numeric';\n if (TEXTURE_VIEW_TYPES.has(entry.type)) return 'texture';\n if (entry.type === 'storage_buffer') return 'storage-buffer';\n return 'sampler';\n}\n\nfunction sha256LayoutIdentity(value: {\n readonly numericMembers: readonly DerivedNumericMember[];\n readonly coordinateRecords: readonly MaterialCoordinateRecordLayout[];\n readonly resourceBindings: readonly MaterialResourceBindingLayout[];\n readonly totalBytes: number;\n readonly bindingSpans: readonly MaterialBindingSpan[];\n readonly userRegion: MaterialUserRegion;\n}): string {\n const canonical = JSON.stringify({\n version: 1,\n numericMembers: value.numericMembers,\n coordinateRecords: value.coordinateRecords,\n resourceBindings: value.resourceBindings,\n totalBytes: value.totalBytes,\n bindingSpans: value.bindingSpans,\n userRegion: value.userRegion,\n });\n return `sha256-${sha256(canonical)}`;\n}\n\nconst SHA256_K = [\n 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,\n 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,\n 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,\n 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,\n 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,\n 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,\n 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,\n 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,\n] as const;\n\nfunction sha256(input: string): string {\n const bytes = new TextEncoder().encode(input);\n const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;\n const padded = new Uint8Array(paddedLength);\n padded.set(bytes);\n padded[bytes.length] = 0x80;\n const view = new DataView(padded.buffer);\n view.setUint32(paddedLength - 4, bytes.length * 8, false);\n let hash = new Uint32Array([\n 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,\n ]);\n for (let block = 0; block < padded.length; block += 64) {\n const words = new Uint32Array(64);\n for (let index = 0; index < 16; index += 1)\n words[index] = view.getUint32(block + index * 4, false);\n for (let index = 16; index < 64; index += 1) {\n const a = words[index - 15] ?? 0;\n const b = words[index - 2] ?? 0;\n words[index] =\n (smallSigma1(b) + (words[index - 7] ?? 0) + smallSigma0(a) + (words[index - 16] ?? 0)) >>>\n 0;\n }\n let a = hash[0] ?? 0;\n let b = hash[1] ?? 0;\n let c = hash[2] ?? 0;\n let d = hash[3] ?? 0;\n let e = hash[4] ?? 0;\n let f = hash[5] ?? 0;\n let g = hash[6] ?? 0;\n let h = hash[7] ?? 0;\n for (let index = 0; index < 64; index += 1) {\n const choose = (e & f) ^ (~e & g);\n const majority = (a & b) ^ (a & c) ^ (b & c);\n const t1 = (h + bigSigma1(e) + choose + (SHA256_K[index] ?? 0) + (words[index] ?? 0)) >>> 0;\n const t2 = (bigSigma0(a) + majority) >>> 0;\n [h, g, f, e, d, c, b, a] = [g, f, e, (d + t1) >>> 0, c, b, a, (t1 + t2) >>> 0];\n }\n const state = [a, b, c, d, e, f, g, h];\n hash = new Uint32Array(hash.map((value, index) => ((value + (state[index] ?? 0)) >>> 0) >>> 0));\n }\n return Array.from(hash, (word) => word.toString(16).padStart(8, '0')).join('');\n}\n\nfunction rotateRight(value: number, shift: number): number {\n return (value >>> shift) | (value << (32 - shift));\n}\n\nfunction bigSigma0(value: number): number {\n return rotateRight(value, 2) ^ rotateRight(value, 13) ^ rotateRight(value, 22);\n}\n\nfunction bigSigma1(value: number): number {\n return rotateRight(value, 6) ^ rotateRight(value, 11) ^ rotateRight(value, 25);\n}\n\nfunction smallSigma0(value: number): number {\n return rotateRight(value, 7) ^ rotateRight(value, 18) ^ (value >>> 3);\n}\n\nfunction smallSigma1(value: number): number {\n return rotateRight(value, 17) ^ rotateRight(value, 19) ^ (value >>> 10);\n}\n\n/**\n * The three user-region material texture fields whose handles flow through the\n * paramSchema-driven extract filter (`validateTextureHandle`). They map to the\n * fixed `@group(1) @binding(2/4/6)` slots in the standard material BGL.\n *\n * `emissiveTexture` / `occlusionTexture` are deliberately EXCLUDED: they live\n * in the engine-managed lightmap injection region (`appendInjection`,\n * bindings 14..17), are sampled by `default-standard-pbr` without a schema\n * entry, and are never filtered by `validateTextureHandle`. Including them\n * would false-positive the engine's own PBR shader.\n */\nconst USER_REGION_TEXTURE_FIELDS: readonly string[] = [\n 'baseColorTexture',\n 'metallicRoughnessTexture',\n 'normalTexture',\n];\n\n/** Strip `//` line comments and block comments before scanning WGSL source. */\nfunction stripWgslComments(source: string): string {\n return source.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/\\/\\/[^\\n]*/g, '');\n}\n\n/**\n * Detect user-region material textures a shader actually `textureSample`s but\n * its paramSchema fails to declare as a texture entry.\n *\n * This is the runtime (register-time) counterpart of the build-time superset\n * gate (`compareParamSchemaSuperset`): user shaders registered directly via\n * `ShaderRegistry.installMaterialArtifact` bypass the vite-plugin-shader\n * reflection path, so an under-declared schema would otherwise let the extract\n * stage's `validateTextureHandle` silently drop the sampled texture's handle\n * and fall back to the default white texture (charter P3 violation — the bug\n * that turned the LearnOpenGL 4.3 blending demo's grass + windows opaque white;\n * see docs/handover/2026-06-19-blending-transparency-regression-bisect.md).\n *\n * Returns the field names that are sampled-but-undeclared (empty = consistent).\n * Scan is name-based on the WGSL var passed as the first `textureSample*`\n * argument; comments are stripped first so a commented-out sample never trips\n * the check. Only the three `USER_REGION_TEXTURE_FIELDS` participate, so a\n * shader that merely *declares* the standard binding layout without sampling it\n * (e.g. an outline / depth-viz shader reusing the PBR BGL) is not flagged.\n */\nexport function findUndeclaredSampledTextures(\n wgslSource: string,\n schema: readonly ParamSchemaEntry[],\n): readonly string[] {\n const declared = derive(schema).textureFieldNames;\n const clean = stripWgslComments(wgslSource);\n const sampleRe = /textureSample[A-Za-z]*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)/g;\n const sampled = new Set<string>();\n for (let m = sampleRe.exec(clean); m !== null; m = sampleRe.exec(clean)) {\n const name = m[1];\n if (name !== undefined) sampled.add(name);\n }\n return USER_REGION_TEXTURE_FIELDS.filter((f) => sampled.has(f) && !declared.has(f));\n}\n","// @forgeax/engine-types - producer-owned asset catalog contract.\n//\n// This is the shared POD boundary between an engine asset producer and any\n// consumer. Consumers must use these facts instead of deriving origin from a\n// DDC URL, filename suffix, or catalog position.\n\nexport type AssetSubjectType = 'asset' | 'package' | 'resource';\n\n/** The producer-owned subject behind a catalog row. */\nexport type CatalogSubject = 'internal-asset' | 'imported-output';\n\n/** Whether the runtime projection is validated directly or cooked. */\nexport type CookExecution = 'direct' | 'cooked';\n\n/** Derived lifecycle states exposed by the catalog. */\nexport type CatalogLifecycle = 'missing' | 'cooking' | 'current' | 'stale' | 'failed';\n\n/** Usage derived from producer refs and build-time content reads. */\nexport type AssetPublicationEvidenceUsage = 'reference' | 'content' | 'both';\n\n/** One external dependency observed while publishing a source package. */\nexport interface AssetPublicationExternalEvidence {\n readonly guid: string;\n readonly usage: AssetPublicationEvidenceUsage;\n readonly generation?: number;\n readonly digest?: string;\n}\n\n/** One ordinary asset output in an atomic source-package publication. */\nexport interface AssetPublicationOutput {\n readonly guid: string;\n readonly sourceKey: string;\n readonly kind: string;\n readonly digest: string;\n /** GUIDs emitted by the owning ordinary producer, in stable order. */\n readonly refs: readonly string[];\n}\n\n/** Receipt facts that prove the complete output set and dependency closure. */\nexport interface AssetPublicationReceipt {\n readonly schemaVersion: 'asset-publication-receipt/1';\n readonly sourcePath: string;\n readonly sourceRevision: string;\n readonly inputFingerprint: string;\n readonly outputDigest: string;\n readonly outputSetDigest: string;\n readonly externalEvidence: readonly AssetPublicationExternalEvidence[];\n}\n\n/** Stable locator for current or last-known-good publication recovery. */\nexport interface AssetPublicationLocator {\n readonly generation: number;\n readonly digest: string;\n readonly outputSetDigest: string;\n readonly packageUrl: string;\n readonly receiptKey: string;\n}\n\nexport type AssetPublicationFailureStage =\n | 'source'\n | 'output'\n | 'receipt'\n | 'route'\n | 'catalog'\n | 'cancelled';\n\n/** Machine-readable failure and recovery facts; messages are not a protocol. */\nexport interface AssetPublicationFailure {\n readonly code: string;\n readonly stage: AssetPublicationFailureStage;\n readonly sourcePath: string;\n readonly sourceRevision?: string;\n readonly generation?: number;\n readonly outputGuid?: string;\n readonly reason: string;\n}\n\n/** Actions a consumer can execute after a candidate publication is rejected. */\nexport interface AssetPublicationRecovery {\n readonly retryable: boolean;\n readonly preserveCurrent: boolean;\n readonly useLastKnownGood: boolean;\n readonly actions: readonly string[];\n}\n\n/**\n * Engine-owned publication SSOT shared by pack, import, Catalog, and consumers.\n * A publication is valid only when receipt and every output belong to one\n * source revision, generation, digest, and output-set digest tuple.\n */\nexport interface AssetPublicationEnvelope {\n readonly schemaVersion: 'asset-publication/1';\n readonly sourcePath: string;\n readonly sourceRevision: string;\n readonly generation: number;\n readonly digest: string;\n readonly outputSetDigest: string;\n readonly outputs: readonly AssetPublicationOutput[];\n readonly receipt: AssetPublicationReceipt;\n readonly externalEvidence: readonly AssetPublicationExternalEvidence[];\n readonly failureStage?: AssetPublicationFailureStage;\n readonly failure?: AssetPublicationFailure;\n readonly current?: AssetPublicationLocator;\n readonly lastKnownGood?: AssetPublicationLocator;\n readonly recovery?: AssetPublicationRecovery;\n}\n\n/**\n * Immutable source-level identity carried by an authored generated Scene mount.\n * A mount is consumable only when every member resolves from this complete\n * publication tuple; no single output GUID or generation number is sufficient.\n */\nexport interface ScenePublicationFence {\n readonly schemaVersion: 'scene-publication-fence/1';\n readonly sourcePath: string;\n readonly sourceRevision: string;\n readonly publicationGeneration: number;\n readonly outputDigest: string;\n readonly outputSetDigest: string;\n readonly receiptIdentity: string;\n}\n\nexport type CatalogOperationName =\n | 'preview'\n | 'save'\n | 'rebuild'\n | 'sourceOverride'\n | 'instanceOverride'\n | 'promote';\n\nexport interface CatalogOperationDescriptor {\n readonly operation: CatalogOperationName;\n readonly enabled: boolean;\n readonly reason?: string;\n}\n\nexport type CatalogOperations = Readonly<Record<CatalogOperationName, CatalogOperationDescriptor>>;\n\nexport interface CatalogProjectionInput {\n readonly subject: CatalogSubject;\n readonly execution: CookExecution;\n readonly lifecycle: CatalogLifecycle;\n}\n\n/** The explicit three-axis projection consumed by AI-facing catalog clients. */\nexport interface CatalogProjection extends CatalogProjectionInput {\n readonly operations: CatalogOperations;\n readonly lastKnownGood?: {\n readonly packageUrl: string;\n readonly receiptUrl?: string;\n };\n}\n\n/**\n * Derive operation descriptors from catalog facts only.\n *\n * `kind`, paths, and diagnostic messages are intentionally absent from this\n * function: a consumer receives a complete operation matrix and can branch\n * on `enabled` without reimplementing producer policy.\n */\nexport function catalogOperationsFor(input: CatalogProjectionInput): CatalogOperations {\n const imported = input.subject === 'imported-output';\n const current = input.lifecycle === 'current';\n const ready = current && (input.execution === 'direct' || input.execution === 'cooked');\n const canRebuild = input.execution === 'cooked';\n const canPreview = input.execution === 'cooked' && input.lifecycle !== 'missing';\n const operation = (name: CatalogOperationName, enabled: boolean, reason?: string) => ({\n operation: name,\n enabled,\n ...(reason === undefined ? {} : { reason }),\n });\n\n return {\n preview: operation('preview', canPreview, canPreview ? undefined : 'no projection to preview'),\n save: operation(\n 'save',\n !imported && input.execution === 'direct' && ready,\n imported ? 'imported output is read-only' : 'direct projection is not current',\n ),\n rebuild: operation(\n 'rebuild',\n canRebuild,\n canRebuild ? undefined : 'direct assets do not require a cook',\n ),\n sourceOverride: operation(\n 'sourceOverride',\n imported && canRebuild,\n imported\n ? canRebuild\n ? undefined\n : 'cooked projection is not available'\n : 'only imported output has a source override',\n ),\n instanceOverride: operation(\n 'instanceOverride',\n imported && current,\n imported\n ? current\n ? undefined\n : 'projection is not current'\n : 'only imported output has an instance override',\n ),\n promote: operation(\n 'promote',\n imported && current,\n imported\n ? current\n ? undefined\n : 'projection is not current'\n : 'internal assets are already authored',\n ),\n };\n}\n\n/** Reject impossible axis combinations before a catalog row is published. */\nexport function isCatalogProjectionValid(input: CatalogProjection): boolean {\n if (input.execution === 'direct' && input.lifecycle !== 'current') return false;\n if (input.subject === 'imported-output' && input.execution !== 'cooked') return false;\n return Object.entries(input.operations).every(\n ([name, descriptor]) => name === descriptor.operation,\n );\n}\n\nexport interface AssetAuthoringUnavailableReason {\n readonly code: 'unsupported-asset-kind' | 'missing-producer-capability';\n readonly hint: string;\n}\n\n/** Engine-facing operation shape exposed by a producer-owned catalog row. */\nexport type AssetPlacementCapability =\n | { readonly operation: 'spawnEntity' }\n | { readonly operation: 'addSceneAssetToScene' }\n | { readonly operation: 'unavailable'; readonly reason: AssetAuthoringUnavailableReason };\n\nexport interface AssetBindingTarget {\n readonly component: string;\n readonly field: string;\n readonly assetType: string;\n readonly cardinality: 'single' | 'array';\n}\n\n/**\n * One projection exposed by the producer-owned UI authoring contract.\n *\n * `supported` describes a real engine seam; `unavailable` is deliberately\n * structured so an editor or AI client cannot silently invent a consumer-side\n * implementation for a missing producer capability.\n */\nexport type UiAuthoringProjection =\n | {\n readonly status: 'supported';\n readonly operation:\n | 'createUiPreviewSession'\n | 'mountUi'\n | 'gameProjection'\n | 'dom-native'\n | 'ui-artifact-companion';\n readonly contractVersion: '1';\n }\n | {\n readonly status: 'unavailable';\n readonly reason: AssetAuthoringUnavailableReason;\n };\n\n/**\n * Versioned UI authoring facts published beside every `kind: 'ui'` catalog\n * row. This is a protocol descriptor, not a promise that the editor may reach\n * into a game world: runtime state/action/read semantics remain owned by the\n * game projection registrar and the UI mount/preview seams remain owned by the\n * engine UI package.\n */\nexport interface UiAuthoringCapability {\n readonly contractVersion: '1';\n readonly profileVersion: '1';\n readonly preview: {\n readonly operation: 'createUiPreviewSession';\n readonly lifecycle: 'open-rebuild-retry-dispose';\n };\n readonly mount: {\n readonly operation: 'mountUi';\n readonly lifecycle: 'mount-dispose';\n readonly actionPort: 'onAction';\n };\n readonly state: UiAuthoringProjection;\n readonly actions: UiAuthoringProjection;\n readonly reads: UiAuthoringProjection;\n readonly input: UiAuthoringProjection;\n readonly navigation: UiAuthoringProjection;\n readonly font: UiAuthoringProjection;\n readonly localization: UiAuthoringProjection;\n}\n\nexport type AssetBindingCapability =\n | {\n readonly operation: 'bindAssetRef' | 'createMaterialThenBindAssetRef';\n readonly target: AssetBindingTarget;\n readonly requiredSlots: 1;\n }\n | { readonly operation: 'unavailable'; readonly reason: AssetAuthoringUnavailableReason };\n\n/** Producer-owned placement and binding facts for one catalog asset. */\nexport interface AssetAuthoringCapability {\n readonly placement: AssetPlacementCapability;\n readonly binding: AssetBindingCapability;\n /** Present for producer-owned UI assets; absent for unrelated kinds. */\n readonly ui?: UiAuthoringCapability;\n readonly sourceOverrides?: readonly SourceOverrideDescriptor[];\n}\n\nconst UI_AUTHORING_CAPABILITY: UiAuthoringCapability = {\n contractVersion: '1',\n profileVersion: '1',\n preview: {\n operation: 'createUiPreviewSession',\n lifecycle: 'open-rebuild-retry-dispose',\n },\n mount: {\n operation: 'mountUi',\n lifecycle: 'mount-dispose',\n actionPort: 'onAction',\n },\n state: { status: 'supported', operation: 'gameProjection', contractVersion: '1' },\n actions: { status: 'supported', operation: 'gameProjection', contractVersion: '1' },\n reads: { status: 'supported', operation: 'gameProjection', contractVersion: '1' },\n input: { status: 'supported', operation: 'dom-native', contractVersion: '1' },\n navigation: { status: 'supported', operation: 'dom-native', contractVersion: '1' },\n font: { status: 'supported', operation: 'ui-artifact-companion', contractVersion: '1' },\n localization: {\n status: 'unavailable',\n reason: {\n code: 'missing-producer-capability',\n hint: 'UI localization resources are not yet published through the UI authoring contract.',\n },\n },\n};\n\n/** Built-in defaults for legacy rows that do not carry an explicit override. */\nexport function authoringCapabilityForAssetKind(kind: string): AssetAuthoringCapability {\n switch (kind) {\n case 'ui':\n return {\n placement: {\n operation: 'unavailable',\n reason: {\n code: 'unsupported-asset-kind',\n hint: 'UI assets mount through the UI runtime and are not ECS scene placements.',\n },\n },\n binding: {\n operation: 'unavailable',\n reason: {\n code: 'unsupported-asset-kind',\n hint: 'UI assets bind through their producer-owned UI runtime contract.',\n },\n },\n ui: UI_AUTHORING_CAPABILITY,\n };\n case 'scene':\n return {\n placement: { operation: 'addSceneAssetToScene' },\n binding: {\n operation: 'unavailable',\n reason: {\n code: 'unsupported-asset-kind',\n hint: 'Scene assets are placed as a scene mount.',\n },\n },\n };\n case 'mesh':\n return {\n placement: { operation: 'spawnEntity' },\n binding: {\n operation: 'bindAssetRef',\n target: {\n component: 'MeshFilter',\n field: 'assetHandle',\n assetType: 'MeshAsset',\n cardinality: 'single',\n },\n requiredSlots: 1,\n },\n };\n case 'material':\n return {\n placement: { operation: 'spawnEntity' },\n binding: {\n operation: 'bindAssetRef',\n target: {\n component: 'MeshRenderer',\n field: 'materials',\n assetType: 'MaterialAsset',\n cardinality: 'array',\n },\n requiredSlots: 1,\n },\n };\n case 'texture':\n return {\n placement: { operation: 'spawnEntity' },\n binding: {\n operation: 'createMaterialThenBindAssetRef',\n target: {\n component: 'MeshRenderer',\n field: 'materials',\n assetType: 'MaterialAsset',\n cardinality: 'array',\n },\n requiredSlots: 1,\n },\n };\n case 'particle-effect':\n return {\n placement: { operation: 'spawnEntity' },\n binding: {\n operation: 'bindAssetRef',\n target: {\n component: 'ParticleEffectPlayer',\n field: 'effect',\n assetType: 'ParticleEffectAsset',\n cardinality: 'single',\n },\n requiredSlots: 1,\n },\n };\n default:\n return {\n placement: {\n operation: 'unavailable',\n reason: {\n code: 'unsupported-asset-kind',\n hint: `No placement capability is published for asset kind '${kind}'.`,\n },\n },\n binding: {\n operation: 'unavailable',\n reason: {\n code: 'unsupported-asset-kind',\n hint: `No binding capability is published for asset kind '${kind}'.`,\n },\n },\n };\n }\n}\n\n/** Stable producer subject identity used by relations and diagnostics. */\nexport interface AssetSubjectRef {\n readonly type: AssetSubjectType;\n readonly id: string;\n}\n\n/** Provider identity carried with producer-owned facts; not a catalog locator. */\nexport interface ProviderProvenance {\n readonly provider: string;\n readonly version: string;\n readonly source?: string;\n}\n\n/** Producer-owned JSON payload for one stable imported-output source key. */\nexport type SourceOverridePayload = Readonly<Record<string, unknown>>;\n\n/** Optional Meta author facts keyed by the producer's stable sourceKey. */\nexport type SourceOverrideMap = Readonly<Record<string, SourceOverridePayload>>;\n\nexport interface SourceOverrideDescriptor {\n readonly sourceKey: string;\n /** Producer-owned semantic used by authoring hosts for catalog-aware validation. */\n readonly semantic?: 'mesh-material-slot-defaults';\n readonly payloadSchema?: unknown;\n}\n\n/** Shared payload contract explicitly published by Mesh-producing importers. */\nexport const MESH_MATERIAL_SLOT_SOURCE_OVERRIDE_PAYLOAD_SCHEMA = {\n type: 'object',\n properties: {\n materialSlots: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n slotName: { type: 'string', minLength: 1 },\n sourceKey: { type: 'string', minLength: 1 },\n defaultMaterialGuid: { type: 'string' },\n },\n required: ['slotName'],\n additionalProperties: true,\n },\n },\n materialSlotDefaultOverrides: {\n type: 'object',\n additionalProperties: { type: 'string', nullable: true },\n },\n },\n additionalProperties: true,\n} as const;\n\nexport type SourceOverrideErrorCode =\n | 'unknown-source-key'\n | 'duplicate-source-key'\n | 'invalid-source-overrides'\n | 'invalid-source-override-payload';\n\nexport interface SourceOverrideDiagnostic {\n readonly code: SourceOverrideErrorCode;\n readonly expected: string;\n readonly actual?: string;\n readonly hint: string;\n}\n\nexport type SourceOverrideValidationResult =\n | { readonly ok: true; readonly value: SourceOverrideMap | undefined }\n | { readonly ok: false; readonly error: SourceOverrideDiagnostic };\n\nfunction sourceOverrideError(\n code: SourceOverrideErrorCode,\n expected: string,\n hint: string,\n actual?: string,\n): SourceOverrideValidationResult {\n return {\n ok: false,\n error: { code, expected, hint, ...(actual === undefined ? {} : { actual }) },\n };\n}\n\nfunction isSourceOverridePayload(value: unknown): value is SourceOverridePayload {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Canonicalize legacy/empty override maps without changing producer payloads. */\nexport function canonicalizeSourceOverrides(value: unknown): SourceOverrideMap | undefined {\n if (value === undefined) return undefined;\n if (!isSourceOverridePayload(value)) return undefined;\n const keys = Object.keys(value);\n if (keys.length === 0) return undefined;\n return Object.fromEntries(keys.sort().map((key) => [key, value[key]])) as SourceOverrideMap;\n}\n\nfunction validateSourceOverrideEntry(\n sourceKey: string,\n payload: unknown,\n declared: ReadonlySet<string>,\n seen: Set<string>,\n): SourceOverrideDiagnostic | undefined {\n if (seen.has(sourceKey)) {\n return {\n code: 'duplicate-source-key',\n expected: 'sourceKey values to be unique within sourceOverrides',\n hint: 'remove the duplicate source override',\n actual: sourceKey,\n };\n }\n seen.add(sourceKey);\n if (!declared.has(sourceKey)) {\n return {\n code: 'unknown-source-key',\n expected: 'sourceKey to be declared by the producer topology',\n hint: 'request a fresh Catalog topology before writing Meta',\n actual: sourceKey,\n };\n }\n if (!isSourceOverridePayload(payload)) {\n return {\n code: 'invalid-source-override-payload',\n expected: 'each source override payload to be a producer-owned object',\n hint: 'validate the payload with the producer schema',\n actual: sourceKey,\n };\n }\n return undefined;\n}\n\nfunction validateSourceOverrideEntries(\n entries: readonly (readonly [string, unknown])[],\n declared: ReadonlySet<string>,\n): SourceOverrideValidationResult {\n const seen = new Set<string>();\n for (const [sourceKey, payload] of entries) {\n const error = validateSourceOverrideEntry(sourceKey, payload, declared, seen);\n if (error !== undefined) return { ok: false, error };\n }\n return { ok: true, value: canonicalizeSourceOverrides(Object.fromEntries(entries)) };\n}\n\n/** Validate source override identity while leaving payload interpretation to the producer. */\nexport function validateSourceOverrideMap(\n value: unknown,\n declaredSourceKeys: readonly string[],\n): SourceOverrideValidationResult {\n const declared = new Set<string>();\n for (const sourceKey of declaredSourceKeys) {\n if (declared.has(sourceKey)) {\n return sourceOverrideError(\n 'duplicate-source-key',\n 'sourceKey values declared by a producer to be unique',\n 'repair the producer topology before publishing Meta',\n sourceKey,\n );\n }\n declared.add(sourceKey);\n }\n if (value === undefined) return { ok: true, value: undefined };\n if (Array.isArray(value)) {\n const entries: (readonly [string, unknown])[] = [];\n for (const item of value) {\n if (!Array.isArray(item) || item.length !== 2 || typeof item[0] !== 'string') {\n return sourceOverrideError(\n 'invalid-source-overrides',\n 'sourceOverrides to be an object keyed by sourceKey',\n 'pass a producer-owned source override map',\n );\n }\n entries.push([item[0], item[1]]);\n }\n return validateSourceOverrideEntries(entries, declared);\n }\n if (!isSourceOverridePayload(value)) {\n return sourceOverrideError(\n 'invalid-source-overrides',\n 'sourceOverrides to be an object keyed by sourceKey',\n 'pass a producer-owned source override map',\n );\n }\n return validateSourceOverrideEntries(Object.entries(value), declared);\n}\n\n/** Monotonic producer observation used to validate catalog continuity. */\nexport interface ResourceRevision {\n readonly digest: string;\n readonly observedAt: number;\n readonly rootId: string;\n}\n\nexport type AssetRelationType =\n | 'references'\n | 'reads'\n | 'depends-on'\n | 'owns'\n | 'contains'\n | 'produces'\n | 'materialized-as'\n | (string & {});\n\nexport interface AssetRelationPolicy {\n readonly ownership?: 'owned' | 'shared';\n readonly lifecycle?: 'authored' | 'derived';\n readonly strength?: 'required' | 'optional';\n}\n\n/** Structured graph edge emitted by a producer; consumers must preserve its fields. */\nexport interface AssetRelation {\n readonly from: AssetSubjectRef;\n readonly to: AssetSubjectRef;\n readonly type: AssetRelationType;\n readonly policy?: AssetRelationPolicy;\n readonly provenance: ProviderProvenance;\n}\n\nexport type CatalogDiagnosticSeverity = 'info' | 'warning' | 'blocking';\n\n/** Machine-readable catalog problem; consumers branch on fields, never message text. */\nexport interface CatalogDiagnostic {\n readonly code: string;\n readonly severity: CatalogDiagnosticSeverity;\n readonly message?: string;\n readonly subject?: AssetSubjectRef;\n readonly expected?: string;\n readonly actual?: string;\n readonly hint?: string;\n readonly authority?: 'producer' | 'pack' | 'catalog';\n readonly evidence?: readonly AssetSubjectRef[];\n readonly recoveryIntents?: readonly string[];\n}\n\n/** Closed set of contract failures returned by producer validation. */\nexport type ProducerContractErrorCode =\n | SourceOverrideErrorCode\n | TopologyConflictReason\n | 'invalid-source-key'\n | 'invalid-source-index'\n | 'invalid-producer-fact';\n\n/** Structured producer validation failure with its owning authority. */\nexport interface ProducerContractDiagnostic {\n readonly code: ProducerContractErrorCode;\n readonly subject: AssetSubjectRef;\n readonly expected: string;\n readonly actual?: string;\n readonly hint: string;\n readonly authority: 'producer' | 'pack';\n}\n\n/** Result boundary for producer validation; success and failure are discriminated by `ok`. */\nexport type ProducerContractResult<T> =\n | { readonly ok: true; readonly value: T }\n | { readonly ok: false; readonly error: ProducerContractDiagnostic };\n\n/** Canonical producer output declaration used for topology matching and recovery. */\nexport interface ImportedOutputDeclaration {\n readonly guid: string;\n readonly sourceKey?: string;\n readonly sourceIndex: number;\n readonly kind: string;\n readonly name?: string;\n /** New kinds that may reuse this output's prior GUID. */\n readonly compatiblePreviousKinds?: readonly string[];\n}\n\nexport type ProposedOutput = ImportedOutputDeclaration;\n\nexport type ExistingOutput = ImportedOutputDeclaration;\n\nexport interface KindChange {\n readonly guid: string;\n readonly oldKind: string;\n readonly newKind: string;\n readonly sourceKey?: string;\n readonly action: 'remove-add' | 'preserve-guid';\n}\n\nexport type TopologyConflictReason =\n | 'duplicate-source-key'\n | 'missing-source-key'\n | 'source-index-ambiguous';\n\nexport interface MatchConflict {\n readonly reason: TopologyConflictReason;\n readonly sourceKey?: string;\n readonly previous: readonly ExistingOutput[];\n readonly next: readonly ProposedOutput[];\n}\n\nexport interface TopologyPreserved {\n readonly guid: string;\n readonly oldKey: string;\n readonly newKey: string;\n}\n\nexport interface TopologyDiff {\n readonly preserved: readonly TopologyPreserved[];\n readonly added: readonly ProposedOutput[];\n readonly removed: readonly ExistingOutput[];\n readonly changedKind: readonly KindChange[];\n readonly ambiguous: readonly MatchConflict[];\n}\n","import type { AssetPublicationTuple } from './asset.js';\nimport type {\n AssetAuthoringCapability,\n AssetPublicationEnvelope,\n AssetRelation,\n CatalogDiagnostic,\n CatalogLifecycle,\n CatalogProjection,\n CatalogSubject,\n CookExecution,\n ProviderProvenance,\n ResourceRevision,\n SourceOverrideDescriptor,\n SourceOverrideMap,\n TopologyDiff,\n} from './asset-producer';\nimport { err, ok, type Result } from './result';\n\nexport type {\n AssetPublicationEnvelope,\n AssetPublicationEvidenceUsage,\n AssetPublicationExternalEvidence,\n AssetPublicationFailure,\n AssetPublicationFailureStage,\n AssetPublicationLocator,\n AssetPublicationOutput,\n AssetPublicationReceipt,\n AssetPublicationRecovery,\n CatalogLifecycle,\n CatalogProjection,\n CatalogSubject,\n CookExecution,\n} from './asset-producer';\n\n/**\n * Strict contract row for the Catalog projection.\n *\n * This is evidence for an AI consumer, not authoring authority: a Catalog row\n * projects producer facts and runtime navigation while preserving lifecycle,\n * execution, and sourceKey distinctions.\n */\nexport interface CatalogEntryV2 extends AssetPublicationTuple {\n readonly guid: string;\n readonly packageUrl: string;\n readonly kind: string;\n readonly sourcePath: string;\n readonly subject: CatalogSubject;\n readonly execution: CookExecution;\n readonly lifecycle: CatalogLifecycle;\n readonly projection: CatalogProjection;\n}\n/** One producer revision point in a catalog continuity window. */\nexport interface CatalogRevisionPoint {\n readonly rootId: string;\n readonly revision: number;\n}\n\n/** Baseline/current revision sets used to reject stale or partial updates. */\nexport interface CatalogRevisionWindow {\n readonly baseline: readonly CatalogRevisionPoint[];\n readonly current: readonly CatalogRevisionPoint[];\n}\n\n/** One stable row from a development or build catalog snapshot. */\nexport interface CatalogEntry {\n readonly guid: string;\n /** GUID-to-pack navigation only; artifact paths live inside Pack v2. */\n readonly packageUrl: string;\n readonly kind: string;\n /** Producer-owned placement/binding facts; absent only on legacy rows. */\n readonly authoring?: AssetAuthoringCapability;\n /** Source declaration navigation for diagnostics, not runtime content. */\n readonly sourcePath: string;\n /** Stable package identity; path is a locator, never the package identity. */\n readonly packageId?: string;\n /** Producer-owned importer/provider identity and version. */\n readonly provenance?: ProviderProvenance;\n /** Producer-owned resource/package revision used for conflict checks. */\n readonly revision?: ResourceRevision;\n /** Stable producer key for imported-output topology matching; never infer it from sourceIndex. */\n readonly sourceKey?: string;\n /** Producer-declared output position; never used as identity when sourceKey exists. */\n readonly sourceIndex?: number;\n /** Producer-owned author facts carried through the catalog without interpretation. */\n readonly sourceOverrides?: SourceOverrideMap;\n readonly sourceOverrideDescriptors?: readonly SourceOverrideDescriptor[];\n /** Typed graph edges emitted by the producer. */\n readonly relations?: readonly AssetRelation[];\n /** Structured producer diagnostics; consumers must not parse messages. */\n readonly diagnostics?: readonly CatalogDiagnostic[];\n readonly name?: string;\n /** Optional navigation to the producer-owned cook receipt. */\n readonly cookReceiptUrl?: string;\n readonly refs?: readonly string[];\n /** Explicit producer-owned runtime projection axes. */\n readonly subject?: CatalogSubject;\n readonly execution?: CookExecution;\n readonly lifecycle?: CatalogLifecycle;\n readonly projection?: CatalogProjection;\n /** Complete Engine publication tuple, when this row came from a source package. */\n readonly publication?: AssetPublicationEnvelope;\n}\n\n/**\n * A folded, neutral set of catalog-row changes keyed by stable GUID.\n *\n * `authority` and `diagnostics` tell AI-readable consumers whether the delta\n * is safe to apply; a degraded delta carries no identity-bearing changes.\n */\nexport interface CatalogDelta {\n /** Runtime realm identity for dev publications; absent for immutable legacy builds. */\n readonly scopeId?: string;\n readonly generation?: number;\n readonly added: readonly CatalogEntry[];\n readonly changed: readonly CatalogEntry[];\n readonly removed: readonly CatalogEntry['guid'][];\n /** Optional topology evidence for imported-output changes in this delta. */\n readonly topology?: readonly TopologyDiff[];\n /** Present when a watch revision was supplied for continuity validation. */\n readonly authority?: 'authoritative' | 'degraded';\n /** Machine-readable continuity or topology diagnostics. */\n readonly diagnostics?: readonly CatalogDiagnostic[];\n readonly revisions?: CatalogRevisionWindow;\n}\n\nexport interface CatalogDeltaValidationError {\n readonly code: 'catalog-delta-invalid';\n readonly expected: string;\n readonly hint: string;\n readonly detail: { readonly field: string };\n}\n\nfunction catalogInvalid(field: string): Result<never, CatalogDeltaValidationError> {\n return err({\n code: 'catalog-delta-invalid',\n expected: 'a CatalogDelta with complete row identity and string removals',\n hint: 'discard the delta and enumerate a verified catalog snapshot',\n detail: { field },\n });\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\nfunction isStringArray(value: unknown): value is readonly string[] {\n return Array.isArray(value) && value.every(isNonEmptyString);\n}\n\nfunction isSubjectRef(value: unknown): boolean {\n return (\n isRecord(value) &&\n (value.type === 'asset' || value.type === 'package' || value.type === 'resource') &&\n isNonEmptyString(value.id)\n );\n}\n\nfunction isRevision(value: unknown): value is ResourceRevision {\n if (!isRecord(value)) return false;\n const observedAt = value.observedAt;\n return (\n isNonEmptyString(value.digest) &&\n typeof observedAt === 'number' &&\n Number.isSafeInteger(observedAt) &&\n observedAt >= 0 &&\n isNonEmptyString(value.rootId)\n );\n}\n\nfunction isDiagnostic(value: unknown): value is CatalogDiagnostic {\n return (\n isRecord(value) &&\n isNonEmptyString(value.code) &&\n (value.severity === 'info' || value.severity === 'warning' || value.severity === 'blocking') &&\n (value.message === undefined || typeof value.message === 'string') &&\n (value.subject === undefined || isSubjectRef(value.subject)) &&\n (value.expected === undefined || typeof value.expected === 'string') &&\n (value.actual === undefined || typeof value.actual === 'string') &&\n (value.hint === undefined || typeof value.hint === 'string') &&\n (value.authority === undefined ||\n value.authority === 'producer' ||\n value.authority === 'pack' ||\n value.authority === 'catalog') &&\n (value.evidence === undefined ||\n (Array.isArray(value.evidence) && value.evidence.every(isSubjectRef))) &&\n (value.recoveryIntents === undefined || isStringArray(value.recoveryIntents))\n );\n}\n\nfunction isRevisionWindow(value: unknown): value is CatalogRevisionWindow {\n if (!isRecord(value) || !Array.isArray(value.baseline) || !Array.isArray(value.current)) {\n return false;\n }\n const isPoint = (point: unknown): boolean =>\n isRecord(point) &&\n isNonEmptyString(point.rootId) &&\n typeof point.revision === 'number' &&\n Number.isSafeInteger(point.revision) &&\n point.revision >= 0;\n return value.baseline.every(isPoint) && value.current.every(isPoint);\n}\n\nfunction isTopologyDiff(value: unknown): boolean {\n if (!isRecord(value)) return false;\n return ['preserved', 'added', 'removed', 'changedKind', 'ambiguous'].every((field) =>\n Array.isArray(value[field]),\n );\n}\n\nfunction isCatalogEntry(value: unknown): value is CatalogEntry {\n if (!isRecord(value)) return false;\n if (\n !isNonEmptyString(value.guid) ||\n !isNonEmptyString(value.packageUrl) ||\n !isNonEmptyString(value.kind) ||\n !isNonEmptyString(value.sourcePath)\n ) {\n return false;\n }\n if (value.authoring !== undefined && !isRecord(value.authoring)) return false;\n if (value.packageId !== undefined && !isNonEmptyString(value.packageId)) return false;\n if (value.provenance !== undefined) {\n if (!isRecord(value.provenance)) return false;\n if (\n !isNonEmptyString(value.provenance.provider) ||\n !isNonEmptyString(value.provenance.version)\n ) {\n return false;\n }\n if (value.provenance.source !== undefined && !isNonEmptyString(value.provenance.source)) {\n return false;\n }\n }\n if (value.revision !== undefined && !isRevision(value.revision)) return false;\n if (value.sourceKey !== undefined && !isNonEmptyString(value.sourceKey)) return false;\n const sourceIndex = value.sourceIndex;\n if (\n sourceIndex !== undefined &&\n (typeof sourceIndex !== 'number' || !Number.isSafeInteger(sourceIndex) || sourceIndex < 0)\n )\n return false;\n if (value.sourceOverrides !== undefined && !isRecord(value.sourceOverrides)) return false;\n if (\n value.sourceOverrideDescriptors !== undefined &&\n !Array.isArray(value.sourceOverrideDescriptors)\n ) {\n return false;\n }\n if (value.relations !== undefined && !Array.isArray(value.relations)) return false;\n if (\n value.diagnostics !== undefined &&\n (!Array.isArray(value.diagnostics) || !value.diagnostics.every(isDiagnostic))\n )\n return false;\n if (value.name !== undefined && !isNonEmptyString(value.name)) return false;\n if (value.cookReceiptUrl !== undefined && !isNonEmptyString(value.cookReceiptUrl)) return false;\n if (value.refs !== undefined && !isStringArray(value.refs)) return false;\n if (\n value.subject !== undefined &&\n value.subject !== 'internal-asset' &&\n value.subject !== 'imported-output'\n ) {\n return false;\n }\n if (\n value.execution !== undefined &&\n value.execution !== 'direct' &&\n value.execution !== 'cooked'\n ) {\n return false;\n }\n if (\n value.lifecycle !== undefined &&\n !['missing', 'cooking', 'current', 'stale', 'failed'].includes(value.lifecycle as string)\n ) {\n return false;\n }\n if (value.projection !== undefined && !isRecord(value.projection)) return false;\n return value.publication === undefined || isRecord(value.publication);\n}\n\nexport function validateCatalogDelta(\n value: unknown,\n): Result<CatalogDelta, CatalogDeltaValidationError> {\n if (!isRecord(value)) return catalogInvalid('delta');\n if (!Array.isArray(value.added)) return catalogInvalid('added');\n if (!Array.isArray(value.changed)) return catalogInvalid('changed');\n if (!Array.isArray(value.removed)) return catalogInvalid('removed');\n if (!value.added.every(isCatalogEntry)) return catalogInvalid('added.entry');\n if (!value.changed.every(isCatalogEntry)) return catalogInvalid('changed.entry');\n if (!value.removed.every((guid): guid is string => typeof guid === 'string' && guid.length > 0)) {\n return catalogInvalid('removed.guid');\n }\n const generation = value.generation;\n if (\n (value.scopeId !== undefined && !isNonEmptyString(value.scopeId)) ||\n (generation !== undefined &&\n (typeof generation !== 'number' || !Number.isSafeInteger(generation) || generation < 1))\n ) {\n return catalogInvalid('scope');\n }\n if ((value.scopeId === undefined) !== (value.generation === undefined)) {\n return catalogInvalid('scope.generation');\n }\n if (\n value.authority !== undefined &&\n value.authority !== 'authoritative' &&\n value.authority !== 'degraded'\n ) {\n return catalogInvalid('authority');\n }\n if (\n value.diagnostics !== undefined &&\n (!Array.isArray(value.diagnostics) || !value.diagnostics.every(isDiagnostic))\n ) {\n return catalogInvalid('diagnostics');\n }\n if (value.revisions !== undefined && !isRevisionWindow(value.revisions)) {\n return catalogInvalid('revisions');\n }\n if (\n value.topology !== undefined &&\n (!Array.isArray(value.topology) || !value.topology.every(isTopologyDiff))\n ) {\n return catalogInvalid('topology');\n }\n const identityKeys = [...value.added, ...value.changed].map((entry) => entry.guid.toLowerCase());\n if (new Set(identityKeys).size !== identityKeys.length) return catalogInvalid('duplicate.guid');\n const removedKeys = value.removed.map((guid) => guid.toLowerCase());\n if (new Set(removedKeys).size !== removedKeys.length) return catalogInvalid('duplicate.removed');\n if (value.authority === 'degraded' && identityKeys.length > 0) {\n return catalogInvalid('degraded.identity');\n }\n return ok(value as unknown as CatalogDelta);\n}\n\nfunction canonicalCatalogValue(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonicalCatalogValue).join(',')}]`;\n if (isRecord(value)) {\n return `{${Object.entries(value)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, child]) => `${JSON.stringify(key)}:${canonicalCatalogValue(child)}`)\n .join(',')}}`;\n }\n return JSON.stringify(value) ?? 'null';\n}\n\nfunction digestPart(value: string, seed: bigint): string {\n let hash = seed;\n for (let index = 0; index < value.length; index += 1) {\n hash ^= BigInt(value.charCodeAt(index));\n hash = BigInt.asUintN(64, hash * 0x100000001b3n);\n }\n return hash.toString(16).padStart(16, '0');\n}\n\nconst CATALOG_DIGEST_SEEDS = [\n 0xcbf29ce484222325n,\n 0x84222325cbf29ce4n,\n 0x9e3779b185ebca87n,\n 0x517cc1b727220a95n,\n];\n\nfunction catalogDigest(value: unknown): string {\n const canonical = canonicalCatalogValue(value);\n return `sha256:${CATALOG_DIGEST_SEEDS.map((seed) => digestPart(canonical, seed)).join('')}`;\n}\n\n/** Canonical semantic identity for one Catalog row, independent of object key order. */\nexport function catalogEntryDigest(entry: CatalogEntry): string {\n return catalogDigest({ ...entry, guid: entry.guid.toLowerCase() });\n}\n\nexport function catalogDeltaDigest(delta: CatalogDelta): string {\n return catalogDigest({\n ...delta,\n added: [...delta.added].sort((left, right) => left.guid.localeCompare(right.guid)),\n changed: [...delta.changed].sort((left, right) => left.guid.localeCompare(right.guid)),\n removed: [...delta.removed].sort(),\n });\n}\n\nexport type PackIndexEntry = CatalogEntry;\n","import type { AssetStageErrorBase } from './asset-errors.js';\nimport type { SourceOverrideErrorCode, SourceOverrideMap } from './asset-producer.js';\nimport type { PackIndexEntry } from './catalog.js';\nimport type { Asset, AssetCodec, AssetRef, ImageError, TextureAsset } from './index.js';\n\nexport type ImportStageContractError = AssetStageErrorBase<'import', 'import-failed'>;\n\n// === Import contract SSOT (feat-20260603-asset-import-loader-injection M2 / w12) ===\n//\n// Decision anchors:\n// - requirements AC-07 (Importer dispatched by `meta.importer` string key) +\n// AC-08 (`importer-not-registered` fail-fast) + AC-09 (GUID import-stable\n// iron law: `guid-mismatch` / `import-produced-no-assets`) + AC-10\n// (`ImportErrorCode` is a closed union with exhaustive switch without default)\n// - plan-strategy D-6 (error model add-only: `ImportErrorCode` is an\n// independent closed union, not folded into `AssetErrorCode`) + D-1\n// (`ImporterRegistry` register/get/fail-fast mirrors LoaderRegistry) + D-4\n// (import runner skips the reserved `importer: 'shader'` key)\n// - research Finding 8 (`ImportTransport` interface slot; HTTP adapter is\n// OOS-2, landed in M4) + Finding 9 (`PackError` four-field shape is the\n// structural template)\n// - charter P3 (structured failure: `.code` / `.expected` / `.hint` /\n// `.detail`; AI users consume via property access, never `.message`\n// parsing) + P4 (consistent abstraction, structurally parallel to\n// `PackError` / `AssetError`)\n//\n// The import side is the build-time half of the import/load split. An\n// `Importer` turns an external source (a `.gltf` / `.png` / `.ttf` on disk)\n// plus its `*.meta.json` GUID declarations into in-memory `ImportedAsset[]`\n// (internal `Asset` PODs stamped with the meta-declared GUIDs). The import\n// runner then materializes those into the DDC (`.pack.json` / `.bin`). The\n// `Importer` itself stays pure of disk write + GUID minting — it consumes the\n// meta-declared GUIDs (GUID import-stable iron law) and emits PODs only.\n\n/**\n * Closed `ImportErrorCode` union for build-time import failures.\n * Used exclusively by the build-time `@forgeax/engine-import` runner +\n * `ImporterRegistry` fail-fast chain. Domain-separated from the runtime\n * `AssetErrorCode` (the `loadByGuid` / `get` surface) and the disk-scanner\n * `PackErrorCode` — disjoint lifecycle phases. Counts evolve; see\n * AGENTS.md §Error model for the live roster.\n *\n * Exhaustive `switch (err.code)` needs no `default:` — TypeScript guards\n * union completeness at compile time (charter P2 machine-readable union >\n * prose + P3 explicit failure).\n *\n * | code | trigger |\n * |:--|:--|\n * | `'importer-not-registered'` | the import runner read `meta.importer` but the injected `ImporterRegistry` has no importer for that key; `.detail.importer` is the missing key and `.detail.registeredImporters` lists the keys currently wired (charter P3 — AI users read `.detail.registeredImporters` to know what to inject). |\n * | `'source-read-failed'` | the source file referenced by `meta.source` could not be read (missing / unreadable); `.detail.source` is the path and `.detail.reason` the underlying error string. |\n * | `'import-produced-no-assets'` | the importer returned an empty `ImportedAsset[]`, or omitted a GUID that `meta.subAssets[]` declared (the produced GUID set is not a superset of the declared set); `.detail.missingGuids` lists the declared GUIDs the importer failed to produce. |\n * | `'guid-mismatch'` | the importer produced a GUID that `meta.subAssets[]` never declared (violates the GUID import-stable iron law); `.detail.unexpectedGuids` lists the produced GUIDs absent from the declared set. |\n * | `'import-internal-error'` | the importer failed at runtime. Two sub-cases ride `.detail`: a build-time module-LOAD failure surfaces `.detail.loadError`, while a conversion THROW surfaces `.detail.reason`. |\n * | `'source-validation-failed'` | the source was readable but failed an authoring rule; `.detail.diagnostics` contains source-located, machine-readable findings. |\n */\nexport type ImportErrorCode =\n | SourceOverrideErrorCode\n | 'importer-not-registered'\n | 'source-read-failed'\n | 'import-produced-no-assets'\n | 'guid-mismatch'\n | 'mesh-material-slot-topology-change'\n | 'import-internal-error'\n | 'source-validation-failed';\n\n/** A source range shared by all import diagnostics. */\nexport interface ImportSourceRange {\n readonly start: number;\n readonly end: number;\n readonly line: number;\n readonly column: number;\n}\n\n/** A related source location for a cross-file import diagnostic. */\nexport interface ImportDiagnosticLocation {\n readonly sourcePath: string;\n readonly sourceRange: ImportSourceRange;\n}\n\n/** Machine-readable provenance for one blocking or quality import finding. */\nexport interface ImportDiagnostic {\n readonly code: string;\n readonly severity: 'error' | 'warning';\n readonly sourcePath: string;\n readonly sourceRange: ImportSourceRange;\n readonly rule: string;\n readonly expected: string;\n readonly actual: string;\n readonly hint: string;\n readonly relatedLocations?: readonly ImportDiagnosticLocation[];\n}\n\n/**\n * Discriminated detail union for {@link ImportError} — narrowed per\n * `ImportError.code`. AI users access `err.detail.<field>` directly after\n * `switch (err.code)` narrows the variant. Structurally parallel to\n * `PackErrorDetail` (the `code` field is intentionally absent from each\n * variant; identify via the top-level `ImportError.code`).\n */\nexport type ImportErrorDetail =\n | {\n /** The `meta.importer` key with no registered importer. */\n readonly importer: string;\n /** The importer keys currently wired into the registry (insertion order). */\n readonly registeredImporters: readonly string[];\n }\n | {\n /** The `meta.source` path that could not be read. */\n readonly source: string;\n /** The underlying read error message. */\n readonly reason: string;\n }\n | {\n /** Declared sub-asset GUIDs the importer failed to produce (empty when the importer produced nothing at all). */\n readonly missingGuids: readonly string[];\n }\n | {\n /** Produced GUIDs absent from the `meta.subAssets[]` declared set. */\n readonly unexpectedGuids: readonly string[];\n }\n | {\n /** The original thrown error message (importer loaded but its conversion threw). */\n readonly reason: string;\n }\n | {\n /**\n * The module-load failure message (feat-20260629 D-5): the importer\n * module / native addon could not be loaded at build time (e.g.\n * module-not-found, native-addon-not-built). Distinguishes a LOAD\n * failure from a conversion THROW (`reason`) under the same\n * `import-internal-error` code without growing the closed ImportErrorCode\n * union. AI users branch on `'loadError' in err.detail`.\n */\n readonly loadError: string;\n }\n | {\n /** Source-located authoring findings retained across the import boundary. */\n readonly diagnostics: readonly ImportDiagnostic[];\n }\n | {\n /** Source override identity that failed Meta topology validation. */\n readonly sourceKey?: string;\n readonly declaredSourceKeys: readonly string[];\n readonly reason?: string;\n }\n | {\n readonly meshGuid: string;\n readonly meshSourceKey?: string;\n readonly previousIndices: readonly number[];\n readonly nextIndices: readonly number[];\n };\n\n/**\n * Structured import error — four-field surface (`.code` / `.expected` /\n * `.hint` / `.detail`) structurally parallel to `PackError` / `AssetError`\n * (charter P4 consistent abstraction). `.detail` is narrowed per `.code` via\n * {@link ImportErrorDetail}.\n *\n * AI users consume the structured surface via property access:\n * `switch (err.code) { case 'guid-mismatch': ... err.detail.unexpectedGuids ... }`\n * — never by parsing `.message` (charter P3 red line).\n */\nexport class ImportError extends Error {\n readonly code: ImportErrorCode;\n readonly expected: string;\n readonly actual?: string;\n readonly hint: string;\n readonly detail: ImportErrorDetail;\n\n constructor(args: {\n code: ImportErrorCode;\n expected: string;\n actual?: string;\n hint: string;\n detail: ImportErrorDetail;\n }) {\n super(`[ImportError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);\n this.name = 'ImportError';\n this.code = args.code;\n this.expected = args.expected;\n if (args.actual !== undefined) this.actual = args.actual;\n this.hint = args.hint;\n this.detail = args.detail;\n }\n}\n\n/**\n * Per-code `.hint` string literals SSOT. `Record<ImportErrorCode, string>`\n * makes a new closed-union member a compile-time error here as well\n * (reinforces charter P3 explicit failure). Consumed by the import runner +\n * tests so the producer and the fixtures share one source of truth.\n */\nexport const IMPORT_ERROR_HINTS: Readonly<Record<ImportErrorCode, string>> = {\n 'importer-not-registered':\n 'no importer registered for this meta.importer key; register one via importers.register(importer) (the importer carries its own key, e.g. gltfImporter / imageImporter); err.detail.registeredImporters lists the keys currently wired',\n 'source-read-failed':\n 'the file at meta.source could not be read; check the path is correct relative to the sidecar and the process has read access',\n 'import-produced-no-assets':\n 'the importer produced no assets, or omitted a GUID that meta.subAssets[] declared; the produced GUID set must be a superset of the declared set (GUID import-stable iron law); err.detail.missingGuids lists the declared GUIDs not produced',\n 'guid-mismatch':\n 'the importer produced a GUID that meta.subAssets[] never declared (violates the GUID import-stable iron law: GUIDs come from the external meta, never minted by the importer); err.detail.unexpectedGuids lists the offending GUIDs',\n 'mesh-material-slot-topology-change':\n 'the importer could not match previous and current Mesh material slots without ambiguity; name source materials uniquely or repair their stable sourceKey values before reimport',\n 'import-internal-error':\n 'the importer failed at runtime; branch on err.detail: a conversion THROW carries err.detail.reason (the loaded importer threw while converting the source — an importer bug, not a meta / source problem), while a build-time module-LOAD failure carries err.detail.loadError (the host importer module / native addon could not be imported)',\n 'source-validation-failed':\n 'the source violates an import authoring rule; inspect err.detail.diagnostics fields (code, sourcePath, sourceRange, rule, expected, actual, hint, and relatedLocations) and fix the referenced source',\n 'unknown-source-key':\n 'sourceOverrides contains a key absent from meta.subAssets[]; refresh the producer topology and use one of err.detail.declaredSourceKeys',\n 'duplicate-source-key':\n 'the producer declared the same sourceKey more than once; repair the Meta topology before importing',\n 'invalid-source-overrides':\n 'sourceOverrides must be an object keyed by producer-owned sourceKey values',\n 'invalid-source-override-payload':\n 'each sourceOverrides value must be a producer-owned object validated by the importer',\n};\n\n/**\n * One asset produced by an {@link Importer}: the meta-declared `guid`, the\n * in-memory `Asset` POD, and its outbound GUID cross-references (`refs`). The\n * import runner folds these into the DDC `.pack.json` `assets[]` rows (one\n * `ImportedAsset` -> one `{ guid, kind, payload, refs }` row).\n *\n * The `guid` always comes from `meta.subAssets[].guid` (GUID import-stable\n * iron law) — the importer never mints it; it reads the declared GUID off the\n * meta and stamps it here. `kind` mirrors the `Asset.kind` discriminant so the\n * DDC row and the runtime loader dispatch on the same string.\n */\nexport interface ImportedAsset<P = Asset> {\n readonly guid: string;\n readonly kind: string;\n readonly name?: string;\n readonly payload: P;\n readonly refs: readonly AssetRef[];\n readonly artifacts: Readonly<Record<string, ImportedArtifactBody>>;\n}\n\n/**\n * One declared sub-asset entry the import runner hands to an\n * {@link Importer.import} call — the meta-declared `guid` + its `sourceIndex`\n * + `kind`. Mirrors the `meta.subAssets[]` rows so the importer can map a\n * source object index to the GUID it must stamp (GUID import-stable iron law).\n */\nexport interface ImportSubAsset {\n readonly guid: string;\n readonly sourceIndex: number;\n /** Producer-owned semantic identity used to look up sourceOverrides. */\n readonly sourceKey?: string;\n readonly kind: string;\n}\n\n/**\n * Capabilities + declarations the import runner wires into an\n * {@link Importer.import} call. The importer reads the source bytes via\n * `readSource`, the GUID declarations via `subAssets`, and the free-form\n * importer settings via `importSettings`. It stays pure of disk write +\n * registry bookkeeping (pipeline isolation, architecture-principles #4).\n *\n * - `source` — the `meta.source` path (relative to the sidecar), for\n * diagnostics + the importer's own external-resource resolution base.\n * - `readSource()` — fetch the raw source bytes (the runner has already\n * resolved the path); a structured failure here surfaces as\n * `source-read-failed`.\n * - `subAssets` — the `meta.subAssets[]` GUID declarations the importer must\n * honour (GUID import-stable iron law).\n * - `importSettings` — free-form importer settings copied verbatim from the\n * sidecar.\n * - `readSibling(uri)` — fetch raw bytes of a file co-located with the\n * primary source (e.g. an `.gltf` referencing an external `.bin` /\n * `.png` via relative URI). Failures surface as `source-read-failed`\n * (C-6 — no specialised error code; the URI is forensic detail). Used\n * by gltfImporter to resolve `images[].uri` external references at\n * import time.\n * - `decodeImage(bytes, mimeType, importSettings)` — decode raw image\n * bytes (PNG / JPEG) into a `TextureAsset` POD plus a `bytes` copy\n * suitable for `<guid>.bin` emission. When the host applies an offline\n * delivery codec, it also returns the artifact media type and codec so\n * gltfImporter can preserve those facts in the Pack v2 envelope. The seam\n * keeps gltfImporter\n * out of `@forgeax/engine-image` (D-1: zero static `from\n * '@forgeax/engine-image'` edge in `packages/gltf/src`). The\n * concrete implementation (parseImage + format derivation) lives\n * behind `@forgeax/engine-image/image-importer`; the build-time\n * orchestrator (vite-plugin-pack / cli-gltf / tests) binds the\n * callback when constructing the runner's `ImportRunnerFs`.\n */\nexport interface ImportContext {\n readonly source: string;\n readSource(): Promise<\n | { readonly ok: true; readonly value: Uint8Array }\n | { readonly ok: false; readonly error: unknown }\n >;\n readSibling(\n uri: string,\n ): Promise<\n | { readonly ok: true; readonly value: Uint8Array }\n | { readonly ok: false; readonly error: ImportError }\n >;\n decodeImage(\n bytes: Uint8Array,\n mimeType: 'image/png' | 'image/jpeg' | 'image/x-tga',\n importSettings: Readonly<Record<string, unknown>>,\n ): Promise<\n | {\n readonly ok: true;\n readonly value: {\n readonly texture: TextureAsset;\n readonly bytes: Uint8Array;\n readonly mediaType?: string;\n readonly assetCodec?: AssetCodec;\n };\n }\n | { readonly ok: false; readonly error: ImageError }\n >;\n readonly subAssets: readonly ImportSubAsset[];\n readonly importSettings: Readonly<Record<string, unknown>>;\n /** Optional Meta author facts passed through without importer-kind branching. */\n readonly sourceOverrides?: SourceOverrideMap;\n}\n\n/**\n * Build-time importer injected into the `ImporterRegistry`. One importer per\n * `meta.importer` key; the import runner dispatches on the key.\n *\n * `import` is pure of disk write + GUID minting: it reads the source via\n * `ctx.readSource()`, honours the `ctx.subAssets[]` GUID declarations, and\n * returns the produced `ImportedAsset[]`. The runner validates the produced\n * GUID set against the declared set (GUID import-stable iron law) and writes\n * the DDC. A thrown error is wrapped by the runner into\n * `import-internal-error` (charter P3) — importers may throw, but should\n * prefer returning a partial / empty result so the runner can attribute the\n * failure precisely.\n */\nexport interface ImportProductFinalizeArtifact {\n readonly path: string;\n readonly mimeType: string;\n readonly bytes: Uint8Array;\n}\n\nexport interface ImportProductFinalizeOptions {\n readonly artifactUrl: (artifact: ImportProductFinalizeArtifact) => string;\n}\n\nexport type ImportProductFinalizeResult =\n | {\n readonly ok: true;\n readonly value: {\n readonly asset: unknown;\n readonly artifacts: readonly { readonly path: string; readonly mimeType: string }[];\n };\n }\n | {\n readonly ok: false;\n readonly error: {\n readonly code: string;\n readonly expected: string;\n readonly hint: string;\n readonly detail: unknown;\n };\n };\n\n/** Optional producer capability exposed to the generic import runner. */\nexport interface ImporterCapabilities {\n readonly decodeImage?: ImportContext['decodeImage'];\n /** Producer-owned Catalog visibility for declarations before materialization. */\n readonly catalog?: {\n readonly publish?: (input: {\n readonly importSettings: Readonly<Record<string, unknown>>;\n readonly subAssets: readonly ImportSubAsset[];\n }) => boolean;\n };\n}\n\nexport interface Importer {\n readonly key: string;\n // biome-ignore lint/suspicious/noExplicitAny: pending downstream importer migration keeps old consumers source-compatible\n import(ctx: ImportContext): Promise<any> | any;\n readonly capabilities?: ImporterCapabilities;\n /** Optional owner projection for transport artifacts produced by this importer. */\n finalize?: (\n product: ImportProduct<unknown>,\n options: ImportProductFinalizeOptions,\n ) => ImportProductFinalizeResult;\n}\n/**\n * Interface slot for the M4 lazy-import transport (OOS-2). A runtime\n * `ImportTransport` fetches a missing DDC artefact on demand (the shipped form\n * never falls back to a runtime import; see `AssetErrorCode 'asset-not-imported'`).\n * Declared here as a contract seam only — the HTTP adapter lands in M4 w31;\n * M2 does not implement it (plan-strategy D-6 / research Finding 8).\n */\nexport interface ImportTransport {\n /**\n * Trigger an on-demand DDC import for a GUID at runtime. On success the\n * transport returns the freshly imported catalog rows for the GUID (and any\n * sub-asset siblings produced by the same import) so the caller patches just\n * those rows into its catalog cache -- per-asset incremental, never a\n * whole-catalog re-fetch (the four-verb redesign, 2026-06-06). `entries` may\n * be empty when the transport imported the artefact but does not surface the\n * rows; the caller then re-resolves the GUID from its (possibly stale) cache.\n * `ok: false` means the import did not produce an artefact and the caller\n * surfaces `asset-not-imported`.\n */\n fetchPack(\n guid: string,\n scope?: Pick<\n import('./runtime-scope.js').RuntimeAssetBinding,\n 'scopeId' | 'generation' | 'status'\n >,\n ): Promise<\n { readonly ok: true; readonly entries?: readonly PackIndexEntry[] } | { readonly ok: false }\n >;\n}\n\n/** A normalized filesystem path observed by an importer read attempt. */\nexport type SourceDependency = string;\n\n/** Logical artifact content emitted by one imported asset. */\nexport interface ImportedArtifactBody {\n readonly mediaType: string;\n readonly assetCodec?: AssetCodec;\n readonly bytes: Uint8Array;\n}\n\n/** Generic import output shared by every importer and transport. */\nexport interface ImportProduct<P = Asset> {\n readonly assets: readonly ImportedAsset<P>[];\n readonly sourceDependencies: readonly SourceDependency[];\n}\n\n/** Structured result returned by an importer. */\nexport type ImportResult<P = Asset> =\n | { readonly ok: true; readonly value: ImportProduct<P> }\n | { readonly ok: false; readonly error: ImportError };\n\n/* ImportTransport is intentionally kept with the build-time contract. */\n","// @forgeax/engine-types — POD types, union aliases, and cross-package primitives SSOT.\n//\n// Proposition: this package is the single source of truth for shared shapes that\n// must NOT diverge across @forgeax/engine-rhi / ecs / naga / image / gltf / console /\n// render-graph / shader / future renderer packages.\n//\n// Scope:\n// - POD types & union aliases — Asset / MaterialAsset / RenderQueue / PassKind /\n// FontAsset / RenderPipelineAsset / SceneAsset /\n// PackErrorCode / ImageErrorCode / AudioErrorCode / PhysicsErrorCode etc.\n// - Structured error classes — AssetError / FontError / TextError / AudioError /\n// PhysicsError (carry .code / .expected / .hint surface).\n// - Project-wide Result<T, E> + ok / err factories (tweak-20260612-result-into-types\n// consolidated 5 byte-aligned-by-prose copies into this single module).\n// - GPUFlagsConstant aliases for @webgpu/types numeric runtime constants — the\n// global objects (e.g. GPUBufferUsage.MAP_READ) are still consumed directly by\n// upstream callers; only the literal type aliases live here (decision S-6 /\n// research F-2 option (b)).\n//\n// Shape rules:\n// - math-free (no vec / mat / quat dependency).\n// - Single-source policy — fields already exported by @webgpu/types are re-exported\n// verbatim as one-line aliases; never duplicated here.\n//\n// Anchors: requirements §AC AC-01 + MVP-1.5; plan-strategy §2 S-6 + §7.6 propositions\n// 4 / 5; research §F-2 (`GPUFlagsConstant = number` alias).\n\n/// <reference types=\"@webgpu/types\" />\n\n// === Handle SSOT barrel re-export (feat-20260517-handle-type-unify M2 / D-2) ====\n//\n// `./handle` carries the unique double-axis Handle<T extends string, M> brand\n// + AssetTagMap + TagOf + 3 factories (toUnique / toShared / unwrapHandle).\n// The legacy 1-arg form that lived here at M1 has been physically deleted\n// (M2 t10) so the package surface exposes a single Handle shape (charter F1\n// single-entry indexability; AC-03 grep gate).\n\nimport type { AnimationTargetIdValue } from './animation-target';\nimport type { AudioClipAsset } from './asset.js';\nimport type { MaterialAsset } from './material/asset.js';\nimport type { ParticleEffectAsset } from './vfx';\n\nexport type { AnimationTargetIdValue } from './animation-target';\nexport * from './asset.js';\nexport * from './asset-errors.js';\nexport type {\n AssetArtifactReader,\n AssetDecoder,\n AssetDecoderContribution,\n AssetDecoderContributionRef,\n AssetDecoderInput,\n AssetDecoderLease,\n AssetDecoderResult,\n AssetKind,\n AssetKindPayload,\n AssetRegistryAction,\n AssetRuntimeApiGroups,\n BuiltinAssetKind,\n BuiltinAssetKindToken,\n BuiltinAssetPayload,\n} from './asset-runtime.js';\nexport * from './handle';\nexport * from './material/index.js';\n\n// === Result<T, E> SSOT (tweak-20260612-result-into-types) ======================\n//\n// Single physical source of `Result<T, E>` + `ok(...)` / `err(...)`. Replaces\n// the prior dual copies in packages/rhi/src/errors.ts + packages/ecs/src/result.ts\n// (those copies were \"byte-for-byte aligned\" by prose, not by mechanism).\n// AI users import the binary success/failure carrier via `@forgeax/engine-types`\n// (or via the rhi/ecs package barrels which re-export this same module).\nexport * from './result';\nexport * from './runtime-scope';\nexport * from './vfx';\n\n// === Sub-asset POD SSOT (feat-20260615-fbx-importer-via-sdk M1 / t9) ===========\n//\n// Importer-independent pure-data IR types shared across glTF / FBX / future\n// format importers. These are pre-kind, pre-guid data carriers — each importer\n// writes these Pods from its format-specific JSON POD, and `to-asset-pack`\n// promotes them to registry-ready Asset handles.\n//\n// Design axioms:\n// - SSOT (architecture-principles #1): defined once in @forgeax/engine-types,\n// consumed via import by gltf / fbx / future importer packages.\n// - Derive, don't duplicate (#2): gltf/fbx drop their local MeshIr/MeshRecord\n// and import MeshPod — no per-package copy.\n// - No format prefix (plan-strategy section 8): Pod types are named after the\n// *asset kind* they represent, not the source format. AI users write code\n// that reads MeshPod regardless of whether the source was FBX or glTF.\n// - Pre-kind data (charter P4): Pods carry raw geometric/material data without\n// `kind` discriminant fields. The importer bridge layer adds `kind` when\n// converting Pod -> Asset handle.\n//\n// Pod roster (AC-01..AC-07):\n// MeshPod — vertices/indices/attributes/submeshes\n// MaterialPod — PBR parameter values (baseColor/metallic/roughness)\n// ScenePod — entity hierarchy + mount points\n// TexturePod — external file path (cross-platform normalized)\n// SkeletonPod — joint count + inverse bind matrices\n// SkinPod — skeleton reference + joint paths\n// AnimationClipPod — duration + channels + samplers\n\n// === AC-01: MeshPod — pure geometric data ===\n\n/** Per-submesh descriptor within a MeshPod. */\nexport interface MeshSubmeshPod {\n /** Vertex count for this submesh (draw count when non-indexed). */\n readonly vertexCount: number;\n /** Index count when indexed; 0 for non-indexed geometry. */\n readonly indexCount: number;\n /** Byte offset into the shared indices buffer (0-based). */\n readonly indexOffset: number;\n /** Material binding index into the parent document's materials array. */\n readonly materialIndex: number | null;\n /** Primitive topology. */\n readonly topology: 'triangle-list' | 'line-list' | 'line-strip' | 'point-list';\n}\n\n/** MeshPod: pre-kind geometric data IR shared across importers. */\nexport interface MeshPod {\n /** Optional debug name from source document. */\n readonly name?: string;\n /** Packed vertex positions (Float32Array, 3 floats per vertex). */\n readonly vertices: Float32Array;\n /** Packed triangle indices (Uint16Array or Uint32Array). Absent when non-indexed. */\n readonly indices?: Uint16Array | Uint32Array;\n /** Per-vertex attributes keyed by semantic (POSITION/NORMAL/TEXCOORD_0/JOINTS_0/WEIGHTS_0). */\n readonly attributes: Record<string, Float32Array | Uint16Array | Uint32Array>;\n /** Optional additive morph deltas, one entry per target. */\n readonly morphTargets?: readonly MorphTarget[];\n /** Optional default morph weights, one value per target. */\n readonly morphWeights?: Float32Array;\n /** Per-submesh descriptors (>=1). */\n readonly submeshes: readonly MeshSubmeshPod[];\n /** Source mesh index within the original document (for diagnostic mapping). */\n readonly sourceIndex: number;\n}\n\n// === AC-02: MaterialPod — PBR parameter values ===\n\n/** MaterialPod: pre-kind PBR material data IR shared across importers. */\nexport const MATERIAL_TEXTURE_SLOTS = [\n 'baseColorTexture',\n 'normalTexture',\n 'specularTintTexture',\n 'metallicRoughnessTexture',\n 'emissiveTexture',\n 'occlusionTexture',\n] as const;\n\nexport type MaterialTextureSlot = (typeof MATERIAL_TEXTURE_SLOTS)[number];\n\nexport interface MaterialTextureBindingPod {\n /** Standard material value slot receiving the texture reference. */\n readonly slot: MaterialTextureSlot;\n /** Index into the parent document's textures array. */\n readonly textureIndex: number;\n /** Optional source UV set. */\n readonly texCoord?: number;\n}\n\nexport interface MaterialPod {\n /** Optional debug name from source document. */\n readonly name?: string;\n /** RGBA base color factor (linear space). */\n readonly baseColorFactor: readonly [number, number, number, number];\n /** Metallic factor (0..1). */\n readonly metallicFactor: number;\n /** Roughness factor (0..1). */\n readonly roughnessFactor: number;\n /** Index into the parent document's textures array for base color map. */\n readonly baseColorTextureIndex?: number;\n /** Index for metallic-roughness packed texture. */\n readonly metallicRoughnessTextureIndex?: number;\n /** Index for normal map. */\n readonly normalTextureIndex?: number;\n /** Index for occlusion map. */\n readonly occlusionTextureIndex?: number;\n /** Index for emissive map. */\n readonly emissiveTextureIndex?: number;\n /** Index for a specular tint map. */\n readonly specularTintTextureIndex?: number;\n /** Engine-owned semantic texture bindings from the producer. */\n readonly textureBindings?: readonly MaterialTextureBindingPod[];\n}\n\n// === AC-03: ScenePod — entity hierarchy ===\n\n/** A single entity node within a ScenePod hierarchy. */\nexport interface SceneEntityPod {\n /** Entity name (for Name component attachment). */\n readonly name: string;\n /** Decomposed local transform (TRS). */\n readonly transform: {\n readonly translation: readonly [number, number, number];\n readonly rotation: readonly [number, number, number, number];\n readonly scale: readonly [number, number, number];\n };\n /** Index into the parent document's meshes array. Null when not a mesh node. */\n readonly meshIndex: number | null;\n /** Children entity indices in the flattened entities array. */\n readonly children: readonly number[];\n}\n\n/** ScenePod: entity hierarchy IR shared across importers. */\nexport interface ScenePod {\n /** Optional scene name. */\n readonly name?: string;\n /** Flattened entity list (topological order, parents before children). */\n readonly entities: readonly SceneEntityPod[];\n /** Index of the default/root scene entity. */\n readonly rootEntityIndex: number;\n}\n\n// === AC-04: TexturePod — external file path ===\n\n/** TexturePod: external texture reference IR shared across importers. */\nexport interface TexturePod {\n /** Optional texture name. */\n readonly name?: string;\n /** Filesystem path relative to the source document, with '/' separators. */\n readonly filePath: string;\n /** The producer-declared relative path before host resolution. */\n readonly relativeFilePath?: string;\n /** Parse-scope absolute hint; never persist this into project metadata. */\n readonly absoluteFilePath?: string;\n /** Embedded encoded image bytes, when the FBX contains the texture payload. */\n readonly embeddedBytes?: Uint8Array;\n /** Producer texture kind; only file/embedded textures are importable. */\n readonly type?: 'file' | 'layered' | 'procedural' | 'shader' | 'unknown';\n /** Source texture index within the original document. */\n readonly sourceIndex: number;\n}\n\n// === AC-05: SkeletonPod — joint hierarchy ===\n\n/** SkeletonPod: skeleton joint data IR shared across importers. */\nexport interface SkeletonPod {\n /** Number of joints. */\n readonly jointCount: number;\n /** Inverse bind matrices, Float32Array of length jointCount * 16. */\n readonly inverseBindMatrices: Float32Array;\n /** Per-joint name path from scene root (parallel to joints array). */\n readonly jointPaths: readonly string[];\n}\n\n// === AC-06: SkinPod — vertex skinning data ===\n\n/** Per-vertex joint influence descriptor. */\nexport interface SkinVertexInfluencePod {\n /** 4 joint indices (Uint16Array), always padded to 4 entries. */\n readonly jointIndices: Uint16Array;\n /** 4 joint weights (Float32Array), always padded to 4 entries. */\n readonly jointWeights: Float32Array;\n}\n\n/** SkinPod: vertex skinning data IR shared across importers. */\nexport interface SkinPod {\n /** GUID-like identifier for the associated SkeletonAsset (resolved at bridge time). */\n readonly skeletonGuid: string;\n /** Joint name paths (same as SkeletonPod.jointPaths for cross-reference). */\n readonly jointPaths: readonly string[];\n /** Number of influenced vertices. */\n readonly vertexCount: number;\n /** Per-vertex joint influences (4 joints per vertex). */\n readonly influences: readonly SkinVertexInfluencePod[];\n}\n\n// === AC-07: AnimationClipPod — keyframe animation ===\n\n/** Animation sampler (keyframe data for one property). */\nexport interface AnimationSamplerPod {\n /** Keyframe timestamps (ascending, seconds). */\n readonly input: Float32Array;\n /** Keyframe values (packed per-element stride). */\n readonly output: Float32Array;\n /** Interpolation mode. */\n readonly interpolation: 'LINEAR' | 'STEP';\n}\n\n/** Animation channel (one target-property pair). */\nexport interface AnimationChannelPod {\n /** Stable animation target identity. */\n readonly targetId: AnimationTargetIdValue;\n /** Target property: 'translation' | 'rotation' | 'scale' | 'weights'. */\n readonly property: 'translation' | 'rotation' | 'scale' | 'weights';\n /** Sampler driving this channel. */\n readonly sampler: AnimationSamplerPod;\n}\n\n/** AnimationClipPod: keyframe animation clip IR shared across importers. */\nexport interface AnimationClipPod {\n /** Optional clip name. */\n readonly name?: string;\n /** Clip duration in seconds (max sampler.input[last]). */\n readonly duration: number;\n /** Per-animation-target-property channels. */\n readonly channels: readonly AnimationChannelPod[];\n}\n\n// === Asset system v1 SSOT (feat-20260511-asset-system-v1) ======================\n//\n// Decision anchors:\n// - requirements §G7 + §2 row 8 + AC-09 / AC-15 / AC-21 (4-variant Asset\n// discriminated union, 14-key VertexAttributeMap closed set,\n// AssetErrorCode 4-member closed union elevated to TS alias)\n// - plan-strategy §2 D-P1 (@forgeax/engine-types single-file SSOT for\n// Asset union + AssetErrorCode; 4-member AssetErrorCode independent from\n// RhiErrorCode, AI users discover through one-line import)\n// - plan-strategy §7.2 (lowercase key alignment with Three.js r184\n// BufferGeometry mental migration; D-P5 preserves segments 6 params)\n// - plan-strategy §7.3 (AssetError .hint strings per error code, verbatim)\n// - charter proposition 1 (single-entry IDE autocomplete via\n// `@forgeax/engine-types`) + proposition 3 (machine-readable union >\n// prose) + proposition 4 (explicit failure - exhaustive switch needs no\n// default fallback) + proposition 5 (consistent abstraction - structurally\n// parallel to RhiError / InspectorError / MetricError 4-field surface)\n// - architecture-principles #1 SSOT (4 literals + shape live here once;\n// @forgeax/engine-runtime AssetRegistry / tests / AGENTS.md Error model\n// table all reference this module)\n\n/**\n * Mesh asset POD shape aligned with Three.js r184 BufferGeometry mental\n * model (plan-strategy §7.2 naming convention). `vertices` is the interleaved\n * or primary position buffer; `indices` narrows to `Uint16Array | Uint32Array`\n * per WebGPU spec index format; `attributes` is the VertexAttributeMap\n * lowercase-key closed set.\n *\n * Canonical lowercase keys include position, normal, uv, tangent, skinIndex,\n * skinWeight, uv1..uv7 and optional linear RGBA color.\n *\n * Designed for M3 GLTF loader single-layer mapping\n * (`POSITION -> position` / `TEXCOORD_0 -> uv` etc.) without runtime rename.\n */\nexport interface MeshAsset {\n readonly kind: 'mesh';\n readonly vertices: Float32Array;\n /**\n * Index buffer. Optional: vertex-only meshes (point-list / line-list with no\n * shared vertices) omit it, and the engine takes a non-indexed draw path\n * (`pass.draw(vertexCount)` instead of `pass.drawIndexed`). When present the\n * indexed path is byte-for-byte unchanged.\n */\n readonly indices?: Uint16Array | Uint32Array;\n readonly attributes: VertexAttributeMap;\n /**\n * Axis-aligned bounding box in local space: 6 floats [minX, minY, minZ, maxX, maxY, maxZ].\n *\n * Producer obligation: every MeshAsset MUST carry a computed `aabb` from its\n * position attribute. Built-in producers (glTF, FBX, geometry factories)\n * fill it automatically via `box3.fromPositions` -- the single\n * authoritative implementation in @forgeax/engine-math.\n *\n * When `aabb` is `undefined`, the pick() broad-phase and frustum culling\n * silently skip the mesh -- pick() returns `undefined` for every ray query\n * against it with no diagnostic signal. AI users hand-writing MeshAsset\n * must self-check that `aabb` is populated.\n *\n * Empty / degenerate position input (0 vertices) produces an\n * inverted-infinity empty box (min = +Infinity, max = -Infinity).\n * Consumers reject this for picking (min.x > max.x) and may skip it\n * for culling.\n *\n * The bare Float32Array keeps engine-types math-free (no Box3 branded\n * type dependency). Consumers narrow to the math-layer Box3 via\n * `as Box3Like`.\n */\n readonly aabb?: Float32Array;\n /**\n * Submeshes partition the index/vertex range into independent draw calls,\n * each with its own topology (one of the 5 WebGPU primitives:\n * 'point-list' | 'line-list' | 'line-strip' | 'triangle-list' | 'triangle-strip').\n *\n * Every mesh must declare at least one submesh. The engine draws one\n * `drawIndexed` (or `draw` for vertex-only) per submesh entry, and pairs\n * them with `MeshRenderer.materials[]` by index position.\n *\n * Must be non-empty: an empty array triggers a `mesh-asset-submeshes-empty`\n * AssetError at register-time (fail-fast, charter P3 explicit failure).\n */\n readonly submeshes: readonly Submesh[];\n /** Stable, mesh-owned material entry points shared by every instance. */\n readonly materialSlots: readonly MeshMaterialSlot[];\n /** Target-major dense morph deltas. */\n readonly morphTargets?: readonly MorphTarget[];\n /** Authored default weights, one value per morph target when present. */\n readonly morphWeights?: Float32Array;\n}\n\nexport interface MorphTarget {\n readonly position?: Float32Array;\n readonly normal?: Float32Array;\n readonly tangent?: Float32Array;\n}\n\n/** Mesh-owned default binding for one stable material slot. */\nexport interface MeshMaterialSlot {\n /** Unique, non-empty display/tooling name within the mesh. */\n readonly slotName: string;\n /** Producer-owned stable identity used to preserve slot indices on reimport. */\n readonly sourceKey?: string;\n /** Missing means the slot intentionally inherits the neutral engine material. */\n readonly defaultMaterial?: AssetGuid;\n}\n\n/** JSON-safe producer topology persisted beside an imported Mesh output. */\nexport interface MeshMaterialSlotTopologyEntry {\n readonly slotName: string;\n readonly sourceKey?: string;\n readonly defaultMaterialGuid?: string;\n /** Persisted removed-slot identity; never participates in cooked bindings. */\n readonly tombstone?: true;\n}\n\n/** Resolve the persisted source/authoring layers to the one runtime Mesh default. */\nexport function resolveMeshMaterialSlotDefaultGuid(\n slot: MeshMaterialSlotTopologyEntry,\n authoredDefaultMaterialGuid?: string | null,\n): string | undefined {\n if (authoredDefaultMaterialGuid !== undefined) {\n return authoredDefaultMaterialGuid === null ? undefined : authoredDefaultMaterialGuid;\n }\n return slot.defaultMaterialGuid;\n}\n\nexport interface MeshMaterialSlotTopologyChange {\n readonly code: 'mesh-material-slot-topology-change';\n readonly previousIndices: readonly number[];\n readonly nextIndices: readonly number[];\n readonly hint: string;\n}\n\nexport type MeshMaterialSlotReconcileResult =\n | {\n readonly ok: true;\n readonly slots: readonly MeshMaterialSlotTopologyEntry[];\n /** New source-order slot index -> stable persisted slot index. */\n readonly currentToStableSlot: readonly number[];\n }\n | { readonly ok: false; readonly error: MeshMaterialSlotTopologyChange };\n\n/**\n * Preserve positional renderer overrides across source reimport.\n *\n * Stable sourceKey wins, then a unique slotName. A single remaining pair is\n * the only unambiguous source-order fallback. Removed slots remain as\n * defaultless tombstones; new slots append, so an old index never silently\n * starts naming a different source material.\n */\nexport function reconcileMeshMaterialSlotTopology(\n current: readonly MeshMaterialSlotTopologyEntry[],\n previous: readonly MeshMaterialSlotTopologyEntry[] = [],\n): MeshMaterialSlotReconcileResult {\n if (previous.length === 0) {\n return { ok: true, slots: [...current], currentToStableSlot: current.map((_, index) => index) };\n }\n\n const stable: MeshMaterialSlotTopologyEntry[] = previous.map((slot) => ({\n slotName: slot.slotName,\n ...(slot.sourceKey === undefined ? {} : { sourceKey: slot.sourceKey }),\n tombstone: true,\n }));\n const mapping = new Array<number>(current.length).fill(-1);\n const usedPrevious = new Set<number>();\n\n const uniqueIndex = (\n slots: readonly MeshMaterialSlotTopologyEntry[],\n read: (slot: MeshMaterialSlotTopologyEntry) => string | undefined,\n ): Map<string, number> => {\n const first = new Map<string, number>();\n const duplicates = new Set<string>();\n slots.forEach((slot, index) => {\n const key = read(slot)?.trim();\n if (!key) return;\n if (first.has(key)) duplicates.add(key);\n else first.set(key, index);\n });\n for (const duplicate of duplicates) first.delete(duplicate);\n return first;\n };\n\n const match = (read: (slot: MeshMaterialSlotTopologyEntry) => string | undefined): void => {\n const oldByKey = uniqueIndex(previous, read);\n const nextByKey = uniqueIndex(current, read);\n for (const [key, nextIndex] of nextByKey) {\n if (mapping[nextIndex] !== -1) continue;\n const oldIndex = oldByKey.get(key);\n if (oldIndex === undefined || usedPrevious.has(oldIndex)) continue;\n mapping[nextIndex] = oldIndex;\n usedPrevious.add(oldIndex);\n }\n };\n\n match((slot) => slot.sourceKey);\n match((slot) => slot.slotName);\n\n const unmatchedCurrent = mapping\n .map((oldIndex, index) => (oldIndex === -1 ? index : -1))\n .filter((index) => index !== -1);\n const unmatchedPrevious = previous\n .map((_, index) => (usedPrevious.has(index) ? -1 : index))\n .filter((index) => index !== -1);\n if (\n unmatchedCurrent.length === 1 &&\n unmatchedPrevious.length === 1 &&\n current[unmatchedCurrent[0] as number]?.sourceKey === undefined &&\n previous[unmatchedPrevious[0] as number]?.sourceKey === undefined\n ) {\n mapping[unmatchedCurrent[0] as number] = unmatchedPrevious[0] as number;\n usedPrevious.add(unmatchedPrevious[0] as number);\n unmatchedCurrent.length = 0;\n unmatchedPrevious.length = 0;\n }\n const identityInsufficient =\n unmatchedCurrent.some((index) => current[index]?.sourceKey === undefined) &&\n unmatchedPrevious.some((index) => previous[index]?.sourceKey === undefined);\n if (unmatchedCurrent.length > 0 && unmatchedPrevious.length > 0 && identityInsufficient) {\n return {\n ok: false,\n error: {\n code: 'mesh-material-slot-topology-change',\n previousIndices: unmatchedPrevious,\n nextIndices: unmatchedCurrent,\n hint: 'name source materials uniquely or provide stable sourceKey values before reimport',\n },\n };\n }\n\n for (let currentIndex = 0; currentIndex < current.length; currentIndex++) {\n let stableIndex = mapping[currentIndex] as number;\n if (stableIndex === -1) {\n stableIndex = stable.length;\n mapping[currentIndex] = stableIndex;\n }\n stable[stableIndex] = current[currentIndex] as MeshMaterialSlotTopologyEntry;\n }\n return { ok: true, slots: stable, currentToStableSlot: mapping };\n}\n\nexport interface MeshMaterialOverrideMigrationContext {\n readonly meshGuid: string;\n readonly sceneGuid: string;\n readonly entityId: number;\n}\n\nexport interface MeshMaterialOverrideConflict extends MeshMaterialOverrideMigrationContext {\n readonly code: 'mesh-material-slot-override-conflict';\n readonly materialSlot: number;\n readonly submeshIndices: readonly number[];\n readonly overrideHandles?: readonly number[];\n readonly overrideGuids?: readonly string[];\n readonly hint: string;\n}\n\nexport type MeshMaterialOverrideMigrationResult<T extends number | string = number> =\n | { readonly ok: true; readonly overrides: readonly T[] }\n | { readonly ok: false; readonly error: MeshMaterialOverrideConflict };\n\n/**\n * Collapse legacy per-submesh overrides into v3 per-slot overrides.\n * Conflicting section overrides fail atomically with full dependency context.\n */\nexport function migrateLegacyMeshMaterialOverrides<T extends number | string>(\n legacyOverrides: readonly T[],\n submeshes: readonly Pick<Submesh, 'materialSlot'>[],\n materialSlotCount: number,\n context: MeshMaterialOverrideMigrationContext,\n): MeshMaterialOverrideMigrationResult<T> {\n const inherited = (typeof legacyOverrides[0] === 'string' ? '' : 0) as T;\n const overrides = new Array<T>(materialSlotCount).fill(inherited);\n const sectionsBySlot = new Map<number, number[]>();\n for (let submeshIndex = 0; submeshIndex < submeshes.length; submeshIndex++) {\n const materialSlot = submeshes[submeshIndex]?.materialSlot;\n if (materialSlot === undefined || materialSlot < 0 || materialSlot >= materialSlotCount)\n continue;\n const sections = sectionsBySlot.get(materialSlot) ?? [];\n sections.push(submeshIndex);\n sectionsBySlot.set(materialSlot, sections);\n }\n for (const [materialSlot, submeshIndices] of sectionsBySlot) {\n const handles: T[] = submeshIndices.map((index) => legacyOverrides[index] ?? inherited);\n const distinct = [...new Set(handles)];\n if (distinct.length > 1) {\n return {\n ok: false,\n error: {\n code: 'mesh-material-slot-override-conflict',\n ...context,\n materialSlot,\n submeshIndices,\n ...(typeof handles[0] === 'string'\n ? { overrideGuids: handles as string[] }\n : { overrideHandles: handles as number[] }),\n hint: 'split the source slot or choose one override explicitly before v2 to v3 recook',\n },\n };\n }\n overrides[materialSlot] = distinct[0] ?? inherited;\n }\n return { ok: true, overrides };\n}\n\n/**\n * Submesh partitions a mesh's index/vertex range into an independent draw\n * call with its own primitive topology.\n *\n * Every field is required -- there is no default topology; the caller\n * must state the intended primitive type explicitly (charter P3 explicit\n * failure: silent default would mask topology mistakes).\n *\n * Naming aligns with Unity `SubMeshDescriptor` (without firstVertex /\n * baseVertex / bounds which are out of scope per OOS-3/OOS-4).\n *\n * | field | description |\n * |:--|:--|\n * | `indexOffset` | Start offset into the parent mesh's index buffer (in elements, not bytes). For vertex-only (non-indexed) submeshes, set to 0. |\n * | `indexCount` | Number of indices consumed from the index buffer starting at `indexOffset`. For vertex-only submeshes, set to 0. |\n * | `vertexCount` | Number of vertices spanned by this submesh range (used for the non-indexed draw path and for index-range OOB validation). |\n * | `topology` | GPU primitive topology for this submesh (one of the 5 WebGPU primitives: 'point-list' \\| 'line-list' \\| 'line-strip' \\| 'triangle-list' \\| 'triangle-strip'). |\n */\nexport interface Submesh {\n readonly indexOffset: number;\n readonly indexCount: number;\n readonly vertexCount: number;\n readonly topology: PrimitiveTopology;\n /** Index into the owning MeshAsset.materialSlots table. */\n readonly materialSlot: number;\n}\n\n/**\n * Texture asset POD shape aligned with `@webgpu/types ^0.1.69`\n * `GPUTextureDescriptor` subset (plan-strategy D-P1; RHI form rule\n * \"spec-aligned\"). Carries the decoded pixel bytes ready for\n * `GPUQueue.writeTexture` / `copyExternalImageToTexture` upload.\n *\n * `format` is the `GPUTextureFormat` string-literal union; `data` holds the\n * CPU-side decoded pixels (tight-packed, srgb-premultiplied-alpha-false per\n * D-P9). Optional `mipLevelCount` / `sampleCount` default to 1 at upload\n * time; v1 registers only 2D textures (depth / 3D / cube array are future\n * spinoffs).\n *\n * feat-20260515-learn-render-getting-started M3 / T-M3-03 minor-add (Asset\n * closed-union member count unchanged at 5; plan-strategy section 2.5 D Open\n * Q-4 selection (c)):\n * - `colorSpace: 'srgb' | 'linear'` -- AI-user-semantic SSOT (charter P4\n * consistent abstraction); `format='*-srgb'` family <-> `colorSpace='srgb'`\n * enforced by `AssetRegistry.uploadTexture` consistency assertion.\n * - `mipmap: boolean` -- `true` enables runtime mipmap-generator blit chain\n * (research F-1 SSOT three-source convergence; plan-strategy section 2.6\n * D Open Q-5 (a) independent file). `false` ships the single mip level\n * authored in `data`.\n *\n * Both fields are required (no default value) so consumers always make the\n * decision explicit at register-time (charter P3 explicit failure: silent\n * default would mask sRGB encode mistakes).\n */\nexport interface TextureAsset {\n readonly kind: 'texture';\n readonly width: number;\n readonly height: number;\n readonly format: GPUTextureFormat;\n readonly data: Uint8Array | Uint8ClampedArray;\n readonly colorSpace: 'srgb' | 'linear';\n readonly mipmap: boolean;\n readonly mipLevelCount?: number;\n readonly sampleCount?: number;\n}\n\n/**\n * Equirectangular environment-map asset POD shape -- a single 2D HDR image in\n * latitude-longitude projection (feat-20260630-equirect-kind-internalized-ibl-\n * declarative-skyligh M1, replacing the prior `CubeTextureAsset`).\n *\n * `kind:'equirect'` is the build-time-imported `.hdr` artefact: a single\n * `rgba16float` 2D image whose pixels live in `data` (tight-packed, build .bin).\n * Unlike the retired cube-texture, an equirect HAS a single 2D representation,\n * so it folds to a build-time `.bin` like `TextureAsset` (the cube-to-cube IBL\n * projection is a GPU-side pass driven internally by the render-system record\n * arm; AI users declare `Skylight{equirect}` rather than calling an upload).\n *\n * Fields mirror the `TextureAsset` 2D-image surface (charter P4 consistent\n * abstraction): `width` / `height` / `format` / `data` / `colorSpace`. No\n * `mipmap` chain field -- the IBL prefilter mip chain is a GPU-side pass, not a\n * CPU-authored level set.\n */\nexport interface EquirectAsset {\n readonly kind: 'equirect';\n readonly width: number;\n readonly height: number;\n readonly format: GPUTextureFormat;\n readonly data: Uint8Array | Uint8ClampedArray;\n readonly colorSpace: 'srgb' | 'linear';\n}\n\n/**\n * Sampler asset POD shape aligned with `@webgpu/types ^0.1.69`\n * `GPUSamplerDescriptor` subset. AI users supply filter + address modes;\n * the RHI layer materialises the GPU-side sampler on upload.\n */\nexport interface SamplerAsset {\n readonly kind: 'sampler';\n readonly magFilter?: GPUFilterMode;\n readonly minFilter?: GPUFilterMode;\n readonly mipmapFilter?: GPUMipmapFilterMode;\n readonly addressModeU?: GPUAddressMode;\n readonly addressModeV?: GPUAddressMode;\n readonly addressModeW?: GPUAddressMode;\n readonly lodMinClamp?: number;\n readonly lodMaxClamp?: number;\n readonly compare?: GPUCompareFunction;\n readonly maxAnisotropy?: number;\n}\n\n// feat-20260613 fix-issue-4: MATERIAL_PARAM_TYPES_V1 (9-Set) deleted —\n// MATERIAL_PARAM_TYPES (14-tuple, declared below) is the single SSOT.\n// §Change stance forbids v1/v2 dual-paths; the 9 v1 literals are a strict\n// subset of the 14-tuple, so all consumers (buildMaterialAssetValidator,\n// scanner.ts) migrate to the 14-tuple in one cut.\n\n// === MaterialParamType v2 (feat-20260613-material-paramschema-driven-binding M1 / w2) ===\n//\n// Decision anchors:\n// - plan-strategy D-7 paramSchema type set v2 (9 v1 + 5 new = 14 literals)\n// - research finding F-1 the union of all binding types used by the 5 built-in\n// shaders (standard-pbr / pbr-skin / unlit / sprite / shadow-caster) is exactly\n// these 14 entries; CSM (texture_depth_2d) / point shadow (texture_cube_array) /\n// IBL (texture_cube) / sampler_comparison / storage_buffer (skin palette) all\n// already exist downstream\n// - charter P3 explicit failure: closed unions guard exhaustive switching with\n// no default arm; TS verifies completeness\n//\n// Shape:\n// - `MATERIAL_PARAM_TYPES` : 14-element readonly tuple, the SSOT whitelist\n// - `MaterialParamType` : string-literal union derived from the tuple\n// - `NumericParamType` : 7 numeric literals (run-merged into one UBO entry)\n// - `TextureBindingParamType`: 6 literals — texture* + sampler*\n// (per D-4 each texture* auto-pairs a filtering sampler in derive output;\n// sampler / sampler_comparison are user-declared schema entries)\n// - `StorageBindingParamType`: storage_buffer (independent binding)\n// - `ParamSchemaEntry` : discriminated union over the three families\n// (Numeric / TextureBinding / StorageBinding) — exhaustive switching\n// on `entry.type` is closed across the 14 literals.\n\n/** 7 numeric WGSL types — std140-packed into one merged UBO entry (D-3). */\nexport type NumericParamType = 'f32' | 'i32' | 'u32' | 'vec2' | 'vec3' | 'vec4' | 'color';\n\n/** 6 texture-binding-family WGSL types: 4 texture views + 2 sampler kinds. */\nexport type TextureBindingParamType =\n | 'texture2d'\n | 'texture_cube'\n | 'texture_depth_2d'\n | 'texture_cube_array'\n | 'sampler'\n | 'sampler_comparison';\n\n/** 1 storage-binding type (e.g. skin palette buffer). */\nexport type StorageBindingParamType = 'storage_buffer';\n\n/**\n * Closed union of WGSL material-parameter type literals (14 members).\n * Every paramSchema entry's `type` field MUST be a member of this union.\n */\nexport type MaterialParamType =\n | NumericParamType\n | TextureBindingParamType\n | StorageBindingParamType;\n\n/**\n * v2 material parameter type whitelist — 14 ordered literal tuple (D-7).\n * Order is significant only as a stable enumeration source for tests\n * and discoverability; consumers should treat membership as a Set.\n */\nexport const MATERIAL_PARAM_TYPES = [\n 'f32',\n 'i32',\n 'u32',\n 'vec2',\n 'vec3',\n 'vec4',\n 'color',\n 'texture2d',\n 'texture_cube',\n 'texture_depth_2d',\n 'texture_cube_array',\n 'sampler',\n 'sampler_comparison',\n 'storage_buffer',\n] as const satisfies readonly MaterialParamType[];\n\n// Numeric-family schema entry (run-merged into a single UBO entry by derive).\n// `default` is optional; when present, values may omit the key.\n// - scalar numeric (f32 / i32 / u32) defaults to a single number\n// - vector + color types default to a length-N number tuple\nexport interface NumericParamSchemaEntry {\n readonly name: string;\n readonly type: NumericParamType;\n /** Asset-side color transfer function; runtime UBO values are always linear. */\n readonly colorSpace?: 'srgb' | 'linear';\n readonly default?: number | readonly number[];\n}\n\n// Texture-binding-family schema entry (texture* / sampler*).\n// `default` is kept optional for backward compatibility with existing\n// schema fixtures that carry stray defaults; derive ignores it for the\n// non-numeric families (D-4 auto-pair rule resolves samplers; textures\n// resolve via Handle<TextureAsset>).\nexport interface TextureBindingParamSchemaEntry {\n readonly name: string;\n readonly type: TextureBindingParamType;\n readonly default?: unknown;\n}\n\n// Storage-binding-family schema entry (storage_buffer).\n// Always an independent binding (not merged into the UBO run); `default`\n// is optional and ignored by derive (backward compatibility shim).\nexport interface StorageBindingParamSchemaEntry {\n readonly name: string;\n readonly type: StorageBindingParamType;\n readonly default?: unknown;\n}\n\n// Re-export derive(schema) and its output shapes alongside the schema\n// type union so all downstream consumers (runtime / vite-plugin-shader /\n// shader-compiler) reach the SSOT through a single import surface (D-2).\nexport type {\n DerivedMaterialInterface,\n DerivedNumericMember,\n DeriveOutput,\n ImmutableParamSchemaProjection,\n MaterialBindingSpan,\n MaterialCoordinateRecordLayout,\n MaterialParameterProjection,\n MaterialParameterResourceProjection,\n MaterialParameterTextureProjection,\n MaterialResourceBindingLayout,\n MaterialResourceKind,\n ParamSchemaDeriveObservationKind,\n ParamSchemaDeriveObserver,\n ParamSchemaProjectionOwnerStats,\n UboFieldLayout,\n UboLayout,\n} from './derive-paramschema.js';\nexport {\n derive,\n deriveObserved,\n findUndeclaredSampledTextures,\n inferMaterialParameterKind,\n ParamSchemaProjectionOwner,\n} from './derive-paramschema.js';\n\n/**\n * Single material parameter schema entry — discriminated union over the\n * three families (Numeric / TextureBinding / StorageBinding).\n *\n * `name` is the parameter identifier matching a WGSL binding name.\n * `type` is the discriminator — exhaustive `switch (entry.type)` covers the\n * 14 literals without a `default` arm; TS guards completeness (charter P3).\n */\nexport type ParamSchemaEntry =\n | NumericParamSchemaEntry\n | TextureBindingParamSchemaEntry\n | StorageBindingParamSchemaEntry;\n\n// === RenderQueue namespace constants (feat-20260526-material-asset-multipass-renderstate M1 / w3) ===\n//\n// Decision anchors:\n// - requirements AC-04 (5 standard queue values: Background=1000 / Geometry=2000 /\n// AlphaTest=2450 / Transparent=3000 / Overlay=4000)\n// - plan-strategy D-3 (queue values replace three-bucket dispatch)\n// - research F-5 (Utopia queue model; Transparent=3000 gives gap between\n// AlphaTest=2450 and Transparent=3000 for user custom queues)\n// - charter P1 (progressive disclosure: RenderQueue.Geometry autocomplete\n// exposes the value; AI users never need to memorize bare numbers)\n\n/**\n * Standard render queue constants (AC-04). AI users access via IDE autocomplete\n * (`RenderQueue.`) — no bare numbers to memorize (charter P1 progressive disclosure).\n *\n * Queue order (ascending):\n * Background(1000) -> Geometry(2000) -> AlphaTest(2450) -> Transparent(3000) -> Overlay(4000)\n *\n * The gap between AlphaTest(2450) and Transparent(3000) allows user-inserted\n * custom queues without colliding with either boundary (research F-5).\n */\nexport const RenderQueue = {\n /** Skybox / backdrop draw, processed first. */\n Background: 1000,\n /** Opaque geometry draw — default queue for solid surfaces. */\n Geometry: 2000,\n /** Alpha-tested geometry (clip/discard in fragment shader) — drawn after opaque,\n * before transparent to avoid overdraw. */\n AlphaTest: 2450,\n /** Transparent / alpha-blended geometry — drawn back-to-front after opaque pass. */\n Transparent: 3000,\n /** Overlay / UI / debug lines — drawn last. */\n Overlay: 4000,\n} as const;\n\n/** Type alias for the 5-member RenderQueue value union (1000 | 2000 | 2450 | 3000 | 4000). */\nexport type RenderQueue = (typeof RenderQueue)[keyof typeof RenderQueue];\n\n// === MaterialRenderState POD interface (feat-20260526-material-asset-multipass-renderstate M1 / w1) ===\n//\n// Decision anchors:\n// - requirements AC-03 (all fields optional; engine applies known defaults)\n// - plan-strategy D-2 (MaterialRenderState fields optional + engine static defaults)\n// - research F-3 (current hardcoded values become the defaults)\n// - research F-6 (Three.js taxonomy proves this subset is sufficient for LO 4.x)\n// - charter P1 (all fields optional reduces boilerplate; JSDoc on each field exposes\n// the default so AI users discover via IDE hover without reading prose)\n\n/**\n * Stencil face state sub-interface — mirrors `@webgpu/types.GPUStencilFaceState`\n * field-by-field (spec-aligned per RHI form rules). All fields optional so\n * consumers declare only the stencil behavior they need.\n */\nexport interface StencilFaceState {\n /** Default: `'never'`. */\n readonly compare?: GPUCompareFunction;\n /** Default: `'keep'`. */\n readonly failOp?: GPUStencilOperation;\n /** Default: `'keep'`. */\n readonly depthFailOp?: GPUStencilOperation;\n /** Default: `'keep'`. */\n readonly passOp?: GPUStencilOperation;\n}\n\n/**\n * Material render-state POD interface — all fields optional (AC-03).\n *\n * When a field is `undefined`, the pipeline-builder falls back to the\n * engine default value noted in each field's JSDoc. AI users only override\n * the fields that differ from the defaults (charter P1 progressive disclosure).\n *\n * The defaults are:\n * - `cullMode='back'` (back-face culling per WebGPU convention)\n * - `depthCompare='less'` (standard depth testing)\n * - `depthWriteEnabled=true` (write depth for opaque surfaces)\n * - `blend` undefined (no blending — opaque pass)\n * - `stencil` undefined (no stencil operations)\n * - `stencilReadMask` undefined (WebGPU default 0xFFFFFFFF)\n * - `stencilWriteMask` undefined (WebGPU default 0xFFFFFFFF)\n * - `frontFace` undefined (default 'ccw')\n */\nexport interface MaterialRenderState {\n /** Face culling mode. Default: `'back'` (cull back faces). */\n readonly cullMode?: 'none' | 'front' | 'back';\n /** Depth comparison function. Default: `'less'`. */\n readonly depthCompare?: GPUCompareFunction;\n /** Whether depth writes are enabled. Default: `true`. */\n readonly depthWriteEnabled?: boolean;\n /** Blend state descriptor (spec-aligned with `GPUBlendState`). Default: undefined (no blending). */\n readonly blend?: GPUBlendState;\n /** Enable alpha-to-coverage when the active camera uses MSAA. Default: `false`. */\n readonly alphaToCoverageEnabled?: boolean;\n /** Stencil face state. Default: undefined (no stencil operations). */\n readonly stencil?: StencilFaceState;\n /**\n * Stencil read mask (mirrors GPUDepthStencilState.stencilReadMask top-level).\n * Default: undefined (WebGPU default 0xFFFFFFFF).\n */\n readonly stencilReadMask?: number;\n /**\n * Stencil write mask (mirrors GPUDepthStencilState.stencilWriteMask top-level).\n * Default: undefined (WebGPU default 0xFFFFFFFF).\n */\n readonly stencilWriteMask?: number;\n /**\n * Front-face winding (mirrors GPUPrimitiveState.frontFace).\n * Default: `'ccw'`.\n */\n readonly frontFace?: 'ccw' | 'cw';\n}\n\n// === PassKind as open string + KNOWN_PASS_KINDS (feat-20260615-pipeline-spec-ssot D-10) ===\n//\n// Decision anchors:\n// - plan-strategy D-10 (PassKind opened from closed union to string; KNOWN_PASS_KINDS\n// is a discoverable documentation constant)\n// - requirements AC-09 (PassKind: open string; unknown passKind -> PipelineSpecError\n// code='unknown-pass-kind')\n// - charter P3 (fail-fast on unknown passKind via PipelineSpecError, not silent route)\n\n/**\n * Render-pass kind -- open string, no longer a closed union.\n *\n * `KNOWN_PASS_KINDS` (below) documents the engine-shipped pass kinds; user-defined\n * pass kinds are supported through {@link ShaderRegistry} registration. An unknown\n * pass kind triggers {@link PipelineSpecError} with code `'unknown-pass-kind'`\n * at pipeline-spec build time (charter P3 explicit failure).\n *\n * @see {@link KNOWN_PASS_KINDS} for the discoverable pass-kind catalogue\n * @see plan-strategy D-10 (PassKind opened from closed union)\n */\nexport type PassKind = string;\n\n/**\n * Engine-shipped pass kinds -- discoverable constant catalogue (D-10).\n *\n * Consumers iterate or lookup against this set to validate pass kinds before\n * submitting to `getOrBuildPipeline`. An unknown pass kind still triggers a\n * structured `PipelineSpecError` with code `'unknown-pass-kind'` carrying\n * `.detail.expected = KNOWN_PASS_KINDS` and `.detail.actual`.\n */\nexport const KNOWN_PASS_KINDS: readonly string[] = [\n 'forward',\n 'deferred',\n 'lighting',\n 'shadow-caster',\n 'point-shadow-caster',\n 'post-process',\n 'skybox',\n] as const;\n\n// === PassSelector type (feat-20260526-material-asset-multipass-renderstate M1 / w4) ===\n//\n// Decision anchors:\n// - requirements AC-05 (tags + PassSelector matching: all selector keys must\n// exist in pass tags with value in the selector's value list)\n// - plan-strategy D-4 (Tags free Record + PassSelector Record<string, string[]>;\n// enum-based categories rejected — adding a pipeline stage should not require\n// editing the types package)\n// - charter P1 (type signature itself is the match-rule documentation;\n// `Record<string, string[]>` is self-describing)\n\n/**\n * Pass selector — maps tag keys to allowed value lists (AC-05).\n *\n * A pass matches the selector when **every** key in the selector exists in the\n * pass's `tags` and the pass's tag value is in the selector's value list for that key.\n * An empty selector matches every pass (no key constraints).\n *\n * The type signature itself is the API docs (charter P1): each entry maps a\n * tag key (string) to its allowed values (string array).\n *\n * @example Match passes tagged with `RenderType: 'Opaque'` or `RenderType: 'Transparent'`\n * ```ts\n * const selector: PassSelector = { RenderType: ['Opaque', 'Transparent'] };\n * ```\n */\nexport type PassSelector = Record<string, readonly string[]>;\n\n// === FontAsset POD shape (feat-20260531-world-space-msdf-text-rendering M2 / w5) ===\n//\n// Decision anchors:\n// - plan-strategy D-6 (FontAsset data shape: atlas Handle<TextureAsset> +\n// sampler Handle<SamplerAsset> + glyphs Record<codepoint, GlyphMetric> +\n// common block + optional notdef fallback; POD, math-free, fields 1:1\n// mirror toolchain wiki §4 BMFont char mapping)\n// - requirements AC-04 (FontAsset enters Asset closed union, 11->12)\n// - AGENTS.md §Component naming (single-semantic components drop the\n// Component suffix; FontAsset is a data asset, not an ECS component)\n//\n// GlyphMetric fields mirror the BMFont char block layout (toolchain wiki §4):\n// advance <- xadvance (horizontal distance to next glyph)\n// bearingX <- xoffset (horizontal offset from cursor)\n// bearingY <- yoffset (vertical offset from baseline)\n// size.{w,h} <- width/height (glyph quad size in layout space, before atlas\n// scale)\n// region.{x,y,w,h} <- atlas UV region in pixels (x/y = top-left corner\n// relative to atlas origin)\n\n/**\n * Per-glyph metric layout — 1:1 mirror of BMFont char block fields\n * (toolchain wiki §4). POD, math-free.\n */\nexport interface GlyphMetric {\n /** Horizontal distance to the next glyph (xadvance). */\n readonly advance: number;\n /** Horizontal offset from cursor (xoffset). */\n readonly bearingX: number;\n /** Vertical offset from baseline (yoffset). */\n readonly bearingY: number;\n /** Glyph quad size in layout space. */\n readonly size: { readonly w: number; readonly h: number };\n /** Atlas UV region in pixels (top-left origin). */\n readonly region: {\n readonly x: number;\n readonly y: number;\n readonly w: number;\n readonly h: number;\n };\n}\n\n/**\n * Font asset POD — atlas texture handle + sampler handle + per-codepoint\n * glyph metrics + common layout block.\n *\n * AI users obtain a FontAsset via `assets.loadByGuid<FontAsset>(guid)` and\n * hand the resulting `Handle<FontAsset>` to `GlyphText.fontHandle`. The atlas\n * texture and sampler are resolved through the handle chain by the glyph\n * layout system; the per-glyph metrics drive the quad-position-and-UV baking\n * (plan-strategy D-6).\n *\n * | Field | Purpose |\n * |:--|:--|\n * | `atlas` | Handle to the baked MSDF atlas `TextureAsset` |\n * | `sampler` | Handle to the `SamplerAsset` for atlas sampling |\n * | `glyphs` | `Record<codepoint, GlyphMetric>` — O(1) codepoint lookup |\n * | `common` | Common layout block (lineHeight / base / distanceRange / pxRange / atlas width/height) |\n * | `notdef` | Optional fallback glyph metric for missing codepoints (TOFU, AC-14) |\n */\nexport interface FontAsset {\n readonly kind: 'font';\n /**\n * Atlas texture GUID (D-19). Payload-internal sub-asset ref stored as an\n * AssetGuid, not a handle — the loadByGuid recursion mints no handle; the\n * World-holding consumer (glyph-text-layout) resolves it once at read time.\n */\n readonly atlas: AssetGuid;\n /** Sampler GUID (D-19). Same GUID-identity contract as `atlas`. */\n readonly sampler: AssetGuid;\n readonly glyphs: Record<number, GlyphMetric>;\n readonly common: {\n readonly lineHeight: number;\n readonly base: number;\n readonly distanceRange: number;\n readonly pxRange: number;\n readonly atlasWidth: number;\n readonly atlasHeight: number;\n };\n readonly notdef?: GlyphMetric;\n}\n\n// === RenderPipelineAsset POD shape =============================================\n//\n// feat-20260601-customizable-render-pipeline-seam-and-dogfood-rend M1 / w5.\n// Asset-layer descriptor that binds a registered render-pipeline logic id to an\n// installable Handle (the MaterialAsset { materialShaderId, params } pattern, scaled to\n// the pipeline layer). `installPipeline(handle)` resolves this POD off the AssetRegistry,\n// looks up `pipelineId` in the pipeline registry, and swaps the per-frame graph.\n//\n// M2 stage (w10): `config.passCount` is the FIRST real config key. The standard pipeline\n// runs with config undefined (default frame byte-identical, AC-01); a custom pipeline can\n// size its declared pass chain from `config.passCount` so its topology varies observably\n// via `renderer.perFramePassNames` (AC-03 / plan-strategy D-C).\n//\n// feat-20260608-cluster-lighting M2 / w8: `config.clusterGrid` is the HDRP cluster grid\n// dimensions config (default {x:16, y:9, z:24}). `pipelineId` is narrowed to a literal\n// union of `'forgeax::urp' | 'forgeax::hdrp' | (string & {})` so TS narrowing on\n// `pipelineId === 'forgeax::hdrp'` narrows `config.clusterGrid`.\n//\n// feat-20260612-hdrp-ssao M4 / w19: `config.ssao` is the SSAO configuration\n// (enabled+radius+bias+intensity). Same shared-config pattern as clusterGrid;\n// HDRP consumes it, URP ignores it at runtime.\n/**\n * Asset-layer descriptor for an installable render pipeline.\n *\n * `pipelineId` references a logic registered via `renderer.registerPipeline(id, impl)`\n * (engine builtins use the `forgeax::` prefix, e.g. `'forgeax::urp'`; user pipelines use\n * `<package>::<id>`). `config` is the per-install tuning the pipeline logic reads at\n * `buildGraph` time; `passCount` is the first real key (a custom pipeline declares that\n * many passes). The URP ignores `config` (its topology is fixed).\n *\n * `config.clusterGrid` is the HDRP cluster grid dimensions (x/y/z each an integer in\n * [1, 64]; default {16, 9, 24}). It is ignored by URP.\n *\n * `config.ssao` enables SSAO (Screen-Space Ambient Occlusion) for the HDRP\n * deferred path. Ignored by URP.\n */\nexport interface RenderPipelineAsset {\n readonly kind: 'render-pipeline';\n readonly pipelineId: 'forgeax::urp' | 'forgeax::hdrp' | (string & {});\n readonly config?: {\n readonly passCount?: number;\n readonly clusterGrid?: { readonly x: number; readonly y: number; readonly z: number };\n readonly ssao?: {\n readonly enabled: boolean;\n readonly radius?: number | undefined;\n readonly bias?: number | undefined;\n readonly intensity?: number | undefined;\n };\n /**\n * feat-20260621 M4': ordered registered post-process shader ids the built-in\n * pipelines composite over the FINAL swap-chain image, after the fxaa pass\n * and before the debug overlay. Each id must be registered via\n * `renderer.postProcess.register(id, entry)` first. The effects run in array\n * order; each samples the current swap-chain (copy) and writes it back, so a\n * chain composes left-to-right. This is the AUGMENT path: the built-in 9-pass\n * chain (shadow cascades, tonemap, bloom, fxaa) renders unchanged and the\n * effects layer on top — unlike installing a wholly custom pipeline, which\n * REPLACES the built-in graph (and would drop its shadow passes). `undefined`\n * or `[]` adds zero passes (default frame byte-identical).\n *\n * WebGPU backend only: each effect reads the mid-frame swap-chain (copy +\n * non-srgb storage-view write), which the WebGL2 fallback swap-chain does not\n * support (no COPY_SRC, no non-srgb reinterpret view) — same constraint the\n * built-in FXAA pass already carries. On a non-WebGPU device leave this empty.\n */\n readonly postEffects?: readonly string[];\n };\n}\n\n/**\n * Asset discriminated union - 13 variants keyed on `.kind`.\n *\n * Variant history:\n * - feat-20260513-instanced-mesh M1 introduced a 5th `'instanced-buffer-asset'`\n * variant carrying packed mat4 transforms + a `version` dirty flag.\n * - feat-20260514-ecs-children-instances-managed-buffer-array M3 (w15)\n * retired that variant: per-entity instanced transforms are now stored\n * directly inside the ECS via the `Instances { transforms: 'array<f32>' }`\n * component (managed by the BufferPool slot column + sidecar count\n * column). Asset closed-union shrinks 5 -> 4 (evolution major rename\n * per AGENTS.md `Change stance`); existing exhaustive `switch\n * (asset.kind)` consumers drop the now-unreachable arm in the same PR.\n * - feat-20260514-scene-as-world-blueprint w3 added `'scene'` variant\n * (4 -> 5, minor add per AGENTS.md `Evolution contract`); declarative\n * SceneEntity list, no overrides at the asset layer.\n * - feat-20260531-world-space-msdf-text-rendering w5 added `'font'` variant\n * (11 -> 12, minor add per AGENTS.md `Evolution contract`);\n * FontAsset with atlas handle + sampler handle + glyph metrics.\n * - feat-20260601-customizable-render-pipeline-seam-and-dogfood-rend w5 added\n * `'render-pipeline'` variant (12 -> 13, minor add per AGENTS.md\n * `Evolution contract`); RenderPipelineAsset with pipelineId + config.\n * - feat-20260623-world-space-video-asset M1 added `'video'` variant\n * (14 -> 15, minor add per AGENTS.md `Evolution contract`);\n * VideoAsset with `{ url }` descriptor, no width/height/duration.\n *\n * Exhaustive `switch (asset.kind)` type-guards against future additions\n * without default fallback (charter proposition 4 + proposition 3).\n *\n * | kind | variant |\n * |:--|:--|\n * | `'mesh'` | `MeshAsset` |\n * | `'texture'` | `TextureAsset` |\n * | `'equirect'` | `EquirectAsset` (single 2D HDR lat-long env map for IBL) |\n * | `'sampler'` | `SamplerAsset` |\n * | `'material'` | `MaterialAsset` (further narrows on `.passes`) |\n * | `'scene'` | `SceneAsset` (declarative SceneEntity list, no overrides) |\n * | `'font'` | `FontAsset` (MSDF atlas handle + glyph metrics) |\n * | `'render-pipeline'` | `RenderPipelineAsset` (installable pipeline logic id + config) |\n * | `'video'` | `VideoAsset` (runtime-only `{ url }` descriptor, no width/height/duration) |\n * | `'animation-graph'` | `AnimationGraph` (Clip/Blend/Add DAG + per-node static weights) |\n */\nexport type Asset =\n | MeshAsset\n | TextureAsset\n | EquirectAsset\n | SamplerAsset\n | MaterialAsset\n | SceneAsset\n | SkeletonAsset\n | SkinAsset\n | AnimationClip\n // === 1 new variant (feat-20260713-animation-state-machine-plugin M2 / w13) ===\n // AnimationGraph POD (Clip/Blend/Add DAG); GUID-addressable, serializable\n // (AC-14 foundation). Closed node union (clip/blend/add), no reserved FSM/Mask\n // fields (OOS-1..4).\n | AnimationGraph\n | AudioClipAsset\n | FontAsset\n | RenderPipelineAsset\n // === 1 new variant (feat-20260608-tilemap-object-layer-rendering M0 baseline rebuild) ===\n // Direct atlases[] form (plan-strategy D-7 one-cut); no intermediate single-`atlas` shape.\n | TilesetAsset\n // === 1 new variant (feat-20260623-world-space-video-asset M1) ===\n // runtime-only { url } descriptor; no width/height/duration in payload.\n | VideoAsset\n | ParticleEffectAsset;\n\n// === Tileset asset POD shape (feat-20260608 M0 baseline rebuild) =================\n//\n// Decision anchors:\n// - requirements §AC-01/03/04/05 (TilesetAsset 9 fields; atlases plural composite;\n// M0 TilesetTileEntry single-field shape with M1 adding 5 optional + collider).\n// - plan-strategy §D-5 (M0 baseline rebuild after main reverted feat-20260604).\n// - plan-strategy §D-7 (`atlases: readonly Handle<TextureAsset,managed>[]` one-cut\n// rename; no `atlas` single-form alias or dual-path).\n// - plan-strategy §D-6 (AssetErrorCode count restoration -- M0 reintroduces\n// `tileset-region-index-out-of-range`; M1 adds `tileset-tile-entry-malformed`).\n// - charter F1 (AI users discover the schema via IDE autocomplete on the closed\n// `Asset` union + `Handle<TilesetAsset>` returns from `AssetRegistry.register`).\n// - charter P4 (atlases plural composite mirrors `MaterialAsset.passes[]` shape).\n\n/**\n * Closed `TilesetTileCollider` union -- per-tile collider schema (M1\n * extension; feat-20260608-tilemap-object-layer-rendering M1 / m1-t2).\n *\n * Three discriminant variants (closed enum, charter P3):\n *\n * - `{ type: 'none' }` -- no collider for this tile.\n * - `{ type: 'rect', rect: readonly [x, y, w, h] }` -- axis-aligned\n * rectangle in normalized cell coordinates `[0, 1]^2`. `w > 0`, `h > 0`,\n * `x + w <= 1`, `y + h <= 1` are enforced by `validateTilesetPayload`\n * (R-6 first-error path).\n * - `{ type: 'polygon', points: readonly [x, y][] }` -- convex/concave\n * polygon in normalized cell coordinates. `points.length >= 3` and\n * each point lies in `[0, 1]^2`.\n *\n * The engine validates this schema at register-time but does NOT consume\n * it (plan-strategy §D-4 -- schema landed, consumer deferred to a future\n * `feat-tilemap-physics-bridge` closed loop). AI users with a physics\n * sidecar consume the schema directly via `tileset.tiles[i].collider`\n * after `assets.register<TilesetAsset>(...)` resolves the handle.\n *\n * Exhaustive switch:\n * ```ts\n * switch (collider.type) {\n * case 'none': return null;\n * case 'rect': return collider.rect;\n * case 'polygon': return collider.points;\n * // No default branch -- TS guards completeness (charter P3).\n * }\n * ```\n */\nexport type TilesetTileCollider =\n | { readonly type: 'none' }\n | { readonly type: 'rect'; readonly rect: readonly [number, number, number, number] }\n | { readonly type: 'polygon'; readonly points: readonly (readonly [number, number])[] };\n\n/**\n * Rectangular sub-region within a tileset atlas (M1 schema extension on\n * top of M0 baseline rebuild).\n *\n * Four required fields define the atlas-space rectangle in pixels:\n * `x` / `y` top-left corner; `width` / `height` extent. `width + x` and\n * `height + y` MUST stay within the parent atlas extent or\n * `validateTilesetPayload` returns\n * `AssetError { code: 'tileset-region-index-out-of-range' }`\n * (charter P3 explicit failure at register-time).\n *\n * Optional `atlasIndex?: number` (M1; default 0) routes the region into\n * `TilesetAsset.atlases[atlasIndex]` for multi-atlas tilesets. Out-of-range\n * `atlasIndex` (`>= atlases.length` or negative) surfaces\n * `AssetError { code: 'tileset-tile-entry-malformed', detail: { field: 'atlasIndex', scope: 'tileset-asset' } }`\n * at register-time (plan-strategy §D-7 three-hop routing; R-6 first-error\n * order places atlasIndex check between region rect bounds and per-tile\n * entry field checks).\n */\nexport interface TilesetRegion {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n readonly atlasIndex?: number;\n}\n\n/**\n * Per-tile entry in `TilesetAsset.tiles[]` (M1 schema extension on top of\n * M0 baseline rebuild).\n *\n * Required `regionIndex` points into the parent `TilesetAsset.regions[]`\n * array. M1 adds five optional fields for the variable-size + custom-pivot\n * object-layer story (plan-strategy §M1):\n *\n * - `widthCells?: number` -- multi-cell width in `Tilemap.cols/rows`\n * coordinate units (default 1; range `(0, 64]`). Anchored at the\n * cell that hosts the non-zero tileId entry; cells inside the\n * `widthCells x heightCells` footprint must stay 0 in `TileLayer.tiles[]`\n * (anchor convention, not enforced at register time).\n * - `heightCells?: number` -- multi-cell height (default 1; range `(0, 64]`).\n * - `pivotX?: number` -- normalized horizontal pivot in `[0, 1]` (default 0.5).\n * The pivot is the world-space anchor: `pivot_world_X = (cellX + pivotX) * tileSizeX`.\n * Quad center is offset by `(0.5 - pivotX) * widthCells * tileSizeX`\n * (plan-strategy §D-2 first-line geometry; M2 implementation).\n * - `pivotY?: number` -- normalized vertical pivot in `[0, 1]` (default 0.5).\n * For asi_world `.tsj` extension: `pivotY = 1.0` means quad bottom\n * anchors the cell, `pivotY = 0.0` means quad top (per-asset convention,\n * not Tiled native). The engine uses `effectivePivotY` for Y-sort.\n * - `collider?: TilesetTileCollider` -- 3-variant closed union schema\n * (charter P3). Engine validates the schema at register-time but does\n * NOT consume it (plan-strategy §D-4).\n *\n * All five fields are optional so unit-cell call sites `{ regionIndex: N }`\n * remain backward compatible (charter F1).\n *\n * Out-of-range `regionIndex` (>= regions.length, or negative) surfaces\n * `AssetError { code: 'tileset-region-index-out-of-range' }`. Out-of-range\n * `widthCells / heightCells / pivotX / pivotY / collider` surface\n * `AssetError { code: 'tileset-tile-entry-malformed', detail: { field, scope: 'tile-entry', tileEntryIndex } }`\n * (plan-strategy §D-6 closed 7-variant `.detail.field` enum).\n */\nexport interface TilesetTileEntry {\n readonly regionIndex: number;\n readonly widthCells?: number;\n readonly heightCells?: number;\n readonly pivotX?: number;\n readonly pivotY?: number;\n readonly collider?: TilesetTileCollider;\n}\n\n/**\n * Tileset asset (M0 baseline rebuild on origin/main).\n *\n * Nine fields:\n * - `kind` -- discriminator literal `'tileset'`.\n * - `atlases` -- one or more durable GUIDs for atlas textures.\n * `atlases.length >= 1` enforced at register time.\n * M0 reads `atlases[0]` exclusively (single-atlas form);\n * M1 adds `regions[].atlasIndex` for multi-atlas routing.\n * - `tileWidth`/`tileHeight` -- per-cell pixel size (atlas grid stride).\n * - `columns`/`rows` -- atlas grid layout (informational metadata; used\n * as fallback when `atlasSizes` is absent to infer\n * atlas pixel extent as `columns * tileWidth`).\n * - `atlasSizes` -- optional per-atlas pixel dimensions. When present,\n * `atlasSizes[i]` gives the exact pixel size of\n * `atlases[i]` and overrides `columns`/`rows` for UV\n * normalisation in the chunk-extract system. Required when\n * the tileset contains multiple atlases with different\n * pixel sizes (e.g. a terrain + object composite tileset).\n * Each entry carries `{ pixelWidth, pixelHeight }`.\n * - `regions` -- array of atlas sub-rectangles (TilesetRegion).\n * - `tiles` -- per-tile entries (TilesetTileEntry), 1-indexed via tile id\n * sentinel where 0 means \"empty\" in `TileLayer.tiles`.\n *\n * Plural composite `atlases` (not single `atlas`) is the one-cut breaking\n * rename versus the old feat-20260604 form (plan-strategy §D-7 + AGENTS.md\n * §Change stance \"optimal > compatible\"); no deprecation alias survives.\n *\n * @example Register a tileset and spawn a Tilemap + TileLayer pair:\n * ```ts\n * const atlasGuid = 'world/object_atlas';\n * const tileset = registry.register<TilesetAsset>({\n * kind: 'tileset',\n * atlases: [atlasGuid],\n * tileWidth: 16,\n * tileHeight: 16,\n * columns: 8,\n * rows: 8,\n * regions: [{ x: 0, y: 0, width: 16, height: 16 }],\n * tiles: [{ regionIndex: 0 }],\n * }).unwrap();\n * ```\n */\nexport interface TilesetAtlasSize {\n readonly pixelWidth: number;\n readonly pixelHeight: number;\n}\n\nexport interface TilesetAsset {\n readonly kind: 'tileset';\n readonly atlases: readonly string[];\n readonly tileWidth: number;\n readonly tileHeight: number;\n readonly columns: number;\n readonly rows: number;\n /** Per-atlas pixel dimensions. `atlasSizes[i]` overrides `columns`/`rows`\n * for UV normalisation of regions whose `atlasIndex === i`. Required when\n * atlases have different pixel sizes. */\n readonly atlasSizes?: readonly TilesetAtlasSize[];\n readonly regions: readonly TilesetRegion[];\n readonly tiles: readonly TilesetTileEntry[];\n}\n\n// === AssetRef + AssetEnvelope (feat-20260622-asset-ref-graph-protocol-unification-refs-as-ssot M1 / w1) ===\n//\n// Decision anchors:\n// - plan-strategy D-1: single envelope type AssetEnvelope = { guid, kind, name?,\n// payload, refs } — ImportedAsset is upgraded to this shape; assetCatalog\n// value type changes from Map<string, Asset> to Map<string, AssetEnvelope>.\n// - plan-strategy D-2: scene refs are importer flat superset (mesh U material U\n// texture U skeleton U skin); texture edges have sourceField=undefined (no\n// per-entity origin).\n// - plan-strategy D-3: sourceField is structured triple { componentName,\n// fieldName, arrayIndex? } — consumers read via property access, not string\n// parse (charter P3).\n// - plan-strategy D-10: edge metadata (AssetRef) does NOT sink into Loader.load\n// refs param — loader still receives GUID string projection.\n// - plan-strategy OOS-1: Asset closed union unchanged; AssetEnvelope wraps it.\n// - plan-strategy OOS-4: AssetErrorCode member set unchanged (21 members).\n// - charter F1: single-entry indexability — refs field name cross-layer\n// consistent (ImportedAsset.refs / AssetEnvelope.refs / pack-index refs).\n\n/**\n * Structured edge metadata carried in an asset envelope's `refs[]`. Each entry\n * records a GUID-level reference plus optional provenance: which scene entity\n * field originated the reference (``sourceField``) and the entity's local id\n * (``sceneEntityId``). Texture edges and other transitive references have no\n * per-entity origin — ``sourceField`` is ``undefined`` for those (D-2).\n *\n * AI users consume ``sourceField`` via property access (``ref.sourceField?.componentName``\n * / ``ref.sourceField?.fieldName`` / ``ref.sourceField?.arrayIndex``), never by\n * parsing a concatenated string (charter P3).\n */\nexport interface AssetRef {\n readonly guid: string;\n readonly sourceField?: {\n readonly componentName?: string;\n readonly fieldName: string;\n readonly arrayIndex?: number;\n };\n readonly sceneEntityId?: number;\n}\n\n/**\n * Self-contained asset envelope — the single shape through which assets flow\n * from import to catalog to recursive load (plan-strategy D-1).\n *\n * ``payload`` carries the closed ``Asset`` union member (mesh / texture / scene /\n * material / etc.). ``refs`` is the authoritative reference graph — every GUID\n * this asset transitively depends on, with optional edge metadata\n * (``sourceField`` / ``sceneEntityId``). ``name`` is the per-asset display name\n * (may be undefined; ``resolveName`` derives the final name via a three-argument\n * XOR that also considers the package path).\n */\nexport interface AssetEnvelope<P = Asset> {\n readonly guid: string;\n readonly kind: string;\n // Per-GUID stored display name -- the `storedName` argument resolveName feeds\n // to deriveAssetName (the single home for the explicit name, replacing the\n // retired storedNameOf side table). `undefined` = no explicit name (resolveName\n // then applies the multi-asset basename fallback / no-package '' branch).\n readonly name?: string;\n readonly payload: P;\n readonly refs: readonly AssetRef[];\n}\n\n// === Package interface (feat-20260618-asset-and-pack-name-fields M1 / w2) ======\n//\n// Decision anchors:\n// - plan-strategy D-7 (Package interface in @forgeax/engine-types, same layer\n// as Asset union, for multi-package consumer discoverability per charter F1)\n// - architecture-principles #2 (Derive, Don't Duplicate): assetCount is\n// derived from assetGuids.size, never stored independently\n// - plan-strategy D-5 (builtin assets -> null Package, not a synthetic path)\n// - Package does not carry a `name` field — resolved names flow through\n// resolveName (D-6), not stored on Package\n//\n// AI users discover Package via IDE autocomplete on @forgeax/engine-types;\n// the runtime AssetRegistry.packageOf Map carries Package | null per guid.\n\n/**\n * Runtime view of one import-source package -- the grouping unit for\n * the two-segment asset identity (`<packagePath>.<name>`).\n *\n * `path` is the import file path (e.g. `'assets/hero.glb'`). Multiple\n * assets imported from the same source file share one `Package`.\n *\n * `assetGuids` lists every GUID that belongs to this package. The\n * runtime keeps it in sync with `registerPackage` insertions.\n *\n * `assetCount` is a derived view (`assetGuids.size`); it is **not**\n * stored as a standalone field (Derive axiom #2).\n */\nexport interface Package {\n readonly path: string;\n readonly assetGuids: ReadonlySet<string>;\n readonly assetCount: number;\n}\n\n// === Scene asset POD shape (feat-20260514-scene-as-world-blueprint w2) ==========\n//\n// Decision anchors:\n// - requirements §AC-01 (AssetUnion 6 elements; SceneAsset top level only\n// `kind` + `entities`, no overrides field at the asset layer)\n// - requirements §AC-02 (LocalEntityId branded number; cross-brand assignment\n// to / from Entity is a TS compile-time error)\n// - charter proposition 1 (single-entry IDE autocomplete from\n// `@forgeax/engine-types`) + proposition 4 (explicit failure: brand\n// phantom rejects untagged number) + proposition 5 (consistent\n// abstraction, structurally parallel to Handle<T> brand)\n//\n// The unique-symbol brand stays private to this module so the brand\n// identity is anchored exactly here; consumers refer to LocalEntityId\n// as opaque number subtypes.\n\ndeclare const LocalEntityIdBrand: unique symbol;\n\n/**\n * Scene-local entity index brand (u32).\n *\n * Authored as `0..entities.length-1` inside a SceneAsset; runtime storage stays\n * a plain JS number (the phantom `[LocalEntityIdBrand]` is erased at runtime\n * but rejects cross-brand assignment with `Entity` at\n * the TS layer).\n *\n * AI users obtain LocalEntityId values from `SceneEntity.localId` accessors and\n * hand them back to `SceneEntity` manipulation methods; plain\n * `number` is not assignable to `LocalEntityId` by design — see\n * `packages/types/src/__tests__/scene-brand.test-d.ts` for the negative\n * assertions.\n */\nexport type LocalEntityId = number & { readonly [LocalEntityIdBrand]: void };\n\n/**\n * Open map shape from component name to the per-component value record\n * authored on a `SceneEntity` (feat-20260514 w2).\n *\n * The map is keyed by component-token name (`'Transform' | 'MeshFilter' |\n * 'ChildOf' | ...`) and each per-component record is a free-form\n * `Record<string, unknown>` POD shape; the precise field types live in the\n * ecs `defineComponent(...)` schema (one layer up). This package stays\n * math-free + ecs-free; the layered alignment with the ecs schema vocab is\n * documented in plan-strategy §3.1 types_pkg sub-graph and tested by w3 /\n * w22 at the ecs / runtime layer.\n *\n * Open shape is intentional: components evolve via add-only minor in their\n * own packages; locking this map to a closed union here would force an edit\n * in @forgeax/engine-types every time a new component appears (charter\n * proposition 5 consistent abstraction — registration discipline owned by\n * each component's defineComponent site).\n */\nexport type ComponentValuesMap = {\n readonly [componentName: string]: Readonly<Record<string, unknown>>;\n};\n\n/**\n * Single SceneEntity POD shape (feat-20260514 w2).\n *\n * Carries one `localId` (LocalEntityId brand) plus a partial map of explicit\n * component field values. The partial keying lets layer 1 (explicit) leave\n * any component absent so layer 2 (component-level defaults) and layer 3\n * (TS type defaults) can fill in the residual fields at instantiate time\n * (plan-strategy §default-values 4-layer fallback table; AC-07 / AC-11 /\n * AC-12 sites).\n */\nexport interface SceneEntity {\n readonly localId: LocalEntityId;\n readonly components: Partial<ComponentValuesMap>;\n}\n\n/**\n * One per-member mount-time modification (add-or-patch) applied to a\n * mounted scene member (feat-20260608-scene-nesting-ecs-fication M1 / w7;\n * feat-20260713-mount-override-component-add-and-shared-ref-round M1 / w2;\n * AC-01, AC-19).\n *\n * `localId` selects a member entity inside the mounted SceneAsset,\n * counted against the mount's `memberFirst` window. `comp` names the\n * target component. `field` is a component-granular discriminant:\n * - present -> PATCH one field: `value` is that single field's value,\n * written over the member's existing component at instantiate time;\n * - absent -> ADD/UPSERT the whole component: `value` is the per-field\n * value map for `comp`, merged onto (or creating) the member's\n * component.\n * `value` stays `unknown` because the per-component schema vocab lives one\n * layer up — runtime fail-fast via 'scene-override-type-mismatch' catches\n * type drift (plan-strategy D-9 / D-1). The discriminant is carried by the\n * shape itself (field present / absent), never a separate `op` tag, so no\n * consumer branches on a `switch (op)` (requirements: consumers do not\n * encode variant knowledge).\n *\n * Boundary notes: add is upsert (an existing component is merged, not\n * rejected); a NULL-sentinel entity value in the payload follows the same\n * `entity` remap rules as SceneEntity.components; overrides apply in\n * `mounts[].overrides[]` array order after the member's own authored\n * components.\n *\n * Charter mapping: proposition 1 (single-entry import surface, three-tier\n * progressive disclosure) + proposition 3 (machine-readable union,\n * MountOverride is a closed POD shape) + proposition 4 (explicit failure:\n * runtime apply path returns Result with structured error code).\n */\nexport interface MountOverride {\n readonly localId: LocalEntityId;\n readonly comp: string;\n readonly field?: string;\n readonly value: unknown;\n}\n\n/**\n * One mount instance authored on a parent SceneAsset\n * (feat-20260608-scene-nesting-ecs-fication M1 / w7; AC-01).\n *\n * A mount embeds another SceneAsset (referenced by `source`, a dual-carrier:\n * `number` = at-rest refs[] index; `string` = post-parse / post-collect GUID\n * string) into the parent's namespace. The mount reserves a contiguous\n * LocalEntityId window\n * `[memberFirst, memberFirst + memberCount)` for the embedded scene's\n * member entities so the parent SceneAsset's namespace invariant\n * `totalSlots = entities.length + mounts.length + sum(memberCount)`\n * holds (plan-strategy §6.3 + requirements §S-1).\n *\n * Optional fields:\n * - `parent`: LocalEntityId in the *parent* scene to which the mount\n * attaches (defaults to the parent scene's outermost root,\n * requirements-decisions §D-4);\n * - `components`: per-component value overlay applied to the mount\n * entity itself (mirrors SceneEntity.components shape; carries the\n * same Partial<ComponentValuesMap> typing);\n * - `overrides`: an array of MountOverride records that further\n * specialise individual member entities at mount-time (AC-19).\n *\n * Charter mapping: proposition 1 (single-entry import surface;\n * SceneInstanceMount sits next to SceneEntity / SceneAsset) +\n * proposition 5 (consistent abstraction: the field set mirrors\n * SceneEntity for AI-user discoverability).\n */\nexport interface SceneInstanceMount {\n readonly localId: LocalEntityId;\n readonly source: number | string;\n readonly memberFirst: LocalEntityId;\n readonly memberCount: number;\n readonly parent?: LocalEntityId;\n readonly components?: Partial<ComponentValuesMap>;\n readonly overrides?: readonly MountOverride[];\n /** Engine-owned publication identity for generated Scene mounts. */\n readonly publicationFence?: import('./asset-producer').ScenePublicationFence;\n}\n\n/**\n * Scene asset POD shape (feat-20260514 w2; sixth member of the closed\n * `Asset` union).\n *\n * Three top-level fields — `kind: 'scene'` discriminator,\n * `entities: readonly SceneEntity[]`, and the optional `mounts:\n * readonly SceneInstanceMount[]` (feat-20260608-scene-nesting-\n * ecs-fication M1 / w7; AC-01). Per-instance overrides at the\n * asset layer are still absent (charter proposition 5: ECS write\n * paths and prefab override paths are explicitly disjoint,\n * plan-strategy §3.2 sequence B); the `mounts[]` window is an\n * authoring-time graph edge, not a write path.\n *\n * Back-compat: `mounts` is optional so legacy SceneAsset values\n * remain assignable; missing `mounts` is semantically equivalent to\n * `mounts: []` (plan-strategy §6.3, ajv default `[]`).\n */\nexport interface SceneAsset {\n readonly kind: 'scene';\n readonly entities: readonly SceneEntity[];\n readonly mounts?: readonly SceneInstanceMount[];\n /**\n * GUIDs of `SkinAsset`s the scene's skinned entities reference (one per\n * SkeletonAsset bound by a `Skin: { skeleton }` component). SkinAssets are\n * not reachable through any `handle<*>` field on a SceneEntity component\n * (`Skin.skeleton` carries the SkeletonAsset GUID; the SkinAsset itself is\n * a sibling identified by matching `skeletonGuid`), so the scene's pack\n * load chain has to surface them explicitly. Without this list the\n * browser-async-pack-fetch path would never load SkinAssets, leaving\n * `postSpawnResolveJoints` unable to populate `Skin.joints[]` and the\n * extract pass fail-fasting on `Skin.joints.length=0` every frame\n * (feat-20260612-skin-palette-per-frame-upload M2 fixup).\n *\n * On disk: refs[] indices, mirror of `mounts[].source`.\n * Post-parseScenePayload: GUID strings (resolved via refs[]).\n * Enumerated in the scene envelope's `refs[]` (the recursion source) so\n * `loadByGuid<SceneAsset>` recursively pulls each SkinAsset before\n * `instantiate`.\n */\n readonly skinGuids?: readonly string[];\n}\n\n// === Skeleton / Skin / AnimationClip asset POD shapes (feat-20260523-skin-skeleton-animation M0) ===\n//\n// Decision anchors:\n// - requirements AC-01 (SkeletonAsset shape: kind, guid, inverseBindMatrices Float32Array, jointCount)\n// - requirements AC-04 (Skin sub-asset shape: kind, guid, skeletonGuid, jointPaths)\n// - requirements AC-07 (AnimationClip shape: kind, guid, duration, channels)\n// - plan-strategy D-1 (3-asset separation: IBM/Skin bindings/AnimationClip curves physically independent)\n// - charter P3 (explicit failure: all shape carry typed fields, no loose Record<string,unknown> payloads)\n//\n// AnimationChannel / AnimationSampler sub-types are inline here since they are\n// exclusively consumed by AnimationClip; no other asset or component references them.\n\n/** Stable target identity linking a sampler to a scene entity. */\nexport interface AnimationChannel {\n /** Stable animation target identity. */\n readonly targetId: AnimationTargetIdValue;\n /** Target transform property: 'translation' | 'rotation' | 'scale' | 'weights'. */\n readonly property: 'translation' | 'rotation' | 'scale' | 'weights';\n /** Sampler driving this channel. */\n readonly sampler: AnimationSampler;\n}\n\n/**\n * Animation sampler — keyframe curve for a single animation-target-property pair.\n *\n * `input` and `output` are Float32Arrays of equal length\n * (`output.length = input.length * elementCount`) where elementCount is:\n * - 3 for 'translation' / 'scale' (vec3)\n * - 4 for 'rotation' (quat)\n * - targetCount for 'weights' (morph weights)\n *\n * `interpolation` is restricted to LINEAR and STEP per D-1 scope;\n * CUBICSPLINE is deferred to OOS-skin-cubicspline (fail-fast at importer).\n */\nexport interface AnimationSampler {\n readonly input: Float32Array;\n readonly output: Float32Array;\n readonly interpolation: 'LINEAR' | 'STEP';\n}\n\n/**\n * Animation clip asset POD shape.\n *\n * `duration` is max(sampler.input[last]) across all channels — the\n * longest channel defines the clip length. Each channel targets one\n * animation-target property, resolved during playback by `targetId`.\n */\nexport interface AnimationClip {\n readonly kind: 'animation-clip';\n readonly duration: number;\n readonly channels: readonly AnimationChannel[];\n}\n\n// === AnimationGraph asset POD + node union (feat-20260713 M2 / w13) ==============\n//\n// Decision anchors:\n// - requirements AC-02 (declarative Clip/Blend/Add + nesting graph carried as\n// a shared<AnimationGraph> asset handle, multi-entity shared).\n// - requirements AC-14 (AnimationGraph joins the closed `Asset` union, owns a\n// GUID, and serializes into pack/scene round-trip — foundation landed here,\n// the serialize/deserialize mechanism itself is M4).\n// - requirements OOS-1/OOS-2/OOS-3/OOS-4 (node union is CLOSED at three\n// variants — clip / blend / add. No FSM state/transition, no bone Mask, no\n// built-in transition layer, no BlendSpace fields are reserved; deferred\n// features add nodes in a future closed loop, not speculative fields now —\n// charter F4 \"no unvalidated abstraction\").\n// - requirements OOS-7 (POD carries only the topology of an engine-authored\n// defineAnimationGraph graph; no DCC import metadata).\n// - plan-strategy D-4 (POD + node union land in types/index.ts, the single-file\n// SSOT for every Asset POD, alongside AnimationClip).\n//\n// The POD mirrors AnimationClip: no inline `guid` field — the GUID is assigned by\n// the AssetRegistry / shared-handle system when the graph is registered (like\n// AnimationClip / MaterialAsset / VideoAsset). Nodes are stored flat in `nodes[]`\n// and referenced by index; `root` is the index of the output node. Clip leaves\n// carry a durable GUID string; the runtime consumer resolves that GUID to a\n// World-local transient shared handle only at evaluation time.\n\n/**\n * Clip leaf node — samples a single `shared<AnimationClip>` at the node's\n * runtime seek-time (M3 evaluation). `weight` is the node's STATIC weight; the\n * effective weight is `runtime weight x static weight` (requirements AC-07\n * orthogonal product).\n */\nexport interface AnimationGraphClipNode {\n readonly type: 'clip';\n readonly clip: string;\n readonly weight: number;\n}\n\n/**\n * Blend node — normalizing lerp over its children (requirements AC-04). Child\n * effective weights are normalized so they sum to 1 at evaluation. `children`\n * are indices into the parent {@link AnimationGraph.nodes} array.\n */\nexport interface AnimationGraphBlendNode {\n readonly type: 'blend';\n readonly children: readonly number[];\n readonly weight: number;\n}\n\n/**\n * Add node — non-normalizing additive stack (requirements AC-05). The `base`\n * child contributes its effective weight unchanged; each `additive` layer is\n * added on top WITHOUT normalization (total may exceed 1). `base` and\n * `additive` are indices into {@link AnimationGraph.nodes}.\n */\nexport interface AnimationGraphAddNode {\n readonly type: 'add';\n readonly base: number;\n readonly additive: readonly number[];\n readonly weight: number;\n}\n\n/**\n * Closed node union — exactly three variants (clip / blend / add). AI users\n * exhaustive `switch (node.type)` without default; TS guards completeness\n * (charter P3). No FSM / Mask / transition / BlendSpace variants are reserved\n * (OOS-1..4).\n */\nexport type AnimationGraphNode =\n | AnimationGraphClipNode\n | AnimationGraphBlendNode\n | AnimationGraphAddNode;\n\n/**\n * AnimationGraph asset POD — a Clip/Blend/Add DAG with per-node static weights.\n *\n * Joins the closed `Asset` union with `kind: 'animation-graph'` (AC-14); minted\n * into a `Handle<'AnimationGraph', 'shared'>` via `world.allocSharedRef` /\n * `AssetRegistry`, shared across multiple entities. Constructed via\n * `defineAnimationGraph` (runtime), which validates topology (no out-of-range\n * refs / cycles / invalid weights / empty graph) before a handle is minted.\n */\nexport interface AnimationGraph {\n readonly kind: 'animation-graph';\n readonly nodes: readonly AnimationGraphNode[];\n readonly root: number;\n}\n\n// === VideoAsset POD shape (feat-20260623-world-space-video-asset M1) ==========\n//\n// Decision anchors:\n// - requirements AC-01 (VideoAsset is Asset closed-union 15th member;\n// kind discriminator 'video'; payload { url: string }, no width/height/duration).\n// - requirements constraint: payload must not inline video bytes, only a URL descriptor.\n// - plan-strategy D-4 (VideoAsset descriptor naming aligns with TextureAsset/AudioClipAsset).\n// - charter F1 (AI users discover the schema via IDE autocomplete on the closed\n// `Asset` union + `Handle<VideoAsset>` returns from `AssetRegistry.register`).\n// - plan-strategy D-5 (resolveTexLike identifies video kind via `payload.kind === 'video'`;\n// video does not masquerade as 'texture').\n//\n// `refs` is always empty (isolated leaf) — VideoAsset carries no sub-asset\n// references (plan-strategy S6.3). The `url` field points to an external video\n// file; the runtime resolves it into an HTMLVideoElement via the host-provided\n// `VideoElementProvider` World Resource (plan-strategy D-1).\n\n/**\n * Video asset POD shape -- pure `{url}` descriptor.\n *\n * `VideoAsset` is a runtime-only asset kind (OOS-1: no import/cook pipeline).\n * The `url` field points to an external video file (e.g. `*.webm` / `*.mp4`);\n * the engine does NOT decode video bytes -- it delegates to the host-side\n * `HTMLVideoElement` via `VideoElementProvider` (plan-strategy D-1).\n *\n * `width` / `height` / `duration` are deliberately absent from the POD:\n * the runtime reads them from `HTMLVideoElement.videoWidth` /\n * `videoHeight` / `duration` after `loadedmetadata` fires (requirements\n * constraint \"payload must not inline video bytes\").\n *\n * Consumers reference a VideoAsset via a material texture value\n * fields (e.g. `baseColorTexture`), sharing the same `texture2d` slot with\n * static textures (charter P4 consistent abstraction). The extraction layer\n * (render-system-extract `resolveTexLike`) identifies the video kind and\n * routes to the per-frame transient texture pathway instead of the static\n * `GpuResourceStore.ensureResident` cache (plan-strategy D-5).\n */\nexport interface VideoAsset {\n readonly kind: 'video';\n readonly url: string;\n}\n\n/**\n * Skeleton asset POD shape — pure rig data, no mesh attachment.\n *\n * `inverseBindMatrices` is a Float32Array of length jointCount * 16\n * (column-major mat4 per joint). Missing IBM in source glTF is filled\n * with identity mat4 at importer time.\n *\n * `jointCount` is the number of joints (= IBM array length / 16).\n * Keys off the glTF skin's `joints[]` array length; validated against\n * MAX_JOINTS (256) at importer time.\n */\nexport interface SkeletonAsset {\n readonly kind: 'skeleton';\n readonly inverseBindMatrices: Float32Array;\n readonly jointCount: number;\n}\n\n/**\n * Skin sub-asset — the binding between a skeleton and a scene node hierarchy.\n *\n * `skeletonGuid` references a SkeletonAsset by GUID (string form).\n * `jointPaths` is a parallel array to the skeleton's joints; each entry\n * is a Name-component path from scene root to the joint entity, used\n * at post-spawn time to populate Skin.joints: Entity[].\n *\n * Zero-Entity-reference at the asset layer (AC-06): no Entity or\n * LocalEntityId fields — the binding is name-based, resolved at instantiate time.\n */\nexport interface SkinAsset {\n readonly kind: 'skin';\n readonly skeletonGuid: string;\n readonly jointPaths: readonly string[];\n}\n\n/**\n * VertexAttributeMap — 14-key closed set (feat-20260823 vertex-color asset closure).\n *\n * Canonical interleaved order (plan-strategy F-1, must match bridge + layout layers):\n * position / normal / uv / tangent / skinIndex / skinWeight / uv1..uv7 / color\n *\n * @location mapping per plan-strategy D-4:\n * position@0 normal@1 uv@2 tangent@3 skinIndex@4 skinWeight@5\n * uv1@6 uv2@7 uv3@8 uv4@9 uv5@10 uv6@11 uv7@12 color@13\n *\n * Keys align with Three.js r184 `BufferGeometry.attributes` naming (D-P1 +\n * plan-strategy §7.2 mental migration stance). Importers rename at ingest\n * (`POSITION -> position` / `TEXCOORD_0 -> uv` / `JOINTS_0 -> skinIndex` /\n * `WEIGHTS_0 -> skinWeight`) so the runtime key space remains lowercase.\n *\n * All 14 keys are optional; a mesh with only `position` (static unlit) is\n * valid. Values accept the three common binary shapes:\n * `ArrayBuffer | Float32Array | Uint16Array` (extend only via minor add per\n * the closed-union evolution contract).\n *\n * AC-15 narrowing: consumer sites writing\n * `for (const [key, buffer] of Object.entries(attributes))` observe `key`\n * typed as the 14-member literal union without `as` casts; any typo (e.g.\n * `'POSITION'`) is a TS compile-time error.\n */\nexport interface VertexAttributeMap {\n position?: ArrayBuffer | Float32Array | Uint16Array;\n normal?: ArrayBuffer | Float32Array | Uint16Array;\n uv?: ArrayBuffer | Float32Array | Uint16Array;\n tangent?: ArrayBuffer | Float32Array | Uint16Array;\n skinIndex?: ArrayBuffer | Float32Array | Uint16Array;\n skinWeight?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 1 (feat-20260629-multi-uv-set-support m3-w3, pre-added by M1 for bridge typecheck). */\n uv1?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 2 */\n uv2?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 3 */\n uv3?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 4 */\n uv4?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 5 */\n uv5?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 6 */\n uv6?: ArrayBuffer | Float32Array | Uint16Array;\n /** UV set 7 */\n uv7?: ArrayBuffer | Float32Array | Uint16Array;\n /** Optional per-vertex linear RGBA color, exactly four finite floats per vertex. */\n color?: Float32Array;\n}\n\n/** Closed storage vocabulary used by the geometry pack boundary. */\nexport type VertexAttributeStorage = 'array-buffer' | 'float32' | 'uint16' | 'other';\n\n/** Lossless detail for one rejected canonical vertex attribute pack. */\nexport type VertexAttributePackDetail =\n | {\n readonly field: 'vertexCount';\n readonly reason: 'vertex-count-invalid';\n readonly actual: number;\n }\n | {\n readonly field: 'attributes';\n readonly reason: 'attributes-empty';\n readonly actualCount: 0;\n }\n | {\n readonly field: keyof VertexAttributeMap;\n readonly reason: 'attribute-storage-invalid';\n readonly expectedStorage: 'float32' | 'uint16';\n readonly actualStorage: VertexAttributeStorage;\n }\n | {\n readonly field: keyof VertexAttributeMap;\n readonly reason: 'attribute-cardinality-mismatch';\n readonly vertexCount: number;\n readonly componentsPerVertex: number;\n readonly expectedLength: number;\n readonly actualLength: number;\n }\n | {\n readonly field: keyof VertexAttributeMap;\n readonly reason: 'attribute-non-finite';\n readonly elementIndex: number;\n readonly actual: 'nan' | 'positive-infinity' | 'negative-infinity';\n };\n\n/**\n * Canonical UV attribute key order (set 0 = `uv`, sets 1..7 = `uv1..uv7`),\n * matching the VertexAttributeMap declaration + @location numbering (D-4).\n * SSOT for \"how many UV sets does this attribute map carry\" so the import,\n * gpu-resource, and layout-derivation layers count identically (no drift).\n */\nexport const UV_ATTRIBUTE_KEYS = ['uv', 'uv1', 'uv2', 'uv3', 'uv4', 'uv5', 'uv6', 'uv7'] as const;\n\n/**\n * Structural input for the UV-set counters: any object that may carry the\n * canonical UV attribute keys. Both the loosely-typed mesh-attribute record\n * (`Record<string, unknown>`) and the closed `VertexAttributeMap` satisfy it,\n * so the import / gpu-resource / layout layers all call one counter.\n */\nexport type UvAttributeSource = Partial<Record<(typeof UV_ATTRIBUTE_KEYS)[number], unknown>>;\n\n/**\n * Total number of UV sets present in an attribute map: the highest populated\n * `uv`/`uv1..uv7` index + 1, or 0 when none are present. A populated key is one\n * whose value is a typed array / array buffer / number array (the binary shapes\n * a vertex attribute can take). This is the single counter all multi-UV layers\n * derive from (feat-20260629 F-4 DRY collapse).\n */\nexport function countUvSets(attrs: UvAttributeSource | undefined): number {\n if (attrs === undefined) return 0;\n for (let i = UV_ATTRIBUTE_KEYS.length - 1; i >= 0; i--) {\n // biome-ignore lint/style/noNonNullAssertion: bounded index on const tuple\n const v = attrs[UV_ATTRIBUTE_KEYS[i]!];\n if (\n v instanceof Float32Array ||\n v instanceof Uint16Array ||\n v instanceof ArrayBuffer ||\n Array.isArray(v)\n ) {\n return i + 1;\n }\n }\n return 0;\n}\n\n/**\n * Extra UV sets beyond set 0 (= `countUvSets - 1`, floored at 0). The\n * gpu-resource + register layers size the dynamic vertex stride from this.\n */\nexport function countExtraUvSets(attrs: UvAttributeSource | undefined): number {\n const total = countUvSets(attrs);\n return total > 0 ? total - 1 : 0;\n}\n\n// === Asset error model SSOT (feat-20260511-asset-system-v1 / D-P1 / w3) =========\n//\n// Decision anchors:\n// - requirements §G3 + AC-03 + AC-10 + AC-21 + §1 callout row 9 (4-member closed\n// `AssetErrorCode` independent from `RhiErrorCode`; `AssetError` class with\n// `.code / .expected / .hint / .message` four-field surface structurally\n// parallel to `RhiError` / `InspectorError` / `MetricError`)\n// - plan-strategy §2 D-P1 (`@forgeax/engine-types` single-file SSOT for\n// AssetErrorCode; independent closed union aligned with\n// MetricErrorCode / InspectorErrorCode precedent)\n// - plan-strategy §7.3 (per-code `.hint` string literals locked verbatim\n// below; any drift updates both this module and the test fixtures)\n// - charter proposition 3 (machine-readable union > prose) + proposition 4\n// (explicit failure — `switch (err.code)` exhaustive without `default:`) +\n// proposition 5 (consistent abstraction — structurally parallel to\n// `@forgeax/engine-rhi` `RhiError` surface)\n// - architecture-principles #1 SSOT (the 4 literals + class shape live here\n// once; M4 AssetRegistry / M3 Geometry factories / AGENTS.md §Error model\n// row all reference this module)\n\n/**\n * Closed `AssetErrorCode` union — 23 members (D-P1 + feat-20260518 D-1 minor\n * evolution + feat-20260520-skylight-ibl-cubemap 5 members +\n * feat-20260523 mesh-upload-fix 1 member +\n * feat-20260523-shader-template-instance-split M1-T02 1 member +\n * feat-20260526-material-asset-multipass-renderstate M1 1 member +\n * feat-20260603-asset-import-loader-injection M1 2 members +\n * feat-20260604-hdr-equirect-cube-importer-loader M2 1 member +\n * feat-20260608-mesh-multi-section-primitive-multi-material-slot M1 3 members +\n * feat-20260621-asset-registry-robustness-invalidate-inflight-cach M2 1 member +\n * feat-20260707-texture-block-compression M5 1 member\n * (mipgen-unsupported-compressed-format);\n * requirements §G3 + AC-03 + AC-21 +\n * feat-20260518 AC-02 + bug-20260523 AC-01. The runtime-guard SSOT for the\n * current member count is the ASSET_ERROR_HINTS key-count test, not this prose.)\n * Exhaustive `switch (err.code)` needs no default fallback — TypeScript guards\n * union completeness at compile time (charter F2/P2 machine-readable union >\n * prose + P3 explicit failure).\n *\n * Domain-separated from `RhiErrorCode 'asset-not-registered'` (which is a\n * render-time registry lookup miss, 18-member closed union in\n * `@forgeax/engine-rhi/src/errors.ts`). The two unions cover disjoint\n * lifecycle phases — AI users face only these 22 alternatives on the\n * `engine.assets.loadByGuid(guid)` / `engine.assets.get(handle)` /\n * `engine.assets.register(payload).unwrap()` surface.\n *\n * | code | trigger |\n * |:--|:--|\n * | `'asset-not-found'` | `AssetRegistry.get(handle)` returned no entry (handle never registered or registry was reset); charter P3 explicit failure. |\n * | `'asset-parse-failed'` | decoded bytes are not a valid image (PNG / JPG header corruption on the load path; dimensions <= 0 / segments < 1 on the procedural geometry constructor path — double semantics locked by requirements §9 \"constructor path\" extension). |\n * | `'asset-format-unsupported'` | URL content-type / magic bytes are neither PNG nor JPG (v1 scope; KTX2 / Basis / GLTF embedded textures deferred to M3+). |\n * | `'asset-fetch-failed'` | `fetch(url)` returned non-2xx, threw, or the URL was otherwise unreachable (404 / network / CORS surface). |\n * | `'asset-invalid-value'` | `register<MaterialAsset>(payload)` value validation fails the 3-tier validator (type-mismatch / extra-key / missing-required) — fail-fast at register entry. |\n * | `'cubemap-handle-missing'` | internal equirect-to-cubemap projection has no live cubemap for a `Skylight.equirect` handle; `.hint` points to loading an EquirectAsset + caps.rgba16floatRenderable. |\n * | `'invalid-source-format'` | image importer path when `.hdr` decode needs rgba16float / rgba32float. |\n * | `'load-failed'` | `loadByGuid` when guid entry exists in catalog but file is inaccessible. |\n * | `'device-unsupported'` | GPU capability gate: `device.caps.rgba16floatRenderable` missing. |\n * | `'ibl-precompute-not-dispatched'` | `IblPipelineCache` when counter increments but `queue.submit` hasn't been called. |\n * | `'mesh-vertex-stride-mismatch'` | `register({ kind: 'mesh', ... })` vertices buffer is not evenly divisible by 12 floats per vertex (position vec3 + normal vec3 + uv vec2 + tangent vec4) or `maxIndex+1 !== vertexCount` — fail-fast at register entry (charter P3 structured failure; `.detail` carries `vertexCount` / `floatsPerVertex`). |\n| `'material-circular-inheritance'` | material resolve detected a cycle in the parent chain; `.hint` carries the full cycle path (e.g. \"A -> B -> A\") via `err.detail.cycle`. |\n * | `'loader-not-registered'` | `loadByGuid` dispatched on `asset.kind` but the injected `LoaderRegistry` has no loader for that kind; `.detail.kind` is the missing kind and `.detail.registeredKinds` lists the kinds currently wired (feat-20260603-asset-import-loader-injection M1; charter P3 — AI users read `.detail.registeredKinds` to know what to inject). |\n * | `'asset-not-imported'` | `loadByGuid` found the GUID in the catalog but its DDC is absent and no `ImportTransport` is wired (shipped form); `.hint` points back to build-time pre-import rather than a runtime workaround (feat-20260603-asset-import-loader-injection M4; logic wired in M4 w31). |\n * | `'texture-source-not-imported'` | `loadTextureAsset` received an uncooked source locator instead of a Pack v2 artifact; the runtime carries no source decoder. |\n */\nexport type AssetErrorCode =\n | 'asset-not-found'\n | 'asset-parse-failed'\n | 'asset-format-unsupported'\n | 'asset-fetch-failed'\n | 'catalog-source-unconfigured'\n | 'asset-invalid-value'\n | 'cubemap-handle-missing'\n | 'invalid-source-format'\n | 'load-failed'\n | 'device-unsupported'\n | 'ibl-precompute-not-dispatched'\n | 'mesh-vertex-stride-mismatch'\n // === 1 new code (feat-20260523-shader-template-instance-split M1-T02) ===\n | 'material-shader-ref-broken'\n // === 1 new code (feat-20260526-material-asset-multipass-renderstate M1 / w6) ===\n | 'material-circular-inheritance'\n // === 2 new codes (feat-20260603-asset-import-loader-injection M1 / w1) ===\n | 'loader-not-registered'\n | 'asset-not-imported'\n // === 1 new code (feat-20260604-hdr-equirect-cube-importer-loader M2 / w4) ===\n | 'texture-source-not-imported'\n // === 1 new code (perf-20260706-raw-container-failfast) ===\n // A mesh/material/scene/skeleton/skin/animation-clip catalog row whose\n // packageUrl is still a raw source container (.glb/.gltf/.fbx), not an\n // importer-produced artifact (.bin/.pack.json). Like texture-source-not-imported\n // this is transport-eligible: the studio form lazily imports via the injected\n // ImportTransport; the shipped form fails fast. Distinct from the generic\n // asset-not-imported so it never masks the parent-missing breadcrumb.\n | 'source-not-imported'\n // === 3 new codes (feat-20260608-mesh-multi-section-primitive-multi-material-slot M1 / w2) ===\n | 'mesh-renderer-material-override-invalid'\n | 'mesh-renderer-material-override-overflow'\n | 'mesh-asset-submeshes-empty'\n | 'mesh-asset-material-slot-index-out-of-range'\n | 'mesh-submesh-index-range-out-of-bounds'\n // === 1 new code (feat-20260608-tilemap-object-layer-rendering M0 baseline rebuild) ===\n // Tileset region rectangle out of atlas extent OR tile entry regionIndex out of\n // regions array bounds (single closed code per plan-strategy §D-6 first-error\n // ordering). 19 -> 20 baseline-restored.\n | 'tileset-region-index-out-of-range'\n // === 1 new code (feat-20260629-multi-uv-set-support M2 / m2-w5) ===\n // mesh-bin header v2 contract violation: version unknown, uvSetCount out of\n // [0,8], or stride/floatsPerVertex self-consistency check failed at encode\n // exit or decode entry (Fail Fast). Carries detail { version, uvSetCount,\n // stride }. .hint = 're-cook the asset via importer'.\n | 'mesh-bin-contract-violation'\n // === 1 new code (feat-20260608-tilemap-object-layer-rendering M1 schema extension) ===\n // Tile entry optional field (widthCells / heightCells / pivotX / pivotY /\n // collider) or top-level atlases / region.atlasIndex schema invariant\n // breached at register time. `.detail.field` carries the closed 7-variant\n // enum + `.scope?` is 'tile-entry' | 'tileset-asset' (plan-strategy §D-6;\n // charter P3 closed enum + AI-grep affordance). 20 -> 21 M1 net add.\n | 'tileset-tile-entry-malformed'\n // === 1 new code (feat-20260621-asset-registry-robustness-invalidate-inflight-cach M2 / w4) ===\n | 'asset-invalidated'\n // === 1 new code (feat-20260707-texture-block-compression M5 / w35, D-9) ===\n // deriveRenderDataTexture fail-fast: a block-compressed `format` requested\n // RUNTIME mip generation (`mipmap:true` with no offline `mipLevelCount>1`\n // chain). Compressed formats are not render targets, so the mipmap blit\n // pipeline cannot generate their mips (F-7); the chain must be baked offline.\n // `.hint` carries the self-recovery (bake offline mips, or set the sidecar\n // `compressionMode:'none'`). A compressed texture whose mips are ALREADY in\n // `data` (mipLevelCount>1 from a KTX2 level chain) does NOT trip this gate.\n | 'mipgen-unsupported-compressed-format';\n\n/**\n * Structured asset error -- four-field surface (`.code` / `.expected` /\n * `.hint` / `.message`) structurally parallel to `@forgeax/engine-rhi`\n * `RhiError` + `@forgeax/engine-remote` `InspectorError` + `MetricError`\n * (charter proposition 5 consistent abstraction; AGENTS.md \"Errors are\n * structured. Return Result, never throw for expected failures.\").\n *\n * AI users consume the structured triple via property access:\n * `switch (err.code) { case 'asset-fetch-failed': ... err.hint ... }`\n * -- never by parsing `.message` (charter proposition 4 explicit failure\n * red line).\n *\n * The `.message` field is auto-composed for human stack traces and carries\n * the same content as `.code` + `.expected` + `.hint`; AI users prefer\n * field access on the structured triple.\n *\n * @example AI-user exhaustive switch on the 22 members (no default fallback)\n * ```ts\n * import { AssetError, type AssetErrorCode } from '@forgeax/engine-types';\n *\n * function recover(code: AssetErrorCode): string {\n * switch (code) {\n * case 'asset-not-found': return 'ensure handle was registered before get()';\n * case 'asset-parse-failed': return 'check file integrity or geometry dimensions';\n * case 'asset-format-unsupported': return 'convert to PNG or JPG';\n * case 'asset-fetch-failed': return 'check url path or dev server';\n * case 'asset-invalid-value': return 'read err.hint / err.detail for the case-specific fix';\n * }\n * }\n * ```\n */\nexport class AssetError extends Error {\n readonly code: AssetErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail?: Readonly<AssetErrorDetail>;\n\n constructor(args: {\n code: AssetErrorCode;\n expected: string;\n hint: string;\n detail?: Readonly<AssetErrorDetail>;\n }) {\n super(`[AssetError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);\n this.name = 'AssetError';\n this.code = args.code;\n this.expected = args.expected;\n this.hint = args.hint;\n if (args.detail !== undefined) {\n this.detail = args.detail;\n }\n }\n}\n\n/**\n * Per-code `.hint` string literals (plan-strategy §7.3 lock-in). Exported\n * so M3 Geometry factories / M4 AssetRegistry / tests consume the same\n * SSOT — any drift here updates both producer call sites and the\n * AGENTS.md §Error model table.\n *\n * The shape is a `Record<AssetErrorCode, string>` so future additions to\n * the closed union are a compile-time error here as well (reinforces\n * charter proposition 4 explicit failure).\n */\nexport const ASSET_ERROR_HINTS: Readonly<Record<AssetErrorCode, string>> = {\n 'asset-fetch-failed':\n 'check url path; verify dev server is running; in tests use data: URL fixture (data:image/png;base64,...)',\n 'catalog-source-unconfigured':\n 'call AssetRegistry.setCatalogSource(source) before enumerateCatalog(), then retry the operation',\n 'asset-parse-failed':\n 'check file bytes are not corrupted; for procedural geometry: verify all dimensions > 0 and segments >= 1',\n 'asset-format-unsupported':\n 'v1 supports png/jpg only; convert .bmp/.webp etc. via image tooling; gltf/glb supported via @forgeax/engine-gltf importer (forgeax-engine-remote-gltf import <gltf-or-glb>)',\n 'asset-not-found':\n 'handle id not in registry; verify register() was called before get(); inspect() returns all live handles',\n 'asset-invalid-value':\n 'a register-time value failed validation; read err.hint for the case-specific fix (e.g. clamp a MaterialAsset param to [0,1], or give a strip-topology MeshAsset an index buffer) and err.detail for the offending field/value',\n 'cubemap-handle-missing':\n 'the equirect-to-cubemap projection (internal to the render-system record arm) has no live cubemap for this Skylight; ensure Skylight.equirect references a loaded EquirectAsset handle and caps.rgba16floatRenderable is true',\n 'invalid-source-format':\n 'decode .hdr via @forgeax/engine-image first; supported formats are rgba16float and rgba32float',\n 'load-failed':\n 'source asset could not be loaded; check GUID validity and file accessibility in the pack-index catalog',\n 'device-unsupported':\n 'GPU device lacks required capability; check device.caps for rgba16float renderable feature',\n 'ibl-precompute-not-dispatched':\n 'check IblPipelineCache.runIblPrecompute is called inside the internal GpuResourceStore equirect-to-cubemap projection; counters must not increment before queue.submit (plan D-7 / N-3 AC-20 invariant)',\n 'mesh-vertex-stride-mismatch':\n 'use meshFromInterleaved (packages/runtime/src/geometry/box.ts) or expand vertices buffer to canonical 12F layout (position vec3 + normal vec3 + uv vec2 + tangent vec4)',\n // === 1 new hint (feat-20260523-shader-template-instance-split M1-T02) ===\n 'material-shader-ref-broken':\n 'the materialShader identifier (path or GUID) resolves to no registered shader; check ShaderRegistry for path identifiers or AssetRegistry for GUID sub-assets',\n // === 1 new hint (feat-20260526-material-asset-multipass-renderstate M1 / w6) ===\n 'material-circular-inheritance':\n 'circular parent chain detected; inspect parent handles — use err.detail.cycle to see the full path (e.g. \"A -> B -> A\")',\n // === 2 new hints (feat-20260603-asset-import-loader-injection M1 / w1) ===\n 'loader-not-registered':\n 'no loader registered for this asset kind; register it via engine.assets.loaders.register(loader) (the loader carries its own kind); err.detail.registeredKinds lists the kinds currently wired',\n 'asset-not-imported':\n 'GUID is in the catalog but its DDC artefact is missing and no ImportTransport is wired (shipped form never falls back to a runtime import); add the asset to the build-time pre-import step instead of importing at runtime',\n // === 1 new hint (feat-20260604-hdr-equirect-cube-importer-loader M2 / w4) ===\n 'texture-source-not-imported':\n 'texture source not imported yet; wire createDevImportTransport() in the studio form for dev lazy-import, or pre-import via the build-time pipeline',\n // === 1 new hint (perf-20260706-raw-container-failfast) ===\n 'source-not-imported':\n 'this mesh/material/scene sub-asset row still points at the raw source container (.glb/.gltf/.fbx); wire createDevImportTransport() for dev lazy-import (POST /__import), or pre-import via the build-time pipeline. The runtime does not parse raw containers at load time.',\n // === 3 new hints (feat-20260608-mesh-multi-section-primitive-multi-material-slot M1 / w2) ===\n 'mesh-renderer-material-override-invalid':\n 'a MeshRenderer.materials slot is stale or does not resolve to a MaterialAsset; the renderer inherited the MeshAsset default for that slot',\n 'mesh-renderer-material-override-overflow':\n 'MeshRenderer.materials contains entries beyond MeshAsset.materialSlots; extra overrides are ignored',\n 'mesh-asset-submeshes-empty':\n 'MeshAsset.submeshes must have at least one entry; every mesh must declare at least one submesh; check MeshAsset registration payload for empty submeshes array',\n 'mesh-asset-material-slot-index-out-of-range':\n 'MeshAsset submesh materialSlot must index MeshAsset.materialSlots; re-cook the mesh and inspect the offending submesh/slot topology',\n 'mesh-submesh-index-range-out-of-bounds':\n 'submesh indexOffset + indexCount exceeds the parent mesh index buffer length; check submesh index range bounds against MeshAsset.indices and MeshAsset.vertices; err.detail carries submeshIndex, indexOffset, indexCount, indexBufferLength, and meshAssetGuid',\n // === 1 new hint (feat-20260608-tilemap-object-layer-rendering M0 baseline rebuild) ===\n 'tileset-region-index-out-of-range':\n 'a TilesetAsset.regions[] rectangle escapes the atlas extent OR a TilesetAsset.tiles[].regionIndex points past TilesetAsset.regions.length; check regions[i] (x + width <= atlasWidth, y + height <= atlasHeight) and tiles[i].regionIndex in [0, regions.length); err.detail carries tilesetGuid, tileId, regionIndex, regionCount',\n // === 1 new hint (feat-20260608-tilemap-object-layer-rendering M1 schema extension) ===\n 'tileset-tile-entry-malformed':\n 'a TilesetTileEntry optional field is out of range (widthCells / heightCells in (0, 64], pivotX / pivotY in [0, 1], collider rect/polygon in normalized [0,1]^2 with rect.length === 4 and polygon.points.length >= 3) OR a top-level field is out of range (atlases.length >= 1, region.atlasIndex in [0, atlases.length)); engine fail-fast at register-time. read err.detail.field (closed enum) + err.detail.scope (tile-entry | tileset-asset) + err.detail.tileEntryIndex to locate the offending entry; switch (err.detail.field) covers the 7 variants exhaustively without default',\n // === 1 new hint (feat-20260621-asset-registry-robustness-invalidate-inflight-cach M2 / w4) ===\n 'asset-invalidated':\n 'The asset was invalidated during load; call loadByGuid(guid) again to retry with a fresh fetch',\n // === 1 new hint (feat-20260629-multi-uv-set-support M2 / m2-w5) ===\n 'mesh-bin-contract-violation':\n 're-cook the asset via importer; the .bin sidecar v4 contract is violated — inspect err.detail.reason and its expected/actual projection, stride, cardinality, and byte-length facts',\n // === 1 new hint (feat-20260707-texture-block-compression M5 / w35, D-9) ===\n 'mipgen-unsupported-compressed-format':\n 'compressed-texture mips must be baked offline (the GPU cannot generate mips for a non-render-target block format); re-cook with an offline mip chain, or set the sidecar .meta.json compressionMode:\"none\" (or mipmap:false) to keep runtime mip generation on an uncompressed texture',\n};\n\n// === Font error model SSOT (feat-20260531-world-space-msdf-text-rendering M2 / w6) ===\n//\n// Decision anchors:\n// - plan-strategy D-11 (two closed unions: FontErrorCode = build/load phase,\n// TextErrorCode = runtime layout phase; structured .code/.expected/.hint/.detail;\n// TOFU is rendering behaviour not an error — AC-14)\n// - requirements AC-15 (non-TTF -> FontErrorCode 'unsupported-font-format')\n// - requirements AC-16 (both unions in types/src/index.ts; exhaustive\n// switch(err.code) without default compiles)\n// - requirements AC-20 (font concurrency > 8 -> TextErrorCode\n// 'font-concurrency-exceeded')\n// - charter P3 (explicit failure: structured error > silent behaviour,\n// D-8 rejects silent LRU eviction for concurrency violation)\n//\n// Domain separation: FontErrorCode covers build-time bake failures and\n// load-time atlas/sampler resolution; TextErrorCode covers runtime glyph\n// layout and text rendering failures.\n\n/**\n * Closed `FontErrorCode` union — build-time bake + load-time resolution\n * errors (plan-strategy D-11).\n *\n * | code | trigger |\n * |:--|:--|\n * | `'unsupported-font-format'` | bake receives non-TTF input (OTF / WOFF2); `.expected: 'ttf'` (AC-15) |\n * | `'font-atlas-missing'` | loadByGuid font handle has missing/empty atlas texture GUID |\n * | `'font-atlas-corrupted'` | sidecar JSON parse failed or glyph metrics shape invalid |\n * | `'bake-failed'` | @zappar/msdf-generator call threw (wasm unavailable / internal error) |\n */\nexport type FontErrorCode =\n | 'unsupported-font-format'\n | 'font-atlas-missing'\n | 'font-atlas-corrupted'\n | 'bake-failed';\n\n/**\n * Closed `TextErrorCode` union — runtime glyph layout and text rendering\n * errors (plan-strategy D-11).\n *\n * | code | trigger |\n * |:--|:--|\n * | `'font-concurrency-exceeded'` | > 8 distinct FontAsset handles active in one frame; `.expected: 8` (AC-20) |\n * | `'font-atlas-missing'` | glyph layout system resolved fontHandle but atlas texture is not yet uploaded |\n * | `'glyph-layout-failed'` | layout computation encountered unexpected state (empty common block, etc.) |\n */\nexport type TextErrorCode =\n | 'font-concurrency-exceeded'\n | 'font-atlas-missing'\n | 'glyph-layout-failed';\n\n/**\n * Structured font error — four-field surface (`.code` / `.expected` /\n * `.hint` / `.message`) in the style of {@link AssetError}.\n *\n * AI users consume via property access:\n * `switch (err.code) { case 'unsupported-font-format': ... err.expected ... }`\n * — never by parsing `.message`.\n */\nexport class FontError extends Error {\n readonly code: FontErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail?: Readonly<Record<string, unknown>>;\n\n constructor(args: {\n code: FontErrorCode;\n expected: string;\n hint: string;\n detail?: Readonly<Record<string, unknown>>;\n }) {\n super(`[FontError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);\n this.name = 'FontError';\n this.code = args.code;\n this.expected = args.expected;\n this.hint = args.hint;\n if (args.detail !== undefined) {\n this.detail = args.detail;\n }\n }\n}\n\n/**\n * Structured text error — four-field surface (`.code` / `.expected` /\n * `.hint` / `.message`) in the style of {@link AssetError}.\n *\n * AI users consume via property access:\n * `switch (err.code) { case 'font-concurrency-exceeded': ... err.hint ... }`\n * — never by parsing `.message`.\n */\nexport class TextError extends Error {\n readonly code: TextErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail?: Readonly<Record<string, unknown>>;\n\n constructor(args: {\n code: TextErrorCode;\n expected: string;\n hint: string;\n detail?: Readonly<Record<string, unknown>>;\n }) {\n super(`[TextError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);\n this.name = 'TextError';\n this.code = args.code;\n this.expected = args.expected;\n this.hint = args.hint;\n if (args.detail !== undefined) {\n this.detail = args.detail;\n }\n }\n}\n\n// === AssetErrorDetail discriminated union (feat-20260523-shader-template-instance-split M1-T02) ===\n//\n// Introduced to type-narrow the AssetError.detail field for the new\n// 'material-shader-ref-broken' variant. Existing AssetErrorCode members\n// keep their Record<string, unknown> detail shapes; the union is\n// backward-compatible because the detail field on AssetError is optional.\n\n/**\n * Detail for `material-shader-ref-broken` — materialShader identifier\n * (path or GUID) could not be resolved to a registered shader.\n */\nexport interface AssetMaterialShaderRefBrokenDetail {\n readonly code: 'material-shader-ref-broken';\n readonly materialAssetGuid: string;\n readonly missingShaderId: string;\n readonly materialShaderPath?: string;\n}\n\n/**\n * Detail for `tileset-region-index-out-of-range` (feat-20260608 M0 baseline rebuild).\n *\n * Carries the offending tileset GUID + tile-entry index + the rejected\n * `regionIndex` + the live `regionCount` so AI consumers can pinpoint\n * the malformed payload field via property access (charter P3 / P4).\n *\n * Surfaced by `validateTilesetPayload` along two paths:\n * - region rectangle escapes the parent atlas extent\n * (regionIndex == the offending rectangle index).\n * - `tiles[i].regionIndex` >= `regions.length` (or negative)\n * (tileId encodes which entry; regionIndex carries the rejected value).\n */\nexport interface AssetTilesetRegionIndexOutOfRangeDetail {\n readonly code: 'tileset-region-index-out-of-range';\n readonly tilesetGuid: string;\n readonly tileId: number;\n readonly regionIndex: number;\n readonly regionCount: number;\n}\n\n/**\n * Detail for `tileset-tile-entry-malformed` (feat-20260608 M1 schema\n * extension; plan-strategy §D-6).\n *\n * Closed 7-variant `.field` enum locks the AI-grep affordance: switch\n * (detail.field) over the union compiles without default (charter P3).\n *\n * - `widthCells` -- `tiles[i].widthCells` out of `(0, 64]`.\n * - `heightCells` -- `tiles[i].heightCells` out of `(0, 64]`.\n * - `pivotX` -- `tiles[i].pivotX` out of `[0, 1]`.\n * - `pivotY` -- `tiles[i].pivotY` out of `[0, 1]`.\n * - `collider` -- `tiles[i].collider` schema invariant (rect.length !==\n * 4 / rect dimension out of `[0, 1]^2` / polygon.points.length < 3 /\n * any point out of `[0, 1]^2` / type discriminator outside the closed\n * 3-variant enum).\n * - `atlases` -- top-level `atlases.length < 1` (empty atlas list).\n * - `atlasIndex` -- `regions[i].atlasIndex` outside `[0, atlases.length)`.\n *\n * `.scope?` is `'tile-entry'` when the violation is in `tiles[i].*` (in\n * which case `.tileEntryIndex` carries the offending `tiles[]` index) and\n * `'tileset-asset'` when the violation is at the top level (atlases /\n * region atlasIndex).\n */\nexport interface AssetTilesetTileEntryMalformedDetail {\n readonly code: 'tileset-tile-entry-malformed';\n readonly field:\n | 'widthCells'\n | 'heightCells'\n | 'pivotX'\n | 'pivotY'\n | 'collider'\n | 'atlases'\n | 'atlasIndex';\n readonly scope?: 'tileset-asset' | 'tile-entry';\n readonly tileEntryIndex?: number;\n readonly tilesetGuid: string;\n readonly expected?: string;\n readonly hint?: string;\n}\n\n/**\n * Detail for `mesh-bin-contract-violation`.\n *\n * The mesh-bin header is a projection of the canonical geometry layout. Keep\n * the complete wire facts in the structured detail so recovery never depends\n * on parsing the human-facing `expected` / `actual` strings. `sourceKey` and\n * `reason` identify the owning payload and the failed invariant; the nested\n * snapshots preserve the lossless expected/actual cardinality facts.\n */\nexport type AssetMeshBinContractViolationReason =\n | 'header-truncated'\n | 'version-unsupported'\n | 'header-invalid'\n | 'projection-mismatch'\n | 'payload-length-mismatch'\n | 'metadata-invalid'\n | 'attribute-invalid'\n | 'payload-non-finite';\n\nexport interface AssetMeshBinContractFacts {\n readonly field?:\n | 'byteLength'\n | 'version'\n | 'projectionVersion'\n | 'mask'\n | 'stride'\n | 'digest'\n | 'vertexBytes'\n | 'indexBytes'\n | 'jsonBytes'\n | 'metadata'\n | 'attribute';\n readonly attribute?: keyof VertexAttributeMap;\n readonly elementIndex?: number;\n readonly expectedLength?: number;\n readonly actualLength?: number;\n readonly actualValue?: 'nan' | 'positive-infinity' | 'negative-infinity';\n readonly version?: number;\n readonly projectionVersion?: number;\n readonly mask?: number;\n readonly digest?: string;\n readonly stride?: number;\n readonly vertexCount?: number;\n readonly vertexBytes?: number;\n readonly indexCount?: number;\n readonly indexWidth?: number;\n readonly indexBytes?: number;\n readonly jsonBytes?: number;\n readonly byteLength?: number;\n}\n\nexport interface AssetMeshBinContractViolationDetail {\n readonly code: 'mesh-bin-contract-violation';\n readonly sourceKey: string;\n readonly reason: AssetMeshBinContractViolationReason;\n readonly expected: Readonly<AssetMeshBinContractFacts>;\n readonly actual: Readonly<AssetMeshBinContractFacts>;\n}\n\n/**\n * Discriminated detail union for AssetError, narrowed per AssetErrorCode.\n *\n * Variants:\n * - `material-shader-ref-broken` -- materialShader identifier (path or GUID)\n * could not be resolved to a registered shader; carries materialAssetGuid +\n * missingShaderId.\n * - `asset-invalid-value` -- a register-time value failed validation;\n * carries `{ field: string; got: unknown }`.\n * - `mesh-renderer-material-override-overflow` -- override entries exceed\n * slot count; carries `{ expectedCount, actualCount, meshAssetGuid }`.\n * - `mesh-asset-submeshes-empty` -- MeshAsset.submeshes is empty array;\n * carries `{ meshAssetGuid }`.\n * - `mesh-submesh-index-range-out-of-bounds` -- submesh index range exceeds\n * parent mesh index buffer; carries `{ submeshIndex, indexOffset,\n * indexCount, indexBufferLength, meshAssetGuid }`.\n */\nexport type AssetErrorDetail =\n | import('./asset.js').AssetCodecFailureDetail\n | AssetMaterialShaderRefBrokenDetail\n | AssetTilesetRegionIndexOutOfRangeDetail\n | AssetTilesetTileEntryMalformedDetail\n | AssetMeshBinContractViolationDetail\n | VertexAttributePackDetail\n | { readonly field: string; readonly got: unknown }\n | { readonly field: string; readonly value: unknown; readonly reason: string }\n | { readonly expectedCount: number; readonly actualCount: number; readonly meshAssetGuid: string }\n | { readonly meshAssetGuid: string; readonly slotIndex: number; readonly handle: number }\n | {\n readonly meshAssetGuid: string;\n readonly slotIndex: number;\n readonly slotName: string;\n readonly defaultMaterialGuid: string;\n readonly actualKind: string;\n }\n | MeshMaterialOverrideConflict\n | {\n readonly meshAssetGuid: string;\n readonly submeshIndex: number;\n readonly materialSlot: number;\n readonly materialSlotCount: number;\n }\n | { readonly meshAssetGuid: string }\n | {\n readonly submeshIndex: number;\n readonly indexOffset: number;\n readonly indexCount: number;\n readonly indexBufferLength: number;\n readonly meshAssetGuid: string;\n }\n // Pre-existing detail shapes used by AssetRegistry / loaders / pipeline-builder\n // (added in M5 / w27 alongside the count-mismatch tightening so the union\n // accommodates every current call site without losing structural narrowing).\n | { readonly sourcePath: string }\n | { readonly kind: string; readonly registeredKinds?: readonly string[] }\n | { readonly key: string; readonly legalPattern: string }\n | { readonly passCount: number }\n | {\n readonly passIndex: number;\n readonly shaderKey: string;\n readonly cause: string;\n }\n | { readonly paramName: string; readonly expectedType: string; readonly got: unknown }\n | { readonly paramName: string; readonly got: unknown }\n | { readonly missingParams: readonly string[] }\n | { readonly cycle: string }\n | {\n readonly localId: number;\n readonly component: string;\n readonly field: string;\n readonly index: number;\n readonly refsLength: number;\n }\n | { readonly vertexCount: number; readonly floatsPerVertex: number }\n // feat-20260622 verify r1: sub-asset load-failure breadcrumb in structured\n // form. The recursive loader composes the same provenance into the `.hint`\n // string; this variant additionally exposes it for property access so AI\n // users locate the broken edge without parsing the hint (charter P3,\n // requirements section error-self-recovery). `sourceField`/`sceneEntityId`\n // mirror the originating AssetRef edge; both undefined for transitive\n // (texture) edges with no per-entity origin (D-2).\n | {\n readonly referencedByGuid: string;\n readonly referencedByKind: string;\n readonly subAssetGuid: string;\n readonly sceneEntityId?: number;\n readonly sourceField?: {\n readonly componentName?: string;\n readonly fieldName: string;\n readonly arrayIndex?: number;\n };\n };\n\n// === Image importer error model SSOT ===\n// ImageErrorDetailByCode owns the closed vocabulary and payload shapes. The\n// envelope and runtime constructors are derived from it so `.code` and\n// `.detail` stay correlated for consumers.\n\n/** Closed code union, derived from the detail map below. */\ninterface ImageErrorDetailByCode {\n 'image-decode-failed': {\n readonly reason: string;\n readonly path?: string;\n };\n 'image-format-unsupported': {\n readonly actualMime: string;\n readonly path?: string;\n readonly formatColorSpaceConflict?: {\n readonly format: string;\n readonly colorSpace: 'srgb' | 'linear';\n readonly expected: 'srgb' | 'linear';\n };\n };\n 'image-dimension-out-of-bounds': {\n readonly requested: { readonly width: number; readonly height: number };\n readonly limit: number;\n };\n 'image-meta-missing': {\n readonly sourcePath: string;\n readonly expectedSidecarPath: string;\n };\n 'image-hdr-decode-failed': {\n readonly reason: string;\n readonly path?: string;\n };\n 'atlas-empty-input': {\n readonly receivedCount: number;\n };\n 'atlas-size-exceeded': {\n readonly name: string;\n readonly width: number;\n readonly height: number;\n readonly maxAtlasSize: number;\n };\n 'atlas-region-mismatch': {\n readonly name: string;\n readonly regionsTotalPixels: number;\n readonly atlasPixels: number;\n };\n}\n\nexport type ImageErrorCode = keyof ImageErrorDetailByCode;\n\n/** Detail union projected from one code-to-payload map. */\nexport type ImageErrorDetailFor<C extends ImageErrorCode> = Readonly<{ code: C }> &\n ImageErrorDetailByCode[C];\n\nexport type ImageErrorDetail = {\n [C in ImageErrorCode]: ImageErrorDetailFor<C>;\n}[ImageErrorCode];\n\n/**\n * Correlated error envelope. `ImageErrorFor<C>` is the producer-facing\n * generic; `ImageError` is its closed union for exhaustive consumer switches.\n */\nexport type ImageErrorFor<C extends ImageErrorCode> = Error & {\n readonly code: C;\n readonly expected: string;\n readonly hint: string;\n readonly detail: ImageErrorDetailFor<C>;\n};\n\nexport type ImageError = {\n [C in ImageErrorCode]: ImageErrorFor<C>;\n}[ImageErrorCode];\n\n/**\n * Per-code `.hint` string literals SSOT (plan-strategy section 2.3 / Tier 2\n * documentation). `Record<ImageErrorCode, string>` ensures compile-time\n * completeness; any future minor add to `ImageErrorCode` raises a TS error\n * on this map until the matching hint is supplied (charter proposition 4\n * explicit failure -- producer/consumer + reviewer all see the missing arm).\n *\n * Each hint embeds an executable command so AI users self-recover by\n * copy-pasting the hint into the shell (plan-strategy Tier 2 \"hint must\n * carry forgeax-engine-remote-image import <path>\" or similar; the image\n * plugin bin lands in feat-future-console-plugin-image — until then the\n * hint references the in-package importer surface).\n */\nexport const IMAGE_ERROR_HINTS: Readonly<Record<ImageErrorCode, string>> = {\n 'image-decode-failed':\n 'check file integrity; re-export from DCC tool (Photoshop / GIMP / Aseprite); dimensions > 0 + valid PNG / JPG header bytes',\n 'image-format-unsupported':\n 'supports PNG / JPG / TGA true-color sources; convert unsupported formats with: magick convert <input> <output>.png; check importSettings.colorSpace consistency with format family if formatColorSpaceConflict present',\n 'image-dimension-out-of-bounds':\n 'downscale source under device caps (typical maxTextureDimension2D = 8192 / 16384); use mipmap chain instead of larger source if lod is the goal',\n 'image-meta-missing': 'run: forgeax-engine-remote-asset import <path>',\n 'image-hdr-decode-failed':\n 'check .hdr file integrity; verify Radiance RGBE header magic (#?RADIANCE) and FORMAT=32-bit_rle_rgbe header field; ensure file was not truncated',\n // feat-20260521-sprite-atlas-animation M1 T-02 — atlas hook hint strings\n // (plan-strategy section 2 D-2). Each hint embeds an executable recovery\n // path so AI users self-repair by copy-pasting the hint into the shell\n // or into the build config (charter P3 explicit failure + AGENTS.md\n // Error model \"hint must carry executable recovery\").\n 'atlas-empty-input':\n 'verify forgeax-engine-remote-asset atlas --input <glob> --name <prefix> --output <dir> matches at least 1 PNG on disk; run `ls <glob>` to inspect the resolved file set; add the missing sprite source or fix the glob pattern',\n 'atlas-size-exceeded':\n 'downscale the source PNG so width * height <= maxAtlasSize^2 (default 4096); or split sprites across multiple atlas runs (forgeax-engine-remote-asset atlas --input <subset-glob> --name <other-prefix> --output <dir>); or raise the cap via --max-atlas-size 8192 if device caps allow it',\n 'atlas-region-mismatch':\n 'shelfPack returned regions exceeding atlas footprint — packer safety net; file a forgeax-engine bug; rerun forgeax-engine-remote-asset atlas with a smaller input set or lower --max-atlas-size as temporary recovery',\n};\n\n/**\n * Image color-space discriminator (plan-strategy section 2.5 D Open Q-4 (c)).\n *\n * `'srgb'` -- baseColor / albedo authored in sRGB display space; uploaded\n * with `format='*-srgb'` so hardware applies the gamma decode automatically\n * (research F-3 spec guarantee for mipmap blits).\n *\n * `'linear'` -- normal / metallic / roughness / data textures authored in\n * linear color space; uploaded with `format='*-unorm'` (no gamma transform).\n */\nexport type ImageColorSpace = 'srgb' | 'linear';\n\n/**\n * Image importer settings POD (plan-strategy section 2.2 D-4 image disk\n * schema; AC-26). 5-field free-form object persisted into the `*.meta.json`\n * `importSettings` field; the GUID lives at the top of the POD so consumers\n * (importer + runtime + console asset import) share one schema (charter\n * proposition 5 consistent abstraction).\n *\n * Fields:\n * - `guid` -- string-form RFC 4122 dash-form UUID identifying the single\n * image sub-asset (image disk schema is currently single-sub-asset by\n * design; cubemap face / array layer reserved for future feat).\n * - `colorSpace` -- `'srgb' | 'linear'` (drives uploadTexture format\n * selection; plan-strategy section 2.5).\n * - `mipmap` -- `'auto' | 'none'` (`'auto'` enables runtime mipmap-generator\n * blit chain; `'none'` ships a single mip level).\n * - `addressMode` -- WGPU address mode for sampler (passed through to\n * uploadTexture's sampler descriptor).\n * - `filterMode` -- magFilter / minFilter selector.\n *\n * The shape stays free-form `Record<string, unknown>` compatible at the\n * `*.meta.json` `importSettings` slot (research F-9 -- meta.schema.json\n * does not lock importSettings sub-shape; minor add of new fields is\n * non-breaking; plan-strategy R5 risk-free).\n */\nexport interface ImageMeta {\n readonly guid: string;\n readonly colorSpace: ImageColorSpace;\n readonly mipmap: 'auto' | 'none';\n readonly addressMode: 'repeat' | 'clamp-to-edge' | 'mirror-repeat';\n readonly filterMode: 'nearest' | 'linear';\n /** Optional asset-owned cooked-payload target dimension. */\n readonly downscaleMaxDimension?: number;\n}\n\n/**\n * Decoded image POD (plan-strategy section 2.2 + section 3.3; AC-26).\n * Producer: `@forgeax/engine-image` parseImage / decodeImageFromFile.\n * Consumer: `@forgeax/engine-runtime` AssetRegistry.uploadTexture (M3).\n *\n * Six-field tight-packed shape:\n * - `bytes` -- decoded pixel buffer (RGBA tight-packed; 4 bytes per pixel).\n * Producer guarantees `bytes.length === width * height * 4`.\n * - `width` / `height` -- pixel dimensions (must satisfy device caps\n * `maxTextureDimension2D` floor; surfaced as `image-dimension-out-of-bounds`\n * when exceeded).\n * - `mime` -- discriminator over the supported set (`'image/jpeg' | 'image/png' | 'image/x-tga'`);\n * keeps the runtime side from sniffing magic bytes.\n * - `colorSpace` -- carried over from `ImageMeta.colorSpace`; uploadTexture\n * asserts `format <-> colorSpace` consistency at the GPU upload entry\n * (plan-strategy section 2.5 D Open Q-4 (c)).\n * - `mipmap` -- boolean derived from `ImageMeta.mipmap === 'auto'`; the\n * runtime mipmap-generator skips the blit chain when false.\n *\n * Math-free POD; no Float32Array / branded handle on this shape (charter\n * proposition 5 consistent abstraction with TextureAsset POD).\n */\nexport interface DecodedImage {\n readonly bytes: Uint8Array;\n readonly width: number;\n readonly height: number;\n readonly mime: 'image/jpeg' | 'image/png' | 'image/x-tga';\n readonly colorSpace: ImageColorSpace;\n readonly mipmap: boolean;\n}\n\n// === AssetGuid — disk-layer GUID brand type (feat-20260513-guid-asset-package-system) ===========\n//\n// Type-only declaration. Implementation (parse / format / equals / random) lives in\n// @forgeax/engine-pack/guid. The brand field is a phantom string literal that prevents\n// accidental assignment from plain Uint8Array or string at compile time.\n\n/** 16-byte UUID brand for disk-layer asset identification. RFC 4122 UUIDv7 wire form. */\nexport type AssetGuid = Uint8Array & { readonly __guidBrand: 'AssetGuid' };\n\n// === PackErrorCode / PackErrorDetail — disk-layer error SSOT (feat-20260513-guid-asset-package-system w15) ===\n//\n// Decision anchors:\n// - requirements §6.1 (13-member closed union literal set SSOT; widened from\n// the original 8 by feat-20260523-shader-template-instance-split (+1) and\n// feat-20260608-scene-nesting-ecs-fication M1 / w8 (+4))\n// - requirements §6.2 AC-05/07 (per-code discriminated detail)\n// - plan-strategy §D-5 (PackError 4-field surface + check-pack-error-detail-narrowed.mjs guard)\n// - AGENTS.md §Error model (structurally parallel to AssetErrorCode / InspectorErrorCode)\n\n/**\n * Closed PackErrorCode union — 15 members.\n * Used exclusively by the @forgeax/engine-pack scanner fail-fast chain.\n *\n * | code | trigger |\n * |:--|:--|\n * | `'pack-malformed-meta'` | .meta.json fails ajv schema validation |\n * | `'pack-malformed-pack'` | .pack.json fails ajv schema validation |\n * | `'pack-guid-malformed'` | a GUID field is not a valid 36-char RFC 4122 dash-form string |\n * | `'pack-orphan-meta'` | .meta.json exists but the corresponding source file does not |\n * | `'pack-meta-missing'` | source file exists but no .meta.json (strict mode) |\n * | `'pack-guid-collision'` | two .pack.json files declare the same GUID |\n * | `'pack-cyclic-reference'` | asset refs[] (incl. mount.source) form a cycle |\n * | `'pack-subasset-index-out-of-range'` | subAsset.sourceIndex >= source count |\n * | `'payload-schema-mismatch'` | material payload fails materialShader / paramSchema schema |\n * | `'pack-mount-localid-overlap'` | mount memberFirst windows overlap or collide with entities[] |\n * | `'pack-mount-count-mismatch'` | mount memberCount disagrees with referenced child SceneAsset |\n * | `'pack-mount-override-localid-out-of-range'` | override.localId outside the mount's member window |\n * | `'pack-mount-override-unknown-field'` | override.comp / override.field unknown to the schema vocab |\n * | `'pack-unknown-path'` | @name references a name not declared in package.json#forgeax.assets.paths |\n * | `'pack-malformed-path-ref'` | source starts with @ but does not match @<name>/<rest> format, or resolves to a path outside the declared directory |\n *\n * Membership history: 8 -> 9 added 'payload-schema-mismatch' (feat-20260523\n * shader-template-instance-split); 9 -> 13 adds the four mount-* codes\n * (feat-20260608-scene-nesting-ecs-fication M1 / w8, plan-strategy D-8 literals\n * locked); 13 -> 15 adds 'pack-unknown-path' + 'pack-malformed-path-ref'\n * (feat-20260625-asset-meta-source-mount-prefix M1 / w1).\n */\nexport type PackErrorCode =\n | 'pack-malformed-meta'\n | 'pack-malformed-pack'\n | 'pack-guid-malformed'\n | 'pack-orphan-meta'\n | 'pack-meta-missing'\n | 'pack-guid-collision'\n | 'pack-cyclic-reference'\n | 'pack-subasset-index-out-of-range'\n // === 1 new code (feat-20260523-shader-template-instance-split M1-T02) ===\n | 'payload-schema-mismatch'\n // === 4 new codes (feat-20260608-scene-nesting-ecs-fication M1 / w8;\n // plan-strategy D-8 literals locked) ===\n | 'pack-mount-localid-overlap'\n | 'pack-mount-count-mismatch'\n | 'pack-mount-override-localid-out-of-range'\n | 'pack-mount-override-unknown-field'\n // === 2 new codes (feat-20260625-asset-meta-source-mount-prefix M1 / w1) ===\n | 'pack-unknown-path'\n | 'pack-malformed-path-ref';\n\n/**\n * Discriminated detail union for PackError — narrowed per PackError.code.\n * AI users access `err.detail.<field>` directly after switch (err.code) narrows\n * the variant. Variants without an own `code` field are narrowed exclusively by\n * the top-level `PackError.code` discriminant (the legacy 7 variants below);\n * variants that carry a `code` field (`payload-schema-mismatch`, the evolved\n * `pack-cyclic-reference`, and the four mount-* additions) double-narrow via\n * `Extract<PackErrorDetail, { code: ... }>` at the type layer (R10).\n *\n * Structurally parallel to RhiErrorDetail / MetricErrorDetail.\n */\nexport type PackErrorDetail =\n | {\n /** Absolute or relative path to the malformed .meta.json file. */\n readonly path: string;\n /** ajv validation errors produced by validateMeta(). */\n readonly ajvErrors: readonly { readonly instancePath: string; readonly message: string }[];\n }\n | {\n /** Absolute or relative path to the malformed .pack.json file. */\n readonly path: string;\n /** ajv validation errors produced by validatePack(). */\n readonly ajvErrors: readonly { readonly instancePath: string; readonly message: string }[];\n /**\n * Optional human-readable reason category. Set to a fixed literal for\n * the runtime instantiate-path SceneEntity field-name typo route:\n * `'unknown component field'` (requirements §AC-08(b)). Absent on\n * scanner-path ajv-validation failures (where the structural\n * ajvErrors[].message string already carries the diagnostic).\n */\n readonly reason?: string;\n }\n | {\n /** The raw string value that failed UUID validation. */\n readonly raw: string;\n /** Human-readable reason (e.g. 'expected 36-char RFC 4122 dash-form UUID'). */\n readonly reason: string;\n }\n | {\n /** Path of the .meta.json that has no corresponding source file. */\n readonly metaPath: string;\n /** Path that was expected to exist as a source file. */\n readonly expectedFile: string;\n }\n | {\n /** Path of the source file that has no accompanying .meta.json. */\n readonly filePath: string;\n }\n | {\n /** The two .pack.json paths that both declare the same GUID (tuple, always length 2). */\n readonly paths: readonly [string, string];\n /** The colliding GUID dash-form string. */\n readonly guid: string;\n }\n // === Evolved variant (feat-20260608-scene-nesting-ecs-fication M1 / w8;\n // plan-strategy R10): pack-cyclic-reference now carries `code` + `kind` so\n // the build-time scanner (kind: 'mount-asset', cycle: GUID list) and the\n // runtime ChildOf detector (kind: 'childof', cycle: LocalEntityId list) stay\n // narrowable from a single error code. ===\n | {\n readonly code: 'pack-cyclic-reference';\n /**\n * Cycle origin tag: 'childof' for runtime ChildOf relationship cycles\n * (LocalEntityId stringified), 'mount-asset' for build-time\n * SceneAsset.mounts[].source GUID cycles (D-1).\n */\n readonly kind: 'childof' | 'mount-asset';\n /** Cycle path as ordered identifier strings; first === last. */\n readonly cycle: readonly string[];\n }\n | {\n /** Path of the .meta.json declaring the out-of-range sourceIndex. */\n readonly metaPath: string;\n /** The declared sourceIndex value. */\n readonly sourceIndex: number;\n /** The maximum valid sourceIndex (exclusive upper bound = source count). */\n readonly max: number;\n }\n // === 1 new variant (feat-20260523-shader-template-instance-split M1-T02) ===\n | {\n /** Discriminated code for material payload schema mismatch. */\n readonly code: 'payload-schema-mismatch';\n /** GUID of the offending material asset. */\n readonly guid: string;\n /** ajv validation errors for the material payload. */\n readonly errors: readonly { readonly instancePath: string; readonly message: string }[];\n }\n // === 4 new variants (feat-20260608-scene-nesting-ecs-fication M1 / w8;\n // plan-strategy D-8 literals locked; AC-04 / AC-05 / AC-06 / AC-07) ===\n | {\n readonly code: 'pack-mount-localid-overlap';\n /** Overlapping LocalEntityId values (sorted ascending). */\n readonly overlapping: readonly number[];\n /**\n * Source labels for the conflicting windows. Each entry is a\n * human-readable origin string (`mount[<localId>]`,\n * `entities[<localId>]`, etc.) of length matching `overlapping[]`.\n */\n readonly sources: readonly string[];\n }\n | {\n readonly code: 'pack-mount-count-mismatch';\n /** localId of the offending mount within its parent SceneAsset. */\n readonly mountLocalId: number;\n /** memberCount declared on the mount. */\n readonly declared: number;\n /** Actual entities[].length resolved from the referenced child SceneAsset. */\n readonly actual: number;\n }\n | {\n readonly code: 'pack-mount-override-localid-out-of-range';\n /** override.localId that fell outside the mount's member window. */\n readonly overrideLocalId: number;\n /** localId of the parent mount. */\n readonly mountLocalId: number;\n /** memberCount of the parent mount (window upper bound, exclusive). */\n readonly memberCount: number;\n }\n | {\n readonly code: 'pack-mount-override-unknown-field';\n /** Component name on which the override was authored. */\n readonly comp: string;\n /** Unknown field name. */\n readonly field: string;\n /** localId of the parent mount carrying the override. */\n readonly mountLocalId: number;\n }\n // === 2 new variants (feat-20260625-asset-meta-source-mount-prefix M1 / w1) ===\n | {\n readonly code: 'pack-unknown-path';\n /** The @name that was not found in package.json#forgeax.assets.paths. */\n readonly pathName: string;\n /** All known path names declared in package.json#forgeax.assets.paths. */\n readonly knownNames: readonly string[];\n }\n | {\n readonly code: 'pack-malformed-path-ref';\n /**\n * Which malformation occurred, so an AI user can branch on a property\n * instead of parsing the human-facing .hint:\n * - 'format': source starts with @ but does not match @<name>/<rest>\n * - 'escape': rest segment resolves outside the declared path directory\n */\n readonly reason: 'format' | 'escape';\n /** The raw source string as written in the .meta.json. */\n readonly rawSource: string;\n /** The expected format description for self-correction. */\n readonly expectedFormat: string;\n };\n\n/**\n * Per-code .hint string literals SSOT.\n * Record<PackErrorCode, string> ensures compile-time completeness.\n */\nexport const PACK_ERROR_HINTS: Readonly<Record<PackErrorCode, string>> = {\n 'pack-malformed-meta':\n 'check guid is a valid RFC 4122 UUID; validate with: ajv validate -s schema/meta.schema.json -d <file>',\n 'pack-malformed-pack':\n 'check all asset guid and refs[] fields are 36-char dash-form UUIDs; validate with pack.schema.json',\n 'pack-guid-malformed':\n 'use AssetGuid.random() or a UUIDv7 generator; all GUID fields must be 36-char RFC 4122 dash-form',\n 'pack-orphan-meta': 'remove the orphan .meta.json or add the missing source file next to it',\n 'pack-meta-missing':\n 'run forgeax-engine-remote-asset scan --roots <dir> to list source files without .meta.json',\n 'pack-guid-collision':\n 'run forgeax-engine-remote-asset verify to list all GUID collisions; each GUID must be globally unique',\n 'pack-cyclic-reference':\n 'run forgeax-engine-remote-asset verify to print the cycle path; break the cycle by removing a refs[] entry',\n 'pack-subasset-index-out-of-range':\n 'check subAssets[].sourceIndex does not exceed the actual sub-image count in the source file',\n // === 1 new hint (feat-20260523-shader-template-instance-split M1-T02) ===\n 'payload-schema-mismatch':\n 'material asset payload failed schema validation; check paramSchema entries all use valid types from MATERIAL_PARAM_TYPES and materialShader is a non-empty string',\n // === 4 new hints (feat-20260608-scene-nesting-ecs-fication M1 / w8;\n // plan-strategy D-8) ===\n 'pack-mount-localid-overlap':\n 'check parent SceneAsset.mounts[].memberFirst windows do not overlap with each other or with entities[].localId; rebuild mount sidecar after the child SceneAsset reimport',\n 'pack-mount-count-mismatch':\n 'mount.memberCount must equal the referenced child SceneAsset totalSlots (entities.length + sum(mounts[].memberCount) + mounts.length); rebuild mount sidecar via forgeax-engine-remote-asset verify <dir> after the child SceneAsset reimport',\n 'pack-mount-override-localid-out-of-range':\n 'override.localId must be in [0, mount.memberCount); shrink the override or extend memberCount to match the child SceneAsset',\n 'pack-mount-override-unknown-field':\n 'override.comp / override.field must match a defined component schema; check defineComponent registry or rebuild mount sidecar after the child SceneAsset reimport',\n // === 2 new hints (feat-20260625-asset-meta-source-mount-prefix M1 / w1) ===\n 'pack-unknown-path':\n 'the @name in source is not declared in package.json#forgeax.assets.paths; add it there or use a known name from the list in error.detail.knownNames',\n 'pack-malformed-path-ref':\n 'source must be @<name>/<rest> where name is a key in package.json#forgeax.assets.paths; the resolved path must not escape the declared directory',\n};\n\n// === AudioErrorCode / AudioError / AudioErrorDetail -- audio error SSOT (feat-20260527-audio-system M1 / w4) ===\n//\n// Decision anchors:\n// - requirements S-8 (5-member independent closed union: context-creation-failed /\n// decode-failed / context-suspended / invalid-clip-handle / bus-not-found)\n// - requirements AC-13 (AudioErrorCode closed union switch exhaustiveness)\n// - plan-strategy D-7 (AudioErrorCode SSOT in engine-types, parallel to\n// ImageErrorCode / GltfErrorCode / AssetErrorCode)\n// - plan-strategy section 8 AI User Affordance (structured 4-field surface:\n// .code / .expected / .hint / .detail)\n// - charter P3 (explicit failure: switch (err.code) exhaustive without default;\n// .hint provides concrete recovery action)\n// - charter P4 (consistent abstraction: structurally parallel to AssetError,\n// ImageError, GltfError same 4-field shape)\n// - architecture-principles #1 SSOT (the 5 literals + class shape + hints table\n// live here once; engine-audio package references this module)\n\n/**\n * Closed `AudioErrorCode` union -- 5 members (plan-strategy D-7;\n * requirements S-8). Exhaustive `switch (err.code)` needs no default\n * fallback -- TypeScript guards union completeness at compile time\n * (charter P3 explicit failure).\n *\n * Domain-separated from `AssetErrorCode` (runtime registry surface, 12 members)\n * and `GltfErrorCode` (importer surface, 13 members). AI users face these 5\n * alternatives at the audio engine surface (`@forgeax/engine-audio`\n * AudioError + `@forgeax/engine-audio-webaudio` backend).\n *\n * | code | trigger |\n * |:--|:--|\n * | `'context-creation-failed'` | `new AudioContext()` threw or returned null (privacy browser / no audio device) |\n * | `'decode-failed'` | `decodeAudioData(arrayBuffer)` rejected (corrupt file / unsupported codec) |\n * | `'context-suspended'` | `play()` called while AudioContext.state is `'suspended'` and gesture listener failed to resume |\n * | `'invalid-clip-handle'` | AudioSource.clip handle is dangling or refers to an unregistered asset |\n * | `'bus-not-found'` | AudioSource.bus refers to a string literal outside the `'sfx' | 'music'` closed set |\n */\nexport type AudioErrorCode =\n | 'context-creation-failed'\n | 'decode-failed'\n | 'context-suspended'\n | 'invalid-clip-handle'\n | 'bus-not-found';\n\n/**\n * Per-code `AudioError` detail shapes -- discriminated payloads narrowed\n * by `AudioError.code` so AI users writing `switch (err.code)` get\n * control-flow-tightened access to the relevant detail fields\n * (charter P3 explicit failure).\n */\n\n/** `context-creation-failed` payload: carries the original error reason. */\nexport interface AudioCtxCreationFailedDetail {\n readonly code: 'context-creation-failed';\n readonly reason: string;\n}\n\n/** `decode-failed` payload: carries the original decode error reason. */\nexport interface AudioDecodeFailedDetail {\n readonly code: 'decode-failed';\n readonly reason: string;\n}\n\n/** `context-suspended` payload: empty marker detail (no extra fields). */\nexport interface AudioCtxSuspendedDetail {\n readonly code: 'context-suspended';\n}\n\n/** `invalid-clip-handle` payload: carries the dangling handle identifier. */\nexport interface AudioInvalidClipHandleDetail {\n readonly code: 'invalid-clip-handle';\n readonly clipHandleId: number;\n}\n\n/** `bus-not-found` payload: carries the invalid bus name attempted. */\nexport interface AudioBusNotFoundDetail {\n readonly code: 'bus-not-found';\n readonly attemptedBus: string;\n}\n\n/**\n * Discriminated detail union for `AudioError`, narrowed per `AudioError.code`.\n * AI users obtain the concrete detail shape via `switch (err.code)` without\n * needing a fallback `as` cast (charter P3).\n */\nexport type AudioErrorDetail =\n | AudioCtxCreationFailedDetail\n | AudioDecodeFailedDetail\n | AudioCtxSuspendedDetail\n | AudioInvalidClipHandleDetail\n | AudioBusNotFoundDetail;\n\n/**\n * Structured audio error -- four-field surface (`.code` / `.expected` /\n * `.hint` / `.detail`) structurally parallel to `@forgeax/engine-types`\n * `AssetError` + `ImageError` + `GltfError` same-shape errors\n * (charter P4 consistent abstraction; AGENTS.md \"Errors are structured.\n * Return Result, never throw for expected failures.\").\n *\n * AI users consume the structured triple via property access:\n * `switch (err.code) { case 'decode-failed': ... err.hint ... }`\n * -- never by parsing `.message` (charter P3 explicit failure red line).\n *\n * The `.message` field is auto-composed for human stack traces and carries\n * the same content as `.code` + `.expected` + `.hint`; AI users prefer\n * field access on the structured triple.\n *\n * @example AI-user exhaustive switch on the 5 members (no default fallback)\n * ```ts\n * import { AudioError, type AudioErrorCode } from '@forgeax/engine-types';\n *\n * function recover(code: AudioErrorCode): string {\n * switch (code) {\n * case 'context-creation-failed': return 'check browser supports AudioContext';\n * case 'decode-failed': return 'ensure audio file is a valid wav/mp3/ogg/flac';\n * case 'context-suspended': return 'call play after user gesture to trigger resume';\n * case 'invalid-clip-handle': return 'verify clip was registered via AssetRegistry';\n * case 'bus-not-found': return 'use sfx or music bus literal';\n * }\n * }\n * ```\n */\nexport class AudioError extends Error {\n readonly code: AudioErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail?: AudioErrorDetail;\n\n constructor(args: {\n code: AudioErrorCode;\n expected: string;\n hint: string;\n detail?: AudioErrorDetail;\n }) {\n super(`[AudioError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);\n this.name = 'AudioError';\n this.code = args.code;\n this.expected = args.expected;\n this.hint = args.hint;\n if (args.detail !== undefined) {\n this.detail = args.detail;\n }\n }\n}\n\n/**\n * Per-code `.hint` string literals SSOT (plan-strategy D-7 lock-in).\n * Exported so engine-audio error helpers and tests consume the same SSOT\n * -- any drift here updates both producer call sites and the AGENTS.md\n * Error model table.\n *\n * The shape is a `Record<AudioErrorCode, string>` so future additions to\n * the closed union are a compile-time error here as well (reinforces\n * charter P3 explicit failure). Each hint embeds an executable recovery\n * action so AI users self-repair (charter P3).\n */\nexport const AUDIO_ERROR_HINTS: Readonly<Record<AudioErrorCode, string>> = {\n 'context-creation-failed':\n 'check browser supports AudioContext; verify no privacy extension blocks audio; try reloading the page after user gesture',\n 'decode-failed':\n 'ensure audio file is a valid wav/mp3/ogg/flac at the GUID path; check file integrity (truncated or empty bytes)',\n 'context-suspended':\n 'call play after a user gesture (click/tap/keydown) to trigger AudioContext.resume(); if in iframe check sandbox attribute',\n 'invalid-clip-handle':\n 'verify clip was registered via AssetRegistry.register() before spawning AudioSource; inspect active handles via assetRegistry.inspect()',\n 'bus-not-found':\n \"use 'sfx' or 'music' bus literal; custom bus names are not supported in v1 (OOS-2)\",\n};\n\n// === PhysicsErrorCode / PhysicsError / PhysicsErrorDetail -- physics error SSOT (feat-20260528-rapier-physics-2d-3d M1 / t6; extended feat-20260617-kinematic M1) ===\n//\n// Decision anchors:\n// - requirements AC-11 (PhysicsErrorCode closed union registration + AGENTS.md update)\n// - plan-strategy D-5 (PhysicsErrorCode 9 members / PhysicsError 4-field surface / PhysicsErrorDetail discriminated)\n// - charter P3 (explicit failure: exhaustive switch without default; .hint provides recovery)\n// - charter P4 (consistent abstraction: structurally parallel to AssetError / AudioError / GltfError)\n// - architecture-principles #1 SSOT (the 9 literals + class + hints table live here once;\n// engine-physics package re-exports from here)\n\n/**\n * Closed `PhysicsErrorCode` union -- 9 members (plan-strategy D-5;\n * requirements AC-11). Exhaustive `switch (err.code)` needs no default\n * fallback -- TypeScript guards union completeness at compile time\n * (charter P3 explicit failure).\n *\n * Domain-separated from `AssetErrorCode` (runtime registry, 13 members)\n * and `AudioErrorCode` (audio engine, 5 members). AI users face these 9\n * alternatives at the physics engine surface.\n *\n * | code | trigger |\n * |:--|:--|\n * | `'wasm-load-failed'` | dynamic import() of Rapier WASM rejected (network / file not found). |\n * | `'wasm-simd-unsupported'` | WebAssembly.validate returned false for SIMD test module; compat fallback also unavailable. |\n * | `'step-failed'` | Rapier World.step threw a WASM trap (invalid body parameters / NaN values). |\n * | `'invalid-body-config'` | mass <= 0 for dynamic bodies, or other validation failure. |\n * | `'body-not-found'` | entity handle resolved to no Rapier rigid body (no RigidBody spawned or handle was freed). |\n * | `'collider-not-found'` | entity handle resolved to no Rapier collider (no Collider spawned or handle was freed). |\n * | `'backend-not-registered'` | PhysicsWorld resource missing from World; use createApp(canvas, { plugins: [physicsPlugin('rapier-3d')] }) or manual registration. |\n * | `'teleport-invalid-body-type'` | teleport() called on a static or kinematic body (only dynamic allowed). |\n * | `'controller-requires-kinematic'` | moveAndSlide() called on a non-kinematic body. |\n */\nexport type PhysicsErrorCode =\n | 'wasm-load-failed'\n | 'wasm-simd-unsupported'\n | 'step-failed'\n | 'invalid-body-config'\n | 'body-not-found'\n | 'collider-not-found'\n | 'backend-not-registered'\n | 'teleport-invalid-body-type'\n | 'controller-requires-kinematic';\n\n/**\n * Per-code `PhysicsError` detail shapes -- discriminated payloads narrowed\n * by `PhysicsError.code` so AI users writing `switch (err.code)` get\n * control-flow-tightened access to the relevant detail fields (charter P3).\n */\n\n/** `wasm-load-failed` payload: carries the original error reason. */\nexport interface PhysicsWasmLoadFailedDetail {\n readonly code: 'wasm-load-failed';\n readonly reason: string;\n}\n\n/** `wasm-simd-unsupported` payload: carries the detection failure reason. */\nexport interface PhysicsWasmSimdUnsupportedDetail {\n readonly code: 'wasm-simd-unsupported';\n readonly reason: string;\n}\n\n/** `step-failed` payload: carries the WASM trap reason. */\nexport interface PhysicsStepFailedDetail {\n readonly code: 'step-failed';\n readonly reason: string;\n}\n\n/** `invalid-body-config` payload: carries the violating field + value. */\nexport interface PhysicsInvalidBodyConfigDetail {\n readonly code: 'invalid-body-config';\n readonly field: string;\n readonly value: unknown;\n}\n\n/** `body-not-found` payload: carries the entity that was not found. */\nexport interface PhysicsBodyNotFoundDetail {\n readonly code: 'body-not-found';\n readonly entity: number;\n}\n\n/** `collider-not-found` payload: carries the entity that was not found. */\nexport interface PhysicsColliderNotFoundDetail {\n readonly code: 'collider-not-found';\n readonly entity: number;\n}\n\n/** `backend-not-registered` payload: carries the attempted backend name. */\nexport interface PhysicsBackendNotRegisteredDetail {\n readonly code: 'backend-not-registered';\n readonly attemptedBackend: string;\n}\n\n/** `teleport-invalid-body-type` payload: carries the entity + disallowed body type. */\nexport interface PhysicsTeleportInvalidBodyTypeDetail {\n readonly code: 'teleport-invalid-body-type';\n readonly entity: number;\n readonly bodyType: string;\n}\n\n/** `controller-requires-kinematic` payload: carries the entity + actual body type. */\nexport interface PhysicsControllerRequiresKinematicDetail {\n readonly code: 'controller-requires-kinematic';\n readonly entity: number;\n readonly bodyType: string;\n}\n\n/**\n * Discriminated detail union for `PhysicsError`, narrowed per `PhysicsError.code`.\n * AI users obtain the concrete detail shape via `switch (err.code)` without\n * needing a fallback `as` cast (charter P3).\n */\nexport type PhysicsErrorDetail =\n | PhysicsWasmLoadFailedDetail\n | PhysicsWasmSimdUnsupportedDetail\n | PhysicsStepFailedDetail\n | PhysicsInvalidBodyConfigDetail\n | PhysicsBodyNotFoundDetail\n | PhysicsColliderNotFoundDetail\n | PhysicsBackendNotRegisteredDetail\n | PhysicsTeleportInvalidBodyTypeDetail\n | PhysicsControllerRequiresKinematicDetail;\n\n/**\n * Structured physics error -- four-field surface (`.code` / `.expected` /\n * `.hint` / `.detail`) structurally parallel to `@forgeax/engine-types`\n * `AssetError` + `AudioError` + `GltfError` (charter P4 consistent abstraction).\n *\n * AI users consume the structured triple via property access:\n * `switch (err.code) { case 'wasm-load-failed': ... err.hint ... }`\n * -- never by parsing `.message` (charter P3 explicit failure red line).\n *\n * @example AI-user exhaustive switch on the 9 members (no default fallback)\n * ```ts\n * import { PhysicsError, type PhysicsErrorCode } from '@forgeax/engine-types';\n *\n * function recover(code: PhysicsErrorCode): string {\n * switch (code) {\n * case 'wasm-load-failed': return 'check network and @dimforge/rapier3d-compat';\n * case 'wasm-simd-unsupported': return 'check browser supports WASM SIMD';\n * case 'step-failed': return 'check for NaN values in transforms';\n * case 'invalid-body-config': return 'ensure mass > 0 for dynamic bodies';\n * case 'body-not-found': return 'ensure RigidBody was spawned before use';\n * case 'collider-not-found': return 'ensure Collider was spawned before use';\n * case 'backend-not-registered': return 'use createApp(canvas, { plugins: [physicsPlugin(...)] })';\n * case 'teleport-invalid-body-type': return 'only dynamic bodies can be teleported';\n * case 'controller-requires-kinematic': return 'set RigidBody.type to kinematic';\n * }\n * }\n * ```\n */\nexport class PhysicsError extends Error {\n readonly code: PhysicsErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail?: PhysicsErrorDetail;\n\n constructor(args: {\n code: PhysicsErrorCode;\n expected: string;\n hint: string;\n detail?: PhysicsErrorDetail;\n }) {\n super(`[PhysicsError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);\n this.name = 'PhysicsError';\n this.code = args.code;\n this.expected = args.expected;\n this.hint = args.hint;\n if (args.detail !== undefined) {\n this.detail = args.detail;\n }\n }\n}\n\n/**\n * Per-code `.hint` string literals SSOT (plan-strategy D-5 lock-in).\n * Exported so engine-physics error helpers and tests consume the same SSOT.\n *\n * The shape is a `Record<PhysicsErrorCode, string>` so future additions to\n * the closed union are a compile-time error here as well (reinforces\n * charter P3 explicit failure).\n */\nexport const PHYSICS_ERROR_HINTS: Readonly<Record<PhysicsErrorCode, string>> = {\n 'wasm-load-failed':\n 'dynamic import() of Rapier WASM rejected; check network, file path, and that @dimforge/rapier3d-compat is installed',\n 'wasm-simd-unsupported':\n 'WebAssembly.validate returned false for the SIMD test module; ensure browser supports WASM SIMD (Chrome 91+, Firefox 89+, Safari 16.4+)',\n 'step-failed':\n 'Rapier World.step threw a WASM trap; check for invalid body parameters or NaN values in transforms',\n 'invalid-body-config':\n 'check mass > 0 for dynamic bodies and valid shape parameters; see PhysicsError.detail.field',\n 'body-not-found':\n 'the entity handle did not resolve to a Rapier rigid body; ensure RigidBody was spawned before calling physics APIs',\n 'collider-not-found':\n 'the entity handle did not resolve to a Rapier collider; ensure Collider was spawned before calling physics APIs',\n 'backend-not-registered':\n \"PhysicsWorld resource not found; use createApp(canvas, { plugins: [physicsPlugin('rapier-3d')] }) or manually register a backend\",\n 'teleport-invalid-body-type':\n 'teleport is only valid for dynamic bodies; static and kinematic bodies have their position managed differently',\n 'controller-requires-kinematic':\n \"moveAndSlide requires a kinematic RigidBody; set the entity's RigidBody.type to 'kinematic'\",\n};\n\n// === RuntimeErrorCode - runtime-layer error code SSOT (feat-20260523-skin-skeleton-animation M0) ===\n//\n// Closed union of runtime-layer error code literals. Defined here as the\n// single source of truth for code-string discovery (charter F1: AI users\n// grep '@forgeax/engine-types' for all error code families). The error\n// classes that carry these codes live in @forgeax/engine-runtime.\n//\n// Decision anchors:\n// - requirements AC-29 (RuntimeErrorCode +6: skin-joint-count-exceeded /\n// skin-joint-despawned / skin-joint-path-unresolved /\n// skin-instances-coexist-forbidden / vertex-storage-buffer-unavailable /\n// skin-palette-overflow)\n// - plan-strategy D-12 (kebab-case + closed union)\n// - charter P3 (explicit failure: exhaustive switch without default)\n\n/** Closed union of runtime-layer error codes. */\nexport type RuntimeErrorCode =\n | 'shadow-invalid-config'\n | 'skin-joint-count-exceeded'\n | 'skin-joint-despawned'\n | 'skin-joint-path-unresolved'\n | 'skin-instances-coexist-forbidden'\n | 'vertex-storage-buffer-unavailable'\n | 'skin-palette-overflow'\n | 'material-resolved-empty-passes'\n | 'equirect-projection-failed'\n | 'mesh-ssbo-capacity-exceeded'\n | 'mesh-ssbo-ceiling-reached'\n | 'hdrp-caps-insufficient'\n | 'hdrp-light-budget-exceeded'\n | 'hdrp-index-list-overflow';\n\n// === GPUFlagsConstant namespace numeric aliases (5 *Flags + 8 Size/Index/Offset/SampleMask) ===\n//\n// One-to-one with the W3C CR §3.6 `unsigned long` definitions; runtime values are\n// surfaced by the global objects (GPUBufferUsage / GPUColorWrite / GPUMapMode /\n// GPUShaderStage / GPUTextureUsage).\n\n/** GPU buffer usage bit flags (OR combination of GPUBufferUsage.MAP_READ / COPY_SRC / ...). */\nexport type BufferUsageFlags = GPUBufferUsageFlags;\n\n/** GPU color write mask bit flags (GPUColorWrite.RED / GREEN / BLUE / ALPHA / ALL). */\nexport type ColorWriteFlags = GPUColorWriteFlags;\n\n/** GPU buffer map mode bit flags (GPUMapMode.READ / WRITE). */\nexport type MapModeFlags = GPUMapModeFlags;\n\n/** GPU shader stage bit flags (GPUShaderStage.VERTEX / FRAGMENT / COMPUTE). */\nexport type ShaderStageFlags = GPUShaderStageFlags;\n\n/** GPU texture usage bit flags (GPUTextureUsage.COPY_SRC / COPY_DST / TEXTURE_BINDING / ...). */\nexport type TextureUsageFlags = GPUTextureUsageFlags;\n\n/** GPU 32-bit unsigned size. */\nexport type Size32 = GPUSize32;\n\n/** GPU 64-bit unsigned size. */\nexport type Size64 = GPUSize64;\n\n/** GPU 32-bit unsigned index. */\nexport type Index32 = GPUIndex32;\n\n/** GPU 32-bit signed offset. */\nexport type SignedOffset32 = GPUSignedOffset32;\n\n/** GPU integer coordinate (used for texture / viewport extents). */\nexport type IntegerCoordinate = GPUIntegerCoordinate;\n\n/** GPU sample mask bit pattern. */\nexport type SampleMask = GPUSampleMask;\n\n/** GPU buffer dynamic offset. */\nexport type BufferDynamicOffset = GPUBufferDynamicOffset;\n\n/** GPU stencil value (reference / read mask / write mask). */\nexport type StencilValue = GPUStencilValue;\n\n// === String literal enum re-exports (already exported by @webgpu/types; we only alias) ===\n\n/** GPU texture format enum (e.g. 'rgba8unorm' / 'depth24plus' / ...). */\nexport type TextureFormat = GPUTextureFormat;\n\n/** GPU texture dimension ('1d' / '2d' / '3d'). */\nexport type TextureDimension = GPUTextureDimension;\n\n/** GPU texture view dimension ('1d' / '2d' / '2d-array' / 'cube' / 'cube-array' / '3d'). */\nexport type TextureViewDimension = GPUTextureViewDimension;\n\n/** GPU compare function ('never' / 'less' / 'equal' / 'less-equal' / 'greater' / 'not-equal' / 'greater-equal' / 'always'). */\nexport type CompareFunction = GPUCompareFunction;\n\n/** GPU filter mode ('nearest' / 'linear'). */\nexport type FilterMode = GPUFilterMode;\n\n/** GPU address mode ('clamp-to-edge' / 'repeat' / 'mirror-repeat'). */\nexport type AddressMode = GPUAddressMode;\n\n/** GPU vertex format enum ('float32' / 'float32x2' / ... 32 variants in total). */\nexport type VertexFormat = GPUVertexFormat;\n\n/** GPU vertex step mode ('vertex' / 'instance'). */\nexport type VertexStepMode = GPUVertexStepMode;\n\n/** GPU index format ('uint16' / 'uint32'). */\nexport type IndexFormat = GPUIndexFormat;\n\n/** GPU primitive topology ('point-list' / 'line-list' / 'line-strip' / 'triangle-list' / 'triangle-strip'). */\nexport type PrimitiveTopology = GPUPrimitiveTopology;\n\n/** GPU triangle cull mode ('none' / 'front' / 'back'). */\nexport type CullMode = GPUCullMode;\n\n/** GPU triangle front-face winding ('ccw' / 'cw'). */\nexport type FrontFace = GPUFrontFace;\n\n/** GPU stencil operation ('keep' / 'zero' / 'replace' / 'invert' / 'increment-clamp' / 'decrement-clamp' / 'increment-wrap' / 'decrement-wrap'). */\nexport type StencilOperation = GPUStencilOperation;\n\n/** GPU blend factor ('zero' / 'one' / 'src' / 'one-minus-src' / ...). */\nexport type BlendFactor = GPUBlendFactor;\n\n/** GPU blend operation ('add' / 'subtract' / 'reverse-subtract' / 'min' / 'max'). */\nexport type BlendOperation = GPUBlendOperation;\n\n/** GPU load op ('load' / 'clear'). */\nexport type LoadOp = GPULoadOp;\n\n/** GPU store op ('store' / 'discard'). */\nexport type StoreOp = GPUStoreOp;\n\n// === Shader pipeline trio SSOT (feat-20260508-shader-pipeline-mvp) =================\n//\n// Decision anchors:\n// - plan-strategy §S-7 + §S-9 (fully-explicit reflection + ShaderError 5-field top level)\n// - requirements §AC-04 (manifest 4 fields) + MVP-2.6 (manifest schema TS SSOT)\n// - research Finding 2 (reflection JSON field-mapping oracle, 9 boundary cases)\n// - charter proposition 4 (explicit failure) + proposition 5 (consistent abstraction:\n// dev-time and runtime errors share one shape)\n\n/**\n * Single shader manifest entry — trio + 4-field manifest SSOT (AC-04).\n *\n * | Field | Shape | Notes |\n * |:--|:--|:--|\n * | `hash` | `string` | content-addressable fingerprint (the on-disk key written by the plugin's `generateBundle`) |\n * | `wgsl` | `string` | WGSL source: relative path or inline literal (the plugin chooses; schema does not constrain) |\n * | `glsl` | `string \\| undefined` | GLSL placeholder (empty string or undefined within M1 scope; reserved for the non-WebGL fallback path) |\n * | `bindings` | `string` | `BindGroupLayoutDescriptor[]` serialized as a JSON string (output derived from reflection) |\n *\n * Written by `@forgeax/engine-shader-compiler`, persisted by `@forgeax/engine-vite-plugin-shader`,\n * loaded and consumed by `@forgeax/engine-shader` — the schema's single source of truth lives\n * in this package across all three sides (charter proposition 5: consistent abstraction).\n */\nexport interface ManifestEntry {\n readonly hash: string;\n readonly wgsl: string;\n readonly glsl: string | undefined;\n readonly bindings: string;\n}\n\n/**\n * Shader compile-time error-code closed union — 7 members\n * (feat-20260512-naga-oil-composition-hmr M3 T-09 extension; D-R7 / S-7 /\n * OQ-2 legacy 4 + D-08 new 3 for naga_oil composition).\n *\n * Symmetric in shape with `@forgeax/engine-rhi`'s `RhiErrorCode` closed union\n * (AGENTS.md error model); exhaustive `switch` needs no default fallback —\n * TypeScript guards union completeness at compile time (charter proposition 4\n * explicit failure / proposition 3 machine-readable union > prose).\n *\n * Evolution: minor-add (requirements §AC-08). The 4 legacy positions remain\n * byte-for-byte at the top (AGENTS.md `Evolution contract`: members can be\n * added only — no rename / delete / reorder). The 3 new members appear at the\n * bottom:\n * - `shader-import-not-found` — naga_oil `ImportNotFound` variant surfaces\n * when `#import <moduleId>::<symbol>` cannot bind to any module registered\n * through `options.imports` (plan-strategy D-08 + D-12 offset passthrough).\n * - `shader-circular-import` — TS-layer DFS (T-11 `detectCycle`) catches\n * `a -> b -> a` style import cycles before calling into the wasm composer\n * (plan-strategy D-03 path A + D-04 cycle first/last repetition form).\n * - `shader-define-conflict` — TS-layer pre-scan (T-12 `scanDefineConflicts`)\n * rejects the same `#define NAME` appearing in >=2 modules (plan-strategy\n * D-07; prevents naga_oil HashMap silent override from research R-07).\n *\n * | code | Trigger |\n * |:--|:--|\n * | `'shader-compile-failed'` | naga `parse_str` / `Validator` failed; also the fallback for any non-ImportNotFound naga_oil ComposerError variant (plan-strategy D-05 non-boolean #define value goes here, never a new 8th member). |\n * | `'compiler-init-failed'` | wasm load / `init()` failed (cold start / missing wasm artifact). |\n * | `'manifest-malformed'` | manifest.json schema validation failed (4 fields missing or JSON unparseable). |\n * | `'shader-not-found'` | `ShaderRegistry.get(hash)` hash miss. |\n * | `'shader-import-not-found'` | `#import <moduleId>` target absent from `options.imports` (or lacks `#define_import_path` header). `err.detail.importPath` + `err.detail.fromModuleId` narrow after the switch. |\n * | `'shader-circular-import'` | import dependency graph contains a cycle; `err.detail.cycle` carries the full chain with first/last repeated (D-04). |\n * | `'shader-define-conflict'` | same `#define NAME` declared in multiple modules; `err.detail.sites[]` lists each offending moduleId. |\n */\nexport type ShaderErrorCode =\n | 'shader-compile-failed'\n | 'compiler-init-failed'\n | 'manifest-malformed'\n | 'shader-not-found'\n | 'shader-import-not-found'\n | 'shader-circular-import'\n | 'shader-define-conflict'\n // === 5 new material-* codes (feat-20260523-shader-template-instance-split M1-T02) ===\n | 'material-schema-mismatch'\n | 'material-shader-not-found'\n | 'material-param-type-mismatch'\n | 'material-param-unknown'\n | 'material-param-missing-required'\n // === build-time superset gate (feat-20260613-material-paramschema-driven-binding M2 / w9) ===\n | 'material-shader-binding-mismatch';\n\n// === Shader error detail discriminated union (feat-20260512 M3 T-09 / D-08) =====\n//\n// Decision anchors:\n// - plan-strategy §2 D-08 (3 new typed variants keyed on `code`, structurally\n// parallel to `RhiErrorDetail` in `packages/rhi/src/errors.ts` lines\n// 165-189; 4 legacy members stay as prose detail for backwards compat).\n// - plan-strategy §2 D-04 (cycle first/last repetition form: ['a','b','a']).\n// - plan-strategy §2 D-12 (ImportNotFound offset passthrough when naga_oil\n// carries a source position on the inner variant).\n// - requirements §AC-08 (AGENTS.md §Error model ShaderErrorDetail row 3\n// variants) + §AC-15 (property access over string parsing).\n// - charter proposition 3 (machine-readable union > prose) + proposition 4\n// (explicit failure — narrow via `switch (err.detail.code)` after the\n// `switch (err.code)` tier).\n// - architecture-principles #1 SSOT (3 typed variants live here once;\n// producer site `packages/shader-compiler/src/error-mapper.ts` constructs\n// them verbatim; AGENTS.md §Error model table references this module).\n\n/**\n * Detail for the `shader-import-not-found` path (D-08 + D-12).\n *\n * `importPath` mirrors the bare `#import` target string (`'forgeax_pbr::brdf'`).\n * `fromModuleId` identifies the entry module that issued the unresolved\n * import; when the caller omitted `options.id`, this carries the\n * `<anonymous-entry-<hash8>>` placeholder (plan-strategy D-11). Optional\n * `offset` passes through the naga_oil inner-variant byte offset when present\n * (D-12); AI users surface this in error logs for IDE jump-to-source.\n */\nexport interface ShaderImportNotFoundDetail {\n readonly code: 'shader-import-not-found';\n readonly importPath: string;\n readonly fromModuleId: string;\n readonly offset?: number;\n}\n\n/**\n * Detail for the `shader-circular-import` path (D-08 + D-04).\n *\n * `cycle` lists the full import chain with the first and last element\n * repeated so consumers can visualise the loop at a glance\n * (`['a','b','c','a']`). The array is `readonly` so copy-out sites cannot\n * mutate the structure post-emit (charter proposition 4 explicit failure).\n */\nexport interface ShaderCircularImportDetail {\n readonly code: 'shader-circular-import';\n readonly cycle: readonly string[];\n}\n\n/**\n * Detail for the `shader-define-conflict` path (D-08 + D-07).\n *\n * `defineName` names the offending `#define NAME` literal; `sites` lists each\n * moduleId that declared it so the AI user can navigate to every duplicate\n * without re-scanning the source set (charter proposition 3 machine-readable\n * > prose).\n */\nexport interface ShaderDefineConflictDetail {\n readonly code: 'shader-define-conflict';\n readonly defineName: string;\n readonly sites: readonly { readonly moduleId: string }[];\n}\n\n/**\n * Detail for the `shader-compile-failed` path\n * (feat-small-20260513-dx-docs-types-cleanup D-9 / requirements §3.1.7 (A)).\n *\n * `compilerMessages` forwards the full 6 fields of `GPUCompilationMessage`\n * from `@webgpu/types ^0.1.69` (`message` / `type` / `lineNum` / `linePos` /\n * `offset` / `length`); the array is `readonly` so copy-out sites cannot\n * mutate the structure post-emit (charter proposition 4 explicit failure).\n * Optional `reason` carries a prose supplement when the wasm side surfaces a\n * higher-level summary alongside the raw compiler frame.\n *\n * @see RhiShaderCompileDetail in @forgeax/engine-rhi for the RhiError parallel\n * (R-7 namespace separation: `ShaderError.detail` vs `RhiError.detail` cover\n * disjoint lifecycle phases — compile-time vs async runtime dispatch — and\n * AI users distinguish them by import path).\n */\nexport interface ShaderCompileFailedDetail {\n readonly code: 'shader-compile-failed';\n readonly compilerMessages: readonly GPUCompilationMessage[];\n readonly reason?: string;\n}\n\n/**\n * Detail for the `compiler-init-failed` path\n * (feat-small-20260513-dx-docs-types-cleanup D-9 / requirements §3.1.7 (A)).\n *\n * Constructed by `@forgeax/engine-naga` when the wasm cold start fails\n * (`ensureReady()` rejects, the artefact is missing, or `init()` itself\n * throws). The `code` literal narrows `.detail` after the top-level\n * `switch (err.code)`; optional `reason` carries the wasm-side error message\n * when available (charter proposition 3 machine-readable union > prose).\n */\nexport interface ShaderInitFailedDetail {\n readonly code: 'compiler-init-failed';\n readonly reason?: string;\n}\n\n/**\n * Detail for the `manifest-malformed` path\n * (feat-small-20260513-dx-docs-types-cleanup D-9 / requirements §3.1.7 (A)).\n *\n * Constructed by `@forgeax/engine-naga` / `@forgeax/engine-shader-compiler`\n * when the shader manifest fails the 4-field schema (`{hash, wgsl, glsl,\n * bindings}`) or the JSON itself is unparseable. Optional `reason` carries\n * the schema validator or `JSON.parse` error message when available\n * (charter proposition 4 explicit failure: typed `.reason` access never\n * requires parsing `.message`).\n */\nexport interface ShaderManifestMalformedDetail {\n readonly code: 'manifest-malformed';\n readonly reason?: string;\n}\n\n// === 5 new material-* ShaderErrorDetail variants (feat-20260523-shader-template-instance-split M1-T02) ===\n//\n// Decision anchors:\n// - plan-strategy D-NewErrorCodes-Anchor (5 ShaderErrorCode + 5 detail variants in types SSOT)\n// - plan-strategy F-6 round 2 (material-schema-mismatch.mismatchKind is 4-element union:\n// schema-extra | shader-extra | type-mismatch | bg-overflow)\n// - requirements AC-12 (each new error code has structured detail)\n\n/**\n * Detail for `material-schema-mismatch` — paramSchema vs BGL mismatch at build-time.\n *\n * `mismatchKind` narrows on the 4-way mismatch category (F-6 round 2):\n * - 'schema-extra': paramSchema declares a name not in BGL\n * - 'shader-extra': BGL has a binding not in paramSchema\n * - 'type-mismatch': param type differs from BGL entry type\n * - 'bg-overflow': binding group count exceeds maxBindGroups (4) — AC-07\n *\n * Optional `expectedParam` / `actualBinding` carry the specific mismatch detail\n * for schema-extra / shader-extra / type-mismatch variants. `actualCount` /\n * `maxAllowed` populated for bg-overflow.\n */\nexport interface MaterialSchemaMismatchDetail {\n readonly code: 'material-schema-mismatch';\n readonly mismatchKind: 'schema-extra' | 'shader-extra' | 'type-mismatch' | 'bg-overflow';\n readonly materialShaderPath: string;\n readonly expectedParam?: string;\n readonly actualBinding?: number;\n readonly actualCount?: number;\n readonly maxAllowed?: number;\n}\n\n/**\n * Detail for `material-shader-not-found` — ShaderRegistry lookup miss.\n */\nexport interface MaterialShaderNotFoundDetail {\n readonly code: 'material-shader-not-found';\n readonly identifier: string;\n}\n\n/**\n * Detail for `material-param-type-mismatch` — a material value does not match\n * paramSchema expected type at runtime register.\n */\nexport interface MaterialParamTypeMismatchDetail {\n readonly code: 'material-param-type-mismatch';\n readonly paramName: string;\n readonly expectedType: string;\n readonly actualValue: unknown;\n}\n\n/**\n * Detail for `material-param-unknown` — material values contain a key not in\n * paramSchema.\n */\nexport interface MaterialParamUnknownDetail {\n readonly code: 'material-param-unknown';\n readonly paramName: string;\n}\n\n/**\n * Detail for `material-param-missing-required` — material values miss a key\n * that paramSchema declares without a default.\n */\nexport interface MaterialParamMissingRequiredDetail {\n readonly code: 'material-param-missing-required';\n readonly paramName: string;\n}\n\n/**\n * Detail for `material-shader-binding-mismatch` — vite-plugin-shader build-time\n * single-direction superset gate (feat-20260613-material-paramschema-driven-\n * binding M2 / D-9 / D-10).\n *\n * The actual reflected BGL must contain every binding emitted by\n * derive(schema); otherwise the build fails with this code. Extra bindings on\n * the actual side are tolerated (engine-injection placeholders such as shadow\n * / IBL / lightmap bind groups land at register-time).\n *\n * `expected` is the BGL entry derive(schema) emitted (the binding number +\n * resource layout the shader source must declare). `actual` is the entry the\n * reflector found at the same binding number, or `undefined` when the binding\n * is absent altogether. `expectedParam` names the paramSchema entry that\n * produced `expected` so AI users can grep the sidecar quickly. `mismatchKind`\n * narrows the failure category for AI-side branching.\n */\nexport interface MaterialShaderBindingMismatchDetail {\n readonly code: 'material-shader-binding-mismatch';\n readonly mismatchKind: 'binding-missing' | 'binding-type-mismatch';\n readonly materialShaderPath: string;\n readonly expected: BindGroupLayoutEntry;\n readonly actual?: BindGroupLayoutEntry;\n readonly expectedParam: string;\n}\n\n/**\n * Discriminated union of the 6 typed `.detail` variants keyed on `code`\n * (D-08 legacy 3 variants + feat-small-20260513-dx-docs-types-cleanup D-9\n * minor-add 3 variants; parallel to `RhiErrorDetail` lines 165-189 of\n * `packages/rhi/src/errors.ts`).\n *\n * AI users narrow to the per-code shape after the top-level\n * `switch (err.code)` via the nested `if (err.detail?.code === '<literal>')`\n * guard — `err.detail.compilerMessages` / `err.detail.importPath` /\n * `err.detail.reason` etc. are then typed property accesses with full IDE\n * autocomplete (charter proposition 3 machine-readable union > prose +\n * proposition 4 explicit failure).\n *\n * The 7th member `'shader-not-found'` has no typed detail variant — the naga\n * `shaderNotFound` factory leaves `.detail` undefined because the surface\n * carries no per-instance payload (the `hash` is already embedded in\n * `.message` / `.expected`; OOS-11 deferring a typed variant).\n *\n * Listed in the same order as the corresponding `ShaderErrorCode` members so\n * a reviewer can grep the two unions vertically for drift\n * (T-09 acceptance check ties `ShaderErrorDetail` grep hit to this layout).\n */\nexport type ShaderErrorDetail =\n | ShaderImportNotFoundDetail\n | ShaderCircularImportDetail\n | ShaderDefineConflictDetail\n | ShaderCompileFailedDetail\n | ShaderInitFailedDetail\n | ShaderManifestMalformedDetail\n // === 5 new material-* detail variants (feat-20260523-shader-template-instance-split M1-T02) ===\n | MaterialSchemaMismatchDetail\n | MaterialShaderNotFoundDetail\n | MaterialParamTypeMismatchDetail\n | MaterialParamUnknownDetail\n | MaterialParamMissingRequiredDetail\n // === build-time superset gate (feat-20260613-material-paramschema-driven-binding M2 / w9) ===\n | MaterialShaderBindingMismatchDetail;\n\n/**\n * Bind group layout descriptor — shape-aligned with\n * `Pick<GPUBindGroupLayoutDescriptor, 'entries' | 'label'>` (S-9 / AC-04).\n *\n * **Shape rules**:\n * - `entries` is narrowed here to a concrete `readonly BindGroupLayoutEntry[]` (the\n * spec uses `Iterable<...>`; reflection-derived output is always an array shape).\n * - All optional fields are uniformly `?: T | undefined` (guarded by\n * exactOptionalPropertyTypes).\n * - Field names match `@webgpu/types` exactly, character for character (spec-alignment rule).\n *\n * **Fully-explicit reflection JSON constraint** (plan-strategy §S-9 / D-R9):\n * the `bindings` JSON emitted by `@forgeax/engine-shader-compiler` must populate every default\n * field defined in W3C spec §5 (e.g. `hasDynamicOffset: false` / `minBindingSize: 0`);\n * `visibility` is output as the `GPUShaderStage` integer bitmask (VERTEX=0x1 /\n * FRAGMENT=0x2 / COMPUTE=0x4 OR-ed together) — string-array form is **forbidden**.\n * This type only describes the schema shape; full explicitness is enforced on the\n * producer side.\n */\nexport interface BindGroupLayoutDescriptor {\n readonly label?: string | undefined;\n readonly entries: readonly BindGroupLayoutEntry[];\n}\n\n/**\n * Single bind group layout entry — shape-aligned with\n * `@webgpu/types.GPUBindGroupLayoutEntry`.\n *\n * `binding` / `visibility` are required; the four resource layouts (buffer / sampler /\n * texture / storageTexture) form the \"exactly one set\" constraint per W3C spec §5\n * (`externalTexture` is out of scope for the forgeax MVP and is not surfaced here yet).\n */\nexport interface BindGroupLayoutEntry {\n readonly binding: GPUIndex32;\n readonly visibility: GPUShaderStageFlags;\n readonly buffer?: GPUBufferBindingLayout | undefined;\n readonly sampler?: GPUSamplerBindingLayout | undefined;\n readonly texture?: GPUTextureBindingLayout | undefined;\n readonly storageTexture?: GPUStorageTextureBindingLayout | undefined;\n}\n\n// === RemoteHandle (feat-20260629-inspector-two-layer-model M4 / w17) ========\n//\n// Decision anchors:\n// - plan-strategy secondary D-6: RemoteHandle defined in @forgeax/engine-types\n// (neutral package, no temporal coupling to @forgeax/engine-remote)\n// - requirements AC-11: app.remote typed as RemoteHandle | undefined,\n// exposed on the createApp return value for host inspection\n//\n// Shape:\n// port — number, the listen port (determined by the server on startup)\n// close — Promise<void>, tear down the server (Surface Plugin pattern\n// from startServer's returned ConsoleHandle)\n\n/**\n * Handle for a running remote eval server (feat-20260629-inspector-two-layer-model M4).\n *\n * AI users access `app.remote.port` for WS connection / status, and call\n * `await app.remote.close()` to tear down. The field is `undefined` when the\n * server is not started (production build or headless without opt-in).\n *\n * @see {@link startServer} in @forgeax/engine-remote for the producer side\n */\nexport interface RemoteHandle {\n /** Server listen port (number). Non-zero when the server is running. */\n readonly port: number;\n /** Tear down the server. Returns a Promise that resolves once the WS\n * server has closed all connections. */\n close(): Promise<void>;\n}\n\n// === Remote error model SSOT (feat-20260629-inspector-two-layer-model) ====\n//\n// Decision anchors:\n// - requirements §10.1 + §10.2 + AC-05 (`RemoteErrorCode` 5-member\n// closed union + structured `RemoteError` shape independent from RhiError /\n// ShaderError)\n// - plan-strategy §2 D-5 (rename InspectorErrorCode -> RemoteErrorCode,\n// delete inspector-write-denied, delete script-timeout, rename\n// console-* -> server-*)\n// - charter proposition 3 (machine-readable union > prose) +\n// proposition 4 (explicit failure — `switch (err.code)` is exhaustive\n// without default fallback) + proposition 5 (consistent abstraction —\n// structurally aligned with @forgeax/engine-rhi's RhiError surface)\n// - architecture-principles #1 SSOT (the 5 string literals + structured\n// structural shape live here once; @forgeax/engine-remote's runtime `RemoteError`\n// class implements this interface; consumers import the type\n// alias without dragging the runtime class through static deps —\n// parallel to the existing ShaderErrorCode pattern)\n\n/**\n * Closed `RemoteErrorCode` union — 5 members (feat-20260629-inspector-two-layer-model\n * D-5; requirements AC-05). Exhaustive `switch` needs no default\n * fallback — TypeScript guards union completeness at compile time\n * (charter proposition 4 explicit failure + proposition 3 machine-readable\n * union > prose).\n *\n * | code | trigger |\n * |:--|:--|\n * | `'script-syntax-error'` | Script body is not parseable JavaScript (SyntaxError from eval). |\n * | `'script-runtime-error'` | Script threw a non-syntax exception during execution (e.g. ReferenceError / TypeError). |\n * | `'server-startup-failed'` | The remote eval server failed to come up: WebSocketServer raised 'error' (EADDRINUSE / other listen failure), dynamic-import resolution failed, or the target package lacks the `startServer` factory. |\n * | `'server-not-running'` | CLI client's `new WebSocket('ws://localhost:<port>/inspector')` failed to connect (server not started; `app.remote` not wired in the demo). |\n * | `'eval-result-not-serializable'` | A successful eval result cannot cross the JSON-RPC wire, such as a BigInt or cyclic object. |\n *\n * **Independence from `RhiError | ShaderError` union** — `RemoteErrorCode`\n * is **not** merged into the GPU / asset error union (charter proposition 5 +\n * architecture-principles #1 SSOT). Engine-side errors stream is OOS-1\n * (errors.subscribe v2 spinoff); remote callers only face these 5\n * alternatives.\n */\nexport type RemoteErrorCode =\n | 'script-syntax-error'\n | 'script-runtime-error'\n | 'server-startup-failed'\n | 'server-not-running'\n | 'eval-result-not-serializable';\n\n/**\n * Structural shape of a forgeax remote error (feat-20260629-inspector-two-layer-model\n * D-5). Structured surface mirroring `@forgeax/engine-rhi` `RhiError`\n * (charter proposition 5 consistent abstraction; AGENTS.md \"Errors are\n * structured\"):\n *\n * - `.code` closed union member (L1 key signal; switch-able).\n * - `.expected` expected-state description (L2 detail).\n * - `.hint` actionable recovery guidance (L2 detail).\n * - `.message` auto-composed string for human stack traces (AI users\n * prefer property access on `.code` / `.expected` / `.hint`).\n * - `.name` Error name marker (`'RemoteError'`) for cross-realm\n * dispatch under JSON-RPC transport.\n *\n * This interface intentionally extends `Error` so a runtime `RemoteError`\n * **class** (defined in `@forgeax/engine-remote/errors`) satisfies the contract\n * without re-declaring the inherited `name` / `message` slots.\n *\n * AI users consume the structured triple via property access — never by\n * parsing `.message` (charter proposition 4 explicit failure red line).\n */\nexport interface RemoteError extends Error {\n readonly code: RemoteErrorCode;\n readonly expected: string;\n readonly hint: string;\n /**\n * Optional discriminated detail payload (feat-20260517 D-7). Per-code\n * variant carries structured provenance that would otherwise pollute the\n * single-line `.hint` copy. AI users narrow via `switch (err.code)`; the\n * `.detail` slot is `undefined` for codes whose discriminator has no\n * payload (charter P4 explicit failure: signal absence by type).\n */\n readonly detail?: RemoteErrorDetail;\n}\n\n/**\n * Discriminated detail union for {@link RemoteError} (feat-20260517 D-7).\n * Each variant pairs a {@link RemoteErrorCode} member with the\n * structured payload AI users need to act on the error without grepping\n * prose. Variants without payload are intentionally absent — the\n * `RemoteError.detail` slot is `undefined` for those codes.\n *\n * The `server-startup-failed` variant carries bounded startup provenance.\n */\nexport type RemoteErrorDetail = ServerStartupFailedDetail | EvalResultNotSerializableDetail;\n\n/**\n * `server-startup-failed` discriminator variant with bounded provenance.\n */\nexport interface ServerStartupFailedDetail {\n readonly code: 'server-startup-failed';\n readonly removedAt: string;\n readonly docAnchor: string;\n}\n\n/**\n * SSOT for the legacy-inspect routing hint template. Keep the recovery copy\n * executable and byte-stable for CLI and remote consumers.\n */\nexport function legacyInspectHint(legacyInspectTarget: string): string {\n return `did you mean 'forgeax-engine-remote-ecs ${legacyInspectTarget}'?`;\n}\n\n/**\n * `eval-result-not-serializable` discriminator variant. The shape is a\n * bounded classification only; it carries no part of the returned object\n * so failed transport cannot leak arbitrary engine state.\n */\nexport interface EvalResultNotSerializableDetail {\n readonly code: 'eval-result-not-serializable';\n readonly shape: 'bigint' | 'cyclic-object' | 'unsupported';\n}\n\n// === Metric registry error model SSOT (feat-20260512-threejs-pixel-parity-bench) ===\n//\n// Decision anchors:\n// - requirements §3.5 + AC-04 + AC-05 + AC-11 (`MetricErrorCode` 4-member closed\n// union elevated to TS alias; B-1 regression-prevention callout — exhaustive\n// `switch (err.code)` without `default:` must compile under tsc strict)\n// - plan-strategy §2 D-P3 (MetricErrorCode TS alias goes first in the topology;\n// M1 T-001 ships only the 4 legacy members verbatim from AGENTS.md Error\n// model table)\n// - research Finding 9 (`MetricErrorCode` currently has zero TS alias = direct\n// B-1 regression risk; §6 g9 checklist item 1: introduce\n// `export type MetricErrorCode = ...` in `packages/types/src/index.ts`,\n// structurally parallel to ShaderErrorCode / RemoteErrorCode)\n// - charter proposition 3 (machine-readable union > prose) + proposition 4\n// (explicit failure — closed-union exhaustive switch needs no default fallback;\n// tsc strict mode guards completeness) + proposition 5 (consistent abstraction —\n// structurally aligned with @forgeax/engine-rhi RhiError and RemoteError)\n// - architecture-principles #1 SSOT (the 4 string literals live here once;\n// `scripts/check-metrics-declared.mjs` / `scripts/metrics/run-all.mjs` /\n// `scripts/metrics/run-fps.mjs` are .mjs producer sites that emit the same\n// literals at throw points; parallel to ShaderErrorCode pattern)\n\n/**\n * Closed `MetricErrorCode` union — 4 members (M1 T-001 elevation of the\n * pre-existing 4 `.mjs` producer literals to a TS alias; research Finding 9\n * §6 g9 checklist item 1). Exhaustive `switch` needs no default fallback —\n * TypeScript guards union completeness at compile time (charter proposition 4\n * explicit failure + proposition 3 machine-readable union > prose).\n *\n * | code | trigger |\n * |:--|:--|\n * | `'metric-not-declared'` | a workspace member lacks `package.json#forgeax.metrics` or the declaration is not a plain object; emitted by `scripts/check-metrics-declared.mjs` + `scripts/metrics/run-all.mjs`. |\n * | `'metric-kind-unknown'` | `forgeax.metrics` contains a key not in the closed `MetricKind` union (`bundle-size` / `fps` / `bench` / `gate` / `spike-report`); typo guard via ajv `additionalProperties: false`. |\n * | `'metric-status-not-ok'` | dispatcher (bundle-size / bench / gate / fps / spike-report) returned `status !== 'ok'`; the offending `report/<package>/<kind>.json` carries the value-vs-threshold detail. |\n * | `'metric-schema-malformed'` | `forgeax-metrics.schema.json` failed to parse / compile as JSON Schema 2020-12; precondition failure surfaced by both `check-metrics-declared.mjs` and `run-all.mjs`. |\n *\n * **B-1 regression prevention** (requirements AC-05 + AC-11): an alias without\n * a TS consumer site cannot be exhaustively switched; M1 T-002 adds type-level\n * tests against this alias, and M2 evaluator + M2 runner CLI add the two\n * non-test exhaustive `switch (err.code)` consumer sites (D-P9 plan-strategy\n * decision).\n *\n * Per-feat extension to 6 members (M1 T-002, D-P3): `'pixel-parity-threshold-exceeded'`\n * + `'pixel-parity-capture-failed'` extend the alias at the bottom; AGENTS.md\n * Error model table flips from `(4)` to `(6)` in lockstep. The two new members\n * encode the double-gate of the pixel-parity bench (research Finding 10 +\n * plan-strategy D-P2): Layer A per-pixel YIQ tolerance ` perPixelThreshold` is\n * pixelmatch-internal and never raises on its own; Layer B aggregate cap\n * `threshold` raises `'pixel-parity-threshold-exceeded'`; any capture-side\n * failure (chromium launch / vite preview / readPixels / size mismatch /\n * pixelmatch internal throw) collapses into `'pixel-parity-capture-failed'`\n * with a `.detail.stage` discriminator (charter proposition 5 consistent\n * abstraction — pixelmatch internal exception does NOT get a third member;\n * see D-P3 decision rationale).\n */\nexport type MetricErrorCode =\n | 'metric-not-declared'\n | 'metric-kind-unknown'\n | 'metric-status-not-ok'\n | 'metric-schema-malformed'\n | 'pixel-parity-threshold-exceeded'\n | 'pixel-parity-capture-failed';\n\n/**\n * Per-code detail shape for the four legacy `MetricErrorCode` members\n * (`'metric-not-declared'` / `'metric-kind-unknown'` / `'metric-status-not-ok'`\n * / `'metric-schema-malformed'`).\n *\n * The four legacy `.mjs` producer sites (`scripts/check-metrics-declared.mjs`,\n * `scripts/metrics/run-all.mjs`, `scripts/metrics/run-fps.mjs`) emit textual\n * `[reason] / [hint]` lines and never carry a structured payload — they live\n * in CI-only scripts and exit 1 directly. The `.detail` slot is therefore left\n * `undefined` so AI consumers do not waste a narrowing step looking for a\n * non-existent payload (charter proposition 4 explicit failure: signal absence\n * by type).\n */\nexport interface MetricLegacyDetail {\n readonly stage?: undefined;\n}\n\n/**\n * Detail shape exclusive to the `'pixel-parity-threshold-exceeded'` path\n * (M1 T-002 / D-P11). Carries the full numeric verdict so AI users can\n * surface the value-vs-threshold delta in stderr / sticky-comment renderings\n * without parsing `.message`.\n *\n * | Field | Meaning |\n * |:--|:--|\n * | `diffPixelCount` | Aggregate count from `pixelmatch(left, right, ...)` (Layer B reading). |\n * | `diffPercent` | `diffPixelCount / (width * height)` rendered as a 0..1 float for sticky-comment formatting. |\n * | `maxChannelDelta` | Maximum per-channel uint8 delta across all differing pixels (0..255). Helps disambiguate \"many tiny diffs\" from \"few big diffs\". |\n * | `threshold` | The declared Layer B integer cap (`package.json#forgeax.metrics.bench.pixelDiff.threshold`). |\n * | `perPixelThreshold` | The Layer A `pixelmatch` per-pixel YIQ float threshold actually used; equals the declared value or the `0.1` fallback (D-P2 default semantics). |\n *\n * The exhaustive discriminator is `code === 'pixel-parity-threshold-exceeded'`\n * — AI users access `.detail.diffPixelCount` directly after the type guard\n * with full IDE autocomplete (charter proposition 3 machine-readable union >\n * prose; AI-user review F-1 IDE autocomplete affordance).\n */\nexport interface ParityThresholdDetail {\n readonly diffPixelCount: number;\n readonly diffPercent: number;\n readonly maxChannelDelta: number;\n readonly threshold: number;\n readonly perPixelThreshold: number;\n}\n\n/**\n * Detail shape exclusive to the `'pixel-parity-capture-failed'` path (M1 T-002\n * / D-P11). Carries a discriminator `.stage` that pinpoints which step of the\n * capture pipeline collapsed (charter proposition 5 consistent abstraction:\n * pixelmatch-internal throw becomes `.stage='diff'` rather than a third\n * `MetricErrorCode` member — plan-strategy D-P3 decision).\n *\n * | `.stage` | trigger |\n * |:--|:--|\n * | `'chromium-launch'` | `chromium.launch({...})` threw (research Finding 6: `--enable-unsafe-webgpu` flag still rejected on the host). |\n * | `'vite-preview'` | spawned vite preview never reached `wait-on tcp 30s` (research Finding 4 cleanup pattern). |\n * | `'pixel-readback'` | `gl.readPixels(...)` or `commandEncoder.copyTextureToBuffer(...)` failed, or `window.__captureLeft/Right` was missing. |\n * | `'size-mismatch'` | left and right `Uint8Array.length` differ; `leftSize` / `rightSize` carry the actual byte counts. |\n * | `'diff'` | `pixelmatch(left, right, ...)` itself threw (charter proposition 4 explicit failure: no silent catch; EC-06). |\n *\n * Optional `leftSize` / `rightSize` are populated for the `'size-mismatch'`\n * stage; they are absent for the other stages because the failure happened\n * before any byte count was known.\n */\nexport interface ParityCaptureDetail {\n readonly stage: 'chromium-launch' | 'vite-preview' | 'pixel-readback' | 'size-mismatch' | 'diff';\n readonly leftSize?: number;\n readonly rightSize?: number;\n /**\n * Optional human-readable cause string for the failure (typically the\n * caught `Error.message` text or an inferred reason). Aligned with ECMA\n * 2022 `Error.cause` naming convention so IDE hover invokes the same mental\n * model. Filled by `scripts/bench/pixel-parity.mjs` at every stage that\n * surfaces a non-empty message; absent when the failure is purely\n * structural (e.g. `'size-mismatch'` where `leftSize` / `rightSize` carry\n * the diagnostic payload instead).\n */\n readonly cause?: string;\n}\n\n/**\n * Non-optional detail projection carried by `MetricError`.\n *\n * `MetricError` owns the complete code-to-detail relation. `NonNullable` removes\n * only the four legacy absence markers; parity payloads and the legacy detail\n * shape remain in the public family without a second manually maintained list.\n */\nexport type MetricErrorDetail = NonNullable<MetricError['detail']>;\n\n/**\n * Structural shape of a forgeax metric error (feat-20260512 T-002).\n *\n * Three-field surface (`.code` / `.expected` / `.hint`) plus per-code-narrowed\n * `.detail`, structurally aligned with `@forgeax/engine-rhi` `RhiError` and\n * `InspectorError` (charter proposition 5 consistent abstraction; AGENTS.md\n * \"Errors are structured. Return Result, never throw for expected failures\").\n *\n * `MetricError` is a TypeScript discriminated union of 6 per-code interfaces;\n * each variant narrows `.detail` to the corresponding `MetricErrorDetail`\n * branch. AI users perform a single `switch (err.code)` and pick up\n * `.detail.diffPixelCount` (threshold-exceeded path) or `.detail.stage`\n * (capture-failed path) with full IDE autocomplete (AI-user review F-1\n * affordance; D-P11).\n *\n * - `.code` closed union member (L1 key signal; switch-able).\n * - `.expected` expected-state description (L2 detail; mirrors the `[reason]`\n * line emitted by `failStructured(...)` in the three `.mjs`\n * producer sites).\n * - `.hint` actionable recovery guidance (L2 detail; mirrors the `[hint]`\n * line in `failStructured(...)`).\n * - `.detail` path-specific structured payload narrowed per `.code`.\n *\n * AI users consume the structured triple via property access — never by\n * parsing `.message` (charter proposition 4 explicit failure red line).\n */\nexport type MetricError =\n | (MetricErrorBase & {\n readonly code: 'metric-not-declared';\n readonly detail?: MetricLegacyDetail | undefined;\n })\n | (MetricErrorBase & {\n readonly code: 'metric-kind-unknown';\n readonly detail?: MetricLegacyDetail | undefined;\n })\n | (MetricErrorBase & {\n readonly code: 'metric-status-not-ok';\n readonly detail?: MetricLegacyDetail | undefined;\n })\n | (MetricErrorBase & {\n readonly code: 'metric-schema-malformed';\n readonly detail?: MetricLegacyDetail | undefined;\n })\n | (MetricErrorBase & {\n readonly code: 'pixel-parity-threshold-exceeded';\n readonly detail: ParityThresholdDetail;\n })\n | (MetricErrorBase & {\n readonly code: 'pixel-parity-capture-failed';\n readonly detail: ParityCaptureDetail;\n });\n\n/**\n * Common base of every `MetricError` variant (D-P11 internal helper —\n * never instantiated on its own, only intersected into the per-code\n * branches of `MetricError`).\n */\ninterface MetricErrorBase {\n readonly code: MetricErrorCode;\n readonly expected: string;\n readonly hint: string;\n}\n\n// === Pack-index catalog entry POD (feat-20260517-vite-plugin-image-build-time-cook D-2) ===\n//\n// PackIndexEntry is the in-memory shape of one row in `pack-index.json` (build\n// path) and `/__pack/index` JSON response (dev path). It is the SSOT contract\n// between the build-time catalog builder (`@forgeax/engine-vite-plugin-pack`)\n// and the runtime asset loader (`@forgeax/engine-runtime` `parseAssetPayload`).\n//\n// Decision anchors:\n// - plan-strategy D-2 (5-field metadata sub-structure: width / height /\n// format / colorSpace / mipmap, mirrors TextureAsset POD field names so\n// `metadata.colorSpace` greps to the same surface across catalog / POD /\n// sidecar).\n// - plan-strategy D-5 (sidecar `mipmap: 'auto' | 'none'` is mapped to the\n// `boolean` form by the catalog builder; runtime is unaware of the\n// string token).\n// - charter P1 (progressive disclosure -- core 4 fields stay flat,\n// image-only metadata sinks into a sub-structure that texture-arm\n// consumers narrow into).\n// - charter P4 (consistent abstraction -- `metadata` field-by-field\n// mirrors `TextureAsset` POD field names; `width` / `height` / `format`\n// / `colorSpace` / `mipmap` align byte-for-byte).\n//\n// Backward compatibility (D-2 'minor' evolution):\n// - `metadata` is `?: ImageMetadata | undefined` -- legacy 4-field entries\n// emitted by older builds (or future non-texture kinds: 'mesh' / 'scene' /\n// 'material') stay valid; runtime consumers narrow on `entry.metadata !==\n// undefined` before accessing fields.\n// - The interface stays open over `kind` (string) so future 'audio' /\n// 'video' arms can join without re-typing PackIndexEntry; the texture\n// arm narrows via `entry.kind === 'texture'` + `entry.metadata`\n// existence in `parseAssetPayload`.\n\n/**\n * Metadata sub-structure carried by `PackIndexEntry` rows of `kind: 'texture'`.\n *\n * Five fields mirror `TextureAsset` POD field names (`width` / `height` /\n * `format` / `colorSpace` / `mipmap`) so AI users can grep one identifier and\n * see the same surface in catalog rows, sidecar `*.meta.json`\n * `importSettings`, and the runtime `TextureAsset` POD (charter P4 consistent\n * abstraction).\n *\n * `width` / `height` are optional because dev-mode catalog rows folded from a\n * `*.meta.json` sidecar may lack pixel dimensions until `parseImage`\n * decodes the JPG bytes; build-mode (import) rows always have them filled\n * because the import step has already run `parseImage` to produce the RGBA\n * bytes.\n *\n * `format` is `GPUTextureFormat` to align with the `TextureAsset.format`\n * field (math-free, spec-aligned with `@webgpu/types ^0.1.70`).\n *\n * `colorSpace` and `mipmap` are required because the sidecar\n * `importSettings` always carries them (D-5: `'auto'` / `'none'` string\n * tokens are mapped to `true` / `false` at the catalog builder; runtime never\n * sees the string form).\n *\n * `compression` is the build-time compression level this image artefact\n * was stored with. Loop 1 supports `'none'` (passthrough) and `'zstd'`.\n * Loop 2 may add members like `'basis-uastc'`. Absent for legacy rows.\n */\n\n/**\n * Asset compression strategy — closed literal union (SSOT, D-3 / D-9).\n *\n * Five flat, mutually-exclusive members describing how an artefact is stored:\n * - `'none'` — pass-through (uncompressed bytes)\n * - `'zstd'` — generic zstd container compression (Loop 1)\n * - `'basis-etc1s'` / `'basis-uastc'` / `'basis-uastc-hdr'` — a Basis-encoded\n * KTX2 texture (Loop 2). The `basis-*` members fully describe the delivered\n * encoding: the KTX2 container carries its own supercompression (self-\n * described by the KTX2 header) and does NOT stack an outer `'zstd'` layer\n * (mutual exclusion by construction, D-3). The `basis-` kebab prefix is\n * visually distinct from GPU texture-format literals (naming rule, §8).\n *\n * Add-only-minor: Loop 2 appends the three `basis-*` members without repainting\n * `'none'` / `'zstd'` semantics (AC-11a). A missing / `undefined` field means a\n * legacy uncompressed artefact (E1 backward-compat).\n */\nexport type AssetCompression = 'none' | 'zstd' | 'basis-etc1s' | 'basis-uastc' | 'basis-uastc-hdr';\n\nexport interface ImageMetadata {\n readonly kind: 'texture';\n readonly width?: number;\n readonly height?: number;\n readonly format: GPUTextureFormat;\n readonly colorSpace: 'srgb' | 'linear';\n readonly mipmap: boolean;\n /** Build-time compression strategy used for this image artefact. `undefined` for legacy assets. */\n readonly compression?: AssetCompression;\n /**\n * Sidecar control-plane request for the offline texture encoder (D-12).\n * `'auto'` derives the delivery encoding from `colorSpace` + HDR source;\n * `'etc1s'` / `'uastc'` force a Basis encoding; `'none'` keeps the\n * uncompressed `.bin` path. Aligns with the mipmap sidecar tri-state idiom.\n * The `'auto'` default semantics activate in M5; M3 keeps the default `'none'`.\n */\n readonly compressionMode?: 'auto' | 'etc1s' | 'uastc' | 'none';\n /** Optional asset-owned cooked-payload target dimension. */\n readonly downscaleMaxDimension?: number;\n}\n\nexport type {\n AssetAuthoringCapability,\n AssetAuthoringUnavailableReason,\n AssetBindingCapability,\n AssetBindingTarget,\n AssetPlacementCapability,\n AssetRelation,\n AssetRelationPolicy,\n AssetRelationType,\n AssetSubjectRef,\n AssetSubjectType,\n CatalogDiagnostic,\n CatalogDiagnosticSeverity,\n CatalogLifecycle,\n CatalogOperationDescriptor,\n CatalogOperationName,\n CatalogOperations,\n CatalogProjection,\n CatalogProjectionInput,\n CatalogSubject,\n CookExecution,\n ExistingOutput,\n ImportedOutputDeclaration,\n KindChange,\n MatchConflict,\n ProducerContractDiagnostic,\n ProducerContractErrorCode,\n ProducerContractResult,\n ProposedOutput,\n ProviderProvenance,\n ResourceRevision,\n ScenePublicationFence,\n SourceOverrideDescriptor,\n SourceOverrideDiagnostic,\n SourceOverrideErrorCode,\n SourceOverrideMap,\n SourceOverridePayload,\n SourceOverrideValidationResult,\n TopologyConflictReason,\n TopologyDiff,\n TopologyPreserved,\n UiAuthoringCapability,\n UiAuthoringProjection,\n} from './asset-producer';\nexport {\n authoringCapabilityForAssetKind,\n canonicalizeSourceOverrides,\n catalogOperationsFor,\n isCatalogProjectionValid,\n MESH_MATERIAL_SLOT_SOURCE_OVERRIDE_PAYLOAD_SCHEMA,\n validateSourceOverrideMap,\n} from './asset-producer';\n/**\n * One row in the pack-index catalog (`pack-index.json` for build path,\n * `/__pack/index` JSON response for dev path).\n *\n * Core fields (4) stay flat for AI users to grep one identifier:\n * - `guid`: UUIDv5/v7 lowercase string (asset identity SSOT)\n * - `packageUrl`: cooked Pack v2 package navigation URL.\n * - `kind`: closed-string discriminator (`'texture'` / `'mesh'` / `'scene'`\n * / `'material'` / future arms); narrowed by runtime `parseAssetPayload`\n * via exhaustive switch.\n * - `sourcePath`: relative path to the on-disk source artefact for\n * debugging + grep (dev: source JPG path; build: same source JPG path\n * even though `packageUrl` points to the cooked package).\n *\n * Optional 5th field:\n * - `metadata`: `ImageMetadata | undefined` -- present when `kind ===\n * 'texture'`; absent for non-texture kinds (legacy `.pack.json` entries\n * emit 4-field rows). Runtime consumers narrow with `entry.metadata !==\n * undefined` before consumption (D-2 backward-compat strategy).\n */\nexport type {\n AssetPublicationEnvelope,\n AssetPublicationEvidenceUsage,\n AssetPublicationExternalEvidence,\n AssetPublicationFailure,\n AssetPublicationFailureStage,\n AssetPublicationLocator,\n AssetPublicationOutput,\n AssetPublicationReceipt,\n AssetPublicationRecovery,\n CatalogDelta,\n CatalogDeltaValidationError,\n CatalogEntry,\n CatalogEntry as PackIndexEntry,\n CatalogEntryV2,\n CatalogRevisionPoint,\n CatalogRevisionWindow,\n} from './catalog';\nexport { catalogDeltaDigest, catalogEntryDigest, validateCatalogDelta } from './catalog';\n\n// === InspectEntry / InspectSnapshot (feat-20260618-asset-and-pack-name-fields M1 / w3) ===\n//\n// Decision anchors:\n// - plan-strategy D-9 (InspectEntry.name: string via resolveName, non-optional\n// with empty string as legal value; relocated from runtime private to types\n// for single-entry discoverability per charter F1)\n// - requirements AC-12 (inspector assets root carries resolved name per entry)\n//\n// These types were originally private interfaces in asset-registry.ts.\n// They are promoted to @forgeax/engine-types so console + future inspector\n// consumers import them from a single entry point (charter F1).\n\n/** One row in the inspector's `assets[]` snapshot (JSON-RPC over WS). */\nexport interface InspectEntry {\n readonly guid: string;\n /** Asset kind discriminant string (e.g. `'mesh'`, `'texture'`, `'scene'`). */\n readonly kind: string;\n /** Display name resolved by resolveName (empty string is legal). */\n readonly name: string;\n}\n\n/** Snapshot returned by `AssetRegistry.inspect()` -- the inspector root. */\nexport interface InspectSnapshot {\n readonly assets: ReadonlyArray<InspectEntry>;\n}\n\n// === Loader contract SSOT (feat-20260603-asset-import-loader-injection M1 / w3) ===\n//\n// Decision anchors:\n// - plan-strategy D-1 (runtime LoaderRegistry dispatches on `asset.kind`;\n// host injects loaders via `wireDefaultLoaders`, mirroring Console\n// `wireDefaultInspectors`) + D-2 (contract SSOT lives here in\n// `@forgeax/engine-types`, math-free, so `@forgeax/engine-runtime` only\n// depends on the interface, never reverse-imports a concrete loader)\n// - requirements core principle (third DIP instance after RHI / Console)\n// - charter P3 (structured failure) + P4 (consistent abstraction)\n//\n// A `Loader` is the runtime-side half of the import/load split: it turns an\n// already-imported internal artefact (a `.pack.json` payload, or fetched\n// bytes for texture / font) into an in-memory `Asset` POD. It stays pure of\n// the registry's bookkeeping — `registerWithGuid` is the AssetRegistry's job,\n// never the loader's (plan-strategy D-2).\n//\n// Two dispatch shapes share this one contract (the asymmetry is intentional,\n// matching the two pre-existing AssetRegistry load paths the M1 refactor\n// converges; research Finding 1 + Finding 2):\n// (a) inline pack-payload kinds (mesh / scene / material /\n// skeleton / skin / animation-clip) parse synchronously and return\n// `Asset | undefined` (`undefined` = parse rejected, the caller maps it\n// to a structured `AssetError`).\n// (b) upstream-branch kinds (texture / font / equirect) fetch + decode\n// asynchronously and return a `Promise<LoaderAsyncResult>` carrying either\n// the produced `Asset` POD or a structured error.\n\n/**\n * Result envelope returned by the async branch of {@link Loader.load}\n * (texture / font). Mirrors the `Result<T, E>` shape used across the engine\n * (`.ok` discriminant) but is declared math-free here so\n * `@forgeax/engine-types` need not import `@forgeax/engine-rhi`. The error is\n * left as `unknown` so the runtime can surface its own\n * `AssetError | ImageError | RhiError` union without leaking those classes\n * into the types package (charter P4 — the runtime narrows; types stays\n * dependency-free).\n */\nexport type LoaderAsyncResult<P = Asset> =\n | { readonly ok: true; readonly value: P }\n | { readonly ok: false; readonly error: unknown };\n\n/**\n * Output of {@link Loader.load}. The synchronous arm returns `Asset` (parse\n * succeeded) or `undefined` (parse rejected); the asynchronous arm returns a\n * `Promise<LoaderAsyncResult>`.\n */\nexport type LoaderOutput<P = Asset> =\n | P\n | undefined\n | { readonly ok: false; readonly error: ParseErrorDetail }\n | Promise<LoaderAsyncResult<P>>;\n\n/**\n * Capabilities the host wires into a {@link Loader} at load time. A loader\n * receives this context so it never reaches back into AssetRegistry\n * internals (pipeline isolation, architecture-principles #4).\n *\n * Exactly three capabilities (plan-strategy D-3 rationale):\n * - `fetchBinary(url)` — fetch raw bytes for the artefact (texture import\n * `.bin`, `.hdr`, source image, font pack JSON).\n * - `resolveRef(guid)` — recursively resolve a referenced sub-asset GUID to\n * its registered handle id (font atlas / sampler). Returns the raw handle\n * number so the loader can stamp it into the produced POD; the runtime\n * performs the recursive `loadByGuid` + registration underneath.\n * - `device` — opaque GPU device slot, present for future GPU-touching\n * loaders; current loaders register CPU PODs only and never touch it\n * (research Finding 3 — texture GPU upload is decoupled from load time via\n * the pull-model `GpuResourceStore`). Typed `unknown` so types stays\n * RHI-free.\n *\n * F21 (feat-20260621): the error-contextualization callback has been removed.\n */\nexport interface ParseErrorDetail {\n readonly localId: number;\n readonly component: string;\n readonly field: string;\n readonly index: number;\n readonly refsLength: number;\n}\n\n/**\n * Device texture-compression capabilities the transcode target selector reads\n * (feat-20260707 M5 / D-8, D-11).\n *\n * Three independent booleans mirror the WebGPU `texture-compression-{bc,etc2,\n * astc}` device features (and the `RhiCaps.textureCompression{Bc,Etc2,Astc}`\n * triple they are projected from — createRenderer does the one-line RhiCaps ->\n * TranscodeCaps projection). This shape is structurally identical to the codec\n * package's own `TranscodeCaps` (`@forgeax/engine-codec`): the codec keeps a\n * LOCAL copy on purpose (D-8 — codec is a pure, dependency-light transcode\n * library and must not take a `@forgeax/engine-types` edge just to name its\n * pure-function input). The runtime passes a value of this type straight into\n * `selectTranscodeTarget` by structural compatibility; there is exactly one\n * value threaded through `LoadContext`, so no fact is duplicated at runtime.\n */\nexport interface TranscodeCaps {\n readonly bc: boolean;\n readonly etc2: boolean;\n readonly astc: boolean;\n}\n\nexport interface LoadContext {\n /**\n * Fetch raw bytes for an asset artefact, with optional decompression.\n *\n * feat-20260706 M3 / w19: extended signature per D-2 — a `compression`\n * opt triggers the decompression gate inside the closure\n * (`@forgeax/engine-codec` lazy-init). `undefined` / `'none'` = E1\n * pass-through (backward-compat for legacy catalog rows).\n */\n fetchBinary(\n url: string,\n opts?: { readonly compression?: AssetCompression },\n ): Promise<\n | { readonly ok: true; readonly value: Uint8Array }\n | { readonly ok: false; readonly error: unknown }\n >;\n resolveRef(\n guid: string,\n ): Promise<\n { readonly ok: true; readonly value: number } | { readonly ok: false; readonly error: unknown }\n >;\n /**\n * feat-20260613-material-paramschema-driven-binding M4 / w22 (D-5 graceful):\n * derive(paramSchema).textureFieldNames for the given material-shader id,\n * built from the registered shader's paramSchema. Used by materialLoader\n * to know which material value fields carry refs[] indices vs scalar values\n * (replacing the deleted hardcoded texture-field allowlist Set per AC-03).\n *\n * Returns `undefined` when the shader is not yet registered (the cross-\n * worktree shader-late-register path of plan R-4): the loader then falls\n * back to a graceful \"try every int paramValue as a refs index\" walk that\n * may misclassify scalar f32 fields whose value happens to land in\n * [0, refs.length); the extract layer (M4 / w23) catches mis-typed\n * handles via paramSchema validation and falls back to MISSING_TEXTURE_HANDLE.\n */\n getMaterialShaderTextureFieldNames?(shaderId: string): ReadonlySet<string> | undefined;\n /**\n * feat-20260707 M5 / w33 (D-11): device compression caps the texture / equirect\n * Basis arms feed to `selectTranscodeTarget` to pick a transcode target. Wired\n * by `createRenderer` from `RhiCaps` (D-8 one-line projection); a bare\n * AssetRegistry (test / headless path) defaults to all-false, which drives the\n * uncompressed `rgba8unorm` / `rgba16float` fallback (section 8 P3, AC-04).\n * Extends the single ctx input face rather than opening a new loader channel\n * (Pipeline Isolation — inputs declared explicitly).\n */\n readonly transcodeCaps: TranscodeCaps;\n readonly device: unknown;\n}\n\n/**\n * Runtime-side loader injected into the `LoaderRegistry`. One loader per\n * `asset.kind`; the registry dispatches `loadByGuid` on the kind.\n *\n * `load` is pure of registry bookkeeping (no `registerWithGuid`); it only\n * produces the `Asset` POD (or a structured error / `undefined`). See the\n * module comment above for the sync vs async dispatch asymmetry.\n */\nexport interface Loader<P = Asset> {\n readonly kind: string;\n /** Optional Pack v2 dispatch that retains asset-local artifact bytes. */\n readonly loadPack?: (\n input: {\n readonly guid: string;\n readonly kind: string;\n readonly payload: Record<string, unknown>;\n readonly refs: readonly string[];\n readonly artifacts: Readonly<\n Record<\n string,\n {\n readonly descriptor: {\n readonly path: string;\n readonly mediaType: string;\n readonly assetCodec?: {\n readonly name: string;\n readonly container?: 'ktx2' | 'basis';\n readonly profile?: string;\n readonly version?: string;\n };\n };\n readonly bytes: Uint8Array;\n }\n >\n >;\n },\n ctx: LoadContext,\n ) => LoaderOutput<P>;\n load(\n payload: Record<string, unknown>,\n refs: readonly string[] | undefined,\n ctx: LoadContext,\n ): LoaderOutput<P>;\n}\n\nexport type {\n ImportContext,\n ImportDiagnostic,\n ImportDiagnosticLocation,\n ImportErrorCode,\n ImportErrorDetail,\n ImportedArtifactBody,\n ImportedAsset,\n Importer,\n ImporterCapabilities,\n ImportProduct,\n ImportProductFinalizeArtifact,\n ImportProductFinalizeOptions,\n ImportProductFinalizeResult,\n ImportResult,\n ImportSourceRange,\n ImportSubAsset,\n ImportTransport,\n SourceDependency,\n} from './import.js';\nexport {\n IMPORT_ERROR_HINTS,\n ImportError,\n} from './import.js';\n// === EngineMetrics contract SSOT (feat-20260705-runtime-tier2-decomposition M1 / w2, D-3) ===\n//\n// Decision anchors:\n// - plan-strategy D-3 (the 3-method EngineMetrics interface has zero type\n// dependencies; sinking it into @forgeax/engine-types makes types the SSOT\n// leaf. EngineMetricsImpl + createEngineMetrics stay in runtime.)\n// - research F5 (asset-registry.ts `import type { EngineMetrics }` was the\n// third reverse edge from assets cluster to runtime; type-only but the\n// project-reference layer still had to resolve it — relocating the contract\n// to the leaf removes the edge).\n//\n// The interface was originally defined in runtime/src/engine-metrics.ts. It is\n// promoted here so both @forgeax/engine-runtime and @forgeax/engine-assets-runtime\n// import the contract from a single entry point (charter F1).\n\n/**\n * Per-Renderer metrics counter API. Backed by a `Map<string, number>`; reads\n * return a frozen plain object so external mutation never leaks back into the\n * registry (D-5 + R-2 mutation-resistance).\n *\n * Three methods cover the full surface:\n *\n * const r = await createRenderer(canvas);\n * // ... renderer hits some nineslice runtime soft-warn\n * r.metrics.snapshot()['nineslice.scale-too-small']; // -> number | undefined\n *\n * r.metrics.increment('nineslice.scale-too-small'); // mutate counter\n * r.metrics.reset(); // drop all counters\n *\n * Callers can `for (const k in renderer.metrics.snapshot())` to enumerate\n * fired events without knowing the namespace ahead of time.\n *\n * @remarks Closed namespace (charter P5):\n * - `nineslice.scale-too-small`\n * - `nineslice.tile-needs-repeat-sampler`\n * - `render.instancing.foldedDraws`\n */\nexport interface EngineMetrics {\n /**\n * Bump the counter for `name` by 1. Counters start at 0 implicitly; the\n * first `increment(name)` lands a 1 in the snapshot. Names are free-form\n * strings (no prior registration), so feat-local namespaces (e.g.\n * `nineslice.*`) coexist without coordination.\n */\n increment(name: string): void;\n /**\n * Read all counters as an immutable `Readonly<Record<string, number>>`.\n * The returned object is frozen — external mutation throws in strict mode\n * and is silently ignored otherwise. Snapshot-then-mutate is decoupled\n * from the registry: a later `increment` does not retroactively alter the\n * already-returned snapshot.\n */\n snapshot(): Readonly<Record<string, number>>;\n /**\n * Drop every counter back to 0 (counter rows physically removed). Provided\n * for test isolation and Inspector reset workflows; production code on\n * the hot path never calls this.\n */\n reset(): void;\n}\n\n// inspector-client is Node-only (imports 'ws'); consume via\n// import { ... } from '@forgeax/engine-types/inspector-client'\n","import {\n type AssetDecoderContribution,\n type AssetKind,\n err,\n ok,\n type SkeletonAsset,\n type SkinAsset,\n} from '@forgeax/engine-types';\n\nfunction floatArray(value: unknown): Float32Array | undefined {\n if (value instanceof Float32Array) return value;\n if (Array.isArray(value) && value.every((item) => typeof item === 'number')) {\n return Float32Array.from(value);\n }\n return undefined;\n}\n\nexport const skinContribution: AssetDecoderContribution<SkinAsset, 'skin'> = {\n kind: { kind: 'skin' } as AssetKind<SkinAsset, 'skin'>,\n consumer: 'resolveSkinJoints',\n decoder: {\n async decode({ envelope }) {\n const payload = envelope.payload;\n if (\n payload.kind === 'skin' &&\n payload.skeletonGuid.length > 0 &&\n payload.jointPaths.length > 0\n ) {\n return ok(payload);\n }\n return err({\n code: 'asset-package-invalid',\n expected: 'a skin payload with a skeleton GUID and joint paths',\n hint: 'recook the skin binding and publish its skeleton reference',\n detail: { guid: envelope.guid, reason: 'skin owner validation failed' },\n });\n },\n },\n};\n\nexport const skeletonContribution: AssetDecoderContribution<SkeletonAsset, 'skeleton'> = {\n kind: { kind: 'skeleton' } as AssetKind<SkeletonAsset, 'skeleton'>,\n consumer: 'resolveSkinJoints',\n decoder: {\n async decode({ envelope }) {\n const payload = envelope.payload as unknown;\n if (payload !== null && typeof payload === 'object') {\n const source = payload as Record<string, unknown>;\n const inverseBindMatrices = floatArray(source.inverseBindMatrices);\n const jointCount = source.jointCount;\n if (\n source.kind === 'skeleton' &&\n inverseBindMatrices !== undefined &&\n Number.isSafeInteger(jointCount) &&\n (jointCount as number) >= 0 &&\n inverseBindMatrices.length === (jointCount as number) * 16\n ) {\n return ok({ kind: 'skeleton', inverseBindMatrices, jointCount: jointCount as number });\n }\n }\n return err({\n code: 'asset-package-invalid',\n expected: 'a skeleton payload with one inverse-bind matrix per joint',\n hint: 'recook the skeleton and publish its complete joint data',\n detail: { guid: envelope.guid, reason: 'skeleton owner validation failed' },\n });\n },\n },\n};\n","// @forgeax/engine-runtime -- skin cluster error classes.\n//\n// feat-20260704-runtime-tier1-decomposition M2 / w8 (D-3): skin / skeleton\n// animation cluster -- joint count / despawn / path / coexistence and\n// extract-stage binding failures. Palette/material/GPU failures stay in the\n// render error union because render owns the emitting frame stages. Binding\n// class names, .code literals, and .detail shapes are preserved byte-for-byte\n// (OOS-4).\n//\n// SkinExtractErrorCode (the 3-member extract-stage subset union) is kept as a\n// named export and folded into SkinErrorCode, preserving the pre-existing\n// public symbol (OOS-4).\n\n// ── SkinExtractErrorCode subset union ───────────────────────────────────────\n\n/**\n * feat-20260612-skin-palette-per-frame-upload M2 / m2-5 subset union.\n *\n * Covers the three new fail-fast extract-stage errors that fire from\n * `render-system-extract.ts` `hasSkin` segment when the per-frame palette\n * upload pipeline cannot resolve a slice for an entity. Single-entity\n * `continue` semantics: the entity is skipped, sibling entities in the\n * same frame keep extracting (plan-strategy D-5).\n *\n * | code | class | trigger |\n * |:--|:--|:--|\n * | `'skeleton-resolve-failed'` | `SkeletonResolveFailedError` | `assets.get<SkeletonAsset>(skin.skeleton)` returns null/undefined |\n * | `'joint-count-mismatch'` | `JointCountMismatchError` | `Skin.joints.length !== SkeletonAsset.jointCount` |\n * | `'joint-entity-dangling'` | `JointEntityDanglingError` | `Skin.joints[i]` Entity is despawned (Transform.world view undefined) |\n *\n * AI users discriminate via `switch (err.code)` over `RuntimeErrorCode`;\n * each member narrows to its `*Error` class with structured `.detail`.\n *\n * NOTE: distinct from the pre-existing `'skin-joint-despawned'` /\n * `'skin-joint-path-unresolved'` / `'skin-joint-count-exceeded'`\n * (advanceAnimationPlayer + post-spawn jointPath resolution); plan-strategy\n * D-4 forbids reusing those codes for the new extract-stage triggers.\n */\nexport type SkinExtractErrorCode =\n | 'skeleton-resolve-failed'\n | 'joint-count-mismatch'\n | 'joint-entity-dangling';\n\n// ── SkinJointCountExceededError ────────────────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'skin-joint-count-exceeded'`.\n *\n * Emitted when a glTF skin has more than MAX_JOINTS (256) joints.\n */\nexport interface SkinJointCountExceededDetail {\n readonly jointCount: number;\n readonly max: number;\n}\n\n/**\n * Structured error for skin joint count exceeding the engine cap.\n *\n * Emitted during skin import/validation. Four-field surface:\n * - `.code = 'skin-joint-count-exceeded'`\n * - `.expected` — max allowed (256)\n * - `.hint` — reduce joint count in the source asset\n * - `.detail = { jointCount, max }` — actual vs limit\n */\nexport class SkinJointCountExceededError extends Error {\n readonly code = 'skin-joint-count-exceeded' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: SkinJointCountExceededDetail;\n\n constructor(jointCount: number, max = 256) {\n const expected = `jointCount <= ${max}`;\n const hint = `skin has ${jointCount} joints (max ${max}); reduce joint count in the source glTF asset (OOS-skin-many-joints)`;\n super(`skin joint count ${jointCount} exceeds max ${max}`);\n this.name = 'SkinJointCountExceededError';\n this.expected = expected;\n this.hint = hint;\n this.detail = { jointCount, max };\n }\n}\n\n// ── SkinJointDespawnedError ─────────────────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'skin-joint-despawned'`.\n *\n * Emitted at extract time when a Skin.joints[i] Entity has been despawned.\n */\nexport interface SkinJointDespawnedDetail {\n readonly meshEntity: number;\n readonly jointIndex: number;\n}\n\n/**\n * Structured error for despawned skin joint Entity.\n *\n * Emitted at extract time; the mesh draw is fully skipped.\n * - `.code = 'skin-joint-despawned'`\n * - `.expected` — all Skin.joints alive\n * - `.hint` — remove the Skin component or re-spawn joints\n * - `.detail = { meshEntity, jointIndex }`\n */\nexport class SkinJointDespawnedError extends Error {\n readonly code = 'skin-joint-despawned' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: SkinJointDespawnedDetail;\n\n constructor(meshEntity: number, jointIndex: number) {\n const expected = `Skin.joints[${jointIndex}] references a live entity`;\n const hint = `joint[${jointIndex}] of entity ${meshEntity} has been despawned; remove Skin component or re-spawn the joint entity (OOS-skin-joint-respawn)`;\n super(`skin joint[${jointIndex}] despawned for entity ${meshEntity}`);\n this.name = 'SkinJointDespawnedError';\n this.expected = expected;\n this.hint = hint;\n this.detail = { meshEntity, jointIndex };\n }\n}\n\n// ── SkinJointPathUnresolvedError ────────────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'skin-joint-path-unresolved'`.\n *\n * Emitted at post-spawn time when a jointPath leaf name cannot be found.\n */\nexport interface SkinJointPathUnresolvedDetail {\n readonly skinEntity: number;\n readonly path: readonly string[];\n readonly failedAtIndex: number;\n}\n\n/**\n * Structured error for unresolved jointPath post-spawn.\n *\n * Emitted by resolveSkinJoints when Name lookup fails.\n * - `.code = 'skin-joint-path-unresolved'`\n * - `.expected` — Name-bearing entity exists for each jointPath leaf\n * - `.hint` — verify glTF node Name preservation in the importer\n * - `.detail = { skinEntity, path, failedAtIndex }`\n */\nexport class SkinJointPathUnresolvedError extends Error {\n readonly code = 'skin-joint-path-unresolved' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: SkinJointPathUnresolvedDetail;\n\n constructor(skinEntity: number, path: readonly string[], failedAtIndex: number) {\n const leafName = path[failedAtIndex] ?? '<unknown>';\n const expected = `joint entity with Name=\"${leafName}\" exists in the world`;\n const hint = `joint path \"${path.join('/')}\" for skin entity ${skinEntity} could not be resolved; verify glTF node names are preserved`;\n super(\n `joint path \"${path.join('/')}\" unresolved at index ${failedAtIndex} for entity ${skinEntity}`,\n );\n this.name = 'SkinJointPathUnresolvedError';\n this.expected = expected;\n this.hint = hint;\n this.detail = { skinEntity, path, failedAtIndex };\n }\n}\n\n// ── SkinInstancesCoexistForbiddenError ──────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'skin-instances-coexist-forbidden'`.\n *\n * Emitted at extract time when Skin + Instances coexist on the same entity.\n */\nexport interface SkinInstancesCoexistForbiddenDetail {\n readonly entity: number;\n}\n\n/**\n * Structured error for Skin + Instances coexistence on same entity.\n *\n * Emitted at extract time; the entity draw is skipped.\n * - `.code = 'skin-instances-coexist-forbidden'`\n * - `.expected` — Skin and Instances on separate entities\n * - `.hint` — split skinned meshes from instanced meshes into separate entities (OOS-skin-instances-coexist)\n * - `.detail = { entity }`\n */\nexport class SkinInstancesCoexistForbiddenError extends Error {\n readonly code = 'skin-instances-coexist-forbidden' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: SkinInstancesCoexistForbiddenDetail;\n\n constructor(entity: number) {\n const expected = 'Skin and Instances must not coexist on the same entity';\n const hint = `entity ${entity} has both Skin and Instances; split skinned meshes from instanced meshes into separate entities (OOS-skin-instances-coexist)`;\n super(`Skin + Instances coexistence forbidden on entity ${entity}`);\n this.name = 'SkinInstancesCoexistForbiddenError';\n this.expected = expected;\n this.hint = hint;\n this.detail = { entity };\n }\n}\n\n// ── SkeletonResolveFailedError ─────────────────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'skeleton-resolve-failed'`.\n *\n * Emitted at extract time when `assets.get<SkeletonAsset>(skin.skeleton)`\n * returns `null` / `undefined`. The skeleton handle is non-zero (the entity\n * declared a Skin) but the asset is not registered (importer drift /\n * AssetRegistry not warmed).\n */\nexport interface SkeletonResolveFailedDetail {\n readonly entity: number;\n readonly skeletonHandle: number;\n}\n\n/**\n * Structured error for unresolved SkeletonAsset handle at extract time\n * (feat-20260612-skin-palette-per-frame-upload M2 / m2-5).\n *\n * Emitted at extract time; the entity draw is skipped (continue), other\n * entities in the same frame keep extracting.\n * - `.code = 'skeleton-resolve-failed'`\n * - `.expected` — Skin.skeleton handle resolves to a registered SkeletonAsset\n * - `.hint` — verify SkeletonAsset is imported into pack-index AND registered\n * via AssetRegistry.register(handle, asset) before extractFrame\n * - `.detail = { entity, skeletonHandle }`\n */\nexport class SkeletonResolveFailedError extends Error {\n readonly code = 'skeleton-resolve-failed' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: SkeletonResolveFailedDetail;\n\n constructor(entity: number, skeletonHandle: number) {\n const expected = `Skin.skeleton handle ${skeletonHandle} resolves to a registered SkeletonAsset`;\n const hint = `entity ${entity} Skin.skeleton handle ${skeletonHandle} is not registered; check that the SkeletonAsset went through the gltf importer into pack-index AND that AssetRegistry.register was called for the handle before extractFrame runs`;\n super(`Skin skeleton resolve failed on entity ${entity}: handle ${skeletonHandle}`);\n this.name = 'SkeletonResolveFailedError';\n this.expected = expected;\n this.hint = hint;\n this.detail = { entity, skeletonHandle };\n }\n}\n\n// ── JointCountMismatchError ────────────────────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'joint-count-mismatch'`.\n *\n * Emitted at extract time when `Skin.joints.length !== SkeletonAsset.jointCount`.\n * `expected` is the SkeletonAsset's jointCount (the source of truth);\n * `actual` is the entity's `Skin.joints.length` (the runtime entity reference\n * list materialized at post-spawn time).\n */\nexport interface JointCountMismatchDetail {\n readonly entity: number;\n readonly expected: number;\n readonly actual: number;\n}\n\n/**\n * Structured error for SkinAsset.joints[] vs SkeletonAsset.jointCount disagreement\n * (feat-20260612-skin-palette-per-frame-upload M2 / m2-5).\n *\n * Emitted at extract time; the entity draw is skipped (continue).\n * - `.code = 'joint-count-mismatch'`\n * - `.expected` — Skin.joints.length === SkeletonAsset.jointCount\n * - `.hint` — verify SkinAsset.joints[] and SkeletonAsset jointPaths[]\n * come from the same glTF skin node\n * - `.detail = { entity, expected, actual }`\n */\nexport class JointCountMismatchError extends Error {\n readonly code = 'joint-count-mismatch' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: JointCountMismatchDetail;\n\n constructor(entity: number, expected: number, actual: number) {\n const expectedStr = `Skin.joints.length === SkeletonAsset.jointCount (=${expected})`;\n const hint = `entity ${entity}: Skin.joints.length=${actual} disagrees with SkeletonAsset.jointCount=${expected}; verify SkinAsset.joints[] and SkeletonAsset jointPaths[] come from the same glTF skin node`;\n super(\n `joint count mismatch on entity ${entity}: SkeletonAsset.jointCount=${expected}, Skin.joints.length=${actual}`,\n );\n this.name = 'JointCountMismatchError';\n this.expected = expectedStr;\n this.hint = hint;\n this.detail = { entity, expected, actual };\n }\n}\n\n// ── JointEntityDanglingError ──────────────────────────────────────────────\n\n/**\n * Detail for `RuntimeErrorCode 'joint-entity-dangling'`.\n *\n * Emitted at extract time when `Skin.joints[i]` points at an Entity that has\n * been despawned (or lost its Transform component) so\n * `worldInternal._getArrayView(jointEntity, Transform, 'world')` returns\n * undefined. `jointIndex` is the position within `Skin.joints[]`.\n */\nexport interface JointEntityDanglingDetail {\n readonly entity: number;\n readonly jointIndex: number;\n}\n\n/**\n * Structured error for despawned (or Transform-less) joint Entity at extract\n * time (feat-20260612-skin-palette-per-frame-upload M2 / m2-5).\n *\n * Distinct from the pre-existing `SkinJointDespawnedError` which fires from\n * advanceAnimationPlayer (animation-stage); this one fires from extractFrame\n * (palette-upload stage) when the per-joint world mat4 view is missing.\n *\n * Emitted at extract time; the entity draw is skipped (continue).\n * - `.code = 'joint-entity-dangling'`\n * - `.expected` — Skin.joints[i] references a live Entity with Transform\n * - `.hint` — sync Skin.joints[] when joint entities are despawned, or\n * re-import the scene through the gltf importer to refresh Entity refs\n * - `.detail = { entity, jointIndex }`\n */\nexport class JointEntityDanglingError extends Error {\n readonly code = 'joint-entity-dangling' as const;\n readonly expected: string;\n readonly hint: string;\n readonly detail: JointEntityDanglingDetail;\n\n constructor(entity: number, jointIndex: number) {\n const expected = `Skin.joints[${jointIndex}] references a live Entity with Transform`;\n const hint = `entity ${entity} Skin.joints[${jointIndex}] points at a despawned (or Transform-less) Entity; sync Skin.joints[] when joint entities are despawned, or re-import the scene through the gltf importer to refresh Entity references`;\n super(`joint entity dangling on entity ${entity} at jointIndex ${jointIndex}`);\n this.name = 'JointEntityDanglingError';\n this.expected = expected;\n this.hint = hint;\n this.detail = { entity, jointIndex };\n }\n}\n\n// -- SkinErrorCode / SkinError closed unions ------------------------------------\n\n/**\n * Closed union of skin-cluster error codes derived from the correlated error\n * union. AI users perform exhaustive `switch (err.code)` without default; TS\n * guards completeness.\n */\nexport type SkinErrorCode = SkinError['code'];\n\n/**\n * Closed union of the skin-cluster structured error classes, each carrying a\n * `SkinErrorCode` discriminant on `.code`.\n */\nexport type SkinError =\n | SkinJointCountExceededError\n | SkinJointDespawnedError\n | SkinJointPathUnresolvedError\n | SkinInstancesCoexistForbiddenError\n | SkeletonResolveFailedError\n | JointCountMismatchError\n | JointEntityDanglingError;\n","// @forgeax/engine-runtime - Skin component (skeleton handle + joint Entity slots).\n//\n// Schema: { skeleton: 'shared<SkeletonAsset>', joints: 'array<entity>' }.\n//\n// `skeleton` carries the immutable SkeletonAsset handle (IBM + jointCount);\n// `joints` carries the live Entity[] resolved at post-spawn time from the\n// SkinAsset.jointPaths via Name-component BFS/DFS lookup. The joint list is\n// consumed by advanceAnimationPlayer (write target) and render-system-extract\n// (CPU palette pre-multiply source).\n//\n// Naming: single-semantic component drops the 'Component' suffix\n// (AGENTS.md §Component naming rule #1). `joints` field takes the holder's\n// perspective (AGENTS.md §Component naming rule #3).\n//\n// Component registered alongside MeshFilter / MeshRenderer / Transform as\n// a sibling on the same entity (AC-13 / AC-37). Skin + Instances coexistence\n// on the same entity is forbidden (M2 fail-fast 'skin-instances-coexist-forbidden').\n//\n// Decision anchors:\n// - requirements AC-13 (Skin sibling to MeshFilter / MeshRenderer)\n// - requirements AC-15 (joint Entity slots, no marker component)\n// - requirements AC-37 (no Component suffix)\n// - plan-strategy D-10 (SkinPaletteSlice naming + Skin x Instances fail-fast)\n// - charter P3 (explicit failure: joint despawn fail-fast)\n// - schema vocab 'shared<SkeletonAsset>' v1 missing item #4 alignment\n//\n// ## Transform contract (post-bug-20260615 fix)\n//\n// **Old (buggy) implicit contract (pre-bug-20260615):** The Skin entity's\n// Transform.world was double-applied during skinning -- the shader computed\n// `world = meshes[0].worldFromLocal x palette x pos`, so any non-identity\n// Transform on the Skin entity (or its non-joint ancestors) caused doubled\n// motion (translation 2x, rotation 2x). Holders had to manually pin the Skin\n// entity's Transform to identity to avoid doubled motion. This contract was\n// undocumented and easy to violate.\n//\n// **New explicit contract (post-bug-20260615 fix):** An entity carrying `Skin`\n// has its own `Transform` ignored at render time; the world transform is\n// determined entirely by the joints' world matrices fed through the palette:\n//\n// palette[i] = jointWorld_i x IBM_i\n// shader: world = palette[i] x pos\n//\n// No additional left-multiply by `meshes[0].worldFromLocal` or `instanceLocal`.\n// To move the rig, parent the joint root (or any common ancestor of the joints\n// in `Skin.joints[]`) to your driving entity -- moving the Skin entity itself\n// has no rendering effect. This aligns with glTF 2.0 SSkins Implementation\n// Note: \"the transform of the node that the mesh is attached to must be\n// ignored when performing skinning.\"\n//\n// Full pipeline documentation: packages/runtime/README.md\n// SSkinPaletteAllocator.\n//\n// Fix commits:\n// - M0 (red): 15425c2b -- parented skin double-transform unit test\n// - M1 (green): 2ad509b7 -- shader Plan A: drop meshes[0] left-multiply\n// - M2 (cleanup): 4118e463 -- extract.ts joint read -> world.get API\n// - M3 (demo): 94d7db66 -- hello-skin parented Fox under non-identity rig\n// - M4 (baseline): b6ddf46d -- palette-hash counter-proof + submodule pointer\n\nimport { defineComponent } from '@forgeax/engine-ecs';\n\nexport const Skin = defineComponent('Skin', {\n // The renderer owns the live skeleton asset/palette binding; joint entity\n // relationships remain the portable simulation-side pose contract.\n skeleton: { type: 'shared<SkeletonAsset>' },\n joints: { type: 'array<entity>' },\n});\n","import type { Component, World } from '@forgeax/engine-ecs';\nimport type { Plugin } from '@forgeax/engine-plugin';\nimport { Skin } from './skin';\n\nconst SKINNING_COMPONENTS: readonly Component[] = [Skin];\n\nfunction registerSkinningComponents(world: World): () => void {\n const leases = SKINNING_COMPONENTS.map((component) =>\n world.components.register(component).unwrap(),\n );\n return () => {\n for (let index = leases.length - 1; index >= 0; index -= 1) leases[index]?.dispose();\n };\n}\n\n/** Install skeletal binding components in a World that consumes skinned scenes. */\nexport function skinningPlugin(): Plugin {\n return {\n name: 'skinning',\n inject: ['world'],\n apply(ctx) {\n ctx.effect(() => registerSkinningComponents(ctx.world), 'skinning/components');\n },\n };\n}\n","import type { EntityHandle } from '@forgeax/engine-ecs';\nimport { type SkinError, SkinJointPathUnresolvedError } from './errors.js';\n\nexport function resolveSkinJoints(\n jointPaths: readonly string[],\n names: ReadonlyMap<string, EntityHandle>,\n skinEntity: EntityHandle,\n): { ok: true; value: Uint32Array } | { ok: false; error: SkinError } {\n const joints: number[] = [];\n for (const path of jointPaths) {\n const segments = path.split('/').filter(Boolean);\n if (segments.length === 0) continue;\n const failedAtIndex = segments.length - 1;\n const entity = names.get(segments[failedAtIndex] ?? '');\n if (entity === undefined) {\n return {\n ok: false,\n error: new SkinJointPathUnresolvedError(skinEntity as number, segments, failedAtIndex),\n };\n }\n joints.push(entity as number);\n }\n return { ok: true, value: new Uint32Array(joints) };\n}\n"],"mappings":";AC2EA,IAAM,WAAW;EACf,SAAyC;AACvC,WAAO,KAAK;EACd;EACA,SAAkC,eAAiC;AACjE,WAAO,KAAK;EACd;AACF;AAEA,IAAM,YAAY;EAChB,SAAwC;AAEtC,UAAM,KAAK;EACb;EACA,SAAsC,cAAoB;AACxD,WAAO;EACT;AACF;AAaO,SAAS,GAAM,OAAuB;AAC3C,QAAM,IAAI,OAAO,OAAO,QAAQ;AAChC,IAAE,KAAK;AACP,IAAE,QAAQ;AACV,SAAO;AACT;AAUO,SAAS,IAAO,OAAwB;AAC7C,QAAM,IAAI,OAAO,OAAO,SAAS;AACjC,IAAE,KAAK;AACP,IAAE,QAAQ;AACV,SAAO;AACT;AE+HO,IAAM,YAAY,KAAK,MAAM;AG5P7B,IAAM,uBAAuB;EAClC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AACF;AA0KA,IAAM,wBAAwB;EAC5B,6BAA6B;IAC3B,UAAU;IACV,MAAM;EAAA;EAER,iCAAiC;IAC/B,UAAU;IACV,MAAM;EAAA;EAER,8BAA8B;IAC5B,UAAU;IACV,MAAM;EAAA;EAER,0BAA0B;IACxB,UAAU;IACV,MAAM;EAAA;EAER,gCAAgC;IAC9B,UAAU;IACV,MAAM;EAAA;EAER,sCAAsC;IACpC,UAAU;IACV,MAAM;EAAA;EAER,4BAA4B;IAC1B,UAAU;IACV,MAAM;EAAA;EAER,8BAA8B;IAC5B,UAAU;IACV,MAAM;EAAA;EAER,2BAA2B;IACzB,UAAU;IACV,MAAM;EAAA;EAER,oCAAoC;IAClC,UAAU;IACV,MAAM;EAAA;EAER,wCAAwC;IACtC,UAAU;IACV,MAAM;EAAA;EAER,sCAAsC;IACpC,UAAU;IACV,MAAM;EAAA;EAER,4CAA4C;IAC1C,UAAU;IACV,MAAM;EAAA;EAER,gCAAgC;IAC9B,UAAU;IACV,MAAM;EAAA;EAER,uCAAuC;IACrC,UAAU;IACV,MAAM;EAAA;EAER,uCAAuC;IACrC,UAAU;IACV,MAAM;EAAA;EAER,2BAA2B;IACzB,UAAU;IACV,MAAM;EAAA;AAEV;AAOO,IAAM,0BACX,OAAO;EACL,qBAAqB,IAAI,CAAC,SAAS,CAAC,MAAM,sBAAsB,IAAI,EAAE,QAAQ,CAAC;AACjF;AAEK,IAAM,uBAAoE,OAAO;EACtF,qBAAqB,IAAI,CAAC,SAAS,CAAC,MAAM,sBAAsB,IAAI,EAAE,IAAI,CAAC;AAC7E;AGhPA,IAAM,gBAAA,oBAAoD,IAAuB;EAC/E;EACA;EACA;EACA;EACA;EACA;EACA;AACF,CAAC;AAED,IAAM,qBAAA,oBAAyD,IAAuB;EACpF;EACA;EACA;EACA;AACF,CAAC;AAED,IAAM,gBAAA,oBAAoD,IAAuB;EAC/E;EACA;AACF,CAAC;AAED,IAAM,YAAA,oBAAgD,IAAuB;EAC3E,GAAG;EACH,GAAG;EACH,GAAG;EACH;AACF,CAAC;;;AKjDD,SAAS,WAAW,OAA0C;AAC5D,MAAI,iBAAiB,aAAc,QAAO;AAC1C,MAAI,MAAM,QAAQ,KAAK,KAAK,MAAM,MAAM,CAAC,SAAS,OAAO,SAAS,QAAQ,GAAG;AAC3E,WAAO,aAAa,KAAK,KAAK;AAAA,EAChC;AACA,SAAO;AACT;AAEO,IAAM,mBAAgE;AAAA,EAC3E,MAAM,EAAE,MAAM,OAAO;AAAA,EACrB,UAAU;AAAA,EACV,SAAS;AAAA,IACP,MAAM,OAAO,EAAE,SAAS,GAAG;AACzB,YAAM,UAAU,SAAS;AACzB,UACE,QAAQ,SAAS,UACjB,QAAQ,aAAa,SAAS,KAC9B,QAAQ,WAAW,SAAS,GAC5B;AACA,eAAO,GAAG,OAAO;AAAA,MACnB;AACA,aAAO,IAAI;AAAA,QACT,MAAM;AAAA,QACN,UAAU;AAAA,QACV,MAAM;AAAA,QACN,QAAQ,EAAE,MAAM,SAAS,MAAM,QAAQ,+BAA+B;AAAA,MACxE,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEO,IAAM,uBAA4E;AAAA,EACvF,MAAM,EAAE,MAAM,WAAW;AAAA,EACzB,UAAU;AAAA,EACV,SAAS;AAAA,IACP,MAAM,OAAO,EAAE,SAAS,GAAG;AACzB,YAAM,UAAU,SAAS;AACzB,UAAI,YAAY,QAAQ,OAAO,YAAY,UAAU;AACnD,cAAM,SAAS;AACf,cAAM,sBAAsB,WAAW,OAAO,mBAAmB;AACjE,cAAM,aAAa,OAAO;AAC1B,YACE,OAAO,SAAS,cAChB,wBAAwB,UACxB,OAAO,cAAc,UAAU,KAC9B,cAAyB,KAC1B,oBAAoB,WAAY,aAAwB,IACxD;AACA,iBAAO,GAAG,EAAE,MAAM,YAAY,qBAAqB,WAAiC,CAAC;AAAA,QACvF;AAAA,MACF;AACA,aAAO,IAAI;AAAA,QACT,MAAM;AAAA,QACN,UAAU;AAAA,QACV,MAAM;AAAA,QACN,QAAQ,EAAE,MAAM,SAAS,MAAM,QAAQ,mCAAmC;AAAA,MAC5E,CAAC;AAAA,IACH;AAAA,EACF;AACF;;;ACJO,IAAM,8BAAN,cAA0C,MAAM;AAAA,EAC5C,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,YAAoB,MAAM,KAAK;AACzC,UAAM,WAAW,iBAAiB,GAAG;AACrC,UAAM,OAAO,YAAY,UAAU,gBAAgB,GAAG;AACtD,UAAM,oBAAoB,UAAU,gBAAgB,GAAG,EAAE;AACzD,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,YAAY,IAAI;AAAA,EAClC;AACF;AAuBO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EACxC,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,YAAoB,YAAoB;AAClD,UAAM,WAAW,eAAe,UAAU;AAC1C,UAAM,OAAO,SAAS,UAAU,eAAe,UAAU;AACzD,UAAM,cAAc,UAAU,0BAA0B,UAAU,EAAE;AACpE,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,YAAY,WAAW;AAAA,EACzC;AACF;AAwBO,IAAM,+BAAN,cAA2C,MAAM;AAAA,EAC7C,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,YAAoB,MAAyB,eAAuB;AAC9E,UAAM,WAAW,KAAK,aAAa,KAAK;AACxC,UAAM,WAAW,2BAA2B,QAAQ;AACpD,UAAM,OAAO,eAAe,KAAK,KAAK,GAAG,CAAC,qBAAqB,UAAU;AACzE;AAAA,MACE,eAAe,KAAK,KAAK,GAAG,CAAC,yBAAyB,aAAa,eAAe,UAAU;AAAA,IAC9F;AACA,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,YAAY,MAAM,cAAc;AAAA,EAClD;AACF;AAsBO,IAAM,qCAAN,cAAiD,MAAM;AAAA,EACnD,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,QAAgB;AAC1B,UAAM,WAAW;AACjB,UAAM,OAAO,UAAU,MAAM;AAC7B,UAAM,oDAAoD,MAAM,EAAE;AAClE,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,OAAO;AAAA,EACzB;AACF;AA6BO,IAAM,6BAAN,cAAyC,MAAM;AAAA,EAC3C,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,QAAgB,gBAAwB;AAClD,UAAM,WAAW,wBAAwB,cAAc;AACvD,UAAM,OAAO,UAAU,MAAM,yBAAyB,cAAc;AACpE,UAAM,0CAA0C,MAAM,YAAY,cAAc,EAAE;AAClF,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,QAAQ,eAAe;AAAA,EACzC;AACF;AA6BO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EACxC,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,QAAgB,UAAkB,QAAgB;AAC5D,UAAM,cAAc,qDAAqD,QAAQ;AACjF,UAAM,OAAO,UAAU,MAAM,wBAAwB,MAAM,4CAA4C,QAAQ;AAC/G;AAAA,MACE,kCAAkC,MAAM,8BAA8B,QAAQ,wBAAwB,MAAM;AAAA,IAC9G;AACA,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,QAAQ,UAAU,OAAO;AAAA,EAC3C;AACF;AAgCO,IAAM,2BAAN,cAAuC,MAAM;AAAA,EACzC,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,QAAgB,YAAoB;AAC9C,UAAM,WAAW,eAAe,UAAU;AAC1C,UAAM,OAAO,UAAU,MAAM,gBAAgB,UAAU;AACvD,UAAM,mCAAmC,MAAM,kBAAkB,UAAU,EAAE;AAC7E,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,OAAO;AACZ,SAAK,SAAS,EAAE,QAAQ,WAAW;AAAA,EACrC;AACF;;;ACjRA,SAAS,uBAAuB;AAEzB,IAAM,OAAO,gBAAgB,QAAQ;AAAA;AAAA;AAAA,EAG1C,UAAU,EAAE,MAAM,wBAAwB;AAAA,EAC1C,QAAQ,EAAE,MAAM,gBAAgB;AAClC,CAAC;;;AC/DD,IAAM,sBAA4C,CAAC,IAAI;AAEvD,SAAS,2BAA2B,OAA0B;AAC5D,QAAM,SAAS,oBAAoB;AAAA,IAAI,CAAC,cACtC,MAAM,WAAW,SAAS,SAAS,EAAE,OAAO;AAAA,EAC9C;AACA,SAAO,MAAM;AACX,aAAS,QAAQ,OAAO,SAAS,GAAG,SAAS,GAAG,SAAS,EAAG,QAAO,KAAK,GAAG,QAAQ;AAAA,EACrF;AACF;AAGO,SAAS,iBAAyB;AACvC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ,CAAC,OAAO;AAAA,IAChB,MAAM,KAAK;AACT,UAAI,OAAO,MAAM,2BAA2B,IAAI,KAAK,GAAG,qBAAqB;AAAA,IAC/E;AAAA,EACF;AACF;;;ACrBO,SAAS,kBACd,YACA,OACA,YACoE;AACpE,QAAM,SAAmB,CAAC;AAC1B,aAAW,QAAQ,YAAY;AAC7B,UAAM,WAAW,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAC/C,QAAI,SAAS,WAAW,EAAG;AAC3B,UAAM,gBAAgB,SAAS,SAAS;AACxC,UAAM,SAAS,MAAM,IAAI,SAAS,aAAa,KAAK,EAAE;AACtD,QAAI,WAAW,QAAW;AACxB,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,OAAO,IAAI,6BAA6B,YAAsB,UAAU,aAAa;AAAA,MACvF;AAAA,IACF;AACA,WAAO,KAAK,MAAgB;AAAA,EAC9B;AACA,SAAO,EAAE,IAAI,MAAM,OAAO,IAAI,YAAY,MAAM,EAAE;AACpD;","names":[]}
|