@mark1russell7/layout-contract 0.2.4 → 0.2.6

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/src/hash.ts ADDED
@@ -0,0 +1,198 @@
1
+ import { RECORD_EPS } from "./tolerance.ts";
2
+ import { utf8 } from "./utf8.ts";
3
+
4
+ /** The round constants of SHA-256. */
5
+ const K: readonly number[] = [
6
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
7
+ 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
8
+ 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
9
+ 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
10
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
11
+ ];
12
+
13
+ /**
14
+ * This function gives the SHA-256 digest of the UTF-8 bytes of a string, as 64 lowercase hex characters.
15
+ * It uses no API of Node or of the browser.
16
+ */
17
+ export function sha256Hex(text: string): string {
18
+ const bytes = utf8(text);
19
+ const bits = bytes.length * 8;
20
+ const total = Math.ceil((bytes.length + 9) / 64) * 64;
21
+ const data = new Uint8Array(total);
22
+ data.set(bytes);
23
+ data[bytes.length] = 0x80;
24
+ const view = new DataView(data.buffer);
25
+ view.setUint32(total - 8, Math.floor(bits / 0x100000000));
26
+ view.setUint32(total - 4, bits >>> 0);
27
+ const h = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
28
+ const w = new Uint32Array(64);
29
+ const rotr = (x: number, n: number): number => (x >>> n) | (x << (32 - n));
30
+ for (let off = 0; off < total; off += 64) {
31
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + 4 * i);
32
+ for (let i = 16; i < 64; i++) {
33
+ const a = w[i - 15]!;
34
+ const b = w[i - 2]!;
35
+ const s0 = rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3);
36
+ const s1 = rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10);
37
+ w[i] = (w[i - 16]! + s0 + w[i - 7]! + s1) >>> 0;
38
+ }
39
+ let [a, b, c, d, e, f, g, k] = h as [number, number, number, number, number, number, number, number];
40
+ for (let i = 0; i < 64; i++) {
41
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
42
+ const ch = (e & f) ^ (~e & g);
43
+ const t1 = (k + s1 + ch + K[i]! + w[i]!) >>> 0;
44
+ const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
45
+ const maj = (a & b) ^ (a & c) ^ (b & c);
46
+ const t2 = (s0 + maj) >>> 0;
47
+ k = g;
48
+ g = f;
49
+ f = e;
50
+ e = (d + t1) >>> 0;
51
+ d = c;
52
+ c = b;
53
+ b = a;
54
+ a = (t1 + t2) >>> 0;
55
+ }
56
+ h[0] = (h[0]! + a) >>> 0;
57
+ h[1] = (h[1]! + b) >>> 0;
58
+ h[2] = (h[2]! + c) >>> 0;
59
+ h[3] = (h[3]! + d) >>> 0;
60
+ h[4] = (h[4]! + e) >>> 0;
61
+ h[5] = (h[5]! + f) >>> 0;
62
+ h[6] = (h[6]! + g) >>> 0;
63
+ h[7] = (h[7]! + k) >>> 0;
64
+ }
65
+ return h.map((x) => x.toString(16).padStart(8, "0")).join("");
66
+ }
67
+
68
+ /**
69
+ * This function gives the canonical form of a value: sorted keys and no whitespace, with the numbers of `JSON.stringify`.
70
+ * A key with the value `undefined` is not in the form. An `undefined` in an array becomes `null`.
71
+ */
72
+ export function canonicalJson(value: unknown): string {
73
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
74
+ if (Array.isArray(value)) return `[${value.map((v: unknown) => (v === undefined ? "null" : canonicalJson(v))).join(",")}]`;
75
+ const o = value as Record<string, unknown>;
76
+ const keys = Object.keys(o)
77
+ .filter((k) => o[k] !== undefined)
78
+ .sort();
79
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
80
+ }
81
+
82
+ /** The options of `outputHash` and `documentHash`. */
83
+ export interface HashOptions {
84
+ /**
85
+ * The step of the grid of the numbers. The default is `RECORD_EPS`, so a number keeps 3 decimals.
86
+ * The value `0` keeps the exact numbers. A negative value or a value that is not finite causes a `RangeError`.
87
+ */
88
+ readonly grid?: number;
89
+ }
90
+
91
+ /** This function gives the factor of a grid: `1 / grid`, or `undefined` for the exact numbers. */
92
+ function gridFactor(options: HashOptions): number | undefined {
93
+ const grid = options.grid ?? RECORD_EPS;
94
+ if (!Number.isFinite(grid) || grid < 0) throw new RangeError(`The grid ${grid} is not a finite number of 0 or more.`);
95
+ return grid === 0 ? undefined : 1 / grid;
96
+ }
97
+
98
+ /**
99
+ * This function gives a copy of a value with each number on the grid. The value `-0` becomes `0`.
100
+ * It throws a `RangeError` for a number that is not finite.
101
+ */
102
+ function onGrid(value: unknown, path: string, k: number | undefined): unknown {
103
+ if (typeof value === "number") {
104
+ if (!Number.isFinite(value)) throw new RangeError(`The value has the number ${value} at ${path}. JSON has no form for it.`);
105
+ if (k === undefined) return value;
106
+ const r = Math.round(value * k) / k;
107
+ if (!Number.isFinite(r)) throw new RangeError(`The number at ${path} is too large for the grid.`);
108
+ return r === 0 ? 0 : r;
109
+ }
110
+ if (value === null || typeof value !== "object") return value;
111
+ if (Array.isArray(value)) return value.map((v: unknown, i) => onGrid(v, `${path}[${i}]`, k));
112
+ const o = value as Record<string, unknown>;
113
+ const out: Record<string, unknown> = {};
114
+ for (const key of Object.keys(o)) out[key] = onGrid(o[key], `${path}.${key}`, k);
115
+ return out;
116
+ }
117
+
118
+ /**
119
+ * This function gives the hash of an output: the first 16 hex characters of the SHA-256 of its canonical form.
120
+ * Before the canonical form, each number goes to the nearest point of the grid, and `-0` becomes `0`.
121
+ * With the default grid, the rounding is `round3` of `packing`: `Math.round(x * 1000) / 1000`.
122
+ * The function throws a `RangeError` for `NaN` and for an infinite number.
123
+ *
124
+ * The grid can split two outputs. Two numbers near a rounding boundary can go to two different points.
125
+ * The grid can also merge two outputs whose numbers go to the same points. This merge is intentional: it removes the noise of floating-point numbers.
126
+ */
127
+ export function outputHash(value: unknown, options: HashOptions = {}): string {
128
+ return sha256Hex(canonicalJson(onGrid(value, "$", gridFactor(options)))).slice(0, 16);
129
+ }
130
+
131
+ /**
132
+ * This function gives the hash of a document. It uses the rule of `outputHash`, but the form does not include the field `hash` of the document.
133
+ * With the default grid, it is the input hash of `docs/FORMAT.md` of `packing`.
134
+ */
135
+ export function documentHash(doc: object, options: HashOptions = {}): string {
136
+ const { hash: _hash, ...rest } = doc as { readonly hash?: unknown };
137
+ return outputHash(rest, options);
138
+ }
139
+
140
+ /** One output group: the candidates with the same output hash on each decision input. */
141
+ export interface OutputGroup {
142
+ /** The id of the group: its first member. */
143
+ readonly id: string;
144
+ /** The ids of the candidates of the group, in sorted order. */
145
+ readonly members: readonly string[];
146
+ }
147
+
148
+ /**
149
+ * This function gives the output groups of the candidates. The key of `hashes` is the id of a candidate.
150
+ * Its value is the list of the output hashes of the candidate, in the order of the decision inputs.
151
+ * Candidates with equal lists make one group. The groups are in the order of their ids.
152
+ * The function throws a `RangeError` when two lists have different lengths.
153
+ */
154
+ export function outputGroups(hashes: Readonly<Record<string, readonly string[]>>): OutputGroup[] {
155
+ const byList = new Map<string, string[]>();
156
+ let first: string | undefined;
157
+ for (const id of Object.keys(hashes)) {
158
+ const list = hashes[id]!;
159
+ if (first === undefined) first = id;
160
+ else if (list.length !== hashes[first]!.length)
161
+ throw new RangeError(`The candidate "${id}" has ${list.length} hashes, but "${first}" has ${hashes[first]!.length}.`);
162
+ const key = JSON.stringify(list);
163
+ const members = byList.get(key);
164
+ if (members) members.push(id);
165
+ else byList.set(key, [id]);
166
+ }
167
+ const groups = [...byList.values()].map((members): OutputGroup => {
168
+ const sorted = [...members].sort();
169
+ return { id: sorted[0]!, members: sorted };
170
+ });
171
+ return groups.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
172
+ }
173
+
174
+ /** The evidence that a candidate is exercised on its decision inputs. */
175
+ export interface ExerciseEvidence {
176
+ /** The move counts of the candidate, by name. A record keeps each one as the trace measure `move.<name>`. */
177
+ readonly moves?: Readonly<Record<string, number>>;
178
+ /** The output hashes of the candidate, in the order of the decision inputs. */
179
+ readonly hashes?: readonly string[];
180
+ /** The output hashes of the base of the candidate, in the same order. */
181
+ readonly baseHashes?: readonly string[];
182
+ }
183
+
184
+ /**
185
+ * This function tells if a candidate is exercised. It gives `true` when a move count is more than 0.
186
+ * It also gives `true` when `hashes` and `baseHashes` are different at one place or more.
187
+ * Without evidence, it gives `false`.
188
+ * The function throws a `RangeError` when only one of the two lists is given, or when their lengths are different.
189
+ */
190
+ export function exercised(evidence: ExerciseEvidence): boolean {
191
+ const { moves, hashes, baseHashes } = evidence;
192
+ if ((hashes === undefined) !== (baseHashes === undefined)) throw new RangeError("The evidence must have both hashes and baseHashes, or neither of them.");
193
+ if (hashes && baseHashes && hashes.length !== baseHashes.length)
194
+ throw new RangeError(`The candidate has ${hashes.length} hashes, but its base has ${baseHashes.length}.`);
195
+ if (moves && Object.values(moves).some((n) => n > 0)) return true;
196
+ if (hashes && baseHashes) return hashes.some((h, i) => h !== baseHashes[i]);
197
+ return false;
198
+ }
package/src/index.ts CHANGED
@@ -9,3 +9,5 @@ export * from "./measures.ts";
9
9
  export * from "./strategy.ts";
10
10
  export * from "./random.ts";
11
11
  export * from "./edges.ts";
12
+ export * from "./hash.ts";
13
+ export * from "./portfolio.ts";
@@ -0,0 +1,300 @@
1
+ import { outputHash } from "./hash.ts";
2
+ import { compareMeasure, type MeasureSpec } from "./measures.ts";
3
+
4
+ /** The part of a `MeasureSpec` that a pick uses: the direction and the `minEffect`. */
5
+ export type PickSpec = Pick<MeasureSpec, "direction" | "minEffect">;
6
+
7
+ /** A member of a portfolio: a function with an id. A portfolio is also a member. */
8
+ export interface Member<I, O> {
9
+ readonly id: string;
10
+ /** This function gives the output of the member for an input. */
11
+ run(input: I): O;
12
+ }
13
+
14
+ /** One level of the key of a portfolio: a measure of an output. */
15
+ export interface PortfolioLevel<I, O> {
16
+ /** The id of the level. The id `hard` is reserved. */
17
+ readonly id: string;
18
+ readonly spec: PickSpec;
19
+ /** This function gives the value of the measure for the output of a member. The value must be finite. */
20
+ value(input: I, output: O): number;
21
+ }
22
+
23
+ /** The key of a portfolio: the hard level, then the other levels in order. */
24
+ export interface PortfolioKey<I, O> {
25
+ /** This function gives the count of hard breaks. It is level 0, with the direction `lower` and the `minEffect` 0. */
26
+ hard(input: I, output: O): number;
27
+ /** The levels after the hard level, in the order of the pick. */
28
+ readonly levels: readonly PortfolioLevel<I, O>[];
29
+ /** This function gives the hash of an output. The default is `outputHash` with the default grid. */
30
+ hash?(output: O): string;
31
+ }
32
+
33
+ /** The options of a portfolio. */
34
+ export interface PortfolioOptions {
35
+ /** The time limit in milliseconds, 0 or more. The first member always starts. A member that starts always finishes. */
36
+ readonly budgetMs?: number;
37
+ /** The largest count of members that run, a whole number of 1 or more. The other members are cut. */
38
+ readonly maxMembers?: number;
39
+ /** The clock in milliseconds. The default is `performance.now`, or `Date.now` without `performance`. */
40
+ readonly now?: () => number;
41
+ }
42
+
43
+ /** The status of a member in a pick. */
44
+ export type MemberStatus = "ran" | "cut" | "error";
45
+
46
+ /** The trace of one member in a pick. */
47
+ export interface MemberTrace {
48
+ readonly id: string;
49
+ readonly status: MemberStatus;
50
+ /** The value of each level, with the key `hard` for the hard level. Only a member with the status `ran` has values. */
51
+ readonly values?: Readonly<Record<string, number>>;
52
+ /** The time of the run of the member, in milliseconds. */
53
+ readonly ms?: number;
54
+ /** The hash of the output of the member. */
55
+ readonly outputHash?: string;
56
+ /** The message of the error of a member with the status `error`. */
57
+ readonly error?: string;
58
+ }
59
+
60
+ /** The trace of one pick of a portfolio. */
61
+ export interface PortfolioTrace {
62
+ /** One entry for each member, in the order of the members. A cut member is also in the list. */
63
+ readonly members: readonly MemberTrace[];
64
+ readonly winner: string;
65
+ /** The index of the winner in the list of the members. */
66
+ readonly winnerIndex: number;
67
+ /** The value `true` tells that the winner is not member 0 and has a different output hash. */
68
+ readonly picked: boolean;
69
+ /** The time of the full pick, in milliseconds. */
70
+ readonly totalMs: number;
71
+ readonly budgetMs?: number;
72
+ readonly maxMembers?: number;
73
+ /** The count of the cut members. */
74
+ readonly cut: number;
75
+ /** The value `false` tells that the time limit cut one member or more. Then a run on a different computer can give a different pick. */
76
+ readonly deterministic: boolean;
77
+ /** The ids of the members that stay in after each level of `selectBest`. The first list is for the hard level. */
78
+ readonly kept: readonly (readonly string[])[];
79
+ }
80
+
81
+ /** The result of one pick of a portfolio: the output of the winner, its id and the trace. */
82
+ export interface PortfolioPick<O> {
83
+ readonly output: O;
84
+ readonly winner: string;
85
+ readonly trace: PortfolioTrace;
86
+ }
87
+
88
+ /** A portfolio: a member that runs its members and gives the output of the best member. */
89
+ export interface Portfolio<I, O> extends Member<I, O> {
90
+ /** This function starts the members on an input and gives the output of the winner with the trace. */
91
+ pick(input: I): PortfolioPick<O>;
92
+ }
93
+
94
+ /** A candidate of `selectBest`: an id and the value of each level. */
95
+ export interface SelectCandidate {
96
+ readonly id: string;
97
+ readonly values: Readonly<Record<string, number>>;
98
+ }
99
+
100
+ /** A level of `selectBest`: the id of a value and its spec. */
101
+ export interface SelectLevel {
102
+ readonly id: string;
103
+ readonly spec: PickSpec;
104
+ }
105
+
106
+ /** The result of `selectBest`. */
107
+ export interface Selection {
108
+ readonly winner: string;
109
+ /** The ids of the candidates that stay in after each level, in the given order. */
110
+ readonly kept: readonly (readonly string[])[];
111
+ }
112
+
113
+ /**
114
+ * This function picks the best candidate. For each level, it finds the best value of the candidates that stay in.
115
+ * It keeps each candidate that is not worse than that best value by `compareMeasure`.
116
+ * The winner is the first candidate, in the given order, that stays in after the last level.
117
+ * Without levels, the winner is the first candidate and `kept` is empty.
118
+ *
119
+ * The reason for the rule: a scan that compares each candidate with the current winner is not transitive, because of the `minEffect` ties.
120
+ * Such a scan can drift away from the best value. An example: "a" is lower with the `minEffect` 0.005, and "b" is higher.
121
+ * The candidates A (0, 1), B (0.004, 2) and C (0.008, 3) give the winner B.
122
+ * The scan picks C, but A is better than C on "a".
123
+ *
124
+ * The function throws a `RangeError` for an empty list of candidates, for a missing value, and for a value that is not finite.
125
+ */
126
+ export function selectBest(candidates: readonly SelectCandidate[], levels: readonly SelectLevel[]): Selection {
127
+ if (candidates.length === 0) throw new RangeError("selectBest: the list of candidates is empty.");
128
+ for (const level of levels) {
129
+ for (const c of candidates) {
130
+ const v = c.values[level.id];
131
+ if (typeof v !== "number") throw new RangeError(`selectBest: the candidate "${c.id}" has no value for the level "${level.id}".`);
132
+ if (!Number.isFinite(v)) throw new RangeError(`selectBest: the candidate "${c.id}" has the value ${v} for the level "${level.id}".`);
133
+ }
134
+ }
135
+ let alive: readonly SelectCandidate[] = candidates;
136
+ const kept: (readonly string[])[] = [];
137
+ for (const level of levels) {
138
+ const at = (c: SelectCandidate): number => c.values[level.id]!;
139
+ let best = at(alive[0]!);
140
+ for (const c of alive) best = level.spec.direction === "lower" ? Math.min(best, at(c)) : Math.max(best, at(c));
141
+ alive = alive.filter((c) => compareMeasure(at(c), best, level.spec) !== "worse");
142
+ kept.push(alive.map((c) => c.id));
143
+ }
144
+ return { winner: alive[0]!.id, kept };
145
+ }
146
+
147
+ /** This function gives the move `pick` of a pick: 1 when `picked` is true, else 0. A record keeps it as the trace measure `move.pick`. */
148
+ export function pickMoves(trace: Pick<PortfolioTrace, "picked">): { pick: number } {
149
+ return { pick: trace.picked ? 1 : 0 };
150
+ }
151
+
152
+ /** This function gives the default clock: `performance.now`, or `Date.now` without `performance`. */
153
+ function defaultClock(): () => number {
154
+ const perf = (globalThis as { readonly performance?: { now(): number } }).performance;
155
+ return perf ? () => perf.now() : () => Date.now();
156
+ }
157
+
158
+ /** This function gives the message of an error value. */
159
+ function messageOf(e: unknown): string {
160
+ return e instanceof Error ? e.message : String(e);
161
+ }
162
+
163
+ /** This function throws a `RangeError` for an empty list of members or for two members with the same id. */
164
+ function checkMembers(id: string, members: readonly Member<unknown, unknown>[]): void {
165
+ if (members.length === 0) throw new RangeError(`The portfolio "${id}" has no members.`);
166
+ const seen = new Set<string>();
167
+ for (const m of members) {
168
+ if (seen.has(m.id)) throw new RangeError(`The portfolio "${id}" has two members with the id "${m.id}".`);
169
+ seen.add(m.id);
170
+ }
171
+ }
172
+
173
+ /** The result of the run of one member. */
174
+ type Ran<O> = { readonly ok: true; readonly output: O; readonly values: Record<string, number>; readonly hash: string } | { readonly ok: false; readonly error: unknown };
175
+
176
+ /**
177
+ * This function makes a portfolio. Its `pick` starts the members in their order. `selectBest` picks the winner of the members with the status `ran`.
178
+ * The first level is the hard level, then the levels of the key follow.
179
+ * `members` is a list, or a function of the input that gives the list.
180
+ *
181
+ * A member that throws, or that gives a value that is not finite, gets the status `error`. It does not win.
182
+ * When each member has an error, `pick` throws the error of the first member.
183
+ * `docs/PORTFOLIO.md` tells the rules of the time limit, the trace and `picked`.
184
+ *
185
+ * The function throws a `RangeError` for a level with the id `hard`, for two levels with the same id and for a bad option.
186
+ * It also throws a `RangeError` for an empty list of members, or for two members with the same id.
187
+ */
188
+ export function portfolio<I, O>(
189
+ id: string,
190
+ members: readonly Member<I, O>[] | ((input: I) => readonly Member<I, O>[]),
191
+ key: PortfolioKey<I, O>,
192
+ options: PortfolioOptions = {},
193
+ ): Portfolio<I, O> {
194
+ const levelIds = new Set<string>();
195
+ for (const level of key.levels) {
196
+ if (level.id === "hard") throw new RangeError(`The portfolio "${id}" has a level with the reserved id "hard".`);
197
+ if (levelIds.has(level.id)) throw new RangeError(`The portfolio "${id}" has two levels with the id "${level.id}".`);
198
+ levelIds.add(level.id);
199
+ }
200
+ const { budgetMs, maxMembers } = options;
201
+ if (budgetMs !== undefined && !(budgetMs >= 0)) throw new RangeError(`The portfolio "${id}" has the budgetMs ${budgetMs}. It must be 0 or more.`);
202
+ if (maxMembers !== undefined && !(Number.isInteger(maxMembers) && maxMembers >= 1))
203
+ throw new RangeError(`The portfolio "${id}" has the maxMembers ${maxMembers}. It must be a whole number of 1 or more.`);
204
+ if (typeof members !== "function") checkMembers(id, members);
205
+ const levels: readonly SelectLevel[] = [{ id: "hard", spec: { direction: "lower", minEffect: 0 } }, ...key.levels.map((l) => ({ id: l.id, spec: l.spec }))];
206
+ const now = options.now ?? defaultClock();
207
+
208
+ const evaluate = (m: Member<I, O>, input: I, output: O): Ran<O> => {
209
+ const values: Record<string, number> = {};
210
+ const measure = (level: string, f: () => number): void => {
211
+ let v: number;
212
+ try {
213
+ v = f();
214
+ } catch (e) {
215
+ throw new RangeError(`The level "${level}" of the member "${m.id}" of the portfolio "${id}" throws: ${messageOf(e)}`, { cause: e });
216
+ }
217
+ if (typeof v !== "number" || !Number.isFinite(v))
218
+ throw new RangeError(`The member "${m.id}" of the portfolio "${id}" has the value ${v} for the level "${level}".`);
219
+ values[level] = v;
220
+ };
221
+ try {
222
+ measure("hard", () => key.hard(input, output));
223
+ for (const level of key.levels) measure(level.id, () => level.value(input, output));
224
+ let hash: string;
225
+ try {
226
+ hash = key.hash ? key.hash(output) : outputHash(output);
227
+ } catch (e) {
228
+ throw new RangeError(`The hash of the member "${m.id}" of the portfolio "${id}" throws: ${messageOf(e)}`, { cause: e });
229
+ }
230
+ return { ok: true, output, values, hash };
231
+ } catch (e) {
232
+ return { ok: false, error: e };
233
+ }
234
+ };
235
+
236
+ const pick = (input: I): PortfolioPick<O> => {
237
+ const list = typeof members === "function" ? members(input) : members;
238
+ if (typeof members === "function") checkMembers(id, list);
239
+ const start = now();
240
+ const traces: MemberTrace[] = [];
241
+ const outputs = new Map<string, O>();
242
+ const ran: SelectCandidate[] = [];
243
+ let firstError: { readonly error: unknown } | undefined;
244
+ let cut = 0;
245
+ let outOfTime = false;
246
+ for (let i = 0; i < list.length; i++) {
247
+ const m = list[i]!;
248
+ if (maxMembers !== undefined && i >= maxMembers) {
249
+ traces.push({ id: m.id, status: "cut" });
250
+ cut++;
251
+ continue;
252
+ }
253
+ if (i > 0 && budgetMs !== undefined && (outOfTime || now() - start >= budgetMs)) {
254
+ outOfTime = true;
255
+ traces.push({ id: m.id, status: "cut" });
256
+ cut++;
257
+ continue;
258
+ }
259
+ const t0 = now();
260
+ let result: Ran<O>;
261
+ let ms: number;
262
+ try {
263
+ const output = m.run(input);
264
+ ms = now() - t0;
265
+ result = evaluate(m, input, output);
266
+ } catch (e) {
267
+ ms = now() - t0;
268
+ result = { ok: false, error: e };
269
+ }
270
+ if (result.ok) {
271
+ traces.push({ id: m.id, status: "ran", values: result.values, ms, outputHash: result.hash });
272
+ outputs.set(m.id, result.output);
273
+ ran.push({ id: m.id, values: result.values });
274
+ } else {
275
+ traces.push({ id: m.id, status: "error", ms, error: messageOf(result.error) });
276
+ firstError ??= { error: result.error };
277
+ }
278
+ }
279
+ if (ran.length === 0) throw firstError!.error;
280
+ const { winner, kept } = selectBest(ran, levels);
281
+ const winnerIndex = traces.findIndex((t) => t.id === winner);
282
+ const base = traces[0]!;
283
+ const picked = winnerIndex > 0 && (base.status !== "ran" || traces[winnerIndex]!.outputHash !== base.outputHash);
284
+ const trace: PortfolioTrace = {
285
+ members: traces,
286
+ winner,
287
+ winnerIndex,
288
+ picked,
289
+ totalMs: now() - start,
290
+ ...(budgetMs !== undefined ? { budgetMs } : {}),
291
+ ...(maxMembers !== undefined ? { maxMembers } : {}),
292
+ cut,
293
+ deterministic: !outOfTime,
294
+ kept,
295
+ };
296
+ return { output: outputs.get(winner)!, winner, trace };
297
+ };
298
+
299
+ return { id, pick, run: (input: I): O => pick(input).output };
300
+ }
package/src/random.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { utf8 } from "./utf8.ts";
2
+
1
3
  /** This function gives a generator of random numbers in [0, 1). It is `mulberry32`, as in `docs/FORMAT.md` of `packing`. */
2
4
  export function mulberry32(seed: number): () => number {
3
5
  let s = seed >>> 0;
@@ -16,19 +18,6 @@ export function fnv1a32(text: string): number {
16
18
  return h;
17
19
  }
18
20
 
19
- /** The UTF-8 bytes of a string. The package has no runtime dependencies, thus it does not use `TextEncoder`. */
20
- function utf8(text: string): number[] {
21
- const bytes: number[] = [];
22
- for (const ch of text) {
23
- const c = ch.codePointAt(0)!;
24
- if (c < 0x80) bytes.push(c);
25
- else if (c < 0x800) bytes.push(0xc0 | (c >> 6), 0x80 | (c & 63));
26
- else if (c < 0x10000) bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
27
- else bytes.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
28
- }
29
- return bytes;
30
- }
31
-
32
21
  /** This function gives the generator of one step of a seeded generator: `mulberry32(fnv1a32("<seed>:<step>"))`. */
33
22
  export function stepRandom(seed: number, step: string): () => number {
34
23
  return mulberry32(fnv1a32(`${seed}:${step}`));
package/src/utf8.ts ADDED
@@ -0,0 +1,12 @@
1
+ /** This function gives the UTF-8 bytes of a string. The package has no runtime dependencies, thus it does not use `TextEncoder`. */
2
+ export function utf8(text: string): number[] {
3
+ const bytes: number[] = [];
4
+ for (const ch of text) {
5
+ const c = ch.codePointAt(0)!;
6
+ if (c < 0x80) bytes.push(c);
7
+ else if (c < 0x800) bytes.push(0xc0 | (c >> 6), 0x80 | (c & 63));
8
+ else if (c < 0x10000) bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
9
+ else bytes.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
10
+ }
11
+ return bytes;
12
+ }