zarr-metadata 0.2.0 → 0.4.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.
package/README.md CHANGED
@@ -76,9 +76,13 @@ package's flat problem lists; `flattenTree`/`treeOf` convert between the
76
76
  two, and the flat path+kind form remains the cross-language interchange
77
77
  format the conformance corpus asserts on.
78
78
 
79
- Validation is structural (key presence, value shapes, fixed literals), not
80
- domain-level: extension points (codecs, chunk grids, data types) are never
81
- interpreted, matching the Python package's layering.
79
+ The structural validators check key presence, value shapes, and fixed
80
+ literals without interpreting extension points, matching the Python
81
+ package's layering. `validateArraySemanticsV3` is a separate,
82
+ TS-only semantic layer on top: it interprets the well-known core extension
83
+ points to enforce the specs' cross-field prose rules (regular chunk grid
84
+ arity, transpose permutations, sharding divisibility, fill_value vs data
85
+ type), skipping unrecognized names — the extension name space is open.
82
86
 
83
87
  Consumers include the
84
88
  [Zarr Metadata VS Code extension](https://github.com/d-v-b/vscode-zarr).
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  export type { JSONValue, ZarrV3MetadataFieldJSON, ZarrV3NamedConfigJSON } from "./common.js";
9
9
  export { flattenTree, formatTree, isEmptyTree, MetadataValidationError, treeOf, type ErrorTree, type Issue, type IssueKind, type IssuePath, type ParseResult, type PathedIssue, } from "./errors.js";
10
10
  export * from "./schemas.js";
11
+ export * from "./semantics.js";
11
12
  export type { StandardSchemaV1 } from "./standard-schema.js";
12
13
  export * from "./v2.js";
13
14
  export * from "./v3.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EAAE,SAAS,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAC7F,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,MAAM,EACN,KAAK,SAAS,EACd,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,WAAW,GACjB,MAAM,aAAa,CAAC;AACrB,cAAc,cAAc,CAAC;AAC7B,YAAY,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EAAE,SAAS,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAC7F,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,MAAM,EACN,KAAK,SAAS,EACd,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,WAAW,GACjB,MAAM,aAAa,CAAC;AACrB,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,YAAY,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
  export { flattenTree, formatTree, isEmptyTree, MetadataValidationError, treeOf, } from "./errors.js";
9
9
  export * from "./schemas.js";
10
+ export * from "./semantics.js";
10
11
  export * from "./v2.js";
11
12
  export * from "./v3.js";
12
13
  export * from "./validation.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,MAAM,GAOP,MAAM,aAAa,CAAC;AACrB,cAAc,cAAc,CAAC;AAE7B,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,MAAM,GAOP,MAAM,aAAa,CAAC;AACrB,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAE/B,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Semantic (cross-field) validation for v3 array metadata.
3
+ *
4
+ * The structural validators never interpret what an extension point names;
5
+ * this module deliberately interprets a small set of WELL-KNOWN core
6
+ * extension points to enforce the cross-field rules the specs state in
7
+ * prose:
8
+ *
9
+ * - the `regular` chunk grid's `chunk_shape` has one length per dimension
10
+ * of `shape`;
11
+ * - the `rectilinear` chunk grid's `chunk_shapes` has one entry per
12
+ * dimension, and each explicit chunk list (integers and `[size, count]`
13
+ * run-length pairs) sums exactly to that dimension's length (the
14
+ * bare-integer uniform shorthand carries no sum constraint, like the
15
+ * regular grid);
16
+ * - a `transpose` codec's `order` is a permutation of `0..n-1`, with one
17
+ * entry per array dimension;
18
+ * - a `sharding_indexed` codec's inner `chunk_shape` matches the array's
19
+ * dimensionality and evenly divides every chunk it shards — the grid's
20
+ * chunk at the top level (each distinct per-dimension size, for
21
+ * rectilinear grids), the parent shard's inner chunk when sharding
22
+ * nests;
23
+ * - `fill_value` has a JSON shape permitted for the named core data type
24
+ * (booleans for `bool`, ranged integers for the int types, numbers /
25
+ * the "NaN"-family sentinels / width-checked "0x…" strings for the
26
+ * float types, two-element arrays for the complex types).
27
+ *
28
+ * Unrecognized names are skipped everywhere — the extension name space is
29
+ * open, and a rule that guessed would lie. This layer has no counterpart
30
+ * in the Python reference implementation (which stops at structure), so it
31
+ * is covered by this package's own tests rather than the conformance
32
+ * corpus.
33
+ */
34
+ import { type ErrorTree } from "./errors.js";
35
+ /**
36
+ * Every semantic (cross-field) problem in a v3 array metadata document, as
37
+ * an error tree; an empty tree means no rule found a violation. Values that
38
+ * are not v3 array documents yield an empty tree; run the structural
39
+ * validators for structure.
40
+ */
41
+ export declare function validateArraySemanticsV3(value: unknown): ErrorTree;
42
+ //# sourceMappingURL=semantics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantics.d.ts","sourceRoot":"","sources":["../src/semantics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAU,KAAK,SAAS,EAAoB,MAAM,aAAa,CAAC;AAuMvE;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAqFlE"}
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Semantic (cross-field) validation for v3 array metadata.
3
+ *
4
+ * The structural validators never interpret what an extension point names;
5
+ * this module deliberately interprets a small set of WELL-KNOWN core
6
+ * extension points to enforce the cross-field rules the specs state in
7
+ * prose:
8
+ *
9
+ * - the `regular` chunk grid's `chunk_shape` has one length per dimension
10
+ * of `shape`;
11
+ * - the `rectilinear` chunk grid's `chunk_shapes` has one entry per
12
+ * dimension, and each explicit chunk list (integers and `[size, count]`
13
+ * run-length pairs) sums exactly to that dimension's length (the
14
+ * bare-integer uniform shorthand carries no sum constraint, like the
15
+ * regular grid);
16
+ * - a `transpose` codec's `order` is a permutation of `0..n-1`, with one
17
+ * entry per array dimension;
18
+ * - a `sharding_indexed` codec's inner `chunk_shape` matches the array's
19
+ * dimensionality and evenly divides every chunk it shards — the grid's
20
+ * chunk at the top level (each distinct per-dimension size, for
21
+ * rectilinear grids), the parent shard's inner chunk when sharding
22
+ * nests;
23
+ * - `fill_value` has a JSON shape permitted for the named core data type
24
+ * (booleans for `bool`, ranged integers for the int types, numbers /
25
+ * the "NaN"-family sentinels / width-checked "0x…" strings for the
26
+ * float types, two-element arrays for the complex types).
27
+ *
28
+ * Unrecognized names are skipped everywhere — the extension name space is
29
+ * open, and a rule that guessed would lie. This layer has no counterpart
30
+ * in the Python reference implementation (which stops at structure), so it
31
+ * is covered by this package's own tests rather than the conformance
32
+ * corpus.
33
+ */
34
+ import { treeOf } from "./errors.js";
35
+ function isPlainObject(value) {
36
+ if (typeof value !== "object" || value === null || Array.isArray(value))
37
+ return false;
38
+ const proto = Object.getPrototypeOf(value);
39
+ return proto === null || proto === Object.prototype;
40
+ }
41
+ function isIntArray(value) {
42
+ return (Array.isArray(value) &&
43
+ Object.keys(value).length === value.length &&
44
+ value.every((item) => Number.isInteger(item)));
45
+ }
46
+ /** The `{name, configuration}` of a metadata field, when extractable. */
47
+ function fieldParts(field) {
48
+ if (typeof field === "string")
49
+ return { name: field, configuration: undefined };
50
+ if (isPlainObject(field) && typeof field["name"] === "string") {
51
+ const configuration = field["configuration"];
52
+ return {
53
+ name: field["name"],
54
+ configuration: isPlainObject(configuration) ? configuration : undefined,
55
+ };
56
+ }
57
+ return undefined;
58
+ }
59
+ // --- fill_value vs data_type ----------------------------------------------
60
+ const INT_RANGES = {
61
+ int8: [-128, 127],
62
+ int16: [-32768, 32767],
63
+ int32: [-2147483648, 2147483647],
64
+ int64: [-9223372036854775808, 9223372036854775807],
65
+ uint8: [0, 255],
66
+ uint16: [0, 65535],
67
+ uint32: [0, 4294967295],
68
+ uint64: [0, 18446744073709551615],
69
+ };
70
+ const FLOAT_HEX_DIGITS = {
71
+ float16: 4,
72
+ float32: 8,
73
+ float64: 16,
74
+ };
75
+ /** Hex digit width of each complex type's component floats. */
76
+ const COMPLEX_COMPONENT_HEX_DIGITS = {
77
+ complex64: 8,
78
+ complex128: 16,
79
+ };
80
+ function isFloatFill(value, hexDigits) {
81
+ if (typeof value === "number")
82
+ return true;
83
+ if (typeof value !== "string")
84
+ return false;
85
+ if (value === "NaN" || value === "Infinity" || value === "-Infinity")
86
+ return true;
87
+ return new RegExp(`^0x[0-9a-fA-F]{${hexDigits}}$`).test(value);
88
+ }
89
+ function floatFillMessage(name, hexDigits) {
90
+ return (`expected a number, "NaN", "Infinity", "-Infinity", or a ` +
91
+ `${hexDigits}-hex-digit "0x..." string for data type ${JSON.stringify(name)}`);
92
+ }
93
+ /** The fill_value issue for a KNOWN core data type, if any; undefined otherwise. */
94
+ function fillValueIssue(dataTypeName, fill) {
95
+ if (dataTypeName === "bool") {
96
+ return typeof fill === "boolean"
97
+ ? undefined
98
+ : 'expected a boolean fill value for data type "bool"';
99
+ }
100
+ const intRange = INT_RANGES[dataTypeName];
101
+ if (intRange !== undefined) {
102
+ const [low, high] = intRange;
103
+ return Number.isInteger(fill) && fill >= low && fill <= high
104
+ ? undefined
105
+ : `expected an integer in [${low}, ${high}] for data type ${JSON.stringify(dataTypeName)}`;
106
+ }
107
+ const floatDigits = FLOAT_HEX_DIGITS[dataTypeName];
108
+ if (floatDigits !== undefined) {
109
+ return isFloatFill(fill, floatDigits)
110
+ ? undefined
111
+ : floatFillMessage(dataTypeName, floatDigits);
112
+ }
113
+ const complexDigits = COMPLEX_COMPONENT_HEX_DIGITS[dataTypeName];
114
+ if (complexDigits !== undefined) {
115
+ const ok = Array.isArray(fill) &&
116
+ fill.length === 2 &&
117
+ fill.every((part) => isFloatFill(part, complexDigits));
118
+ return ok
119
+ ? undefined
120
+ : `expected a two-element [real, imaginary] array for data type ${JSON.stringify(dataTypeName)}`;
121
+ }
122
+ return undefined; // unrecognized data type: no verdict
123
+ }
124
+ // --- codec pipelines (transpose / sharding) --------------------------------
125
+ function isPermutation(order) {
126
+ const seen = new Set(order);
127
+ return seen.size === order.length && order.every((entry) => entry >= 0 && entry < order.length);
128
+ }
129
+ /**
130
+ * Walk one codec pipeline. `dims` is the array dimensionality at this point
131
+ * (undefined when unknowable); `outerChunkSizes` holds the distinct chunk
132
+ * lengths per dimension this pipeline may encode — what a sharding codec's
133
+ * inner chunks must divide. A regular grid contributes one size per
134
+ * dimension; a rectilinear grid may contribute several.
135
+ */
136
+ function pipelineIssues(pipeline, path, dims, outerChunkSizes) {
137
+ const issues = [];
138
+ pipeline.forEach((entry, index) => {
139
+ const parts = fieldParts(entry);
140
+ if (parts === undefined)
141
+ return;
142
+ const { name, configuration } = parts;
143
+ if (name === "transpose") {
144
+ const order = configuration?.["order"];
145
+ if (!isIntArray(order))
146
+ return; // shape errors are the schema layer's
147
+ const orderPath = [...path, index, "configuration", "order"];
148
+ if (!isPermutation(order)) {
149
+ issues.push({
150
+ path: orderPath,
151
+ message: `expected a permutation of the integers 0..${order.length - 1}`,
152
+ kind: "invalid_value",
153
+ });
154
+ }
155
+ if (dims !== undefined && order.length !== dims) {
156
+ issues.push({
157
+ path: orderPath,
158
+ message: `expected one entry per array dimension (${dims})`,
159
+ kind: "invalid_value",
160
+ });
161
+ }
162
+ }
163
+ else if (name === "sharding_indexed") {
164
+ const chunkShape = configuration?.["chunk_shape"];
165
+ if (!isIntArray(chunkShape))
166
+ return;
167
+ const chunkShapePath = [...path, index, "configuration", "chunk_shape"];
168
+ if (dims !== undefined && chunkShape.length !== dims) {
169
+ issues.push({
170
+ path: chunkShapePath,
171
+ message: `expected one length per array dimension (${dims})`,
172
+ kind: "invalid_value",
173
+ });
174
+ }
175
+ else if (outerChunkSizes !== undefined &&
176
+ chunkShape.length === outerChunkSizes.length &&
177
+ chunkShape.every((length) => length > 0)) {
178
+ let violation;
179
+ outerChunkSizes.forEach((sizes, axis) => {
180
+ if (violation !== undefined)
181
+ return;
182
+ const bad = sizes.find((size) => size % chunkShape[axis] !== 0);
183
+ if (bad !== undefined)
184
+ violation = { axis, size: bad };
185
+ });
186
+ if (violation !== undefined) {
187
+ const { axis, size } = violation;
188
+ const uniform = outerChunkSizes.every((sizes) => sizes.length === 1);
189
+ issues.push({
190
+ path: chunkShapePath,
191
+ message: uniform
192
+ ? `expected ${JSON.stringify(chunkShape)} to evenly divide the outer chunk shape ${JSON.stringify(outerChunkSizes.map((sizes) => sizes[0]))}`
193
+ : `expected ${JSON.stringify(chunkShape)} to evenly divide every chunk size of the grid (dimension ${axis} has chunk size ${size})`,
194
+ kind: "invalid_value",
195
+ });
196
+ }
197
+ }
198
+ const inner = configuration?.["codecs"];
199
+ if (Array.isArray(inner)) {
200
+ issues.push(...pipelineIssues(inner, [...path, index, "configuration", "codecs"], dims, chunkShape.map((length) => [length])));
201
+ }
202
+ // index_codecs encode the shard index, whose shape differs from the
203
+ // array's — no dimensional context applies there.
204
+ }
205
+ });
206
+ return issues;
207
+ }
208
+ /**
209
+ * Every semantic (cross-field) problem in a v3 array metadata document, as
210
+ * an error tree; an empty tree means no rule found a violation. Values that
211
+ * are not v3 array documents yield an empty tree; run the structural
212
+ * validators for structure.
213
+ */
214
+ export function validateArraySemanticsV3(value) {
215
+ if (!isPlainObject(value) || value["node_type"] !== "array")
216
+ return treeOf([]);
217
+ const issues = [];
218
+ const shape = isIntArray(value["shape"]) ? value["shape"] : undefined;
219
+ const dims = shape?.length;
220
+ const grid = fieldParts(value["chunk_grid"]);
221
+ let chunkSizes;
222
+ if (grid?.name === "regular") {
223
+ const configured = grid.configuration?.["chunk_shape"];
224
+ if (isIntArray(configured)) {
225
+ if (dims !== undefined && configured.length !== dims) {
226
+ issues.push({
227
+ path: ["chunk_grid", "configuration", "chunk_shape"],
228
+ message: `expected one length per dimension of shape (${dims})`,
229
+ kind: "invalid_value",
230
+ });
231
+ // wrong arity: unusable as division context
232
+ }
233
+ else {
234
+ chunkSizes = configured.map((length) => [length]);
235
+ }
236
+ }
237
+ }
238
+ else if (grid?.name === "rectilinear") {
239
+ const specs = grid.configuration?.["chunk_shapes"];
240
+ if (Array.isArray(specs)) {
241
+ if (dims !== undefined && specs.length !== dims) {
242
+ issues.push({
243
+ path: ["chunk_grid", "configuration", "chunk_shapes"],
244
+ message: `expected one entry per dimension of shape (${dims})`,
245
+ kind: "invalid_value",
246
+ });
247
+ }
248
+ else {
249
+ const perDim = specs.map((spec, dim) => {
250
+ // Bare integer: uniform shorthand, no sum constraint (edge chunks
251
+ // are permitted, as with the regular grid).
252
+ if (Number.isInteger(spec))
253
+ return spec > 0 ? [spec] : undefined;
254
+ if (!Array.isArray(spec))
255
+ return undefined; // registry schema's problem
256
+ const sizes = new Set();
257
+ let total = 0;
258
+ for (const entry of spec) {
259
+ if (Number.isInteger(entry)) {
260
+ sizes.add(entry);
261
+ total += entry;
262
+ }
263
+ else if (Array.isArray(entry) &&
264
+ entry.length === 2 &&
265
+ Number.isInteger(entry[0]) &&
266
+ Number.isInteger(entry[1])) {
267
+ sizes.add(entry[0]);
268
+ total += entry[0] * entry[1];
269
+ }
270
+ else {
271
+ return undefined; // malformed entry: registry schema's problem
272
+ }
273
+ }
274
+ const extent = shape?.[dim];
275
+ if (extent !== undefined && total !== extent) {
276
+ issues.push({
277
+ path: ["chunk_grid", "configuration", "chunk_shapes", dim],
278
+ message: `expected chunk sizes summing to ${extent} along dimension ${dim}, got ${total}`,
279
+ kind: "invalid_value",
280
+ });
281
+ }
282
+ return [...sizes];
283
+ });
284
+ if (perDim.every((sizes) => sizes !== undefined)) {
285
+ chunkSizes = perDim;
286
+ }
287
+ }
288
+ }
289
+ }
290
+ const dataType = fieldParts(value["data_type"]);
291
+ if (dataType !== undefined && "fill_value" in value) {
292
+ const message = fillValueIssue(dataType.name, value["fill_value"]);
293
+ if (message !== undefined) {
294
+ issues.push({ path: ["fill_value"], message, kind: "invalid_value" });
295
+ }
296
+ }
297
+ const codecs = value["codecs"];
298
+ if (Array.isArray(codecs)) {
299
+ issues.push(...pipelineIssues(codecs, ["codecs"], dims, chunkSizes));
300
+ }
301
+ return treeOf(issues);
302
+ }
303
+ //# sourceMappingURL=semantics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantics.js","sourceRoot":"","sources":["../src/semantics.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zarr-metadata",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Spec-defined metadata types and validators for Zarr v2 and v3.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export {
21
21
  type PathedIssue,
22
22
  } from "./errors.js";
23
23
  export * from "./schemas.js";
24
+ export * from "./semantics.js";
24
25
  export type { StandardSchemaV1 } from "./standard-schema.js";
25
26
  export * from "./v2.js";
26
27
  export * from "./v3.js";
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Semantic (cross-field) validation for v3 array metadata.
3
+ *
4
+ * The structural validators never interpret what an extension point names;
5
+ * this module deliberately interprets a small set of WELL-KNOWN core
6
+ * extension points to enforce the cross-field rules the specs state in
7
+ * prose:
8
+ *
9
+ * - the `regular` chunk grid's `chunk_shape` has one length per dimension
10
+ * of `shape`;
11
+ * - the `rectilinear` chunk grid's `chunk_shapes` has one entry per
12
+ * dimension, and each explicit chunk list (integers and `[size, count]`
13
+ * run-length pairs) sums exactly to that dimension's length (the
14
+ * bare-integer uniform shorthand carries no sum constraint, like the
15
+ * regular grid);
16
+ * - a `transpose` codec's `order` is a permutation of `0..n-1`, with one
17
+ * entry per array dimension;
18
+ * - a `sharding_indexed` codec's inner `chunk_shape` matches the array's
19
+ * dimensionality and evenly divides every chunk it shards — the grid's
20
+ * chunk at the top level (each distinct per-dimension size, for
21
+ * rectilinear grids), the parent shard's inner chunk when sharding
22
+ * nests;
23
+ * - `fill_value` has a JSON shape permitted for the named core data type
24
+ * (booleans for `bool`, ranged integers for the int types, numbers /
25
+ * the "NaN"-family sentinels / width-checked "0x…" strings for the
26
+ * float types, two-element arrays for the complex types).
27
+ *
28
+ * Unrecognized names are skipped everywhere — the extension name space is
29
+ * open, and a rule that guessed would lie. This layer has no counterpart
30
+ * in the Python reference implementation (which stops at structure), so it
31
+ * is covered by this package's own tests rather than the conformance
32
+ * corpus.
33
+ */
34
+
35
+ import { treeOf, type ErrorTree, type PathedIssue } from "./errors.js";
36
+
37
+ type Path = ReadonlyArray<string | number>;
38
+
39
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
40
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
41
+ const proto: unknown = Object.getPrototypeOf(value);
42
+ return proto === null || proto === Object.prototype;
43
+ }
44
+
45
+ function isIntArray(value: unknown): value is number[] {
46
+ return (
47
+ Array.isArray(value) &&
48
+ Object.keys(value).length === value.length &&
49
+ value.every((item) => Number.isInteger(item))
50
+ );
51
+ }
52
+
53
+ /** The `{name, configuration}` of a metadata field, when extractable. */
54
+ function fieldParts(
55
+ field: unknown,
56
+ ): { name: string; configuration: Record<string, unknown> | undefined } | undefined {
57
+ if (typeof field === "string") return { name: field, configuration: undefined };
58
+ if (isPlainObject(field) && typeof field["name"] === "string") {
59
+ const configuration = field["configuration"];
60
+ return {
61
+ name: field["name"],
62
+ configuration: isPlainObject(configuration) ? configuration : undefined,
63
+ };
64
+ }
65
+ return undefined;
66
+ }
67
+
68
+ // --- fill_value vs data_type ----------------------------------------------
69
+
70
+ const INT_RANGES: Record<string, readonly [number, number]> = {
71
+ int8: [-128, 127],
72
+ int16: [-32768, 32767],
73
+ int32: [-2147483648, 2147483647],
74
+ int64: [-9223372036854775808, 9223372036854775807],
75
+ uint8: [0, 255],
76
+ uint16: [0, 65535],
77
+ uint32: [0, 4294967295],
78
+ uint64: [0, 18446744073709551615],
79
+ };
80
+
81
+ const FLOAT_HEX_DIGITS: Record<string, number> = {
82
+ float16: 4,
83
+ float32: 8,
84
+ float64: 16,
85
+ };
86
+
87
+ /** Hex digit width of each complex type's component floats. */
88
+ const COMPLEX_COMPONENT_HEX_DIGITS: Record<string, number> = {
89
+ complex64: 8,
90
+ complex128: 16,
91
+ };
92
+
93
+ function isFloatFill(value: unknown, hexDigits: number): boolean {
94
+ if (typeof value === "number") return true;
95
+ if (typeof value !== "string") return false;
96
+ if (value === "NaN" || value === "Infinity" || value === "-Infinity") return true;
97
+ return new RegExp(`^0x[0-9a-fA-F]{${hexDigits}}$`).test(value);
98
+ }
99
+
100
+ function floatFillMessage(name: string, hexDigits: number): string {
101
+ return (
102
+ `expected a number, "NaN", "Infinity", "-Infinity", or a ` +
103
+ `${hexDigits}-hex-digit "0x..." string for data type ${JSON.stringify(name)}`
104
+ );
105
+ }
106
+
107
+ /** The fill_value issue for a KNOWN core data type, if any; undefined otherwise. */
108
+ function fillValueIssue(dataTypeName: string, fill: unknown): string | undefined {
109
+ if (dataTypeName === "bool") {
110
+ return typeof fill === "boolean"
111
+ ? undefined
112
+ : 'expected a boolean fill value for data type "bool"';
113
+ }
114
+ const intRange = INT_RANGES[dataTypeName];
115
+ if (intRange !== undefined) {
116
+ const [low, high] = intRange;
117
+ return Number.isInteger(fill) && (fill as number) >= low && (fill as number) <= high
118
+ ? undefined
119
+ : `expected an integer in [${low}, ${high}] for data type ${JSON.stringify(dataTypeName)}`;
120
+ }
121
+ const floatDigits = FLOAT_HEX_DIGITS[dataTypeName];
122
+ if (floatDigits !== undefined) {
123
+ return isFloatFill(fill, floatDigits)
124
+ ? undefined
125
+ : floatFillMessage(dataTypeName, floatDigits);
126
+ }
127
+ const complexDigits = COMPLEX_COMPONENT_HEX_DIGITS[dataTypeName];
128
+ if (complexDigits !== undefined) {
129
+ const ok =
130
+ Array.isArray(fill) &&
131
+ fill.length === 2 &&
132
+ fill.every((part) => isFloatFill(part, complexDigits));
133
+ return ok
134
+ ? undefined
135
+ : `expected a two-element [real, imaginary] array for data type ${JSON.stringify(dataTypeName)}`;
136
+ }
137
+ return undefined; // unrecognized data type: no verdict
138
+ }
139
+
140
+ // --- codec pipelines (transpose / sharding) --------------------------------
141
+
142
+ function isPermutation(order: number[]): boolean {
143
+ const seen = new Set(order);
144
+ return seen.size === order.length && order.every((entry) => entry >= 0 && entry < order.length);
145
+ }
146
+
147
+ /**
148
+ * Walk one codec pipeline. `dims` is the array dimensionality at this point
149
+ * (undefined when unknowable); `outerChunkSizes` holds the distinct chunk
150
+ * lengths per dimension this pipeline may encode — what a sharding codec's
151
+ * inner chunks must divide. A regular grid contributes one size per
152
+ * dimension; a rectilinear grid may contribute several.
153
+ */
154
+ function pipelineIssues(
155
+ pipeline: unknown[],
156
+ path: Path,
157
+ dims: number | undefined,
158
+ outerChunkSizes: number[][] | undefined,
159
+ ): PathedIssue[] {
160
+ const issues: PathedIssue[] = [];
161
+ pipeline.forEach((entry, index) => {
162
+ const parts = fieldParts(entry);
163
+ if (parts === undefined) return;
164
+ const { name, configuration } = parts;
165
+ if (name === "transpose") {
166
+ const order = configuration?.["order"];
167
+ if (!isIntArray(order)) return; // shape errors are the schema layer's
168
+ const orderPath = [...path, index, "configuration", "order"];
169
+ if (!isPermutation(order)) {
170
+ issues.push({
171
+ path: orderPath,
172
+ message: `expected a permutation of the integers 0..${order.length - 1}`,
173
+ kind: "invalid_value",
174
+ });
175
+ }
176
+ if (dims !== undefined && order.length !== dims) {
177
+ issues.push({
178
+ path: orderPath,
179
+ message: `expected one entry per array dimension (${dims})`,
180
+ kind: "invalid_value",
181
+ });
182
+ }
183
+ } else if (name === "sharding_indexed") {
184
+ const chunkShape = configuration?.["chunk_shape"];
185
+ if (!isIntArray(chunkShape)) return;
186
+ const chunkShapePath = [...path, index, "configuration", "chunk_shape"];
187
+ if (dims !== undefined && chunkShape.length !== dims) {
188
+ issues.push({
189
+ path: chunkShapePath,
190
+ message: `expected one length per array dimension (${dims})`,
191
+ kind: "invalid_value",
192
+ });
193
+ } else if (
194
+ outerChunkSizes !== undefined &&
195
+ chunkShape.length === outerChunkSizes.length &&
196
+ chunkShape.every((length) => length > 0)
197
+ ) {
198
+ let violation: { axis: number; size: number } | undefined;
199
+ outerChunkSizes.forEach((sizes, axis) => {
200
+ if (violation !== undefined) return;
201
+ const bad = sizes.find((size) => size % (chunkShape[axis] as number) !== 0);
202
+ if (bad !== undefined) violation = { axis, size: bad };
203
+ });
204
+ if (violation !== undefined) {
205
+ const { axis, size } = violation;
206
+ const uniform = outerChunkSizes.every((sizes) => sizes.length === 1);
207
+ issues.push({
208
+ path: chunkShapePath,
209
+ message: uniform
210
+ ? `expected ${JSON.stringify(chunkShape)} to evenly divide the outer chunk shape ${JSON.stringify(outerChunkSizes.map((sizes) => sizes[0]))}`
211
+ : `expected ${JSON.stringify(chunkShape)} to evenly divide every chunk size of the grid (dimension ${axis} has chunk size ${size})`,
212
+ kind: "invalid_value",
213
+ });
214
+ }
215
+ }
216
+ const inner = configuration?.["codecs"];
217
+ if (Array.isArray(inner)) {
218
+ issues.push(
219
+ ...pipelineIssues(
220
+ inner,
221
+ [...path, index, "configuration", "codecs"],
222
+ dims,
223
+ chunkShape.map((length) => [length]),
224
+ ),
225
+ );
226
+ }
227
+ // index_codecs encode the shard index, whose shape differs from the
228
+ // array's — no dimensional context applies there.
229
+ }
230
+ });
231
+ return issues;
232
+ }
233
+
234
+ /**
235
+ * Every semantic (cross-field) problem in a v3 array metadata document, as
236
+ * an error tree; an empty tree means no rule found a violation. Values that
237
+ * are not v3 array documents yield an empty tree; run the structural
238
+ * validators for structure.
239
+ */
240
+ export function validateArraySemanticsV3(value: unknown): ErrorTree {
241
+ if (!isPlainObject(value) || value["node_type"] !== "array") return treeOf([]);
242
+ const issues: PathedIssue[] = [];
243
+ const shape = isIntArray(value["shape"]) ? value["shape"] : undefined;
244
+ const dims = shape?.length;
245
+
246
+ const grid = fieldParts(value["chunk_grid"]);
247
+ let chunkSizes: number[][] | undefined;
248
+ if (grid?.name === "regular") {
249
+ const configured = grid.configuration?.["chunk_shape"];
250
+ if (isIntArray(configured)) {
251
+ if (dims !== undefined && configured.length !== dims) {
252
+ issues.push({
253
+ path: ["chunk_grid", "configuration", "chunk_shape"],
254
+ message: `expected one length per dimension of shape (${dims})`,
255
+ kind: "invalid_value",
256
+ });
257
+ // wrong arity: unusable as division context
258
+ } else {
259
+ chunkSizes = configured.map((length) => [length]);
260
+ }
261
+ }
262
+ } else if (grid?.name === "rectilinear") {
263
+ const specs = grid.configuration?.["chunk_shapes"];
264
+ if (Array.isArray(specs)) {
265
+ if (dims !== undefined && specs.length !== dims) {
266
+ issues.push({
267
+ path: ["chunk_grid", "configuration", "chunk_shapes"],
268
+ message: `expected one entry per dimension of shape (${dims})`,
269
+ kind: "invalid_value",
270
+ });
271
+ } else {
272
+ const perDim: (number[] | undefined)[] = specs.map((spec, dim) => {
273
+ // Bare integer: uniform shorthand, no sum constraint (edge chunks
274
+ // are permitted, as with the regular grid).
275
+ if (Number.isInteger(spec)) return spec as number > 0 ? [spec as number] : undefined;
276
+ if (!Array.isArray(spec)) return undefined; // registry schema's problem
277
+ const sizes = new Set<number>();
278
+ let total = 0;
279
+ for (const entry of spec) {
280
+ if (Number.isInteger(entry)) {
281
+ sizes.add(entry as number);
282
+ total += entry as number;
283
+ } else if (
284
+ Array.isArray(entry) &&
285
+ entry.length === 2 &&
286
+ Number.isInteger(entry[0]) &&
287
+ Number.isInteger(entry[1])
288
+ ) {
289
+ sizes.add(entry[0] as number);
290
+ total += (entry[0] as number) * (entry[1] as number);
291
+ } else {
292
+ return undefined; // malformed entry: registry schema's problem
293
+ }
294
+ }
295
+ const extent = shape?.[dim];
296
+ if (extent !== undefined && total !== extent) {
297
+ issues.push({
298
+ path: ["chunk_grid", "configuration", "chunk_shapes", dim],
299
+ message: `expected chunk sizes summing to ${extent} along dimension ${dim}, got ${total}`,
300
+ kind: "invalid_value",
301
+ });
302
+ }
303
+ return [...sizes];
304
+ });
305
+ if (perDim.every((sizes) => sizes !== undefined)) {
306
+ chunkSizes = perDim as number[][];
307
+ }
308
+ }
309
+ }
310
+ }
311
+
312
+ const dataType = fieldParts(value["data_type"]);
313
+ if (dataType !== undefined && "fill_value" in value) {
314
+ const message = fillValueIssue(dataType.name, value["fill_value"]);
315
+ if (message !== undefined) {
316
+ issues.push({ path: ["fill_value"], message, kind: "invalid_value" });
317
+ }
318
+ }
319
+
320
+ const codecs = value["codecs"];
321
+ if (Array.isArray(codecs)) {
322
+ issues.push(...pipelineIssues(codecs, ["codecs"], dims, chunkSizes));
323
+ }
324
+ return treeOf(issues);
325
+ }