@forgeax/engine-naga 0.1.2

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/index.mjs ADDED
@@ -0,0 +1,165 @@
1
+ import { ensureReady } from '@forgeax/engine-wgpu-wasm';
2
+ import { err, ok } from '@forgeax/engine-types';
3
+ export { err, ok } from '@forgeax/engine-types';
4
+
5
+ // src/index.ts
6
+ var ShaderError = class extends Error {
7
+ name = "ShaderError";
8
+ code;
9
+ expected;
10
+ hint;
11
+ lineNum;
12
+ linePos;
13
+ detail;
14
+ constructor(init) {
15
+ super(init.message);
16
+ this.code = init.code;
17
+ this.expected = init.expected;
18
+ this.hint = init.hint;
19
+ this.lineNum = init.lineNum;
20
+ this.linePos = init.linePos;
21
+ this.detail = init.detail;
22
+ }
23
+ };
24
+ function compileFailed(args) {
25
+ return new ShaderError({
26
+ code: "shader-compile-failed",
27
+ expected: "WGSL source parses + validates against naga IR",
28
+ message: args.message,
29
+ hint: args.hint,
30
+ ...args.lineNum !== void 0 ? { lineNum: args.lineNum } : {},
31
+ ...args.linePos !== void 0 ? { linePos: args.linePos } : {},
32
+ ...args.compilerMessages !== void 0 ? {
33
+ detail: {
34
+ code: "shader-compile-failed",
35
+ compilerMessages: args.compilerMessages,
36
+ ...args.reason !== void 0 ? { reason: args.reason } : {}
37
+ }
38
+ } : {}
39
+ });
40
+ }
41
+ function initFailed(args) {
42
+ return new ShaderError({
43
+ code: "compiler-init-failed",
44
+ expected: "@forgeax/engine-wgpu-wasm ensureReady() resolves with naga raw bindings available",
45
+ message: args.message,
46
+ hint: args.hint,
47
+ detail: {
48
+ code: "compiler-init-failed",
49
+ ...args.reason !== void 0 ? { reason: args.reason } : {}
50
+ }
51
+ });
52
+ }
53
+ function manifestMalformed(args) {
54
+ return new ShaderError({
55
+ code: "manifest-malformed",
56
+ expected: "manifest.json parses + every entry has {hash, wgsl, glsl, bindings}",
57
+ message: args.message,
58
+ hint: args.hint,
59
+ detail: {
60
+ code: "manifest-malformed",
61
+ ...args.reason !== void 0 ? { reason: args.reason } : {}
62
+ }
63
+ });
64
+ }
65
+ function shaderNotFound(args) {
66
+ return new ShaderError({
67
+ code: "shader-not-found",
68
+ expected: `manifest.entries contains entry with hash '${args.hash}'`,
69
+ message: `ShaderRegistry: hash '${args.hash}' not present in manifest`,
70
+ hint: args.hint
71
+ });
72
+ }
73
+ function wrapShaderError(e, hint) {
74
+ if (e instanceof Error) {
75
+ try {
76
+ const payload = JSON.parse(e.message);
77
+ return compileFailed({
78
+ message: payload.summary ?? payload.message ?? e.message,
79
+ hint: hint ?? "fix the WGSL source at the indicated line/column; see ShaderError.detail.compilerMessages for full diagnostic frame",
80
+ ...typeof payload.line_num === "number" ? { lineNum: payload.line_num } : {},
81
+ ...typeof payload.line_pos === "number" ? { linePos: payload.line_pos } : {}
82
+ });
83
+ } catch {
84
+ return compileFailed({
85
+ message: e.message,
86
+ hint: hint ?? "check WGSL syntax + validation rules; consult naga error output for details"
87
+ });
88
+ }
89
+ }
90
+ return compileFailed({
91
+ message: String(e),
92
+ hint: hint ?? "unknown error type from @forgeax/engine-wgpu-wasm; report as @forgeax/engine-naga bug"
93
+ });
94
+ }
95
+
96
+ // src/index.ts
97
+ async function parse(source) {
98
+ let wasm;
99
+ try {
100
+ wasm = await ensureReady();
101
+ } catch (e) {
102
+ return err(
103
+ wrapShaderError(
104
+ e,
105
+ "rerun bash packages/wgpu-wasm/build.sh and verify packages/wgpu-wasm/pkg contains a fresh .wasm"
106
+ )
107
+ );
108
+ }
109
+ try {
110
+ const parsed = wasm.parse(source);
111
+ return ok(parsed);
112
+ } catch (e) {
113
+ return err(wrapShaderError(e));
114
+ }
115
+ }
116
+ async function validate(parsed) {
117
+ let wasm;
118
+ try {
119
+ wasm = await ensureReady();
120
+ } catch (e) {
121
+ return err(
122
+ wrapShaderError(
123
+ e,
124
+ "rerun bash packages/wgpu-wasm/build.sh and verify packages/wgpu-wasm/pkg contains a fresh .wasm"
125
+ )
126
+ );
127
+ }
128
+ try {
129
+ const validated = wasm.validate(parsed);
130
+ return ok(validated);
131
+ } catch (e) {
132
+ return err(wrapShaderError(e));
133
+ }
134
+ }
135
+ async function composeShader(entry, imports, defines) {
136
+ const wasm = await ensureReady();
137
+ const compose = wasm.compose_shader;
138
+ return compose(entry, JSON.stringify(imports), JSON.stringify(defines));
139
+ }
140
+ async function emit_reflection(validated, options_json) {
141
+ let wasm;
142
+ try {
143
+ wasm = await ensureReady();
144
+ } catch (e) {
145
+ return err(
146
+ wrapShaderError(
147
+ e,
148
+ "rerun bash packages/wgpu-wasm/build.sh and verify packages/wgpu-wasm/pkg contains a fresh .wasm"
149
+ )
150
+ );
151
+ }
152
+ try {
153
+ const reflectionJson = wasm.emit_reflection(
154
+ validated,
155
+ options_json
156
+ );
157
+ return ok(reflectionJson);
158
+ } catch (e) {
159
+ return err(wrapShaderError(e));
160
+ }
161
+ }
162
+
163
+ export { ShaderError, compileFailed, composeShader, emit_reflection, initFailed, manifestMalformed, parse, shaderNotFound, validate };
164
+ //# sourceMappingURL=index.mjs.map
165
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["// @forgeax/engine-naga/errors — ShaderError + Result<T, E> + wrapShaderError helper.\n//\n// Form invariants (plan-strategy §D-P4 / requirements MVP-2.3 + AC-09):\n// - ShaderErrorCode (closed 4-member union) is imported from @forgeax/engine-types as\n// the SSOT. This package does **not** redefine the union — +0 breaking points\n// to the error model (AC-09; charter proposition 5 consistent abstraction).\n// - The ShaderError class shape + 4 factory helpers + Result<T, E> shape are\n// byte-for-byte equivalent to @forgeax/engine-shader-compiler/src/errors.ts; that\n// package will re-export from here in w7 after the import switch lands\n// (charter proposition 5 + plan-strategy D-P4 byte-for-byte form recovery).\n// - wrapShaderError is the JsError -> ShaderError adapter for the parse /\n// validate / emit_reflection wrappers in index.ts: it tries to JSON.parse the\n// wasm-side ParseErrorPayload (message / summary / line_num / line_pos)\n// first, falls back to the prose message for validator / reflection failures.\n\n/// <reference types=\"@webgpu/types\" />\n\nimport type { ShaderErrorCode, ShaderErrorDetail } from '@forgeax/engine-types';\n\nexport type { ShaderErrorCode, ShaderErrorDetail };\n\ninterface ShaderErrorInit {\n readonly code: ShaderErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly message: string;\n readonly lineNum?: number | undefined;\n readonly linePos?: number | undefined;\n readonly detail?: ShaderErrorDetail | undefined;\n}\n\n/**\n * Structured shader error.\n *\n * **5 surface fields** (MVP-2.3 top-level surface, AI consumer path):\n * - `.code` — member of the closed `ShaderErrorCode` union (4 variants)\n * - `.message` — display text (the base `Error` field, populated by the constructor)\n * - `.hint` — actionable recovery guidance (charter proposition 3: machine-readable hint over prose)\n * - `.lineNum` / `.linePos` — error source location (mandatory for compile-failed; undefined on other paths)\n *\n * **3 internal fields**:\n * - `.name` = `'ShaderError'` (debug tag)\n * - `.expected` — description of the expected state (symmetric with RhiError)\n * - `.detail` — path-specific extra info (e.g. all 6 fields of GPUCompilationMessage[])\n *\n * **Do not new directly** — construct via the 4 factories (compileFailed /\n * initFailed / manifestMalformed / shaderNotFound) to avoid ad-hoc arguments\n * bypassing union narrowing.\n */\nexport class ShaderError extends Error {\n override readonly name: 'ShaderError' = 'ShaderError';\n readonly code: ShaderErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly lineNum: number | undefined;\n readonly linePos: number | undefined;\n readonly detail: ShaderErrorDetail | undefined;\n\n constructor(init: ShaderErrorInit) {\n super(init.message);\n this.code = init.code;\n this.expected = init.expected;\n this.hint = init.hint;\n this.lineNum = init.lineNum;\n this.linePos = init.linePos;\n this.detail = init.detail;\n }\n}\n\n// === 4 factory helpers (plan-strategy §D-P4 closed union with 4 members) ===========\n\n/** `shader-compile-failed`: naga parse_str or Validator::validate failure. */\nexport function compileFailed(args: {\n readonly message: string;\n readonly hint: string;\n readonly lineNum?: number | undefined;\n readonly linePos?: number | undefined;\n readonly compilerMessages?: readonly GPUCompilationMessage[] | undefined;\n readonly reason?: string | undefined;\n}): ShaderError {\n return new ShaderError({\n code: 'shader-compile-failed',\n expected: 'WGSL source parses + validates against naga IR',\n message: args.message,\n hint: args.hint,\n ...(args.lineNum !== undefined ? { lineNum: args.lineNum } : {}),\n ...(args.linePos !== undefined ? { linePos: args.linePos } : {}),\n ...(args.compilerMessages !== undefined\n ? {\n detail: {\n code: 'shader-compile-failed',\n compilerMessages: args.compilerMessages,\n ...(args.reason !== undefined ? { reason: args.reason } : {}),\n },\n }\n : {}),\n });\n}\n\n/** `compiler-init-failed`: wasm loading or init() failure (cold start / missing wasm artifact). */\nexport function initFailed(args: {\n readonly message: string;\n readonly hint: string;\n readonly reason?: string | undefined;\n}): ShaderError {\n return new ShaderError({\n code: 'compiler-init-failed',\n expected: '@forgeax/engine-wgpu-wasm ensureReady() resolves with naga raw bindings available',\n message: args.message,\n hint: args.hint,\n detail: {\n code: 'compiler-init-failed',\n ...(args.reason !== undefined ? { reason: args.reason } : {}),\n },\n });\n}\n\n/** `manifest-malformed`: manifest.json schema validation failure (a required field is missing or JSON is not parseable). */\nexport function manifestMalformed(args: {\n readonly message: string;\n readonly hint: string;\n readonly reason?: string | undefined;\n}): ShaderError {\n return new ShaderError({\n code: 'manifest-malformed',\n expected: 'manifest.json parses + every entry has {hash, wgsl, glsl, bindings}',\n message: args.message,\n hint: args.hint,\n detail: {\n code: 'manifest-malformed',\n ...(args.reason !== undefined ? { reason: args.reason } : {}),\n },\n });\n}\n\n/** `shader-not-found`: ShaderRegistry.get(hash) hash miss. */\nexport function shaderNotFound(args: {\n readonly hash: string;\n readonly hint: string;\n}): ShaderError {\n return new ShaderError({\n code: 'shader-not-found',\n expected: `manifest.entries contains entry with hash '${args.hash}'`,\n message: `ShaderRegistry: hash '${args.hash}' not present in manifest`,\n hint: args.hint,\n });\n}\n\n// === wrapShaderError: JsError -> ShaderError adapter ================================\n\n/**\n * Translate a thrown wasm-bindgen JsError to a structured ShaderError.\n *\n * The Rust side serializes `ParseErrorPayload { message, summary, line_num,\n * line_pos }` to a JSON string and uses it as the JsError message. We attempt\n * JSON.parse first; on success the lineNum/linePos are extracted as top-level\n * surface fields (MVP-2.3). On failure (validator errors, reflection errors,\n * non-JSON messages) we fall back to the prose message — hint is always\n * populated so AI consumers always have an actionable recovery signal\n * (charter proposition 4 explicit failure + proposition 3 machine-readable hint).\n */\nexport function wrapShaderError(e: unknown, hint?: string): ShaderError {\n if (e instanceof Error) {\n try {\n const payload = JSON.parse(e.message) as {\n message?: string;\n summary?: string;\n line_num?: number | null;\n line_pos?: number | null;\n };\n return compileFailed({\n message: payload.summary ?? payload.message ?? e.message,\n hint:\n hint ??\n 'fix the WGSL source at the indicated line/column; see ShaderError.detail.compilerMessages for full diagnostic frame',\n ...(typeof payload.line_num === 'number' ? { lineNum: payload.line_num } : {}),\n ...(typeof payload.line_pos === 'number' ? { linePos: payload.line_pos } : {}),\n });\n } catch {\n return compileFailed({\n message: e.message,\n hint: hint ?? 'check WGSL syntax + validation rules; consult naga error output for details',\n });\n }\n }\n return compileFailed({\n message: String(e),\n hint:\n hint ??\n 'unknown error type from @forgeax/engine-wgpu-wasm; report as @forgeax/engine-naga bug',\n });\n}\n\n// === Result<T, E> ====================================================================\n//\n// Result<T, E> + ok / err + ResultOk / ResultErr live in `@forgeax/engine-types`\n// (tweak-20260612-result-into-types). They were duplicated here as a lite\n// (plain-object, no `unwrap`) variant; consolidated upstream into the same\n// shape used by rhi / ecs. The barrel here re-exports them so existing\n// `import { err, ok, Result, ResultOk, ResultErr } from '@forgeax/engine-naga'`\n// consumers stay unchanged.\nexport {\n err,\n ok,\n type Result,\n type ResultErr,\n type ResultOk,\n} from '@forgeax/engine-types';\n","// @forgeax/engine-naga — TS-only thin shell over @forgeax/engine-wgpu-wasm raw naga bindings.\n//\n// Form invariants (locked by plan-strategy D-P3 / D-P4 + research F-4):\n//\n// - snake_case three-phase functions byte-for-byte aligned with naga upstream\n// naming (this package replaces the legacy wasm-pack shim archived in\n// feat-20260511-naga-rhi-wgpu-merge M5 — charter proposition 2 industry\n// analogy + proposition 5 consistent abstraction).\n// - Each public function awaits ensureReady() from @forgeax/engine-wgpu-wasm before\n// calling the raw wasm-bindgen export — one wasm boundary crossing per page\n// lifecycle, shared with @forgeax/engine-rhi-wgpu (research F-4 ensureReady SSOT).\n// - Throws are caught at the wrapper boundary and translated to\n// Result.err(ShaderError) — never throw for expected failures\n// (AGENTS.md \"Errors are structured\" + charter proposition 4 explicit failure).\n// - The opaque handle types ParsedModule / ValidatedModule are re-exported\n// so downstream consumers (@forgeax/engine-shader-compiler) can hold the handle\n// between phases without inspecting the underlying naga IR\n// (plan-strategy §S-1 opaque handle invariant).\n\nimport { ensureReady } from '@forgeax/engine-wgpu-wasm';\nimport { err, ok, type Result, type ShaderError, wrapShaderError } from './errors.js';\n\nexport {\n compileFailed,\n err,\n initFailed,\n manifestMalformed,\n ok,\n type Result,\n type ResultErr,\n type ResultOk,\n ShaderError,\n type ShaderErrorCode,\n type ShaderErrorDetail,\n shaderNotFound,\n} from './errors.js';\n\n// === Opaque handle types ============================================================\n\n/**\n * Handle for the `parse` output. The underlying type is a wasm-bindgen exported\n * struct: JS can only hold the handle — it cannot inspect naga IR fields\n * directly (charter proposition 4 + opaque handle invariant). Pass through to\n * `validate` to advance to phase 2.\n *\n * Surface type uses `unknown` to keep this layer math-free and opaque-handle\n * pure (no direct dependency on @forgeax/engine-wgpu-wasm/pkg ABI types). Downstream\n * consumers should not inspect the handle.\n */\nexport type ParsedModule = unknown;\n\n/**\n * Handle for the `validate` output (Module + ModuleInfo); pass through to\n * `emit_reflection` for the reflection JSON emit.\n */\nexport type ValidatedModule = unknown;\n\n/** Structured material facts emitted by the independent raw Naga reflection. */\nexport interface RawMaterialMemberFact {\n readonly name: string;\n readonly type: string;\n readonly offset: number;\n readonly size: number;\n readonly alignment: number;\n}\n\nexport interface RawMaterialResourceFact {\n readonly name: string;\n readonly kind: 'sampler' | 'texture' | 'storage-buffer';\n readonly binding: number;\n}\n\nexport interface RawMaterialReflectionFacts {\n readonly members: readonly RawMaterialMemberFact[];\n readonly resources: readonly RawMaterialResourceFact[];\n readonly totalBytes: number;\n}\n\n// === Phase 1: parse =================================================================\n\n/**\n * WGSL source -> `ParsedModule`.\n *\n * On failure returns `Result.err(ShaderError code='shader-compile-failed')`\n * whose `lineNum` / `linePos` carry the source position (from the wasm-side\n * `ParseErrorPayload`). The hint defaults to actionable WGSL fix guidance.\n *\n * Wasm boundary: awaits ensureReady() on first call (shared singleton with\n * @forgeax/engine-rhi-wgpu); subsequent calls take the cached path.\n */\nexport async function parse(source: string): Promise<Result<ParsedModule, ShaderError>> {\n let wasm: Awaited<ReturnType<typeof ensureReady>>;\n try {\n wasm = await ensureReady();\n } catch (e) {\n return err(\n wrapShaderError(\n e,\n 'rerun bash packages/wgpu-wasm/build.sh and verify packages/wgpu-wasm/pkg contains a fresh .wasm',\n ),\n );\n }\n try {\n const parsed = wasm.parse(source);\n return ok(parsed as ParsedModule);\n } catch (e) {\n return err(wrapShaderError(e));\n }\n}\n\n// === Phase 2: validate ==============================================================\n\n/**\n * `ParsedModule` -> `ValidatedModule` (Module + ModuleInfo).\n *\n * **Ownership transfer** — wasm-bindgen consumes the `parsed` handle. Do not\n * reuse the handle after this call; passing a consumed handle is undefined\n * behaviour on the wasm side (research Finding 6 ownership semantics).\n *\n * On failure returns `Result.err(ShaderError code='shader-compile-failed')`.\n * Validator errors have no source position attached on the wasm side, so\n * `lineNum` / `linePos` remain undefined.\n */\nexport async function validate(\n parsed: ParsedModule,\n): Promise<Result<ValidatedModule, ShaderError>> {\n let wasm: Awaited<ReturnType<typeof ensureReady>>;\n try {\n wasm = await ensureReady();\n } catch (e) {\n return err(\n wrapShaderError(\n e,\n 'rerun bash packages/wgpu-wasm/build.sh and verify packages/wgpu-wasm/pkg contains a fresh .wasm',\n ),\n );\n }\n try {\n const validated = (wasm.validate as (p: unknown) => unknown)(parsed);\n return ok(validated as ValidatedModule);\n } catch (e) {\n return err(wrapShaderError(e));\n }\n}\n\n// === Composer passthrough ===========================================================\n\n/**\n * naga_oil Composer passthrough — `#import` + `#ifdef` composition over WGSL.\n *\n * Thin TS wrap over `@forgeax/engine-wgpu-wasm`'s raw `compose_shader` export\n * (feat-20260512 M1 compose.rs). Three-argument surface:\n *\n * - `entry` — the entry-point WGSL source (may contain `#import` directives\n * and `#ifdef` guards).\n * - `imports` — `moduleId -> wgslSource` map; each value is a companion\n * module whose header declares `#define_import_path <moduleId>` so the\n * upstream composer can register it. The map is JSON.stringified at the\n * wasm boundary.\n * - `defines` — `name -> boolean` map driving `#ifdef` branch elimination\n * (plan-strategy D-06: non-boolean values are rejected at the TS layer by\n * the shader-compiler wrapper; this wrap takes booleans verbatim). Also\n * JSON.stringified at the boundary.\n *\n * Return: the composed WGSL string (entry + inlined imports, `#ifdef` branches\n * resolved).\n *\n * Errors: the raw wasm export throws `JsError` whose message carries a\n * `shader-import-not-found: ...` or `shader-compile-failed: ...` prefix\n * (feat-20260512 M1 compose.rs convention). This wrap does **not** translate\n * the prefix into a structured `ShaderError`; that splitting happens one layer\n * up at `@forgeax/engine-shader-compiler` (feat-20260512 M3), which is where\n * the three-argument `compileShader(src, { imports, defines, id })` entry lives.\n * Callers of this raw passthrough should `try / catch` the thrown error.\n *\n * Wasm boundary: awaits `ensureReady()` on first call (shared singleton with\n * `@forgeax/engine-rhi-wgpu` + other naga phases); subsequent calls take the\n * cached path.\n */\nexport async function composeShader(\n entry: string,\n imports: Record<string, string>,\n defines: Record<string, boolean>,\n): Promise<string> {\n const wasm = await ensureReady();\n const compose = (wasm as { compose_shader: (e: string, i: string, d: string) => string })\n .compose_shader;\n return compose(entry, JSON.stringify(imports), JSON.stringify(defines));\n}\n\n// === Phase 3: emit_reflection =======================================================\n\n/**\n * `ValidatedModule` + options JSON -> `BindGroupLayoutDescriptor[]` JSON string.\n *\n * `options_json` shape: `{ \"dynamicOffsets\": [{ \"group\": u32, \"binding\": u32 }, ...] }`.\n * The naga IR does not express the dynamic-offset dimension (research Finding 2\n * footnote), so it is injected via this JS-side options string. Pass an empty\n * `{}` (or a JSON-encoded object without `dynamicOffsets`) for the no-dynamic-\n * offset path.\n *\n * The validator's borrowed reference is **not** consumed — the same\n * `ValidatedModule` handle can be reused for repeated emits with different\n * options (e.g. for variant generation).\n */\nexport async function emit_reflection(\n validated: ValidatedModule,\n options_json: string,\n): Promise<Result<string, ShaderError>> {\n let wasm: Awaited<ReturnType<typeof ensureReady>>;\n try {\n wasm = await ensureReady();\n } catch (e) {\n return err(\n wrapShaderError(\n e,\n 'rerun bash packages/wgpu-wasm/build.sh and verify packages/wgpu-wasm/pkg contains a fresh .wasm',\n ),\n );\n }\n try {\n const reflectionJson = (wasm.emit_reflection as (v: unknown, o: string) => string)(\n validated,\n options_json,\n );\n return ok(reflectionJson);\n } catch (e) {\n return err(wrapShaderError(e));\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@forgeax/engine-naga",
3
+ "version": "0.1.2",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "sideEffects": false,
8
+ "description": "TS-only thin shell over @forgeax/engine-wgpu-wasm raw naga bindings — build-time shader pipeline tooling (parse / validate / emit_reflection). Consumed by @forgeax/engine-shader-compiler; AI engine users may import directly for WGSL syntax-check reuse.",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "main": "./dist/index.mjs",
17
+ "types": "./dist/index.d.ts",
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "dependencies": {
25
+ "@forgeax/engine-types": "0.1.2",
26
+ "@forgeax/engine-wgpu-wasm": "0.1.2",
27
+ "@webgpu/types": "^0.1.71"
28
+ },
29
+ "forgeax": {
30
+ "metrics": {
31
+ "bundle-size": {
32
+ "enabled": false,
33
+ "reason": "TS-only thin shell; physical wasm size measured at @forgeax/engine-wgpu-wasm workspace baseline. No standalone JS bundle size budget enforced at this layer."
34
+ },
35
+ "fps": {
36
+ "enabled": false,
37
+ "reason": "build-time compiler shim; no runtime canvas"
38
+ },
39
+ "bench": {
40
+ "enabled": false,
41
+ "reason": "naga parse / validate / emit_reflection timing is shader-compile-time bench; pending feat-future-shader-compile-time"
42
+ },
43
+ "gate": {
44
+ "enabled": false,
45
+ "reason": "physical-isolation grep gates (check-shader-no-naga-in-dist + check-shader-runtime-deps + check-shader-no-compiler-import) are repo-root scripts wired into ci.yml lint job, not a per-package gate"
46
+ },
47
+ "spike-report": {
48
+ "enabled": false,
49
+ "reason": "not a spike package; productionised TS-only thin shell over @forgeax/engine-wgpu-wasm"
50
+ }
51
+ }
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "test": "vitest run"
56
+ }
57
+ }
@@ -0,0 +1,335 @@
1
+ // Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
2
+ // biome-ignore-all lint/complexity/noUselessLoneBlockStatements: scope isolation between merged source files
3
+ //
4
+ // Source files (N=5):
5
+ // - packages/naga/src/__tests__/compose.test.ts
6
+ // - packages/naga/src/__tests__/emit_reflection.test.ts
7
+ // - packages/naga/src/__tests__/errors.test.ts
8
+ // - packages/naga/src/__tests__/parse.test.ts
9
+ // - packages/naga/src/__tests__/validate.test.ts
10
+ //
11
+ // Paradigm: each block-scoped describe('<source-filename>.test.ts', ...) preserves
12
+ // source as ancestorTitles[0]. Top-level imports merged + deduped.
13
+ //
14
+ // Naga packages share a common vi.mock('@forgeax/engine-wgpu-wasm') pattern.
15
+ // Merged into one unified mock that provides all needed functions.
16
+
17
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
18
+ import { compileFailed } from '../errors.js';
19
+
20
+ const _parse = vi.fn();
21
+ const _validate = vi.fn();
22
+ const _emit_reflection = vi.fn();
23
+ const _compose = vi.fn();
24
+
25
+ vi.mock('@forgeax/engine-wgpu-wasm', () => ({
26
+ ensureReady: vi.fn(async () => ({
27
+ parse: _parse,
28
+ validate: _validate,
29
+ emit_reflection: _emit_reflection,
30
+ compose_shader: _compose,
31
+ })),
32
+ }));
33
+
34
+ {
35
+ // ─── from compose.test.ts ───
36
+
37
+ describe('compose.test.ts', () => {
38
+ describe('@forgeax/engine-naga composeShader wrapper', () => {
39
+ beforeEach(() => {
40
+ _compose.mockReset();
41
+ _parse.mockReset();
42
+ _validate.mockReset();
43
+ _emit_reflection.mockReset();
44
+ vi.resetModules();
45
+ });
46
+
47
+ afterEach(() => {
48
+ vi.clearAllMocks();
49
+ });
50
+
51
+ it('forwards entry + JSON-stringified imports + defines to wasm compose_shader and returns the composed WGSL string (basic #import happy path)', async () => {
52
+ const entry = [
53
+ '#import forgeax_pbr::brdf',
54
+ '@vertex fn vs() -> @builtin(position) vec4<f32> { return brdf::sample(); }',
55
+ ].join('\n');
56
+ const imports = {
57
+ 'forgeax_pbr::brdf': [
58
+ '#define_import_path forgeax_pbr::brdf',
59
+ 'fn sample() -> vec4<f32> { return vec4<f32>(0.0); }',
60
+ ].join('\n'),
61
+ };
62
+ const defines = { FOO: true };
63
+ const composed = '// composed wgsl\nfn sample() -> vec4<f32> { return vec4<f32>(0.0); }';
64
+ _compose.mockReturnValueOnce(composed);
65
+
66
+ const { composeShader } = await import('../index.js');
67
+ const out = await composeShader(entry, imports, defines);
68
+
69
+ expect(out).toBe(composed);
70
+ expect(_compose).toHaveBeenCalledTimes(1);
71
+ const call = _compose.mock.calls[0];
72
+ if (!call) throw new Error('compose_shader mock was not called');
73
+ const [forwardedEntry, forwardedImportsJson, forwardedDefinesJson] = call;
74
+ expect(forwardedEntry).toBe(entry);
75
+ expect(JSON.parse(forwardedImportsJson)).toEqual(imports);
76
+ expect(JSON.parse(forwardedDefinesJson)).toEqual(defines);
77
+ });
78
+
79
+ it('propagates shader-import-not-found: prefix error from wasm compose_shader JsError', async () => {
80
+ const entry =
81
+ '#import forgeax_missing::mod\n@vertex fn vs() -> @builtin(position) vec4<f32> { return vec4<f32>(0.0); }';
82
+ _compose.mockImplementationOnce(() => {
83
+ throw new Error('shader-import-not-found: module forgeax_missing::mod not registered');
84
+ });
85
+
86
+ const { composeShader } = await import('../index.js');
87
+ await expect(composeShader(entry, {}, {})).rejects.toThrow(/shader-import-not-found:/);
88
+ });
89
+
90
+ it('passes #ifdef defines through to wasm so the upstream composer can eliminate branches (define=true branch retained; define=false branch dropped)', async () => {
91
+ const entry = [
92
+ '#ifdef WANT_FOO',
93
+ 'fn foo() -> f32 { return 1.0; }',
94
+ '#endif',
95
+ '#ifdef WANT_BAR',
96
+ 'fn bar() -> f32 { return 2.0; }',
97
+ '#endif',
98
+ ].join('\n');
99
+
100
+ const composed = 'fn foo() -> f32 { return 1.0; }';
101
+ _compose.mockReturnValueOnce(composed);
102
+
103
+ const { composeShader } = await import('../index.js');
104
+ const out = await composeShader(entry, {}, { WANT_FOO: true, WANT_BAR: false });
105
+
106
+ expect(out).toBe(composed);
107
+ const call = _compose.mock.calls[0];
108
+ if (!call) throw new Error('compose_shader mock was not called');
109
+ const [, , forwardedDefinesJson] = call;
110
+ expect(JSON.parse(forwardedDefinesJson)).toEqual({ WANT_FOO: true, WANT_BAR: false });
111
+ });
112
+ });
113
+ });
114
+ }
115
+
116
+ {
117
+ // ─── from emit_reflection.test.ts ───
118
+
119
+ describe('emit_reflection.test.ts', () => {
120
+ describe('@forgeax/engine-naga emit_reflection wrapper', () => {
121
+ beforeEach(() => {
122
+ _parse.mockReset();
123
+ _validate.mockReset();
124
+ _emit_reflection.mockReset();
125
+ vi.resetModules();
126
+ });
127
+
128
+ afterEach(() => {
129
+ vi.clearAllMocks();
130
+ });
131
+
132
+ it('returns Result.ok(reflectionJson) and forwards (validated, options_json) verbatim', async () => {
133
+ const validatedHandle = { _tag: 'ValidatedModule' };
134
+ const expectedJson = '[{"label":"@group(0)","entries":[]}]';
135
+ _emit_reflection.mockReturnValueOnce(expectedJson);
136
+ const optionsJson = JSON.stringify({ dynamicOffsets: [{ group: 0, binding: 0 }] });
137
+ const { emit_reflection } = await import('../index.js');
138
+ const r = await emit_reflection(validatedHandle, optionsJson);
139
+ expect(r.ok).toBe(true);
140
+ if (r.ok) {
141
+ expect(r.value).toBe(expectedJson);
142
+ }
143
+ expect(_emit_reflection).toHaveBeenCalledWith(validatedHandle, optionsJson);
144
+ });
145
+
146
+ it('preserves structured material member and resource facts from raw reflection', async () => {
147
+ const expectedJson = JSON.stringify({
148
+ bindings: [],
149
+ uvSetCount: 0,
150
+ material: {
151
+ members: [
152
+ {
153
+ name: 'albedoCoordinatesTransform',
154
+ type: 'vec4<f32>',
155
+ offset: 0,
156
+ size: 16,
157
+ alignment: 16,
158
+ },
159
+ ],
160
+ resources: [{ name: 'albedo', kind: 'texture', binding: 2 }],
161
+ totalBytes: 16,
162
+ },
163
+ });
164
+ _emit_reflection.mockReturnValueOnce(expectedJson);
165
+ const { emit_reflection } = await import('../index.js');
166
+ const result = await emit_reflection({ _tag: 'ValidatedModule' }, '{}');
167
+
168
+ expect(result.ok).toBe(true);
169
+ if (result.ok) {
170
+ expect(JSON.parse(result.value).material.members[0].offset).toBe(0);
171
+ expect(JSON.parse(result.value).material.resources[0].binding).toBe(2);
172
+ }
173
+ });
174
+
175
+ it('returns Result.err(ShaderError) with non-empty hint when raw emit_reflection throws', async () => {
176
+ _emit_reflection.mockImplementationOnce(() => {
177
+ throw new Error('reflection serialize failed: cyclic type graph');
178
+ });
179
+ const { emit_reflection } = await import('../index.js');
180
+ const r = await emit_reflection({ _tag: 'ValidatedModule' }, '{}');
181
+ expect(r.ok).toBe(false);
182
+ if (!r.ok) {
183
+ expect(r.error.code).toBe('shader-compile-failed');
184
+ expect(r.error.message).toContain('cyclic type graph');
185
+ expect(r.error.hint).toBeTruthy();
186
+ expect(r.error.hint.length).toBeGreaterThan(0);
187
+ }
188
+ });
189
+ });
190
+ });
191
+ }
192
+
193
+ {
194
+ // ─── from errors.test.ts ───
195
+
196
+ describe('errors.test.ts', () => {
197
+ describe('compileFailed factory — D-PS-3 path (b) contract', () => {
198
+ it('compileFailed without compilerMessages keeps detail undefined', () => {
199
+ const err = compileFailed({
200
+ message: 'WGSL parse failed at line 3, column 5',
201
+ hint: 'fix the WGSL source at the indicated line/column',
202
+ });
203
+ expect(err.code).toBe('shader-compile-failed');
204
+ expect(err.detail).toBeUndefined();
205
+ });
206
+
207
+ it('compileFailed with compilerMessages constructs typed detail (regression smoke)', () => {
208
+ const err = compileFailed({
209
+ message: 'WGSL parse failed',
210
+ hint: 'see ShaderError.detail.compilerMessages',
211
+ compilerMessages: [],
212
+ });
213
+ expect(err.code).toBe('shader-compile-failed');
214
+ expect(err.detail).toBeDefined();
215
+ });
216
+ });
217
+ });
218
+ }
219
+
220
+ {
221
+ // ─── from parse.test.ts ───
222
+
223
+ describe('parse.test.ts', () => {
224
+ describe('@forgeax/engine-naga parse wrapper', () => {
225
+ beforeEach(() => {
226
+ _parse.mockReset();
227
+ _validate.mockReset();
228
+ _emit_reflection.mockReset();
229
+ vi.resetModules();
230
+ });
231
+
232
+ afterEach(() => {
233
+ vi.clearAllMocks();
234
+ });
235
+
236
+ it('returns Result.ok(opaque handle) when raw wasm parse succeeds', async () => {
237
+ const opaqueHandle = { _tag: 'ParsedModule' };
238
+ _parse.mockReturnValueOnce(opaqueHandle);
239
+ const { parse } = await import('../index.js');
240
+ const r = await parse(
241
+ '@vertex fn vs() -> @builtin(position) vec4<f32> { return vec4<f32>(0.0); }',
242
+ );
243
+ expect(r.ok).toBe(true);
244
+ if (r.ok) {
245
+ expect(r.value).toBe(opaqueHandle);
246
+ }
247
+ expect(_parse).toHaveBeenCalledTimes(1);
248
+ });
249
+
250
+ it('returns Result.err(ShaderError code=shader-compile-failed) with lineNum/linePos when raw parse throws JsError with ParseErrorPayload', async () => {
251
+ const payload = {
252
+ message: "expected ';', found '@'",
253
+ summary: "expected ';', found '@' at line 3 col 12",
254
+ line_num: 3,
255
+ line_pos: 12,
256
+ };
257
+ _parse.mockImplementationOnce(() => {
258
+ throw new Error(JSON.stringify(payload));
259
+ });
260
+ const { parse } = await import('../index.js');
261
+ const r = await parse('invalid wgsl');
262
+ expect(r.ok).toBe(false);
263
+ if (!r.ok) {
264
+ expect(r.error.code).toBe('shader-compile-failed');
265
+ expect(r.error.lineNum).toBe(3);
266
+ expect(r.error.linePos).toBe(12);
267
+ expect(r.error.message).toContain('line 3');
268
+ }
269
+ });
270
+
271
+ it('hint is non-empty for every error path (charter proposition 3)', async () => {
272
+ const payload = { message: 'parse failed', line_num: 1, line_pos: 1 };
273
+ _parse.mockImplementationOnce(() => {
274
+ throw new Error(JSON.stringify(payload));
275
+ });
276
+ const { parse } = await import('../index.js');
277
+ const r = await parse('invalid');
278
+ expect(r.ok).toBe(false);
279
+ if (!r.ok) {
280
+ expect(r.error.hint).toBeTruthy();
281
+ expect(r.error.hint.length).toBeGreaterThan(0);
282
+ }
283
+ });
284
+ });
285
+ });
286
+ }
287
+
288
+ {
289
+ // ─── from validate.test.ts ───
290
+
291
+ describe('validate.test.ts', () => {
292
+ describe('@forgeax/engine-naga validate wrapper', () => {
293
+ beforeEach(() => {
294
+ _parse.mockReset();
295
+ _validate.mockReset();
296
+ _emit_reflection.mockReset();
297
+ vi.resetModules();
298
+ });
299
+
300
+ afterEach(() => {
301
+ vi.clearAllMocks();
302
+ });
303
+
304
+ it('returns Result.ok(opaque handle) when raw wasm validate succeeds', async () => {
305
+ const parsedHandle = { _tag: 'ParsedModule' };
306
+ const validatedHandle = { _tag: 'ValidatedModule' };
307
+ _validate.mockReturnValueOnce(validatedHandle);
308
+ const { validate } = await import('../index.js');
309
+ const r = await validate(parsedHandle);
310
+ expect(r.ok).toBe(true);
311
+ if (r.ok) {
312
+ expect(r.value).toBe(validatedHandle);
313
+ }
314
+ expect(_validate).toHaveBeenCalledTimes(1);
315
+ expect(_validate).toHaveBeenCalledWith(parsedHandle);
316
+ });
317
+
318
+ it('returns Result.err(ShaderError) with prose fallback when validator throws non-JSON message', async () => {
319
+ _validate.mockImplementationOnce(() => {
320
+ throw new Error('validate failed: type mismatch in @location(0)');
321
+ });
322
+ const { validate } = await import('../index.js');
323
+ const r = await validate({ _tag: 'ParsedModule' });
324
+ expect(r.ok).toBe(false);
325
+ if (!r.ok) {
326
+ expect(r.error.code).toBe('shader-compile-failed');
327
+ expect(r.error.message).toContain('type mismatch');
328
+ expect(r.error.lineNum).toBeUndefined();
329
+ expect(r.error.linePos).toBeUndefined();
330
+ expect(r.error.hint).toBeTruthy();
331
+ }
332
+ });
333
+ });
334
+ });
335
+ }