@forgeax/engine-gltf 0.0.0-dev.8d955ade1c79

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.
Files changed (220) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +204 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/animation-target-errors.unit.test.d.ts +2 -0
  5. package/dist/__tests__/animation-target-errors.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/bridge-material-samplers.unit.test.d.ts +2 -0
  7. package/dist/__tests__/bridge-material-samplers.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/bridge-material-values.unit.test.d.ts +2 -0
  9. package/dist/__tests__/bridge-material-values.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/bridge-mixed-color.unit.test.d.ts +2 -0
  11. package/dist/__tests__/bridge-mixed-color.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/bridge-sampler-required.test-d.d.ts +2 -0
  13. package/dist/__tests__/bridge-sampler-required.test-d.d.ts.map +1 -0
  14. package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts +2 -0
  15. package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/bridge-skin-stride.unit.test.d.ts +2 -0
  17. package/dist/__tests__/bridge-skin-stride.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/bridge-test-helpers.d.ts +5 -0
  19. package/dist/__tests__/bridge-test-helpers.d.ts.map +1 -0
  20. package/dist/__tests__/bridge.unit.test.d.ts +2 -0
  21. package/dist/__tests__/bridge.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/cli-gltf.integration.test.d.ts +2 -0
  23. package/dist/__tests__/cli-gltf.integration.test.d.ts.map +1 -0
  24. package/dist/__tests__/color-0-accessor.unit.test.d.ts +2 -0
  25. package/dist/__tests__/color-0-accessor.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/data-uri-surface.unit.test.d.ts +2 -0
  27. package/dist/__tests__/data-uri-surface.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/extension-admission.unit.test.d.ts +2 -0
  29. package/dist/__tests__/extension-admission.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/gltf-error-derived-views.test-d.d.ts +2 -0
  31. package/dist/__tests__/gltf-error-derived-views.test-d.d.ts.map +1 -0
  32. package/dist/__tests__/gltf-error-migration.test.d.ts +2 -0
  33. package/dist/__tests__/gltf-error-migration.test.d.ts.map +1 -0
  34. package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts +2 -0
  35. package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts.map +1 -0
  36. package/dist/__tests__/gltf-importer.test.d.ts +2 -0
  37. package/dist/__tests__/gltf-importer.test.d.ts.map +1 -0
  38. package/dist/__tests__/gltf-local-artifacts.test.d.ts +2 -0
  39. package/dist/__tests__/gltf-local-artifacts.test.d.ts.map +1 -0
  40. package/dist/__tests__/gltf.unit.test.d.ts +2 -0
  41. package/dist/__tests__/gltf.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts +2 -0
  43. package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts.map +1 -0
  44. package/dist/__tests__/material-missing-uv.unit.test.d.ts +2 -0
  45. package/dist/__tests__/material-missing-uv.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/material-pack-refs.integration.test.d.ts +2 -0
  47. package/dist/__tests__/material-pack-refs.integration.test.d.ts.map +1 -0
  48. package/dist/__tests__/material-per-slot-ir.unit.test.d.ts +2 -0
  49. package/dist/__tests__/material-per-slot-ir.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/material-texture-transform.unit.test.d.ts +2 -0
  51. package/dist/__tests__/material-texture-transform.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/material-unused-uv.unit.test.d.ts +2 -0
  53. package/dist/__tests__/material-unused-uv.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/meshopt-decode.unit.test.d.ts +2 -0
  55. package/dist/__tests__/meshopt-decode.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/meshopt-falsifier.unit.test.d.ts +2 -0
  57. package/dist/__tests__/meshopt-falsifier.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts +2 -0
  59. package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts.map +1 -0
  60. package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts +2 -0
  61. package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts.map +1 -0
  62. package/dist/__tests__/morph-import.integration.test.d.ts +2 -0
  63. package/dist/__tests__/morph-import.integration.test.d.ts.map +1 -0
  64. package/dist/__tests__/morph-import.unit.test.d.ts +2 -0
  65. package/dist/__tests__/morph-import.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/parse-decode.unit.test.d.ts +2 -0
  67. package/dist/__tests__/parse-decode.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/parse-gltf-multi-uv.test.d.ts +2 -0
  69. package/dist/__tests__/parse-gltf-multi-uv.test.d.ts.map +1 -0
  70. package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts +2 -0
  71. package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts.map +1 -0
  72. package/dist/__tests__/parse-gltf.unit.test.d.ts +2 -0
  73. package/dist/__tests__/parse-gltf.unit.test.d.ts.map +1 -0
  74. package/dist/__tests__/pick-e2e.integration.test.d.ts +2 -0
  75. package/dist/__tests__/pick-e2e.integration.test.d.ts.map +1 -0
  76. package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
  77. package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
  78. package/dist/__tests__/source-key-diagnostics.integration.test.d.ts +2 -0
  79. package/dist/__tests__/source-key-diagnostics.integration.test.d.ts.map +1 -0
  80. package/dist/__tests__/source-key-producer.integration.test.d.ts +2 -0
  81. package/dist/__tests__/source-key-producer.integration.test.d.ts.map +1 -0
  82. package/dist/__tests__/source-key.unit.test.d.ts +2 -0
  83. package/dist/__tests__/source-key.unit.test.d.ts.map +1 -0
  84. package/dist/accessor/decode-accessor.d.ts +75 -0
  85. package/dist/accessor/decode-accessor.d.ts.map +1 -0
  86. package/dist/accessor/decode-color.d.ts +15 -0
  87. package/dist/accessor/decode-color.d.ts.map +1 -0
  88. package/dist/accessor/index.d.ts +3 -0
  89. package/dist/accessor/index.d.ts.map +1 -0
  90. package/dist/bridge.d.ts +103 -0
  91. package/dist/bridge.d.ts.map +1 -0
  92. package/dist/check-extensions.d.ts +29 -0
  93. package/dist/check-extensions.d.ts.map +1 -0
  94. package/dist/cli-gltf.d.ts +8 -0
  95. package/dist/cli-gltf.d.ts.map +1 -0
  96. package/dist/cli-gltf.mjs +2435 -0
  97. package/dist/cli-gltf.mjs.map +1 -0
  98. package/dist/data-uri.d.ts +6 -0
  99. package/dist/data-uri.d.ts.map +1 -0
  100. package/dist/errors.d.ts +201 -0
  101. package/dist/errors.d.ts.map +1 -0
  102. package/dist/extract-image-bytes.d.ts +42 -0
  103. package/dist/extract-image-bytes.d.ts.map +1 -0
  104. package/dist/gltf-importer.d.ts +21 -0
  105. package/dist/gltf-importer.d.ts.map +1 -0
  106. package/dist/image-color-space.d.ts +35 -0
  107. package/dist/image-color-space.d.ts.map +1 -0
  108. package/dist/importer-entry.d.ts +10 -0
  109. package/dist/importer-entry.d.ts.map +1 -0
  110. package/dist/importer-entry.mjs +3378 -0
  111. package/dist/importer-entry.mjs.map +1 -0
  112. package/dist/index.d.ts +25 -0
  113. package/dist/index.d.ts.map +1 -0
  114. package/dist/index.mjs +3673 -0
  115. package/dist/index.mjs.map +1 -0
  116. package/dist/meshopt-decode.d.ts +39 -0
  117. package/dist/meshopt-decode.d.ts.map +1 -0
  118. package/dist/node-file-entry.d.ts +14 -0
  119. package/dist/node-file-entry.d.ts.map +1 -0
  120. package/dist/node-file-entry.mjs +1946 -0
  121. package/dist/node-file-entry.mjs.map +1 -0
  122. package/dist/node-path.d.ts +14 -0
  123. package/dist/node-path.d.ts.map +1 -0
  124. package/dist/parse-animation.d.ts +70 -0
  125. package/dist/parse-animation.d.ts.map +1 -0
  126. package/dist/parse-glb-chunks.d.ts +25 -0
  127. package/dist/parse-glb-chunks.d.ts.map +1 -0
  128. package/dist/parse-glb.d.ts +3 -0
  129. package/dist/parse-glb.d.ts.map +1 -0
  130. package/dist/parse-gltf-header.d.ts +18 -0
  131. package/dist/parse-gltf-header.d.ts.map +1 -0
  132. package/dist/parse-gltf.d.ts +263 -0
  133. package/dist/parse-gltf.d.ts.map +1 -0
  134. package/dist/parse-skin.d.ts +70 -0
  135. package/dist/parse-skin.d.ts.map +1 -0
  136. package/dist/reimport-reuse-meta.d.ts +48 -0
  137. package/dist/reimport-reuse-meta.d.ts.map +1 -0
  138. package/dist/serialize-meta.d.ts +2 -0
  139. package/dist/serialize-meta.d.ts.map +1 -0
  140. package/dist/source-key.d.ts +44 -0
  141. package/dist/source-key.d.ts.map +1 -0
  142. package/dist/sub-asset-key.d.ts +12 -0
  143. package/dist/sub-asset-key.d.ts.map +1 -0
  144. package/dist/to-asset-pack.d.ts +2 -0
  145. package/dist/to-asset-pack.d.ts.map +1 -0
  146. package/dist/transform.d.ts +37 -0
  147. package/dist/transform.d.ts.map +1 -0
  148. package/package.json +92 -0
  149. package/src/__tests__/animation-target-errors.unit.test.ts +51 -0
  150. package/src/__tests__/bridge-material-samplers.unit.test.ts +67 -0
  151. package/src/__tests__/bridge-material-values.unit.test.ts +106 -0
  152. package/src/__tests__/bridge-mixed-color.unit.test.ts +100 -0
  153. package/src/__tests__/bridge-sampler-required.test-d.ts +59 -0
  154. package/src/__tests__/bridge-skin-shader-route.unit.test.ts +42 -0
  155. package/src/__tests__/bridge-skin-stride.unit.test.ts +115 -0
  156. package/src/__tests__/bridge-test-helpers.ts +9 -0
  157. package/src/__tests__/bridge.unit.test.ts +1227 -0
  158. package/src/__tests__/cli-gltf.integration.test.ts +122 -0
  159. package/src/__tests__/color-0-accessor.unit.test.ts +174 -0
  160. package/src/__tests__/data-uri-surface.unit.test.ts +26 -0
  161. package/src/__tests__/extension-admission.unit.test.ts +19 -0
  162. package/src/__tests__/fixtures/color-0/float-vec3.gltf +13 -0
  163. package/src/__tests__/fixtures/color-0/interleaved-vec3.gltf +6 -0
  164. package/src/__tests__/fixtures/color-0/normalized-ubyte.gltf +6 -0
  165. package/src/__tests__/fixtures/color-0/normalized-ushort.gltf +6 -0
  166. package/src/__tests__/gltf-error-derived-views.test-d.ts +152 -0
  167. package/src/__tests__/gltf-error-migration.test.ts +147 -0
  168. package/src/__tests__/gltf-error-policy-owner.unit.test.ts +185 -0
  169. package/src/__tests__/gltf-importer.test.ts +12 -0
  170. package/src/__tests__/gltf-local-artifacts.test.ts +11 -0
  171. package/src/__tests__/gltf.unit.test.ts +2975 -0
  172. package/src/__tests__/malformed-buffer-data-uri.integration.test.ts +140 -0
  173. package/src/__tests__/material-missing-uv.unit.test.ts +29 -0
  174. package/src/__tests__/material-pack-refs.integration.test.ts +58 -0
  175. package/src/__tests__/material-per-slot-ir.unit.test.ts +90 -0
  176. package/src/__tests__/material-texture-transform.unit.test.ts +105 -0
  177. package/src/__tests__/material-unused-uv.unit.test.ts +44 -0
  178. package/src/__tests__/meshopt-decode.unit.test.ts +87 -0
  179. package/src/__tests__/meshopt-falsifier.unit.test.ts +92 -0
  180. package/src/__tests__/meshopt-mode-filter-owner.test-d.ts +79 -0
  181. package/src/__tests__/meshopt-real-matrix.integration.test.ts +370 -0
  182. package/src/__tests__/morph-import.integration.test.ts +115 -0
  183. package/src/__tests__/morph-import.unit.test.ts +62 -0
  184. package/src/__tests__/parse-decode.unit.test.ts +919 -0
  185. package/src/__tests__/parse-gltf-multi-uv.test.ts +238 -0
  186. package/src/__tests__/parse-gltf-skin-attrs.unit.test.ts +191 -0
  187. package/src/__tests__/parse-gltf.unit.test.ts +133 -0
  188. package/src/__tests__/pick-e2e.integration.test.ts +238 -0
  189. package/src/__tests__/public-surface.unit.test.ts +10 -0
  190. package/src/__tests__/source-key-diagnostics.integration.test.ts +140 -0
  191. package/src/__tests__/source-key-producer.integration.test.ts +171 -0
  192. package/src/__tests__/source-key.unit.test.ts +159 -0
  193. package/src/accessor/decode-accessor.ts +309 -0
  194. package/src/accessor/decode-color.ts +128 -0
  195. package/src/accessor/index.ts +15 -0
  196. package/src/bridge.ts +928 -0
  197. package/src/check-extensions.ts +85 -0
  198. package/src/cli-gltf.ts +339 -0
  199. package/src/data-uri.ts +29 -0
  200. package/src/errors.ts +386 -0
  201. package/src/extract-image-bytes.ts +343 -0
  202. package/src/gltf-importer.ts +976 -0
  203. package/src/image-color-space.ts +104 -0
  204. package/src/importer-entry.ts +24 -0
  205. package/src/index.ts +148 -0
  206. package/src/meshopt-decode.ts +200 -0
  207. package/src/node-file-entry.ts +98 -0
  208. package/src/node-path.ts +41 -0
  209. package/src/parse-animation.ts +286 -0
  210. package/src/parse-glb-chunks.ts +167 -0
  211. package/src/parse-glb.ts +16 -0
  212. package/src/parse-gltf-header.ts +58 -0
  213. package/src/parse-gltf.ts +1731 -0
  214. package/src/parse-skin.ts +238 -0
  215. package/src/reimport-reuse-meta.ts +134 -0
  216. package/src/serialize-meta.ts +24 -0
  217. package/src/source-key.ts +106 -0
  218. package/src/sub-asset-key.ts +61 -0
  219. package/src/to-asset-pack.ts +8 -0
  220. package/src/transform.ts +117 -0
@@ -0,0 +1,122 @@
1
+ // packages/gltf/src/__tests__/cli-gltf.integration.test.ts
2
+ //
3
+ // Integration test: run `forgeax-engine-remote-gltf import` against real
4
+ // glTF files and verify the written .meta.json sidecar contains texture
5
+ // sub-assets (feat-20260608 M6 w29 AC-18).
6
+ //
7
+ // Uses the BoxTextured fixture from forgeax-engine-assets.
8
+
9
+ import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
10
+ import { tmpdir } from 'node:os';
11
+ import { dirname, join } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
14
+ import { runCliGltf } from '../cli-gltf.js';
15
+
16
+ interface CapturedIO {
17
+ stdout: string[];
18
+ stderr: string[];
19
+ }
20
+
21
+ function makeIO(): CapturedIO {
22
+ return { stdout: [], stderr: [] };
23
+ }
24
+
25
+ function ctxFor(io: CapturedIO) {
26
+ return {
27
+ stdoutWrite: (line: string): void => {
28
+ io.stdout.push(line);
29
+ },
30
+ stderrWrite: (line: string): void => {
31
+ io.stderr.push(line);
32
+ },
33
+ };
34
+ }
35
+
36
+ describe('cli-gltf integration (AC-18)', () => {
37
+ let tempDir: string;
38
+
39
+ beforeEach(async () => {
40
+ tempDir = await mkdtemp(join(tmpdir(), 'gltf-cli-int-'));
41
+ });
42
+
43
+ afterEach(async () => {
44
+ await rm(tempDir, { recursive: true, force: true });
45
+ });
46
+
47
+ it('produces texture sub-assets in .meta.json for BoxTextured.gltf (external-uri)', async () => {
48
+ const assetsDir = join(
49
+ dirname(fileURLToPath(import.meta.url)),
50
+ '../../../../../forgeax-engine-assets/khronos-gltf-samples',
51
+ );
52
+ const gltfDir = join(assetsDir, 'BoxTextured', 'glTF');
53
+ const sourcePath = join(gltfDir, 'BoxTextured.gltf');
54
+
55
+ // Verify the fixture exists.
56
+ let fileStats: { isFile(): boolean } | undefined;
57
+ try {
58
+ fileStats = await stat(sourcePath);
59
+ } catch {
60
+ // Fixture not available; skip the test.
61
+ return;
62
+ }
63
+ expect(fileStats.isFile()).toBe(true);
64
+
65
+ // Symlink the fixture into a temp dir so the sidecar is written there.
66
+ const dest = join(tempDir, 'BoxTextured.gltf');
67
+ await writeFile(dest, await readFile(sourcePath));
68
+ // Also copy sibling .bin and .png files needed for parseGltf.
69
+ for (const siblingName of ['BoxTextured.bin', 'CesiumLogoFlat.png']) {
70
+ const src = join(gltfDir, siblingName);
71
+ try {
72
+ await stat(src);
73
+ await writeFile(join(tempDir, siblingName), await readFile(src));
74
+ } catch {
75
+ // Skip if missing.
76
+ }
77
+ }
78
+
79
+ const io = makeIO();
80
+ const code = await runCliGltf(['import', dest], ctxFor(io));
81
+ expect(code).toBe(0);
82
+
83
+ const metaPath = `${dest}.meta.json`;
84
+ const metaRaw = await readFile(metaPath, 'utf-8');
85
+ const meta = JSON.parse(metaRaw) as { subAssets?: Array<{ kind: string }> };
86
+ expect(meta.subAssets).toBeDefined();
87
+
88
+ const kinds = meta.subAssets?.map((s) => s.kind) ?? [];
89
+ expect(kinds).toContain('texture');
90
+ });
91
+
92
+ it('produces texture sub-assets in .meta.json for BoxTextured.glb (bufferView)', async () => {
93
+ const assetsDir = join(
94
+ dirname(fileURLToPath(import.meta.url)),
95
+ '../../../../../forgeax-engine-assets/khronos-gltf-samples',
96
+ );
97
+ const sourcePath = join(assetsDir, 'BoxTextured', 'BoxTextured.glb');
98
+
99
+ let fileStatsForGlb: { isFile(): boolean } | undefined;
100
+ try {
101
+ fileStatsForGlb = await stat(sourcePath);
102
+ } catch {
103
+ return;
104
+ }
105
+ expect(fileStatsForGlb.isFile()).toBe(true);
106
+
107
+ const destGlb = join(tempDir, 'BoxTextured.glb');
108
+ await writeFile(destGlb, await readFile(sourcePath));
109
+
110
+ const io = makeIO();
111
+ const code = await runCliGltf(['import', destGlb], ctxFor(io));
112
+ expect(code).toBe(0);
113
+
114
+ const metaPath = `${destGlb}.meta.json`;
115
+ const metaRaw = await readFile(metaPath, 'utf-8');
116
+ const meta = JSON.parse(metaRaw) as { subAssets?: Array<{ kind: string }> };
117
+ expect(meta.subAssets).toBeDefined();
118
+
119
+ const kinds = meta.subAssets?.map((s) => s.kind) ?? [];
120
+ expect(kinds).toContain('texture');
121
+ });
122
+ });
@@ -0,0 +1,174 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { decodeColorAccessor } from '../accessor/decode-color.js';
4
+ import { dataUriBase64Payload, decodeBase64 } from '../data-uri.js';
5
+
6
+ type Fixture = {
7
+ readonly buffers: readonly { readonly uri: string; readonly byteLength: number }[];
8
+ readonly bufferViews: readonly {
9
+ readonly buffer: number;
10
+ readonly byteLength: number;
11
+ readonly byteOffset?: number;
12
+ readonly byteStride?: number;
13
+ }[];
14
+ readonly accessors: readonly {
15
+ readonly bufferView: number;
16
+ readonly byteOffset?: number;
17
+ readonly componentType: number;
18
+ readonly count: number;
19
+ readonly normalized?: boolean;
20
+ readonly type: string;
21
+ readonly sparse?: unknown;
22
+ }[];
23
+ };
24
+
25
+ type ColorError = {
26
+ readonly code: string;
27
+ readonly expected: string;
28
+ readonly hint: string;
29
+ readonly detail: {
30
+ readonly semantic: string;
31
+ readonly accessorIndex: number;
32
+ readonly reason: string;
33
+ };
34
+ };
35
+
36
+ function readFixture(name: string): Fixture {
37
+ const url = new URL(`./fixtures/color-0/${name}.gltf`, import.meta.url);
38
+ return JSON.parse(readFileSync(url, 'utf8')) as Fixture;
39
+ }
40
+
41
+ function inputFor(name: string, accessorIndex = 0) {
42
+ const fixture = readFixture(name);
43
+ const accessor = fixture.accessors[accessorIndex];
44
+ if (accessor === undefined) throw new Error(`missing fixture accessor ${accessorIndex}`);
45
+ const view = fixture.bufferViews[accessor.bufferView ?? -1];
46
+ if (view === undefined) throw new Error(`missing fixture bufferView ${accessor.bufferView}`);
47
+ const source = fixture.buffers[view.buffer];
48
+ if (source === undefined) throw new Error(`missing fixture buffer ${view.buffer}`);
49
+ const payload = dataUriBase64Payload(source.uri);
50
+ if (payload === undefined) throw new Error('fixture buffer must be a base64 data URI');
51
+ return {
52
+ accessorIndex,
53
+ accessor,
54
+ bufferView: view,
55
+ buffer: decodeBase64(payload),
56
+ semantic: 'COLOR_0' as const,
57
+ bufferIndex: view.buffer,
58
+ };
59
+ }
60
+
61
+ function expectColorError(result: unknown, code: string, reason: string, accessorIndex: number) {
62
+ expect(result).toMatchObject({ ok: false });
63
+ if (typeof result !== 'object' || result === null || !('ok' in result) || result.ok) {
64
+ throw new Error('expected a structured color error');
65
+ }
66
+ const error = (result as unknown as { readonly error: ColorError }).error;
67
+ expect(error.code).toBe(code);
68
+ expect(error.expected).toEqual(expect.any(String));
69
+ expect(error.hint).toEqual(expect.any(String));
70
+ expect(error.detail).toMatchObject({ semantic: 'COLOR_0', accessorIndex, reason });
71
+ }
72
+
73
+ describe('COLOR_0 accessor support matrix', () => {
74
+ function expectFloat32Values(actual: Float32Array, expected: readonly number[]) {
75
+ expect(actual).toHaveLength(expected.length);
76
+ expected.forEach((value, index) => {
77
+ expect(actual[index]).toBeCloseTo(value, 6);
78
+ });
79
+ }
80
+
81
+ it('decodes VEC3 FLOAT and supplies alpha=1, including endpoint and midpoint values', () => {
82
+ const result = decodeColorAccessor(inputFor('float-vec3', 1));
83
+ expect(result.ok).toBe(true);
84
+ if (!result.ok) return;
85
+ expect(Array.from(result.value)).toEqual([0, 0.5, 1, 1, 1, 0.25, 0, 1]);
86
+ });
87
+
88
+ it('decodes normalized UBYTE VEC4 to linear RGBA endpoints and midpoint', () => {
89
+ const result = decodeColorAccessor(inputFor('normalized-ubyte'));
90
+ expect(result.ok).toBe(true);
91
+ if (!result.ok) return;
92
+ expectFloat32Values(result.value, [0, 128 / 255, 1, 64 / 255, 1, 32 / 255, 128 / 255, 1]);
93
+ });
94
+
95
+ it('decodes normalized USHORT VEC4 without sRGB conversion', () => {
96
+ const result = decodeColorAccessor(inputFor('normalized-ushort'));
97
+ expect(result.ok).toBe(true);
98
+ if (!result.ok) return;
99
+ expectFloat32Values(result.value, [
100
+ 0,
101
+ 32768 / 65535,
102
+ 1,
103
+ 16384 / 65535,
104
+ 1,
105
+ 8192 / 65535,
106
+ 49152 / 65535,
107
+ 1,
108
+ ]);
109
+ });
110
+
111
+ it('walks each element of a valid interleaved byteStride', () => {
112
+ const result = decodeColorAccessor(inputFor('interleaved-vec3'));
113
+ expect(result.ok).toBe(true);
114
+ if (!result.ok) return;
115
+ expect(Array.from(result.value)).toEqual([0, 0.25, 1, 1, 1, 0.5, 0, 1]);
116
+ });
117
+
118
+ it.each([
119
+ ['component', { componentType: 5120 }],
120
+ ['type', { type: 'VEC2' }],
121
+ ['normalized', { normalized: false }],
122
+ ] as const)('classifies unsupported %s as unsupported, not absent', (reason, patch) => {
123
+ const base = inputFor('float-vec3', 1);
124
+ const result = decodeColorAccessor({ ...base, accessor: { ...base.accessor, ...patch } });
125
+ expectColorError(result, 'gltf-color-accessor-unsupported', reason, 1);
126
+ });
127
+
128
+ it.each([
129
+ ['count', { count: 0 }],
130
+ ['bounds', { byteOffset: 20 }],
131
+ ['range', { byteOffset: 0 }],
132
+ ] as const)('classifies malformed %s with accessor context', (reason, patch) => {
133
+ const base = inputFor('float-vec3', 1);
134
+ const accessor = { ...base.accessor, ...patch };
135
+ if (reason === 'range') {
136
+ const bytes = base.buffer.slice();
137
+ new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).setFloat32(24, 2, true);
138
+ const result = decodeColorAccessor({ ...base, accessor, buffer: bytes });
139
+ expectColorError(result, 'gltf-color-accessor-malformed', reason, 1);
140
+ return;
141
+ }
142
+ const result = decodeColorAccessor({ ...base, accessor });
143
+ expectColorError(result, 'gltf-color-accessor-malformed', reason, 1);
144
+ });
145
+
146
+ it('rejects non-finite FLOAT values as malformed rather than publishing partial data', () => {
147
+ const base = inputFor('float-vec3', 1);
148
+ const bytes = base.buffer.slice();
149
+ new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).setFloat32(24, Number.NaN, true);
150
+ const result = decodeColorAccessor({ ...base, buffer: bytes });
151
+ expectColorError(result, 'gltf-color-accessor-malformed', 'finite', 1);
152
+ });
153
+
154
+ it('rejects a bad buffer reference as malformed', () => {
155
+ const base = inputFor('float-vec3', 1);
156
+ const result = decodeColorAccessor({
157
+ ...base,
158
+ bufferView: { ...base.bufferView, buffer: 4 },
159
+ });
160
+ expectColorError(result, 'gltf-color-accessor-malformed', 'reference', 1);
161
+ });
162
+
163
+ it('reports sparse and morph COLOR_0 as deferred unsupported', () => {
164
+ const base = inputFor('float-vec3', 1);
165
+ const sparse = decodeColorAccessor({
166
+ ...base,
167
+ accessor: { ...base.accessor, sparse: { count: 1 } },
168
+ });
169
+ expectColorError(sparse, 'gltf-color-accessor-unsupported', 'sparse', 1);
170
+
171
+ const morph = decodeColorAccessor(base, { morph: true });
172
+ expectColorError(morph, 'gltf-color-accessor-unsupported', 'morph', 1);
173
+ });
174
+ });
@@ -0,0 +1,26 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { Base64DecodeError, dataUriBase64Payload, decodeBase64 } from '../data-uri.js';
4
+
5
+ const ownerSource = readFileSync(new URL('../data-uri.ts', import.meta.url), 'utf8');
6
+ const parseSource = readFileSync(new URL('../parse-gltf.ts', import.meta.url), 'utf8');
7
+ const extractSource = readFileSync(new URL('../extract-image-bytes.ts', import.meta.url), 'utf8');
8
+
9
+ describe('data URI helper surface', () => {
10
+ it('owns matching, payload preservation, and browser-safe decoding', () => {
11
+ const payload = dataUriBase64Payload('data:application/octet-stream;base64,AAE=');
12
+
13
+ expect(payload).toBe('AAE=');
14
+ expect(dataUriBase64Payload('https://example.com/mesh.bin')).toBeUndefined();
15
+ expect(dataUriBase64Payload('data:application/octet-stream;base64,')).toBe('');
16
+ expect([...decodeBase64(payload ?? '')]).toEqual([0, 1]);
17
+ expect(() => decodeBase64('%%%%')).toThrow(Base64DecodeError);
18
+ expect(ownerSource.match(/const DATA_URI_BASE64_RE =/g)).toHaveLength(1);
19
+ expect(parseSource).not.toMatch(/const DATA_URI_BASE64_RE =/);
20
+ expect(extractSource).not.toMatch(/const DATA_URI_BASE64_RE =/);
21
+ expect(parseSource).not.toMatch(/function decodeBase64/);
22
+ expect(extractSource).not.toMatch(/function decodeBase64/);
23
+ expect(parseSource).toMatch(/dataUriBase64Payload/);
24
+ expect(extractSource).toMatch(/dataUriBase64Payload/);
25
+ });
26
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { checkExtensions } from '../check-extensions.js';
3
+
4
+ describe('glTF required extension admission', () => {
5
+ it('keeps unknown required extensions fail-closed', () => {
6
+ const result = checkExtensions({
7
+ extensionsRequired: ['KHR_forgeax_unknown'],
8
+ });
9
+
10
+ expect(result.ok).toBe(false);
11
+ if (result.ok) return;
12
+ expect(result.error.code).toBe('gltf-extension-unsupported');
13
+ if (result.error.code !== 'gltf-extension-unsupported') return;
14
+ expect(result.error.detail).toEqual({
15
+ extension: 'KHR_forgeax_unknown',
16
+ source: 'extensionsRequired',
17
+ });
18
+ });
19
+ });
@@ -0,0 +1,13 @@
1
+ {
2
+ "asset": { "version": "2.0" },
3
+ "buffers": [{ "byteLength": 48, "uri": "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAD8AAIA/AACAPwAAgD4AAAAA" }],
4
+ "bufferViews": [
5
+ { "buffer": 0, "byteOffset": 0, "byteLength": 24 },
6
+ { "buffer": 0, "byteOffset": 24, "byteLength": 24 }
7
+ ],
8
+ "accessors": [
9
+ { "bufferView": 0, "componentType": 5126, "count": 2, "type": "VEC3" },
10
+ { "bufferView": 1, "componentType": 5126, "count": 2, "type": "VEC3" }
11
+ ],
12
+ "meshes": [{ "primitives": [{ "attributes": { "POSITION": 0, "COLOR_0": 1 } }] }]
13
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "asset": { "version": "2.0" },
3
+ "buffers": [{ "byteLength": 32, "uri": "data:application/octet-stream;base64,AAAAAAAAgD4AAIA/AAAAAAAAgD8AAAA/AAAAAAAAAAA=" }],
4
+ "bufferViews": [{ "buffer": 0, "byteLength": 32, "byteStride": 16 }],
5
+ "accessors": [{ "bufferView": 0, "componentType": 5126, "count": 2, "type": "VEC3" }]
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "asset": { "version": "2.0" },
3
+ "buffers": [{ "byteLength": 8, "uri": "data:application/octet-stream;base64,AID/QP8ggP8=" }],
4
+ "bufferViews": [{ "buffer": 0, "byteLength": 8 }],
5
+ "accessors": [{ "bufferView": 0, "componentType": 5121, "normalized": true, "count": 2, "type": "VEC4" }]
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "asset": { "version": "2.0" },
3
+ "buffers": [{ "byteLength": 16, "uri": "data:application/octet-stream;base64,AAAAgP//AED//wAgAMD//w==" }],
4
+ "bufferViews": [{ "buffer": 0, "byteLength": 16 }],
5
+ "accessors": [{ "bufferView": 0, "componentType": 5123, "normalized": true, "count": 2, "type": "VEC4" }]
6
+ }
@@ -0,0 +1,152 @@
1
+ import { describe, expectTypeOf, it } from 'vitest';
2
+ import {
3
+ type GltfAccessorTypeMismatchDetail,
4
+ type GltfAnimationCubicsplineUnsupportedDetail,
5
+ type GltfAnimationTargetInvalidDetail,
6
+ type GltfBufferOutOfBoundsDetail,
7
+ type GltfColorAccessorMalformedDetail,
8
+ type GltfColorAccessorUnsupportedDetail,
9
+ type GltfError,
10
+ type GltfErrorCode,
11
+ type GltfErrorDetail,
12
+ type GltfExtensionUnsupportedDetail,
13
+ type GltfImageExtractFailedDetail,
14
+ type GltfImageMimeUnsupportedDetail,
15
+ type GltfInstancingCountMismatchDetail,
16
+ type GltfMalformedHeaderDetail,
17
+ type GltfMeshBridgeInvalidDetail,
18
+ type GltfMeshoptDecodeFailedDetail,
19
+ type GltfMeshoptDecoderRequiredDetail,
20
+ type GltfMetaMissingDetail,
21
+ type GltfMorphInvalidDetail,
22
+ type GltfMorphUnsupportedDetail,
23
+ type GltfSkinAttrAsymmetricDetail,
24
+ type GltfSkinJointCountExceededDetail,
25
+ type GltfSkinJointNameMissingDetail,
26
+ type GltfTextureLoadFailedDetail,
27
+ type GltfVersionUnsupportedDetail,
28
+ gltfErr,
29
+ } from '../errors.js';
30
+
31
+ type ExpectedDetails = {
32
+ readonly 'gltf-malformed-header': GltfMalformedHeaderDetail;
33
+ readonly 'gltf-version-unsupported': GltfVersionUnsupportedDetail;
34
+ readonly 'gltf-buffer-out-of-bounds': GltfBufferOutOfBoundsDetail;
35
+ readonly 'gltf-extension-unsupported': GltfExtensionUnsupportedDetail;
36
+ readonly 'gltf-accessor-type-mismatch': GltfAccessorTypeMismatchDetail;
37
+ readonly 'gltf-texture-load-failed': GltfTextureLoadFailedDetail;
38
+ readonly 'gltf-meta-missing': GltfMetaMissingDetail;
39
+ readonly 'gltf-instancing-count-mismatch': GltfInstancingCountMismatchDetail;
40
+ readonly 'gltf-image-mime-unsupported': GltfImageMimeUnsupportedDetail;
41
+ readonly 'gltf-skin-joint-count-exceeded': GltfSkinJointCountExceededDetail;
42
+ readonly 'gltf-animation-cubicspline-unsupported': GltfAnimationCubicsplineUnsupportedDetail;
43
+ readonly 'gltf-morph-unsupported': GltfMorphUnsupportedDetail;
44
+ readonly 'gltf-skin-joint-name-missing': GltfSkinJointNameMissingDetail;
45
+ readonly 'gltf-image-extract-failed': GltfImageExtractFailedDetail;
46
+ readonly 'gltf-skin-attr-asymmetric': GltfSkinAttrAsymmetricDetail;
47
+ readonly 'gltf-animation-target-invalid': GltfAnimationTargetInvalidDetail;
48
+ readonly 'gltf-meshopt-decoder-required': GltfMeshoptDecoderRequiredDetail;
49
+ readonly 'gltf-meshopt-decode-failed': GltfMeshoptDecodeFailedDetail;
50
+ readonly 'gltf-morph-invalid': GltfMorphInvalidDetail;
51
+ readonly 'gltf-color-accessor-unsupported': GltfColorAccessorUnsupportedDetail;
52
+ readonly 'gltf-color-accessor-malformed': GltfColorAccessorMalformedDetail;
53
+ readonly 'gltf-mesh-bridge-invalid': GltfMeshBridgeInvalidDetail;
54
+ };
55
+
56
+ type ExpectedCodes = keyof ExpectedDetails;
57
+ type ExpectedError = {
58
+ readonly [C in ExpectedCodes]: {
59
+ readonly code: C;
60
+ readonly expected: string;
61
+ readonly hint: string;
62
+ readonly detail: ExpectedDetails[C];
63
+ };
64
+ }[ExpectedCodes];
65
+
66
+ function exhaustive(error: GltfError): string {
67
+ switch (error.code) {
68
+ case 'gltf-malformed-header':
69
+ return `${error.detail.filePath}:${error.detail.byteOffset}`;
70
+ case 'gltf-version-unsupported':
71
+ return error.detail.actualVersion;
72
+ case 'gltf-buffer-out-of-bounds':
73
+ return `${error.detail.accessor}:${error.detail.bufferIndex}`;
74
+ case 'gltf-extension-unsupported':
75
+ return error.detail.extension;
76
+ case 'gltf-accessor-type-mismatch':
77
+ return error.detail.reason;
78
+ case 'gltf-texture-load-failed':
79
+ return error.detail.uri;
80
+ case 'gltf-meta-missing':
81
+ return error.detail.expectedMetaPath;
82
+ case 'gltf-instancing-count-mismatch':
83
+ return error.detail.accessor;
84
+ case 'gltf-image-mime-unsupported':
85
+ return error.detail.mimeType;
86
+ case 'gltf-skin-joint-count-exceeded':
87
+ return `${error.detail.skinIndex}:${error.detail.maxJoints}`;
88
+ case 'gltf-animation-cubicspline-unsupported':
89
+ return `${error.detail.animationIndex}:${error.detail.samplerIndex}`;
90
+ case 'gltf-morph-unsupported':
91
+ return `${error.detail.animationIndex}:${error.detail.channelIndex}`;
92
+ case 'gltf-skin-joint-name-missing':
93
+ return error.detail.reason;
94
+ case 'gltf-image-extract-failed':
95
+ return error.detail.source;
96
+ case 'gltf-skin-attr-asymmetric':
97
+ return `${error.detail.meshIndex}:${error.detail.primitiveIndex}`;
98
+ case 'gltf-animation-target-invalid':
99
+ return error.detail.reason;
100
+ case 'gltf-meshopt-decoder-required':
101
+ return `${error.detail.bufferView}:${error.detail.actual}`;
102
+ case 'gltf-meshopt-decode-failed':
103
+ return `${error.detail.bufferView}:${error.detail.mode}`;
104
+ case 'gltf-morph-invalid':
105
+ return `${error.detail.meshIndex}:${error.detail.primitiveIndex}:${error.detail.reason}`;
106
+ case 'gltf-color-accessor-unsupported':
107
+ return `${error.detail.semantic}:${error.detail.accessorIndex}:${error.detail.reason}`;
108
+ case 'gltf-color-accessor-malformed':
109
+ return `${error.detail.semantic}:${error.detail.accessorIndex}:${error.detail.reason}`;
110
+ case 'gltf-mesh-bridge-invalid':
111
+ return error.detail.reason;
112
+ }
113
+ return error;
114
+ }
115
+
116
+ describe('GltfError derived public views', () => {
117
+ it('keeps the exact twenty-two-code vocabulary and correlated union', () => {
118
+ expectTypeOf<GltfErrorCode>().toEqualTypeOf<ExpectedCodes>();
119
+ expectTypeOf<GltfError>().toEqualTypeOf<ExpectedError>();
120
+ expectTypeOf<GltfErrorCode>().toEqualTypeOf<GltfError['code']>();
121
+ expectTypeOf<GltfErrorDetail>().toEqualTypeOf<GltfError['detail']>();
122
+ });
123
+
124
+ it('rejects unknown codes and mismatched code/detail pairs', () => {
125
+ // @ts-expect-error unknown literals are outside the closed code view
126
+ const invalidCode: GltfErrorCode = 'gltf-not-a-real-error';
127
+ void invalidCode;
128
+
129
+ // @ts-expect-error the detail must match the selected code
130
+ gltfErr('gltf-malformed-header', { uri: 'textures/wrong.png' });
131
+ });
132
+
133
+ it('preserves gltfErr inference for a selected correlated variant', () => {
134
+ const error = gltfErr('gltf-version-unsupported', {
135
+ filePath: 'model.gltf',
136
+ actualVersion: '1.0',
137
+ });
138
+ expectTypeOf(error).toEqualTypeOf<
139
+ Extract<GltfError, { readonly code: 'gltf-version-unsupported' }>
140
+ >();
141
+ expectTypeOf(error.detail).toEqualTypeOf<GltfVersionUnsupportedDetail>();
142
+ });
143
+
144
+ it('supports exhaustive narrowing across every derived variant', () => {
145
+ expectTypeOf(exhaustive).toBeCallableWith({
146
+ code: 'gltf-meta-missing',
147
+ expected: 'sidecar',
148
+ hint: 'import it',
149
+ detail: { filePath: 'model.gltf', expectedMetaPath: 'model.gltf.meta.json' },
150
+ });
151
+ });
152
+ });
@@ -0,0 +1,147 @@
1
+ // gltf-error-migration.test.ts - M1 grep gate for GltfErrorCode migration.
2
+ //
3
+ // TDD red-green: written BEFORE migration (t6/t7/t8). Three grep assertions
4
+ // guard the DIP boundary: types must have zero gltf- literals, types must
5
+ // have zero GltfError* exports, and gltf/errors.ts must have >=1 GltfError*
6
+ // export. These are RED on first run (types still has GltfError*); they go
7
+ // GREEN after t6/t7/t8 complete the migration.
8
+ //
9
+ // Anchors:
10
+ // - requirements AC-28 (types grep GltfError = 0 hits)
11
+ // - requirements AC-27 (DIP three grep gates)
12
+ // - plan-strategy section 5.3 (DIP grep gate AC-27/AC-28)
13
+ // - charter P3 (explicit failure: grep gate fails-fast on drift)
14
+
15
+ import { execFileSync } from 'node:child_process';
16
+ import { resolve } from 'node:path';
17
+ import { describe, expect, it } from 'vitest';
18
+
19
+ const REPO_ROOT = resolve(import.meta.dirname ?? '.', '..', '..', '..', '..');
20
+ const TYPES_INDEX = resolve(REPO_ROOT, 'packages', 'types', 'src', 'index.ts');
21
+ const GLTF_ERRORS = resolve(REPO_ROOT, 'packages', 'gltf', 'src', 'errors.ts');
22
+
23
+ function grepCount(pattern: string, file: string): number {
24
+ try {
25
+ const out = execFileSync('rg', ['--count', '--regexp', pattern, file], { encoding: 'utf-8' });
26
+ return Number.parseInt(out.trim(), 10);
27
+ } catch (cause: unknown) {
28
+ if ((cause as { status?: number }).status === 1) return 0;
29
+ if ((cause as NodeJS.ErrnoException).code === 'ENOENT') {
30
+ try {
31
+ const out = execFileSync('grep', ['-E', '-c', pattern, file], { encoding: 'utf-8' });
32
+ return Number.parseInt(out.trim(), 10);
33
+ } catch (fallbackCause: unknown) {
34
+ if ((fallbackCause as { status?: number }).status === 1) return 0;
35
+ throw fallbackCause;
36
+ }
37
+ }
38
+ throw cause;
39
+ }
40
+ }
41
+
42
+ function grepCountImportGltfErrorFromTypes(): number {
43
+ try {
44
+ // Search tracked source paths so coverage runs do not recursively walk
45
+ // generated or ignored trees on a busy CI runner. The migration contract
46
+ // is about repository source, which is exactly the tracked worktree set.
47
+ const out = execFileSync(
48
+ 'git',
49
+ [
50
+ 'grep',
51
+ '--no-color',
52
+ '-E',
53
+ '-l',
54
+ '-e',
55
+ 'import.*GltfError.*from.*@forgeax/engine-types',
56
+ '--',
57
+ 'packages',
58
+ 'apps',
59
+ ':(exclude)packages/gltf/src/__tests__/gltf-error-migration.test.ts',
60
+ ],
61
+ { encoding: 'utf-8' },
62
+ );
63
+ return out.trim().split('\n').filter(Boolean).length;
64
+ } catch (cause: unknown) {
65
+ if ((cause as { status?: number }).status === 1) return 0;
66
+ if ((cause as NodeJS.ErrnoException).code === 'ENOENT') {
67
+ try {
68
+ const out = execFileSync(
69
+ 'rg',
70
+ [
71
+ '--files-with-matches',
72
+ '--glob',
73
+ '*.ts',
74
+ '--glob',
75
+ '!**/node_modules/**',
76
+ '--glob',
77
+ '!**/dist/**',
78
+ '--glob',
79
+ '!**/gltf-error-migration.test.ts',
80
+ '--regexp',
81
+ 'import.*GltfError.*from.*@forgeax/engine-types',
82
+ resolve(REPO_ROOT, 'packages'),
83
+ resolve(REPO_ROOT, 'apps'),
84
+ ],
85
+ { encoding: 'utf-8' },
86
+ );
87
+ return out.trim().split('\n').filter(Boolean).length;
88
+ } catch (fallbackCause: unknown) {
89
+ if ((fallbackCause as { status?: number }).status === 1) return 0;
90
+ if ((fallbackCause as NodeJS.ErrnoException).code === 'ENOENT') {
91
+ try {
92
+ const out = execFileSync(
93
+ 'grep',
94
+ [
95
+ '-R',
96
+ '-l',
97
+ '-E',
98
+ '--include=*.ts',
99
+ '--exclude-dir=node_modules',
100
+ '--exclude-dir=dist',
101
+ 'import.*GltfError.*from.*@forgeax/engine-types',
102
+ resolve(REPO_ROOT, 'packages'),
103
+ resolve(REPO_ROOT, 'apps'),
104
+ ],
105
+ { encoding: 'utf-8' },
106
+ );
107
+ return out
108
+ .trim()
109
+ .split('\n')
110
+ .filter(
111
+ (file) =>
112
+ file &&
113
+ !file.endsWith('/packages/gltf/src/__tests__/gltf-error-migration.test.ts'),
114
+ ).length;
115
+ } catch (grepCause: unknown) {
116
+ if ((grepCause as { status?: number }).status === 1) return 0;
117
+ throw grepCause;
118
+ }
119
+ }
120
+ throw fallbackCause;
121
+ }
122
+ }
123
+ throw cause;
124
+ }
125
+ }
126
+
127
+ describe('M1 GltfErrorCode migration grep gate (AC-28)', () => {
128
+ it("(a) types/src/index.ts contains zero 'gltf-' string literals", () => {
129
+ const count = grepCount("'gltf-", TYPES_INDEX);
130
+ expect(count).toBe(0);
131
+ });
132
+
133
+ it('(b) types/src/index.ts contains zero export type GltfError* exports', () => {
134
+ const count = grepCount('export type GltfError', TYPES_INDEX);
135
+ expect(count).toBe(0);
136
+ });
137
+
138
+ it('(c) gltf/errors.ts contains >=1 export type GltfError* exports', () => {
139
+ const count = grepCount('export type GltfError', GLTF_ERRORS);
140
+ expect(count).toBeGreaterThanOrEqual(1);
141
+ });
142
+
143
+ it('(d) zero import GltfError from @forgeax/engine-types in all packages/apps', () => {
144
+ const count = grepCountImportGltfErrorFromTypes();
145
+ expect(count).toBe(0);
146
+ });
147
+ });