@variance-authority/core 0.14.0 → 0.15.0
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 +454 -0
- package/README.md +13 -10
- package/dist/attribute/component-hash.d.ts +3 -2
- package/dist/attribute/component-hash.js +3 -2
- 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/region.js +1 -5
- 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/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 +2 -2
- 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 +6 -3
- package/dist/format/index.js +4 -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/snapshot.d.ts +2 -0
- package/dist/format/snapshot.js.map +1 -1
- package/dist/format/stabilize.d.ts +28 -5
- package/dist/format/stabilize.js +48 -100
- package/dist/format/stabilize.js.map +1 -1
- 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/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- 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 +35 -37
- 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/dominate.d.ts +19 -0
- package/dist/relate/dominate.js +77 -0
- package/dist/relate/dominate.js.map +1 -0
- package/dist/relate/index.d.ts +3 -0
- package/dist/relate/index.js +3 -0
- 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 +1 -1
- package/dist/relate/reach.js +10 -53
- package/dist/relate/reach.js.map +1 -1
- package/dist/relate/records.d.ts +1 -1
- 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 +1 -0
- package/dist/rules/normalize/fixture.js +1 -0
- 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 +117 -0
- package/dist/shard/placement.js +170 -0
- package/dist/shard/placement.js.map +1 -0
- package/package.json +8 -1
package/dist/attribute/source.js
CHANGED
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
* configuring a build, and it answers with where a component is *declared* —
|
|
22
22
|
* coarser than a call site, and enough to open the right file.
|
|
23
23
|
*
|
|
24
|
-
* The index is plain data. `core` performs no I/O (ADR-0006)
|
|
25
|
-
* a caller's job
|
|
24
|
+
* The index is plain data. `core` performs no I/O (ADR-0006) and has no parser,
|
|
25
|
+
* so building one is a caller's job — `indexDeclarations` in
|
|
26
|
+
* [`@variance-authority/sense`](../../../sense/src/declarations.ts) reads it off
|
|
27
|
+
* a module's parse — and resolving against one is here.
|
|
26
28
|
*/
|
|
27
29
|
export function resolveSource(name, index) {
|
|
28
30
|
const refs = index[name];
|
|
@@ -46,42 +48,6 @@ export function formatSource(resolution) {
|
|
|
46
48
|
const others = resolution.refs.slice(1).map((ref) => `${ref.file}:${ref.line}`);
|
|
47
49
|
return `${primary} (ambiguous — also ${others.join(', ')})`;
|
|
48
50
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Extract component declarations from one file's source.
|
|
51
|
-
*
|
|
52
|
-
* A regex scan, and the limits are worth stating rather than discovering. It
|
|
53
|
-
* finds exported and local declarations in the three shapes React components are
|
|
54
|
-
* written in, and it will miss a component produced by a factory, assigned
|
|
55
|
-
* dynamically, or re-exported under another name. It can also match a function
|
|
56
|
-
* that merely looks like a component — capitalised, declared at top level — and
|
|
57
|
-
* name a non-component in a report.
|
|
58
|
-
*
|
|
59
|
-
* Both failures are survivable in a way a wrong file path would not be: a miss
|
|
60
|
-
* degrades the report to the component name, which is what it said before, and a
|
|
61
|
-
* false positive can only appear if attribution already named that identifier.
|
|
62
|
-
* Parsing properly is the right answer eventually; it is not worth a parser
|
|
63
|
-
* dependency to find out whether the link is useful.
|
|
64
|
-
*/
|
|
65
|
-
export function indexSource(file, contents) {
|
|
66
|
-
const found = {};
|
|
67
|
-
const lines = contents.split('\n');
|
|
68
|
-
const patterns = [
|
|
69
|
-
{ re: /^\s*(?:export\s+)?(?:default\s+)?function\s+([A-Z][A-Za-z0-9_]*)/, via: 'function' },
|
|
70
|
-
{ re: /^\s*(?:export\s+)?(?:const|let)\s+([A-Z][A-Za-z0-9_]*)\s*[:=]/, via: 'const' },
|
|
71
|
-
{ re: /^\s*(?:export\s+)?(?:default\s+)?class\s+([A-Z][A-Za-z0-9_]*)/, via: 'class' },
|
|
72
|
-
];
|
|
73
|
-
for (const [index, line] of lines.entries()) {
|
|
74
|
-
for (const { re, via } of patterns) {
|
|
75
|
-
const match = re.exec(line);
|
|
76
|
-
if (!match)
|
|
77
|
-
continue;
|
|
78
|
-
const name = match[1];
|
|
79
|
-
(found[name] ??= []).push({ file, line: index + 1, via });
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return found;
|
|
84
|
-
}
|
|
85
51
|
/** Merge per-file indexes. A name declared in several files keeps every ref. */
|
|
86
52
|
export function mergeSourceIndexes(indexes) {
|
|
87
53
|
const merged = {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/attribute/source.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/attribute/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAoCH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,KAAkB;IAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;AACpD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,UAAsB;IACjD,MAAM,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC;IAChC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC,IAAI,CAAC;IAEhD,MAAM,OAAO,GAAG,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,SAAS;QAAE,OAAO,OAAO,CAAC;IAE1C,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAChF,OAAO,GAAG,OAAO,sBAAsB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAC9D,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,kBAAkB,CAAC,OAA+B;IAChE,MAAM,MAAM,GAAgC,EAAE,CAAC;IAE/C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACjD,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAiB,EAAE,OAAoB;IACxE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC;AACjC,CAAC","sourcesContent":["/**\n * Connecting a change to a file.\n *\n * A report that says `Button` is one step short of useful. An agent asked to fix\n * it still has to find `Button`, and a reviewer still has to guess whether the\n * `Button` in question is the design-system one or the local one in checkout.\n * The last hop — **which file** — is what turns a finding into an edit.\n *\n * There are two mechanisms, and this file is the second one.\n *\n * The first is exact. Spec §6.1 assumes the build tells us: every JSX transform\n * already computes each element's file, line and column, and\n * `@variance-authority/jsx-source` is the `jsxImportSource` setting that keeps\n * that location as far as the fiber — React 19 drops it otherwise, on every path.\n * When it is on, a finding names the element's own line and this index is not\n * consulted.\n *\n * The second is this one, and it is what a repository that has changed nothing\n * gets. Attribution names *components*, so a **component → file** index answers\n * the question attribution asks. It is built by reading source rather than by\n * configuring a build, and it answers with where a component is *declared* —\n * coarser than a call site, and enough to open the right file.\n *\n * The index is plain data. `core` performs no I/O (ADR-0006) and has no parser,\n * so building one is a caller's job — `indexDeclarations` in\n * [`@variance-authority/sense`](../../../sense/src/declarations.ts) reads it off\n * a module's parse — and resolving against one is here.\n */\n\nexport interface SourceRef {\n /** Repository-relative, so a report is portable between machines and CI. */\n readonly file: string;\n readonly line: number;\n /**\n * How the declaration was recognised. Carried so a bad match is debuggable.\n *\n * The first three say which kind of statement declared the name; a `let`\n * binding is shown as `const`. `engine` is the one that is not read from\n * source at all: the browser was asked where the function it rendered was\n * compiled from, and that position was mapped back to the file. It names the\n * component that ran, whatever it was called.\n */\n readonly via: 'function' | 'const' | 'class' | 'engine';\n}\n\n/**\n * Component name → where it is declared.\n *\n * A name may map to several files. That is not an error to be resolved by\n * picking one: two components genuinely can share a name, and silently choosing\n * the first would send an agent to edit the wrong file with full confidence.\n * Ambiguity is reported (ADR-0003's rule for overlapping projections, applied to\n * source).\n */\nexport type SourceIndex = Readonly<Record<string, readonly SourceRef[]>>;\n\nexport interface Resolution {\n readonly name: string;\n readonly refs: readonly SourceRef[];\n /** `true` when more than one file declares this name. */\n readonly ambiguous: boolean;\n}\n\nexport function resolveSource(name: string, index: SourceIndex): Resolution | null {\n const refs = index[name];\n if (refs === undefined || refs.length === 0) return null;\n\n return { name, refs, ambiguous: refs.length > 1 };\n}\n\n/**\n * `file:line`, or a disambiguating list when a name is declared more than once.\n *\n * Formatted as `path:line` because that is the form an editor, a terminal, and\n * every agent harness already know how to open.\n */\nexport function formatSource(resolution: Resolution): string {\n const [first] = resolution.refs;\n if (first === undefined) return resolution.name;\n\n const primary = `${first.file}:${first.line}`;\n if (!resolution.ambiguous) return primary;\n\n const others = resolution.refs.slice(1).map((ref) => `${ref.file}:${ref.line}`);\n return `${primary} (ambiguous — also ${others.join(', ')})`;\n}\n\n/** Merge per-file indexes. A name declared in several files keeps every ref. */\nexport function mergeSourceIndexes(indexes: readonly SourceIndex[]): SourceIndex {\n const merged: Record<string, SourceRef[]> = {};\n\n for (const index of indexes) {\n for (const [name, refs] of Object.entries(index)) {\n (merged[name] ??= []).push(...refs);\n }\n }\n\n return merged;\n}\n\n/**\n * One index laid over another: a name the overlay holds replaces the base's refs.\n *\n * Not a merge. A scan finds every file that declares a `Button`, and reports the\n * pair as ambiguous because the text cannot say which one a page rendered. The\n * engine can, and when it has answered for a name the scan's guesses for that\n * name are no longer the question. A name the overlay never saw keeps what the\n * base had, so a component the page did not mount still has a file.\n */\nexport function overlaySourceIndex(base: SourceIndex, overlay: SourceIndex): SourceIndex {\n return { ...base, ...overlay };\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type SubjectComposition } from './composition.js';
|
|
2
|
+
/** Another subject beside the one read. */
|
|
3
|
+
export interface SubjectShare {
|
|
4
|
+
readonly subject: string;
|
|
5
|
+
/** Its footprint: distinct renderings outside structure. */
|
|
6
|
+
readonly footprint: number;
|
|
7
|
+
/** Renderings of the subject read that it holds too. */
|
|
8
|
+
readonly shared: number;
|
|
9
|
+
}
|
|
10
|
+
/** Renderings of one component that no piece holds. */
|
|
11
|
+
export interface Residue {
|
|
12
|
+
readonly component: string;
|
|
13
|
+
readonly renderings: number;
|
|
14
|
+
/** The pieces that mount the component and render it otherwise. Absent on a component no piece mounts. */
|
|
15
|
+
readonly pieces?: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
export interface SubjectPieces {
|
|
18
|
+
/** Distinct renderings it holds outside structure. */
|
|
19
|
+
readonly footprint: number;
|
|
20
|
+
/** Structural components it mounts. */
|
|
21
|
+
readonly structure: number;
|
|
22
|
+
/** Subjects whose footprint is exactly its own, in plan order. */
|
|
23
|
+
readonly alike: readonly string[];
|
|
24
|
+
/** Smaller subjects inside it, most shared first. */
|
|
25
|
+
readonly pieces: readonly SubjectShare[];
|
|
26
|
+
/** Larger subjects holding it, smallest first. */
|
|
27
|
+
readonly wholes: readonly SubjectShare[];
|
|
28
|
+
/** Renderings of its footprint some piece holds. */
|
|
29
|
+
readonly explained: number;
|
|
30
|
+
/** Residue in components no piece mounts, by component name. */
|
|
31
|
+
readonly own: readonly Residue[];
|
|
32
|
+
/** Residue in components a piece mounts and renders otherwise, by component name. */
|
|
33
|
+
readonly inContext: readonly Residue[];
|
|
34
|
+
}
|
|
35
|
+
/** Every subject's pieces, keyed by subject. A pure fold over the suite, so it reads the same however the run was scheduled. */
|
|
36
|
+
export declare function piecesOf(subjects: readonly SubjectComposition[]): ReadonlyMap<string, SubjectPieces>;
|
|
37
|
+
//# sourceMappingURL=subject-pieces.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { structuralComponents } from './composition.js';
|
|
2
|
+
import { attributed } from './instances.js';
|
|
3
|
+
import { codeUnitOrder } from '../segment/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Each subject read as the narrower subjects inside it.
|
|
6
|
+
*
|
|
7
|
+
* The same algebra the recording answers for a test, over renderings instead
|
|
8
|
+
* of regions. A subject's footprint is the distinct renderings it holds — a
|
|
9
|
+
* component and the digest of what it rendered — less structure, the
|
|
10
|
+
* components more than half the suite mounts. A piece is a smaller subject with
|
|
11
|
+
* at least nine tenths of its footprint inside this one: the chip story inside
|
|
12
|
+
* the footer story inside the page. A whole is a larger subject holding at
|
|
13
|
+
* least nine tenths of this one. A subject with exactly the same footprint is
|
|
14
|
+
* alike, and is neither.
|
|
15
|
+
*
|
|
16
|
+
* What no piece renders is the residue, counted rendering by rendering and
|
|
17
|
+
* never as a difference of totals. It splits in two. A component no piece
|
|
18
|
+
* mounts is the subject's own: only it shows that component, so only it can
|
|
19
|
+
* catch a change there. A component a piece mounts, rendered here in a way the
|
|
20
|
+
* piece never renders it, is the component in context — the narrow subject
|
|
21
|
+
* does not watch this rendering, and a change to it is the page's to catch.
|
|
22
|
+
*
|
|
23
|
+
* It decides nothing. A subject whose pieces explain all of it is not therefore
|
|
24
|
+
* redundant: the bytes a page holds are the bytes its pieces hold, and the
|
|
25
|
+
* arrangement between them is not a rendering of any one component.
|
|
26
|
+
*/
|
|
27
|
+
/** A piece holds at least this share of its own footprint inside the subject; a whole holds at least this share of the subject's. */
|
|
28
|
+
const HOLDS = 0.9;
|
|
29
|
+
/** Every subject's pieces, keyed by subject. A pure fold over the suite, so it reads the same however the run was scheduled. */
|
|
30
|
+
export function piecesOf(subjects) {
|
|
31
|
+
const structural = structuralComponents(subjects);
|
|
32
|
+
const footprints = new Map();
|
|
33
|
+
const mounted = new Map();
|
|
34
|
+
const structure = new Map();
|
|
35
|
+
const holders = new Map();
|
|
36
|
+
for (const { subject, instances } of subjects) {
|
|
37
|
+
const held = new Set();
|
|
38
|
+
const components = new Set();
|
|
39
|
+
const frame = new Set();
|
|
40
|
+
for (const instance of instances) {
|
|
41
|
+
if (!attributed(instance))
|
|
42
|
+
continue;
|
|
43
|
+
if (structural.has(instance.component)) {
|
|
44
|
+
frame.add(instance.component);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
held.add(keyOf(instance.component, instance.rendering));
|
|
48
|
+
components.add(instance.component);
|
|
49
|
+
}
|
|
50
|
+
footprints.set(subject, held);
|
|
51
|
+
mounted.set(subject, components);
|
|
52
|
+
structure.set(subject, frame.size);
|
|
53
|
+
for (const key of held) {
|
|
54
|
+
const list = holders.get(key);
|
|
55
|
+
if (list === undefined)
|
|
56
|
+
holders.set(key, [subject]);
|
|
57
|
+
else
|
|
58
|
+
list.push(subject);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const order = new Map(subjects.map(({ subject }, at) => [subject, at]));
|
|
62
|
+
const answers = new Map();
|
|
63
|
+
for (const { subject } of subjects) {
|
|
64
|
+
const mine = footprints.get(subject) ?? new Set();
|
|
65
|
+
const size = mine.size;
|
|
66
|
+
const shared = new Map();
|
|
67
|
+
for (const key of mine) {
|
|
68
|
+
for (const other of holders.get(key) ?? []) {
|
|
69
|
+
if (other !== subject)
|
|
70
|
+
shared.set(other, (shared.get(other) ?? 0) + 1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const pieces = [];
|
|
74
|
+
const wholes = [];
|
|
75
|
+
const alike = [];
|
|
76
|
+
for (const [other, with_] of shared) {
|
|
77
|
+
const theirs = footprints.get(other)?.size ?? 0;
|
|
78
|
+
const share = { subject: other, footprint: theirs, shared: with_ };
|
|
79
|
+
if (theirs < size && with_ >= HOLDS * theirs)
|
|
80
|
+
pieces.push(share);
|
|
81
|
+
else if (theirs > size && with_ >= HOLDS * size)
|
|
82
|
+
wholes.push(share);
|
|
83
|
+
else if (theirs === size && with_ === size)
|
|
84
|
+
alike.push(other);
|
|
85
|
+
}
|
|
86
|
+
const planned = (a, b) => (order.get(a.subject) ?? 0) - (order.get(b.subject) ?? 0);
|
|
87
|
+
pieces.sort((a, b) => b.shared - a.shared || b.footprint - a.footprint || planned(a, b));
|
|
88
|
+
wholes.sort((a, b) => a.footprint - b.footprint || planned(a, b));
|
|
89
|
+
alike.sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
|
|
90
|
+
const kept = new Set(pieces.map((piece) => piece.subject));
|
|
91
|
+
let explained = 0;
|
|
92
|
+
const left = new Map();
|
|
93
|
+
for (const key of mine) {
|
|
94
|
+
if ((holders.get(key) ?? []).some((other) => kept.has(other)))
|
|
95
|
+
explained += 1;
|
|
96
|
+
else {
|
|
97
|
+
const component = componentOf(key);
|
|
98
|
+
left.set(component, (left.get(component) ?? 0) + 1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const own = [];
|
|
102
|
+
const inContext = [];
|
|
103
|
+
for (const component of [...left.keys()].sort(codeUnitOrder)) {
|
|
104
|
+
const renderings = left.get(component) ?? 0;
|
|
105
|
+
const by = pieces.map((piece) => piece.subject).filter((piece) => mounted.get(piece)?.has(component) === true);
|
|
106
|
+
if (by.length === 0)
|
|
107
|
+
own.push({ component, renderings });
|
|
108
|
+
else
|
|
109
|
+
inContext.push({ component, renderings, pieces: by });
|
|
110
|
+
}
|
|
111
|
+
answers.set(subject, { footprint: size, structure: structure.get(subject) ?? 0, alike, pieces, wholes, explained, own, inContext });
|
|
112
|
+
}
|
|
113
|
+
return answers;
|
|
114
|
+
}
|
|
115
|
+
/** A component and a rendering, joined by a character no component name holds. */
|
|
116
|
+
function keyOf(component, rendering) {
|
|
117
|
+
return `${component}\u0000${rendering}`;
|
|
118
|
+
}
|
|
119
|
+
function componentOf(key) {
|
|
120
|
+
return key.slice(0, key.indexOf('\u0000'));
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=subject-pieces.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subject-pieces.js","sourceRoot":"","sources":["../../src/attribute/subject-pieces.ts"],"names":[],"mappings":"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","sourcesContent":["import { structuralComponents, type SubjectComposition } from './composition.js';\nimport { attributed } from './instances.js';\nimport { codeUnitOrder } from '../segment/index.js';\n\n/**\n * Each subject read as the narrower subjects inside it.\n *\n * The same algebra the recording answers for a test, over renderings instead\n * of regions. A subject's footprint is the distinct renderings it holds — a\n * component and the digest of what it rendered — less structure, the\n * components more than half the suite mounts. A piece is a smaller subject with\n * at least nine tenths of its footprint inside this one: the chip story inside\n * the footer story inside the page. A whole is a larger subject holding at\n * least nine tenths of this one. A subject with exactly the same footprint is\n * alike, and is neither.\n *\n * What no piece renders is the residue, counted rendering by rendering and\n * never as a difference of totals. It splits in two. A component no piece\n * mounts is the subject's own: only it shows that component, so only it can\n * catch a change there. A component a piece mounts, rendered here in a way the\n * piece never renders it, is the component in context — the narrow subject\n * does not watch this rendering, and a change to it is the page's to catch.\n *\n * It decides nothing. A subject whose pieces explain all of it is not therefore\n * redundant: the bytes a page holds are the bytes its pieces hold, and the\n * arrangement between them is not a rendering of any one component.\n */\n\n/** A piece holds at least this share of its own footprint inside the subject; a whole holds at least this share of the subject's. */\nconst HOLDS = 0.9;\n\n/** Another subject beside the one read. */\nexport interface SubjectShare {\n readonly subject: string;\n /** Its footprint: distinct renderings outside structure. */\n readonly footprint: number;\n /** Renderings of the subject read that it holds too. */\n readonly shared: number;\n}\n\n/** Renderings of one component that no piece holds. */\nexport interface Residue {\n readonly component: string;\n readonly renderings: number;\n /** The pieces that mount the component and render it otherwise. Absent on a component no piece mounts. */\n readonly pieces?: readonly string[];\n}\n\nexport interface SubjectPieces {\n /** Distinct renderings it holds outside structure. */\n readonly footprint: number;\n /** Structural components it mounts. */\n readonly structure: number;\n /** Subjects whose footprint is exactly its own, in plan order. */\n readonly alike: readonly string[];\n /** Smaller subjects inside it, most shared first. */\n readonly pieces: readonly SubjectShare[];\n /** Larger subjects holding it, smallest first. */\n readonly wholes: readonly SubjectShare[];\n /** Renderings of its footprint some piece holds. */\n readonly explained: number;\n /** Residue in components no piece mounts, by component name. */\n readonly own: readonly Residue[];\n /** Residue in components a piece mounts and renders otherwise, by component name. */\n readonly inContext: readonly Residue[];\n}\n\n/** Every subject's pieces, keyed by subject. A pure fold over the suite, so it reads the same however the run was scheduled. */\nexport function piecesOf(subjects: readonly SubjectComposition[]): ReadonlyMap<string, SubjectPieces> {\n const structural = structuralComponents(subjects);\n const footprints = new Map<string, ReadonlySet<string>>();\n const mounted = new Map<string, ReadonlySet<string>>();\n const structure = new Map<string, number>();\n const holders = new Map<string, string[]>();\n for (const { subject, instances } of subjects) {\n const held = new Set<string>();\n const components = new Set<string>();\n const frame = new Set<string>();\n for (const instance of instances) {\n if (!attributed(instance)) continue;\n if (structural.has(instance.component)) {\n frame.add(instance.component);\n continue;\n }\n held.add(keyOf(instance.component, instance.rendering));\n components.add(instance.component);\n }\n footprints.set(subject, held);\n mounted.set(subject, components);\n structure.set(subject, frame.size);\n for (const key of held) {\n const list = holders.get(key);\n if (list === undefined) holders.set(key, [subject]);\n else list.push(subject);\n }\n }\n\n const order = new Map(subjects.map(({ subject }, at) => [subject, at]));\n const answers = new Map<string, SubjectPieces>();\n for (const { subject } of subjects) {\n const mine = footprints.get(subject) ?? new Set<string>();\n const size = mine.size;\n const shared = new Map<string, number>();\n for (const key of mine) {\n for (const other of holders.get(key) ?? []) {\n if (other !== subject) shared.set(other, (shared.get(other) ?? 0) + 1);\n }\n }\n const pieces: SubjectShare[] = [];\n const wholes: SubjectShare[] = [];\n const alike: string[] = [];\n for (const [other, with_] of shared) {\n const theirs = footprints.get(other)?.size ?? 0;\n const share = { subject: other, footprint: theirs, shared: with_ };\n if (theirs < size && with_ >= HOLDS * theirs) pieces.push(share);\n else if (theirs > size && with_ >= HOLDS * size) wholes.push(share);\n else if (theirs === size && with_ === size) alike.push(other);\n }\n const planned = (a: SubjectShare, b: SubjectShare) => (order.get(a.subject) ?? 0) - (order.get(b.subject) ?? 0);\n pieces.sort((a, b) => b.shared - a.shared || b.footprint - a.footprint || planned(a, b));\n wholes.sort((a, b) => a.footprint - b.footprint || planned(a, b));\n alike.sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));\n\n const kept = new Set(pieces.map((piece) => piece.subject));\n let explained = 0;\n const left = new Map<string, number>();\n for (const key of mine) {\n if ((holders.get(key) ?? []).some((other) => kept.has(other))) explained += 1;\n else {\n const component = componentOf(key);\n left.set(component, (left.get(component) ?? 0) + 1);\n }\n }\n const own: Residue[] = [];\n const inContext: Residue[] = [];\n for (const component of [...left.keys()].sort(codeUnitOrder)) {\n const renderings = left.get(component) ?? 0;\n const by = pieces.map((piece) => piece.subject).filter((piece) => mounted.get(piece)?.has(component) === true);\n if (by.length === 0) own.push({ component, renderings });\n else inContext.push({ component, renderings, pieces: by });\n }\n answers.set(subject, { footprint: size, structure: structure.get(subject) ?? 0, alike, pieces, wholes, explained, own, inContext });\n }\n return answers;\n}\n\n/** A component and a rendering, joined by a character no component name holds. */\nfunction keyOf(component: string, rendering: string): string {\n return `${component}\\u0000${rendering}`;\n}\n\nfunction componentOf(key: string): string {\n return key.slice(0, key.indexOf('\\u0000'));\n}\n"]}
|
package/dist/format/capture.d.ts
CHANGED
|
@@ -61,6 +61,15 @@ export interface RawCapture {
|
|
|
61
61
|
* Recorded so the coupling can be reported before it bites.
|
|
62
62
|
*/
|
|
63
63
|
readonly couplings?: readonly string[];
|
|
64
|
+
/**
|
|
65
|
+
* The document's base URL, `document.baseURI`, as the page reported it.
|
|
66
|
+
*
|
|
67
|
+
* An engine resolves every `url()` in a computed value against this, so a
|
|
68
|
+
* build served on a different port comes back with different strings. The
|
|
69
|
+
* normalizer drops this origin from them. Absent when the collector had no
|
|
70
|
+
* page to ask, and then nothing is dropped.
|
|
71
|
+
*/
|
|
72
|
+
readonly baseUrl?: string;
|
|
64
73
|
/** Anything the collector could not do. Empty is the expected case. */
|
|
65
74
|
readonly diagnostics: readonly Diagnostic[];
|
|
66
75
|
}
|
|
@@ -88,6 +97,11 @@ export interface SubjectRef {
|
|
|
88
97
|
* ruleset, versioned by nothing.
|
|
89
98
|
*/
|
|
90
99
|
export interface RawNode {
|
|
100
|
+
/**
|
|
101
|
+
* Lower-case element name, `#text` for a text run, or `::before` / `::after`
|
|
102
|
+
* for a box CSS generates inside its parent — children with no DOM node,
|
|
103
|
+
* which paint like any other child.
|
|
104
|
+
*/
|
|
91
105
|
readonly tag: string;
|
|
92
106
|
readonly attributes: Readonly<Record<string, string>>;
|
|
93
107
|
/** Resolved accessibility properties, when the profile provides them. */
|
|
@@ -154,6 +168,15 @@ export interface RawNode {
|
|
|
154
168
|
* re-baseline the repository.
|
|
155
169
|
*/
|
|
156
170
|
readonly ignoredBy?: readonly string[];
|
|
171
|
+
/**
|
|
172
|
+
* Why part of this node was not read, on a node whose existence is known and
|
|
173
|
+
* whose values are not — a `::before` whose `content` holds a `counter()` under
|
|
174
|
+
* a host with no layout engine.
|
|
175
|
+
*
|
|
176
|
+
* A marker, never a value: the fields it describes are absent rather than
|
|
177
|
+
* guessed (ADR-0002). Outside every hash, like `ignoredBy` above it.
|
|
178
|
+
*/
|
|
179
|
+
readonly unread?: string;
|
|
157
180
|
}
|
|
158
181
|
export interface RawAria {
|
|
159
182
|
readonly role: string | null;
|
|
@@ -203,6 +226,8 @@ export interface Rect {
|
|
|
203
226
|
readonly width: number;
|
|
204
227
|
readonly height: number;
|
|
205
228
|
}
|
|
229
|
+
/** The area two rects share; zero when they only touch or do not meet. */
|
|
230
|
+
export declare function intersectionArea(a: Rect, b: Rect): number;
|
|
206
231
|
export interface Diagnostic {
|
|
207
232
|
readonly severity: 'warn' | 'error';
|
|
208
233
|
readonly code: string;
|
package/dist/format/capture.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/** The area two rects share; zero when they only touch or do not meet. */
|
|
2
|
+
export function intersectionArea(a, b) {
|
|
3
|
+
const width = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
|
|
4
|
+
const height = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
|
|
5
|
+
return width > 0 && height > 0 ? width * height : 0;
|
|
6
|
+
}
|
|
1
7
|
/**
|
|
2
8
|
* One list from several, with a diagnostic said once however many times it was
|
|
3
9
|
* found.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/format/capture.ts"],"names":[],"mappings":"AA8OA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAG,KAAqD;IAExD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAsB,CAAC;IACjD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,UAAU,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;YACpC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;gBACzB,UAAU,CAAC,QAAQ;gBACnB,UAAU,CAAC,IAAI;gBACf,UAAU,CAAC,OAAO;gBAClB,UAAU,CAAC,QAAQ,IAAI,IAAI;aAC5B,CAAC,CAAC;YACH,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;AAClC,CAAC","sourcesContent":["import type { EnvironmentInputs } from './environment.js';\nimport type { ObservationProfile } from './profile.js';\nimport type { Provenance } from './provenance.js';\nimport type { Wiring } from './wiring.js';\nimport type { Holding } from './holding.js';\n\n/**\n * The collector/core boundary.\n *\n * Collectors **extract**; `core` **normalizes**. Nothing in this file is a live\n * object — a `RawCapture` is plain data, fully serializable, produced by touching\n * a DOM and consumed by code that has never seen one.\n *\n * Two properties fall out of that split, and both are the reason for it:\n *\n * 1. **One ruleset, by construction.** Normalization is the moat (ADR-0003) and\n * the thing most likely to drift between two implementations. If the JSDOM and\n * Chromium collectors each normalized their own captures, \"the same rules ran\"\n * would be a claim maintained by discipline. Here it is a fact of the call\n * graph: there is one normalizer and both profiles enter it.\n * 2. **The sub-renderer can be anywhere.** A capture crossing a worker boundary,\n * a pipe, or a network hop to a device farm is the same value it was in\n * process. No code may assume the collector is local.\n */\nexport interface RawCapture {\n readonly captureVersion: 1;\n readonly subject: SubjectRef;\n readonly profile: ObservationProfile;\n\n /**\n * Environment inputs the *collector* is positioned to know — engine version,\n * fonts actually loaded, resolved conditions, asset hashes. The ruleset and\n * allowlist versions are `core`'s to supply, so they are absent here and the\n * key is completed during normalization.\n */\n readonly environment: Omit<EnvironmentInputs, 'ruleset' | 'allowlist'>;\n\n readonly root: RawNode;\n\n /**\n * Inherited values in force at the subject root.\n *\n * Mandatory, not an optimization. CSS applicability pruning (ADR-0003) drops\n * every rule that matches nothing inside the subtree — including rules on\n * ancestors *outside* it whose inheritable properties still reach in. Without\n * this seed the cheap tier is unsound and will report false `unchanged`.\n */\n readonly inheritedSeed: Readonly<Record<string, string>>;\n\n /**\n * Subtrees this subject renders through portals, in fiber traversal order.\n *\n * A subject's boundary is a component-tree question, not a DOM-containment\n * one: `createPortal` renders elsewhere in the document while remaining part\n * of the tree rooted here. Omitting these makes an opening modal report\n * `unchanged`, because its own container is byte-identical (ADR-0007).\n */\n readonly portals?: readonly RawNode[];\n\n /**\n * Shared root state this subject is *latently* coupled to.\n *\n * Rules like `html.dark .card` contribute nothing today and everything the\n * moment `dark` lands on `<html>`. In a session that reuses one document across\n * subjects, that moment may arrive because a *different* subject put it there —\n * turning this subject's baseline order-dependent with no visible cause.\n * Recorded so the coupling can be reported before it bites.\n */\n readonly couplings?: readonly string[];\n\n /** Anything the collector could not do. Empty is the expected case. */\n readonly diagnostics: readonly Diagnostic[];\n}\n\nexport interface SubjectRef {\n /** Stable across renames of the file, e.g. `story:components-button--primary`. */\n readonly id: string;\n\n /**\n * What kind of thing this is, which is not the same question as what produced\n * it.\n *\n * `value` is the one that is not a rendering: a JSON body, a schema, a route\n * table. It is a fourth member rather than a reuse of `route`, which already\n * means *a page rendered at a URL* — a route table is a value about the same\n * paths, and a subject list where one word meant both is one nobody can filter.\n */\n readonly kind: 'story' | 'route' | 'fixture' | 'value';\n readonly title?: string;\n}\n\n/**\n * A node exactly as observed, before any rule is applied.\n *\n * Raw means raw: `attributes` still holds generated ids and hashed class names,\n * `matchedRules` still holds cascade losers. Normalization needs the unedited\n * input — a collector that helpfully pre-cleaned would be a second, invisible\n * ruleset, versioned by nothing.\n */\nexport interface RawNode {\n readonly tag: string;\n readonly attributes: Readonly<Record<string, string>>;\n\n /** Resolved accessibility properties, when the profile provides them. */\n readonly aria?: RawAria;\n\n /**\n * Rules matching this node, as authored.\n *\n * Losing declarations are retained: `core` resolves the cascade for profiles\n * whose engine did not (ADR-0003 step 5), and cannot do that from winners\n * alone. Shorthands are retained *unexpanded* — expansion is `core`'s job, so\n * that it is versioned by the ruleset rather than by each collector.\n *\n * A collector MAY drop a declaration whose property is neither allowlisted nor\n * a shorthand of an allowlisted property, and SHOULD when the capture crosses\n * a network hop. It MUST NOT normalize values; a helpfully pre-cleaned capture\n * is a second, invisible ruleset versioned by nothing.\n */\n readonly matchedRules: readonly MatchedRule[];\n\n readonly inlineStyle?: Readonly<Record<string, string>>;\n\n /** Engine-resolved computed style. Present only when `profile.computedStyle`. */\n readonly computedStyle?: Readonly<Record<string, string>>;\n\n /** Present only when `profile.layout`. */\n readonly rect?: Rect;\n\n /** Literal text of a text node. Digested during normalization, per policy. */\n readonly text?: string;\n\n readonly provenance?: Provenance;\n\n /**\n * How the framework holds this node's component, when an adapter supplied it.\n *\n * Beside `provenance` because it arrives from the same seam and under the same\n * rule: the collector carries no framework dependency and both are injected.\n * See {@link Wiring} for why it is a dimension rather than another attribution\n * field.\n */\n readonly wiring?: Wiring;\n\n /**\n * What the component that authored this node was holding, when an adapter\n * supplied it.\n *\n * A third injection at the same seam, and deliberately not folded into\n * `wiring`: wiring is hashed into a band and this may never be, so a caller\n * that wants the band must be able to have it without also carrying values,\n * and a caller that wants explanations must be able to ask for them without\n * changing a single digest. See {@link Holding}.\n */\n readonly holding?: Holding;\n\n readonly children: readonly RawNode[];\n\n /** Nodes inside a shadow root, kept distinct from light-DOM children. */\n readonly shadowChildren?: readonly RawNode[];\n\n /**\n * Ignore rules the collector found this element under (spec 0024).\n *\n * A mark, not a removal. The collector is the only thing holding a live DOM, so\n * it is the only thing that can run a selector or read a marker attribute — but\n * it must not act on what it finds. An element deleted from the capture would\n * be invisible to every count downstream, and the difference between \"absorbed\n * by the `carousel` rule\" and \"was never there\" is the difference between an\n * ignore and a blind spot.\n *\n * Never hashed. Normalization turns these into\n * {@link import('./snapshot.js').SemanticSnapshot.ignoreSites}, which sits\n * beside `styleProvenance` outside the render hash for the same reason: masking\n * a clock changes what a run *says*, not what it *renders*, and must not\n * re-baseline the repository.\n */\n readonly ignoredBy?: readonly string[];\n}\n\nexport interface RawAria {\n readonly role: string | null;\n readonly name: string | null;\n\n /**\n * Accessible description — what `aria-describedby` and `title` resolve to.\n *\n * Captured because nothing else captures it, and its absence was a hole. The\n * attribute allowlist drops every `aria-*` attribute on the stated grounds\n * that they \"are resolved into role/name/state\" — true of `aria-label` and\n * `aria-selected`, and false of `aria-describedby`, which resolves into a\n * description and had nowhere to land. A field whose `aria-describedby`\n * pointed at a deleted error message compared **equal**: the reference was\n * dropped by the allowlist, the name was unaffected, and the regression was\n * silent on every tier including raster.\n */\n readonly description?: string | null;\n\n /** `checked`, `disabled`, `expanded`, `selected`, … */\n readonly state: Readonly<Record<string, string | boolean | number>>;\n}\n\nexport interface MatchedRule {\n /** Identifies the origin sheet for attribution: href, or a synthetic id. */\n readonly sheet: string;\n readonly selector: string;\n /** `[idCount, classCount, typeCount]` — CSS specificity, most significant first. */\n readonly specificity: readonly [number, number, number];\n /** Document order of the rule. Breaks specificity ties, per the cascade. */\n readonly order: number;\n readonly declarations: readonly Declaration[];\n /** Source position, when the sheet exposes one. Turns a diff into a file:line. */\n readonly source?: { readonly file: string; readonly line: number };\n}\n\nexport interface Declaration {\n readonly property: string;\n readonly value: string;\n readonly important: boolean;\n /** Custom properties referenced by the value, for token-keyed attribution. */\n readonly references?: readonly string[];\n}\n\n/** Browser-layout coordinates retained as evidence, without a preferred layout policy. */\nexport interface Rect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\nexport interface Diagnostic {\n readonly severity: 'warn' | 'error';\n readonly code: string;\n readonly message: string;\n readonly nodePath?: string;\n}\n\n/**\n * One list from several, with a diagnostic said once however many times it was\n * found.\n *\n * A subject is described by a document, a snapshot and a comparison, and the\n * same condition is visible to more than one of them -- a missing font is in the\n * document that declared it and in the snapshot that measured it. A reader asks\n * \"what is wrong with this subject\" once, and a list that answers twice reads as\n * two findings.\n *\n * Identity is every field, `nodePath` included: the same code on two nodes is\n * two facts, and collapsing them would report one element where the page has a\n * dozen.\n */\nexport function mergeDiagnostics(\n ...lists: readonly (readonly Diagnostic[] | undefined)[]\n): readonly Diagnostic[] {\n const byIdentity = new Map<string, Diagnostic>();\n for (const list of lists) {\n for (const diagnostic of list ?? []) {\n const key = JSON.stringify([\n diagnostic.severity,\n diagnostic.code,\n diagnostic.message,\n diagnostic.nodePath ?? null,\n ]);\n if (!byIdentity.has(key)) byIdentity.set(key, diagnostic);\n }\n }\n return [...byIdentity.values()];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/format/capture.ts"],"names":[],"mappings":"AAgQA,0EAA0E;AAC1E,MAAM,UAAU,gBAAgB,CAAC,CAAO,EAAE,CAAO;IAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7E,OAAO,KAAK,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACtD,CAAC;AASD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAG,KAAqD;IAExD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAsB,CAAC;IACjD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,UAAU,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;YACpC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;gBACzB,UAAU,CAAC,QAAQ;gBACnB,UAAU,CAAC,IAAI;gBACf,UAAU,CAAC,OAAO;gBAClB,UAAU,CAAC,QAAQ,IAAI,IAAI;aAC5B,CAAC,CAAC;YACH,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;AAClC,CAAC","sourcesContent":["import type { EnvironmentInputs } from './environment.js';\nimport type { ObservationProfile } from './profile.js';\nimport type { Provenance } from './provenance.js';\nimport type { Wiring } from './wiring.js';\nimport type { Holding } from './holding.js';\n\n/**\n * The collector/core boundary.\n *\n * Collectors **extract**; `core` **normalizes**. Nothing in this file is a live\n * object — a `RawCapture` is plain data, fully serializable, produced by touching\n * a DOM and consumed by code that has never seen one.\n *\n * Two properties fall out of that split, and both are the reason for it:\n *\n * 1. **One ruleset, by construction.** Normalization is the moat (ADR-0003) and\n * the thing most likely to drift between two implementations. If the JSDOM and\n * Chromium collectors each normalized their own captures, \"the same rules ran\"\n * would be a claim maintained by discipline. Here it is a fact of the call\n * graph: there is one normalizer and both profiles enter it.\n * 2. **The sub-renderer can be anywhere.** A capture crossing a worker boundary,\n * a pipe, or a network hop to a device farm is the same value it was in\n * process. No code may assume the collector is local.\n */\nexport interface RawCapture {\n readonly captureVersion: 1;\n readonly subject: SubjectRef;\n readonly profile: ObservationProfile;\n\n /**\n * Environment inputs the *collector* is positioned to know — engine version,\n * fonts actually loaded, resolved conditions, asset hashes. The ruleset and\n * allowlist versions are `core`'s to supply, so they are absent here and the\n * key is completed during normalization.\n */\n readonly environment: Omit<EnvironmentInputs, 'ruleset' | 'allowlist'>;\n\n readonly root: RawNode;\n\n /**\n * Inherited values in force at the subject root.\n *\n * Mandatory, not an optimization. CSS applicability pruning (ADR-0003) drops\n * every rule that matches nothing inside the subtree — including rules on\n * ancestors *outside* it whose inheritable properties still reach in. Without\n * this seed the cheap tier is unsound and will report false `unchanged`.\n */\n readonly inheritedSeed: Readonly<Record<string, string>>;\n\n /**\n * Subtrees this subject renders through portals, in fiber traversal order.\n *\n * A subject's boundary is a component-tree question, not a DOM-containment\n * one: `createPortal` renders elsewhere in the document while remaining part\n * of the tree rooted here. Omitting these makes an opening modal report\n * `unchanged`, because its own container is byte-identical (ADR-0007).\n */\n readonly portals?: readonly RawNode[];\n\n /**\n * Shared root state this subject is *latently* coupled to.\n *\n * Rules like `html.dark .card` contribute nothing today and everything the\n * moment `dark` lands on `<html>`. In a session that reuses one document across\n * subjects, that moment may arrive because a *different* subject put it there —\n * turning this subject's baseline order-dependent with no visible cause.\n * Recorded so the coupling can be reported before it bites.\n */\n readonly couplings?: readonly string[];\n\n /**\n * The document's base URL, `document.baseURI`, as the page reported it.\n *\n * An engine resolves every `url()` in a computed value against this, so a\n * build served on a different port comes back with different strings. The\n * normalizer drops this origin from them. Absent when the collector had no\n * page to ask, and then nothing is dropped.\n */\n readonly baseUrl?: string;\n\n /** Anything the collector could not do. Empty is the expected case. */\n readonly diagnostics: readonly Diagnostic[];\n}\n\nexport interface SubjectRef {\n /** Stable across renames of the file, e.g. `story:components-button--primary`. */\n readonly id: string;\n\n /**\n * What kind of thing this is, which is not the same question as what produced\n * it.\n *\n * `value` is the one that is not a rendering: a JSON body, a schema, a route\n * table. It is a fourth member rather than a reuse of `route`, which already\n * means *a page rendered at a URL* — a route table is a value about the same\n * paths, and a subject list where one word meant both is one nobody can filter.\n */\n readonly kind: 'story' | 'route' | 'fixture' | 'value';\n readonly title?: string;\n}\n\n/**\n * A node exactly as observed, before any rule is applied.\n *\n * Raw means raw: `attributes` still holds generated ids and hashed class names,\n * `matchedRules` still holds cascade losers. Normalization needs the unedited\n * input — a collector that helpfully pre-cleaned would be a second, invisible\n * ruleset, versioned by nothing.\n */\nexport interface RawNode {\n /**\n * Lower-case element name, `#text` for a text run, or `::before` / `::after`\n * for a box CSS generates inside its parent — children with no DOM node,\n * which paint like any other child.\n */\n readonly tag: string;\n readonly attributes: Readonly<Record<string, string>>;\n\n /** Resolved accessibility properties, when the profile provides them. */\n readonly aria?: RawAria;\n\n /**\n * Rules matching this node, as authored.\n *\n * Losing declarations are retained: `core` resolves the cascade for profiles\n * whose engine did not (ADR-0003 step 5), and cannot do that from winners\n * alone. Shorthands are retained *unexpanded* — expansion is `core`'s job, so\n * that it is versioned by the ruleset rather than by each collector.\n *\n * A collector MAY drop a declaration whose property is neither allowlisted nor\n * a shorthand of an allowlisted property, and SHOULD when the capture crosses\n * a network hop. It MUST NOT normalize values; a helpfully pre-cleaned capture\n * is a second, invisible ruleset versioned by nothing.\n */\n readonly matchedRules: readonly MatchedRule[];\n\n readonly inlineStyle?: Readonly<Record<string, string>>;\n\n /** Engine-resolved computed style. Present only when `profile.computedStyle`. */\n readonly computedStyle?: Readonly<Record<string, string>>;\n\n /** Present only when `profile.layout`. */\n readonly rect?: Rect;\n\n /** Literal text of a text node. Digested during normalization, per policy. */\n readonly text?: string;\n\n readonly provenance?: Provenance;\n\n /**\n * How the framework holds this node's component, when an adapter supplied it.\n *\n * Beside `provenance` because it arrives from the same seam and under the same\n * rule: the collector carries no framework dependency and both are injected.\n * See {@link Wiring} for why it is a dimension rather than another attribution\n * field.\n */\n readonly wiring?: Wiring;\n\n /**\n * What the component that authored this node was holding, when an adapter\n * supplied it.\n *\n * A third injection at the same seam, and deliberately not folded into\n * `wiring`: wiring is hashed into a band and this may never be, so a caller\n * that wants the band must be able to have it without also carrying values,\n * and a caller that wants explanations must be able to ask for them without\n * changing a single digest. See {@link Holding}.\n */\n readonly holding?: Holding;\n\n readonly children: readonly RawNode[];\n\n /** Nodes inside a shadow root, kept distinct from light-DOM children. */\n readonly shadowChildren?: readonly RawNode[];\n\n /**\n * Ignore rules the collector found this element under (spec 0024).\n *\n * A mark, not a removal. The collector is the only thing holding a live DOM, so\n * it is the only thing that can run a selector or read a marker attribute — but\n * it must not act on what it finds. An element deleted from the capture would\n * be invisible to every count downstream, and the difference between \"absorbed\n * by the `carousel` rule\" and \"was never there\" is the difference between an\n * ignore and a blind spot.\n *\n * Never hashed. Normalization turns these into\n * {@link import('./snapshot.js').SemanticSnapshot.ignoreSites}, which sits\n * beside `styleProvenance` outside the render hash for the same reason: masking\n * a clock changes what a run *says*, not what it *renders*, and must not\n * re-baseline the repository.\n */\n readonly ignoredBy?: readonly string[];\n\n /**\n * Why part of this node was not read, on a node whose existence is known and\n * whose values are not — a `::before` whose `content` holds a `counter()` under\n * a host with no layout engine.\n *\n * A marker, never a value: the fields it describes are absent rather than\n * guessed (ADR-0002). Outside every hash, like `ignoredBy` above it.\n */\n readonly unread?: string;\n}\n\nexport interface RawAria {\n readonly role: string | null;\n readonly name: string | null;\n\n /**\n * Accessible description — what `aria-describedby` and `title` resolve to.\n *\n * Captured because nothing else captures it, and its absence was a hole. The\n * attribute allowlist drops every `aria-*` attribute on the stated grounds\n * that they \"are resolved into role/name/state\" — true of `aria-label` and\n * `aria-selected`, and false of `aria-describedby`, which resolves into a\n * description and had nowhere to land. A field whose `aria-describedby`\n * pointed at a deleted error message compared **equal**: the reference was\n * dropped by the allowlist, the name was unaffected, and the regression was\n * silent on every tier including raster.\n */\n readonly description?: string | null;\n\n /** `checked`, `disabled`, `expanded`, `selected`, … */\n readonly state: Readonly<Record<string, string | boolean | number>>;\n}\n\nexport interface MatchedRule {\n /** Identifies the origin sheet for attribution: href, or a synthetic id. */\n readonly sheet: string;\n readonly selector: string;\n /** `[idCount, classCount, typeCount]` — CSS specificity, most significant first. */\n readonly specificity: readonly [number, number, number];\n /** Document order of the rule. Breaks specificity ties, per the cascade. */\n readonly order: number;\n readonly declarations: readonly Declaration[];\n /** Source position, when the sheet exposes one. Turns a diff into a file:line. */\n readonly source?: { readonly file: string; readonly line: number };\n}\n\nexport interface Declaration {\n readonly property: string;\n readonly value: string;\n readonly important: boolean;\n /** Custom properties referenced by the value, for token-keyed attribution. */\n readonly references?: readonly string[];\n}\n\n/** Browser-layout coordinates retained as evidence, without a preferred layout policy. */\nexport interface Rect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/** The area two rects share; zero when they only touch or do not meet. */\nexport function intersectionArea(a: Rect, b: Rect): number {\n const width = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);\n const height = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);\n return width > 0 && height > 0 ? width * height : 0;\n}\n\nexport interface Diagnostic {\n readonly severity: 'warn' | 'error';\n readonly code: string;\n readonly message: string;\n readonly nodePath?: string;\n}\n\n/**\n * One list from several, with a diagnostic said once however many times it was\n * found.\n *\n * A subject is described by a document, a snapshot and a comparison, and the\n * same condition is visible to more than one of them -- a missing font is in the\n * document that declared it and in the snapshot that measured it. A reader asks\n * \"what is wrong with this subject\" once, and a list that answers twice reads as\n * two findings.\n *\n * Identity is every field, `nodePath` included: the same code on two nodes is\n * two facts, and collapsing them would report one element where the page has a\n * dozen.\n */\nexport function mergeDiagnostics(\n ...lists: readonly (readonly Diagnostic[] | undefined)[]\n): readonly Diagnostic[] {\n const byIdentity = new Map<string, Diagnostic>();\n for (const list of lists) {\n for (const diagnostic of list ?? []) {\n const key = JSON.stringify([\n diagnostic.severity,\n diagnostic.code,\n diagnostic.message,\n diagnostic.nodePath ?? null,\n ]);\n if (!byIdentity.has(key)) byIdentity.set(key, diagnostic);\n }\n }\n return [...byIdentity.values()];\n}\n"]}
|
|
@@ -198,8 +198,8 @@ export interface RenderIdentity {
|
|
|
198
198
|
* The gap itself is acceptable; a *difference* in it between two runs is not,
|
|
199
199
|
* because the images then differ for a reason that is not the code and the
|
|
200
200
|
* report blames whichever component sits under the pixels. Folding it in here
|
|
201
|
-
*
|
|
202
|
-
*
|
|
201
|
+
* names that gap as the cause of the change instead, by the same mechanism
|
|
202
|
+
* that names a moved machine.
|
|
203
203
|
*
|
|
204
204
|
* Optional only because a renderer may predate the field. Absent means the
|
|
205
205
|
* renderer did not record what it did, which is emphatically not the same as
|
package/dist/format/document.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { digestValue } from './hash.js';
|
|
2
|
+
import { pageOrigin, withoutOrigin } from './origin.js';
|
|
2
3
|
/**
|
|
3
4
|
* Content address of a document: *what is to be painted*.
|
|
4
5
|
*
|
|
@@ -21,6 +22,7 @@ import { digestValue } from './hash.js';
|
|
|
21
22
|
* never a wrong image.
|
|
22
23
|
*/
|
|
23
24
|
export function documentDigest(document) {
|
|
25
|
+
const origin = pageOrigin(document.baseUrl);
|
|
24
26
|
return digestValue({
|
|
25
27
|
documentVersion: document.documentVersion,
|
|
26
28
|
html: document.html,
|
|
@@ -40,11 +42,15 @@ export function documentDigest(document) {
|
|
|
40
42
|
inherited: { ...document.inherited },
|
|
41
43
|
fonts: [...document.fonts],
|
|
42
44
|
...(document.assets ? { assets: { ...document.assets } } : {}),
|
|
43
|
-
|
|
45
|
+
// The base and the page's own resources as paths: a collector serves the
|
|
46
|
+
// build on a free port, and a digest holding it would miss the render cache
|
|
47
|
+
// on every run. The document keeps both absolute, because the renderer
|
|
48
|
+
// resolves against them; a resource another origin serves stays absolute.
|
|
49
|
+
...(document.baseUrl !== undefined ? { baseUrl: withoutOrigin(document.baseUrl, origin) } : {}),
|
|
44
50
|
...(document.resources !== undefined
|
|
45
51
|
? {
|
|
46
52
|
resources: Object.fromEntries(Object.entries(document.resources).map(([url, resource]) => [
|
|
47
|
-
url,
|
|
53
|
+
withoutOrigin(url, origin),
|
|
48
54
|
{ ...resource },
|
|
49
55
|
])),
|
|
50
56
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document.js","sourceRoot":"","sources":["../../src/format/document.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Diagnostic, SubjectRef } from './capture.js';\nimport type { Viewport } from './environment.js';\nimport { digestValue, type Digest } from './hash.js';\nimport type { ComponentHash } from './snapshot.js';\n\n/**\n * The render document: what gets *sent* somewhere to become an image.\n *\n * This is the acquisition phase's output and the only thing the rendering side\n * ever sees. It is plain serializable data for the same reason `RawCapture` is\n * (ADR-0006): the renderer may be a page in this process, a browser on this\n * machine, or a pinned container two networks away, and no code upstream of it\n * may assume which.\n *\n * Two routes produce one of these, and the point of the type is that they are\n * interchangeable:\n *\n * 1. **jsdom → document → render elsewhere.** jsdom cannot rasterize. It can\n * describe exactly what to rasterize, which is the sub-renderer split ADR-0002\n * asks for: the cheap tier decides almost everything, and the residue is\n * handed to something that owns a GPU.\n * 2. **playwright → document → render on a server.** The same payload, acquired\n * from a real engine. Used when the *deciding* machine and the *pinned*\n * machine are different, which is the case every CI setup actually has.\n *\n * A browser may instead capture a `Raster` in place. That is the sibling branch\n * of `CaptureMaterial`, not a third route that somehow produces this type.\n *\n * ## Why this is small\n *\n * `css` holds the *applicable* rules only — the pruning of ADR-0003, which on\n * `@variance-authority/dom`'s accretion fixture takes 1010 rules to 1. That\n * number is a cost claim as much as a\n * correctness one: shipping a document over a network hop is only sensible if the\n * document is not the entire design system plus Storybook's chrome.\n */\nexport interface RenderDocument {\n readonly documentVersion: 1;\n readonly subject: SubjectRef;\n\n /**\n * The subject subtree, serialized.\n *\n * Serialized rather than referenced by URL: a URL makes the renderer\n * responsible for reproducing the application's state, which it cannot do and\n * should not try. What is sent is the markup that existed at the moment of\n * capture, which is the only definition of \"this render\" that survives a hop.\n */\n readonly html: string;\n\n /**\n * The ancestor context the subject was rendered inside.\n *\n * Not decoration. Pruning keeps rules like `html.dark .card` and\n * `.app .list > li`, whose left-hand side lives *above* the subject — render\n * the subtree bare and those rules match nothing, so the image is missing\n * exactly the styling the collector went to the trouble of proving applies.\n *\n * Reproduced as empty open tags rather than by re-rendering the application:\n * what a selector needs is a chain of elements with the right tags, ids,\n * classes, and attributes, and nothing above the subject contributes anything\n * else to it that {@link RenderDocument.inherited} does not already carry.\n */\n readonly frame: RenderFrame;\n\n /**\n * Applicable stylesheet text, in cascade order.\n *\n * Order is load-bearing and is the caller's to preserve — the cascade breaks\n * specificity ties by document order, so a set that arrives shuffled paints\n * differently while hashing the same if this were a set rather than a list.\n */\n readonly css: readonly string[];\n\n readonly viewport: Viewport;\n\n /**\n * Inherited values in force at the subject root, applied to its wrapper.\n *\n * The same requirement as `RawCapture.inheritedSeed` and for the same reason:\n * pruning drops rules on ancestors outside the subtree, and a font-size\n * inherited from `<html>` is not optional decoration — dropping it changes\n * every metric in the image.\n */\n readonly inherited: Readonly<Record<string, string>>;\n\n /**\n * Fonts the acquiring side observed as loaded.\n *\n * Carried, not resolved. A renderer that lacks one of these will substitute\n * and produce different metrics, and that has to be *detectable* rather than\n * silently absorbed — see {@link RenderIdentity}.\n */\n readonly fonts: readonly string[];\n\n /** External references, url → content hash. Present so a swap is visible. */\n readonly assets?: Readonly<Record<string, string>>;\n\n /**\n * Base against which relative resource references were resolved.\n *\n * Preserves relative-reference resolution for either document kind. Resource\n * closure is denoted by `resources`, not by the presence of this field.\n */\n readonly baseUrl?: string;\n\n /**\n * Immutable responses needed to paint this document, keyed by absolute URL.\n *\n * Presence — including an empty object — means the document is\n * resource-closed: a renderer must not consult the network for a missing URL.\n * Absence preserves the older network-capable document contract and must not\n * be described as portable to another machine.\n */\n readonly resources?: Readonly<Record<string, RenderResource>>;\n\n readonly diagnostics: readonly Diagnostic[];\n}\n\nexport interface RenderResource {\n readonly contentType: string;\n /** Response bytes, base64. */\n readonly bytes: string;\n readonly digest: Digest;\n\n /**\n * The status a renderer answers this URL with. Absent means 200.\n *\n * A subject is allowed to reference something that is not there: a fallback\n * test does it on purpose, and the browser it was acquired in painted the\n * broken state rather than the image. Recording the failure as a failure is\n * what lets a renderer reproduce that state without asking the network.\n */\n readonly status?: number;\n}\n\nexport interface RenderFrame {\n /** Attributes on `<html>`. `class=\"dark\"` here decides half a design system. */\n readonly html: Readonly<Record<string, string>>;\n readonly body: Readonly<Record<string, string>>;\n /** Elements between `<body>` and the subject root, outermost first. */\n readonly ancestors: readonly FrameElement[];\n\n /**\n * Used width of the subject's parent box, in CSS pixels, when layout was\n * observable.\n *\n * A subject declaring `width: 100%` is a different size in a 1024px page and\n * inside a 600px panel, and nothing else in this document says which it was.\n * Absent under a profile with no layout engine, in which case the render falls\n * back to the viewport and the difference, if any, is real rather than hidden.\n */\n readonly containerWidth?: number;\n}\n\nexport interface FrameElement {\n readonly tag: string;\n readonly attributes: Readonly<Record<string, string>>;\n}\n\n/**\n * Content address of a document: *what is to be painted*.\n *\n * This is the deferral lever. Rendering is the expensive phase, and a document\n * whose digest has been rendered before under the same identity has an image\n * already — so the cheapest render is the one that is skipped. Nothing about the\n * branch, the commit, or the file path enters this (Principle 4), so moving a\n * story between files does not cost a re-render.\n *\n * Deliberately covers `viewport` in full, `deviceScaleFactor` included: unlike\n * the semantic key (ADR-0010), a raster genuinely differs at 2x and the whole\n * purpose of this digest is to address the raster.\n *\n * **Limit, stated rather than discovered.** Declaration values arrive serialized\n * by the acquiring engine's CSSOM — jsdom writes `rgb(18, 52, 86)` where the\n * author wrote `#123456` — and this phase deliberately does not canonicalize\n * them, because a collector that normalizes is a second ruleset versioned by\n * nothing (ADR-0001). So two engines can address the same page differently. That\n * makes a render cache *per acquiring engine*, which is a missed cache hit and\n * never a wrong image.\n */\nexport function documentDigest(document: RenderDocument): Digest {\n return digestValue({\n documentVersion: document.documentVersion,\n html: document.html,\n frame: {\n html: { ...document.frame.html },\n body: { ...document.frame.body },\n ancestors: document.frame.ancestors.map((element) => ({\n tag: element.tag,\n attributes: { ...element.attributes },\n })),\n ...(document.frame.containerWidth !== undefined\n ? { containerWidth: document.frame.containerWidth }\n : {}),\n },\n css: [...document.css],\n viewport: { ...document.viewport },\n inherited: { ...document.inherited },\n fonts: [...document.fonts],\n ...(document.assets ? { assets: { ...document.assets } } : {}),\n ...(document.baseUrl !== undefined ? { baseUrl: document.baseUrl } : {}),\n ...(document.resources !== undefined\n ? {\n resources: Object.fromEntries(\n Object.entries(document.resources).map(([url, resource]) => [\n url,\n { ...resource },\n ]),\n ),\n }\n : {}),\n });\n}\n\n/**\n * Who painted it.\n *\n * The durable half of this system rests on one requirement — *images compared\n * against each other must come from the same machine* — and a requirement that\n * is merely stated gets violated by a CI runner upgrade nobody announced. This\n * makes it a value, so it can be compared rather than assumed.\n *\n * Every field is something that has been observed to move pixels without moving\n * markup. None of them is a guess about what *might* matter.\n */\nexport interface RenderIdentity {\n /** `playwright-chromium@1.49.0`, `remote:render.internal`, … */\n readonly renderer: string;\n /** Engine build. A Chromium bump repaints text; that is not a regression. */\n readonly engine: string;\n /** OS and architecture. Font rasterization differs across both. */\n readonly platform: string;\n readonly deviceScaleFactor: number;\n /**\n * Fonts the *renderer* actually has.\n *\n * Compared against {@link RenderDocument.fonts} at render time, because a\n * substituted font is the single most common way two machines disagree, and\n * it is invisible in every artifact except the image itself.\n */\n readonly fonts: readonly string[];\n\n /**\n * Digest of what the renderer did *to* the page so it could be observed.\n *\n * Pausing animations, hiding a caret, waiting for fonts — each is necessary\n * and each widens the gap between what was measured and what a person sees.\n * The gap itself is acceptable; a *difference* in it between two runs is not,\n * because the images then differ for a reason that is not the code and the\n * report blames whichever component sits under the pixels. Folding it in here\n * makes that comparison `incomparable` instead, by the same mechanism that\n * refuses a cross-machine one.\n *\n * Optional only because a renderer may predate the field. Absent means the\n * renderer did not record what it did, which is emphatically not the same as\n * \"it did nothing\" — and two such renderers will compare, which is the hazard\n * that argues for every renderer setting it.\n */\n readonly stabilization?: Digest;\n\n /**\n * Digest of pixel-affecting browser launch and raster settings.\n *\n * Separate from stabilization: one describes how the browser paints, the\n * other what was done to the page before it was read.\n */\n readonly rasterization?: Digest;\n}\n\nexport function identityDigest(identity: RenderIdentity): Digest {\n return digestValue({\n renderer: identity.renderer,\n engine: identity.engine,\n platform: identity.platform,\n ...(identity.stabilization !== undefined ? { stabilization: identity.stabilization } : {}),\n ...(identity.rasterization !== undefined ? { rasterization: identity.rasterization } : {}),\n deviceScaleFactor: identity.deviceScaleFactor,\n fonts: [...identity.fonts],\n });\n}\n\n/**\n * A rendered image and the conditions it was produced under.\n *\n * `bytes` is base64 rather than a `Buffer` so the whole value survives the same\n * hops the document does. A raster that could only exist in Node would make the\n * remote route a special case in every function that touches one.\n *\n * The image is optional, and the three fields that describe it move together.\n * A subject that occupies no pixels — a wrapper whose only child went to a\n * portal, a `describeConformance` mount with no children — still has a\n * document, rules, components and an accessibility tree, all of which compare.\n * Refusing to photograph it is right; refusing the subject reported a quarter\n * of Material UI's unit tier as unobserved over an axis nothing was asking\n * about. Absent here means *this subject has no pixels to observe*, which is a\n * measurement; it never means *the image was lost*.\n */\nexport interface Raster {\n readonly documentDigest: Digest;\n readonly identity: RenderIdentity;\n /**\n * Device pixels. `width / viewport.width` is the scale, and is asserted.\n *\n * Absent exactly when {@link bytes} is, and {@link pictured} is the one place\n * that reads the three together.\n */\n readonly width?: number;\n readonly height?: number;\n /** PNG, base64. Absent when the subject occupies no pixels. */\n readonly bytes?: string;\n /** Fonts the document declared that the renderer did not have. */\n readonly missingFonts: readonly string[];\n\n /**\n * The browser-computed accessibility tree from the acquisition that produced\n * this candidate. It is independent evidence: pixel equality cannot settle it.\n * Absent means the acquisition host did not observe this boundary.\n */\n readonly accessibility?: import('./accessibility.js').AccessibilitySnapshot;\n\n /**\n * What the document that painted this said about its own components (ADR-0018).\n *\n * The field that makes a baseline self-describing, and the reason it is here\n * rather than only in a history store. Separating the component that *caused* a\n * change from the components the change merely moved needs both revisions, and\n * a stored baseline is an image: without this, a run has one document and ranks\n * by area — which\n * [journal 0013](../../../../docs/context/journal/0013-observability.md)\n * measured as backwards.\n *\n * Text, and small: one line per component boundary. It rides in the sidecar\n * beside a PNG that dominates it, so a baseline carries its own explanation\n * wherever it is copied, and a store or a transport that drops it degrades to\n * ranking by area rather than to a wrong answer.\n *\n * Optional because a baseline written before this existed does not have it, and\n * because a profile or a collector that supplies no snapshot cannot produce it.\n * Absent means *unknown*, never *no components*.\n */\n readonly components?: readonly ComponentHash[];\n\n /**\n * What inspection found in the document that painted this, as marks.\n *\n * The field that lets a later run say *this defect was already here* instead of\n * printing every defect it finds with no indication of when it arrived. A\n * finding is produced from one render with no baseline consulted — that is the\n * point of having one — and the consequence is that the list is identical on\n * the run that introduced a defect and on the two hundred runs after it. A\n * reviewer reading `a control inside another control` has no way to tell an\n * inherited condition from something they just wrote, and the two are different\n * decisions.\n *\n * Marks rather than findings, from {@link findingMark}. The sentence, the\n * landmark phrase and the source line are all rewritten by edits that do not\n * touch the defect, so storing them would report a defect as new every time the\n * copy beside it changed — and they are the bulk of the bytes, in a sidecar\n * that is committed beside every baseline.\n *\n * **Absent means nothing inspected it, never that it was clean.** A baseline\n * written before this existed, or by a collector that supplies no snapshot, has\n * no list, and a run that read absence as `[]` would announce every standing\n * defect in the suite as newly introduced on the first run after an upgrade.\n */\n readonly findingMarks?: readonly string[];\n}\n\n/**\n * A raster whose image is in hand.\n *\n * Every pixel comparison in the codebase needs all three of `bytes`, `width`\n * and `height`, and they are absent together or present together. Narrowing\n * them one at a time is how a caller ends up reading the bytes of a subject\n * that was never photographed, so the narrowing lives here and is done once.\n */\nexport type Pictured = Raster & {\n readonly width: number;\n readonly height: number;\n readonly bytes: string;\n};\n\n/** Whether this record carries an image, narrowing all three fields together. */\nexport function pictured(raster: Raster): raster is Pictured {\n return raster.bytes !== undefined && raster.width !== undefined && raster.height !== undefined;\n}\n\n/**\n * Whether the subject occupied any pixels, answered without the image.\n *\n * The question {@link pictured} cannot be asked of a sidecar. Bytes live in a\n * separate file, row or object, so a record read without them carries the width\n * and the height and nothing else — and those *are* the store's record of\n * whether there was ever anything to photograph, which is why `sidecarFrom`\n * refuses a sidecar carrying one of them.\n *\n * So: this for a record, {@link pictured} for an image in hand. A caller that\n * reached for `pictured` here would read every sidecar in the store as a subject\n * with no pixels, because none of them have bytes.\n */\nexport function occupiesPixels(raster: Omit<Raster, 'bytes'>): boolean {\n return raster.width !== undefined && raster.height !== undefined;\n}\n\n/**\n * The image, or a refusal naming the subject that has none.\n *\n * For the paths where a missing image is a programming error rather than a\n * measurement — a diff, a PNG encode — so they read as assertions instead of\n * as a non-null assertion nobody can audit.\n */\nexport function picture(raster: Raster, subject: string): Pictured {\n if (!pictured(raster)) {\n throw new Error(\n `\\`${subject}\\` occupies no pixels, and this path needs an image; ` +\n 'a subject with no raster compares on its document alone',\n );\n }\n return raster;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"document.js","sourceRoot":"","sources":["../../src/format/document.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Diagnostic, SubjectRef } from './capture.js';\nimport type { Viewport } from './environment.js';\nimport { digestValue, type Digest } from './hash.js';\nimport { pageOrigin, withoutOrigin } from './origin.js';\nimport type { ComponentHash } from './snapshot.js';\n\n/**\n * The render document: what gets *sent* somewhere to become an image.\n *\n * This is the acquisition phase's output and the only thing the rendering side\n * ever sees. It is plain serializable data for the same reason `RawCapture` is\n * (ADR-0006): the renderer may be a page in this process, a browser on this\n * machine, or a pinned container two networks away, and no code upstream of it\n * may assume which.\n *\n * Two routes produce one of these, and the point of the type is that they are\n * interchangeable:\n *\n * 1. **jsdom → document → render elsewhere.** jsdom cannot rasterize. It can\n * describe exactly what to rasterize, which is the sub-renderer split ADR-0002\n * asks for: the cheap tier decides almost everything, and the residue is\n * handed to something that owns a GPU.\n * 2. **playwright → document → render on a server.** The same payload, acquired\n * from a real engine. Used when the *deciding* machine and the *pinned*\n * machine are different, which is the case every CI setup actually has.\n *\n * A browser may instead capture a `Raster` in place. That is the sibling branch\n * of `CaptureMaterial`, not a third route that somehow produces this type.\n *\n * ## Why this is small\n *\n * `css` holds the *applicable* rules only — the pruning of ADR-0003, which on\n * `@variance-authority/dom`'s accretion fixture takes 1010 rules to 1. That\n * number is a cost claim as much as a\n * correctness one: shipping a document over a network hop is only sensible if the\n * document is not the entire design system plus Storybook's chrome.\n */\nexport interface RenderDocument {\n readonly documentVersion: 1;\n readonly subject: SubjectRef;\n\n /**\n * The subject subtree, serialized.\n *\n * Serialized rather than referenced by URL: a URL makes the renderer\n * responsible for reproducing the application's state, which it cannot do and\n * should not try. What is sent is the markup that existed at the moment of\n * capture, which is the only definition of \"this render\" that survives a hop.\n */\n readonly html: string;\n\n /**\n * The ancestor context the subject was rendered inside.\n *\n * Not decoration. Pruning keeps rules like `html.dark .card` and\n * `.app .list > li`, whose left-hand side lives *above* the subject — render\n * the subtree bare and those rules match nothing, so the image is missing\n * exactly the styling the collector went to the trouble of proving applies.\n *\n * Reproduced as empty open tags rather than by re-rendering the application:\n * what a selector needs is a chain of elements with the right tags, ids,\n * classes, and attributes, and nothing above the subject contributes anything\n * else to it that {@link RenderDocument.inherited} does not already carry.\n */\n readonly frame: RenderFrame;\n\n /**\n * Applicable stylesheet text, in cascade order.\n *\n * Order is load-bearing and is the caller's to preserve — the cascade breaks\n * specificity ties by document order, so a set that arrives shuffled paints\n * differently while hashing the same if this were a set rather than a list.\n */\n readonly css: readonly string[];\n\n readonly viewport: Viewport;\n\n /**\n * Inherited values in force at the subject root, applied to its wrapper.\n *\n * The same requirement as `RawCapture.inheritedSeed` and for the same reason:\n * pruning drops rules on ancestors outside the subtree, and a font-size\n * inherited from `<html>` is not optional decoration — dropping it changes\n * every metric in the image.\n */\n readonly inherited: Readonly<Record<string, string>>;\n\n /**\n * Fonts the acquiring side observed as loaded.\n *\n * Carried, not resolved. A renderer that lacks one of these will substitute\n * and produce different metrics, and that has to be *detectable* rather than\n * silently absorbed — see {@link RenderIdentity}.\n */\n readonly fonts: readonly string[];\n\n /** External references, url → content hash. Present so a swap is visible. */\n readonly assets?: Readonly<Record<string, string>>;\n\n /**\n * Base against which relative resource references were resolved.\n *\n * Preserves relative-reference resolution for either document kind. Resource\n * closure is denoted by `resources`, not by the presence of this field.\n */\n readonly baseUrl?: string;\n\n /**\n * Immutable responses needed to paint this document, keyed by absolute URL.\n *\n * Presence — including an empty object — means the document is\n * resource-closed: a renderer must not consult the network for a missing URL.\n * Absence preserves the older network-capable document contract and must not\n * be described as portable to another machine.\n */\n readonly resources?: Readonly<Record<string, RenderResource>>;\n\n readonly diagnostics: readonly Diagnostic[];\n}\n\nexport interface RenderResource {\n readonly contentType: string;\n /** Response bytes, base64. */\n readonly bytes: string;\n readonly digest: Digest;\n\n /**\n * The status a renderer answers this URL with. Absent means 200.\n *\n * A subject is allowed to reference something that is not there: a fallback\n * test does it on purpose, and the browser it was acquired in painted the\n * broken state rather than the image. Recording the failure as a failure is\n * what lets a renderer reproduce that state without asking the network.\n */\n readonly status?: number;\n}\n\nexport interface RenderFrame {\n /** Attributes on `<html>`. `class=\"dark\"` here decides half a design system. */\n readonly html: Readonly<Record<string, string>>;\n readonly body: Readonly<Record<string, string>>;\n /** Elements between `<body>` and the subject root, outermost first. */\n readonly ancestors: readonly FrameElement[];\n\n /**\n * Used width of the subject's parent box, in CSS pixels, when layout was\n * observable.\n *\n * A subject declaring `width: 100%` is a different size in a 1024px page and\n * inside a 600px panel, and nothing else in this document says which it was.\n * Absent under a profile with no layout engine, in which case the render falls\n * back to the viewport and the difference, if any, is real rather than hidden.\n */\n readonly containerWidth?: number;\n}\n\nexport interface FrameElement {\n readonly tag: string;\n readonly attributes: Readonly<Record<string, string>>;\n}\n\n/**\n * Content address of a document: *what is to be painted*.\n *\n * This is the deferral lever. Rendering is the expensive phase, and a document\n * whose digest has been rendered before under the same identity has an image\n * already — so the cheapest render is the one that is skipped. Nothing about the\n * branch, the commit, or the file path enters this (Principle 4), so moving a\n * story between files does not cost a re-render.\n *\n * Deliberately covers `viewport` in full, `deviceScaleFactor` included: unlike\n * the semantic key (ADR-0010), a raster genuinely differs at 2x and the whole\n * purpose of this digest is to address the raster.\n *\n * **Limit, stated rather than discovered.** Declaration values arrive serialized\n * by the acquiring engine's CSSOM — jsdom writes `rgb(18, 52, 86)` where the\n * author wrote `#123456` — and this phase deliberately does not canonicalize\n * them, because a collector that normalizes is a second ruleset versioned by\n * nothing (ADR-0001). So two engines can address the same page differently. That\n * makes a render cache *per acquiring engine*, which is a missed cache hit and\n * never a wrong image.\n */\nexport function documentDigest(document: RenderDocument): Digest {\n const origin = pageOrigin(document.baseUrl);\n return digestValue({\n documentVersion: document.documentVersion,\n html: document.html,\n frame: {\n html: { ...document.frame.html },\n body: { ...document.frame.body },\n ancestors: document.frame.ancestors.map((element) => ({\n tag: element.tag,\n attributes: { ...element.attributes },\n })),\n ...(document.frame.containerWidth !== undefined\n ? { containerWidth: document.frame.containerWidth }\n : {}),\n },\n css: [...document.css],\n viewport: { ...document.viewport },\n inherited: { ...document.inherited },\n fonts: [...document.fonts],\n ...(document.assets ? { assets: { ...document.assets } } : {}),\n // The base and the page's own resources as paths: a collector serves the\n // build on a free port, and a digest holding it would miss the render cache\n // on every run. The document keeps both absolute, because the renderer\n // resolves against them; a resource another origin serves stays absolute.\n ...(document.baseUrl !== undefined ? { baseUrl: withoutOrigin(document.baseUrl, origin) } : {}),\n ...(document.resources !== undefined\n ? {\n resources: Object.fromEntries(\n Object.entries(document.resources).map(([url, resource]) => [\n withoutOrigin(url, origin),\n { ...resource },\n ]),\n ),\n }\n : {}),\n });\n}\n\n/**\n * Who painted it.\n *\n * The durable half of this system rests on one requirement — *images compared\n * against each other must come from the same machine* — and a requirement that\n * is merely stated gets violated by a CI runner upgrade nobody announced. This\n * makes it a value, so it can be compared rather than assumed.\n *\n * Every field is something that has been observed to move pixels without moving\n * markup. None of them is a guess about what *might* matter.\n */\nexport interface RenderIdentity {\n /** `playwright-chromium@1.49.0`, `remote:render.internal`, … */\n readonly renderer: string;\n /** Engine build. A Chromium bump repaints text; that is not a regression. */\n readonly engine: string;\n /** OS and architecture. Font rasterization differs across both. */\n readonly platform: string;\n readonly deviceScaleFactor: number;\n /**\n * Fonts the *renderer* actually has.\n *\n * Compared against {@link RenderDocument.fonts} at render time, because a\n * substituted font is the single most common way two machines disagree, and\n * it is invisible in every artifact except the image itself.\n */\n readonly fonts: readonly string[];\n\n /**\n * Digest of what the renderer did *to* the page so it could be observed.\n *\n * Pausing animations, hiding a caret, waiting for fonts — each is necessary\n * and each widens the gap between what was measured and what a person sees.\n * The gap itself is acceptable; a *difference* in it between two runs is not,\n * because the images then differ for a reason that is not the code and the\n * report blames whichever component sits under the pixels. Folding it in here\n * names that gap as the cause of the change instead, by the same mechanism\n * that names a moved machine.\n *\n * Optional only because a renderer may predate the field. Absent means the\n * renderer did not record what it did, which is emphatically not the same as\n * \"it did nothing\" — and two such renderers will compare, which is the hazard\n * that argues for every renderer setting it.\n */\n readonly stabilization?: Digest;\n\n /**\n * Digest of pixel-affecting browser launch and raster settings.\n *\n * Separate from stabilization: one describes how the browser paints, the\n * other what was done to the page before it was read.\n */\n readonly rasterization?: Digest;\n}\n\nexport function identityDigest(identity: RenderIdentity): Digest {\n return digestValue({\n renderer: identity.renderer,\n engine: identity.engine,\n platform: identity.platform,\n ...(identity.stabilization !== undefined ? { stabilization: identity.stabilization } : {}),\n ...(identity.rasterization !== undefined ? { rasterization: identity.rasterization } : {}),\n deviceScaleFactor: identity.deviceScaleFactor,\n fonts: [...identity.fonts],\n });\n}\n\n/**\n * A rendered image and the conditions it was produced under.\n *\n * `bytes` is base64 rather than a `Buffer` so the whole value survives the same\n * hops the document does. A raster that could only exist in Node would make the\n * remote route a special case in every function that touches one.\n *\n * The image is optional, and the three fields that describe it move together.\n * A subject that occupies no pixels — a wrapper whose only child went to a\n * portal, a `describeConformance` mount with no children — still has a\n * document, rules, components and an accessibility tree, all of which compare.\n * Refusing to photograph it is right; refusing the subject reported a quarter\n * of Material UI's unit tier as unobserved over an axis nothing was asking\n * about. Absent here means *this subject has no pixels to observe*, which is a\n * measurement; it never means *the image was lost*.\n */\nexport interface Raster {\n readonly documentDigest: Digest;\n readonly identity: RenderIdentity;\n /**\n * Device pixels. `width / viewport.width` is the scale, and is asserted.\n *\n * Absent exactly when {@link bytes} is, and {@link pictured} is the one place\n * that reads the three together.\n */\n readonly width?: number;\n readonly height?: number;\n /** PNG, base64. Absent when the subject occupies no pixels. */\n readonly bytes?: string;\n /** Fonts the document declared that the renderer did not have. */\n readonly missingFonts: readonly string[];\n\n /**\n * The browser-computed accessibility tree from the acquisition that produced\n * this candidate. It is independent evidence: pixel equality cannot settle it.\n * Absent means the acquisition host did not observe this boundary.\n */\n readonly accessibility?: import('./accessibility.js').AccessibilitySnapshot;\n\n /**\n * What the document that painted this said about its own components (ADR-0018).\n *\n * The field that makes a baseline self-describing, and the reason it is here\n * rather than only in a history store. Separating the component that *caused* a\n * change from the components the change merely moved needs both revisions, and\n * a stored baseline is an image: without this, a run has one document and ranks\n * by area — which\n * [journal 0013](../../../../docs/context/journal/0013-observability.md)\n * measured as backwards.\n *\n * Text, and small: one line per component boundary. It rides in the sidecar\n * beside a PNG that dominates it, so a baseline carries its own explanation\n * wherever it is copied, and a store or a transport that drops it degrades to\n * ranking by area rather than to a wrong answer.\n *\n * Optional because a baseline written before this existed does not have it, and\n * because a profile or a collector that supplies no snapshot cannot produce it.\n * Absent means *unknown*, never *no components*.\n */\n readonly components?: readonly ComponentHash[];\n\n /**\n * What inspection found in the document that painted this, as marks.\n *\n * The field that lets a later run say *this defect was already here* instead of\n * printing every defect it finds with no indication of when it arrived. A\n * finding is produced from one render with no baseline consulted — that is the\n * point of having one — and the consequence is that the list is identical on\n * the run that introduced a defect and on the two hundred runs after it. A\n * reviewer reading `a control inside another control` has no way to tell an\n * inherited condition from something they just wrote, and the two are different\n * decisions.\n *\n * Marks rather than findings, from {@link findingMark}. The sentence, the\n * landmark phrase and the source line are all rewritten by edits that do not\n * touch the defect, so storing them would report a defect as new every time the\n * copy beside it changed — and they are the bulk of the bytes, in a sidecar\n * that is committed beside every baseline.\n *\n * **Absent means nothing inspected it, never that it was clean.** A baseline\n * written before this existed, or by a collector that supplies no snapshot, has\n * no list, and a run that read absence as `[]` would announce every standing\n * defect in the suite as newly introduced on the first run after an upgrade.\n */\n readonly findingMarks?: readonly string[];\n}\n\n/**\n * A raster whose image is in hand.\n *\n * Every pixel comparison in the codebase needs all three of `bytes`, `width`\n * and `height`, and they are absent together or present together. Narrowing\n * them one at a time is how a caller ends up reading the bytes of a subject\n * that was never photographed, so the narrowing lives here and is done once.\n */\nexport type Pictured = Raster & {\n readonly width: number;\n readonly height: number;\n readonly bytes: string;\n};\n\n/** Whether this record carries an image, narrowing all three fields together. */\nexport function pictured(raster: Raster): raster is Pictured {\n return raster.bytes !== undefined && raster.width !== undefined && raster.height !== undefined;\n}\n\n/**\n * Whether the subject occupied any pixels, answered without the image.\n *\n * The question {@link pictured} cannot be asked of a sidecar. Bytes live in a\n * separate file, row or object, so a record read without them carries the width\n * and the height and nothing else — and those *are* the store's record of\n * whether there was ever anything to photograph, which is why `sidecarFrom`\n * refuses a sidecar carrying one of them.\n *\n * So: this for a record, {@link pictured} for an image in hand. A caller that\n * reached for `pictured` here would read every sidecar in the store as a subject\n * with no pixels, because none of them have bytes.\n */\nexport function occupiesPixels(raster: Omit<Raster, 'bytes'>): boolean {\n return raster.width !== undefined && raster.height !== undefined;\n}\n\n/**\n * The image, or a refusal naming the subject that has none.\n *\n * For the paths where a missing image is a programming error rather than a\n * measurement — a diff, a PNG encode — so they read as assertions instead of\n * as a non-null assertion nobody can audit.\n */\nexport function picture(raster: Raster, subject: string): Pictured {\n if (!pictured(raster)) {\n throw new Error(\n `\\`${subject}\\` occupies no pixels, and this path needs an image; ` +\n 'a subject with no raster compares on its document alone',\n );\n }\n return raster;\n}\n"]}
|