@simulatte/doppler 0.1.3 → 0.1.5

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 (114) hide show
  1. package/README.md +11 -5
  2. package/package.json +27 -4
  3. package/src/client/doppler-api.browser.d.ts +1 -0
  4. package/src/client/doppler-api.browser.js +288 -0
  5. package/src/client/doppler-api.d.ts +80 -0
  6. package/src/client/doppler-api.js +298 -0
  7. package/src/client/doppler-provider/types.js +1 -1
  8. package/src/client/doppler-registry.d.ts +23 -0
  9. package/src/client/doppler-registry.js +88 -0
  10. package/src/client/doppler-registry.json +39 -0
  11. package/src/config/execution-contract-check.d.ts +82 -0
  12. package/src/config/execution-contract-check.js +317 -0
  13. package/src/config/execution-v0-contract-check.d.ts +94 -0
  14. package/src/config/execution-v0-contract-check.js +251 -0
  15. package/src/config/execution-v0-graph-contract-check.d.ts +20 -0
  16. package/src/config/execution-v0-graph-contract-check.js +64 -0
  17. package/src/config/kernel-path-contract-check.d.ts +76 -0
  18. package/src/config/kernel-path-contract-check.js +479 -0
  19. package/src/config/kernel-path-loader.d.ts +16 -0
  20. package/src/config/kernel-path-loader.js +54 -0
  21. package/src/config/kernels/kernel-ref-digests.js +12 -0
  22. package/src/config/kernels/registry.json +556 -0
  23. package/src/config/loader.js +90 -67
  24. package/src/config/merge-contract-check.d.ts +16 -0
  25. package/src/config/merge-contract-check.js +321 -0
  26. package/src/config/merge-helpers.d.ts +58 -0
  27. package/src/config/merge-helpers.js +54 -0
  28. package/src/config/merge.js +3 -6
  29. package/src/config/presets/models/janus-text.json +27 -0
  30. package/src/config/quantization-contract-check.d.ts +12 -0
  31. package/src/config/quantization-contract-check.js +91 -0
  32. package/src/config/required-inference-fields-contract-check.d.ts +24 -0
  33. package/src/config/required-inference-fields-contract-check.js +231 -0
  34. package/src/config/schema/browser-suite-metrics.schema.d.ts +17 -0
  35. package/src/config/schema/browser-suite-metrics.schema.js +46 -0
  36. package/src/config/schema/conversion-report.schema.d.ts +40 -0
  37. package/src/config/schema/conversion-report.schema.js +108 -0
  38. package/src/config/schema/doppler.schema.js +12 -18
  39. package/src/config/schema/index.d.ts +22 -0
  40. package/src/config/schema/index.js +18 -0
  41. package/src/converter/core.d.ts +10 -0
  42. package/src/converter/core.js +49 -11
  43. package/src/converter/parsers/diffusion.js +63 -3
  44. package/src/converter/tokenizer-utils.js +17 -3
  45. package/src/formats/rdrr/validation.js +13 -0
  46. package/src/gpu/kernels/depthwise_conv2d.d.ts +29 -0
  47. package/src/gpu/kernels/depthwise_conv2d.js +98 -0
  48. package/src/gpu/kernels/depthwise_conv2d.wgsl +58 -0
  49. package/src/gpu/kernels/depthwise_conv2d_f16.wgsl +62 -0
  50. package/src/gpu/kernels/grouped_pointwise_conv2d.d.ts +27 -0
  51. package/src/gpu/kernels/grouped_pointwise_conv2d.js +92 -0
  52. package/src/gpu/kernels/grouped_pointwise_conv2d.wgsl +47 -0
  53. package/src/gpu/kernels/grouped_pointwise_conv2d_f16.wgsl +51 -0
  54. package/src/gpu/kernels/index.d.ts +30 -0
  55. package/src/gpu/kernels/index.js +25 -0
  56. package/src/gpu/kernels/relu.d.ts +18 -0
  57. package/src/gpu/kernels/relu.js +45 -0
  58. package/src/gpu/kernels/relu.wgsl +21 -0
  59. package/src/gpu/kernels/relu_f16.wgsl +23 -0
  60. package/src/gpu/kernels/repeat_channels.d.ts +21 -0
  61. package/src/gpu/kernels/repeat_channels.js +60 -0
  62. package/src/gpu/kernels/repeat_channels.wgsl +29 -0
  63. package/src/gpu/kernels/repeat_channels_f16.wgsl +31 -0
  64. package/src/gpu/kernels/sana_linear_attention.d.ts +27 -0
  65. package/src/gpu/kernels/sana_linear_attention.js +122 -0
  66. package/src/gpu/kernels/sana_linear_attention_apply.wgsl +44 -0
  67. package/src/gpu/kernels/sana_linear_attention_apply_f16.wgsl +47 -0
  68. package/src/gpu/kernels/sana_linear_attention_summary.wgsl +47 -0
  69. package/src/gpu/kernels/sana_linear_attention_summary_f16.wgsl +49 -0
  70. package/src/index-browser.d.ts +1 -0
  71. package/src/index-browser.js +2 -1
  72. package/src/index.d.ts +1 -0
  73. package/src/index.js +2 -1
  74. package/src/inference/browser-harness.js +164 -38
  75. package/src/inference/pipelines/diffusion/init.js +14 -0
  76. package/src/inference/pipelines/diffusion/pipeline.js +206 -77
  77. package/src/inference/pipelines/diffusion/sana-transformer.d.ts +53 -0
  78. package/src/inference/pipelines/diffusion/sana-transformer.js +738 -0
  79. package/src/inference/pipelines/diffusion/scheduler.d.ts +17 -1
  80. package/src/inference/pipelines/diffusion/scheduler.js +91 -3
  81. package/src/inference/pipelines/diffusion/text-encoder-gpu.d.ts +6 -4
  82. package/src/inference/pipelines/diffusion/text-encoder-gpu.js +270 -0
  83. package/src/inference/pipelines/diffusion/text-encoder.js +18 -1
  84. package/src/inference/pipelines/diffusion/types.d.ts +4 -0
  85. package/src/inference/pipelines/diffusion/vae.js +782 -78
  86. package/src/inference/pipelines/text/config.d.ts +5 -0
  87. package/src/inference/pipelines/text/config.js +1 -1
  88. package/src/inference/pipelines/text/execution-v0.js +141 -101
  89. package/src/inference/pipelines/text/init.js +41 -10
  90. package/src/inference/pipelines/text.js +7 -1
  91. package/src/rules/execution-rules-contract-check.d.ts +17 -0
  92. package/src/rules/execution-rules-contract-check.js +245 -0
  93. package/src/rules/kernels/depthwise-conv2d.rules.json +6 -0
  94. package/src/rules/kernels/grouped-pointwise-conv2d.rules.json +6 -0
  95. package/src/rules/kernels/relu.rules.json +6 -0
  96. package/src/rules/kernels/repeat-channels.rules.json +6 -0
  97. package/src/rules/kernels/sana-linear-attention.rules.json +6 -0
  98. package/src/rules/layer-pattern-contract-check.d.ts +17 -0
  99. package/src/rules/layer-pattern-contract-check.js +231 -0
  100. package/src/rules/rule-registry.d.ts +28 -0
  101. package/src/rules/rule-registry.js +38 -0
  102. package/src/tooling/conversion-config-materializer.d.ts +24 -0
  103. package/src/tooling/conversion-config-materializer.js +99 -0
  104. package/src/tooling/lean-execution-contract-runner.d.ts +43 -0
  105. package/src/tooling/lean-execution-contract-runner.js +158 -0
  106. package/src/tooling/lean-execution-contract.d.ts +16 -0
  107. package/src/tooling/lean-execution-contract.js +81 -0
  108. package/src/tooling/node-convert.d.ts +10 -0
  109. package/src/tooling/node-converter.js +59 -0
  110. package/src/tooling/node-webgpu.js +30 -9
  111. package/src/version.d.ts +2 -0
  112. package/src/version.js +2 -0
  113. package/tools/convert-safetensors-node.js +47 -0
  114. package/tools/doppler-cli.js +167 -6
@@ -1,3 +1,5 @@
1
+ import { chooseDefined, chooseDefinedWithSource } from './merge-helpers.js';
2
+
1
3
  // =============================================================================
2
4
  // Merge Implementation
3
5
  // =============================================================================
@@ -8,12 +10,7 @@ function overlay(
8
10
  runtimeValue,
9
11
  sources
10
12
  ) {
11
- if (runtimeValue !== undefined) {
12
- sources.set(path, 'runtime');
13
- return runtimeValue;
14
- }
15
- sources.set(path, 'manifest');
16
- return manifestValue;
13
+ return chooseDefinedWithSource(path, runtimeValue, manifestValue, sources);
17
14
  }
18
15
 
19
16
  function mergeAttention(
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "janus_text",
3
+ "name": "Janus Text Core",
4
+ "extends": "transformer",
5
+ "modelType": "transformer",
6
+
7
+ "inference": {
8
+ "normalization": {
9
+ "rmsNormEps": 1e-6
10
+ },
11
+ "rope": {
12
+ "ropeTheta": 10000
13
+ }
14
+ },
15
+
16
+ "tokenizer": {
17
+ "bosToken": "<|begin▁of▁sentence|>",
18
+ "eosTokens": ["<|end▁of▁sentence|>"],
19
+ "addBosToken": true,
20
+ "addEosToken": false
21
+ },
22
+
23
+ "detection": {
24
+ "architecturePatterns": ["JanusTextForCausalLM"],
25
+ "modelTypePatterns": ["janus_text"]
26
+ }
27
+ }
@@ -0,0 +1,12 @@
1
+ export interface QuantizationContractArtifact {
2
+ schemaVersion: 1;
3
+ source: 'doppler';
4
+ ok: boolean;
5
+ checks: Array<{ id: string; ok: boolean }>;
6
+ errors: string[];
7
+ stats: {
8
+ sampledSizes: number;
9
+ };
10
+ }
11
+
12
+ export declare function buildQuantizationContractArtifact(): QuantizationContractArtifact;
@@ -0,0 +1,91 @@
1
+ import {
2
+ K_SCALE_SIZE,
3
+ Q4K_BLOCK_BYTES,
4
+ Q6K_BLOCK_BYTES,
5
+ Q8_0_BLOCK_BYTES,
6
+ Q8_0_BLOCK_SIZE,
7
+ QK4_K_BLOCK_SIZE,
8
+ QK_K,
9
+ padToQ4KBlock,
10
+ q4kBlockCount,
11
+ } from './schema/quantization.schema.js';
12
+ import {
13
+ TILE_SIZES,
14
+ QUANTIZATION,
15
+ } from '../gpu/kernels/constants.js';
16
+ import * as loaderQuantization from '../loader/quantization-constants.js';
17
+
18
+ const EXPECTED_CONSTANTS = Object.freeze({
19
+ QK_K: 256,
20
+ Q4K_BLOCK_BYTES: 144,
21
+ Q6K_BLOCK_BYTES: 210,
22
+ Q8_0_BLOCK_BYTES: 34,
23
+ Q8_0_BLOCK_SIZE: 32,
24
+ K_SCALE_SIZE: 12,
25
+ });
26
+
27
+ export function buildQuantizationContractArtifact() {
28
+ const errors = [];
29
+ const checks = [];
30
+
31
+ const literalConstantsOk =
32
+ QK_K === EXPECTED_CONSTANTS.QK_K
33
+ && Q4K_BLOCK_BYTES === EXPECTED_CONSTANTS.Q4K_BLOCK_BYTES
34
+ && Q6K_BLOCK_BYTES === EXPECTED_CONSTANTS.Q6K_BLOCK_BYTES
35
+ && Q8_0_BLOCK_BYTES === EXPECTED_CONSTANTS.Q8_0_BLOCK_BYTES
36
+ && Q8_0_BLOCK_SIZE === EXPECTED_CONSTANTS.Q8_0_BLOCK_SIZE
37
+ && K_SCALE_SIZE === EXPECTED_CONSTANTS.K_SCALE_SIZE
38
+ && QK4_K_BLOCK_SIZE === Q4K_BLOCK_BYTES;
39
+ if (!literalConstantsOk) {
40
+ errors.push('[QuantizationContract] schema constants drifted from the expected Q4K/Q6K/Q8 values.');
41
+ }
42
+ checks.push({ id: 'quantization.constants.schema', ok: literalConstantsOk });
43
+
44
+ const crossModuleOk =
45
+ loaderQuantization.QK_K === QK_K
46
+ && loaderQuantization.Q4K_BLOCK_BYTES === Q4K_BLOCK_BYTES
47
+ && loaderQuantization.Q6K_BLOCK_BYTES === Q6K_BLOCK_BYTES
48
+ && loaderQuantization.Q8_0_BLOCK_BYTES === Q8_0_BLOCK_BYTES
49
+ && loaderQuantization.Q8_0_BLOCK_SIZE === Q8_0_BLOCK_SIZE
50
+ && TILE_SIZES.Q4K_SUPER_BLOCK_SIZE === QK_K
51
+ && QUANTIZATION.Q4K_BLOCK_BYTES === Q4K_BLOCK_BYTES;
52
+ if (!crossModuleOk) {
53
+ errors.push('[QuantizationContract] loader/GPU quantization constants drifted from schema constants.');
54
+ }
55
+ checks.push({ id: 'quantization.constants.crossModule', ok: crossModuleOk });
56
+
57
+ let padPropertiesOk = true;
58
+ let q4kCoverageOk = true;
59
+ let previous = -1;
60
+ for (let size = 0; size <= QK_K * 2 + 7; size += 1) {
61
+ const padded = padToQ4KBlock(size);
62
+ if (padded < size || padded % QK_K !== 0 || padToQ4KBlock(padded) !== padded || padded < previous) {
63
+ padPropertiesOk = false;
64
+ break;
65
+ }
66
+ previous = padded;
67
+ if (q4kBlockCount(size) * QK_K < size) {
68
+ q4kCoverageOk = false;
69
+ break;
70
+ }
71
+ }
72
+ if (!padPropertiesOk) {
73
+ errors.push('[QuantizationContract] padToQ4KBlock must be monotone, aligned, and idempotent.');
74
+ }
75
+ checks.push({ id: 'quantization.padToQ4KBlock.properties', ok: padPropertiesOk });
76
+ if (!q4kCoverageOk) {
77
+ errors.push('[QuantizationContract] q4kBlockCount must cover the requested element count.');
78
+ }
79
+ checks.push({ id: 'quantization.q4kBlockCount.coverage', ok: q4kCoverageOk });
80
+
81
+ return {
82
+ schemaVersion: 1,
83
+ source: 'doppler',
84
+ ok: errors.length === 0,
85
+ checks,
86
+ errors,
87
+ stats: {
88
+ sampledSizes: QK_K * 2 + 8,
89
+ },
90
+ };
91
+ }
@@ -0,0 +1,24 @@
1
+ export interface RequiredInferenceFieldsContractArtifact {
2
+ schemaVersion: 1;
3
+ source: 'doppler';
4
+ ok: boolean;
5
+ checks: Array<{ id: string; ok: boolean }>;
6
+ errors: string[];
7
+ stats: {
8
+ fieldCases: number;
9
+ nullableCases: number;
10
+ nonNullableCases: number;
11
+ };
12
+ }
13
+
14
+ export declare function buildRequiredInferenceFieldsContractArtifact(): RequiredInferenceFieldsContractArtifact;
15
+
16
+ export interface ManifestRequiredInferenceFieldsArtifact extends RequiredInferenceFieldsContractArtifact {
17
+ scope: 'manifest';
18
+ label: string;
19
+ }
20
+
21
+ export declare function buildManifestRequiredInferenceFieldsArtifact(
22
+ inference: Record<string, unknown> | null | undefined,
23
+ label?: string
24
+ ): ManifestRequiredInferenceFieldsArtifact;
@@ -0,0 +1,231 @@
1
+ import { validateRequiredInferenceFields } from '../inference/pipelines/text/config.js';
2
+
3
+ function cloneJson(value) {
4
+ if (typeof structuredClone === 'function') {
5
+ return structuredClone(value);
6
+ }
7
+ return JSON.parse(JSON.stringify(value));
8
+ }
9
+
10
+ function setPath(root, path, value) {
11
+ let current = root;
12
+ for (let i = 0; i < path.length - 1; i += 1) {
13
+ current = current[path[i]];
14
+ }
15
+ current[path[path.length - 1]] = value;
16
+ }
17
+
18
+ function deletePath(root, path) {
19
+ let current = root;
20
+ for (let i = 0; i < path.length - 1; i += 1) {
21
+ current = current[path[i]];
22
+ }
23
+ delete current[path[path.length - 1]];
24
+ }
25
+
26
+ function createValidInferenceFixture() {
27
+ return {
28
+ attention: {
29
+ queryPreAttnScalar: 256,
30
+ queryKeyNorm: true,
31
+ attentionBias: false,
32
+ causal: true,
33
+ slidingWindow: null,
34
+ attnLogitSoftcapping: null,
35
+ },
36
+ normalization: {
37
+ rmsNormWeightOffset: true,
38
+ rmsNormEps: 1e-6,
39
+ postAttentionNorm: true,
40
+ preFeedforwardNorm: true,
41
+ postFeedforwardNorm: true,
42
+ },
43
+ ffn: {
44
+ activation: 'gelu',
45
+ gatedActivation: true,
46
+ swigluLimit: null,
47
+ },
48
+ rope: {
49
+ ropeTheta: 1000000,
50
+ ropeScalingFactor: 1.0,
51
+ ropeScalingType: null,
52
+ ropeLocalTheta: null,
53
+ yarnBetaFast: null,
54
+ yarnBetaSlow: null,
55
+ yarnOriginalMaxPos: null,
56
+ },
57
+ output: {
58
+ tieWordEmbeddings: true,
59
+ scaleEmbeddings: true,
60
+ embeddingTranspose: false,
61
+ finalLogitSoftcapping: null,
62
+ embeddingVocabSize: null,
63
+ },
64
+ layerPattern: {
65
+ type: 'every_n',
66
+ globalPattern: null,
67
+ period: 6,
68
+ offset: 0,
69
+ },
70
+ chatTemplate: {
71
+ type: null,
72
+ enabled: true,
73
+ },
74
+ defaultKernelPath: 'unit-test',
75
+ };
76
+ }
77
+
78
+ const FIELD_CASES = Object.freeze([
79
+ { kind: 'nonNullable', path: ['attention', 'queryPreAttnScalar'], message: 'attention.queryPreAttnScalar is required' },
80
+ { kind: 'nonNullable', path: ['attention', 'queryKeyNorm'], message: 'attention.queryKeyNorm is required' },
81
+ { kind: 'nonNullable', path: ['attention', 'attentionBias'], message: 'attention.attentionBias is required' },
82
+ { kind: 'nonNullable', path: ['attention', 'causal'], message: 'attention.causal is required' },
83
+ { kind: 'nullable', path: ['attention', 'slidingWindow'], message: 'attention.slidingWindow must be explicitly set' },
84
+ { kind: 'nullable', path: ['attention', 'attnLogitSoftcapping'], message: 'attention.attnLogitSoftcapping must be explicitly set' },
85
+ { kind: 'nonNullable', path: ['normalization', 'rmsNormWeightOffset'], message: 'normalization.rmsNormWeightOffset is required' },
86
+ { kind: 'nonNullable', path: ['normalization', 'rmsNormEps'], message: 'normalization.rmsNormEps is required' },
87
+ { kind: 'nonNullable', path: ['normalization', 'postAttentionNorm'], message: 'normalization.postAttentionNorm is required' },
88
+ { kind: 'nonNullable', path: ['normalization', 'preFeedforwardNorm'], message: 'normalization.preFeedforwardNorm is required' },
89
+ { kind: 'nonNullable', path: ['normalization', 'postFeedforwardNorm'], message: 'normalization.postFeedforwardNorm is required' },
90
+ { kind: 'nonNullable', path: ['ffn', 'activation'], message: 'ffn.activation is required' },
91
+ { kind: 'nonNullable', path: ['ffn', 'gatedActivation'], message: 'ffn.gatedActivation is required' },
92
+ { kind: 'nullable', path: ['ffn', 'swigluLimit'], message: 'ffn.swigluLimit must be explicitly set' },
93
+ { kind: 'nonNullable', path: ['rope', 'ropeTheta'], message: 'rope.ropeTheta is required' },
94
+ { kind: 'nonNullable', path: ['rope', 'ropeScalingFactor'], message: 'rope.ropeScalingFactor is required' },
95
+ { kind: 'nullable', path: ['rope', 'ropeScalingType'], message: 'rope.ropeScalingType must be explicitly set' },
96
+ { kind: 'nullable', path: ['rope', 'ropeLocalTheta'], message: 'rope.ropeLocalTheta must be explicitly set' },
97
+ { kind: 'nullable', path: ['rope', 'yarnBetaFast'], message: 'rope.yarnBetaFast must be explicitly set' },
98
+ { kind: 'nullable', path: ['rope', 'yarnBetaSlow'], message: 'rope.yarnBetaSlow must be explicitly set' },
99
+ { kind: 'nullable', path: ['rope', 'yarnOriginalMaxPos'], message: 'rope.yarnOriginalMaxPos must be explicitly set' },
100
+ { kind: 'nonNullable', path: ['output', 'tieWordEmbeddings'], message: 'output.tieWordEmbeddings is required' },
101
+ { kind: 'nonNullable', path: ['output', 'scaleEmbeddings'], message: 'output.scaleEmbeddings is required' },
102
+ { kind: 'nonNullable', path: ['output', 'embeddingTranspose'], message: 'output.embeddingTranspose is required' },
103
+ { kind: 'nullable', path: ['output', 'finalLogitSoftcapping'], message: 'output.finalLogitSoftcapping must be explicitly set' },
104
+ { kind: 'nullable', path: ['output', 'embeddingVocabSize'], message: 'output.embeddingVocabSize must be explicitly set' },
105
+ { kind: 'nonNullable', path: ['layerPattern', 'type'], message: 'layerPattern.type is required' },
106
+ { kind: 'nullable', path: ['layerPattern', 'globalPattern'], message: 'layerPattern.globalPattern must be explicitly set' },
107
+ { kind: 'nullable', path: ['layerPattern', 'period'], message: 'layerPattern.period must be explicitly set' },
108
+ { kind: 'nullable', path: ['layerPattern', 'offset'], message: 'layerPattern.offset must be explicitly set' },
109
+ { kind: 'nullable', path: ['chatTemplate', 'type'], message: 'chatTemplate.type must be explicitly set' },
110
+ { kind: 'nonNullable', path: ['chatTemplate', 'enabled'], message: 'chatTemplate.enabled is required' },
111
+ ]);
112
+
113
+ export function buildRequiredInferenceFieldsContractArtifact() {
114
+ const errors = [];
115
+ const checks = [];
116
+ const baseInference = createValidInferenceFixture();
117
+
118
+ try {
119
+ validateRequiredInferenceFields(cloneJson(baseInference), 'required-fields-contract');
120
+ checks.push({ id: 'requiredInferenceFields.validFixture', ok: true });
121
+ } catch (error) {
122
+ errors.push(error instanceof Error ? error.message : String(error));
123
+ checks.push({ id: 'requiredInferenceFields.validFixture', ok: false });
124
+ }
125
+
126
+ for (const field of FIELD_CASES) {
127
+ const missingInference = cloneJson(baseInference);
128
+ if (field.kind === 'nonNullable') {
129
+ setPath(missingInference, field.path, null);
130
+ } else {
131
+ deletePath(missingInference, field.path);
132
+ }
133
+ let rejectsAsExpected = false;
134
+ try {
135
+ validateRequiredInferenceFields(missingInference, 'required-fields-contract');
136
+ } catch (error) {
137
+ const message = error instanceof Error ? error.message : String(error);
138
+ rejectsAsExpected = message.includes(field.message);
139
+ }
140
+ if (!rejectsAsExpected) {
141
+ errors.push(
142
+ `[RequiredInferenceFieldsContract] ${field.path.join('.')} did not produce the expected required-field failure.`
143
+ );
144
+ }
145
+ checks.push({
146
+ id: `requiredInferenceFields.${field.path.join('.')}.rejectsInvalid`,
147
+ ok: rejectsAsExpected,
148
+ });
149
+
150
+ if (field.kind === 'nullable') {
151
+ const nullableInference = cloneJson(baseInference);
152
+ setPath(nullableInference, field.path, null);
153
+ let nullableAccepted = true;
154
+ try {
155
+ validateRequiredInferenceFields(nullableInference, 'required-fields-contract');
156
+ } catch {
157
+ nullableAccepted = false;
158
+ }
159
+ if (!nullableAccepted) {
160
+ errors.push(
161
+ `[RequiredInferenceFieldsContract] ${field.path.join('.')} should allow explicit null but was rejected.`
162
+ );
163
+ }
164
+ checks.push({
165
+ id: `requiredInferenceFields.${field.path.join('.')}.acceptsNull`,
166
+ ok: nullableAccepted,
167
+ });
168
+ }
169
+ }
170
+
171
+ const customInference = cloneJson(baseInference);
172
+ customInference.layerPattern.type = 'custom';
173
+ delete customInference.layerPattern.layerTypes;
174
+ let customLayerTypesRejected = false;
175
+ try {
176
+ validateRequiredInferenceFields(customInference, 'required-fields-contract');
177
+ } catch (error) {
178
+ const message = error instanceof Error ? error.message : String(error);
179
+ customLayerTypesRejected = message.includes('layerPattern.layerTypes must be explicitly set for custom patterns');
180
+ }
181
+ if (!customLayerTypesRejected) {
182
+ errors.push('[RequiredInferenceFieldsContract] custom layerPattern without layerTypes was not rejected.');
183
+ }
184
+ checks.push({
185
+ id: 'requiredInferenceFields.layerPattern.layerTypes.rejectsMissingForCustom',
186
+ ok: customLayerTypesRejected,
187
+ });
188
+
189
+ return {
190
+ schemaVersion: 1,
191
+ source: 'doppler',
192
+ ok: errors.length === 0,
193
+ checks,
194
+ errors,
195
+ stats: {
196
+ fieldCases: FIELD_CASES.length,
197
+ nullableCases: FIELD_CASES.filter((field) => field.kind === 'nullable').length,
198
+ nonNullableCases: FIELD_CASES.filter((field) => field.kind === 'nonNullable').length,
199
+ },
200
+ };
201
+ }
202
+
203
+ export function buildManifestRequiredInferenceFieldsArtifact(inference, label = 'manifest.inference') {
204
+ const errors = [];
205
+ const checks = [];
206
+ let ok = true;
207
+ try {
208
+ validateRequiredInferenceFields(cloneJson(inference), label);
209
+ } catch (error) {
210
+ ok = false;
211
+ errors.push(error instanceof Error ? error.message : String(error));
212
+ }
213
+ checks.push({
214
+ id: `${label}.requiredInferenceFields`,
215
+ ok,
216
+ });
217
+ return {
218
+ schemaVersion: 1,
219
+ source: 'doppler',
220
+ scope: 'manifest',
221
+ label,
222
+ ok,
223
+ checks,
224
+ errors,
225
+ stats: {
226
+ fieldCases: FIELD_CASES.length,
227
+ nullableCases: FIELD_CASES.filter((field) => field.kind === 'nullable').length,
228
+ nonNullableCases: FIELD_CASES.filter((field) => field.kind === 'nonNullable').length,
229
+ },
230
+ };
231
+ }
@@ -0,0 +1,17 @@
1
+ export interface BrowserSuiteMetricsSchema {
2
+ schemaVersion: 1;
3
+ source: 'doppler';
4
+ suite: string;
5
+ executionContractArtifact: Record<string, unknown> | null;
6
+ executionV0GraphContractArtifact: Record<string, unknown> | null;
7
+ layerPatternContractArtifact: Record<string, unknown> | null;
8
+ requiredInferenceFieldsArtifact: Record<string, unknown> | null;
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ export declare const BROWSER_SUITE_METRICS_SCHEMA_VERSION: 1;
13
+ export declare const DEFAULT_BROWSER_SUITE_METRICS: Readonly<BrowserSuiteMetricsSchema>;
14
+
15
+ export declare function validateBrowserSuiteMetrics(
16
+ metrics: Record<string, unknown>
17
+ ): BrowserSuiteMetricsSchema;
@@ -0,0 +1,46 @@
1
+ function assertPlainObject(value, label) {
2
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
3
+ throw new Error(`browser suite metrics: ${label} must be an object.`);
4
+ }
5
+ }
6
+
7
+ function assertString(value, label) {
8
+ if (typeof value !== 'string' || !value.trim()) {
9
+ throw new Error(`browser suite metrics: ${label} must be a non-empty string.`);
10
+ }
11
+ }
12
+
13
+ function assertNullablePlainObject(value, label) {
14
+ if (value == null) return;
15
+ assertPlainObject(value, label);
16
+ }
17
+
18
+ export const BROWSER_SUITE_METRICS_SCHEMA_VERSION = 1;
19
+
20
+ export const DEFAULT_BROWSER_SUITE_METRICS = Object.freeze({
21
+ schemaVersion: BROWSER_SUITE_METRICS_SCHEMA_VERSION,
22
+ source: 'doppler',
23
+ suite: 'inference',
24
+ executionContractArtifact: null,
25
+ executionV0GraphContractArtifact: null,
26
+ layerPatternContractArtifact: null,
27
+ requiredInferenceFieldsArtifact: null,
28
+ });
29
+
30
+ export function validateBrowserSuiteMetrics(metrics) {
31
+ assertPlainObject(metrics, 'metrics');
32
+ if (metrics.schemaVersion !== BROWSER_SUITE_METRICS_SCHEMA_VERSION) {
33
+ throw new Error(
34
+ `browser suite metrics: schemaVersion must be ${BROWSER_SUITE_METRICS_SCHEMA_VERSION}.`
35
+ );
36
+ }
37
+ if (metrics.source !== 'doppler') {
38
+ throw new Error('browser suite metrics: source must be "doppler".');
39
+ }
40
+ assertString(metrics.suite, 'suite');
41
+ assertNullablePlainObject(metrics.executionContractArtifact, 'executionContractArtifact');
42
+ assertNullablePlainObject(metrics.executionV0GraphContractArtifact, 'executionV0GraphContractArtifact');
43
+ assertNullablePlainObject(metrics.layerPatternContractArtifact, 'layerPatternContractArtifact');
44
+ assertNullablePlainObject(metrics.requiredInferenceFieldsArtifact, 'requiredInferenceFieldsArtifact');
45
+ return metrics;
46
+ }
@@ -0,0 +1,40 @@
1
+ export interface ConversionReportResultSchema {
2
+ presetId: string | null;
3
+ modelType: string | null;
4
+ outputDir: string | null;
5
+ shardCount: number | null;
6
+ tensorCount: number | null;
7
+ totalSize: number | null;
8
+ }
9
+
10
+ export interface ConversionReportManifestSchema {
11
+ quantization: string | null;
12
+ quantizationInfo: Record<string, unknown> | null;
13
+ inference: {
14
+ presetId: string | null;
15
+ schema: string | null;
16
+ defaultKernelPath: string | null;
17
+ } | null;
18
+ }
19
+
20
+ export interface ConversionReportSchema {
21
+ schemaVersion: 1;
22
+ suite: 'convert';
23
+ command: 'convert';
24
+ modelId: string;
25
+ timestamp: string;
26
+ source: 'doppler';
27
+ result: ConversionReportResultSchema;
28
+ manifest: ConversionReportManifestSchema | null;
29
+ executionContractArtifact: Record<string, unknown> | null;
30
+ executionV0GraphContractArtifact: Record<string, unknown> | null;
31
+ layerPatternContractArtifact: Record<string, unknown> | null;
32
+ requiredInferenceFieldsArtifact: Record<string, unknown> | null;
33
+ }
34
+
35
+ export declare const CONVERSION_REPORT_SCHEMA_VERSION: 1;
36
+ export declare const DEFAULT_CONVERSION_REPORT: ConversionReportSchema;
37
+
38
+ export declare function validateConversionReport(
39
+ report: Record<string, unknown>
40
+ ): ConversionReportSchema;
@@ -0,0 +1,108 @@
1
+ function assertPlainObject(value, label) {
2
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
3
+ throw new Error(`conversion report: ${label} must be an object.`);
4
+ }
5
+ }
6
+
7
+ function assertString(value, label) {
8
+ if (typeof value !== 'string' || !value.trim()) {
9
+ throw new Error(`conversion report: ${label} must be a non-empty string.`);
10
+ }
11
+ }
12
+
13
+ function assertNullableString(value, label) {
14
+ if (value === null || value === undefined) return;
15
+ assertString(value, label);
16
+ }
17
+
18
+ function assertNullableFiniteNumber(value, label) {
19
+ if (value === null || value === undefined) return;
20
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
21
+ throw new Error(`conversion report: ${label} must be a finite number when provided.`);
22
+ }
23
+ }
24
+
25
+ function assertNullablePlainObject(value, label) {
26
+ if (value === null || value === undefined) return;
27
+ assertPlainObject(value, label);
28
+ }
29
+
30
+ export const CONVERSION_REPORT_SCHEMA_VERSION = 1;
31
+
32
+ export const DEFAULT_CONVERSION_REPORT = Object.freeze({
33
+ schemaVersion: CONVERSION_REPORT_SCHEMA_VERSION,
34
+ suite: 'convert',
35
+ command: 'convert',
36
+ modelId: 'unknown',
37
+ timestamp: '1970-01-01T00:00:00.000Z',
38
+ source: 'doppler',
39
+ result: {
40
+ presetId: null,
41
+ modelType: null,
42
+ outputDir: null,
43
+ shardCount: null,
44
+ tensorCount: null,
45
+ totalSize: null,
46
+ },
47
+ manifest: {
48
+ quantization: null,
49
+ quantizationInfo: null,
50
+ inference: {
51
+ presetId: null,
52
+ schema: null,
53
+ defaultKernelPath: null,
54
+ },
55
+ },
56
+ executionContractArtifact: null,
57
+ executionV0GraphContractArtifact: null,
58
+ layerPatternContractArtifact: null,
59
+ requiredInferenceFieldsArtifact: null,
60
+ });
61
+
62
+ export function validateConversionReport(report) {
63
+ assertPlainObject(report, 'report');
64
+ if (report.schemaVersion !== CONVERSION_REPORT_SCHEMA_VERSION) {
65
+ throw new Error(
66
+ `conversion report: schemaVersion must be ${CONVERSION_REPORT_SCHEMA_VERSION}.`
67
+ );
68
+ }
69
+ if (report.suite !== 'convert') {
70
+ throw new Error('conversion report: suite must be "convert".');
71
+ }
72
+ if (report.command !== 'convert') {
73
+ throw new Error('conversion report: command must be "convert".');
74
+ }
75
+ if (report.source !== 'doppler') {
76
+ throw new Error('conversion report: source must be "doppler".');
77
+ }
78
+ assertString(report.modelId, 'modelId');
79
+ assertString(report.timestamp, 'timestamp');
80
+ assertPlainObject(report.result, 'result');
81
+ assertNullableString(report.result.presetId, 'result.presetId');
82
+ assertNullableString(report.result.modelType, 'result.modelType');
83
+ assertNullableString(report.result.outputDir, 'result.outputDir');
84
+ assertNullableFiniteNumber(report.result.shardCount, 'result.shardCount');
85
+ assertNullableFiniteNumber(report.result.tensorCount, 'result.tensorCount');
86
+ assertNullableFiniteNumber(report.result.totalSize, 'result.totalSize');
87
+
88
+ assertNullablePlainObject(report.manifest, 'manifest');
89
+ if (report.manifest) {
90
+ assertNullableString(report.manifest.quantization, 'manifest.quantization');
91
+ assertNullablePlainObject(report.manifest.quantizationInfo, 'manifest.quantizationInfo');
92
+ assertNullablePlainObject(report.manifest.inference, 'manifest.inference');
93
+ if (report.manifest.inference) {
94
+ assertNullableString(report.manifest.inference.presetId, 'manifest.inference.presetId');
95
+ assertNullableString(report.manifest.inference.schema, 'manifest.inference.schema');
96
+ assertNullableString(
97
+ report.manifest.inference.defaultKernelPath,
98
+ 'manifest.inference.defaultKernelPath'
99
+ );
100
+ }
101
+ }
102
+
103
+ assertNullablePlainObject(report.executionContractArtifact, 'executionContractArtifact');
104
+ assertNullablePlainObject(report.executionV0GraphContractArtifact, 'executionV0GraphContractArtifact');
105
+ assertNullablePlainObject(report.layerPatternContractArtifact, 'layerPatternContractArtifact');
106
+ assertNullablePlainObject(report.requiredInferenceFieldsArtifact, 'requiredInferenceFieldsArtifact');
107
+ return report;
108
+ }
@@ -3,6 +3,13 @@ import { DEFAULT_INFERENCE_DEFAULTS_CONFIG } from './inference-defaults.schema.j
3
3
  import { DEFAULT_SHARED_RUNTIME_CONFIG } from './shared-runtime.schema.js';
4
4
  import { DEFAULT_EMULATION_CONFIG, createEmulationConfig } from './emulation.schema.js';
5
5
  import { mergeEcosystemConfig } from './ecosystem.schema.js';
6
+ import {
7
+ chooseNullish,
8
+ mergeExecutionPatchLists,
9
+ mergeKernelPathPolicy,
10
+ mergeShallowObject,
11
+ replaceSubtree,
12
+ } from '../merge-helpers.js';
6
13
 
7
14
  // =============================================================================
8
15
  // Runtime Config (all non-model-specific settings)
@@ -172,8 +179,6 @@ function mergeInferenceConfig(
172
179
  const overrideExecutionPatch = overrides.executionPatch ?? {};
173
180
  const baseKernelPathPolicy = base.kernelPathPolicy ?? {};
174
181
  const overrideKernelPathPolicy = overrides.kernelPathPolicy ?? {};
175
- const baseKernelPathSourceScope = baseKernelPathPolicy.sourceScope ?? baseKernelPathPolicy.allowSources;
176
- const overrideKernelPathSourceScope = overrideKernelPathPolicy.sourceScope ?? overrideKernelPathPolicy.allowSources;
177
182
  const hasRuntimeKernelProfiles = Object.prototype.hasOwnProperty.call(
178
183
  overrideSessionCompute,
179
184
  'kernelProfiles'
@@ -236,15 +241,8 @@ function mergeInferenceConfig(
236
241
  pipeline: overrides.pipeline ?? base.pipeline,
237
242
  kernelPath: overrides.kernelPath ?? base.kernelPath,
238
243
  kernelPathSource: overrides.kernelPathSource ?? base.kernelPathSource,
239
- kernelPathPolicy: {
240
- mode: overrideKernelPathPolicy.mode ?? baseKernelPathPolicy.mode,
241
- sourceScope: overrideKernelPathSourceScope ?? baseKernelPathSourceScope,
242
- allowSources: overrideKernelPathSourceScope ?? baseKernelPathSourceScope,
243
- onIncompatible: overrideKernelPathPolicy.onIncompatible ?? baseKernelPathPolicy.onIncompatible,
244
- },
245
- chatTemplate: overrides.chatTemplate
246
- ? { ...base.chatTemplate, ...overrides.chatTemplate }
247
- : base.chatTemplate,
244
+ kernelPathPolicy: mergeKernelPathPolicy(baseKernelPathPolicy, overrideKernelPathPolicy),
245
+ chatTemplate: mergeShallowObject(base.chatTemplate, overrides.chatTemplate),
248
246
  session: {
249
247
  ...baseSession,
250
248
  ...overrideSession,
@@ -259,14 +257,10 @@ function mergeInferenceConfig(
259
257
  ? { kernelProfiles: overrideSessionCompute.kernelProfiles }
260
258
  : { kernelProfiles: baseSessionCompute.kernelProfiles }),
261
259
  },
262
- kvcache: overrideSession.kvcache ?? baseSession.kvcache,
263
- decodeLoop: overrideSession.decodeLoop ?? baseSession.decodeLoop,
264
- },
265
- executionPatch: {
266
- set: overrideExecutionPatch.set ?? baseExecutionPatch.set ?? [],
267
- remove: overrideExecutionPatch.remove ?? baseExecutionPatch.remove ?? [],
268
- add: overrideExecutionPatch.add ?? baseExecutionPatch.add ?? [],
260
+ kvcache: replaceSubtree(overrideSession.kvcache, baseSession.kvcache),
261
+ decodeLoop: replaceSubtree(overrideSession.decodeLoop, baseSession.decodeLoop),
269
262
  },
263
+ executionPatch: mergeExecutionPatchLists(baseExecutionPatch, overrideExecutionPatch),
270
264
  // Model-specific inference overrides (merged with manifest.inference at load time)
271
265
  modelOverrides: overrides.modelOverrides ?? base.modelOverrides,
272
266
  };