zarr-metadata 0.2.0 → 0.3.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,36 @@
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
+ * - a `transpose` codec's `order` is a permutation of `0..n-1`, with one
12
+ * entry per array dimension;
13
+ * - a `sharding_indexed` codec's inner `chunk_shape` matches the array's
14
+ * dimensionality and evenly divides the shape of the chunk it shards
15
+ * (the regular grid's chunk at the top level, the parent shard's inner
16
+ * chunk when sharding nests);
17
+ * - `fill_value` has a JSON shape permitted for the named core data type
18
+ * (booleans for `bool`, ranged integers for the int types, numbers /
19
+ * the "NaN"-family sentinels / width-checked "0x…" strings for the
20
+ * float types, two-element arrays for the complex types).
21
+ *
22
+ * Unrecognized names are skipped everywhere — the extension name space is
23
+ * open, and a rule that guessed would lie. This layer has no counterpart
24
+ * in the Python reference implementation (which stops at structure), so it
25
+ * is covered by this package's own tests rather than the conformance
26
+ * corpus.
27
+ */
28
+ import { type ErrorTree } from "./errors.js";
29
+ /**
30
+ * Every semantic (cross-field) problem in a v3 array metadata document, as
31
+ * an error tree; an empty tree means no rule found a violation. Values that
32
+ * are not v3 array documents yield an empty tree; run the structural
33
+ * validators for structure.
34
+ */
35
+ export declare function validateArraySemanticsV3(value: unknown): ErrorTree;
36
+ //# sourceMappingURL=semantics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantics.d.ts","sourceRoot":"","sources":["../src/semantics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAU,KAAK,SAAS,EAAoB,MAAM,aAAa,CAAC;AAqLvE;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAoClE"}
@@ -0,0 +1,228 @@
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
+ * - a `transpose` codec's `order` is a permutation of `0..n-1`, with one
12
+ * entry per array dimension;
13
+ * - a `sharding_indexed` codec's inner `chunk_shape` matches the array's
14
+ * dimensionality and evenly divides the shape of the chunk it shards
15
+ * (the regular grid's chunk at the top level, the parent shard's inner
16
+ * chunk when sharding nests);
17
+ * - `fill_value` has a JSON shape permitted for the named core data type
18
+ * (booleans for `bool`, ranged integers for the int types, numbers /
19
+ * the "NaN"-family sentinels / width-checked "0x…" strings for the
20
+ * float types, two-element arrays for the complex types).
21
+ *
22
+ * Unrecognized names are skipped everywhere — the extension name space is
23
+ * open, and a rule that guessed would lie. This layer has no counterpart
24
+ * in the Python reference implementation (which stops at structure), so it
25
+ * is covered by this package's own tests rather than the conformance
26
+ * corpus.
27
+ */
28
+ import { treeOf } from "./errors.js";
29
+ function isPlainObject(value) {
30
+ if (typeof value !== "object" || value === null || Array.isArray(value))
31
+ return false;
32
+ const proto = Object.getPrototypeOf(value);
33
+ return proto === null || proto === Object.prototype;
34
+ }
35
+ function isIntArray(value) {
36
+ return (Array.isArray(value) &&
37
+ Object.keys(value).length === value.length &&
38
+ value.every((item) => Number.isInteger(item)));
39
+ }
40
+ /** The `{name, configuration}` of a metadata field, when extractable. */
41
+ function fieldParts(field) {
42
+ if (typeof field === "string")
43
+ return { name: field, configuration: undefined };
44
+ if (isPlainObject(field) && typeof field["name"] === "string") {
45
+ const configuration = field["configuration"];
46
+ return {
47
+ name: field["name"],
48
+ configuration: isPlainObject(configuration) ? configuration : undefined,
49
+ };
50
+ }
51
+ return undefined;
52
+ }
53
+ // --- fill_value vs data_type ----------------------------------------------
54
+ const INT_RANGES = {
55
+ int8: [-128, 127],
56
+ int16: [-32768, 32767],
57
+ int32: [-2147483648, 2147483647],
58
+ int64: [-9223372036854775808, 9223372036854775807],
59
+ uint8: [0, 255],
60
+ uint16: [0, 65535],
61
+ uint32: [0, 4294967295],
62
+ uint64: [0, 18446744073709551615],
63
+ };
64
+ const FLOAT_HEX_DIGITS = {
65
+ float16: 4,
66
+ float32: 8,
67
+ float64: 16,
68
+ };
69
+ /** Hex digit width of each complex type's component floats. */
70
+ const COMPLEX_COMPONENT_HEX_DIGITS = {
71
+ complex64: 8,
72
+ complex128: 16,
73
+ };
74
+ function isFloatFill(value, hexDigits) {
75
+ if (typeof value === "number")
76
+ return true;
77
+ if (typeof value !== "string")
78
+ return false;
79
+ if (value === "NaN" || value === "Infinity" || value === "-Infinity")
80
+ return true;
81
+ return new RegExp(`^0x[0-9a-fA-F]{${hexDigits}}$`).test(value);
82
+ }
83
+ function floatFillMessage(name, hexDigits) {
84
+ return (`expected a number, "NaN", "Infinity", "-Infinity", or a ` +
85
+ `${hexDigits}-hex-digit "0x..." string for data type ${JSON.stringify(name)}`);
86
+ }
87
+ /** The fill_value issue for a KNOWN core data type, if any; undefined otherwise. */
88
+ function fillValueIssue(dataTypeName, fill) {
89
+ if (dataTypeName === "bool") {
90
+ return typeof fill === "boolean"
91
+ ? undefined
92
+ : 'expected a boolean fill value for data type "bool"';
93
+ }
94
+ const intRange = INT_RANGES[dataTypeName];
95
+ if (intRange !== undefined) {
96
+ const [low, high] = intRange;
97
+ return Number.isInteger(fill) && fill >= low && fill <= high
98
+ ? undefined
99
+ : `expected an integer in [${low}, ${high}] for data type ${JSON.stringify(dataTypeName)}`;
100
+ }
101
+ const floatDigits = FLOAT_HEX_DIGITS[dataTypeName];
102
+ if (floatDigits !== undefined) {
103
+ return isFloatFill(fill, floatDigits)
104
+ ? undefined
105
+ : floatFillMessage(dataTypeName, floatDigits);
106
+ }
107
+ const complexDigits = COMPLEX_COMPONENT_HEX_DIGITS[dataTypeName];
108
+ if (complexDigits !== undefined) {
109
+ const ok = Array.isArray(fill) &&
110
+ fill.length === 2 &&
111
+ fill.every((part) => isFloatFill(part, complexDigits));
112
+ return ok
113
+ ? undefined
114
+ : `expected a two-element [real, imaginary] array for data type ${JSON.stringify(dataTypeName)}`;
115
+ }
116
+ return undefined; // unrecognized data type: no verdict
117
+ }
118
+ // --- codec pipelines (transpose / sharding) --------------------------------
119
+ function isPermutation(order) {
120
+ const seen = new Set(order);
121
+ return seen.size === order.length && order.every((entry) => entry >= 0 && entry < order.length);
122
+ }
123
+ /**
124
+ * Walk one codec pipeline. `dims` is the array dimensionality at this point
125
+ * (undefined when unknowable); `outerChunkShape` is the shape of the chunk
126
+ * this pipeline encodes — what a sharding codec's inner chunks must divide.
127
+ */
128
+ function pipelineIssues(pipeline, path, dims, outerChunkShape) {
129
+ const issues = [];
130
+ pipeline.forEach((entry, index) => {
131
+ const parts = fieldParts(entry);
132
+ if (parts === undefined)
133
+ return;
134
+ const { name, configuration } = parts;
135
+ if (name === "transpose") {
136
+ const order = configuration?.["order"];
137
+ if (!isIntArray(order))
138
+ return; // shape errors are the schema layer's
139
+ const orderPath = [...path, index, "configuration", "order"];
140
+ if (!isPermutation(order)) {
141
+ issues.push({
142
+ path: orderPath,
143
+ message: `expected a permutation of the integers 0..${order.length - 1}`,
144
+ kind: "invalid_value",
145
+ });
146
+ }
147
+ if (dims !== undefined && order.length !== dims) {
148
+ issues.push({
149
+ path: orderPath,
150
+ message: `expected one entry per array dimension (${dims})`,
151
+ kind: "invalid_value",
152
+ });
153
+ }
154
+ }
155
+ else if (name === "sharding_indexed") {
156
+ const chunkShape = configuration?.["chunk_shape"];
157
+ if (!isIntArray(chunkShape))
158
+ return;
159
+ const chunkShapePath = [...path, index, "configuration", "chunk_shape"];
160
+ if (dims !== undefined && chunkShape.length !== dims) {
161
+ issues.push({
162
+ path: chunkShapePath,
163
+ message: `expected one length per array dimension (${dims})`,
164
+ kind: "invalid_value",
165
+ });
166
+ }
167
+ else if (outerChunkShape !== undefined &&
168
+ chunkShape.length === outerChunkShape.length &&
169
+ chunkShape.every((length) => length > 0) &&
170
+ outerChunkShape.some((outer, axis) => outer % chunkShape[axis] !== 0)) {
171
+ issues.push({
172
+ path: chunkShapePath,
173
+ message: `expected ${JSON.stringify(chunkShape)} to evenly divide the outer chunk shape ${JSON.stringify(outerChunkShape)}`,
174
+ kind: "invalid_value",
175
+ });
176
+ }
177
+ const inner = configuration?.["codecs"];
178
+ if (Array.isArray(inner)) {
179
+ issues.push(...pipelineIssues(inner, [...path, index, "configuration", "codecs"], dims, chunkShape));
180
+ }
181
+ // index_codecs encode the shard index, whose shape differs from the
182
+ // array's — no dimensional context applies there.
183
+ }
184
+ });
185
+ return issues;
186
+ }
187
+ /**
188
+ * Every semantic (cross-field) problem in a v3 array metadata document, as
189
+ * an error tree; an empty tree means no rule found a violation. Values that
190
+ * are not v3 array documents yield an empty tree; run the structural
191
+ * validators for structure.
192
+ */
193
+ export function validateArraySemanticsV3(value) {
194
+ if (!isPlainObject(value) || value["node_type"] !== "array")
195
+ return treeOf([]);
196
+ const issues = [];
197
+ const shape = isIntArray(value["shape"]) ? value["shape"] : undefined;
198
+ const dims = shape?.length;
199
+ const grid = fieldParts(value["chunk_grid"]);
200
+ let chunkShape;
201
+ if (grid?.name === "regular") {
202
+ const configured = grid.configuration?.["chunk_shape"];
203
+ if (isIntArray(configured)) {
204
+ chunkShape = configured;
205
+ if (dims !== undefined && configured.length !== dims) {
206
+ issues.push({
207
+ path: ["chunk_grid", "configuration", "chunk_shape"],
208
+ message: `expected one length per dimension of shape (${dims})`,
209
+ kind: "invalid_value",
210
+ });
211
+ chunkShape = undefined; // wrong arity: unusable as division context
212
+ }
213
+ }
214
+ }
215
+ const dataType = fieldParts(value["data_type"]);
216
+ if (dataType !== undefined && "fill_value" in value) {
217
+ const message = fillValueIssue(dataType.name, value["fill_value"]);
218
+ if (message !== undefined) {
219
+ issues.push({ path: ["fill_value"], message, kind: "invalid_value" });
220
+ }
221
+ }
222
+ const codecs = value["codecs"];
223
+ if (Array.isArray(codecs)) {
224
+ issues.push(...pipelineIssues(codecs, ["codecs"], dims, chunkShape));
225
+ }
226
+ return treeOf(issues);
227
+ }
228
+ //# 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.3.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,252 @@
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
+ * - a `transpose` codec's `order` is a permutation of `0..n-1`, with one
12
+ * entry per array dimension;
13
+ * - a `sharding_indexed` codec's inner `chunk_shape` matches the array's
14
+ * dimensionality and evenly divides the shape of the chunk it shards
15
+ * (the regular grid's chunk at the top level, the parent shard's inner
16
+ * chunk when sharding nests);
17
+ * - `fill_value` has a JSON shape permitted for the named core data type
18
+ * (booleans for `bool`, ranged integers for the int types, numbers /
19
+ * the "NaN"-family sentinels / width-checked "0x…" strings for the
20
+ * float types, two-element arrays for the complex types).
21
+ *
22
+ * Unrecognized names are skipped everywhere — the extension name space is
23
+ * open, and a rule that guessed would lie. This layer has no counterpart
24
+ * in the Python reference implementation (which stops at structure), so it
25
+ * is covered by this package's own tests rather than the conformance
26
+ * corpus.
27
+ */
28
+
29
+ import { treeOf, type ErrorTree, type PathedIssue } from "./errors.js";
30
+
31
+ type Path = ReadonlyArray<string | number>;
32
+
33
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
34
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
35
+ const proto: unknown = Object.getPrototypeOf(value);
36
+ return proto === null || proto === Object.prototype;
37
+ }
38
+
39
+ function isIntArray(value: unknown): value is number[] {
40
+ return (
41
+ Array.isArray(value) &&
42
+ Object.keys(value).length === value.length &&
43
+ value.every((item) => Number.isInteger(item))
44
+ );
45
+ }
46
+
47
+ /** The `{name, configuration}` of a metadata field, when extractable. */
48
+ function fieldParts(
49
+ field: unknown,
50
+ ): { name: string; configuration: Record<string, unknown> | undefined } | undefined {
51
+ if (typeof field === "string") return { name: field, configuration: undefined };
52
+ if (isPlainObject(field) && typeof field["name"] === "string") {
53
+ const configuration = field["configuration"];
54
+ return {
55
+ name: field["name"],
56
+ configuration: isPlainObject(configuration) ? configuration : undefined,
57
+ };
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ // --- fill_value vs data_type ----------------------------------------------
63
+
64
+ const INT_RANGES: Record<string, readonly [number, number]> = {
65
+ int8: [-128, 127],
66
+ int16: [-32768, 32767],
67
+ int32: [-2147483648, 2147483647],
68
+ int64: [-9223372036854775808, 9223372036854775807],
69
+ uint8: [0, 255],
70
+ uint16: [0, 65535],
71
+ uint32: [0, 4294967295],
72
+ uint64: [0, 18446744073709551615],
73
+ };
74
+
75
+ const FLOAT_HEX_DIGITS: Record<string, number> = {
76
+ float16: 4,
77
+ float32: 8,
78
+ float64: 16,
79
+ };
80
+
81
+ /** Hex digit width of each complex type's component floats. */
82
+ const COMPLEX_COMPONENT_HEX_DIGITS: Record<string, number> = {
83
+ complex64: 8,
84
+ complex128: 16,
85
+ };
86
+
87
+ function isFloatFill(value: unknown, hexDigits: number): boolean {
88
+ if (typeof value === "number") return true;
89
+ if (typeof value !== "string") return false;
90
+ if (value === "NaN" || value === "Infinity" || value === "-Infinity") return true;
91
+ return new RegExp(`^0x[0-9a-fA-F]{${hexDigits}}$`).test(value);
92
+ }
93
+
94
+ function floatFillMessage(name: string, hexDigits: number): string {
95
+ return (
96
+ `expected a number, "NaN", "Infinity", "-Infinity", or a ` +
97
+ `${hexDigits}-hex-digit "0x..." string for data type ${JSON.stringify(name)}`
98
+ );
99
+ }
100
+
101
+ /** The fill_value issue for a KNOWN core data type, if any; undefined otherwise. */
102
+ function fillValueIssue(dataTypeName: string, fill: unknown): string | undefined {
103
+ if (dataTypeName === "bool") {
104
+ return typeof fill === "boolean"
105
+ ? undefined
106
+ : 'expected a boolean fill value for data type "bool"';
107
+ }
108
+ const intRange = INT_RANGES[dataTypeName];
109
+ if (intRange !== undefined) {
110
+ const [low, high] = intRange;
111
+ return Number.isInteger(fill) && (fill as number) >= low && (fill as number) <= high
112
+ ? undefined
113
+ : `expected an integer in [${low}, ${high}] for data type ${JSON.stringify(dataTypeName)}`;
114
+ }
115
+ const floatDigits = FLOAT_HEX_DIGITS[dataTypeName];
116
+ if (floatDigits !== undefined) {
117
+ return isFloatFill(fill, floatDigits)
118
+ ? undefined
119
+ : floatFillMessage(dataTypeName, floatDigits);
120
+ }
121
+ const complexDigits = COMPLEX_COMPONENT_HEX_DIGITS[dataTypeName];
122
+ if (complexDigits !== undefined) {
123
+ const ok =
124
+ Array.isArray(fill) &&
125
+ fill.length === 2 &&
126
+ fill.every((part) => isFloatFill(part, complexDigits));
127
+ return ok
128
+ ? undefined
129
+ : `expected a two-element [real, imaginary] array for data type ${JSON.stringify(dataTypeName)}`;
130
+ }
131
+ return undefined; // unrecognized data type: no verdict
132
+ }
133
+
134
+ // --- codec pipelines (transpose / sharding) --------------------------------
135
+
136
+ function isPermutation(order: number[]): boolean {
137
+ const seen = new Set(order);
138
+ return seen.size === order.length && order.every((entry) => entry >= 0 && entry < order.length);
139
+ }
140
+
141
+ /**
142
+ * Walk one codec pipeline. `dims` is the array dimensionality at this point
143
+ * (undefined when unknowable); `outerChunkShape` is the shape of the chunk
144
+ * this pipeline encodes — what a sharding codec's inner chunks must divide.
145
+ */
146
+ function pipelineIssues(
147
+ pipeline: unknown[],
148
+ path: Path,
149
+ dims: number | undefined,
150
+ outerChunkShape: number[] | undefined,
151
+ ): PathedIssue[] {
152
+ const issues: PathedIssue[] = [];
153
+ pipeline.forEach((entry, index) => {
154
+ const parts = fieldParts(entry);
155
+ if (parts === undefined) return;
156
+ const { name, configuration } = parts;
157
+ if (name === "transpose") {
158
+ const order = configuration?.["order"];
159
+ if (!isIntArray(order)) return; // shape errors are the schema layer's
160
+ const orderPath = [...path, index, "configuration", "order"];
161
+ if (!isPermutation(order)) {
162
+ issues.push({
163
+ path: orderPath,
164
+ message: `expected a permutation of the integers 0..${order.length - 1}`,
165
+ kind: "invalid_value",
166
+ });
167
+ }
168
+ if (dims !== undefined && order.length !== dims) {
169
+ issues.push({
170
+ path: orderPath,
171
+ message: `expected one entry per array dimension (${dims})`,
172
+ kind: "invalid_value",
173
+ });
174
+ }
175
+ } else if (name === "sharding_indexed") {
176
+ const chunkShape = configuration?.["chunk_shape"];
177
+ if (!isIntArray(chunkShape)) return;
178
+ const chunkShapePath = [...path, index, "configuration", "chunk_shape"];
179
+ if (dims !== undefined && chunkShape.length !== dims) {
180
+ issues.push({
181
+ path: chunkShapePath,
182
+ message: `expected one length per array dimension (${dims})`,
183
+ kind: "invalid_value",
184
+ });
185
+ } else if (
186
+ outerChunkShape !== undefined &&
187
+ chunkShape.length === outerChunkShape.length &&
188
+ chunkShape.every((length) => length > 0) &&
189
+ outerChunkShape.some((outer, axis) => outer % (chunkShape[axis] as number) !== 0)
190
+ ) {
191
+ issues.push({
192
+ path: chunkShapePath,
193
+ message: `expected ${JSON.stringify(chunkShape)} to evenly divide the outer chunk shape ${JSON.stringify(outerChunkShape)}`,
194
+ kind: "invalid_value",
195
+ });
196
+ }
197
+ const inner = configuration?.["codecs"];
198
+ if (Array.isArray(inner)) {
199
+ issues.push(
200
+ ...pipelineIssues(inner, [...path, index, "configuration", "codecs"], dims, chunkShape),
201
+ );
202
+ }
203
+ // index_codecs encode the shard index, whose shape differs from the
204
+ // array's — no dimensional context applies there.
205
+ }
206
+ });
207
+ return issues;
208
+ }
209
+
210
+ /**
211
+ * Every semantic (cross-field) problem in a v3 array metadata document, as
212
+ * an error tree; an empty tree means no rule found a violation. Values that
213
+ * are not v3 array documents yield an empty tree; run the structural
214
+ * validators for structure.
215
+ */
216
+ export function validateArraySemanticsV3(value: unknown): ErrorTree {
217
+ if (!isPlainObject(value) || value["node_type"] !== "array") return treeOf([]);
218
+ const issues: PathedIssue[] = [];
219
+ const shape = isIntArray(value["shape"]) ? value["shape"] : undefined;
220
+ const dims = shape?.length;
221
+
222
+ const grid = fieldParts(value["chunk_grid"]);
223
+ let chunkShape: number[] | undefined;
224
+ if (grid?.name === "regular") {
225
+ const configured = grid.configuration?.["chunk_shape"];
226
+ if (isIntArray(configured)) {
227
+ chunkShape = configured;
228
+ if (dims !== undefined && configured.length !== dims) {
229
+ issues.push({
230
+ path: ["chunk_grid", "configuration", "chunk_shape"],
231
+ message: `expected one length per dimension of shape (${dims})`,
232
+ kind: "invalid_value",
233
+ });
234
+ chunkShape = undefined; // wrong arity: unusable as division context
235
+ }
236
+ }
237
+ }
238
+
239
+ const dataType = fieldParts(value["data_type"]);
240
+ if (dataType !== undefined && "fill_value" in value) {
241
+ const message = fillValueIssue(dataType.name, value["fill_value"]);
242
+ if (message !== undefined) {
243
+ issues.push({ path: ["fill_value"], message, kind: "invalid_value" });
244
+ }
245
+ }
246
+
247
+ const codecs = value["codecs"];
248
+ if (Array.isArray(codecs)) {
249
+ issues.push(...pipelineIssues(codecs, ["codecs"], dims, chunkShape));
250
+ }
251
+ return treeOf(issues);
252
+ }