@forgeax/engine-rhi 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.
Files changed (83) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +234 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/async-form.test-d.d.ts +2 -0
  5. package/dist/__tests__/async-form.test-d.d.ts.map +1 -0
  6. package/dist/__tests__/binding-resource.test-d.d.ts +2 -0
  7. package/dist/__tests__/binding-resource.test-d.d.ts.map +1 -0
  8. package/dist/__tests__/buffer-mapping.test-d.d.ts +2 -0
  9. package/dist/__tests__/buffer-mapping.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/canvasContext.test-d.d.ts +2 -0
  11. package/dist/__tests__/canvasContext.test-d.d.ts.map +1 -0
  12. package/dist/__tests__/caps-three-way.unit.test.d.ts +2 -0
  13. package/dist/__tests__/caps-three-way.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/caps.test-d.d.ts +2 -0
  15. package/dist/__tests__/caps.test-d.d.ts.map +1 -0
  16. package/dist/__tests__/command-encoder-spec-alignment.test-d.d.ts +2 -0
  17. package/dist/__tests__/command-encoder-spec-alignment.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/createComputePipeline.test-d.d.ts +2 -0
  19. package/dist/__tests__/createComputePipeline.test-d.d.ts.map +1 -0
  20. package/dist/__tests__/createQuerySet.test-d.d.ts +2 -0
  21. package/dist/__tests__/createQuerySet.test-d.d.ts.map +1 -0
  22. package/dist/__tests__/createTextureView.test-d.d.ts +2 -0
  23. package/dist/__tests__/createTextureView.test-d.d.ts.map +1 -0
  24. package/dist/__tests__/descriptor-mirror.test-d.d.ts +2 -0
  25. package/dist/__tests__/descriptor-mirror.test-d.d.ts.map +1 -0
  26. package/dist/__tests__/draw-validation-errors.test.d.ts +2 -0
  27. package/dist/__tests__/draw-validation-errors.test.d.ts.map +1 -0
  28. package/dist/__tests__/errors-instancing.unit.test.d.ts +2 -0
  29. package/dist/__tests__/errors-instancing.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/errors.test-d.d.ts +2 -0
  31. package/dist/__tests__/errors.test-d.d.ts.map +1 -0
  32. package/dist/__tests__/limit-exceeded-detail.test-d.d.ts +2 -0
  33. package/dist/__tests__/limit-exceeded-detail.test-d.d.ts.map +1 -0
  34. package/dist/__tests__/mapped-buffer.test-d.d.ts +2 -0
  35. package/dist/__tests__/mapped-buffer.test-d.d.ts.map +1 -0
  36. package/dist/__tests__/opaque-handle.test-d.d.ts +2 -0
  37. package/dist/__tests__/opaque-handle.test-d.d.ts.map +1 -0
  38. package/dist/__tests__/owner-out-of-range-role.test.d.ts +2 -0
  39. package/dist/__tests__/owner-out-of-range-role.test.d.ts.map +1 -0
  40. package/dist/__tests__/queue-writes.test-d.d.ts +2 -0
  41. package/dist/__tests__/queue-writes.test-d.d.ts.map +1 -0
  42. package/dist/__tests__/queue-writes.unit.test.d.ts +2 -0
  43. package/dist/__tests__/queue-writes.unit.test.d.ts.map +1 -0
  44. package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts +2 -0
  45. package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts.map +1 -0
  46. package/dist/__tests__/requestAdapter.test-d.d.ts +2 -0
  47. package/dist/__tests__/requestAdapter.test-d.d.ts.map +1 -0
  48. package/dist/errors.d.ts +315 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.mjs +56 -0
  51. package/dist/errors.mjs.map +1 -0
  52. package/dist/index.d.ts +1749 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.mjs +256 -0
  55. package/dist/index.mjs.map +1 -0
  56. package/dist/queue-writes.d.ts +20 -0
  57. package/dist/queue-writes.d.ts.map +1 -0
  58. package/package.json +61 -0
  59. package/src/__tests__/async-form.test-d.ts +40 -0
  60. package/src/__tests__/binding-resource.test-d.ts +66 -0
  61. package/src/__tests__/buffer-mapping.test-d.ts +84 -0
  62. package/src/__tests__/canvasContext.test-d.ts +106 -0
  63. package/src/__tests__/caps-three-way.unit.test.ts +144 -0
  64. package/src/__tests__/caps.test-d.ts +83 -0
  65. package/src/__tests__/command-encoder-spec-alignment.test-d.ts +117 -0
  66. package/src/__tests__/createComputePipeline.test-d.ts +149 -0
  67. package/src/__tests__/createQuerySet.test-d.ts +81 -0
  68. package/src/__tests__/createTextureView.test-d.ts +127 -0
  69. package/src/__tests__/descriptor-mirror.test-d.ts +267 -0
  70. package/src/__tests__/draw-validation-errors.test.ts +155 -0
  71. package/src/__tests__/errors-instancing.unit.test.ts +167 -0
  72. package/src/__tests__/errors.test-d.ts +199 -0
  73. package/src/__tests__/limit-exceeded-detail.test-d.ts +33 -0
  74. package/src/__tests__/mapped-buffer.test-d.ts +65 -0
  75. package/src/__tests__/opaque-handle.test-d.ts +121 -0
  76. package/src/__tests__/owner-out-of-range-role.test.ts +84 -0
  77. package/src/__tests__/queue-writes.test-d.ts +107 -0
  78. package/src/__tests__/queue-writes.unit.test.ts +30 -0
  79. package/src/__tests__/render-pass-encoder-spec-alignment.test-d.ts +202 -0
  80. package/src/__tests__/requestAdapter.test-d.ts +123 -0
  81. package/src/errors.ts +504 -0
  82. package/src/index.ts +2398 -0
  83. package/src/queue-writes.ts +62 -0
@@ -0,0 +1,167 @@
1
+ // feat-20260622-chunk-gpu-instancing-sprite-tilemap M2 / w8 — RhiErrorCode
2
+ // 21st member ('instancing-exceeds-uniform-cap') + .detail shape unit tests.
3
+ //
4
+ // Plan-strategy 2 D-2: closed RhiErrorCode add-only minor evolution; AC-05 (a)
5
+ // surface — RhiInstancingExceedsUniformCapDetail tagged union member with
6
+ // fields { requested: number, limit: 128, scope: 'sprite' | 'tilemap-chunk' }.
7
+ //
8
+ // AI-user contract (charter proposition 4 explicit failure + plan-strategy
9
+ // 8.3 actionable hint): consumers narrow .detail through property access
10
+ // (err.detail.requested / err.detail.limit / err.detail.scope) instead of
11
+ // parsing err.message string. The 128-instance cap originates from the
12
+ // uniform-fallback path's WebGL2 minimum 16384B UBO size — research N-1
13
+ // observed value (128 * 64B = 8192B leaves headroom for the per-frame
14
+ // material UBO slice). research F-4 + plan-decisions D-3 lock the limit.
15
+ //
16
+ // The runtime-shape assertions (sections 1 + 2) double as compile-time
17
+ // guarantees: if the union member or detail shape regress, tsc + vitest
18
+ // both flip red. Section 3 protects pre-existing 'limit-exceeded' detail
19
+ // from incidental drift (LimitExceededDetail must keep
20
+ // { maxStorageBufferBindingSize, requestedBytes } per
21
+ // feat-20260513-instanced-mesh M5 reshape).
22
+
23
+ import { describe, expect, expectTypeOf, it } from 'vitest';
24
+ import {
25
+ type LimitExceededDetail,
26
+ RhiError,
27
+ type RhiErrorCode,
28
+ type RhiErrorDetail,
29
+ type RhiInstancingExceedsUniformCapDetail,
30
+ } from '../errors';
31
+
32
+ describe('M2 / w8 — RhiErrorCode 21st member instancing-exceeds-uniform-cap', () => {
33
+ it('contains instancing-exceeds-uniform-cap in the closed RhiErrorCode union', () => {
34
+ expectTypeOf<'instancing-exceeds-uniform-cap'>().toMatchTypeOf<RhiErrorCode>();
35
+ });
36
+
37
+ it('exhaustive switch over all 21 members compiles without default fallback', () => {
38
+ function describeCode(code: RhiErrorCode): string {
39
+ switch (code) {
40
+ case 'adapter-unavailable':
41
+ return 'adapter';
42
+ case 'feature-not-enabled':
43
+ return 'feature';
44
+ case 'limit-exceeded':
45
+ return 'limit';
46
+ case 'shader-compile-failed':
47
+ return 'shader';
48
+ case 'rhi-not-available':
49
+ return 'reserved';
50
+ case 'webgpu-runtime-error':
51
+ return 'webgpu-runtime';
52
+ case 'command-encoder-finished':
53
+ return 'encoder-finished';
54
+ case 'render-pass-not-ended':
55
+ return 'pass-not-ended';
56
+ case 'queue-submit-failed':
57
+ return 'queue-submit';
58
+ case 'queue-write-buffer-out-of-bounds':
59
+ return 'queue-bounds';
60
+ case 'render-system-no-camera':
61
+ return 'no-camera';
62
+ case 'render-system-multi-camera':
63
+ return 'multi-camera';
64
+ case 'render-system-multi-light':
65
+ return 'multi-light';
66
+ case 'asset-not-registered':
67
+ return 'asset-miss';
68
+ case 'device-lost':
69
+ return 'device-lost';
70
+ case 'oom':
71
+ return 'oom';
72
+ case 'internal-error':
73
+ return 'internal-error';
74
+ case 'hierarchy-broken':
75
+ return 'hierarchy-broken';
76
+ case 'destroy-after-destroy':
77
+ return 'destroy-after-destroy';
78
+ case 'rhi-descriptor-invalid':
79
+ return 'descriptor-invalid';
80
+ case 'instancing-exceeds-uniform-cap':
81
+ return 'instancing-cap';
82
+ case 'render-system-empty-worlds':
83
+ return 'empty-worlds';
84
+ case 'render-system-owner-out-of-range':
85
+ return 'owner-oob';
86
+ }
87
+ // No default — tsc strict guards: union drift here triggers compile-time red.
88
+ }
89
+ expect(describeCode('instancing-exceeds-uniform-cap')).toBe('instancing-cap');
90
+ });
91
+ });
92
+
93
+ describe('M2 / w8 — RhiInstancingExceedsUniformCapDetail shape', () => {
94
+ it('detail equals { requested: number, limit: 128, scope: sprite|tilemap-chunk }', () => {
95
+ expectTypeOf<RhiInstancingExceedsUniformCapDetail>().toEqualTypeOf<{
96
+ readonly requested: number;
97
+ readonly limit: 128;
98
+ readonly scope: 'sprite' | 'tilemap-chunk';
99
+ }>();
100
+ });
101
+
102
+ it('detail is a member of the RhiErrorDetail discriminated union', () => {
103
+ const detail: RhiInstancingExceedsUniformCapDetail = {
104
+ requested: 200,
105
+ limit: 128,
106
+ scope: 'sprite',
107
+ };
108
+ const widened: RhiErrorDetail = detail;
109
+ expectTypeOf(widened).toMatchTypeOf<RhiErrorDetail>();
110
+ });
111
+
112
+ it('limit field is the literal 128, not a generic number', () => {
113
+ // tsc will reject `limit: 256` etc. — locked to the literal 128.
114
+ const detail: RhiInstancingExceedsUniformCapDetail = {
115
+ requested: 1024,
116
+ limit: 128,
117
+ scope: 'tilemap-chunk',
118
+ };
119
+ expect(detail.limit).toBe(128);
120
+ expectTypeOf<RhiInstancingExceedsUniformCapDetail['limit']>().toEqualTypeOf<128>();
121
+ });
122
+
123
+ it('scope field is the closed sprite | tilemap-chunk literal union', () => {
124
+ expectTypeOf<RhiInstancingExceedsUniformCapDetail['scope']>().toEqualTypeOf<
125
+ 'sprite' | 'tilemap-chunk'
126
+ >();
127
+ });
128
+
129
+ it('AI-user property-access consumption path: read .requested / .limit / .scope', () => {
130
+ const err = new RhiError({
131
+ code: 'instancing-exceeds-uniform-cap',
132
+ expected: 'bucket instance count <= 128 (uniform fallback cap)',
133
+ hint: 'reduce bucket size, switch to a WebGPU-capable backend, or accept per-cell fallback',
134
+ detail: { requested: 256, limit: 128, scope: 'tilemap-chunk' },
135
+ });
136
+ expect(err.code).toBe('instancing-exceeds-uniform-cap');
137
+ if (err.code === 'instancing-exceeds-uniform-cap') {
138
+ // After narrowing on .code, .detail is reachable via property access
139
+ // — no string parsing of err.message.
140
+ const d = err.detail as RhiInstancingExceedsUniformCapDetail;
141
+ expect(d.requested).toBe(256);
142
+ expect(d.limit).toBe(128);
143
+ expect(d.scope).toBe('tilemap-chunk');
144
+ }
145
+ expect(err.expected.length).toBeGreaterThan(0);
146
+ expect(err.hint.length).toBeGreaterThan(0);
147
+ });
148
+ });
149
+
150
+ describe('M2 / w8 — pre-existing limit-exceeded detail unchanged', () => {
151
+ it('LimitExceededDetail keeps { maxStorageBufferBindingSize, requestedBytes }', () => {
152
+ expectTypeOf<LimitExceededDetail>().toEqualTypeOf<{
153
+ readonly maxStorageBufferBindingSize: number;
154
+ readonly requestedBytes: number;
155
+ }>();
156
+ });
157
+
158
+ it("'limit-exceeded' member remains in the closed union (no rename / reorder)", () => {
159
+ expectTypeOf<'limit-exceeded'>().toMatchTypeOf<RhiErrorCode>();
160
+ });
161
+
162
+ it('limit-exceeded and instancing-exceeds-uniform-cap are distinct members', () => {
163
+ type Le = Extract<RhiErrorCode, 'limit-exceeded'>;
164
+ type Ic = Extract<RhiErrorCode, 'instancing-exceeds-uniform-cap'>;
165
+ expectTypeOf<Le>().not.toEqualTypeOf<Ic>();
166
+ });
167
+ });
@@ -0,0 +1,199 @@
1
+ // MVP-1.7 (type-level) — closed RhiErrorCode union completeness + exhaustive switch
2
+ // without default fallback. Round 1 baseline: 6 members; extended to 10 members in
3
+ // feat-20260508-rhi-surface-completion w7 (D-S3); extended to 14 members in
4
+ // feat-20260509-ecs-render-bridge-mvp w6 (D-S7); extended to 17 members in
5
+ // feat-20260511-rhi-spec-realign-aggressive w1 (red) -> w6 (green) per
6
+ // plan-strategy D-P4 + requirements AC-04. Three new members map W3C WebGPU
7
+ // 22.2 spec subtypes (device-lost / oom / internal-error) to the forgeax union.
8
+ // Extended to 20 members in feat-20260619-wasm-fault-isolation M3 w7:
9
+ // 'rhi-descriptor-invalid' for wgpu-wasm descriptor parse failures.
10
+ //
11
+ // 20 members:
12
+ // 1) 'adapter-unavailable' (Round 1 baseline)
13
+ // 2) 'feature-not-enabled' (Round 1 baseline)
14
+ // 3) 'limit-exceeded' (Round 1 baseline)
15
+ // 4) 'shader-compile-failed' (Round 1 baseline)
16
+ // 5) 'rhi-not-available' (Round 1 baseline; @reserved + placeholder)
17
+ // 6) 'webgpu-runtime-error' (verify-gpu-smoke-gate K-9 sixth member)
18
+ // 7) 'command-encoder-finished' (rhi-surface-completion D-S3)
19
+ // 8) 'render-pass-not-ended' (rhi-surface-completion D-S3)
20
+ // 9) 'queue-submit-failed' (rhi-surface-completion D-S3)
21
+ // 10) 'queue-write-buffer-out-of-bounds' (rhi-surface-completion D-S3)
22
+ // 11) 'render-system-no-camera' (ecs-render-bridge-mvp D-S7)
23
+ // 12) 'render-system-multi-camera' (ecs-render-bridge-mvp D-S7)
24
+ // 13) 'render-system-multi-light' (ecs-render-bridge-mvp D-S7)
25
+ // 14) 'asset-not-registered' (ecs-render-bridge-mvp D-S7)
26
+ // 15) 'device-lost' (rhi-spec-realign-aggressive D-P4 / R-02 §2.1)
27
+ // 16) 'oom' (rhi-spec-realign-aggressive D-P4 / R-02 §2.1)
28
+ // 17) 'internal-error' (rhi-spec-realign-aggressive D-P4 / R-02 §2.1)
29
+ // 18) 'hierarchy-broken' (asset-system-v1 w4 / D-P2 + requirements §9 row 8)
30
+ // 19) 'destroy-after-destroy' (feat-20260612-rhi-destroy-renderer-dispose-gpu-lifecycle M1 / D-6 + D-7)
31
+ // 20) 'rhi-descriptor-invalid' (feat-20260619-wasm-fault-isolation M3 w7 / D-1 + D-2 + D-8)
32
+ //
33
+ // charter mapping: proposition 4 (explicit failure via closed union) +
34
+ // proposition 3 (machine-readable union > prose) — switch (err.code) without
35
+ // default fallback; tsc strict mode rejects union drift at compile time.
36
+ //
37
+ // Related: requirements AC-04 (18-member union) + AC MVP-1.7 + AC-10 + AC-RSC-07;
38
+ // plan-strategy D-P4 (.expected / .hint templates) + D-P2 (hierarchy-broken) +
39
+ // S-6 (types/rhi single source) + 7.2 naming convention; research R-02 §2.1
40
+ // (W3C spec 22.2 subtypes).
41
+
42
+ import { describe, expectTypeOf, it } from 'vitest';
43
+ import type { RhiError, RhiErrorCode } from '../errors';
44
+
45
+ describe('MVP-1.7 — RhiErrorCode closed union 20 members', () => {
46
+ it('contains adapter-unavailable', () => {
47
+ expectTypeOf<'adapter-unavailable'>().toMatchTypeOf<RhiErrorCode>();
48
+ });
49
+
50
+ it('contains feature-not-enabled', () => {
51
+ expectTypeOf<'feature-not-enabled'>().toMatchTypeOf<RhiErrorCode>();
52
+ });
53
+
54
+ it('contains limit-exceeded', () => {
55
+ expectTypeOf<'limit-exceeded'>().toMatchTypeOf<RhiErrorCode>();
56
+ });
57
+
58
+ it('contains shader-compile-failed', () => {
59
+ expectTypeOf<'shader-compile-failed'>().toMatchTypeOf<RhiErrorCode>();
60
+ });
61
+
62
+ it('contains rhi-not-available (@reserved + placeholder methods)', () => {
63
+ expectTypeOf<'rhi-not-available'>().toMatchTypeOf<RhiErrorCode>();
64
+ });
65
+
66
+ it('contains webgpu-runtime-error (verify-gpu-smoke-gate K-9 sixth member)', () => {
67
+ expectTypeOf<'webgpu-runtime-error'>().toMatchTypeOf<RhiErrorCode>();
68
+ });
69
+
70
+ it('contains command-encoder-finished (D-S3 new member)', () => {
71
+ expectTypeOf<'command-encoder-finished'>().toMatchTypeOf<RhiErrorCode>();
72
+ });
73
+
74
+ it('contains render-pass-not-ended (D-S3 new member)', () => {
75
+ expectTypeOf<'render-pass-not-ended'>().toMatchTypeOf<RhiErrorCode>();
76
+ });
77
+
78
+ it('contains queue-submit-failed (D-S3 new member)', () => {
79
+ expectTypeOf<'queue-submit-failed'>().toMatchTypeOf<RhiErrorCode>();
80
+ });
81
+
82
+ it('contains queue-write-buffer-out-of-bounds (D-S3 new member)', () => {
83
+ expectTypeOf<'queue-write-buffer-out-of-bounds'>().toMatchTypeOf<RhiErrorCode>();
84
+ });
85
+
86
+ it('contains render-system-no-camera (D-S7 new member)', () => {
87
+ expectTypeOf<'render-system-no-camera'>().toMatchTypeOf<RhiErrorCode>();
88
+ });
89
+
90
+ it('contains render-system-multi-camera (D-S7 new member)', () => {
91
+ expectTypeOf<'render-system-multi-camera'>().toMatchTypeOf<RhiErrorCode>();
92
+ });
93
+
94
+ it('contains render-system-multi-light (D-S7 new member)', () => {
95
+ expectTypeOf<'render-system-multi-light'>().toMatchTypeOf<RhiErrorCode>();
96
+ });
97
+
98
+ it('contains asset-not-registered (D-S7 new member)', () => {
99
+ expectTypeOf<'asset-not-registered'>().toMatchTypeOf<RhiErrorCode>();
100
+ });
101
+
102
+ it('contains device-lost (D-P4 new member; spec 22.1 device lost)', () => {
103
+ expectTypeOf<'device-lost'>().toMatchTypeOf<RhiErrorCode>();
104
+ });
105
+
106
+ it('contains oom (D-P4 new member; spec 22.2 GPUOutOfMemoryError subtype)', () => {
107
+ expectTypeOf<'oom'>().toMatchTypeOf<RhiErrorCode>();
108
+ });
109
+
110
+ it('contains internal-error (D-P4 new member; spec 22.2 GPUInternalError subtype)', () => {
111
+ expectTypeOf<'internal-error'>().toMatchTypeOf<RhiErrorCode>();
112
+ });
113
+
114
+ it('contains hierarchy-broken (w4 asset-system-v1 D-P2 new member; propagateTransforms stale ChildOf ref)', () => {
115
+ expectTypeOf<'hierarchy-broken'>().toMatchTypeOf<RhiErrorCode>();
116
+ });
117
+
118
+ it('contains destroy-after-destroy (feat-20260612 M1 / D-6 + D-7; double destroy fail-fast)', () => {
119
+ expectTypeOf<'destroy-after-destroy'>().toMatchTypeOf<RhiErrorCode>();
120
+ });
121
+
122
+ it('contains rhi-descriptor-invalid (feat-20260619 M3 w7; wgpu-wasm descriptor parse failure)', () => {
123
+ expectTypeOf<'rhi-descriptor-invalid'>().toMatchTypeOf<RhiErrorCode>();
124
+ });
125
+
126
+ it('union remains closed: rejects non-member literal', () => {
127
+ // @ts-expect-error MVP-1.7: union is closed — 'not-a-real-code' is not a member.
128
+ const _bogus: RhiErrorCode = 'not-a-real-code';
129
+ void _bogus;
130
+ });
131
+
132
+ it('exhaustive switch with no default fallback for all members (charter proposition 4)', () => {
133
+ function describeCode(code: RhiErrorCode): string {
134
+ switch (code) {
135
+ case 'adapter-unavailable':
136
+ return 'adapter';
137
+ case 'feature-not-enabled':
138
+ return 'feature';
139
+ case 'limit-exceeded':
140
+ return 'limit';
141
+ case 'shader-compile-failed':
142
+ return 'shader';
143
+ case 'rhi-not-available':
144
+ return 'reserved';
145
+ case 'webgpu-runtime-error':
146
+ return 'webgpu-runtime';
147
+ case 'command-encoder-finished':
148
+ return 'encoder-finished';
149
+ case 'render-pass-not-ended':
150
+ return 'pass-not-ended';
151
+ case 'queue-submit-failed':
152
+ return 'queue-submit';
153
+ case 'queue-write-buffer-out-of-bounds':
154
+ return 'queue-bounds';
155
+ case 'render-system-no-camera':
156
+ return 'no-camera';
157
+ case 'render-system-multi-camera':
158
+ return 'multi-camera';
159
+ case 'render-system-multi-light':
160
+ return 'multi-light';
161
+ case 'asset-not-registered':
162
+ return 'asset-miss';
163
+ case 'device-lost':
164
+ return 'device-lost';
165
+ case 'oom':
166
+ return 'oom';
167
+ case 'internal-error':
168
+ return 'internal-error';
169
+ case 'hierarchy-broken':
170
+ return 'hierarchy-broken';
171
+ case 'destroy-after-destroy':
172
+ return 'destroy-after-destroy';
173
+ case 'rhi-descriptor-invalid':
174
+ return 'descriptor-invalid';
175
+ case 'instancing-exceeds-uniform-cap':
176
+ return 'instancing-cap';
177
+ case 'render-system-empty-worlds':
178
+ return 'empty-worlds';
179
+ case 'render-system-owner-out-of-range':
180
+ return 'owner-oob';
181
+ }
182
+ // No default — TS guards: union drift here triggers compile-time red.
183
+ }
184
+ expectTypeOf(describeCode).returns.toEqualTypeOf<string>();
185
+ });
186
+
187
+ it('RhiError.code literal type is exactly RhiErrorCode union', () => {
188
+ expectTypeOf<RhiError['code']>().toEqualTypeOf<RhiErrorCode>();
189
+ });
190
+
191
+ it('RhiError has readonly code / expected / hint fields', () => {
192
+ type ErrShape = {
193
+ readonly code: RhiErrorCode;
194
+ readonly expected: string;
195
+ readonly hint: string;
196
+ };
197
+ expectTypeOf<RhiError>().toMatchTypeOf<ErrShape>();
198
+ });
199
+ });
@@ -0,0 +1,33 @@
1
+ // feat-20260513-instanced-mesh M5 (T-M5-2): LimitExceededDetail field
2
+ // reshape — type-level assertions (vitest typecheck layer, *.test-d.ts).
3
+ //
4
+ // Companion to `limit-exceeded-detail.test.ts` (runtime). Vitest's
5
+ // typecheck pass (vitest 4.x default `typecheck.include === **/*.test-d.{ts,tsx}`)
6
+ // runs `expectTypeOf` here against the source LimitExceededDetail type;
7
+ // a legacy regression that re-introduces `renderableCount` / `limit`
8
+ // fields would flip these red.
9
+ //
10
+ // Plan rationale: requirements AC-15 (LimitExceededDetail field reshape
11
+ // evolution major) + plan-strategy D-3 (rename + replace, AGENTS.md
12
+ // Change stance — optimal > compatible).
13
+
14
+ import { describe, expectTypeOf, it } from 'vitest';
15
+ import type { LimitExceededDetail, RhiErrorDetail } from '../errors';
16
+
17
+ describe('feat-20260513-instanced-mesh T-M5-2 LimitExceededDetail (type-level)', () => {
18
+ it('equals the new { maxStorageBufferBindingSize, requestedBytes } shape', () => {
19
+ expectTypeOf<LimitExceededDetail>().toEqualTypeOf<{
20
+ readonly maxStorageBufferBindingSize: number;
21
+ readonly requestedBytes: number;
22
+ }>();
23
+ });
24
+
25
+ it('is a member of the RhiErrorDetail discriminated union', () => {
26
+ const detail: LimitExceededDetail = {
27
+ maxStorageBufferBindingSize: 256,
28
+ requestedBytes: 4096,
29
+ };
30
+ const widened: RhiErrorDetail = detail;
31
+ expectTypeOf(widened).toMatchTypeOf<RhiErrorDetail>();
32
+ });
33
+ });
@@ -0,0 +1,65 @@
1
+ // Type-level — MappedBuffer brand + method form (this: MappedBuffer guard).
2
+ //
3
+ // Introduced in feat-20260511-rhi-spec-realign-aggressive w4 (red) -> w10
4
+ // (green) per requirements AC-11 + plan-strategy §7.1 MappedBuffer + D-P2
5
+ // break-point #6 (brand + method form merged into one row).
6
+ //
7
+ // Shape:
8
+ // - `MappedBuffer = Buffer & { readonly __mapped: void }`
9
+ // - `buffer.mapAsync(mode, ...)` returns `Promise<Result<MappedBuffer,
10
+ // RhiError>>` — success branch carries the branded handle the caller
11
+ // subsequently consumes for getMappedRange / unmap.
12
+ // - `MappedBuffer.getMappedRange(...)` / `MappedBuffer.unmap()` are method
13
+ // forms on the brand (`this: MappedBuffer` implicit guard); the previous
14
+ // top-level `Buffer.getMappedRange` / `Buffer.unmap` are removed.
15
+ //
16
+ // charter mapping: proposition 4 (TS compile-time guard against calling
17
+ // getMappedRange on an un-mapped Buffer) + proposition 5 (consistent
18
+ // abstraction — the brand encodes state at the type level, no runtime
19
+ // re-validation needed at every call site).
20
+ //
21
+ // Related Rust analogue: `wgpu::BufferSlice<'_>` ownership/borrow tracking;
22
+ // forgeax uses a brand instead of lifetimes because TS lacks affine types.
23
+
24
+ import { describe, expectTypeOf, it } from 'vitest';
25
+ import type { Buffer, MappedBuffer, Result, RhiError } from '../index';
26
+
27
+ describe('MappedBuffer brand — type-level state machine', () => {
28
+ it('mapAsync resolves to Promise<Result<MappedBuffer, RhiError>>', () => {
29
+ type MapAsyncRet = ReturnType<Buffer['mapAsync']>;
30
+ expectTypeOf<MapAsyncRet>().toEqualTypeOf<Promise<Result<MappedBuffer, RhiError>>>();
31
+ });
32
+
33
+ it('MappedBuffer is a structural superset of Buffer (Buffer & { __mapped })', () => {
34
+ expectTypeOf<MappedBuffer>().toMatchTypeOf<Buffer>();
35
+ // Negative direction — plain Buffer is NOT a MappedBuffer (brand guard).
36
+ // @ts-expect-error TS2345 — Buffer is missing the __mapped brand.
37
+ const _bogus: MappedBuffer = undefined as unknown as Buffer;
38
+ void _bogus;
39
+ });
40
+
41
+ it('getMappedRange is a method on MappedBuffer (not on Buffer)', () => {
42
+ type GMR = MappedBuffer['getMappedRange'];
43
+ expectTypeOf<GMR>().toMatchTypeOf<
44
+ (offset?: number, size?: number) => Result<ArrayBuffer, RhiError>
45
+ >();
46
+ });
47
+
48
+ it('unmap is a method on MappedBuffer (not on Buffer)', () => {
49
+ type Unmap = MappedBuffer['unmap'];
50
+ expectTypeOf<Unmap>().toMatchTypeOf<() => void>();
51
+ });
52
+
53
+ it('AI user pattern: const r = await buf.mapAsync(...); if (r.ok) mappedBuffer.getMappedRange()', async () => {
54
+ const buffer = undefined as unknown as Buffer;
55
+ const r = await buffer.mapAsync(1);
56
+ if (r.ok) {
57
+ const mappedBuffer: MappedBuffer = r.value;
58
+ // method-form: mappedBuffer.getMappedRange and mappedBuffer.unmap exist
59
+ const range = mappedBuffer.getMappedRange();
60
+ expectTypeOf(range).toEqualTypeOf<Result<ArrayBuffer, RhiError>>();
61
+ const u = mappedBuffer.unmap();
62
+ expectTypeOf<typeof u>().toEqualTypeOf<void>();
63
+ }
64
+ });
65
+ });
@@ -0,0 +1,121 @@
1
+ // MVP-1.3 — 14 opaque handle types do not surface internal GPU fields
2
+ // (type-level red → green).
3
+ //
4
+ // Shape: 14 typed `Id<T>` brand-only types; user attempts to access
5
+ // `.gpuBuffer` / `.__brand` etc. → TypeScript compile-time red signal
6
+ // (auto-guarded by `@ts-expect-error`).
7
+ //
8
+ // Red expected state (at the M1 t7 commit): t10 has not declared these types
9
+ // → `tsc -b` reports a 'no exported member' red.
10
+ // Green expected state (after the M1 t10 commit): the 14 handle types are
11
+ // exported and brand-only → all 14 `ts-expect-error` directive blocks fire
12
+ // (attempts to access internal fields fail).
13
+ //
14
+ // Anchors: requirements §AC MVP-1.3 + §AI User Affordances / resource handles
15
+ // opaque; plan-strategy §4.3 key-test-point table row 2; §7
16
+ // proposition 4 / proposition 5; research §F-1 + §R5 (`__brand` is
17
+ // the d.ts nominal-typing workaround).
18
+
19
+ import { describe, it } from 'vitest';
20
+ import type {
21
+ BindGroup,
22
+ BindGroupLayout,
23
+ Buffer,
24
+ CommandBuffer,
25
+ CommandEncoder,
26
+ ComputePipeline,
27
+ Fence,
28
+ PipelineLayout,
29
+ QuerySet,
30
+ RenderPipeline,
31
+ Sampler,
32
+ ShaderModule,
33
+ Texture,
34
+ TextureView,
35
+ } from '../index';
36
+
37
+ describe('MVP-1.3 — 14 opaque handle types reject access to internal GPU fields', () => {
38
+ it('Buffer rejects access to the internal GPU object', () => {
39
+ const h = {} as Buffer;
40
+ // @ts-expect-error MVP-1.3: Buffer is opaque; gpuBuffer is not exposed.
41
+ h.gpuBuffer;
42
+ });
43
+
44
+ it('Texture rejects access to the internal GPU object', () => {
45
+ const h = {} as Texture;
46
+ // @ts-expect-error MVP-1.3: Texture is opaque; gpuTexture is not exposed.
47
+ h.gpuTexture;
48
+ });
49
+
50
+ it('TextureView rejects access to the internal GPU object', () => {
51
+ const h = {} as TextureView;
52
+ // @ts-expect-error MVP-1.3: TextureView is opaque; gpuTextureView is not exposed.
53
+ h.gpuTextureView;
54
+ });
55
+
56
+ it('Sampler rejects access to the internal GPU object', () => {
57
+ const h = {} as Sampler;
58
+ // @ts-expect-error MVP-1.3: Sampler is opaque; gpuSampler is not exposed.
59
+ h.gpuSampler;
60
+ });
61
+
62
+ it('BindGroup rejects access to the internal GPU object', () => {
63
+ const h = {} as BindGroup;
64
+ // @ts-expect-error MVP-1.3: BindGroup is opaque; gpuBindGroup is not exposed.
65
+ h.gpuBindGroup;
66
+ });
67
+
68
+ it('BindGroupLayout rejects access to the internal GPU object', () => {
69
+ const h = {} as BindGroupLayout;
70
+ // @ts-expect-error MVP-1.3: BindGroupLayout is opaque; gpuBindGroupLayout is not exposed.
71
+ h.gpuBindGroupLayout;
72
+ });
73
+
74
+ it('PipelineLayout rejects access to the internal GPU object', () => {
75
+ const h = {} as PipelineLayout;
76
+ // @ts-expect-error MVP-1.3: PipelineLayout is opaque; gpuPipelineLayout is not exposed.
77
+ h.gpuPipelineLayout;
78
+ });
79
+
80
+ it('RenderPipeline rejects access to the internal GPU object', () => {
81
+ const h = {} as RenderPipeline;
82
+ // @ts-expect-error MVP-1.3: RenderPipeline is opaque; gpuRenderPipeline is not exposed.
83
+ h.gpuRenderPipeline;
84
+ });
85
+
86
+ it('ComputePipeline rejects access to the internal GPU object', () => {
87
+ const h = {} as ComputePipeline;
88
+ // @ts-expect-error MVP-1.3: ComputePipeline is opaque; gpuComputePipeline is not exposed.
89
+ h.gpuComputePipeline;
90
+ });
91
+
92
+ it('ShaderModule rejects access to the internal GPU object', () => {
93
+ const h = {} as ShaderModule;
94
+ // @ts-expect-error MVP-1.3: ShaderModule is opaque; gpuShaderModule is not exposed.
95
+ h.gpuShaderModule;
96
+ });
97
+
98
+ it('QuerySet rejects access to the internal GPU object', () => {
99
+ const h = {} as QuerySet;
100
+ // @ts-expect-error MVP-1.3: QuerySet is opaque; gpuQuerySet is not exposed.
101
+ h.gpuQuerySet;
102
+ });
103
+
104
+ it('Fence rejects access to the internal sync object', () => {
105
+ const h = {} as Fence;
106
+ // @ts-expect-error MVP-1.3: Fence is opaque; internal sync handle is not exposed.
107
+ h.gpuFence;
108
+ });
109
+
110
+ it('CommandEncoder rejects access to the internal GPU object', () => {
111
+ const h = {} as CommandEncoder;
112
+ // @ts-expect-error MVP-1.3: CommandEncoder is opaque; gpuCommandEncoder is not exposed.
113
+ h.gpuCommandEncoder;
114
+ });
115
+
116
+ it('CommandBuffer rejects access to the internal GPU object', () => {
117
+ const h = {} as CommandBuffer;
118
+ // @ts-expect-error MVP-1.3: CommandBuffer is opaque; gpuCommandBuffer is not exposed.
119
+ h.gpuCommandBuffer;
120
+ });
121
+ });
@@ -0,0 +1,84 @@
1
+ // feat-20260709-editor-world-partition-editorworld-super-composite / M1 / w3
2
+ // (RED — impl lands in w6/w7). Contract test for the `role` field added to the
3
+ // `render-system-owner-out-of-range` structured error detail.
4
+ //
5
+ // Today the draw entry validates a SINGLE `owner` index and, on out-of-range,
6
+ // returns `RhiError { code: 'render-system-owner-out-of-range', detail: {
7
+ // owner, worldCount } }` (errors.ts validateDrawArgs). This feature splits the
8
+ // single owner into `cameraOwner` + `resourceOwner`, so a validation failure
9
+ // must say WHICH index was out of range. w7 extends the detail to
10
+ // `{ role: 'camera' | 'resource', owner, worldCount }`; w6 threads two indices
11
+ // through the draw entry and validates cameraOwner BEFORE resourceOwner.
12
+ //
13
+ // This test pins the future contract as the SSOT for w6/w7. The rhi package is
14
+ // World-free (it owns the RhiErrorCode closed union, architecture-principles §1
15
+ // SSOT) and cannot import the runtime Renderer — so, exactly like the existing
16
+ // draw-validation-errors.test.ts, the contract is exercised through the pure
17
+ // `validateDrawArgs` primitive rather than through `renderer.draw` itself. The
18
+ // future primitive shape is `validateDrawArgs(worldCount, { cameraOwner,
19
+ // resourceOwner })`. Until w6/w7 evolve the signature + detail, these calls are
20
+ // a type/shape mismatch and this file is RED (test-first).
21
+ //
22
+ // No new error code is added (D-3: 0 new error codes) — only `.detail` grows a
23
+ // `role` discriminator.
24
+ //
25
+ // Anchors:
26
+ // requirements AC-08 (out-of-range/missing returns a structured error aligned
27
+ // with the existing `render-system-owner-out-of-range` + `.detail`)
28
+ // plan-strategy §3.3 interface example (err.detail === { role: 'resource',
29
+ // owner: 3, worldCount: 2 }) + §2 D-3
30
+ // research F2.3 (existing .detail = { owner, worldCount }; add role field)
31
+
32
+ import { describe, expect, it } from 'vitest';
33
+ import { validateDrawArgs } from '../errors';
34
+
35
+ // The role-carrying detail w7 will attach to the existing code. Declared
36
+ // locally so this test states the future contract independent of the
37
+ // (not-yet-updated) RhiOwnerOutOfRangeDetail export.
38
+ interface OwnerOutOfRangeRoleDetail {
39
+ readonly role: 'camera' | 'resource';
40
+ readonly owner: number;
41
+ readonly worldCount: number;
42
+ }
43
+
44
+ describe('owner-out-of-range role detail (w3, AC-08)', () => {
45
+ it("cameraOwner out of range -> code render-system-owner-out-of-range, detail.role === 'camera'", () => {
46
+ // worldCount=2, cameraOwner=99 is out of range; resourceOwner=0 is valid.
47
+ const r = validateDrawArgs(2, { cameraOwner: 99, resourceOwner: 0 });
48
+ expect(r.ok).toBe(false);
49
+ if (!r.ok) {
50
+ expect(r.error.code).toBe('render-system-owner-out-of-range');
51
+ const d = r.error.detail as OwnerOutOfRangeRoleDetail;
52
+ expect(d.role).toBe('camera');
53
+ expect(d.owner).toBe(99);
54
+ expect(d.worldCount).toBe(2);
55
+ }
56
+ });
57
+
58
+ it("resourceOwner out of range -> code render-system-owner-out-of-range, detail.role === 'resource'", () => {
59
+ // worldCount=2, cameraOwner=0 is valid; resourceOwner=99 is out of range.
60
+ const r = validateDrawArgs(2, { cameraOwner: 0, resourceOwner: 99 });
61
+ expect(r.ok).toBe(false);
62
+ if (!r.ok) {
63
+ expect(r.error.code).toBe('render-system-owner-out-of-range');
64
+ const d = r.error.detail as OwnerOutOfRangeRoleDetail;
65
+ expect(d.role).toBe('resource');
66
+ expect(d.owner).toBe(99);
67
+ expect(d.worldCount).toBe(2);
68
+ }
69
+ });
70
+
71
+ it("both indices out of range -> reports the FIRST offender (camera), detail.role === 'camera'", () => {
72
+ // Both out of range. D-3 / w6 orders the check cameraOwner-before-resource,
73
+ // so the reported role is 'camera' (the first offender) with its own index.
74
+ const r = validateDrawArgs(2, { cameraOwner: 7, resourceOwner: 8 });
75
+ expect(r.ok).toBe(false);
76
+ if (!r.ok) {
77
+ expect(r.error.code).toBe('render-system-owner-out-of-range');
78
+ const d = r.error.detail as OwnerOutOfRangeRoleDetail;
79
+ expect(d.role).toBe('camera');
80
+ expect(d.owner).toBe(7);
81
+ expect(d.worldCount).toBe(2);
82
+ }
83
+ });
84
+ });