@mark1russell7/layout-contract 0.2.11 → 0.2.13

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/build/index.js CHANGED
@@ -110,6 +110,134 @@ function overlapOf(a, b) {
110
110
  y: Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y)
111
111
  };
112
112
  }
113
+ // src/shapes.ts
114
+ /** The declared shape kinds, in their order. Only `rect` has all of its geometry. The other kinds have their bounding box only. */
115
+ const SHAPE_KINDS = Object.freeze([
116
+ {
117
+ id: "rect",
118
+ fields: ["w", "h"],
119
+ regions: ["rect"],
120
+ geometry: {
121
+ bounds: true,
122
+ area: true,
123
+ contains: true,
124
+ overlap: true
125
+ }
126
+ },
127
+ {
128
+ id: "polygon",
129
+ fields: ["points", "holes?"],
130
+ regions: ["polygon"],
131
+ geometry: {
132
+ bounds: true,
133
+ area: false,
134
+ contains: false,
135
+ overlap: false
136
+ }
137
+ },
138
+ {
139
+ id: "circle",
140
+ fields: [
141
+ "cx",
142
+ "cy",
143
+ "r"
144
+ ],
145
+ regions: ["circle"],
146
+ geometry: {
147
+ bounds: true,
148
+ area: false,
149
+ contains: false,
150
+ overlap: false
151
+ }
152
+ },
153
+ {
154
+ id: "free",
155
+ fields: [
156
+ "w",
157
+ "h",
158
+ "shape?"
159
+ ],
160
+ regions: [
161
+ "rect",
162
+ "polygon",
163
+ "circle",
164
+ "ellipse"
165
+ ],
166
+ geometry: {
167
+ bounds: true,
168
+ area: false,
169
+ contains: false,
170
+ overlap: false
171
+ }
172
+ }
173
+ ]);
174
+ /** The ids of `SHAPE_KINDS`, in their order. */
175
+ const SHAPE_KIND_IDS = Object.freeze(SHAPE_KINDS.map((k) => k.id));
176
+ /** The shape kinds of a measure or a rule without `shapes`. The contract measured only rectangles before L46. */
177
+ const DEFAULT_SHAPES = Object.freeze(["rect"]);
178
+ const REGION_KIND = {
179
+ rect: "rect",
180
+ polygon: "polygon",
181
+ circle: "circle",
182
+ ellipse: "free"
183
+ };
184
+ /** This function tells if a value is a declared shape kind. */
185
+ function isShapeKind(value) {
186
+ return typeof value === "string" && SHAPE_KIND_IDS.includes(value);
187
+ }
188
+ /**
189
+ * This function gives the shape kind of a box. A box with `shapeKind` has that kind.
190
+ * Without it, the kind comes from `shape`: no shape is a `rect`, and an ellipse is `free`.
191
+ */
192
+ function shapeKindOf(box) {
193
+ if (box.shapeKind !== void 0) return box.shapeKind;
194
+ return box.shape === void 0 ? "rect" : REGION_KIND[box.shape.kind];
195
+ }
196
+ /** This function gives the shape kinds of the boxes of an input, in the order of `SHAPE_KINDS`. An input without boxes gives an empty list. */
197
+ function shapeKindsOf(input) {
198
+ const seen = new Set(input.boxes.map(shapeKindOf));
199
+ return SHAPE_KIND_IDS.filter((k) => seen.has(k));
200
+ }
201
+ /** This function gives the value of the cell dimension `itemShape` of an input: its one kind, `mixed` for more kinds, or `rect` without boxes. */
202
+ function itemShapeOf(input) {
203
+ const kinds = shapeKindsOf(input);
204
+ return kinds.length === 0 ? "rect" : kinds.length === 1 ? kinds[0] : "mixed";
205
+ }
206
+ /** This function gives the problem of the shape kind of a box, or `undefined` when `shapeKind` and `shape` agree. */
207
+ function shapeKindProblem(box) {
208
+ const k = box.shapeKind;
209
+ if (k === void 0) return void 0;
210
+ const spec = SHAPE_KINDS.find((s) => s.id === k);
211
+ if (spec === void 0) return `box ${box.id}: the shape kind ${String(k)} is not a declared kind`;
212
+ if (box.shape === void 0) return k === "rect" || k === "free" ? void 0 : `box ${box.id}: the shape kind ${k} has no shape`;
213
+ if (!spec.regions.includes(box.shape.kind)) return `box ${box.id}: the shape kind ${k} does not agree with a shape of the kind ${box.shape.kind}`;
214
+ }
215
+ /** This function gives the shape kinds that a measure or a rule supports. */
216
+ function shapesOf(use) {
217
+ return use.shapes ?? DEFAULT_SHAPES;
218
+ }
219
+ /**
220
+ * This function checks that each measure and each rule of a run supports each shape kind of the input. An empty list tells that the run is good.
221
+ * Each break has the claim `shape`, the id of the input, and the first 5 ids of the boxes of the kind.
222
+ */
223
+ function checkShapes(input, uses) {
224
+ const kinds = shapeKindsOf(input);
225
+ const out = [];
226
+ for (const use of uses) {
227
+ const shapes = shapesOf(use);
228
+ for (const kind of kinds) {
229
+ if (shapes.includes(kind)) continue;
230
+ const ids = input.boxes.filter((b) => shapeKindOf(b) === kind).map((b) => b.id);
231
+ const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, and ${ids.length - 5} more` : "");
232
+ out.push({
233
+ claim: "shape",
234
+ detail: `${use.id} supports only the shape kinds ${shapes.join(", ") || "(none)"}, but the input ${input.id} has ${kind} boxes: ${shown}`,
235
+ inputs: [input.id]
236
+ });
237
+ }
238
+ }
239
+ return out;
240
+ }
113
241
  // src/tolerance.ts
114
242
  /** The epsilon for the float compares in an engine. */
115
243
  const EPS = 1e-6;
@@ -256,8 +384,13 @@ function applyPack(input, output) {
256
384
  };
257
385
  }
258
386
  // src/record.ts
259
- /** This function gives the record of a run. The input gives its id, its hash, its set and the counts of the nodes and the edges. */
387
+ /**
388
+ * This function gives the record of a run. The input gives its id, its hash, its set and the counts of the nodes and the edges.
389
+ * The input also gives `shapes`. An input with only `rect` boxes gets no `shapes`, so its record does not change.
390
+ */
260
391
  function toLayoutRecord(input, run) {
392
+ const shapes = shapeKindsOf(input);
393
+ const onlyRect = shapes.length === 0 || shapes.length === 1 && shapes[0] === "rect";
261
394
  return {
262
395
  schema: "layout-record/1",
263
396
  ...run,
@@ -265,7 +398,8 @@ function toLayoutRecord(input, run) {
265
398
  inputHash: input.hash ?? "",
266
399
  set: input.set ?? "",
267
400
  nodes: input.boxes.length,
268
- edges: input.edges?.length ?? 0
401
+ edges: input.edges?.length ?? 0,
402
+ ...onlyRect ? {} : { shapes }
269
403
  };
270
404
  }
271
405
  // src/rules.ts
@@ -359,6 +493,29 @@ function countPortsOffBorder(boxes, eps = RECORD_EPS) {
359
493
  }
360
494
  return count;
361
495
  }
496
+ /**
497
+ * The hard rules of a pack, with the shape kinds that each rule supports.
498
+ * `no-overlap` and `in-container` test the bounding box of a shape. That count is exact only for a `rect`.
499
+ * `locked-kept` and `complete` read only the positions and the ids, so they support each kind.
500
+ */
501
+ const PACK_HARD_RULES = Object.freeze([
502
+ {
503
+ id: "no-overlap",
504
+ shapes: ["rect"]
505
+ },
506
+ {
507
+ id: "in-container",
508
+ shapes: ["rect"]
509
+ },
510
+ {
511
+ id: "locked-kept",
512
+ shapes: SHAPE_KIND_IDS
513
+ },
514
+ {
515
+ id: "complete",
516
+ shapes: SHAPE_KIND_IDS
517
+ }
518
+ ]);
362
519
  /** This function tells if the inside of `region` and the inside of `rect` have a common point. */
363
520
  function regionHitsRect(region, rect) {
364
521
  if (rect.w <= 0 || rect.h <= 0) return false;
@@ -561,13 +718,22 @@ function checkInput(input) {
561
718
  ids.add(box.id);
562
719
  if (!(box.w > 0 && box.h > 0 && Number.isFinite(box.w) && Number.isFinite(box.h))) problems.push(`box ${box.id}: the size is not positive and finite`);
563
720
  if (box.locked === true && (box.x === void 0 || box.y === void 0)) problems.push(`box ${box.id}: a locked box has no position`);
721
+ const shape = shapeKindProblem(box);
722
+ if (shape !== void 0) problems.push(shape);
564
723
  }
565
724
  if (countPortsOffBorder(input.boxes) > 0) problems.push("a fixed port is not on the border of its box");
566
725
  for (const edge of input.edges ?? []) if (!ids.has(edge.source) || !ids.has(edge.target)) problems.push(`edge ${edge.id}: a box of the edge is not in the input`);
567
726
  return problems;
568
727
  }
569
728
  // src/measures.ts
570
- /** The specs of the joint measures, in the order of `docs/JOINT-MEASURES.md`. */
729
+ /** The shape kinds of a joint measure that reads only the bounding boxes, the centers and the positions of the boxes. */
730
+ const BOUNDS_ONLY = SHAPE_KIND_IDS;
731
+ /** The shape kinds of a joint measure that reads the area `w` by `h` of each box. */
732
+ const AREA = ["rect"];
733
+ /**
734
+ * The specs of the joint measures, in the order of `docs/JOINT-MEASURES.md`.
735
+ * `joint.fill` and `joint.straight.length` read the area of each box, so they support only `rect`. `docs/SHAPES.md` tells the reasons.
736
+ */
571
737
  const JOINT_MEASURES = Object.freeze([
572
738
  {
573
739
  id: "joint.fill",
@@ -577,7 +743,8 @@ const JOINT_MEASURES = Object.freeze([
577
743
  min: 0,
578
744
  max: 1,
579
745
  floor: 1,
580
- role: "guard"
746
+ role: "guard",
747
+ shapes: AREA
581
748
  },
582
749
  {
583
750
  id: "joint.aspectError",
@@ -588,7 +755,8 @@ const JOINT_MEASURES = Object.freeze([
588
755
  max: Infinity,
589
756
  floor: 0,
590
757
  tol: .01,
591
- role: "guard"
758
+ role: "guard",
759
+ shapes: BOUNDS_ONLY
592
760
  },
593
761
  {
594
762
  id: "joint.spill",
@@ -599,7 +767,8 @@ const JOINT_MEASURES = Object.freeze([
599
767
  max: 1,
600
768
  floor: 0,
601
769
  tol: .001,
602
- role: "guard"
770
+ role: "guard",
771
+ shapes: BOUNDS_ONLY
603
772
  },
604
773
  {
605
774
  id: "joint.straight.crossings",
@@ -610,7 +779,8 @@ const JOINT_MEASURES = Object.freeze([
610
779
  max: Infinity,
611
780
  floor: 0,
612
781
  tol: 0,
613
- role: "guard"
782
+ role: "guard",
783
+ shapes: BOUNDS_ONLY
614
784
  },
615
785
  {
616
786
  id: "joint.straight.length",
@@ -621,7 +791,8 @@ const JOINT_MEASURES = Object.freeze([
621
791
  max: Infinity,
622
792
  floor: 0,
623
793
  tol: .01,
624
- role: "guard"
794
+ role: "guard",
795
+ shapes: AREA
625
796
  },
626
797
  {
627
798
  id: "joint.stability.mean",
@@ -632,7 +803,8 @@ const JOINT_MEASURES = Object.freeze([
632
803
  max: Infinity,
633
804
  floor: 0,
634
805
  tol: .005,
635
- role: "change-guard"
806
+ role: "change-guard",
807
+ shapes: BOUNDS_ONLY
636
808
  },
637
809
  {
638
810
  id: "joint.stability.moved",
@@ -643,7 +815,8 @@ const JOINT_MEASURES = Object.freeze([
643
815
  max: 1,
644
816
  floor: 0,
645
817
  tol: .02,
646
- role: "change-guard"
818
+ role: "change-guard",
819
+ shapes: BOUNDS_ONLY
647
820
  },
648
821
  {
649
822
  id: "joint.proxy.rank",
@@ -651,7 +824,8 @@ const JOINT_MEASURES = Object.freeze([
651
824
  direction: "higher",
652
825
  min: -1,
653
826
  max: 1,
654
- role: "diagnostic"
827
+ role: "diagnostic",
828
+ shapes: BOUNDS_ONLY
655
829
  },
656
830
  {
657
831
  id: "joint.ms",
@@ -659,7 +833,8 @@ const JOINT_MEASURES = Object.freeze([
659
833
  direction: "lower",
660
834
  min: 0,
661
835
  max: Infinity,
662
- role: "time"
836
+ role: "time",
837
+ shapes: BOUNDS_ONLY
663
838
  }
664
839
  ]);
665
840
  /** The threshold of `joint.stability.moved`, in widths `W` of the base. */
@@ -1764,12 +1939,28 @@ function optionProblems(stage, o) {
1764
1939
  if (problem !== void 0) out.push(["when", `${pat} has a when value that is not a value of ${p.when.param}: ${problem}`]);
1765
1940
  }
1766
1941
  });
1942
+ if (o.supports !== void 0) out.push(...supportsProblems(o.supports, at));
1767
1943
  if (o.members !== void 0) {
1768
1944
  const { min, max, ordered } = o.members;
1769
1945
  if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || min > max || typeof ordered !== "boolean") out.push(["members", `${at} has a member rule without whole counts 1 <= min <= max and a boolean ordered`]);
1770
1946
  }
1771
1947
  return out;
1772
1948
  }
1949
+ /** This function gives the problems of the `supports` of an option. */
1950
+ function supportsProblems(supports, at) {
1951
+ if (!isObject(supports)) return [["shape", `the supports of ${at} are not an object`]];
1952
+ const shape = supports["shape"];
1953
+ if (shape === void 0) return [];
1954
+ if (!Array.isArray(shape) || shape.length === 0) return [["shape", `${at} supports no shape kind. Remove shape to support each kind`]];
1955
+ const out = [];
1956
+ shape.forEach((k, i) => {
1957
+ if (!isShapeKind(k)) out.push(["shape", `${at} supports the shape kind ${show(k)}, which is not one of ${SHAPE_KIND_IDS.join(", ")}`]);
1958
+ else if (shape.indexOf(k) !== i) out.push(["duplicate", `${at} has the shape kind ${k} two times`]);
1959
+ });
1960
+ return out;
1961
+ }
1962
+ /** This function tells if an option supports each shape kind of the list. */
1963
+ const supportsShapes = (o, shapes) => shapes === void 0 || o.supports?.shape === void 0 || shapes.every((k) => o.supports.shape.includes(k));
1773
1964
  /** This function gives the problems of the params of one stage choice. */
1774
1965
  function choiceParamProblems(o, params, at) {
1775
1966
  if (params !== void 0 && !isObject(params)) return [["config", `the params of ${at} are not an object`]];
@@ -1791,7 +1982,7 @@ function choiceParamProblems(o, params, at) {
1791
1982
  return out;
1792
1983
  }
1793
1984
  /** This function gives the problems of a config of a space. A member of a meta option has `member: true`. */
1794
- function configProblems(space, config, member) {
1985
+ function configProblems(space, config, member, shapes) {
1795
1986
  if (!isObject(config) || !isObject(config["stages"])) return [["config", "the config is not an object with stages"]];
1796
1987
  const out = [];
1797
1988
  const stages = config["stages"];
@@ -1829,6 +2020,10 @@ function configProblems(space, config, member) {
1829
2020
  }
1830
2021
  chosen.set(s.name, o.id);
1831
2022
  const at = `the option ${s.name}.${o.id}`;
2023
+ if (!supportsShapes(o, shapes)) {
2024
+ const missing = shapes.filter((k) => !o.supports.shape.includes(k));
2025
+ out.push(["shape", `${at} supports the shape kinds ${o.supports.shape.join(", ")}, not ${missing.join(", ")}`]);
2026
+ }
1832
2027
  if (meta !== void 0 && meta !== s.name) out.push(["meta", `the stage ${s.name} is set, but the meta option of ${meta} replaces the other stages of its part`]);
1833
2028
  out.push(...choiceParamProblems(o, c["params"], at));
1834
2029
  if (o.members === void 0) {
@@ -1839,19 +2034,19 @@ function configProblems(space, config, member) {
1839
2034
  out.push(["depth", `a member has the meta option ${s.name}.${o.id}. The depth of a meta config is 1`]);
1840
2035
  continue;
1841
2036
  }
1842
- out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at));
2037
+ out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at, shapes));
1843
2038
  }
1844
2039
  for (const c of space.constraints ?? []) if (!holds(c, chosen)) out.push(["constraint", `the config breaks the constraint ${describe(c)}`]);
1845
2040
  return out;
1846
2041
  }
1847
2042
  /** This function gives the problems of the members of a meta option. `part` is the space of the members. */
1848
- function memberProblems(part, rule, members, at) {
2043
+ function memberProblems(part, rule, members, at, shapes) {
1849
2044
  if (!Array.isArray(members)) return [["members", `${at} is a meta option without a list of members`]];
1850
2045
  const out = [];
1851
2046
  if (members.length < rule.min || members.length > rule.max) out.push(["members", `${at} has ${members.length} members, not ${rule.min} to ${rule.max}`]);
1852
2047
  const ids = /* @__PURE__ */ new Map();
1853
2048
  members.forEach((m, k) => {
1854
- const inner = configProblems(part, m, true);
2049
+ const inner = configProblems(part, m, true, shapes);
1855
2050
  for (const [claim, detail] of inner) out.push([claim, `the member ${k} of ${at}: ${detail}`]);
1856
2051
  if (inner.length > 0) return;
1857
2052
  const id = idOf(canonicalOf(part, m, true));
@@ -1868,9 +2063,10 @@ const toBreaks = (problems, inputs = []) => problems.map(([claim, detail]) => ({
1868
2063
  /**
1869
2064
  * This function checks one config of a space. It gives one break for each problem. An empty list tells that the config is good.
1870
2065
  * The `claim` of a break is the kind of the problem, for example `missing`, `unknown`, `inactive`, `param`, `constraint`, `members` or `depth`.
2066
+ * With `shapes`, an option that does not support a kind of the list gives the claim `shape`. A member of a meta option gets the same check.
1871
2067
  */
1872
- function checkConfig(space, config) {
1873
- return toBreaks(configProblems(space, config, false));
2068
+ function checkConfig(space, config, options = {}) {
2069
+ return toBreaks(configProblems(space, config, false, options.shapes));
1874
2070
  }
1875
2071
  /**
1876
2072
  * This function checks a space. It gives one break for each problem. An empty list tells that the space is good.
@@ -2136,14 +2332,40 @@ function combos(space, levels, meta, limit) {
2136
2332
  /**
2137
2333
  * This function gives the count of the grid points of a space, with `levels` levels for each numeric param.
2138
2334
  * It counts only the active params and the combinations that keep the constraints. It gives the meta configs apart.
2335
+ * With `shapes`, it counts only the options that support each kind of the list.
2139
2336
  */
2140
2337
  function spaceSize(space, options) {
2141
2338
  assertSpace(space);
2142
2339
  assertWhole(options.levels, "levels", 1);
2143
- const limit = options.maxMeta ?? 10n ** 18n;
2340
+ assertShapes(options.shapes);
2341
+ return sizeOf(restrict(space, options.shapes), options.levels, options.maxMeta ?? 10n ** 18n);
2342
+ }
2343
+ /**
2344
+ * This function gives the space with only the options that support each shape kind of `shapes`. Without `shapes`, it gives the same space.
2345
+ * A stage can lose all of its options. Then the space has no config with that stage.
2346
+ */
2347
+ function restrict(space, shapes) {
2348
+ if (shapes === void 0) return space;
2349
+ return {
2350
+ ...space,
2351
+ stages: space.stages.map((s) => ({
2352
+ ...s,
2353
+ options: s.options.filter((o) => supportsShapes(o, shapes))
2354
+ })),
2355
+ ...space.parts === void 0 ? {} : { parts: space.parts.map((p) => restrict(p, shapes)) }
2356
+ };
2357
+ }
2358
+ /** This function throws a `RangeError` for a list of shape kinds with a kind that is not declared. */
2359
+ function assertShapes(shapes) {
2360
+ if (shapes === void 0) return;
2361
+ if (!Array.isArray(shapes)) throw new RangeError("The option shapes is not a list of shape kinds.");
2362
+ for (const k of shapes) if (!isShapeKind(k)) throw new RangeError(`The shape kind ${show(k)} is not one of ${SHAPE_KIND_IDS.join(", ")}.`);
2363
+ }
2364
+ /** This function gives the count of the grid points of a space that is known to be good. */
2365
+ function sizeOf(space, levels, limit) {
2144
2366
  let base = 0n;
2145
2367
  let meta = 0n;
2146
- for (const c of combos(space, options.levels, true, limit)) if (c.base) base += c.weight;
2368
+ for (const c of combos(space, levels, true, limit)) if (c.base) base += c.weight;
2147
2369
  else meta = meta === null || c.weight === null || meta + c.weight > limit ? null : meta + c.weight;
2148
2370
  return {
2149
2371
  base,
@@ -2163,14 +2385,12 @@ function* product(lists, i = 0, acc = []) {
2163
2385
  }
2164
2386
  }
2165
2387
  /** This function gives the configs of the grid of a space, without meta options. */
2166
- function gridConfigs(space, levels, limit) {
2167
- const size = spaceSize(space, {
2168
- levels,
2169
- maxMeta: 0n
2170
- }).base;
2388
+ function gridConfigs(space, levels, limit, shapes) {
2389
+ const kept = restrict(space, shapes);
2390
+ const size = sizeOf(kept, levels, 0n).base;
2171
2391
  if (size > BigInt(limit)) throw new RangeError(`The grid of the space ${space.engine} has ${size} configs, more than the limit ${limit}.`);
2172
2392
  const out = [];
2173
- for (const c of combos(space, levels, false, 0n)) {
2393
+ for (const c of combos(kept, levels, false, 0n)) {
2174
2394
  const names = [...c.choices.keys()];
2175
2395
  const lists = names.map((k) => {
2176
2396
  const o = c.choices.get(k);
@@ -2218,6 +2438,7 @@ function sampleConfigs(space, n, seed, meta, maxTries) {
2218
2438
  const lists = stages.map((s) => {
2219
2439
  const list = s.options.filter((o) => meta || o.members === void 0);
2220
2440
  if (s.optional) list.push(null);
2441
+ if (list.length === 0) throw new Error(`The stage ${s.name} of the space ${space.engine} has no option for a sample. A shape filter can remove all of its options.`);
2221
2442
  return list;
2222
2443
  });
2223
2444
  const points = [];
@@ -2294,12 +2515,14 @@ function sampleConfigs(space, n, seed, meta, maxTries) {
2294
2515
  * - The mode `grid` gives each grid point without a meta option, in a stable order. Stages, options and values go in the order of the space.
2295
2516
  * - The mode `sample` gives `n` configs of a seeded Latin hypercube. The options of a stage get equal strata, and each active param gets its own strata.
2296
2517
  * - A sample draws again when a point breaks a constraint. After `maxTries` draws, it throws an `Error`. Two sampled configs can be equal.
2518
+ * - With `shapes`, only the options that support each kind of the list count. Without `shapes`, the result does not change.
2297
2519
  */
2298
2520
  function enumerateSpace(space, options) {
2299
2521
  assertSpace(space);
2522
+ assertShapes(options.shapes);
2300
2523
  if (options.mode === "grid") {
2301
2524
  assertWhole(options.levels, "levels", 1);
2302
- return gridConfigs(space, options.levels, options.limit ?? 1e5);
2525
+ return gridConfigs(space, options.levels, options.limit ?? 1e5, options.shapes);
2303
2526
  }
2304
2527
  if (options.mode !== "sample") throw new RangeError(`The mode ${String(options.mode)} is not grid or sample.`);
2305
2528
  assertWhole(options.n, "n", 0);
@@ -2307,7 +2530,15 @@ function enumerateSpace(space, options) {
2307
2530
  const meta = options.meta ?? "skip";
2308
2531
  if (meta !== "skip" && meta !== "sample") throw new RangeError(`The option meta is ${String(meta)}, not skip or sample.`);
2309
2532
  if (options.n === 0) return [];
2310
- return sampleConfigs(space, options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1e3, 100 * options.n));
2533
+ return sampleConfigs(restrict(space, options.shapes), options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1e3, 100 * options.n));
2534
+ }
2535
+ /**
2536
+ * This function gives the shape kinds that a space accepts, in the order of `SHAPE_KINDS`. The value comes from the `supports` of the options.
2537
+ * A space accepts a kind when it has at least one config without a meta option on that kind.
2538
+ */
2539
+ function spaceShapeKinds(space) {
2540
+ assertSpace(space);
2541
+ return SHAPE_KIND_IDS.filter((k) => sizeOf(restrict(space, [k]), 1, 0n).base > 0n);
2311
2542
  }
2312
2543
  /**
2313
2544
  * This function joins configs of the parts of a product space into one config of the space `space`.
@@ -2410,6 +2641,25 @@ function signP(wins, losses) {
2410
2641
  }
2411
2642
  return Math.min(1, 2 * tail / 2 ** n);
2412
2643
  }
2644
+ /** This function gives the breadth of a verdict with the side `side`: 1, -1, or 0 for no side. */
2645
+ function breadthOf(families, side) {
2646
+ const ties = families.filter((f) => f.sign === 0).length;
2647
+ return {
2648
+ against: side === 0 ? 0 : families.filter((f) => f.sign === -side).length,
2649
+ of: families.length,
2650
+ ties
2651
+ };
2652
+ }
2653
+ /** This function gives the line of a verdict: its label, its scope and its breadth (`docs/VERDICT.md`). */
2654
+ function summaryOf(label, side, breadth) {
2655
+ const ties = breadth.ties === 0 ? "" : ` (${breadth.ties} ${breadth.ties === 1 ? "tie" : "ties"})`;
2656
+ const word = side > 0 ? "better" : "worse";
2657
+ const other = side > 0 ? "worse" : "better";
2658
+ const count = breadth.against === 0 ? "none" : String(breadth.against);
2659
+ if (label === "better" || label === "worse") return `${label} on these families, ${other} on ${count} of ${breadth.of}${ties}`;
2660
+ if (side === 0) return `${label}: the family mean is 0, on ${breadth.of} families${ties}`;
2661
+ return `${label}: ${word} on the family mean, ${other} on ${count} of ${breadth.of}${ties}`;
2662
+ }
2413
2663
  /** This function tells if a value is a positive whole number. */
2414
2664
  const isCount = (v) => Number.isInteger(v) && v > 0;
2415
2665
  /** This function gives the test of one side. */
@@ -2468,6 +2718,7 @@ function pairsOf(records, options) {
2468
2718
  * - `stratified` is the mean of the family means. Its bootstrap draws the seeds inside each family. `pooled` is the mean of the inputs.
2469
2719
  * - The permutation test changes the sign of each difference inside its family. Its statistic is the mean of the family means.
2470
2720
  * - The label is `better` or `worse` only when `agree` is `true`. Opposite signs of the two means give `depends on the mix`.
2721
+ * - `scope`, `breadth` and `summary` tell that the claim is about these families, and how many families are against it.
2471
2722
  *
2472
2723
  * It throws a `RangeError` for a missing `tie` or `level`, and for no pair. Two records of one side and input, or a value that is not finite, also cause a `RangeError`.
2473
2724
  */
@@ -2562,6 +2813,8 @@ function verdict(records, options) {
2562
2813
  const sign = Math.sign(stratified.mean);
2563
2814
  const agree = sign !== 0 && sign === Math.sign(pooled.mean) && clearOn(stratified, sign) && clearOn(pooled, sign) && permutation.p < level;
2564
2815
  const label = agree ? sign > 0 ? "better" : "worse" : sign * Math.sign(pooled.mean) < 0 ? "depends on the mix" : "no verdict";
2816
+ const breadth = breadthOf(families, sign);
2817
+ const summary = summaryOf(label, sign, breadth);
2565
2818
  return {
2566
2819
  measure: measure.id,
2567
2820
  n,
@@ -2572,9 +2825,12 @@ function verdict(records, options) {
2572
2825
  signTest,
2573
2826
  agree,
2574
2827
  label,
2828
+ scope: "these families",
2829
+ breadth,
2830
+ summary,
2575
2831
  dropped
2576
2832
  };
2577
2833
  }
2578
- export { DEFAULT_TOLERANCE, EDGE_DENSITY, EDGE_STEP_VERSION, EPS, FAMILY_ANY_KIND, JOINT_MEASURES, JOINT_MOVED_MIN, RECORD_EPS, RECORD_SCALE, SIDES, SPLIT_SCHEMA, STRATEGY_SPACE_SCHEMA, VERDICT_DRAWS, VERDICT_EXACT_CAP, VERDICT_EXACT_MAX, VERDICT_RUNS, VERDICT_SEED, applyPack, boundsOf, canonicalConfig, canonicalJson, checkConfig, checkDeclared, checkInput, checkSpace, checkSplit, compareMeasure, configId, corpusHashOf, countObstacleHits, countOutside, countOverlaps, countPortsOffBorder, declaredClaimOf, declaredCounts, documentHash, edgeStep, enumerateSpace, exercised, familyOf, fnv1a32, grow, joinConfigs, leaveOneSeedOut, marginSides, minEffectOf, mulberry32, outputGroups, outputHash, overlapOf, packHard, paramLevels, pickMoves, placedRect, portfolio, productSpace, randomInt, rectInRegion, regionHitsRect, selectBest, sha256Hex, spaceSize, splitConfig, splitFileName, splitFolds, stepRandom, toLayoutRecord, toPackInput, translate, turnBox, turnMargin, turnPoint, turnPort, turnRegion, turnSide, verdict };
2834
+ export { DEFAULT_SHAPES, DEFAULT_TOLERANCE, EDGE_DENSITY, EDGE_STEP_VERSION, EPS, FAMILY_ANY_KIND, JOINT_MEASURES, JOINT_MOVED_MIN, PACK_HARD_RULES, RECORD_EPS, RECORD_SCALE, SHAPE_KINDS, SHAPE_KIND_IDS, SIDES, SPLIT_SCHEMA, STRATEGY_SPACE_SCHEMA, VERDICT_DRAWS, VERDICT_EXACT_CAP, VERDICT_EXACT_MAX, VERDICT_RUNS, VERDICT_SEED, applyPack, boundsOf, canonicalConfig, canonicalJson, checkConfig, checkDeclared, checkInput, checkShapes, checkSpace, checkSplit, compareMeasure, configId, corpusHashOf, countObstacleHits, countOutside, countOverlaps, countPortsOffBorder, declaredClaimOf, declaredCounts, documentHash, edgeStep, enumerateSpace, exercised, familyOf, fnv1a32, grow, isShapeKind, itemShapeOf, joinConfigs, leaveOneSeedOut, marginSides, minEffectOf, mulberry32, outputGroups, outputHash, overlapOf, packHard, paramLevels, pickMoves, placedRect, portfolio, productSpace, randomInt, rectInRegion, regionHitsRect, selectBest, sha256Hex, shapeKindOf, shapeKindProblem, shapeKindsOf, shapesOf, spaceShapeKinds, spaceSize, splitConfig, splitFileName, splitFolds, stepRandom, toLayoutRecord, toPackInput, translate, turnBox, turnMargin, turnPoint, turnPort, turnRegion, turnSide, verdict };
2579
2835
 
2580
2836
  //# sourceMappingURL=index.js.map