@mark1russell7/layout-contract 0.2.9 → 0.2.11

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/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.9",
4
+ "version": "0.2.11",
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/corpus.ts CHANGED
@@ -19,6 +19,78 @@ export interface DeclaredClaim {
19
19
  readonly thin: readonly string[];
20
20
  /** The count of the inputs for each property id. */
21
21
  readonly counts: Readonly<Record<string, number>>;
22
+ /** The key of the family of an input. `familyOf` gives the key of one cell. The family is the unit of a verdict. */
23
+ readonly familyBy?: FamilyBy;
24
+ /** The minimum count of the inputs of each family. Without it, `checkDeclared` does not count the families. */
25
+ readonly familyMin?: number;
26
+ }
27
+
28
+ /**
29
+ * The key of the family of a cell (`docs/CORPUS-CHECKS.md`).
30
+ *
31
+ * - The flat form is a list of dimensions. The key is the values of these dimensions of the cell, in order, with `/` between them.
32
+ * - The map form chooses an entry by the field `kind` of the cell, or the entry `"*"`. The key is the kind, then the values of the dimensions of the entry.
33
+ */
34
+ export type FamilyBy = readonly string[] | { readonly [kind: string]: readonly string[] };
35
+
36
+ /** A cell of an input: one value for each dimension. The map form of `FamilyBy` also reads the field `kind`. */
37
+ export type FamilyCell = Readonly<Record<string, unknown>>;
38
+
39
+ /** The entry of the map form of `FamilyBy` for each kind without its own entry. */
40
+ export const FAMILY_ANY_KIND = "*";
41
+
42
+ /** The separator of the parts of a family key. */
43
+ const FAMILY_SEP = "/";
44
+
45
+ /** The parts of the key of one cell, or the text of the problem. */
46
+ type FamilyParts = { readonly parts: readonly string[] } | { readonly problem: string };
47
+
48
+ /** This function gives the text of a dimension value. A missing value, or a value that is not a string, a number or a boolean, gives `undefined`. */
49
+ function partOf(value: unknown): string | undefined {
50
+ if (typeof value === "string") return value;
51
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
52
+ return undefined;
53
+ }
54
+
55
+ /** This function gives the parts of the family key of a cell. A missing dimension, a missing kind or a kind without an entry gives a problem. */
56
+ function familyParts(cell: FamilyCell, familyBy: FamilyBy): FamilyParts {
57
+ let dims: readonly string[];
58
+ let kind: string | undefined;
59
+ const parts: string[] = [];
60
+ if (Array.isArray(familyBy)) {
61
+ dims = familyBy as readonly string[];
62
+ } else {
63
+ const map = familyBy as Readonly<Record<string, readonly string[]>>;
64
+ kind = partOf(cell["kind"]);
65
+ if (kind === undefined) return { problem: "a cell without kind, which the map form of familyBy needs" };
66
+ const entry = Object.hasOwn(map, kind) ? map[kind] : Object.hasOwn(map, FAMILY_ANY_KIND) ? map[FAMILY_ANY_KIND] : undefined;
67
+ if (entry === undefined) return { problem: `the kind "${kind}", which has no entry in familyBy and no entry "${FAMILY_ANY_KIND}"` };
68
+ dims = entry;
69
+ parts.push(kind);
70
+ }
71
+ for (const dim of dims) {
72
+ const part = partOf(cell[dim]);
73
+ if (part === undefined) return { problem: `a cell${kind === undefined ? "" : ` of the kind "${kind}"`} without the dimension "${dim}" of familyBy` };
74
+ parts.push(part);
75
+ }
76
+ return { parts };
77
+ }
78
+
79
+ /**
80
+ * This function gives the family key of a cell. It throws a `RangeError` when the cell does not have a dimension of the key.
81
+ * In the map form, it also throws for a cell without `kind`, and for a kind with no entry and no entry `"*"`.
82
+ * For example, `{ "rect-grid": ["obstacles"], "*": [] }` gives `rect-grid/wall` for a `rect-grid` cell and `gallery` for a `gallery` cell.
83
+ */
84
+ export function familyOf(cell: FamilyCell, familyBy: FamilyBy): string {
85
+ const found = familyParts(cell, familyBy);
86
+ if ("problem" in found) throw new RangeError(`familyOf: ${found.problem}.`);
87
+ return found.parts.join(FAMILY_SEP);
88
+ }
89
+
90
+ /** This function tells if a value is a `FamilyBy`: a list of dimensions, or a map from a kind to a list of dimensions. */
91
+ function isFamilyBy(value: unknown): value is FamilyBy {
92
+ const isDims = (v: unknown): boolean => Array.isArray(v) && v.every((d) => typeof d === "string");
93
+ return isDims(value) || (isObject(value) && Object.values(value).every(isDims));
22
94
  }
23
95
 
24
96
  /** The hook that gives the ids of the properties that one input declares. */
@@ -54,7 +126,16 @@ export function declaredClaimOf(manifest: object): DeclaredClaim | undefined {
54
126
  if (!isObject(counts) || !Object.values(counts).every((n) => Number.isInteger(n) && (n as number) >= 0)) {
55
127
  throw new Error("The field claims.declared.counts gives a whole count for each property id.");
56
128
  }
57
- return { minCount: minCount as number, thin: [...(thin as string[])], counts: { ...(counts as Record<string, number>) } };
129
+ const { familyBy, familyMin } = declared;
130
+ if (familyBy !== undefined && !isFamilyBy(familyBy)) throw new Error("The field claims.declared.familyBy is a list of dimensions, or a map from a kind to a list of dimensions.");
131
+ if (familyMin !== undefined && (!Number.isInteger(familyMin) || (familyMin as number) < 0)) throw new Error("The field claims.declared.familyMin is a whole number of 0 or more.");
132
+ return {
133
+ minCount: minCount as number,
134
+ thin: [...(thin as string[])],
135
+ counts: { ...(counts as Record<string, number>) },
136
+ ...(familyBy === undefined ? {} : { familyBy: Array.isArray(familyBy) ? [...familyBy] : { ...(familyBy as Record<string, readonly string[]>) } }),
137
+ ...(familyMin === undefined ? {} : { familyMin: familyMin as number }),
138
+ };
58
139
  }
59
140
 
60
141
  /**
@@ -64,9 +145,22 @@ export function declaredClaimOf(manifest: object): DeclaredClaim | undefined {
64
145
  * - Each id of `ids` has a count in `claim.counts`, and `claim.counts` has no other id. Each count is the count of the inputs.
65
146
  * - An id with a count below `minCount` is in `thin`. Each id of `thin` is an id of `ids`.
66
147
  * - Each id that `declares` gives is an id of `ids`.
148
+ *
149
+ * With `claim.familyBy`, the hook `cellOf` gives the cell of each input. The default reads the field `cell` of the input.
150
+ *
151
+ * - Each cell has each dimension of its key. In the map form, each cell has a `kind`, and the kind has an entry or the entry `"*"` is there.
152
+ * - Two cells with different dimension values do not give the same key.
153
+ * - With `claim.familyMin`, each family has `familyMin` inputs or more.
67
154
  */
68
- export function checkDeclared<I>(inputs: Iterable<I>, declares: Declares<I>, claim: DeclaredClaim, ids: readonly string[] = Object.keys(claim.counts)): ClaimBreak[] {
69
- const computed = declaredCounts(inputs, declares, ids);
155
+ export function checkDeclared<I>(
156
+ inputs: Iterable<I>,
157
+ declares: Declares<I>,
158
+ claim: DeclaredClaim,
159
+ ids: readonly string[] = Object.keys(claim.counts),
160
+ cellOf: (input: I) => FamilyCell | undefined = cellField,
161
+ ): ClaimBreak[] {
162
+ const list = [...inputs];
163
+ const computed = declaredCounts(list, declares, ids);
70
164
  const out: string[] = [];
71
165
  for (const id of ids) {
72
166
  if (!(id in claim.counts)) out.push(`the property "${id}" has no count`);
@@ -79,7 +173,53 @@ export function checkDeclared<I>(inputs: Iterable<I>, declares: Declares<I>, cla
79
173
  }
80
174
  for (const id of claim.thin) if (!ids.includes(id)) out.push(`"${id}" of thin is not a declared property`);
81
175
  for (const id of Object.keys(computed)) if (!ids.includes(id)) out.push(`${computed[id]} inputs declare "${id}", which is not a declared property`);
82
- return out.map((detail) => ({ claim: "declared", detail, inputs: [] }));
176
+ const breaks: ClaimBreak[] = out.map((detail) => ({ claim: "declared", detail, inputs: [] }));
177
+ if (claim.familyBy !== undefined) breaks.push(...checkFamilies(list, claim.familyBy, claim.familyMin, cellOf));
178
+ return breaks;
179
+ }
180
+
181
+ /** This function gives the field `cell` of an input, when it is an object. */
182
+ function cellField(input: unknown): FamilyCell | undefined {
183
+ const cell = isObject(input) ? input["cell"] : undefined;
184
+ return isObject(cell) ? cell : undefined;
185
+ }
186
+
187
+ /** This function gives the field `id` of an input, or its place in the list. */
188
+ function inputId(input: unknown, at: number): string {
189
+ const id = isObject(input) ? input["id"] : undefined;
190
+ return typeof id === "string" ? id : `#${at}`;
191
+ }
192
+
193
+ /** This function gives the breaks of the family keys of the inputs: the problems of each key, the collisions and the thin families. */
194
+ function checkFamilies<I>(inputs: readonly I[], familyBy: FamilyBy, familyMin: number | undefined, cellOf: (input: I) => FamilyCell | undefined): ClaimBreak[] {
195
+ const problems = new Map<string, string[]>();
196
+ const members = new Map<string, { readonly tuples: Map<string, string[]>; readonly ids: string[] }>();
197
+ inputs.forEach((input, at) => {
198
+ const id = inputId(input, at);
199
+ const cell = cellOf(input);
200
+ const found: FamilyParts = cell === undefined ? { problem: "no cell, which familyBy needs" } : familyParts(cell, familyBy);
201
+ if ("problem" in found) {
202
+ problems.set(found.problem, [...(problems.get(found.problem) ?? []), id]);
203
+ return;
204
+ }
205
+ const key = found.parts.join(FAMILY_SEP);
206
+ const family = members.get(key) ?? { tuples: new Map<string, string[]>(), ids: [] };
207
+ const tuple = JSON.stringify(found.parts);
208
+ family.tuples.set(tuple, [...(family.tuples.get(tuple) ?? []), id]);
209
+ family.ids.push(id);
210
+ members.set(key, family);
211
+ });
212
+ const breaks: ClaimBreak[] = [];
213
+ const add = (detail: string, ids: readonly string[]): void => void breaks.push({ claim: "declared", detail, inputs: [...ids] });
214
+ for (const [problem, ids] of problems) add(`${ids.length} inputs have ${problem}`, ids);
215
+ for (const [key, family] of members) {
216
+ if (family.tuples.size > 1) {
217
+ const forms = [...family.tuples.keys()].join(" and ");
218
+ add(`the family "${key}" comes from different dimension values: ${forms}`, family.ids);
219
+ }
220
+ if (familyMin !== undefined && family.ids.length < familyMin) add(`the family "${key}" has ${family.ids.length} inputs, below familyMin ${familyMin}`, family.ids);
221
+ }
222
+ return breaks;
83
223
  }
84
224
 
85
225
  /** The id and the hash of one input of a corpus. */
package/src/index.ts CHANGED
@@ -12,3 +12,5 @@ export * from "./edges.ts";
12
12
  export * from "./hash.ts";
13
13
  export * from "./portfolio.ts";
14
14
  export * from "./corpus.ts";
15
+ export * from "./strategy-space.ts";
16
+ export * from "./verdict.ts";