@kaminos/webgpu-inference-kit 0.1.0

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.
@@ -0,0 +1,84 @@
1
+ import { defineWebGpuRoute } from './route-boundary.js';
2
+ import {
3
+ createKernelProfileMetadata,
4
+ createRouteKernelProfileMetadata,
5
+ } from './kernel-profile.js';
6
+ import {
7
+ createRouteReceiptArtifacts,
8
+ createRouteReceiptInputArtifact,
9
+ createWebGpuRouteReceiptFromArtifacts,
10
+ } from './route-receipt-helper.js';
11
+
12
+ export const MOGE_DEPTH_NORMAL_ROUTE_ID = 'moge.depth-normal.webgpu-local.v0';
13
+ const MOGE_MODEL_ID = 'Ruicheng/moge-2-vitl-normal';
14
+ const DEFAULT_KERNEL_PROFILE = 'conv-transpose2d-stride2';
15
+ const REQUIRED_STAGES = ['backbone', 'decoder-heads', 'output-readback'];
16
+ const OUTPUT_ROLES = [
17
+ { key: 'depth', role: 'depth', required: true },
18
+ { key: 'normal', role: 'normal', required: true },
19
+ { key: 'pointMap', role: 'pointmap', required: false },
20
+ { key: 'mask', role: 'mask', required: false },
21
+ ];
22
+
23
+ export function createMogeDepthNormalRouteReceipt(input) {
24
+ if (!input || typeof input !== 'object') throw new Error('input must be an object');
25
+ if (!input.input?.artifactId || !input.input?.sha256) {
26
+ throw new Error('input image artifactId and sha256 are required');
27
+ }
28
+ if (!input.outputs?.depth) throw new Error('depth output is required');
29
+ if (!input.outputs?.normal) throw new Error('normal output is required');
30
+
31
+ return createWebGpuRouteReceiptFromArtifacts({
32
+ requestedRouteId: MOGE_DEPTH_NORMAL_ROUTE_ID,
33
+ effectiveRouteId: input.effectiveRouteId || MOGE_DEPTH_NORMAL_ROUTE_ID,
34
+ status: input.status || (input.fallbackReason ? 'fallback' : 'real'),
35
+ fallbackReason: input.fallbackReason || null,
36
+ backend: input.backend,
37
+ model: {
38
+ id: MOGE_MODEL_ID,
39
+ revision: input.model?.revision,
40
+ weightsHash: input.model?.weightsHash,
41
+ dtype: input.model?.dtype || 'fp16',
42
+ },
43
+ kernel: createKernelProfileMetadata(input.kernel, { requireProfile: true }),
44
+ inputs: [
45
+ createRouteReceiptInputArtifact('source-image', input.input),
46
+ ],
47
+ outputs: createRouteReceiptArtifacts({ artifacts: input.outputs, roles: OUTPUT_ROLES }),
48
+ profile: input.profile,
49
+ });
50
+ }
51
+
52
+ export function createMogeDepthNormalRouteDefinition(input = {}) {
53
+ const routeMetadata = createRouteKernelProfileMetadata(input, {
54
+ defaultProfile: DEFAULT_KERNEL_PROFILE,
55
+ requiredStages: REQUIRED_STAGES,
56
+ timingSource: 'queue-submit-wait',
57
+ });
58
+
59
+ return defineWebGpuRoute({
60
+ routeId: MOGE_DEPTH_NORMAL_ROUTE_ID,
61
+ backendKind: 'webgpu-local',
62
+ model: {
63
+ id: MOGE_MODEL_ID,
64
+ revision: input.model?.revision || 'local-vitl-normal',
65
+ dtype: input.model?.dtype || 'fp16',
66
+ },
67
+ kernel: routeMetadata.kernel,
68
+ inputs: [
69
+ { role: 'source-image', required: true, artifactRequired: true, hashRequired: true },
70
+ ],
71
+ outputs: [
72
+ { role: 'depth', required: true, artifactRequired: true, hashRequired: true, shape: [592, 592] },
73
+ { role: 'normal', required: true, artifactRequired: true, hashRequired: true, shape: [3, 592, 592] },
74
+ { role: 'pointmap', required: false, artifactRequired: true, hashRequired: true, shape: [3, 592, 592] },
75
+ { role: 'mask', required: false, artifactRequired: true, hashRequired: true, shape: [592, 592] },
76
+ ],
77
+ requiredFeatures: input.requiredFeatures || [],
78
+ requiredStages: routeMetadata.requiredStages,
79
+ timingSource: routeMetadata.timingSource,
80
+ worker: input.worker || {
81
+ exportName: 'runMogeDepthNormalRoute',
82
+ },
83
+ });
84
+ }
@@ -0,0 +1,347 @@
1
+ import { validateWebGpuBackendIdentity } from './gpu-environment.js';
2
+ import {
3
+ assertAuthoritativeRouteReceipt,
4
+ validateRouteReceipt,
5
+ } from './route-receipt.js';
6
+
7
+ export const WEBGPU_ROUTE_DEFINITION_SCHEMA = 'kaminos.webgpu-route-definition.v0';
8
+ export const WEBGPU_ROUTE_REQUEST_SCHEMA = 'kaminos.webgpu-route-request.v0';
9
+ export const WEBGPU_ROUTE_RESULT_SCHEMA = 'kaminos.webgpu-route-result.v0';
10
+
11
+ function clone(value) {
12
+ return value == null ? value : JSON.parse(JSON.stringify(value));
13
+ }
14
+
15
+ function isNonEmptyString(value) {
16
+ return typeof value === 'string' && value.trim().length > 0;
17
+ }
18
+
19
+ function requireString(errors, value, path) {
20
+ if (!isNonEmptyString(value)) errors.push(`${path} must be a non-empty string`);
21
+ }
22
+
23
+ function roleName(entry) {
24
+ return typeof entry === 'string' ? entry : entry?.role;
25
+ }
26
+
27
+ function normalizeRoles(entries = [], { defaultRequired = true } = {}) {
28
+ if (!Array.isArray(entries)) return [];
29
+
30
+ return entries.map(entry => {
31
+ if (typeof entry === 'string') {
32
+ return {
33
+ role: entry,
34
+ required: defaultRequired,
35
+ artifactRequired: true,
36
+ hashRequired: true,
37
+ };
38
+ }
39
+
40
+ return {
41
+ role: entry.role,
42
+ required: entry.required !== false,
43
+ artifactRequired: entry.artifactRequired !== false,
44
+ hashRequired: entry.hashRequired !== false,
45
+ shape: Array.isArray(entry.shape) ? [...entry.shape] : undefined,
46
+ };
47
+ });
48
+ }
49
+
50
+ function roleSet(roles) {
51
+ return new Set(roles.map(role => role.role));
52
+ }
53
+
54
+ function artifactArray(roles, artifacts = {}, { includeOptional = true, requireKnown = true } = {}) {
55
+ const byRole = Array.isArray(artifacts)
56
+ ? Object.fromEntries(artifacts.map(artifact => [artifact?.role, artifact]))
57
+ : artifacts;
58
+ const knownRoles = roleSet(roles);
59
+ const out = [];
60
+
61
+ for (const role of roles) {
62
+ const artifact = byRole?.[role.role];
63
+ if (!artifact) {
64
+ if (role.required || !includeOptional) {
65
+ out.push({ role: role.role });
66
+ }
67
+ continue;
68
+ }
69
+
70
+ out.push({
71
+ role: role.role,
72
+ artifactId: artifact.artifactId,
73
+ sha256: artifact.sha256,
74
+ shape: Array.isArray(artifact.shape) ? [...artifact.shape] : undefined,
75
+ status: artifact.status,
76
+ });
77
+ }
78
+
79
+ if (requireKnown && byRole && typeof byRole === 'object') {
80
+ for (const key of Object.keys(byRole)) {
81
+ if (!knownRoles.has(key)) {
82
+ out.push({ role: key, unknownRole: true, ...clone(byRole[key]) });
83
+ }
84
+ }
85
+ }
86
+
87
+ return out;
88
+ }
89
+
90
+ function requiredRoleNames(roles) {
91
+ return roles.filter(role => role.required !== false).map(role => role.role);
92
+ }
93
+
94
+ function optionalRoleNames(roles) {
95
+ return roles.filter(role => role.required === false).map(role => role.role);
96
+ }
97
+
98
+ function validateArtifacts(errors, artifacts, roles, path, { requireHash }) {
99
+ const knownRoles = roleSet(roles);
100
+ const artifactsByRole = new Map();
101
+
102
+ if (!Array.isArray(artifacts)) {
103
+ errors.push(`${path} must be an array`);
104
+ return;
105
+ }
106
+
107
+ artifacts.forEach((artifact, index) => {
108
+ const artifactPath = `${path}[${index}]`;
109
+ requireString(errors, artifact?.role, `${artifactPath}.role`);
110
+ if (isNonEmptyString(artifact?.role)) {
111
+ if (artifactsByRole.has(artifact.role)) errors.push(`${artifactPath}.role duplicates ${artifact.role}`);
112
+ artifactsByRole.set(artifact.role, artifact);
113
+ if (!knownRoles.has(artifact.role)) errors.push(`${artifactPath}.role is not defined by route`);
114
+ }
115
+ if (artifact?.unknownRole) errors.push(`${artifactPath}.role is not defined by route`);
116
+ requireString(errors, artifact?.artifactId, `${artifactPath}.artifactId`);
117
+ if (requireHash) requireString(errors, artifact?.sha256, `${artifactPath}.sha256`);
118
+ if (artifact?.shape != null && (!Array.isArray(artifact.shape) || !artifact.shape.every(Number.isInteger))) {
119
+ errors.push(`${artifactPath}.shape must be an integer array when present`);
120
+ }
121
+ });
122
+
123
+ for (const role of roles) {
124
+ if (role.required !== false && !artifactsByRole.has(role.role)) {
125
+ errors.push(`${path} missing required role ${role.role}`);
126
+ }
127
+ }
128
+ }
129
+
130
+ export function defineWebGpuRoute(input) {
131
+ if (!input || typeof input !== 'object') throw new Error('route input must be an object');
132
+
133
+ const inputRoles = normalizeRoles(input.inputs || input.inputRoles, { defaultRequired: true });
134
+ const outputRoles = normalizeRoles(input.outputs || input.outputRoles, { defaultRequired: true });
135
+
136
+ return {
137
+ schema: WEBGPU_ROUTE_DEFINITION_SCHEMA,
138
+ routeId: input.routeId,
139
+ backendKind: input.backendKind || 'webgpu-local',
140
+ model: clone(input.model),
141
+ kernel: clone(input.kernel),
142
+ inputRoles,
143
+ outputRoles,
144
+ requiredInputRoles: requiredRoleNames(inputRoles),
145
+ requiredOutputRoles: requiredRoleNames(outputRoles),
146
+ optionalOutputRoles: optionalRoleNames(outputRoles),
147
+ requiredFeatures: Array.isArray(input.requiredFeatures) ? [...input.requiredFeatures].map(String).sort() : [],
148
+ requiredStages: Array.isArray(input.requiredStages) ? [...input.requiredStages] : [],
149
+ timingSource: input.timingSource || 'queue-submit-wait',
150
+ worker: clone(input.worker || null),
151
+ };
152
+ }
153
+
154
+ export function validateRouteDefinition(route) {
155
+ const errors = [];
156
+
157
+ if (!route || typeof route !== 'object') {
158
+ return { ok: false, errors: ['route must be an object'] };
159
+ }
160
+
161
+ if (route.schema !== WEBGPU_ROUTE_DEFINITION_SCHEMA) errors.push(`schema must be ${WEBGPU_ROUTE_DEFINITION_SCHEMA}`);
162
+ requireString(errors, route.routeId, 'routeId');
163
+ if (route.backendKind !== 'webgpu-local') errors.push('backendKind must be webgpu-local');
164
+
165
+ if (!route.model || typeof route.model !== 'object') {
166
+ errors.push('model must be an object');
167
+ } else {
168
+ requireString(errors, route.model.id, 'model.id');
169
+ }
170
+
171
+ if (!route.kernel || typeof route.kernel !== 'object') {
172
+ errors.push('kernel must be an object');
173
+ } else {
174
+ requireString(errors, route.kernel.profile, 'kernel.profile');
175
+ }
176
+
177
+ if (!Array.isArray(route.inputRoles) || route.inputRoles.length === 0) {
178
+ errors.push('inputRoles must be a non-empty array');
179
+ } else {
180
+ route.inputRoles.forEach((role, index) => requireString(errors, role?.role, `inputRoles[${index}].role`));
181
+ }
182
+
183
+ if (!Array.isArray(route.outputRoles) || route.outputRoles.length === 0) {
184
+ errors.push('outputRoles must be a non-empty array');
185
+ } else {
186
+ route.outputRoles.forEach((role, index) => requireString(errors, role?.role, `outputRoles[${index}].role`));
187
+ }
188
+
189
+ if (!Array.isArray(route.requiredInputRoles) || route.requiredInputRoles.length === 0) {
190
+ errors.push('requiredInputRoles must be a non-empty array');
191
+ }
192
+ if (!Array.isArray(route.requiredOutputRoles) || route.requiredOutputRoles.length === 0) {
193
+ errors.push('requiredOutputRoles must be a non-empty array');
194
+ }
195
+ if (!isNonEmptyString(route.timingSource)) errors.push('timingSource must be a non-empty string');
196
+
197
+ return { ok: errors.length === 0, errors };
198
+ }
199
+
200
+ export function createWebGpuRouteRegistry(initialRoutes = []) {
201
+ const routes = new Map();
202
+
203
+ function register(route) {
204
+ const result = validateRouteDefinition(route);
205
+ if (!result.ok) throw new Error(`invalid route definition: ${result.errors.join('; ')}`);
206
+ if (routes.has(route.routeId)) throw new Error(`duplicate route: ${route.routeId}`);
207
+ routes.set(route.routeId, route);
208
+ return route;
209
+ }
210
+
211
+ for (const route of initialRoutes) register(route);
212
+
213
+ return {
214
+ register,
215
+ has(routeId) {
216
+ return routes.has(routeId);
217
+ },
218
+ get(routeId) {
219
+ const route = routes.get(routeId);
220
+ if (!route) throw new Error(`unknown route: ${routeId}`);
221
+ return route;
222
+ },
223
+ list() {
224
+ return Array.from(routes.values());
225
+ },
226
+ createRequest(routeId, input) {
227
+ return createRouteInvocationRequest(this.get(routeId), input);
228
+ },
229
+ };
230
+ }
231
+
232
+ export function createRouteInvocationRequest(route, input) {
233
+ const result = validateRouteDefinition(route);
234
+ if (!result.ok) throw new Error(`invalid route definition: ${result.errors.join('; ')}`);
235
+ if (!input || typeof input !== 'object') throw new Error('route request input must be an object');
236
+ if (!isNonEmptyString(input.requestId)) throw new Error('requestId must be a non-empty string');
237
+
238
+ return {
239
+ schema: WEBGPU_ROUTE_REQUEST_SCHEMA,
240
+ requestId: input.requestId,
241
+ routeId: route.routeId,
242
+ backendKind: route.backendKind,
243
+ inputs: artifactArray(route.inputRoles, input.inputs, { includeOptional: false }),
244
+ outputs: artifactArray(route.outputRoles, input.outputs, { includeOptional: true }).map(output => ({
245
+ role: output.role,
246
+ artifactId: output.artifactId,
247
+ shape: output.shape,
248
+ unknownRole: output.unknownRole,
249
+ })),
250
+ routeConfig: clone(input.routeConfig || {}),
251
+ model: clone(route.model),
252
+ kernel: clone(route.kernel),
253
+ createdAt: input.createdAt || new Date().toISOString(),
254
+ };
255
+ }
256
+
257
+ export function validateRouteInvocationRequest(request, route) {
258
+ const errors = [];
259
+ const routeResult = validateRouteDefinition(route);
260
+ if (!routeResult.ok) errors.push(...routeResult.errors.map(error => `route.${error}`));
261
+
262
+ if (!request || typeof request !== 'object') {
263
+ return { ok: false, errors: ['request must be an object'] };
264
+ }
265
+ if (request.schema !== WEBGPU_ROUTE_REQUEST_SCHEMA) errors.push(`schema must be ${WEBGPU_ROUTE_REQUEST_SCHEMA}`);
266
+ requireString(errors, request.requestId, 'requestId');
267
+ if (request.routeId !== route?.routeId) errors.push('routeId must match route definition');
268
+ if (request.backendKind !== 'webgpu-local') errors.push('backendKind must be webgpu-local');
269
+
270
+ if (routeResult.ok) {
271
+ validateArtifacts(errors, request.inputs, route.inputRoles, 'inputs', { requireHash: true });
272
+ validateArtifacts(errors, request.outputs, route.outputRoles, 'outputs', { requireHash: false });
273
+ }
274
+
275
+ return { ok: errors.length === 0, errors };
276
+ }
277
+
278
+ export function createRouteWorkerResult(route, input) {
279
+ if (!input || typeof input !== 'object') throw new Error('worker result input must be an object');
280
+ const request = input.request || {};
281
+ const receipt = input.receipt;
282
+
283
+ return {
284
+ schema: WEBGPU_ROUTE_RESULT_SCHEMA,
285
+ requestId: request.requestId,
286
+ routeId: route.routeId,
287
+ status: receipt?.status || 'unknown',
288
+ request: clone(request),
289
+ receipt: clone(receipt),
290
+ backend: clone(receipt?.backend),
291
+ outputs: clone(receipt?.outputs || []),
292
+ timings: clone(receipt?.timings),
293
+ createdAt: input.createdAt || new Date().toISOString(),
294
+ };
295
+ }
296
+
297
+ export function validateRouteWorkerResult(result, route) {
298
+ const errors = [];
299
+ const routeResult = validateRouteDefinition(route);
300
+ if (!routeResult.ok) errors.push(...routeResult.errors.map(error => `route.${error}`));
301
+
302
+ if (!result || typeof result !== 'object') {
303
+ return { ok: false, errors: ['result must be an object'] };
304
+ }
305
+
306
+ if (result.schema !== WEBGPU_ROUTE_RESULT_SCHEMA) errors.push(`schema must be ${WEBGPU_ROUTE_RESULT_SCHEMA}`);
307
+ requireString(errors, result.requestId, 'requestId');
308
+ if (result.routeId !== route?.routeId) errors.push('routeId must match route definition');
309
+
310
+ if (result.request != null) {
311
+ const requestResult = validateRouteInvocationRequest(result.request, route);
312
+ if (!requestResult.ok) errors.push(...requestResult.errors.map(error => `request.${error}`));
313
+ }
314
+
315
+ const receiptResult = validateRouteReceipt(result.receipt);
316
+ if (!receiptResult.ok) {
317
+ errors.push(...receiptResult.errors.map(error => `receipt.${error}`));
318
+ } else {
319
+ if (result.receipt.requestedRouteId !== route.routeId) {
320
+ errors.push('receipt.requestedRouteId must match route definition');
321
+ }
322
+ if (result.receipt.effectiveRouteId !== route.routeId) {
323
+ errors.push('receipt.effectiveRouteId must match route definition');
324
+ }
325
+ }
326
+
327
+ const backendResult = validateWebGpuBackendIdentity(result.backend);
328
+ if (!backendResult.ok) errors.push(...backendResult.errors.map(error => `backend.${error}`));
329
+
330
+ if (routeResult.ok && Array.isArray(result.outputs)) {
331
+ validateArtifacts(errors, result.outputs, route.outputRoles, 'outputs', { requireHash: true });
332
+ } else if (!Array.isArray(result.outputs)) {
333
+ errors.push('outputs must be an array');
334
+ }
335
+
336
+ return { ok: errors.length === 0, errors };
337
+ }
338
+
339
+ export function assertAuthoritativeRouteWorkerResult(result, route) {
340
+ const validation = validateRouteWorkerResult(result, route);
341
+ if (!validation.ok) {
342
+ throw new Error(`invalid route worker result: ${validation.errors.join('; ')}`);
343
+ }
344
+
345
+ assertAuthoritativeRouteReceipt(result.receipt);
346
+ return result;
347
+ }
@@ -0,0 +1,160 @@
1
+ import {
2
+ validateWebGpuBackendIdentity,
3
+ } from './gpu-environment.js';
4
+ import {
5
+ validateRouteReceipt,
6
+ } from './route-receipt.js';
7
+ import {
8
+ validateWebGpuRouteBackpressureProfile,
9
+ validateWebGpuRouteSchedulerProfile,
10
+ } from './scheduler-backpressure.js';
11
+
12
+ export const WEBGPU_ROUTE_EVIDENCE_CLASSIFICATION_SCHEMA = 'kaminos.webgpu-route-evidence-classification.v0';
13
+
14
+ function isNonEmptyString(value) {
15
+ return typeof value === 'string' && value.trim().length > 0;
16
+ }
17
+
18
+ function parseTime(value) {
19
+ const ms = Date.parse(value);
20
+ return Number.isFinite(ms) ? ms : null;
21
+ }
22
+
23
+ function staleReason(receipt, options) {
24
+ if (!Number.isFinite(options.maxAgeMs)) return null;
25
+ const createdMs = parseTime(receipt.createdAt);
26
+ const nowMs = parseTime(options.now || new Date().toISOString());
27
+ if (createdMs == null) return 'createdAt is missing or invalid for freshness check';
28
+ if (nowMs == null) return 'now is invalid for freshness check';
29
+ return nowMs - createdMs > options.maxAgeMs ? `receipt is older than ${options.maxAgeMs}ms` : null;
30
+ }
31
+
32
+ function classifyNonRealStatus(status) {
33
+ if (status === 'fallback') return 'fallback';
34
+ if (status === 'cached') return 'cache';
35
+ if (status === 'cache') return 'cache';
36
+ if (status === 'demo') return 'demo';
37
+ if (status === 'fixture') return 'demo';
38
+ if (status === 'partial') return 'partial';
39
+ return 'non-real';
40
+ }
41
+
42
+ function outputClassification(outputs = []) {
43
+ if (!Array.isArray(outputs)) return null;
44
+ if (outputs.some(output => output?.status === 'partial')) return 'partial';
45
+ if (outputs.some(output => output?.status === 'cached' || output?.status === 'cache')) return 'cache';
46
+ if (outputs.some(output => output?.status === 'demo' || output?.status === 'fixture')) return 'demo';
47
+ if (outputs.some(output => output?.status !== 'real')) return 'non-real-output';
48
+ return null;
49
+ }
50
+
51
+ function baseClassification(receipt, options = {}) {
52
+ const reasons = [];
53
+ const receiptResult = validateRouteReceipt(receipt);
54
+ if (!receiptResult.ok) reasons.push(...receiptResult.errors);
55
+
56
+ const backendResult = validateWebGpuBackendIdentity(receipt?.backend);
57
+ if (!backendResult.ok) reasons.push(...backendResult.errors.map(error => `backend.${error}`));
58
+
59
+ if (receipt?.runtime?.scheduler) {
60
+ const schedulerResult = validateWebGpuRouteSchedulerProfile(receipt.runtime.scheduler);
61
+ if (!schedulerResult.ok) reasons.push(...schedulerResult.errors.map(error => `scheduler.${error}`));
62
+ }
63
+
64
+ if (receipt?.runtime?.backpressure) {
65
+ const backpressureResult = validateWebGpuRouteBackpressureProfile(receipt.runtime.backpressure);
66
+ if (!backpressureResult.ok) reasons.push(...backpressureResult.errors.map(error => `backpressure.${error}`));
67
+ }
68
+
69
+ if (reasons.length > 0) {
70
+ return { classification: 'invalid', authoritative: false, reasons };
71
+ }
72
+
73
+ if (isNonEmptyString(options.expectedRouteId) && receipt.effectiveRouteId !== options.expectedRouteId) {
74
+ return {
75
+ classification: 'route-mismatch',
76
+ authoritative: false,
77
+ reasons: [`effectiveRouteId ${receipt.effectiveRouteId} does not match expected ${options.expectedRouteId}`],
78
+ };
79
+ }
80
+
81
+ const stale = staleReason(receipt, options);
82
+ if (stale) {
83
+ return { classification: 'stale', authoritative: false, reasons: [stale] };
84
+ }
85
+
86
+ if (receipt.status !== 'real') {
87
+ return {
88
+ classification: classifyNonRealStatus(receipt.status),
89
+ authoritative: false,
90
+ reasons: [`receipt status is ${receipt.status}`],
91
+ };
92
+ }
93
+
94
+ if (receipt.fallbackReason) {
95
+ return {
96
+ classification: 'fallback',
97
+ authoritative: false,
98
+ reasons: [`fallback reason present: ${receipt.fallbackReason}`],
99
+ };
100
+ }
101
+
102
+ const outputStatus = outputClassification(receipt.outputs);
103
+ if (outputStatus) {
104
+ return {
105
+ classification: outputStatus,
106
+ authoritative: false,
107
+ reasons: [`one or more outputs are ${outputStatus}`],
108
+ };
109
+ }
110
+
111
+ return {
112
+ classification: 'authoritative-live-webgpu',
113
+ authoritative: true,
114
+ reasons: [],
115
+ };
116
+ }
117
+
118
+ export function classifyWebGpuRouteReceiptEvidence(receipt, options = {}) {
119
+ const base = baseClassification(receipt, options);
120
+ const scheduler = receipt?.runtime?.scheduler || null;
121
+ const backpressure = receipt?.runtime?.backpressure || null;
122
+ return {
123
+ schema: WEBGPU_ROUTE_EVIDENCE_CLASSIFICATION_SCHEMA,
124
+ classification: base.classification,
125
+ authoritative: base.authoritative,
126
+ reasons: base.reasons,
127
+ routeId: receipt?.effectiveRouteId || receipt?.requestedRouteId || null,
128
+ requestedRouteId: receipt?.requestedRouteId || null,
129
+ effectiveRouteId: receipt?.effectiveRouteId || null,
130
+ backendKind: receipt?.backend?.kind || null,
131
+ adapterName: receipt?.backend?.adapterName || null,
132
+ timingSource: receipt?.timings?.source || receipt?.timings?.profile?.timingSource || null,
133
+ totalMs: Number.isFinite(receipt?.timings?.totalMs) ? receipt.timings.totalMs : null,
134
+ schedulerVerificationState: scheduler?.verificationState || null,
135
+ schedulerMode: scheduler?.effectiveScheduler?.mode || scheduler?.requestedScheduler?.mode || null,
136
+ schedulerUnsupportedFields: Array.isArray(scheduler?.effectiveScheduler?.unsupportedFields)
137
+ ? [...scheduler.effectiveScheduler.unsupportedFields]
138
+ : [],
139
+ requestedBudget: backpressure?.requestedBudget || null,
140
+ effectiveBudget: backpressure?.effectiveBudget || null,
141
+ longFrameCount: Number.isInteger(backpressure?.frameTail?.longFrameCount)
142
+ ? backpressure.frameTail.longFrameCount
143
+ : null,
144
+ maxFrameGapMs: Number.isFinite(backpressure?.frameTail?.maxFrameGapMs)
145
+ ? backpressure.frameTail.maxFrameGapMs
146
+ : null,
147
+ outputRoles: Array.isArray(receipt?.outputs) ? receipt.outputs.map(output => output.role) : [],
148
+ createdAt: receipt?.createdAt || null,
149
+ };
150
+ }
151
+
152
+ export function classifyWebGpuRouteWorkerResultEvidence(result, options = {}) {
153
+ const classification = classifyWebGpuRouteReceiptEvidence(result?.receipt, options);
154
+ return {
155
+ ...classification,
156
+ requestId: result?.requestId || null,
157
+ resultRouteId: result?.routeId || null,
158
+ resultStatus: result?.status || null,
159
+ };
160
+ }
@@ -0,0 +1,86 @@
1
+ import { validateWebGpuBackendIdentity } from './gpu-environment.js';
2
+ import { createWebGpuLocalRouteReceipt } from './route-receipt.js';
3
+ import { finishStagedSubmitProfile, validateStagedSubmitProfile } from './staged-profile.js';
4
+
5
+ export function validateRouteReceiptArtifact(value, name) {
6
+ if (!value || typeof value !== 'object') throw new Error(`${name} output must be an object`);
7
+ if (typeof value.artifactId !== 'string' || value.artifactId.length === 0) {
8
+ throw new Error(`${name} output must include artifactId`);
9
+ }
10
+ if (typeof value.sha256 !== 'string' || value.sha256.length === 0) {
11
+ throw new Error(`${name} output must include sha256`);
12
+ }
13
+ if (!Array.isArray(value.shape) || value.shape.length === 0) {
14
+ throw new Error(`${name} output must include shape`);
15
+ }
16
+ }
17
+
18
+ export function createRouteReceiptInputArtifact(role, artifact) {
19
+ return {
20
+ role,
21
+ artifactId: artifact.artifactId,
22
+ sha256: artifact.sha256,
23
+ shape: Array.isArray(artifact.shape) ? [...artifact.shape] : undefined,
24
+ };
25
+ }
26
+
27
+ export function createRouteReceiptArtifacts({ artifacts, roles }) {
28
+ if (!artifacts || typeof artifacts !== 'object') throw new Error('artifacts must be an object');
29
+ if (!Array.isArray(roles) || roles.length === 0) throw new Error('roles must be a non-empty array');
30
+
31
+ const outputs = [];
32
+ for (const role of roles) {
33
+ const key = role.key;
34
+ const artifact = artifacts[key];
35
+ if (!artifact) {
36
+ if (role.required !== false) throw new Error(`${key} output is required`);
37
+ continue;
38
+ }
39
+
40
+ validateRouteReceiptArtifact(artifact, key);
41
+ outputs.push({
42
+ role: role.role,
43
+ artifactId: artifact.artifactId,
44
+ sha256: artifact.sha256,
45
+ shape: [...artifact.shape],
46
+ status: artifact.status || 'real',
47
+ });
48
+ }
49
+ return outputs;
50
+ }
51
+
52
+ export function finishAndValidateRouteProfile(profile) {
53
+ const finished = finishStagedSubmitProfile(profile);
54
+ const result = validateStagedSubmitProfile(finished);
55
+ if (!result.ok) throw new Error(`invalid staged profile: ${result.errors.join('; ')}`);
56
+ return finished;
57
+ }
58
+
59
+ export function validateRouteReceiptBackendIdentity(backend) {
60
+ const result = validateWebGpuBackendIdentity(backend);
61
+ if (!result.ok) throw new Error(`invalid WebGPU backend identity: ${result.errors.join('; ')}`);
62
+ return backend;
63
+ }
64
+
65
+ export function createWebGpuRouteReceiptFromArtifacts(input) {
66
+ validateRouteReceiptBackendIdentity(input.backend);
67
+ const profile = finishAndValidateRouteProfile(input.profile);
68
+
69
+ return createWebGpuLocalRouteReceipt({
70
+ requestedRouteId: input.requestedRouteId,
71
+ effectiveRouteId: input.effectiveRouteId || input.requestedRouteId,
72
+ status: input.status || (input.fallbackReason ? 'fallback' : 'real'),
73
+ fallbackReason: input.fallbackReason || null,
74
+ backend: input.backend,
75
+ model: input.model,
76
+ kernel: input.kernel,
77
+ inputs: input.inputs,
78
+ outputs: input.outputs,
79
+ timings: {
80
+ source: profile.timingSource,
81
+ totalMs: profile.totalMs,
82
+ stages: profile.stages,
83
+ profile,
84
+ },
85
+ });
86
+ }