@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/README.md +6 -4
- package/build/index.d.ts +270 -147
- package/build/index.d.ts.map +1 -1
- package/build/index.js +285 -29
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/input.ts +8 -0
- package/src/measures.ts +21 -10
- package/src/record.ts +11 -2
- package/src/rules.ts +15 -0
- package/src/shapes.ts +119 -0
- package/src/strategy-space.ts +94 -16
- package/src/verdict.ts +47 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/layout-contract",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.13",
|
|
5
5
|
"description": "The shared contract of the packing and highways layout engines: the input and record formats, the turn rule, margins, tolerances and hard rules.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"layout",
|
package/src/index.ts
CHANGED
package/src/input.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Margin, Region, Side } from "./geometry.ts";
|
|
2
|
+
import type { ShapeKind } from "./shapes.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A port of a box: a place where edges go into the box.
|
|
@@ -28,6 +29,11 @@ export interface LayoutBox {
|
|
|
28
29
|
readonly group?: string;
|
|
29
30
|
/** A region relative to the origin of the box. Without it, the box is the rectangle `w` by `h`. */
|
|
30
31
|
readonly shape?: Region;
|
|
32
|
+
/**
|
|
33
|
+
* The shape kind of the box (`docs/SHAPES.md`). Without it, `shapeKindOf` gives the kind of `shape`, or `rect` without a shape.
|
|
34
|
+
* The kind `free` tells that the contract knows only the bounding box `w` by `h`.
|
|
35
|
+
*/
|
|
36
|
+
readonly shapeKind?: ShapeKind;
|
|
31
37
|
readonly ports?: readonly Port[];
|
|
32
38
|
/** A ring around the box. No other box enters it. Only a route from a port of the box enters it, at a right angle. */
|
|
33
39
|
readonly margin?: Margin;
|
|
@@ -92,6 +98,8 @@ export interface LayoutCell {
|
|
|
92
98
|
readonly edgeShape?: "random" | "clustered" | "hubChain" | "longRange" | "tree" | "bundled";
|
|
93
99
|
readonly groups?: "none" | "flat" | "nested";
|
|
94
100
|
readonly ports?: "free" | "sides" | "fixed";
|
|
101
|
+
/** The shape kinds of the boxes: one kind, or `mixed` for more kinds. Without it, each box is a `rect`. `itemShapeOf` gives the value. */
|
|
102
|
+
readonly itemShape?: ShapeKind | "mixed";
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
/** An input of the corpus: the format `layout-input/1`. */
|
package/src/measures.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SHAPE_KIND_IDS, type ShapeKind } from "./shapes.ts";
|
|
1
2
|
import { RECORD_EPS, RECORD_SCALE } from "./tolerance.ts";
|
|
2
3
|
|
|
3
4
|
/** The direction of a measure: the side of the better values. */
|
|
@@ -29,6 +30,8 @@ export interface MeasureSpec<Id extends string = string> {
|
|
|
29
30
|
readonly minEffect?: number | { readonly rel: number };
|
|
30
31
|
/** The role of the measure in the joint selector. */
|
|
31
32
|
readonly role?: "guard" | "change-guard" | "diagnostic" | "time";
|
|
33
|
+
/** The shape kinds of the boxes that the measure supports. Without it, the measure supports only `rect`. `checkShapes` refuses the other kinds. */
|
|
34
|
+
readonly shapes?: readonly ShapeKind[];
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/** The id of a joint measure. `docs/JOINT-MEASURES.md` gives the definitions. */
|
|
@@ -43,17 +46,25 @@ export type JointMeasureId =
|
|
|
43
46
|
| "joint.proxy.rank"
|
|
44
47
|
| "joint.ms";
|
|
45
48
|
|
|
46
|
-
/** The
|
|
49
|
+
/** The shape kinds of a joint measure that reads only the bounding boxes, the centers and the positions of the boxes. */
|
|
50
|
+
const BOUNDS_ONLY = SHAPE_KIND_IDS;
|
|
51
|
+
/** The shape kinds of a joint measure that reads the area `w` by `h` of each box. */
|
|
52
|
+
const AREA: readonly ShapeKind[] = ["rect"];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The specs of the joint measures, in the order of `docs/JOINT-MEASURES.md`.
|
|
56
|
+
* `joint.fill` and `joint.straight.length` read the area of each box, so they support only `rect`. `docs/SHAPES.md` tells the reasons.
|
|
57
|
+
*/
|
|
47
58
|
export const JOINT_MEASURES: readonly MeasureSpec<JointMeasureId>[] = Object.freeze([
|
|
48
|
-
{ id: "joint.fill", minEffect: 0.001, unit: "0 to 1", direction: "higher", min: 0, max: 1, floor: 1, role: "guard" },
|
|
49
|
-
{ id: "joint.aspectError", minEffect: 0.01, unit: "natural log", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard" },
|
|
50
|
-
{ id: "joint.spill", minEffect: 0.001, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.001, role: "guard" },
|
|
51
|
-
{ id: "joint.straight.crossings", minEffect: 0, unit: "count", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0, role: "guard" },
|
|
52
|
-
{ id: "joint.straight.length", minEffect: 0.01, unit: "S for each edge", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard" },
|
|
53
|
-
{ id: "joint.stability.mean", minEffect: 0.005, unit: "W", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.005, role: "change-guard" },
|
|
54
|
-
{ id: "joint.stability.moved", minEffect: 0.02, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.02, role: "change-guard" },
|
|
55
|
-
{ id: "joint.proxy.rank", unit: "-1 to 1", direction: "higher", min: -1, max: 1, role: "diagnostic" },
|
|
56
|
-
{ id: "joint.ms", unit: "milliseconds", direction: "lower", min: 0, max: Infinity, role: "time" },
|
|
59
|
+
{ id: "joint.fill", minEffect: 0.001, unit: "0 to 1", direction: "higher", min: 0, max: 1, floor: 1, role: "guard", shapes: AREA },
|
|
60
|
+
{ id: "joint.aspectError", minEffect: 0.01, unit: "natural log", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard", shapes: BOUNDS_ONLY },
|
|
61
|
+
{ id: "joint.spill", minEffect: 0.001, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.001, role: "guard", shapes: BOUNDS_ONLY },
|
|
62
|
+
{ id: "joint.straight.crossings", minEffect: 0, unit: "count", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0, role: "guard", shapes: BOUNDS_ONLY },
|
|
63
|
+
{ id: "joint.straight.length", minEffect: 0.01, unit: "S for each edge", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard", shapes: AREA },
|
|
64
|
+
{ id: "joint.stability.mean", minEffect: 0.005, unit: "W", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.005, role: "change-guard", shapes: BOUNDS_ONLY },
|
|
65
|
+
{ id: "joint.stability.moved", minEffect: 0.02, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.02, role: "change-guard", shapes: BOUNDS_ONLY },
|
|
66
|
+
{ id: "joint.proxy.rank", unit: "-1 to 1", direction: "higher", min: -1, max: 1, role: "diagnostic", shapes: BOUNDS_ONLY },
|
|
67
|
+
{ id: "joint.ms", unit: "milliseconds", direction: "lower", min: 0, max: Infinity, role: "time", shapes: BOUNDS_ONLY },
|
|
57
68
|
] as const);
|
|
58
69
|
|
|
59
70
|
/** The threshold of `joint.stability.moved`, in widths `W` of the base. */
|
package/src/record.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Tolerance } from "./tolerance.ts";
|
|
2
2
|
import type { LayoutInput } from "./input.ts";
|
|
3
|
+
import { shapeKindsOf, type ShapeKind } from "./shapes.ts";
|
|
3
4
|
|
|
4
5
|
/** The stage of a record: the packer, the router, or both together. */
|
|
5
6
|
export type Stage = "pack" | "route" | "joint";
|
|
@@ -21,6 +22,8 @@ export interface LayoutRecord {
|
|
|
21
22
|
readonly seed: number;
|
|
22
23
|
readonly nodes: number;
|
|
23
24
|
readonly edges: number;
|
|
25
|
+
/** The shape kinds of the boxes of the input, in the order of `SHAPE_KINDS`. Without it, each box is a `rect`. */
|
|
26
|
+
readonly shapes?: readonly ShapeKind[];
|
|
24
27
|
/** The value of each measure, by its name: `pack.*`, `route.*` or `joint.*`. */
|
|
25
28
|
readonly measures: Readonly<Record<string, number>>;
|
|
26
29
|
/** The count of the breaks of each hard rule. A good run has 0 for each rule. */
|
|
@@ -67,10 +70,15 @@ export interface RunEnv {
|
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
/** The facts of a run that the input does not give. */
|
|
70
|
-
export type RunFacts = Omit<LayoutRecord, "schema" | "input" | "inputHash" | "set" | "nodes" | "edges">;
|
|
73
|
+
export type RunFacts = Omit<LayoutRecord, "schema" | "input" | "inputHash" | "set" | "nodes" | "edges" | "shapes">;
|
|
71
74
|
|
|
72
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* 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.
|
|
77
|
+
* The input also gives `shapes`. An input with only `rect` boxes gets no `shapes`, so its record does not change.
|
|
78
|
+
*/
|
|
73
79
|
export function toLayoutRecord(input: LayoutInput, run: RunFacts): LayoutRecord {
|
|
80
|
+
const shapes = shapeKindsOf(input);
|
|
81
|
+
const onlyRect = shapes.length === 0 || (shapes.length === 1 && shapes[0] === "rect");
|
|
74
82
|
return {
|
|
75
83
|
schema: "layout-record/1",
|
|
76
84
|
...run,
|
|
@@ -79,5 +87,6 @@ export function toLayoutRecord(input: LayoutInput, run: RunFacts): LayoutRecord
|
|
|
79
87
|
set: input.set ?? "",
|
|
80
88
|
nodes: input.boxes.length,
|
|
81
89
|
edges: input.edges?.length ?? 0,
|
|
90
|
+
...(onlyRect ? {} : { shapes }),
|
|
82
91
|
};
|
|
83
92
|
}
|
package/src/rules.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { boundsOf, grow, overlapOf, type Rect, type Region } from "./geometry.ts";
|
|
2
2
|
import type { LayoutBox, LayoutInput, Obstacle } from "./input.ts";
|
|
3
3
|
import { applyPack, type PackOutput } from "./output.ts";
|
|
4
|
+
import { SHAPE_KIND_IDS, shapeKindProblem, type ShapeSupport } from "./shapes.ts";
|
|
4
5
|
import { DEFAULT_TOLERANCE, EPS, RECORD_EPS } from "./tolerance.ts";
|
|
5
6
|
|
|
6
7
|
/** This function gives the area of a placed box: its shape or its rectangle, at its position. A box without a position gives `undefined`. */
|
|
@@ -96,6 +97,18 @@ export interface PackHard {
|
|
|
96
97
|
readonly complete: number;
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The hard rules of a pack, with the shape kinds that each rule supports.
|
|
102
|
+
* `no-overlap` and `in-container` test the bounding box of a shape. That count is exact only for a `rect`.
|
|
103
|
+
* `locked-kept` and `complete` read only the positions and the ids, so they support each kind.
|
|
104
|
+
*/
|
|
105
|
+
export const PACK_HARD_RULES: readonly ShapeSupport<keyof PackHard>[] = Object.freeze([
|
|
106
|
+
{ id: "no-overlap", shapes: ["rect"] },
|
|
107
|
+
{ id: "in-container", shapes: ["rect"] },
|
|
108
|
+
{ id: "locked-kept", shapes: SHAPE_KIND_IDS },
|
|
109
|
+
{ id: "complete", shapes: SHAPE_KIND_IDS },
|
|
110
|
+
] as const);
|
|
111
|
+
|
|
99
112
|
/** This function tells if the inside of `region` and the inside of `rect` have a common point. */
|
|
100
113
|
export function regionHitsRect(region: Region, rect: Rect): boolean {
|
|
101
114
|
if (rect.w <= 0 || rect.h <= 0) return false;
|
|
@@ -314,6 +327,8 @@ export function checkInput(input: LayoutInput): readonly string[] {
|
|
|
314
327
|
ids.add(box.id);
|
|
315
328
|
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`);
|
|
316
329
|
if (box.locked === true && (box.x === undefined || box.y === undefined)) problems.push(`box ${box.id}: a locked box has no position`);
|
|
330
|
+
const shape = shapeKindProblem(box);
|
|
331
|
+
if (shape !== undefined) problems.push(shape);
|
|
317
332
|
}
|
|
318
333
|
if (countPortsOffBorder(input.boxes) > 0) problems.push("a fixed port is not on the border of its box");
|
|
319
334
|
for (const edge of input.edges ?? []) {
|
package/src/shapes.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// The shape kinds of the items (`docs/SHAPES.md`): the declared set, the kind of a box and the check of the measures and rules on a run.
|
|
2
|
+
import type { ClaimBreak } from "./corpus.ts";
|
|
3
|
+
import type { Region } from "./geometry.ts";
|
|
4
|
+
import type { LayoutBox, LayoutInput } from "./input.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The kind of the shape of an item. A box without `shape` and without `shapeKind` is a `rect`.
|
|
8
|
+
* The set is open: a later version can add a kind. A reader treats an unknown kind as a kind that it does not support.
|
|
9
|
+
*/
|
|
10
|
+
export type ShapeKind = "rect" | "polygon" | "circle" | "free";
|
|
11
|
+
|
|
12
|
+
/** The geometry of a shape kind that this package gives. A `false` value tells that the kind declares the operation, but this package does not supply it. */
|
|
13
|
+
export interface ShapeGeometry {
|
|
14
|
+
/** The bounding box: `boundsOf` and `placedRect`. */
|
|
15
|
+
readonly bounds: boolean;
|
|
16
|
+
/** The area of the shape. */
|
|
17
|
+
readonly area: boolean;
|
|
18
|
+
/** The test that the shape is inside a region: `rectInRegion` for a `rect`. */
|
|
19
|
+
readonly contains: boolean;
|
|
20
|
+
/** The test that two shapes overlap: `overlapOf` for two `rect` shapes. */
|
|
21
|
+
readonly overlap: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The facts of one shape kind: its fields, the kinds of `Region` that give it, and its geometry. */
|
|
25
|
+
export interface ShapeKindSpec {
|
|
26
|
+
readonly id: ShapeKind;
|
|
27
|
+
/** The fields of the outline, relative to the top-left corner of the box. A name with `?` is optional. */
|
|
28
|
+
readonly fields: readonly string[];
|
|
29
|
+
/** The kinds of `LayoutBox.shape` that agree with this kind. */
|
|
30
|
+
readonly regions: readonly Region["kind"][];
|
|
31
|
+
readonly geometry: ShapeGeometry;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The declared shape kinds, in their order. Only `rect` has all of its geometry. The other kinds have their bounding box only. */
|
|
35
|
+
export const SHAPE_KINDS: readonly ShapeKindSpec[] = Object.freeze([
|
|
36
|
+
{ id: "rect", fields: ["w", "h"], regions: ["rect"], geometry: { bounds: true, area: true, contains: true, overlap: true } },
|
|
37
|
+
{ id: "polygon", fields: ["points", "holes?"], regions: ["polygon"], geometry: { bounds: true, area: false, contains: false, overlap: false } },
|
|
38
|
+
{ id: "circle", fields: ["cx", "cy", "r"], regions: ["circle"], geometry: { bounds: true, area: false, contains: false, overlap: false } },
|
|
39
|
+
{ id: "free", fields: ["w", "h", "shape?"], regions: ["rect", "polygon", "circle", "ellipse"], geometry: { bounds: true, area: false, contains: false, overlap: false } },
|
|
40
|
+
] as const);
|
|
41
|
+
|
|
42
|
+
/** The ids of `SHAPE_KINDS`, in their order. */
|
|
43
|
+
export const SHAPE_KIND_IDS: readonly ShapeKind[] = Object.freeze(SHAPE_KINDS.map((k) => k.id));
|
|
44
|
+
|
|
45
|
+
/** The shape kinds of a measure or a rule without `shapes`. The contract measured only rectangles before L46. */
|
|
46
|
+
export const DEFAULT_SHAPES: readonly ShapeKind[] = Object.freeze(["rect"] as const);
|
|
47
|
+
|
|
48
|
+
/** A measure or a rule, with the shape kinds that it supports. Without `shapes`, it supports `DEFAULT_SHAPES`. */
|
|
49
|
+
export interface ShapeSupport<Id extends string = string> {
|
|
50
|
+
readonly id: Id;
|
|
51
|
+
readonly shapes?: readonly ShapeKind[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const REGION_KIND: Readonly<Record<Region["kind"], ShapeKind>> = { rect: "rect", polygon: "polygon", circle: "circle", ellipse: "free" };
|
|
55
|
+
|
|
56
|
+
/** This function tells if a value is a declared shape kind. */
|
|
57
|
+
export function isShapeKind(value: unknown): value is ShapeKind {
|
|
58
|
+
return typeof value === "string" && (SHAPE_KIND_IDS as readonly string[]).includes(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* This function gives the shape kind of a box. A box with `shapeKind` has that kind.
|
|
63
|
+
* Without it, the kind comes from `shape`: no shape is a `rect`, and an ellipse is `free`.
|
|
64
|
+
*/
|
|
65
|
+
export function shapeKindOf(box: LayoutBox): ShapeKind {
|
|
66
|
+
if (box.shapeKind !== undefined) return box.shapeKind;
|
|
67
|
+
return box.shape === undefined ? "rect" : REGION_KIND[box.shape.kind];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 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. */
|
|
71
|
+
export function shapeKindsOf(input: Pick<LayoutInput, "boxes">): ShapeKind[] {
|
|
72
|
+
const seen = new Set(input.boxes.map(shapeKindOf));
|
|
73
|
+
return SHAPE_KIND_IDS.filter((k) => seen.has(k));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** This function gives the value of the cell dimension `itemShape` of an input: its one kind, `mixed` for more kinds, or `rect` without boxes. */
|
|
77
|
+
export function itemShapeOf(input: Pick<LayoutInput, "boxes">): ShapeKind | "mixed" {
|
|
78
|
+
const kinds = shapeKindsOf(input);
|
|
79
|
+
return kinds.length === 0 ? "rect" : kinds.length === 1 ? kinds[0]! : "mixed";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** This function gives the problem of the shape kind of a box, or `undefined` when `shapeKind` and `shape` agree. */
|
|
83
|
+
export function shapeKindProblem(box: LayoutBox): string | undefined {
|
|
84
|
+
const k: unknown = box.shapeKind;
|
|
85
|
+
if (k === undefined) return undefined;
|
|
86
|
+
const spec = SHAPE_KINDS.find((s) => s.id === k);
|
|
87
|
+
if (spec === undefined) return `box ${box.id}: the shape kind ${String(k)} is not a declared kind`;
|
|
88
|
+
if (box.shape === undefined) return k === "rect" || k === "free" ? undefined : `box ${box.id}: the shape kind ${k} has no shape`;
|
|
89
|
+
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}`;
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** This function gives the shape kinds that a measure or a rule supports. */
|
|
94
|
+
export function shapesOf(use: Pick<ShapeSupport, "shapes">): readonly ShapeKind[] {
|
|
95
|
+
return use.shapes ?? DEFAULT_SHAPES;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 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.
|
|
100
|
+
* Each break has the claim `shape`, the id of the input, and the first 5 ids of the boxes of the kind.
|
|
101
|
+
*/
|
|
102
|
+
export function checkShapes(input: Pick<LayoutInput, "id" | "boxes">, uses: readonly ShapeSupport[]): ClaimBreak[] {
|
|
103
|
+
const kinds = shapeKindsOf(input);
|
|
104
|
+
const out: ClaimBreak[] = [];
|
|
105
|
+
for (const use of uses) {
|
|
106
|
+
const shapes = shapesOf(use);
|
|
107
|
+
for (const kind of kinds) {
|
|
108
|
+
if (shapes.includes(kind)) continue;
|
|
109
|
+
const ids = input.boxes.filter((b) => shapeKindOf(b) === kind).map((b) => b.id);
|
|
110
|
+
const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, and ${ids.length - 5} more` : "");
|
|
111
|
+
out.push({
|
|
112
|
+
claim: "shape",
|
|
113
|
+
detail: `${use.id} supports only the shape kinds ${shapes.join(", ") || "(none)"}, but the input ${input.id} has ${kind} boxes: ${shown}`,
|
|
114
|
+
inputs: [input.id],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
package/src/strategy-space.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { ClaimBreak } from "./corpus.ts";
|
|
3
3
|
import { canonicalJson, sha256Hex } from "./hash.ts";
|
|
4
4
|
import { stepRandom } from "./random.ts";
|
|
5
|
+
import { isShapeKind, SHAPE_KIND_IDS, type ShapeKind } from "./shapes.ts";
|
|
5
6
|
|
|
6
7
|
/** The schema of a strategy space. */
|
|
7
8
|
export const STRATEGY_SPACE_SCHEMA = "strategy-space/1";
|
|
@@ -47,11 +48,25 @@ export interface MemberRule {
|
|
|
47
48
|
readonly ordered: boolean;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/** The inputs that an option supports. A missing field supports each value. */
|
|
52
|
+
export interface OptionSupports {
|
|
53
|
+
/** The shape kinds of the boxes (`docs/SHAPES.md`). Without it, the option supports each kind. */
|
|
54
|
+
readonly shape?: readonly ShapeKind[];
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
/** One implementation of a stage. An option with `members` is a meta option: it chooses between whole configs, or wraps them. */
|
|
51
58
|
export interface StageOption {
|
|
52
59
|
readonly id: string;
|
|
53
60
|
readonly params: readonly SpaceParam[];
|
|
54
61
|
readonly members?: MemberRule;
|
|
62
|
+
/** The inputs that the option supports. It describes the option, so it is not part of a config or of its id. */
|
|
63
|
+
readonly supports?: OptionSupports;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The shape kinds of the inputs of a check, a count or an enumeration. Without `shapes`, each option counts. */
|
|
67
|
+
export interface ShapeFilter {
|
|
68
|
+
/** The shape kinds of the input. Only the options that support each of these kinds count. */
|
|
69
|
+
readonly shapes?: readonly ShapeKind[];
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
/** One stage of the pipeline of an engine. A config of an `optional` stage can have no option. */
|
|
@@ -113,7 +128,7 @@ export interface SpaceSize {
|
|
|
113
128
|
}
|
|
114
129
|
|
|
115
130
|
/** The options of `spaceSize`. */
|
|
116
|
-
export interface SpaceSizeOptions {
|
|
131
|
+
export interface SpaceSizeOptions extends ShapeFilter {
|
|
117
132
|
/** The count of the levels of each numeric param, a whole number of 1 or more. */
|
|
118
133
|
readonly levels: number;
|
|
119
134
|
/** The largest meta count that `spaceSize` gives. The default is `10n ** 18n`. */
|
|
@@ -121,7 +136,7 @@ export interface SpaceSizeOptions {
|
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
/** The options of a grid. The grid gives only the configs without a meta option. */
|
|
124
|
-
export interface GridOptions {
|
|
139
|
+
export interface GridOptions extends ShapeFilter {
|
|
125
140
|
readonly mode: "grid";
|
|
126
141
|
readonly levels: number;
|
|
127
142
|
/** The largest count of configs. A larger grid causes a `RangeError`. The default is 100000. */
|
|
@@ -129,7 +144,7 @@ export interface GridOptions {
|
|
|
129
144
|
}
|
|
130
145
|
|
|
131
146
|
/** The options of a sample. */
|
|
132
|
-
export interface SampleOptions {
|
|
147
|
+
export interface SampleOptions extends ShapeFilter {
|
|
133
148
|
readonly mode: "sample";
|
|
134
149
|
/** The count of the configs, a whole number of 0 or more. */
|
|
135
150
|
readonly n: number;
|
|
@@ -271,6 +286,7 @@ function optionProblems(stage: SpaceStage, o: StageOption): Problem[] {
|
|
|
271
286
|
if (problem !== undefined) out.push(["when", `${pat} has a when value that is not a value of ${p.when.param}: ${problem}`]);
|
|
272
287
|
}
|
|
273
288
|
});
|
|
289
|
+
if (o.supports !== undefined) out.push(...supportsProblems(o.supports, at));
|
|
274
290
|
if (o.members !== undefined) {
|
|
275
291
|
const { min, max, ordered } = o.members;
|
|
276
292
|
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || min > max || typeof ordered !== "boolean")
|
|
@@ -279,6 +295,24 @@ function optionProblems(stage: SpaceStage, o: StageOption): Problem[] {
|
|
|
279
295
|
return out;
|
|
280
296
|
}
|
|
281
297
|
|
|
298
|
+
/** This function gives the problems of the `supports` of an option. */
|
|
299
|
+
function supportsProblems(supports: unknown, at: string): Problem[] {
|
|
300
|
+
if (!isObject(supports)) return [["shape", `the supports of ${at} are not an object`]];
|
|
301
|
+
const shape = supports["shape"];
|
|
302
|
+
if (shape === undefined) return [];
|
|
303
|
+
if (!Array.isArray(shape) || shape.length === 0) return [["shape", `${at} supports no shape kind. Remove shape to support each kind`]];
|
|
304
|
+
const out: Problem[] = [];
|
|
305
|
+
shape.forEach((k: unknown, i) => {
|
|
306
|
+
if (!isShapeKind(k)) out.push(["shape", `${at} supports the shape kind ${show(k)}, which is not one of ${SHAPE_KIND_IDS.join(", ")}`]);
|
|
307
|
+
else if (shape.indexOf(k) !== i) out.push(["duplicate", `${at} has the shape kind ${k} two times`]);
|
|
308
|
+
});
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** This function tells if an option supports each shape kind of the list. */
|
|
313
|
+
const supportsShapes = (o: StageOption, shapes: readonly ShapeKind[] | undefined): boolean =>
|
|
314
|
+
shapes === undefined || o.supports?.shape === undefined || shapes.every((k) => o.supports!.shape!.includes(k));
|
|
315
|
+
|
|
282
316
|
/** This function gives the problems of the params of one stage choice. */
|
|
283
317
|
function choiceParamProblems(o: StageOption, params: unknown, at: string): Problem[] {
|
|
284
318
|
if (params !== undefined && !isObject(params)) return [["config", `the params of ${at} are not an object`]];
|
|
@@ -301,7 +335,7 @@ function choiceParamProblems(o: StageOption, params: unknown, at: string): Probl
|
|
|
301
335
|
}
|
|
302
336
|
|
|
303
337
|
/** This function gives the problems of a config of a space. A member of a meta option has `member: true`. */
|
|
304
|
-
function configProblems(space: StrategySpace, config: unknown, member: boolean): Problem[] {
|
|
338
|
+
function configProblems(space: StrategySpace, config: unknown, member: boolean, shapes?: readonly ShapeKind[]): Problem[] {
|
|
305
339
|
if (!isObject(config) || !isObject(config["stages"])) return [["config", "the config is not an object with stages"]];
|
|
306
340
|
const out: Problem[] = [];
|
|
307
341
|
const stages = config["stages"];
|
|
@@ -339,6 +373,10 @@ function configProblems(space: StrategySpace, config: unknown, member: boolean):
|
|
|
339
373
|
}
|
|
340
374
|
chosen.set(s.name, o.id);
|
|
341
375
|
const at = `the option ${s.name}.${o.id}`;
|
|
376
|
+
if (!supportsShapes(o, shapes)) {
|
|
377
|
+
const missing = shapes!.filter((k) => !o.supports!.shape!.includes(k));
|
|
378
|
+
out.push(["shape", `${at} supports the shape kinds ${o.supports!.shape!.join(", ")}, not ${missing.join(", ")}`]);
|
|
379
|
+
}
|
|
342
380
|
if (meta !== undefined && 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`]);
|
|
343
381
|
out.push(...choiceParamProblems(o, c["params"], at));
|
|
344
382
|
if (o.members === undefined) {
|
|
@@ -349,20 +387,20 @@ function configProblems(space: StrategySpace, config: unknown, member: boolean):
|
|
|
349
387
|
out.push(["depth", `a member has the meta option ${s.name}.${o.id}. The depth of a meta config is 1`]);
|
|
350
388
|
continue;
|
|
351
389
|
}
|
|
352
|
-
out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at));
|
|
390
|
+
out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at, shapes));
|
|
353
391
|
}
|
|
354
392
|
for (const c of space.constraints ?? []) if (!holds(c, chosen)) out.push(["constraint", `the config breaks the constraint ${describe(c)}`]);
|
|
355
393
|
return out;
|
|
356
394
|
}
|
|
357
395
|
|
|
358
396
|
/** This function gives the problems of the members of a meta option. `part` is the space of the members. */
|
|
359
|
-
function memberProblems(part: StrategySpace, rule: MemberRule, members: unknown, at: string): Problem[] {
|
|
397
|
+
function memberProblems(part: StrategySpace, rule: MemberRule, members: unknown, at: string, shapes?: readonly ShapeKind[]): Problem[] {
|
|
360
398
|
if (!Array.isArray(members)) return [["members", `${at} is a meta option without a list of members`]];
|
|
361
399
|
const out: Problem[] = [];
|
|
362
400
|
if (members.length < rule.min || members.length > rule.max) out.push(["members", `${at} has ${members.length} members, not ${rule.min} to ${rule.max}`]);
|
|
363
401
|
const ids = new Map<string, number>();
|
|
364
402
|
members.forEach((m: unknown, k) => {
|
|
365
|
-
const inner = configProblems(part, m, true);
|
|
403
|
+
const inner = configProblems(part, m, true, shapes);
|
|
366
404
|
for (const [claim, detail] of inner) out.push([claim, `the member ${k} of ${at}: ${detail}`]);
|
|
367
405
|
if (inner.length > 0) return;
|
|
368
406
|
const id = idOf(canonicalOf(part, m as StrategyConfig, true));
|
|
@@ -377,9 +415,10 @@ const toBreaks = (problems: readonly Problem[], inputs: readonly string[] = []):
|
|
|
377
415
|
/**
|
|
378
416
|
* This function checks one config of a space. It gives one break for each problem. An empty list tells that the config is good.
|
|
379
417
|
* The `claim` of a break is the kind of the problem, for example `missing`, `unknown`, `inactive`, `param`, `constraint`, `members` or `depth`.
|
|
418
|
+
* 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.
|
|
380
419
|
*/
|
|
381
|
-
export function checkConfig(space: StrategySpace, config: StrategyConfig): ClaimBreak[] {
|
|
382
|
-
return toBreaks(configProblems(space, config, false));
|
|
420
|
+
export function checkConfig(space: StrategySpace, config: StrategyConfig, options: ShapeFilter = {}): ClaimBreak[] {
|
|
421
|
+
return toBreaks(configProblems(space, config, false, options.shapes));
|
|
383
422
|
}
|
|
384
423
|
|
|
385
424
|
/**
|
|
@@ -646,14 +685,40 @@ function combos(space: StrategySpace, levels: number, meta: boolean, limit: bigi
|
|
|
646
685
|
/**
|
|
647
686
|
* This function gives the count of the grid points of a space, with `levels` levels for each numeric param.
|
|
648
687
|
* It counts only the active params and the combinations that keep the constraints. It gives the meta configs apart.
|
|
688
|
+
* With `shapes`, it counts only the options that support each kind of the list.
|
|
649
689
|
*/
|
|
650
690
|
export function spaceSize(space: StrategySpace, options: SpaceSizeOptions): SpaceSize {
|
|
651
691
|
assertSpace(space);
|
|
652
692
|
assertWhole(options.levels, "levels", 1);
|
|
653
|
-
|
|
693
|
+
assertShapes(options.shapes);
|
|
694
|
+
return sizeOf(restrict(space, options.shapes), options.levels, options.maxMeta ?? 10n ** 18n);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* This function gives the space with only the options that support each shape kind of `shapes`. Without `shapes`, it gives the same space.
|
|
699
|
+
* A stage can lose all of its options. Then the space has no config with that stage.
|
|
700
|
+
*/
|
|
701
|
+
function restrict(space: StrategySpace, shapes: readonly ShapeKind[] | undefined): StrategySpace {
|
|
702
|
+
if (shapes === undefined) return space;
|
|
703
|
+
return {
|
|
704
|
+
...space,
|
|
705
|
+
stages: space.stages.map((s) => ({ ...s, options: s.options.filter((o) => supportsShapes(o, shapes)) })),
|
|
706
|
+
...(space.parts === undefined ? {} : { parts: space.parts.map((p) => restrict(p, shapes)) }),
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** This function throws a `RangeError` for a list of shape kinds with a kind that is not declared. */
|
|
711
|
+
function assertShapes(shapes: readonly ShapeKind[] | undefined): void {
|
|
712
|
+
if (shapes === undefined) return;
|
|
713
|
+
if (!Array.isArray(shapes)) throw new RangeError("The option shapes is not a list of shape kinds.");
|
|
714
|
+
for (const k of shapes) if (!isShapeKind(k)) throw new RangeError(`The shape kind ${show(k)} is not one of ${SHAPE_KIND_IDS.join(", ")}.`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** This function gives the count of the grid points of a space that is known to be good. */
|
|
718
|
+
function sizeOf(space: StrategySpace, levels: number, limit: bigint): SpaceSize {
|
|
654
719
|
let base = 0n;
|
|
655
720
|
let meta: bigint | null = 0n;
|
|
656
|
-
for (const c of combos(space,
|
|
721
|
+
for (const c of combos(space, levels, true, limit)) {
|
|
657
722
|
if (c.base) base += c.weight!;
|
|
658
723
|
else meta = meta === null || c.weight === null || meta + c.weight > limit ? null : meta + c.weight;
|
|
659
724
|
}
|
|
@@ -674,11 +739,12 @@ function* product<T>(lists: readonly (readonly T[])[], i = 0, acc: T[] = []): Ge
|
|
|
674
739
|
}
|
|
675
740
|
|
|
676
741
|
/** This function gives the configs of the grid of a space, without meta options. */
|
|
677
|
-
function gridConfigs(space: StrategySpace, levels: number, limit: number): StrategyConfig[] {
|
|
678
|
-
const
|
|
742
|
+
function gridConfigs(space: StrategySpace, levels: number, limit: number, shapes: readonly ShapeKind[] | undefined): StrategyConfig[] {
|
|
743
|
+
const kept = restrict(space, shapes);
|
|
744
|
+
const size = sizeOf(kept, levels, 0n).base;
|
|
679
745
|
if (size > BigInt(limit)) throw new RangeError(`The grid of the space ${space.engine} has ${size} configs, more than the limit ${limit}.`);
|
|
680
746
|
const out: StrategyConfig[] = [];
|
|
681
|
-
for (const c of combos(
|
|
747
|
+
for (const c of combos(kept, levels, false, 0n)) {
|
|
682
748
|
const names = [...c.choices.keys()];
|
|
683
749
|
const lists = names.map((k) => {
|
|
684
750
|
const o = c.choices.get(k)!;
|
|
@@ -724,6 +790,7 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
724
790
|
const lists = stages.map((s) => {
|
|
725
791
|
const list: (StageOption | null)[] = s.options.filter((o) => meta || o.members === undefined);
|
|
726
792
|
if (s.optional) list.push(null);
|
|
793
|
+
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.`);
|
|
727
794
|
return list;
|
|
728
795
|
});
|
|
729
796
|
const points: Map<string, StageOption | null>[] = [];
|
|
@@ -794,12 +861,14 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
794
861
|
* - 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.
|
|
795
862
|
* - 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.
|
|
796
863
|
* - A sample draws again when a point breaks a constraint. After `maxTries` draws, it throws an `Error`. Two sampled configs can be equal.
|
|
864
|
+
* - With `shapes`, only the options that support each kind of the list count. Without `shapes`, the result does not change.
|
|
797
865
|
*/
|
|
798
866
|
export function enumerateSpace(space: StrategySpace, options: EnumerateOptions): StrategyConfig[] {
|
|
799
867
|
assertSpace(space);
|
|
868
|
+
assertShapes(options.shapes);
|
|
800
869
|
if (options.mode === "grid") {
|
|
801
870
|
assertWhole(options.levels, "levels", 1);
|
|
802
|
-
return gridConfigs(space, options.levels, options.limit ?? 100_000);
|
|
871
|
+
return gridConfigs(space, options.levels, options.limit ?? 100_000, options.shapes);
|
|
803
872
|
}
|
|
804
873
|
if (options.mode !== "sample") throw new RangeError(`The mode ${String((options as { mode?: unknown }).mode)} is not grid or sample.`);
|
|
805
874
|
assertWhole(options.n, "n", 0);
|
|
@@ -807,7 +876,16 @@ export function enumerateSpace(space: StrategySpace, options: EnumerateOptions):
|
|
|
807
876
|
const meta = options.meta ?? "skip";
|
|
808
877
|
if (meta !== "skip" && meta !== "sample") throw new RangeError(`The option meta is ${String(meta)}, not skip or sample.`);
|
|
809
878
|
if (options.n === 0) return [];
|
|
810
|
-
return sampleConfigs(space, options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1000, 100 * options.n));
|
|
879
|
+
return sampleConfigs(restrict(space, options.shapes), options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1000, 100 * options.n));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* 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.
|
|
884
|
+
* A space accepts a kind when it has at least one config without a meta option on that kind.
|
|
885
|
+
*/
|
|
886
|
+
export function spaceShapeKinds(space: StrategySpace): ShapeKind[] {
|
|
887
|
+
assertSpace(space);
|
|
888
|
+
return SHAPE_KIND_IDS.filter((k) => sizeOf(restrict(space, [k]), 1, 0n).base > 0n);
|
|
811
889
|
}
|
|
812
890
|
|
|
813
891
|
/**
|
package/src/verdict.ts
CHANGED
|
@@ -91,6 +91,25 @@ export interface VerdictSignTest {
|
|
|
91
91
|
/** The label of a verdict. `better` and `worse` tell the side of `b`. */
|
|
92
92
|
export type VerdictLabel = "better" | "worse" | "depends on the mix" | "no verdict";
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* The scope of a verdict. The test keeps the families of the corpus as fixed, so its claim is about these families only.
|
|
96
|
+
* It is not a claim about a new kind of input.
|
|
97
|
+
*/
|
|
98
|
+
export type VerdictScope = "these families";
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The breadth of a verdict: the count of the families against its side.
|
|
102
|
+
* The side is the side of the label. For `depends on the mix` and `no verdict`, it is the sign of the stratified mean.
|
|
103
|
+
*/
|
|
104
|
+
export interface VerdictBreadth {
|
|
105
|
+
/** The count of the families with a sign against the side. A stratified mean of exactly 0 has no side, so this count is 0. */
|
|
106
|
+
readonly against: number;
|
|
107
|
+
/** The count of the families. */
|
|
108
|
+
readonly of: number;
|
|
109
|
+
/** The count of the families with the sign 0. */
|
|
110
|
+
readonly ties: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
94
113
|
/** The result of `verdict`. */
|
|
95
114
|
export interface VerdictResult {
|
|
96
115
|
readonly measure: string;
|
|
@@ -107,6 +126,12 @@ export interface VerdictResult {
|
|
|
107
126
|
/** `true` only when the two means have the same sign, the two intervals are clear of 0 and the permutation p is below `level`. */
|
|
108
127
|
readonly agree: boolean;
|
|
109
128
|
readonly label: VerdictLabel;
|
|
129
|
+
/** The scope of the claim: the families of the corpus, not a new kind of input. */
|
|
130
|
+
readonly scope: VerdictScope;
|
|
131
|
+
/** The count of the families against the side of the verdict, of all the families and of the ties. */
|
|
132
|
+
readonly breadth: VerdictBreadth;
|
|
133
|
+
/** The label, the scope and the breadth in one line, for example `better on these families, worse on 2 of 6 (1 tie)`. */
|
|
134
|
+
readonly summary: string;
|
|
110
135
|
/** The inputs without a pair: one side has no record, a skipped record or no value of the measure. */
|
|
111
136
|
readonly dropped: readonly string[];
|
|
112
137
|
}
|
|
@@ -154,6 +179,24 @@ function signP(wins: number, losses: number): number {
|
|
|
154
179
|
return Math.min(1, (2 * tail) / 2 ** n);
|
|
155
180
|
}
|
|
156
181
|
|
|
182
|
+
/** This function gives the breadth of a verdict with the side `side`: 1, -1, or 0 for no side. */
|
|
183
|
+
function breadthOf(families: readonly FamilyVerdict[], side: number): VerdictBreadth {
|
|
184
|
+
const ties = families.filter((f) => f.sign === 0).length;
|
|
185
|
+
const against = side === 0 ? 0 : families.filter((f) => f.sign === -side).length;
|
|
186
|
+
return { against, of: families.length, ties };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** This function gives the line of a verdict: its label, its scope and its breadth (`docs/VERDICT.md`). */
|
|
190
|
+
function summaryOf(label: VerdictLabel, side: number, breadth: VerdictBreadth): string {
|
|
191
|
+
const ties = breadth.ties === 0 ? "" : ` (${breadth.ties} ${breadth.ties === 1 ? "tie" : "ties"})`;
|
|
192
|
+
const word = side > 0 ? "better" : "worse";
|
|
193
|
+
const other = side > 0 ? "worse" : "better";
|
|
194
|
+
const count = breadth.against === 0 ? "none" : String(breadth.against);
|
|
195
|
+
if (label === "better" || label === "worse") return `${label} on these families, ${other} on ${count} of ${breadth.of}${ties}`;
|
|
196
|
+
if (side === 0) return `${label}: the family mean is 0, on ${breadth.of} families${ties}`;
|
|
197
|
+
return `${label}: ${word} on the family mean, ${other} on ${count} of ${breadth.of}${ties}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
157
200
|
/** This function tells if a value is a positive whole number. */
|
|
158
201
|
const isCount = (v: number): boolean => Number.isInteger(v) && v > 0;
|
|
159
202
|
|
|
@@ -206,6 +249,7 @@ function pairsOf<R extends VerdictRecord>(records: Iterable<R>, options: Verdict
|
|
|
206
249
|
* - `stratified` is the mean of the family means. Its bootstrap draws the seeds inside each family. `pooled` is the mean of the inputs.
|
|
207
250
|
* - The permutation test changes the sign of each difference inside its family. Its statistic is the mean of the family means.
|
|
208
251
|
* - The label is `better` or `worse` only when `agree` is `true`. Opposite signs of the two means give `depends on the mix`.
|
|
252
|
+
* - `scope`, `breadth` and `summary` tell that the claim is about these families, and how many families are against it.
|
|
209
253
|
*
|
|
210
254
|
* 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`.
|
|
211
255
|
*/
|
|
@@ -292,5 +336,7 @@ export function verdict<R extends VerdictRecord>(records: Iterable<R>, options:
|
|
|
292
336
|
const sign = Math.sign(stratified.mean);
|
|
293
337
|
const agree = sign !== 0 && sign === Math.sign(pooled.mean) && clearOn(stratified, sign) && clearOn(pooled, sign) && permutation.p < level;
|
|
294
338
|
const label: VerdictLabel = agree ? (sign > 0 ? "better" : "worse") : sign * Math.sign(pooled.mean) < 0 ? "depends on the mix" : "no verdict";
|
|
295
|
-
|
|
339
|
+
const breadth = breadthOf(families, sign);
|
|
340
|
+
const summary = summaryOf(label, sign, breadth);
|
|
341
|
+
return { measure: measure.id, n, stratified, pooled, permutation, families, signTest, agree, label, scope: "these families", breadth, summary, dropped };
|
|
296
342
|
}
|