@variance-authority/core 0.14.0 → 0.17.1
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/CHANGELOG.md +878 -0
- package/README.md +38 -17
- package/dist/attribute/component-hash.d.ts +14 -5
- package/dist/attribute/component-hash.js +14 -5
- package/dist/attribute/component-hash.js.map +1 -1
- package/dist/attribute/composition.d.ts +16 -0
- package/dist/attribute/composition.js +4 -6
- package/dist/attribute/composition.js.map +1 -1
- package/dist/attribute/index.d.ts +3 -1
- package/dist/attribute/index.js +2 -1
- package/dist/attribute/index.js.map +1 -1
- package/dist/attribute/movement.d.ts +1 -1
- package/dist/attribute/movement.js.map +1 -1
- package/dist/attribute/region.d.ts +47 -4
- package/dist/attribute/region.js +91 -22
- package/dist/attribute/region.js.map +1 -1
- package/dist/attribute/source.d.ts +10 -24
- package/dist/attribute/source.js +4 -38
- package/dist/attribute/source.js.map +1 -1
- package/dist/attribute/subject-pieces.d.ts +37 -0
- package/dist/attribute/subject-pieces.js +122 -0
- package/dist/attribute/subject-pieces.js.map +1 -0
- package/dist/compare/band.d.ts +8 -2
- package/dist/compare/band.js +7 -2
- package/dist/compare/band.js.map +1 -1
- package/dist/compare/diff/attribution.js +6 -4
- package/dist/compare/diff/attribution.js.map +1 -1
- package/dist/compare/diff/compare-nodes.js +9 -1
- package/dist/compare/diff/compare-nodes.js.map +1 -1
- package/dist/compare/diff/match.d.ts +2 -1
- package/dist/compare/diff/match.js +52 -0
- package/dist/compare/diff/match.js.map +1 -1
- package/dist/compare/parting.js +6 -4
- package/dist/compare/parting.js.map +1 -1
- package/dist/format/capture.d.ts +25 -0
- package/dist/format/capture.js +6 -0
- package/dist/format/capture.js.map +1 -1
- package/dist/format/document.d.ts +28 -5
- package/dist/format/document.js +8 -2
- package/dist/format/document.js.map +1 -1
- package/dist/format/drive.d.ts +191 -0
- package/dist/format/drive.js +343 -0
- package/dist/format/drive.js.map +1 -0
- package/dist/format/environment.d.ts +20 -2
- package/dist/format/environment.js +7 -0
- package/dist/format/environment.js.map +1 -1
- package/dist/format/index.d.ts +13 -3
- package/dist/format/index.js +8 -2
- package/dist/format/index.js.map +1 -1
- package/dist/format/intervention.d.ts +79 -0
- package/dist/format/intervention.js.map +1 -1
- package/dist/format/origin.d.ts +32 -0
- package/dist/format/origin.js +54 -0
- package/dist/format/origin.js.map +1 -0
- package/dist/format/packed.d.ts +32 -0
- package/dist/format/packed.js +23 -0
- package/dist/format/packed.js.map +1 -0
- package/dist/format/shape.d.ts +31 -0
- package/dist/format/shape.js +269 -0
- package/dist/format/shape.js.map +1 -0
- package/dist/format/snapshot.d.ts +2 -0
- package/dist/format/snapshot.js.map +1 -1
- package/dist/format/stabilize.d.ts +35 -6
- package/dist/format/stabilize.js +58 -103
- package/dist/format/stabilize.js.map +1 -1
- package/dist/format/steps.d.ts +44 -0
- package/dist/format/steps.js +67 -0
- package/dist/format/steps.js.map +1 -0
- package/dist/format/waits.d.ts +12 -0
- package/dist/format/waits.js +156 -0
- package/dist/format/waits.js.map +1 -0
- package/dist/format/width.d.ts +35 -0
- package/dist/format/width.js +38 -0
- package/dist/format/width.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/judge/docket.js +1 -0
- package/dist/judge/docket.js.map +1 -1
- package/dist/judge/index.d.ts +1 -0
- package/dist/judge/index.js +1 -0
- package/dist/judge/index.js.map +1 -1
- package/dist/judge/restamp.d.ts +20 -0
- package/dist/judge/restamp.js +20 -0
- package/dist/judge/restamp.js.map +1 -0
- package/dist/nearest.d.ts +46 -0
- package/dist/nearest.js +102 -0
- package/dist/nearest.js.map +1 -0
- package/dist/relate/before.d.ts +19 -24
- package/dist/relate/before.js +37 -39
- package/dist/relate/before.js.map +1 -1
- package/dist/relate/beyond.d.ts +59 -0
- package/dist/relate/beyond.js +98 -0
- package/dist/relate/beyond.js.map +1 -0
- package/dist/relate/carried.d.ts +122 -0
- package/dist/relate/carried.js +279 -0
- package/dist/relate/carried.js.map +1 -0
- package/dist/relate/graph.d.ts +14 -12
- package/dist/relate/graph.js +12 -11
- package/dist/relate/graph.js.map +1 -1
- package/dist/relate/index.d.ts +3 -1
- package/dist/relate/index.js +3 -1
- package/dist/relate/index.js.map +1 -1
- package/dist/relate/merkle.js +2 -11
- package/dist/relate/merkle.js.map +1 -1
- package/dist/relate/narrow.d.ts +2 -2
- package/dist/relate/narrow.js +26 -73
- package/dist/relate/narrow.js.map +1 -1
- package/dist/relate/reach.d.ts +6 -6
- package/dist/relate/reach.js +10 -53
- package/dist/relate/reach.js.map +1 -1
- package/dist/relate/records.d.ts +6 -5
- package/dist/relate/records.js +3 -2
- package/dist/relate/records.js.map +1 -1
- package/dist/rules/index.d.ts +3 -3
- package/dist/rules/index.js +3 -3
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/normalize/cascade.d.ts +12 -0
- package/dist/rules/normalize/cascade.js +34 -16
- package/dist/rules/normalize/cascade.js.map +1 -1
- package/dist/rules/normalize/fixture.d.ts +3 -0
- package/dist/rules/normalize/fixture.js +3 -1
- package/dist/rules/normalize/fixture.js.map +1 -1
- package/dist/rules/normalize/index.d.ts +2 -2
- package/dist/rules/normalize/index.js +9 -2
- package/dist/rules/normalize/index.js.map +1 -1
- package/dist/rules/normalize/value.d.ts +13 -2
- package/dist/rules/normalize/value.js +42 -8
- package/dist/rules/normalize/value.js.map +1 -1
- package/dist/rules/normalize/wrapper.js +5 -0
- package/dist/rules/normalize/wrapper.js.map +1 -1
- package/dist/rules/ruleset.d.ts +24 -4
- package/dist/rules/ruleset.js +24 -4
- package/dist/rules/ruleset.js.map +1 -1
- package/dist/segment/index.d.ts +20 -4
- package/dist/segment/index.js +28 -7
- package/dist/segment/index.js.map +1 -1
- package/dist/shard/index.d.ts +11 -0
- package/dist/shard/index.js +11 -0
- package/dist/shard/index.js.map +1 -0
- package/dist/shard/placement.d.ts +119 -0
- package/dist/shard/placement.js +156 -0
- package/dist/shard/placement.js.map +1 -0
- package/dist/share/index.d.ts +1 -1
- package/dist/share/index.js +1 -1
- package/dist/share/index.js.map +1 -1
- package/package.json +8 -1
package/dist/segment/index.d.ts
CHANGED
|
@@ -17,9 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* The reader that matters next is not this one. Every field is a run of
|
|
19
19
|
* fixed-width little-endian values at a computable offset, so a reader in
|
|
20
|
-
* another language takes a slice where this one takes a loop
|
|
21
|
-
* [`record-format.ts`](../../../sense/src/test-selection/record-format.ts)
|
|
22
|
-
* already made for the coverage segments.
|
|
20
|
+
* another language takes a slice where this one takes a loop.
|
|
23
21
|
*
|
|
24
22
|
* ## Why the header is JSON
|
|
25
23
|
*
|
|
@@ -58,9 +56,27 @@ export interface OpenSegment {
|
|
|
58
56
|
/** Refuse the file, in the words the caller's format uses. */
|
|
59
57
|
reject(): Error;
|
|
60
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* A segment of the named format, written in a version this reader does not
|
|
61
|
+
* read. Not a damaged file: the bytes are what another release wrote, and a
|
|
62
|
+
* caller that rebuilds or refuses says which release that was.
|
|
63
|
+
*/
|
|
64
|
+
export declare class OtherSegmentVersion extends Error {
|
|
65
|
+
/** The version the segment's header names. */
|
|
66
|
+
readonly written: number;
|
|
67
|
+
/** The version this reader reads. */
|
|
68
|
+
readonly reads: number;
|
|
69
|
+
readonly name = "OtherSegmentVersion";
|
|
70
|
+
constructor(what: string,
|
|
71
|
+
/** The version the segment's header names. */
|
|
72
|
+
written: number,
|
|
73
|
+
/** The version this reader reads. */
|
|
74
|
+
reads: number);
|
|
75
|
+
}
|
|
61
76
|
/**
|
|
62
77
|
* Open a segment, refusing anything that is not one of the named format and
|
|
63
|
-
* version.
|
|
78
|
+
* version. Another version of the named format is refused with
|
|
79
|
+
* {@link OtherSegmentVersion}, so a caller can tell it from a damaged file.
|
|
64
80
|
*
|
|
65
81
|
* `what` is the noun a rejection is phrased with — "source index", "suite index"
|
|
66
82
|
* — because the one thing a caller of this can usefully be told is which reader
|
package/dist/segment/index.js
CHANGED
|
@@ -17,9 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* The reader that matters next is not this one. Every field is a run of
|
|
19
19
|
* fixed-width little-endian values at a computable offset, so a reader in
|
|
20
|
-
* another language takes a slice where this one takes a loop
|
|
21
|
-
* [`record-format.ts`](../../../sense/src/test-selection/record-format.ts)
|
|
22
|
-
* already made for the coverage segments.
|
|
20
|
+
* another language takes a slice where this one takes a loop.
|
|
23
21
|
*
|
|
24
22
|
* ## Why the header is JSON
|
|
25
23
|
*
|
|
@@ -83,9 +81,29 @@ function concat(parts) {
|
|
|
83
81
|
}
|
|
84
82
|
return result;
|
|
85
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* A segment of the named format, written in a version this reader does not
|
|
86
|
+
* read. Not a damaged file: the bytes are what another release wrote, and a
|
|
87
|
+
* caller that rebuilds or refuses says which release that was.
|
|
88
|
+
*/
|
|
89
|
+
export class OtherSegmentVersion extends Error {
|
|
90
|
+
written;
|
|
91
|
+
reads;
|
|
92
|
+
name = 'OtherSegmentVersion';
|
|
93
|
+
constructor(what,
|
|
94
|
+
/** The version the segment's header names. */
|
|
95
|
+
written,
|
|
96
|
+
/** The version this reader reads. */
|
|
97
|
+
reads) {
|
|
98
|
+
super(`a variance-authority ${what} written in format version ${written}, and this reader reads version ${reads}`);
|
|
99
|
+
this.written = written;
|
|
100
|
+
this.reads = reads;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
86
103
|
/**
|
|
87
104
|
* Open a segment, refusing anything that is not one of the named format and
|
|
88
|
-
* version.
|
|
105
|
+
* version. Another version of the named format is refused with
|
|
106
|
+
* {@link OtherSegmentVersion}, so a caller can tell it from a damaged file.
|
|
89
107
|
*
|
|
90
108
|
* `what` is the noun a rejection is phrased with — "source index", "suite index"
|
|
91
109
|
* — because the one thing a caller of this can usefully be told is which reader
|
|
@@ -112,9 +130,12 @@ export function openSegment(format, version, input, what) {
|
|
|
112
130
|
catch {
|
|
113
131
|
throw reject();
|
|
114
132
|
}
|
|
115
|
-
if (header.format !== format
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
if (header.format !== format)
|
|
134
|
+
throw reject();
|
|
135
|
+
if (header.version !== version) {
|
|
136
|
+
throw typeof header.version === 'number' ? new OtherSegmentVersion(what, header.version, version) : reject();
|
|
137
|
+
}
|
|
138
|
+
if (!validSections(header.sections, raw.length - 4 - headerLength))
|
|
118
139
|
throw reject();
|
|
119
140
|
const base = 4 + headerLength;
|
|
120
141
|
const found = new Map(header.sections.map((section) => [section.name, section]));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/segment/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `@variance-authority/core/segment` — a column of facts, as bytes.\n *\n * Two things in this repository already had to be written down and read back on\n * another machine: the source index a scan reuses, and now the suite index a run\n * leaves behind. Both are the same shape underneath — a pile of repeated strings\n * and a pile of small integers — and both were going to grow the same four\n * hundred lines of offset arithmetic independently.\n *\n * So the arithmetic is here and the schemas are not. This module knows about\n * named sections, alignment, interned strings and the validation that a decode\n * must perform before it believes a file. It knows nothing about what any\n * section means, which is the property that lets one format change without\n * touching the other.\n *\n * ## Why columns\n *\n * The reader that matters next is not this one. Every field is a run of\n * fixed-width little-endian values at a computable offset, so a reader in\n * another language takes a slice where this one takes a loop — the same bargain\n * [`record-format.ts`](../../../sense/src/test-selection/record-format.ts)\n * already made for the coverage segments.\n *\n * ## Why the header is JSON\n *\n * It is the one part that is read before anything is known, it is a few hundred\n * bytes against a payload measured in megabytes, and being self-describing is\n * what lets a decoder reject a file by name instead of by crash. The payload —\n * the part that repeats — never is.\n *\n * ## The one rule\n *\n * A malformed reference rejects the whole file. There is no partial read and no\n * salvage: a segment is a cache or a baseline, both of which may be rebuilt, and\n * half of either is worse than neither.\n */\n\nconst ALIGNMENT = 8;\n\n// Declared rather than imported. This package takes no platform's types — it is\n// the layer every other one is allowed to run wherever it likes — and these two\n// are the standard text codecs that every runtime it targets already has. A\n// `Buffer` would have been shorter and would have made `core` a Node package.\ndeclare const TextEncoder: { new (): { encode(input: string): Uint8Array } };\ndeclare const TextDecoder: { new (): { decode(input: Uint8Array): string } };\n\nconst utf8 = { encode: new TextEncoder(), decode: new TextDecoder() };\n\n/** The sentinel a `u32` column uses for \"no value here\". */\nexport const NONE = 0xffff_ffff;\n\ninterface Section {\n readonly name: string;\n readonly offset: number;\n readonly length: number;\n readonly width: 1 | 4;\n}\n\ninterface Header {\n readonly format: string;\n readonly version: number;\n readonly sections: readonly Section[];\n}\n\n/** A column, by the width its element type already declares. */\nexport type Column = Uint8Array | Uint32Array;\n\n/**\n * Write named columns as one aligned segment.\n *\n * Width is taken from the array's own type rather than from a convention about\n * the section's name, so a column that is read back as `u8` is one that was\n * written as `Uint8Array` and a mismatch is a compile error rather than a\n * decode that succeeds against the wrong stride.\n */\nexport function encodeSegment(\n format: string,\n version: number,\n columns: Readonly<Record<string, Column>>,\n): Uint8Array {\n const chunks: Uint8Array[] = [];\n const sections: Section[] = [];\n let offset = 0;\n\n for (const [name, column] of Object.entries(columns)) {\n const value = new Uint8Array(column.buffer, column.byteOffset, column.byteLength);\n sections.push({\n name,\n offset,\n length: value.length,\n width: column instanceof Uint8Array ? 1 : 4,\n });\n chunks.push(value);\n offset += value.length;\n const padding = aligned(offset) - offset;\n if (padding > 0) chunks.push(new Uint8Array(padding));\n offset += padding;\n }\n\n const header: Header = { format, version, sections };\n const encoded = utf8.encode.encode(JSON.stringify(header));\n // The header is padded rather than merely written, so every section offset it\n // names is an aligned offset into the file and not only into the payload.\n const headerLength = aligned(4 + encoded.length) - 4;\n const prefix = new Uint8Array(4);\n new DataView(prefix.buffer).setUint32(0, headerLength, true);\n return concat([prefix, encoded, new Uint8Array(headerLength - encoded.length), ...chunks]);\n}\n\nfunction concat(parts: readonly Uint8Array[]): Uint8Array {\n const result = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));\n let offset = 0;\n for (const part of parts) {\n result.set(part, offset);\n offset += part.length;\n }\n return result;\n}\n\n/** Columns of one opened segment, by name. Every accessor may reject the file. */\nexport interface OpenSegment {\n /** A `u8` column. Absent or of the wrong width rejects the file. */\n u8(name: string): Uint8Array;\n /** A `u32` column. Absent or of the wrong width rejects the file. */\n u32(name: string): Uint32Array;\n /** A `u32` column a writer of this version may legitimately not have written. */\n maybeU32(name: string): Uint32Array;\n /** Refuse the file, in the words the caller's format uses. */\n reject(): Error;\n}\n\n/**\n * Open a segment, refusing anything that is not one of the named format and\n * version.\n *\n * `what` is the noun a rejection is phrased with — \"source index\", \"suite index\"\n * — because the one thing a caller of this can usefully be told is which reader\n * refused, and a shared message would name none of them.\n */\nexport function openSegment(\n format: string,\n version: number,\n input: Uint8Array,\n what: string,\n): OpenSegment {\n const reject = (): Error => new Error(`not a variance-authority ${what}`);\n // Copied when it does not start on an alignment boundary, because the whole\n // point of the padding is that a `u32` column can be a view rather than a\n // parse, and a view is only legal at a multiple of its own width. A read that\n // lands mid-pool — which is what a small file off a disk usually is — pays one\n // copy for that guarantee instead of losing it.\n const raw = input.byteOffset % ALIGNMENT === 0 ? input : input.slice();\n if (raw.length < 4) throw reject();\n const headerLength = new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint32(0, true);\n if (headerLength > raw.length - 4) throw reject();\n\n let header: Header;\n try {\n const text = utf8.decode.decode(raw.subarray(4, 4 + headerLength));\n header = JSON.parse(text.replace(/\\0+$/, '')) as Header;\n } catch {\n throw reject();\n }\n if (\n header.format !== format ||\n header.version !== version ||\n !validSections(header.sections, raw.length - 4 - headerLength)\n ) throw reject();\n\n const base = 4 + headerLength;\n const found = new Map(header.sections.map((section) => [section.name, section]));\n const at = (name: string, width: 1 | 4): Section => {\n const section = found.get(name);\n if (section === undefined || section.width !== width) throw reject();\n return section;\n };\n\n return {\n reject,\n u8(name) {\n const section = at(name, 1);\n return new Uint8Array(raw.buffer, raw.byteOffset + base + section.offset, section.length);\n },\n u32(name) {\n const section = at(name, 4);\n if (section.length % 4 !== 0) throw reject();\n return new Uint32Array(\n raw.buffer,\n raw.byteOffset + base + section.offset,\n section.length / 4,\n );\n },\n maybeU32(name) {\n const section = found.get(name);\n if (section === undefined) return new Uint32Array();\n if (section.width !== 4 || section.length % 4 !== 0) throw reject();\n return new Uint32Array(\n raw.buffer,\n raw.byteOffset + base + section.offset,\n section.length / 4,\n );\n },\n };\n}\n\n/**\n * Every distinct string, sorted, and the number each one is written as.\n *\n * Sorted rather than first-seen because two encodes of equal facts must produce\n * equal bytes: a segment is content-addressed by whoever caches it, and an\n * identical index that hashes differently defeats every transport that would\n * otherwise have skipped the upload.\n */\nexport function intern(values: Iterable<string>): {\n readonly strings: readonly string[];\n readonly id: (value: string) => number;\n /** `NONE` for `undefined`, so an absent field costs a column and not a branch. */\n readonly optionalId: (value: string | undefined) => number;\n} {\n const strings = [...new Set(values)].sort(codeUnitOrder);\n const ids = new Map(strings.map((value, index) => [value, index]));\n const id = (value: string): number => {\n const found = ids.get(value);\n if (found === undefined) throw new Error(`string not interned: ${JSON.stringify(value)}`);\n return found;\n };\n return { strings, id, optionalId: (value) => (value === undefined ? NONE : id(value)) };\n}\n\n/** The two columns a dictionary is written as: the bytes, and where each one ends. */\nexport function stringColumns(strings: readonly string[]): {\n readonly blob: Uint8Array;\n readonly off: Uint32Array;\n} {\n const encoded = strings.map((value) => utf8.encode.encode(value));\n return { blob: concat(encoded), off: offsetsOf(encoded.map((value) => value.length)) };\n}\n\n/** Read strings back out of a dictionary, rejecting any id the blob cannot hold. */\nexport function stringReader(\n blob: Uint8Array,\n off: Uint32Array,\n reject: () => Error,\n): { readonly text: (id: number) => string; readonly optional: (id: number) => string | undefined } {\n validateOffsets(off, blob.length, off.length - 1, reject);\n const text = (id: number): string => {\n const start = off[id];\n const end = off[id + 1];\n if (start === undefined || end === undefined) throw reject();\n return utf8.decode.decode(blob.subarray(start, end));\n };\n return { text, optional: (id) => (id === NONE ? undefined : text(id)) };\n}\n\n/** A running-total column: `result[n]` is where row `n` starts, `result.at(-1)` the end. */\nexport function offsetsOf(lengths: readonly number[]): Uint32Array {\n const result = new Uint32Array(lengths.length + 1);\n for (let index = 0; index < lengths.length; index += 1) {\n result[index + 1] = result[index]! + lengths[index]!;\n }\n return result;\n}\n\n/**\n * Check an offset column before any row is read through it.\n *\n * Checked here rather than per row because an offset column is the one thing in\n * a segment that can turn a corrupt file into a *plausible* value: a pair that\n * runs backwards reads a neighbouring row's data as this row's, and nothing\n * downstream would ever notice.\n */\nexport function validateOffsets(\n column: Uint32Array,\n end: number,\n rows: number,\n reject: () => Error,\n): void {\n if (column.length !== rows + 1 || column[0] !== 0 || column[column.length - 1] !== end) {\n throw reject();\n }\n for (let index = 1; index < column.length; index += 1) {\n if (column[index]! < column[index - 1]!) throw reject();\n }\n}\n\n/** Columns that describe the same rows must agree about how many there are. */\nexport function sameLength(\n length: number,\n columns: readonly { readonly length: number }[],\n reject: () => Error,\n): void {\n if (columns.some((column) => column.length !== length)) throw reject();\n}\n\n/** The indices row `row` owns in the column an offset column points into. */\nexport function rangeOf(offsets: Uint32Array, row: number, reject: () => Error): number[] {\n const start = offsets[row];\n const end = offsets[row + 1];\n if (start === undefined || end === undefined) throw reject();\n return Array.from({ length: end - start }, (_, index) => start + index);\n}\n\n/** A stored boolean, refusing the file for anything that is not one. */\nexport function flagOf(value: number | undefined, reject: () => Error): boolean {\n if (value !== 0 && value !== 1) throw reject();\n return value === 1;\n}\n\nfunction validSections(sections: readonly Section[], available: number): boolean {\n if (!Array.isArray(sections) || sections.length === 0) return false;\n if (new Set(sections.map((section) => section.name)).size !== sections.length) return false;\n const ordered = [...sections].sort((left, right) => left.offset - right.offset);\n let end = 0;\n for (const section of ordered) {\n if (\n typeof section.name !== 'string' ||\n !Number.isSafeInteger(section.offset) ||\n !Number.isSafeInteger(section.length) ||\n section.offset < end ||\n section.offset % ALIGNMENT !== 0 ||\n section.length < 0 ||\n section.length > available - section.offset ||\n (section.width !== 1 && section.width !== 4)\n ) return false;\n end = section.offset + section.length;\n }\n return ordered[0]?.offset === 0;\n}\n\nfunction aligned(value: number): number {\n return Math.ceil(value / ALIGNMENT) * ALIGNMENT;\n}\n\n/**\n * Order two strings by UTF-16 code unit, which is the order a segment is sorted in.\n *\n * Exported because a schema that sorts its own values before interning them has\n * to sort them the same way this does, and `localeCompare` is the trap: it is\n * locale-dependent, so an index written on one machine would interleave\n * differently on another and two encodes of equal facts would stop producing\n * equal bytes.\n */\nexport function codeUnitOrder(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/segment/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `@variance-authority/core/segment` — a column of facts, as bytes.\n *\n * Two things in this repository already had to be written down and read back on\n * another machine: the source index a scan reuses, and now the suite index a run\n * leaves behind. Both are the same shape underneath — a pile of repeated strings\n * and a pile of small integers — and both were going to grow the same four\n * hundred lines of offset arithmetic independently.\n *\n * So the arithmetic is here and the schemas are not. This module knows about\n * named sections, alignment, interned strings and the validation that a decode\n * must perform before it believes a file. It knows nothing about what any\n * section means, which is the property that lets one format change without\n * touching the other.\n *\n * ## Why columns\n *\n * The reader that matters next is not this one. Every field is a run of\n * fixed-width little-endian values at a computable offset, so a reader in\n * another language takes a slice where this one takes a loop.\n *\n * ## Why the header is JSON\n *\n * It is the one part that is read before anything is known, it is a few hundred\n * bytes against a payload measured in megabytes, and being self-describing is\n * what lets a decoder reject a file by name instead of by crash. The payload —\n * the part that repeats — never is.\n *\n * ## The one rule\n *\n * A malformed reference rejects the whole file. There is no partial read and no\n * salvage: a segment is a cache or a baseline, both of which may be rebuilt, and\n * half of either is worse than neither.\n */\n\nconst ALIGNMENT = 8;\n\n// Declared rather than imported. This package takes no platform's types — it is\n// the layer every other one is allowed to run wherever it likes — and these two\n// are the standard text codecs that every runtime it targets already has. A\n// `Buffer` would have been shorter and would have made `core` a Node package.\ndeclare const TextEncoder: { new (): { encode(input: string): Uint8Array } };\ndeclare const TextDecoder: { new (): { decode(input: Uint8Array): string } };\n\nconst utf8 = { encode: new TextEncoder(), decode: new TextDecoder() };\n\n/** The sentinel a `u32` column uses for \"no value here\". */\nexport const NONE = 0xffff_ffff;\n\ninterface Section {\n readonly name: string;\n readonly offset: number;\n readonly length: number;\n readonly width: 1 | 4;\n}\n\ninterface Header {\n readonly format: string;\n readonly version: number;\n readonly sections: readonly Section[];\n}\n\n/** A column, by the width its element type already declares. */\nexport type Column = Uint8Array | Uint32Array;\n\n/**\n * Write named columns as one aligned segment.\n *\n * Width is taken from the array's own type rather than from a convention about\n * the section's name, so a column that is read back as `u8` is one that was\n * written as `Uint8Array` and a mismatch is a compile error rather than a\n * decode that succeeds against the wrong stride.\n */\nexport function encodeSegment(\n format: string,\n version: number,\n columns: Readonly<Record<string, Column>>,\n): Uint8Array {\n const chunks: Uint8Array[] = [];\n const sections: Section[] = [];\n let offset = 0;\n\n for (const [name, column] of Object.entries(columns)) {\n const value = new Uint8Array(column.buffer, column.byteOffset, column.byteLength);\n sections.push({\n name,\n offset,\n length: value.length,\n width: column instanceof Uint8Array ? 1 : 4,\n });\n chunks.push(value);\n offset += value.length;\n const padding = aligned(offset) - offset;\n if (padding > 0) chunks.push(new Uint8Array(padding));\n offset += padding;\n }\n\n const header: Header = { format, version, sections };\n const encoded = utf8.encode.encode(JSON.stringify(header));\n // The header is padded rather than merely written, so every section offset it\n // names is an aligned offset into the file and not only into the payload.\n const headerLength = aligned(4 + encoded.length) - 4;\n const prefix = new Uint8Array(4);\n new DataView(prefix.buffer).setUint32(0, headerLength, true);\n return concat([prefix, encoded, new Uint8Array(headerLength - encoded.length), ...chunks]);\n}\n\nfunction concat(parts: readonly Uint8Array[]): Uint8Array {\n const result = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));\n let offset = 0;\n for (const part of parts) {\n result.set(part, offset);\n offset += part.length;\n }\n return result;\n}\n\n/** Columns of one opened segment, by name. Every accessor may reject the file. */\nexport interface OpenSegment {\n /** A `u8` column. Absent or of the wrong width rejects the file. */\n u8(name: string): Uint8Array;\n /** A `u32` column. Absent or of the wrong width rejects the file. */\n u32(name: string): Uint32Array;\n /** A `u32` column a writer of this version may legitimately not have written. */\n maybeU32(name: string): Uint32Array;\n /** Refuse the file, in the words the caller's format uses. */\n reject(): Error;\n}\n\n/**\n * A segment of the named format, written in a version this reader does not\n * read. Not a damaged file: the bytes are what another release wrote, and a\n * caller that rebuilds or refuses says which release that was.\n */\nexport class OtherSegmentVersion extends Error {\n override readonly name = 'OtherSegmentVersion';\n\n constructor(\n what: string,\n /** The version the segment's header names. */\n readonly written: number,\n /** The version this reader reads. */\n readonly reads: number,\n ) {\n super(`a variance-authority ${what} written in format version ${written}, and this reader reads version ${reads}`);\n }\n}\n\n/**\n * Open a segment, refusing anything that is not one of the named format and\n * version. Another version of the named format is refused with\n * {@link OtherSegmentVersion}, so a caller can tell it from a damaged file.\n *\n * `what` is the noun a rejection is phrased with — \"source index\", \"suite index\"\n * — because the one thing a caller of this can usefully be told is which reader\n * refused, and a shared message would name none of them.\n */\nexport function openSegment(\n format: string,\n version: number,\n input: Uint8Array,\n what: string,\n): OpenSegment {\n const reject = (): Error => new Error(`not a variance-authority ${what}`);\n // Copied when it does not start on an alignment boundary, because the whole\n // point of the padding is that a `u32` column can be a view rather than a\n // parse, and a view is only legal at a multiple of its own width. A read that\n // lands mid-pool — which is what a small file off a disk usually is — pays one\n // copy for that guarantee instead of losing it.\n const raw = input.byteOffset % ALIGNMENT === 0 ? input : input.slice();\n if (raw.length < 4) throw reject();\n const headerLength = new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint32(0, true);\n if (headerLength > raw.length - 4) throw reject();\n\n let header: Header;\n try {\n const text = utf8.decode.decode(raw.subarray(4, 4 + headerLength));\n header = JSON.parse(text.replace(/\\0+$/, '')) as Header;\n } catch {\n throw reject();\n }\n if (header.format !== format) throw reject();\n if (header.version !== version) {\n throw typeof header.version === 'number' ? new OtherSegmentVersion(what, header.version, version) : reject();\n }\n if (!validSections(header.sections, raw.length - 4 - headerLength)) throw reject();\n\n const base = 4 + headerLength;\n const found = new Map(header.sections.map((section) => [section.name, section]));\n const at = (name: string, width: 1 | 4): Section => {\n const section = found.get(name);\n if (section === undefined || section.width !== width) throw reject();\n return section;\n };\n\n return {\n reject,\n u8(name) {\n const section = at(name, 1);\n return new Uint8Array(raw.buffer, raw.byteOffset + base + section.offset, section.length);\n },\n u32(name) {\n const section = at(name, 4);\n if (section.length % 4 !== 0) throw reject();\n return new Uint32Array(\n raw.buffer,\n raw.byteOffset + base + section.offset,\n section.length / 4,\n );\n },\n maybeU32(name) {\n const section = found.get(name);\n if (section === undefined) return new Uint32Array();\n if (section.width !== 4 || section.length % 4 !== 0) throw reject();\n return new Uint32Array(\n raw.buffer,\n raw.byteOffset + base + section.offset,\n section.length / 4,\n );\n },\n };\n}\n\n/**\n * Every distinct string, sorted, and the number each one is written as.\n *\n * Sorted rather than first-seen because two encodes of equal facts must produce\n * equal bytes: a segment is content-addressed by whoever caches it, and an\n * identical index that hashes differently defeats every transport that would\n * otherwise have skipped the upload.\n */\nexport function intern(values: Iterable<string>): {\n readonly strings: readonly string[];\n readonly id: (value: string) => number;\n /** `NONE` for `undefined`, so an absent field costs a column and not a branch. */\n readonly optionalId: (value: string | undefined) => number;\n} {\n const strings = [...new Set(values)].sort(codeUnitOrder);\n const ids = new Map(strings.map((value, index) => [value, index]));\n const id = (value: string): number => {\n const found = ids.get(value);\n if (found === undefined) throw new Error(`string not interned: ${JSON.stringify(value)}`);\n return found;\n };\n return { strings, id, optionalId: (value) => (value === undefined ? NONE : id(value)) };\n}\n\n/** The two columns a dictionary is written as: the bytes, and where each one ends. */\nexport function stringColumns(strings: readonly string[]): {\n readonly blob: Uint8Array;\n readonly off: Uint32Array;\n} {\n const encoded = strings.map((value) => utf8.encode.encode(value));\n return { blob: concat(encoded), off: offsetsOf(encoded.map((value) => value.length)) };\n}\n\n/** Read strings back out of a dictionary, rejecting any id the blob cannot hold. */\nexport function stringReader(\n blob: Uint8Array,\n off: Uint32Array,\n reject: () => Error,\n): { readonly text: (id: number) => string; readonly optional: (id: number) => string | undefined } {\n validateOffsets(off, blob.length, off.length - 1, reject);\n const text = (id: number): string => {\n const start = off[id];\n const end = off[id + 1];\n if (start === undefined || end === undefined) throw reject();\n return utf8.decode.decode(blob.subarray(start, end));\n };\n return { text, optional: (id) => (id === NONE ? undefined : text(id)) };\n}\n\n/** A running-total column: `result[n]` is where row `n` starts, `result.at(-1)` the end. */\nexport function offsetsOf(lengths: readonly number[]): Uint32Array {\n const result = new Uint32Array(lengths.length + 1);\n for (let index = 0; index < lengths.length; index += 1) {\n result[index + 1] = result[index]! + lengths[index]!;\n }\n return result;\n}\n\n/**\n * Check an offset column before any row is read through it.\n *\n * Checked here rather than per row because an offset column is the one thing in\n * a segment that can turn a corrupt file into a *plausible* value: a pair that\n * runs backwards reads a neighbouring row's data as this row's, and nothing\n * downstream would ever notice.\n */\nexport function validateOffsets(\n column: Uint32Array,\n end: number,\n rows: number,\n reject: () => Error,\n): void {\n if (column.length !== rows + 1 || column[0] !== 0 || column[column.length - 1] !== end) {\n throw reject();\n }\n for (let index = 1; index < column.length; index += 1) {\n if (column[index]! < column[index - 1]!) throw reject();\n }\n}\n\n/** Columns that describe the same rows must agree about how many there are. */\nexport function sameLength(\n length: number,\n columns: readonly { readonly length: number }[],\n reject: () => Error,\n): void {\n if (columns.some((column) => column.length !== length)) throw reject();\n}\n\n/** The indices row `row` owns in the column an offset column points into. */\nexport function rangeOf(offsets: Uint32Array, row: number, reject: () => Error): number[] {\n const start = offsets[row];\n const end = offsets[row + 1];\n if (start === undefined || end === undefined) throw reject();\n return Array.from({ length: end - start }, (_, index) => start + index);\n}\n\n/** A stored boolean, refusing the file for anything that is not one. */\nexport function flagOf(value: number | undefined, reject: () => Error): boolean {\n if (value !== 0 && value !== 1) throw reject();\n return value === 1;\n}\n\nfunction validSections(sections: readonly Section[], available: number): boolean {\n if (!Array.isArray(sections) || sections.length === 0) return false;\n if (new Set(sections.map((section) => section.name)).size !== sections.length) return false;\n const ordered = [...sections].sort((left, right) => left.offset - right.offset);\n let end = 0;\n for (const section of ordered) {\n if (\n typeof section.name !== 'string' ||\n !Number.isSafeInteger(section.offset) ||\n !Number.isSafeInteger(section.length) ||\n section.offset < end ||\n section.offset % ALIGNMENT !== 0 ||\n section.length < 0 ||\n section.length > available - section.offset ||\n (section.width !== 1 && section.width !== 4)\n ) return false;\n end = section.offset + section.length;\n }\n return ordered[0]?.offset === 0;\n}\n\nfunction aligned(value: number): number {\n return Math.ceil(value / ALIGNMENT) * ALIGNMENT;\n}\n\n/**\n * Order two strings by UTF-16 code unit, which is the order a segment is sorted in.\n *\n * Exported because a schema that sorts its own values before interning them has\n * to sort them the same way this does, and `localeCompare` is the trap: it is\n * locale-dependent, so an index written on one machine would interleave\n * differently on another and two encodes of equal facts would stop producing\n * equal bytes.\n */\nexport function codeUnitOrder(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@variance-authority/core/shard` — which shard runs which group, and how
|
|
3
|
+
* many shards a suite is worth.
|
|
4
|
+
*
|
|
5
|
+
* One rule for every kind of suite: the stories and routes `variance run
|
|
6
|
+
* --shard` collects, and the test files a runner's `--shard` runs under a
|
|
7
|
+
* seam. No I/O: the groups and their recorded times are handed in by whoever
|
|
8
|
+
* read them.
|
|
9
|
+
*/
|
|
10
|
+
export { MATRIX_LIMIT, durationText, longestFirst, place, priced, shardCount, type Placeable, type Placement, type ShardCount, type ShardCountOptions, type ShardCountReason, } from './placement.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@variance-authority/core/shard` — which shard runs which group, and how
|
|
3
|
+
* many shards a suite is worth.
|
|
4
|
+
*
|
|
5
|
+
* One rule for every kind of suite: the stories and routes `variance run
|
|
6
|
+
* --shard` collects, and the test files a runner's `--shard` runs under a
|
|
7
|
+
* seam. No I/O: the groups and their recorded times are handed in by whoever
|
|
8
|
+
* read them.
|
|
9
|
+
*/
|
|
10
|
+
export { MATRIX_LIMIT, durationText, longestFirst, place, priced, shardCount, } from './placement.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/shard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,KAAK,EACL,MAAM,EACN,UAAU,GAMX,MAAM,gBAAgB,CAAC","sourcesContent":["/**\n * `@variance-authority/core/shard` — which shard runs which group, and how\n * many shards a suite is worth.\n *\n * One rule for every kind of suite: the stories and routes `variance run\n * --shard` collects, and the test files a runner's `--shard` runs under a\n * seam. No I/O: the groups and their recorded times are handed in by whoever\n * read them.\n */\n\nexport {\n MATRIX_LIMIT,\n durationText,\n longestFirst,\n place,\n priced,\n shardCount,\n type Placeable,\n type Placement,\n type ShardCount,\n type ShardCountOptions,\n type ShardCountReason,\n} from './placement.js';\n"]}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which shard runs which group, and how many shards a suite is worth.
|
|
3
|
+
*
|
|
4
|
+
* A group is placed whole: every story of one file, every width of one route,
|
|
5
|
+
* every case of one test file loads the same modules, and split across
|
|
6
|
+
* machines each machine pays for them. Three rules decide where it goes:
|
|
7
|
+
*
|
|
8
|
+
* - **With recorded times, the time decides.** Groups go longest first to the
|
|
9
|
+
* shard with the least time so far (LPT, within 4/3 of the best split). Ties
|
|
10
|
+
* are broken by key in code-unit order, so the placement is a function of
|
|
11
|
+
* the groups and their times and never of the order they were found in.
|
|
12
|
+
* - **A member nothing recorded is priced at the median of the recorded ones.**
|
|
13
|
+
* A new file is a file, and pricing it at zero would stack every new one on
|
|
14
|
+
* one shard.
|
|
15
|
+
* - **With nothing recorded, every group weighs the same.** The same rule
|
|
16
|
+
* places them, so the shards are balanced by count, ties broken by key and
|
|
17
|
+
* then by the lowest shard, and no shard is left empty while there are as
|
|
18
|
+
* many groups as shards.
|
|
19
|
+
*
|
|
20
|
+
* Every shard computes the whole placement and keeps its own part. Nothing is
|
|
21
|
+
* exchanged between shards, so two shards agree exactly when they were handed
|
|
22
|
+
* the same groups and the same times.
|
|
23
|
+
*/
|
|
24
|
+
/** One group placed whole, and the milliseconds recorded for each of its members. */
|
|
25
|
+
export interface Placeable {
|
|
26
|
+
readonly key: string;
|
|
27
|
+
/** `undefined` for a member nothing recorded a time for. */
|
|
28
|
+
readonly costs: readonly (number | undefined)[];
|
|
29
|
+
}
|
|
30
|
+
export interface Placement {
|
|
31
|
+
/** The shard, from 1, of each group in the order they were handed in. */
|
|
32
|
+
readonly owner: readonly number[];
|
|
33
|
+
/** `count` when nothing was recorded and every group weighed one. */
|
|
34
|
+
readonly by: 'count' | 'recorded cost';
|
|
35
|
+
/** Milliseconds each group is priced at, or one each when placed by count. */
|
|
36
|
+
readonly cost: readonly number[];
|
|
37
|
+
/** What each shard was given, shard 1 first: milliseconds, or groups when placed by count. */
|
|
38
|
+
readonly load: readonly number[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Each group's price: its members' recorded milliseconds, with the median of
|
|
42
|
+
* every recorded member standing in for one nothing recorded. Absent when no
|
|
43
|
+
* member of any group is recorded, because then there is no price to give.
|
|
44
|
+
*/
|
|
45
|
+
export declare function priced(groups: readonly Placeable[]): number[] | undefined;
|
|
46
|
+
/** Place every group on one of `total` shards. */
|
|
47
|
+
export declare function place(groups: readonly Placeable[], total: number): Placement;
|
|
48
|
+
/**
|
|
49
|
+
* Group indices, longest first, ties by key in code-unit order: the order a
|
|
50
|
+
* shard's workers take its groups in, so one that finishes early takes the
|
|
51
|
+
* short ones at the end.
|
|
52
|
+
*/
|
|
53
|
+
export declare function longestFirst(keys: readonly string[], cost: readonly number[]): number[];
|
|
54
|
+
/** Why a shard count is the one {@link shardCount} chose. */
|
|
55
|
+
export type ShardCountReason =
|
|
56
|
+
/** No group to run, so no shard to start. */
|
|
57
|
+
'nothing to run'
|
|
58
|
+
/** The fewest shards whose setup and slowest share fit the budget. */
|
|
59
|
+
| 'within budget'
|
|
60
|
+
/** One more shard would save less waiting than the setup it spends. */
|
|
61
|
+
| 'setup'
|
|
62
|
+
/** The slowest group is the longest share already, and no shard finishes before it. */
|
|
63
|
+
| 'slowest group'
|
|
64
|
+
/**
|
|
65
|
+
* The budget is no longer than the setup, which every shard spends before its
|
|
66
|
+
* first group, so no count meets it; the count is the one that finishes soonest.
|
|
67
|
+
*/
|
|
68
|
+
| 'setup over budget'
|
|
69
|
+
/** The most shards allowed. */
|
|
70
|
+
| 'max';
|
|
71
|
+
export interface ShardCountOptions {
|
|
72
|
+
/** Milliseconds a shard spends before its first test: start, checkout, install, build. */
|
|
73
|
+
readonly setup: number;
|
|
74
|
+
/** Milliseconds one shard may take, setup included. */
|
|
75
|
+
readonly budget?: number;
|
|
76
|
+
/** The most shards to start. */
|
|
77
|
+
readonly max?: number;
|
|
78
|
+
/**
|
|
79
|
+
* How many groups one shard runs at once, its runner's workers: a shard is
|
|
80
|
+
* done when its share, divided among them, is, and never before its longest
|
|
81
|
+
* group. One when absent, a shard that runs its groups in turn.
|
|
82
|
+
*/
|
|
83
|
+
readonly workers?: number;
|
|
84
|
+
}
|
|
85
|
+
export interface ShardCount {
|
|
86
|
+
readonly shards: number;
|
|
87
|
+
/** Milliseconds of groups each shard runs, shard 1 first, as {@link place} would place them. */
|
|
88
|
+
readonly load: readonly number[];
|
|
89
|
+
/** Milliseconds until the last shard finishes: the setup and the longest share, divided among its workers. */
|
|
90
|
+
readonly wall: number;
|
|
91
|
+
readonly why: ShardCountReason;
|
|
92
|
+
/** What one more shard would finish in, when the setup it spends is why it was not added. */
|
|
93
|
+
readonly next?: {
|
|
94
|
+
readonly wall: number;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* GitHub Actions starts at most 256 jobs from one matrix, the largest limit
|
|
99
|
+
* any CI this tool is run on documents, so a count past it could not be used.
|
|
100
|
+
*/
|
|
101
|
+
export declare const MATRIX_LIMIT = 256;
|
|
102
|
+
/**
|
|
103
|
+
* How many shards to split groups priced at `costs` milliseconds across.
|
|
104
|
+
*
|
|
105
|
+
* Every shard pays `setup` before it runs a group, so the wait is the setup
|
|
106
|
+
* plus the longest share. With a budget, the count is the fewest shards whose
|
|
107
|
+
* wait fits it. Without one, each shard is charged one setup, and the count is
|
|
108
|
+
* the one whose wait and setups come to least, the fewer of two that tie: a
|
|
109
|
+
* shard is worth starting when it shortens the wait by more than it spends.
|
|
110
|
+
* Every count is weighed, not only the next, because whole groups split
|
|
111
|
+
* unevenly: a third shard can save nothing where a fourth saves a lot. Either
|
|
112
|
+
* way the count stops where the longest share is the slowest group, because
|
|
113
|
+
* past that no shard finishes sooner. A budget no longer than the setup is met
|
|
114
|
+
* by no count, and the setup, not the slowest group, is why.
|
|
115
|
+
*/
|
|
116
|
+
export declare function shardCount(costs: readonly number[], options: ShardCountOptions): ShardCount;
|
|
117
|
+
/** Milliseconds as a person reads them in a shard's line: `340 ms`, `9.0 s`. */
|
|
118
|
+
export declare function durationText(milliseconds: number): string;
|
|
119
|
+
//# sourceMappingURL=placement.d.ts.map
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which shard runs which group, and how many shards a suite is worth.
|
|
3
|
+
*
|
|
4
|
+
* A group is placed whole: every story of one file, every width of one route,
|
|
5
|
+
* every case of one test file loads the same modules, and split across
|
|
6
|
+
* machines each machine pays for them. Three rules decide where it goes:
|
|
7
|
+
*
|
|
8
|
+
* - **With recorded times, the time decides.** Groups go longest first to the
|
|
9
|
+
* shard with the least time so far (LPT, within 4/3 of the best split). Ties
|
|
10
|
+
* are broken by key in code-unit order, so the placement is a function of
|
|
11
|
+
* the groups and their times and never of the order they were found in.
|
|
12
|
+
* - **A member nothing recorded is priced at the median of the recorded ones.**
|
|
13
|
+
* A new file is a file, and pricing it at zero would stack every new one on
|
|
14
|
+
* one shard.
|
|
15
|
+
* - **With nothing recorded, every group weighs the same.** The same rule
|
|
16
|
+
* places them, so the shards are balanced by count, ties broken by key and
|
|
17
|
+
* then by the lowest shard, and no shard is left empty while there are as
|
|
18
|
+
* many groups as shards.
|
|
19
|
+
*
|
|
20
|
+
* Every shard computes the whole placement and keeps its own part. Nothing is
|
|
21
|
+
* exchanged between shards, so two shards agree exactly when they were handed
|
|
22
|
+
* the same groups and the same times.
|
|
23
|
+
*/
|
|
24
|
+
import { codeUnitOrder } from '../segment/index.js';
|
|
25
|
+
/**
|
|
26
|
+
* Each group's price: its members' recorded milliseconds, with the median of
|
|
27
|
+
* every recorded member standing in for one nothing recorded. Absent when no
|
|
28
|
+
* member of any group is recorded, because then there is no price to give.
|
|
29
|
+
*/
|
|
30
|
+
export function priced(groups) {
|
|
31
|
+
const known = groups.flatMap((group) => group.costs.filter((cost) => cost !== undefined));
|
|
32
|
+
if (known.length === 0)
|
|
33
|
+
return undefined;
|
|
34
|
+
const median = known.sort((a, b) => a - b)[Math.floor(known.length / 2)];
|
|
35
|
+
return groups.map((group) => group.costs.reduce((sum, cost) => sum + (cost ?? median), 0));
|
|
36
|
+
}
|
|
37
|
+
/** Place every group on one of `total` shards. */
|
|
38
|
+
export function place(groups, total) {
|
|
39
|
+
const recorded = priced(groups);
|
|
40
|
+
const cost = recorded ?? groups.map(() => 1);
|
|
41
|
+
const order = longestFirst(groups.map((group) => group.key), cost);
|
|
42
|
+
const { owner, load } = packed(order.map((index) => cost[index]), total);
|
|
43
|
+
const byGroup = Array.from({ length: groups.length }, () => 0);
|
|
44
|
+
order.forEach((index, at) => (byGroup[index] = owner[at]));
|
|
45
|
+
return { owner: byGroup, by: recorded === undefined ? 'count' : 'recorded cost', cost, load };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Group indices, longest first, ties by key in code-unit order: the order a
|
|
49
|
+
* shard's workers take its groups in, so one that finishes early takes the
|
|
50
|
+
* short ones at the end.
|
|
51
|
+
*/
|
|
52
|
+
export function longestFirst(keys, cost) {
|
|
53
|
+
return keys.map((_, index) => index).sort((a, b) => cost[b] - cost[a] || codeUnitOrder(keys[a], keys[b]));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* GitHub Actions starts at most 256 jobs from one matrix, the largest limit
|
|
57
|
+
* any CI this tool is run on documents, so a count past it could not be used.
|
|
58
|
+
*/
|
|
59
|
+
export const MATRIX_LIMIT = 256;
|
|
60
|
+
/**
|
|
61
|
+
* How many shards to split groups priced at `costs` milliseconds across.
|
|
62
|
+
*
|
|
63
|
+
* Every shard pays `setup` before it runs a group, so the wait is the setup
|
|
64
|
+
* plus the longest share. With a budget, the count is the fewest shards whose
|
|
65
|
+
* wait fits it. Without one, each shard is charged one setup, and the count is
|
|
66
|
+
* the one whose wait and setups come to least, the fewer of two that tie: a
|
|
67
|
+
* shard is worth starting when it shortens the wait by more than it spends.
|
|
68
|
+
* Every count is weighed, not only the next, because whole groups split
|
|
69
|
+
* unevenly: a third shard can save nothing where a fourth saves a lot. Either
|
|
70
|
+
* way the count stops where the longest share is the slowest group, because
|
|
71
|
+
* past that no shard finishes sooner. A budget no longer than the setup is met
|
|
72
|
+
* by no count, and the setup, not the slowest group, is why.
|
|
73
|
+
*/
|
|
74
|
+
export function shardCount(costs, options) {
|
|
75
|
+
if (costs.length === 0)
|
|
76
|
+
return { shards: 0, load: [], wall: 0, why: 'nothing to run' };
|
|
77
|
+
const sorted = [...costs].sort((a, b) => b - a);
|
|
78
|
+
const slowest = sorted[0];
|
|
79
|
+
const total = sorted.reduce((sum, cost) => sum + cost, 0);
|
|
80
|
+
const limit = Math.max(1, Math.min(options.max ?? MATRIX_LIMIT, sorted.length));
|
|
81
|
+
const workers = options.workers ?? 1;
|
|
82
|
+
const at = (shards) => {
|
|
83
|
+
const { load, first } = packed(sorted, shards);
|
|
84
|
+
const share = Math.max(...load.map((part, shard) => Math.max(part / workers, first[shard])));
|
|
85
|
+
return { shards, load, wall: options.setup + share };
|
|
86
|
+
};
|
|
87
|
+
const counted = (shards, why, next) => ({ ...shards, why, ...(next === undefined ? {} : { next: { wall: next.wall } }) });
|
|
88
|
+
const reached = (shards) => shards.wall - options.setup <= slowest;
|
|
89
|
+
if (options.budget !== undefined) {
|
|
90
|
+
// With no setup, the slowest group is what no count gets under, not the setup.
|
|
91
|
+
const unmet = options.setup > 0 && options.budget <= options.setup;
|
|
92
|
+
for (let here = at(1);; here = at(here.shards + 1)) {
|
|
93
|
+
if (here.wall <= options.budget)
|
|
94
|
+
return counted(here, 'within budget');
|
|
95
|
+
if (reached(here))
|
|
96
|
+
return counted(here, unmet ? 'setup over budget' : 'slowest group');
|
|
97
|
+
if (here.shards === limit)
|
|
98
|
+
return counted(here, unmet ? 'setup over budget' : 'max');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const spent = (shards) => shards.wall + options.setup * shards.shards;
|
|
102
|
+
let best = at(1);
|
|
103
|
+
for (let shards = 2; shards <= limit && !reached(best); shards++) {
|
|
104
|
+
// No split of `shards` waits less than the slowest group or an even share,
|
|
105
|
+
// and every count past this one spends more setup, so none can do better.
|
|
106
|
+
if (options.setup + Math.max(slowest, total / shards / workers) + options.setup * shards >= spent(best))
|
|
107
|
+
break;
|
|
108
|
+
const here = at(shards);
|
|
109
|
+
if (spent(here) < spent(best))
|
|
110
|
+
best = here;
|
|
111
|
+
}
|
|
112
|
+
if (reached(best))
|
|
113
|
+
return counted(best, 'slowest group');
|
|
114
|
+
if (best.shards === limit)
|
|
115
|
+
return counted(best, 'max');
|
|
116
|
+
return counted(best, 'setup', at(best.shards + 1));
|
|
117
|
+
}
|
|
118
|
+
/** Milliseconds as a person reads them in a shard's line: `340 ms`, `9.0 s`. */
|
|
119
|
+
export function durationText(milliseconds) {
|
|
120
|
+
return milliseconds < 1000 ? `${Math.round(milliseconds)} ms` : `${(milliseconds / 1000).toFixed(1)} s`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* LPT over costs already sorted longest first: each to the shard with the
|
|
124
|
+
* least so far, the lowest-numbered of equals. `first` is each shard's
|
|
125
|
+
* longest group, the first it was given.
|
|
126
|
+
*/
|
|
127
|
+
function packed(sorted, total) {
|
|
128
|
+
const load = Array.from({ length: total }, () => 0);
|
|
129
|
+
const first = Array.from({ length: total }, () => 0);
|
|
130
|
+
// A binary heap of shard indices by (load, index), so a suite of tens of
|
|
131
|
+
// thousands of files is placed in n log k rather than n·k.
|
|
132
|
+
const heap = Array.from({ length: total }, (_, index) => index);
|
|
133
|
+
const before = (a, b) => load[a] < load[b] || (load[a] === load[b] && a < b);
|
|
134
|
+
const owner = sorted.map((cost) => {
|
|
135
|
+
const least = heap[0];
|
|
136
|
+
if (load[least] === 0)
|
|
137
|
+
first[least] = cost;
|
|
138
|
+
load[least] = load[least] + cost;
|
|
139
|
+
for (let at = 0;;) {
|
|
140
|
+
const left = 2 * at + 1;
|
|
141
|
+
const right = left + 1;
|
|
142
|
+
let next = at;
|
|
143
|
+
if (left < total && before(heap[left], heap[next]))
|
|
144
|
+
next = left;
|
|
145
|
+
if (right < total && before(heap[right], heap[next]))
|
|
146
|
+
next = right;
|
|
147
|
+
if (next === at)
|
|
148
|
+
break;
|
|
149
|
+
[heap[at], heap[next]] = [heap[next], heap[at]];
|
|
150
|
+
at = next;
|
|
151
|
+
}
|
|
152
|
+
return least + 1;
|
|
153
|
+
});
|
|
154
|
+
return { owner, load, first };
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=placement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placement.js","sourceRoot":"","sources":["../../src/shard/placement.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which shard runs which group, and how many shards a suite is worth.\n *\n * A group is placed whole: every story of one file, every width of one route,\n * every case of one test file loads the same modules, and split across\n * machines each machine pays for them. Three rules decide where it goes:\n *\n * - **With recorded times, the time decides.** Groups go longest first to the\n * shard with the least time so far (LPT, within 4/3 of the best split). Ties\n * are broken by key in code-unit order, so the placement is a function of\n * the groups and their times and never of the order they were found in.\n * - **A member nothing recorded is priced at the median of the recorded ones.**\n * A new file is a file, and pricing it at zero would stack every new one on\n * one shard.\n * - **With nothing recorded, every group weighs the same.** The same rule\n * places them, so the shards are balanced by count, ties broken by key and\n * then by the lowest shard, and no shard is left empty while there are as\n * many groups as shards.\n *\n * Every shard computes the whole placement and keeps its own part. Nothing is\n * exchanged between shards, so two shards agree exactly when they were handed\n * the same groups and the same times.\n */\n\nimport { codeUnitOrder } from '../segment/index.js';\n\n/** One group placed whole, and the milliseconds recorded for each of its members. */\nexport interface Placeable {\n readonly key: string;\n /** `undefined` for a member nothing recorded a time for. */\n readonly costs: readonly (number | undefined)[];\n}\n\nexport interface Placement {\n /** The shard, from 1, of each group in the order they were handed in. */\n readonly owner: readonly number[];\n /** `count` when nothing was recorded and every group weighed one. */\n readonly by: 'count' | 'recorded cost';\n /** Milliseconds each group is priced at, or one each when placed by count. */\n readonly cost: readonly number[];\n /** What each shard was given, shard 1 first: milliseconds, or groups when placed by count. */\n readonly load: readonly number[];\n}\n\n/**\n * Each group's price: its members' recorded milliseconds, with the median of\n * every recorded member standing in for one nothing recorded. Absent when no\n * member of any group is recorded, because then there is no price to give.\n */\nexport function priced(groups: readonly Placeable[]): number[] | undefined {\n const known = groups.flatMap((group) => group.costs.filter((cost): cost is number => cost !== undefined));\n if (known.length === 0) return undefined;\n const median = known.sort((a, b) => a - b)[Math.floor(known.length / 2)]!;\n return groups.map((group) => group.costs.reduce<number>((sum, cost) => sum + (cost ?? median), 0));\n}\n\n/** Place every group on one of `total` shards. */\nexport function place(groups: readonly Placeable[], total: number): Placement {\n const recorded = priced(groups);\n const cost = recorded ?? groups.map(() => 1);\n const order = longestFirst(groups.map((group) => group.key), cost);\n const { owner, load } = packed(order.map((index) => cost[index]!), total);\n const byGroup: number[] = Array.from({ length: groups.length }, () => 0);\n order.forEach((index, at) => (byGroup[index] = owner[at]!));\n return { owner: byGroup, by: recorded === undefined ? 'count' : 'recorded cost', cost, load };\n}\n\n/**\n * Group indices, longest first, ties by key in code-unit order: the order a\n * shard's workers take its groups in, so one that finishes early takes the\n * short ones at the end.\n */\nexport function longestFirst(keys: readonly string[], cost: readonly number[]): number[] {\n return keys.map((_, index) => index).sort((a, b) => cost[b]! - cost[a]! || codeUnitOrder(keys[a]!, keys[b]!));\n}\n\n/** Why a shard count is the one {@link shardCount} chose. */\nexport type ShardCountReason =\n /** No group to run, so no shard to start. */\n | 'nothing to run'\n /** The fewest shards whose setup and slowest share fit the budget. */\n | 'within budget'\n /** One more shard would save less waiting than the setup it spends. */\n | 'setup'\n /** The slowest group is the longest share already, and no shard finishes before it. */\n | 'slowest group'\n /**\n * The budget is no longer than the setup, which every shard spends before its\n * first group, so no count meets it; the count is the one that finishes soonest.\n */\n | 'setup over budget'\n /** The most shards allowed. */\n | 'max';\n\nexport interface ShardCountOptions {\n /** Milliseconds a shard spends before its first test: start, checkout, install, build. */\n readonly setup: number;\n /** Milliseconds one shard may take, setup included. */\n readonly budget?: number;\n /** The most shards to start. */\n readonly max?: number;\n /**\n * How many groups one shard runs at once, its runner's workers: a shard is\n * done when its share, divided among them, is, and never before its longest\n * group. One when absent, a shard that runs its groups in turn.\n */\n readonly workers?: number;\n}\n\nexport interface ShardCount {\n readonly shards: number;\n /** Milliseconds of groups each shard runs, shard 1 first, as {@link place} would place them. */\n readonly load: readonly number[];\n /** Milliseconds until the last shard finishes: the setup and the longest share, divided among its workers. */\n readonly wall: number;\n readonly why: ShardCountReason;\n /** What one more shard would finish in, when the setup it spends is why it was not added. */\n readonly next?: { readonly wall: number };\n}\n\n/**\n * GitHub Actions starts at most 256 jobs from one matrix, the largest limit\n * any CI this tool is run on documents, so a count past it could not be used.\n */\nexport const MATRIX_LIMIT = 256;\n\n/**\n * How many shards to split groups priced at `costs` milliseconds across.\n *\n * Every shard pays `setup` before it runs a group, so the wait is the setup\n * plus the longest share. With a budget, the count is the fewest shards whose\n * wait fits it. Without one, each shard is charged one setup, and the count is\n * the one whose wait and setups come to least, the fewer of two that tie: a\n * shard is worth starting when it shortens the wait by more than it spends.\n * Every count is weighed, not only the next, because whole groups split\n * unevenly: a third shard can save nothing where a fourth saves a lot. Either\n * way the count stops where the longest share is the slowest group, because\n * past that no shard finishes sooner. A budget no longer than the setup is met\n * by no count, and the setup, not the slowest group, is why.\n */\nexport function shardCount(costs: readonly number[], options: ShardCountOptions): ShardCount {\n if (costs.length === 0) return { shards: 0, load: [], wall: 0, why: 'nothing to run' };\n const sorted = [...costs].sort((a, b) => b - a);\n const slowest = sorted[0]!;\n const total = sorted.reduce((sum, cost) => sum + cost, 0);\n const limit = Math.max(1, Math.min(options.max ?? MATRIX_LIMIT, sorted.length));\n const workers = options.workers ?? 1;\n const at = (shards: number) => {\n const { load, first } = packed(sorted, shards);\n const share = Math.max(...load.map((part, shard) => Math.max(part / workers, first[shard]!)));\n return { shards, load, wall: options.setup + share };\n };\n const counted = (shards: ReturnType<typeof at>, why: ShardCountReason, next?: ReturnType<typeof at>): ShardCount =>\n ({ ...shards, why, ...(next === undefined ? {} : { next: { wall: next.wall } }) });\n const reached = (shards: ReturnType<typeof at>) => shards.wall - options.setup <= slowest;\n\n if (options.budget !== undefined) {\n // With no setup, the slowest group is what no count gets under, not the setup.\n const unmet = options.setup > 0 && options.budget <= options.setup;\n for (let here = at(1); ; here = at(here.shards + 1)) {\n if (here.wall <= options.budget) return counted(here, 'within budget');\n if (reached(here)) return counted(here, unmet ? 'setup over budget' : 'slowest group');\n if (here.shards === limit) return counted(here, unmet ? 'setup over budget' : 'max');\n }\n }\n\n const spent = (shards: ReturnType<typeof at>) => shards.wall + options.setup * shards.shards;\n let best = at(1);\n for (let shards = 2; shards <= limit && !reached(best); shards++) {\n // No split of `shards` waits less than the slowest group or an even share,\n // and every count past this one spends more setup, so none can do better.\n if (options.setup + Math.max(slowest, total / shards / workers) + options.setup * shards >= spent(best)) break;\n const here = at(shards);\n if (spent(here) < spent(best)) best = here;\n }\n if (reached(best)) return counted(best, 'slowest group');\n if (best.shards === limit) return counted(best, 'max');\n return counted(best, 'setup', at(best.shards + 1));\n}\n\n/** Milliseconds as a person reads them in a shard's line: `340 ms`, `9.0 s`. */\nexport function durationText(milliseconds: number): string {\n return milliseconds < 1000 ? `${Math.round(milliseconds)} ms` : `${(milliseconds / 1000).toFixed(1)} s`;\n}\n\n/**\n * LPT over costs already sorted longest first: each to the shard with the\n * least so far, the lowest-numbered of equals. `first` is each shard's\n * longest group, the first it was given.\n */\nfunction packed(sorted: readonly number[], total: number): { owner: number[]; load: number[]; first: number[] } {\n const load: number[] = Array.from({ length: total }, () => 0);\n const first: number[] = Array.from({ length: total }, () => 0);\n // A binary heap of shard indices by (load, index), so a suite of tens of\n // thousands of files is placed in n log k rather than n·k.\n const heap = Array.from({ length: total }, (_, index) => index);\n const before = (a: number, b: number) => load[a]! < load[b]! || (load[a] === load[b] && a < b);\n const owner = sorted.map((cost) => {\n const least = heap[0]!;\n if (load[least] === 0) first[least] = cost;\n load[least] = load[least]! + cost;\n for (let at = 0; ; ) {\n const left = 2 * at + 1;\n const right = left + 1;\n let next = at;\n if (left < total && before(heap[left]!, heap[next]!)) next = left;\n if (right < total && before(heap[right]!, heap[next]!)) next = right;\n if (next === at) break;\n [heap[at], heap[next]] = [heap[next]!, heap[at]!];\n at = next;\n }\n return least + 1;\n });\n return { owner, load, first };\n}\n"]}
|
package/dist/share/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Not a baseline store. A baseline is the thing a comparison is *against*, it
|
|
20
20
|
* cannot be re-derived, and losing one silently is how a suite records whatever
|
|
21
21
|
* happened to be on screen — which is why
|
|
22
|
-
* [`RasterStore`](
|
|
22
|
+
* [`RasterStore`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/raster/src/store.ts) throws on every failure. A line
|
|
23
23
|
* holds what can be derived again from the commit it names, so a miss costs the
|
|
24
24
|
* derivation. The two are configured separately because they are answers to
|
|
25
25
|
* different questions, and a share is disposable in a way a baseline may never
|
package/dist/share/index.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Not a baseline store. A baseline is the thing a comparison is *against*, it
|
|
20
20
|
* cannot be re-derived, and losing one silently is how a suite records whatever
|
|
21
21
|
* happened to be on screen — which is why
|
|
22
|
-
* [`RasterStore`](
|
|
22
|
+
* [`RasterStore`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/raster/src/store.ts) throws on every failure. A line
|
|
23
23
|
* holds what can be derived again from the commit it names, so a miss costs the
|
|
24
24
|
* derivation. The two are configured separately because they are answers to
|
|
25
25
|
* different questions, and a share is disposable in a way a baseline may never
|
package/dist/share/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/share/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC","sourcesContent":["/**\n * `@variance-authority/core/share` — one machine's evaluation, reachable from another.\n *\n * A run derives a great deal that is not about the run: what the suite is made\n * of, which subjects exist, every name each one carries, which files declare\n * which components. All of it is a fact about a *commit*, and all of it is\n * expensive — a source scan, a browser, a composition pass — while being\n * identical on every machine that starts from the same tree.\n *\n * So the second machine should not derive it again. A pull-request runner wants\n * what mainline already knows, and mainline already knew it an hour ago on a\n * runner that has since been destroyed. This module is the seam between those\n * two: a `LineCell` holds the latest of what one mainline or one branch\n * published, `publishLine` replaces it, and `readLine` reads it back\n * with the reason when it cannot.\n *\n * ## What this is not\n *\n * Not a baseline store. A baseline is the thing a comparison is *against*, it\n * cannot be re-derived, and losing one silently is how a suite records whatever\n * happened to be on screen — which is why\n * [`RasterStore`](
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/share/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC","sourcesContent":["/**\n * `@variance-authority/core/share` — one machine's evaluation, reachable from another.\n *\n * A run derives a great deal that is not about the run: what the suite is made\n * of, which subjects exist, every name each one carries, which files declare\n * which components. All of it is a fact about a *commit*, and all of it is\n * expensive — a source scan, a browser, a composition pass — while being\n * identical on every machine that starts from the same tree.\n *\n * So the second machine should not derive it again. A pull-request runner wants\n * what mainline already knows, and mainline already knew it an hour ago on a\n * runner that has since been destroyed. This module is the seam between those\n * two: a `LineCell` holds the latest of what one mainline or one branch\n * published, `publishLine` replaces it, and `readLine` reads it back\n * with the reason when it cannot.\n *\n * ## What this is not\n *\n * Not a baseline store. A baseline is the thing a comparison is *against*, it\n * cannot be re-derived, and losing one silently is how a suite records whatever\n * happened to be on screen — which is why\n * [`RasterStore`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/raster/src/store.ts) throws on every failure. A line\n * holds what can be derived again from the commit it names, so a miss costs the\n * derivation. The two are configured separately because they are answers to\n * different questions, and a share is disposable in a way a baseline may never\n * be.\n */\n\nexport * from './line.js';\nexport * from './publish.js';\nexport * from './http-line.js';\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@variance-authority/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "The Variance Authority format, rules, comparison, attribution and verdicts. Pure data in, pure data out, no DOM and no I/O.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"variance-authority",
|
|
@@ -62,6 +62,10 @@
|
|
|
62
62
|
"./plan": {
|
|
63
63
|
"types": "./dist/plan/index.d.ts",
|
|
64
64
|
"default": "./dist/plan/index.js"
|
|
65
|
+
},
|
|
66
|
+
"./shard": {
|
|
67
|
+
"types": "./dist/shard/index.d.ts",
|
|
68
|
+
"default": "./dist/shard/index.js"
|
|
65
69
|
}
|
|
66
70
|
},
|
|
67
71
|
"main": "./dist/index.js",
|
|
@@ -73,5 +77,8 @@
|
|
|
73
77
|
],
|
|
74
78
|
"scripts": {
|
|
75
79
|
"build": "tsc --build"
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"@variance-authority/sense": "^0.17.1"
|
|
76
83
|
}
|
|
77
84
|
}
|