@variance-authority/core 0.1.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 +79 -0
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/artifact.d.ts +47 -0
- package/dist/artifact.js +2 -0
- package/dist/artifact.js.map +1 -0
- package/dist/attribute/because.d.ts +43 -0
- package/dist/attribute/because.js +64 -0
- package/dist/attribute/because.js.map +1 -0
- package/dist/attribute/boundary.d.ts +191 -0
- package/dist/attribute/boundary.js +328 -0
- package/dist/attribute/boundary.js.map +1 -0
- package/dist/attribute/call-site.d.ts +96 -0
- package/dist/attribute/call-site.js +183 -0
- package/dist/attribute/call-site.js.map +1 -0
- package/dist/attribute/component-hash.d.ts +188 -0
- package/dist/attribute/component-hash.js +312 -0
- package/dist/attribute/component-hash.js.map +1 -0
- package/dist/attribute/composition-fixture.d.ts +18 -0
- package/dist/attribute/composition-fixture.js +53 -0
- package/dist/attribute/composition-fixture.js.map +1 -0
- package/dist/attribute/composition.d.ts +182 -0
- package/dist/attribute/composition.js +169 -0
- package/dist/attribute/composition.js.map +1 -0
- package/dist/attribute/control.d.ts +43 -0
- package/dist/attribute/control.js +54 -0
- package/dist/attribute/control.js.map +1 -0
- package/dist/attribute/coverage.d.ts +99 -0
- package/dist/attribute/coverage.js +86 -0
- package/dist/attribute/coverage.js.map +1 -0
- package/dist/attribute/divergence.d.ts +102 -0
- package/dist/attribute/divergence.js +152 -0
- package/dist/attribute/divergence.js.map +1 -0
- package/dist/attribute/index.d.ts +43 -0
- package/dist/attribute/index.js +29 -0
- package/dist/attribute/index.js.map +1 -0
- package/dist/attribute/instability.d.ts +89 -0
- package/dist/attribute/instability.js +183 -0
- package/dist/attribute/instability.js.map +1 -0
- package/dist/attribute/instances.d.ts +145 -0
- package/dist/attribute/instances.js +140 -0
- package/dist/attribute/instances.js.map +1 -0
- package/dist/attribute/locate.d.ts +22 -0
- package/dist/attribute/locate.js +100 -0
- package/dist/attribute/locate.js.map +1 -0
- package/dist/attribute/mask.d.ts +156 -0
- package/dist/attribute/mask.js +201 -0
- package/dist/attribute/mask.js.map +1 -0
- package/dist/attribute/movement.d.ts +211 -0
- package/dist/attribute/movement.js +181 -0
- package/dist/attribute/movement.js.map +1 -0
- package/dist/attribute/region.d.ts +188 -0
- package/dist/attribute/region.js +144 -0
- package/dist/attribute/region.js.map +1 -0
- package/dist/attribute/source-map.d.ts +98 -0
- package/dist/attribute/source-map.js +264 -0
- package/dist/attribute/source-map.js.map +1 -0
- package/dist/attribute/source.d.ts +77 -0
- package/dist/attribute/source.js +95 -0
- package/dist/attribute/source.js.map +1 -0
- package/dist/attribute/stack.d.ts +80 -0
- package/dist/attribute/stack.js +108 -0
- package/dist/attribute/stack.js.map +1 -0
- package/dist/compare/band.d.ts +85 -0
- package/dist/compare/band.js +70 -0
- package/dist/compare/band.js.map +1 -0
- package/dist/compare/cascade.d.ts +45 -0
- package/dist/compare/cascade.js +71 -0
- package/dist/compare/cascade.js.map +1 -0
- package/dist/compare/derive.d.ts +78 -0
- package/dist/compare/derive.js +39 -0
- package/dist/compare/derive.js.map +1 -0
- package/dist/compare/diff/attribution.d.ts +23 -0
- package/dist/compare/diff/attribution.js +244 -0
- package/dist/compare/diff/attribution.js.map +1 -0
- package/dist/compare/diff/compare-nodes.d.ts +15 -0
- package/dist/compare/diff/compare-nodes.js +227 -0
- package/dist/compare/diff/compare-nodes.js.map +1 -0
- package/dist/compare/diff/components.d.ts +20 -0
- package/dist/compare/diff/components.js +76 -0
- package/dist/compare/diff/components.js.map +1 -0
- package/dist/compare/diff/delta.d.ts +165 -0
- package/dist/compare/diff/delta.js +12 -0
- package/dist/compare/diff/delta.js.map +1 -0
- package/dist/compare/diff/index.d.ts +69 -0
- package/dist/compare/diff/index.js +101 -0
- package/dist/compare/diff/index.js.map +1 -0
- package/dist/compare/diff/match.d.ts +37 -0
- package/dist/compare/diff/match.js +167 -0
- package/dist/compare/diff/match.js.map +1 -0
- package/dist/compare/explain.d.ts +23 -0
- package/dist/compare/explain.js +190 -0
- package/dist/compare/explain.js.map +1 -0
- package/dist/compare/holding-diff.d.ts +25 -0
- package/dist/compare/holding-diff.js +74 -0
- package/dist/compare/holding-diff.js.map +1 -0
- package/dist/compare/impact.d.ts +56 -0
- package/dist/compare/impact.js +124 -0
- package/dist/compare/impact.js.map +1 -0
- package/dist/compare/index.d.ts +30 -0
- package/dist/compare/index.js +22 -0
- package/dist/compare/index.js.map +1 -0
- package/dist/compare/instance.d.ts +30 -0
- package/dist/compare/instance.js +145 -0
- package/dist/compare/instance.js.map +1 -0
- package/dist/compare/observability.d.ts +49 -0
- package/dist/compare/observability.js +57 -0
- package/dist/compare/observability.js.map +1 -0
- package/dist/compare/parting.d.ts +179 -0
- package/dist/compare/parting.js +244 -0
- package/dist/compare/parting.js.map +1 -0
- package/dist/compare/slice.d.ts +74 -0
- package/dist/compare/slice.js +79 -0
- package/dist/compare/slice.js.map +1 -0
- package/dist/compare/value.d.ts +60 -0
- package/dist/compare/value.js +111 -0
- package/dist/compare/value.js.map +1 -0
- package/dist/format/accessibility.d.ts +27 -0
- package/dist/format/accessibility.js +17 -0
- package/dist/format/accessibility.js.map +1 -0
- package/dist/format/canonical.d.ts +38 -0
- package/dist/format/canonical.js +65 -0
- package/dist/format/canonical.js.map +1 -0
- package/dist/format/capture.d.ts +212 -0
- package/dist/format/capture.js +2 -0
- package/dist/format/capture.js.map +1 -0
- package/dist/format/document.d.ts +279 -0
- package/dist/format/document.js +65 -0
- package/dist/format/document.js.map +1 -0
- package/dist/format/environment.d.ts +120 -0
- package/dist/format/environment.js +53 -0
- package/dist/format/environment.js.map +1 -0
- package/dist/format/hash.d.ts +45 -0
- package/dist/format/hash.js +41 -0
- package/dist/format/hash.js.map +1 -0
- package/dist/format/holding.d.ts +134 -0
- package/dist/format/holding.js +2 -0
- package/dist/format/holding.js.map +1 -0
- package/dist/format/index.d.ts +39 -0
- package/dist/format/index.js +25 -0
- package/dist/format/index.js.map +1 -0
- package/dist/format/intervention.d.ts +87 -0
- package/dist/format/intervention.js +11 -0
- package/dist/format/intervention.js.map +1 -0
- package/dist/format/profile.d.ts +59 -0
- package/dist/format/profile.js +66 -0
- package/dist/format/profile.js.map +1 -0
- package/dist/format/provenance.d.ts +179 -0
- package/dist/format/provenance.js +222 -0
- package/dist/format/provenance.js.map +1 -0
- package/dist/format/sha256.d.ts +34 -0
- package/dist/format/sha256.js +152 -0
- package/dist/format/sha256.js.map +1 -0
- package/dist/format/snapshot.d.ts +263 -0
- package/dist/format/snapshot.js +2 -0
- package/dist/format/snapshot.js.map +1 -0
- package/dist/format/stabilize.d.ts +190 -0
- package/dist/format/stabilize.js +333 -0
- package/dist/format/stabilize.js.map +1 -0
- package/dist/format/tier.d.ts +19 -0
- package/dist/format/tier.js +11 -0
- package/dist/format/tier.js.map +1 -0
- package/dist/format/value.d.ts +115 -0
- package/dist/format/value.js +182 -0
- package/dist/format/value.js.map +1 -0
- package/dist/format/wiring.d.ts +115 -0
- package/dist/format/wiring.js +60 -0
- package/dist/format/wiring.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/judge/docket.d.ts +85 -0
- package/dist/judge/docket.js +162 -0
- package/dist/judge/docket.js.map +1 -0
- package/dist/judge/fingerprint.d.ts +53 -0
- package/dist/judge/fingerprint.js +144 -0
- package/dist/judge/fingerprint.js.map +1 -0
- package/dist/judge/ignore.d.ts +206 -0
- package/dist/judge/ignore.js +237 -0
- package/dist/judge/ignore.js.map +1 -0
- package/dist/judge/index.d.ts +44 -0
- package/dist/judge/index.js +34 -0
- package/dist/judge/index.js.map +1 -0
- package/dist/judge/inspect.d.ts +148 -0
- package/dist/judge/inspect.js +208 -0
- package/dist/judge/inspect.js.map +1 -0
- package/dist/judge/intent.d.ts +146 -0
- package/dist/judge/intent.js +150 -0
- package/dist/judge/intent.js.map +1 -0
- package/dist/judge/locale.d.ts +111 -0
- package/dist/judge/locale.js +247 -0
- package/dist/judge/locale.js.map +1 -0
- package/dist/judge/scope.d.ts +41 -0
- package/dist/judge/scope.js +47 -0
- package/dist/judge/scope.js.map +1 -0
- package/dist/judge/sensitivity.d.ts +202 -0
- package/dist/judge/sensitivity.js +152 -0
- package/dist/judge/sensitivity.js.map +1 -0
- package/dist/judge/trail.d.ts +122 -0
- package/dist/judge/trail.js +96 -0
- package/dist/judge/trail.js.map +1 -0
- package/dist/judge/verdict.d.ts +39 -0
- package/dist/judge/verdict.js +32 -0
- package/dist/judge/verdict.js.map +1 -0
- package/dist/plan/index.d.ts +12 -0
- package/dist/plan/index.js +11 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/plan/tool.d.ts +85 -0
- package/dist/plan/tool.js +87 -0
- package/dist/plan/tool.js.map +1 -0
- package/dist/relate/graph.d.ts +190 -0
- package/dist/relate/graph.js +234 -0
- package/dist/relate/graph.js.map +1 -0
- package/dist/relate/index.d.ts +16 -0
- package/dist/relate/index.js +16 -0
- package/dist/relate/index.js.map +1 -0
- package/dist/relate/merkle.d.ts +110 -0
- package/dist/relate/merkle.js +246 -0
- package/dist/relate/merkle.js.map +1 -0
- package/dist/relate/reach.d.ts +65 -0
- package/dist/relate/reach.js +111 -0
- package/dist/relate/reach.js.map +1 -0
- package/dist/relate/records.d.ts +129 -0
- package/dist/relate/records.js +112 -0
- package/dist/relate/records.js.map +1 -0
- package/dist/rules/index.d.ts +19 -0
- package/dist/rules/index.js +17 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/normalize/alias.d.ts +50 -0
- package/dist/rules/normalize/alias.js +111 -0
- package/dist/rules/normalize/alias.js.map +1 -0
- package/dist/rules/normalize/cascade.d.ts +85 -0
- package/dist/rules/normalize/cascade.js +324 -0
- package/dist/rules/normalize/cascade.js.map +1 -0
- package/dist/rules/normalize/color.d.ts +30 -0
- package/dist/rules/normalize/color.js +185 -0
- package/dist/rules/normalize/color.js.map +1 -0
- package/dist/rules/normalize/fixture.d.ts +83 -0
- package/dist/rules/normalize/fixture.js +74 -0
- package/dist/rules/normalize/fixture.js.map +1 -0
- package/dist/rules/normalize/index.d.ts +39 -0
- package/dist/rules/normalize/index.js +268 -0
- package/dist/rules/normalize/index.js.map +1 -0
- package/dist/rules/normalize/project.d.ts +42 -0
- package/dist/rules/normalize/project.js +83 -0
- package/dist/rules/normalize/project.js.map +1 -0
- package/dist/rules/normalize/shorthand.d.ts +38 -0
- package/dist/rules/normalize/shorthand.js +388 -0
- package/dist/rules/normalize/shorthand.js.map +1 -0
- package/dist/rules/normalize/value.d.ts +23 -0
- package/dist/rules/normalize/value.js +162 -0
- package/dist/rules/normalize/value.js.map +1 -0
- package/dist/rules/normalize/wrapper.d.ts +16 -0
- package/dist/rules/normalize/wrapper.js +137 -0
- package/dist/rules/normalize/wrapper.js.map +1 -0
- package/dist/rules/ruleset.d.ts +93 -0
- package/dist/rules/ruleset.js +175 -0
- package/dist/rules/ruleset.js.map +1 -0
- package/mark.svg +30 -0
- package/package.json +69 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { Diagnostic, SubjectRef } from './capture.js';
|
|
2
|
+
import type { Viewport } from './environment.js';
|
|
3
|
+
import { type Digest } from './hash.js';
|
|
4
|
+
import type { ComponentHash } from './snapshot.js';
|
|
5
|
+
/**
|
|
6
|
+
* The render document: what gets *sent* somewhere to become an image.
|
|
7
|
+
*
|
|
8
|
+
* This is the acquisition phase's output and the only thing the rendering side
|
|
9
|
+
* ever sees. It is plain serializable data for the same reason `RawCapture` is
|
|
10
|
+
* (ADR-0006): the renderer may be a page in this process, a browser on this
|
|
11
|
+
* machine, or a pinned container two networks away, and no code upstream of it
|
|
12
|
+
* may assume which.
|
|
13
|
+
*
|
|
14
|
+
* Two routes produce one of these, and the point of the type is that they are
|
|
15
|
+
* interchangeable:
|
|
16
|
+
*
|
|
17
|
+
* 1. **jsdom → document → render elsewhere.** jsdom cannot rasterize. It can
|
|
18
|
+
* describe exactly what to rasterize, which is the sub-renderer split ADR-0002
|
|
19
|
+
* asks for: the cheap tier decides almost everything, and the residue is
|
|
20
|
+
* handed to something that owns a GPU.
|
|
21
|
+
* 2. **playwright → document → render on a server.** The same payload, acquired
|
|
22
|
+
* from a real engine. Used when the *deciding* machine and the *pinned*
|
|
23
|
+
* machine are different, which is the case every CI setup actually has.
|
|
24
|
+
*
|
|
25
|
+
* A browser may instead capture a `Raster` in place. That is the sibling branch
|
|
26
|
+
* of `CaptureMaterial`, not a third route that somehow produces this type.
|
|
27
|
+
*
|
|
28
|
+
* ## Why this is small
|
|
29
|
+
*
|
|
30
|
+
* `css` holds the *applicable* rules only — the pruning of ADR-0003, which on
|
|
31
|
+
* `@variance-authority/dom`'s accretion fixture takes 1010 rules to 1. That
|
|
32
|
+
* number is a cost claim as much as a
|
|
33
|
+
* correctness one: shipping a document over a network hop is only sensible if the
|
|
34
|
+
* document is not the entire design system plus Storybook's chrome.
|
|
35
|
+
*/
|
|
36
|
+
export interface RenderDocument {
|
|
37
|
+
readonly documentVersion: 1;
|
|
38
|
+
readonly subject: SubjectRef;
|
|
39
|
+
/**
|
|
40
|
+
* The subject subtree, serialized.
|
|
41
|
+
*
|
|
42
|
+
* Serialized rather than referenced by URL: a URL makes the renderer
|
|
43
|
+
* responsible for reproducing the application's state, which it cannot do and
|
|
44
|
+
* should not try. What is sent is the markup that existed at the moment of
|
|
45
|
+
* capture, which is the only definition of "this render" that survives a hop.
|
|
46
|
+
*/
|
|
47
|
+
readonly html: string;
|
|
48
|
+
/**
|
|
49
|
+
* The ancestor context the subject was rendered inside.
|
|
50
|
+
*
|
|
51
|
+
* Not decoration. Pruning keeps rules like `html.dark .card` and
|
|
52
|
+
* `.app .list > li`, whose left-hand side lives *above* the subject — render
|
|
53
|
+
* the subtree bare and those rules match nothing, so the image is missing
|
|
54
|
+
* exactly the styling the collector went to the trouble of proving applies.
|
|
55
|
+
*
|
|
56
|
+
* Reproduced as empty open tags rather than by re-rendering the application:
|
|
57
|
+
* what a selector needs is a chain of elements with the right tags, ids,
|
|
58
|
+
* classes, and attributes, and nothing above the subject contributes anything
|
|
59
|
+
* else to it that {@link RenderDocument.inherited} does not already carry.
|
|
60
|
+
*/
|
|
61
|
+
readonly frame: RenderFrame;
|
|
62
|
+
/**
|
|
63
|
+
* Applicable stylesheet text, in cascade order.
|
|
64
|
+
*
|
|
65
|
+
* Order is load-bearing and is the caller's to preserve — the cascade breaks
|
|
66
|
+
* specificity ties by document order, so a set that arrives shuffled paints
|
|
67
|
+
* differently while hashing the same if this were a set rather than a list.
|
|
68
|
+
*/
|
|
69
|
+
readonly css: readonly string[];
|
|
70
|
+
readonly viewport: Viewport;
|
|
71
|
+
/**
|
|
72
|
+
* Inherited values in force at the subject root, applied to its wrapper.
|
|
73
|
+
*
|
|
74
|
+
* The same requirement as `RawCapture.inheritedSeed` and for the same reason:
|
|
75
|
+
* pruning drops rules on ancestors outside the subtree, and a font-size
|
|
76
|
+
* inherited from `<html>` is not optional decoration — dropping it changes
|
|
77
|
+
* every metric in the image.
|
|
78
|
+
*/
|
|
79
|
+
readonly inherited: Readonly<Record<string, string>>;
|
|
80
|
+
/**
|
|
81
|
+
* Fonts the acquiring side observed as loaded.
|
|
82
|
+
*
|
|
83
|
+
* Carried, not resolved. A renderer that lacks one of these will substitute
|
|
84
|
+
* and produce different metrics, and that has to be *detectable* rather than
|
|
85
|
+
* silently absorbed — see {@link RenderIdentity}.
|
|
86
|
+
*/
|
|
87
|
+
readonly fonts: readonly string[];
|
|
88
|
+
/** External references, url → content hash. Present so a swap is visible. */
|
|
89
|
+
readonly assets?: Readonly<Record<string, string>>;
|
|
90
|
+
/**
|
|
91
|
+
* Base against which relative resource references were resolved.
|
|
92
|
+
*
|
|
93
|
+
* Preserves relative-reference resolution for either document kind. Resource
|
|
94
|
+
* closure is denoted by `resources`, not by the presence of this field.
|
|
95
|
+
*/
|
|
96
|
+
readonly baseUrl?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Immutable responses needed to paint this document, keyed by absolute URL.
|
|
99
|
+
*
|
|
100
|
+
* Presence — including an empty object — means the document is
|
|
101
|
+
* resource-closed: a renderer must not consult the network for a missing URL.
|
|
102
|
+
* Absence preserves the older network-capable document contract and must not
|
|
103
|
+
* be described as portable to another machine.
|
|
104
|
+
*/
|
|
105
|
+
readonly resources?: Readonly<Record<string, RenderResource>>;
|
|
106
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
107
|
+
}
|
|
108
|
+
export interface RenderResource {
|
|
109
|
+
readonly contentType: string;
|
|
110
|
+
/** Response bytes, base64. */
|
|
111
|
+
readonly bytes: string;
|
|
112
|
+
readonly digest: Digest;
|
|
113
|
+
}
|
|
114
|
+
export interface RenderFrame {
|
|
115
|
+
/** Attributes on `<html>`. `class="dark"` here decides half a design system. */
|
|
116
|
+
readonly html: Readonly<Record<string, string>>;
|
|
117
|
+
readonly body: Readonly<Record<string, string>>;
|
|
118
|
+
/** Elements between `<body>` and the subject root, outermost first. */
|
|
119
|
+
readonly ancestors: readonly FrameElement[];
|
|
120
|
+
/**
|
|
121
|
+
* Used width of the subject's parent box, in CSS pixels, when layout was
|
|
122
|
+
* observable.
|
|
123
|
+
*
|
|
124
|
+
* A subject declaring `width: 100%` is a different size in a 1024px page and
|
|
125
|
+
* inside a 600px panel, and nothing else in this document says which it was.
|
|
126
|
+
* Absent under a profile with no layout engine, in which case the render falls
|
|
127
|
+
* back to the viewport and the difference, if any, is real rather than hidden.
|
|
128
|
+
*/
|
|
129
|
+
readonly containerWidth?: number;
|
|
130
|
+
}
|
|
131
|
+
export interface FrameElement {
|
|
132
|
+
readonly tag: string;
|
|
133
|
+
readonly attributes: Readonly<Record<string, string>>;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Content address of a document: *what is to be painted*.
|
|
137
|
+
*
|
|
138
|
+
* This is the deferral lever. Rendering is the expensive phase, and a document
|
|
139
|
+
* whose digest has been rendered before under the same identity has an image
|
|
140
|
+
* already — so the cheapest render is the one that is skipped. Nothing about the
|
|
141
|
+
* branch, the commit, or the file path enters this (Principle 4), so moving a
|
|
142
|
+
* story between files does not cost a re-render.
|
|
143
|
+
*
|
|
144
|
+
* Deliberately covers `viewport` in full, `deviceScaleFactor` included: unlike
|
|
145
|
+
* the semantic key (ADR-0010), a raster genuinely differs at 2x and the whole
|
|
146
|
+
* purpose of this digest is to address the raster.
|
|
147
|
+
*
|
|
148
|
+
* **Limit, stated rather than discovered.** Declaration values arrive serialized
|
|
149
|
+
* by the acquiring engine's CSSOM — jsdom writes `rgb(18, 52, 86)` where the
|
|
150
|
+
* author wrote `#123456` — and this phase deliberately does not canonicalize
|
|
151
|
+
* them, because a collector that normalizes is a second ruleset versioned by
|
|
152
|
+
* nothing (ADR-0001). So two engines can address the same page differently. That
|
|
153
|
+
* makes a render cache *per acquiring engine*, which is a missed cache hit and
|
|
154
|
+
* never a wrong image.
|
|
155
|
+
*/
|
|
156
|
+
export declare function documentDigest(document: RenderDocument): Digest;
|
|
157
|
+
/**
|
|
158
|
+
* Who painted it.
|
|
159
|
+
*
|
|
160
|
+
* The durable half of this system rests on one requirement — *images compared
|
|
161
|
+
* against each other must come from the same machine* — and a requirement that
|
|
162
|
+
* is merely stated gets violated by a CI runner upgrade nobody announced. This
|
|
163
|
+
* makes it a value, so it can be compared rather than assumed.
|
|
164
|
+
*
|
|
165
|
+
* Every field is something that has been observed to move pixels without moving
|
|
166
|
+
* markup. None of them is a guess about what *might* matter.
|
|
167
|
+
*/
|
|
168
|
+
export interface RenderIdentity {
|
|
169
|
+
/** `playwright-chromium@1.49.0`, `remote:render.internal`, … */
|
|
170
|
+
readonly renderer: string;
|
|
171
|
+
/** Engine build. A Chromium bump repaints text; that is not a regression. */
|
|
172
|
+
readonly engine: string;
|
|
173
|
+
/** OS and architecture. Font rasterization differs across both. */
|
|
174
|
+
readonly platform: string;
|
|
175
|
+
readonly deviceScaleFactor: number;
|
|
176
|
+
/**
|
|
177
|
+
* Fonts the *renderer* actually has.
|
|
178
|
+
*
|
|
179
|
+
* Compared against {@link RenderDocument.fonts} at render time, because a
|
|
180
|
+
* substituted font is the single most common way two machines disagree, and
|
|
181
|
+
* it is invisible in every artifact except the image itself.
|
|
182
|
+
*/
|
|
183
|
+
readonly fonts: readonly string[];
|
|
184
|
+
/**
|
|
185
|
+
* Digest of what the renderer did *to* the page so it could be observed.
|
|
186
|
+
*
|
|
187
|
+
* Pausing animations, hiding a caret, waiting for fonts — each is necessary
|
|
188
|
+
* and each widens the gap between what was measured and what a person sees.
|
|
189
|
+
* The gap itself is acceptable; a *difference* in it between two runs is not,
|
|
190
|
+
* because the images then differ for a reason that is not the code and the
|
|
191
|
+
* report blames whichever component sits under the pixels. Folding it in here
|
|
192
|
+
* makes that comparison `incomparable` instead, by the same mechanism that
|
|
193
|
+
* refuses a cross-machine one.
|
|
194
|
+
*
|
|
195
|
+
* Optional only because a renderer may predate the field. Absent means the
|
|
196
|
+
* renderer did not record what it did, which is emphatically not the same as
|
|
197
|
+
* "it did nothing" — and two such renderers will compare, which is the hazard
|
|
198
|
+
* that argues for every renderer setting it.
|
|
199
|
+
*/
|
|
200
|
+
readonly stabilization?: Digest;
|
|
201
|
+
/**
|
|
202
|
+
* Digest of pixel-affecting browser launch and raster settings.
|
|
203
|
+
*
|
|
204
|
+
* Separate from stabilization: one describes how the browser paints, the
|
|
205
|
+
* other what was done to the page before it was read.
|
|
206
|
+
*/
|
|
207
|
+
readonly rasterization?: Digest;
|
|
208
|
+
}
|
|
209
|
+
export declare function identityDigest(identity: RenderIdentity): Digest;
|
|
210
|
+
/**
|
|
211
|
+
* A rendered image and the conditions it was produced under.
|
|
212
|
+
*
|
|
213
|
+
* `bytes` is base64 rather than a `Buffer` so the whole value survives the same
|
|
214
|
+
* hops the document does. A raster that could only exist in Node would make the
|
|
215
|
+
* remote route a special case in every function that touches one.
|
|
216
|
+
*/
|
|
217
|
+
export interface Raster {
|
|
218
|
+
readonly documentDigest: Digest;
|
|
219
|
+
readonly identity: RenderIdentity;
|
|
220
|
+
/** Device pixels. `width / viewport.width` is the scale, and is asserted. */
|
|
221
|
+
readonly width: number;
|
|
222
|
+
readonly height: number;
|
|
223
|
+
/** PNG, base64. */
|
|
224
|
+
readonly bytes: string;
|
|
225
|
+
/** Fonts the document declared that the renderer did not have. */
|
|
226
|
+
readonly missingFonts: readonly string[];
|
|
227
|
+
/**
|
|
228
|
+
* The browser-computed accessibility tree from the acquisition that produced
|
|
229
|
+
* this candidate. It is independent evidence: pixel equality cannot settle it.
|
|
230
|
+
* Absent means the acquisition host did not observe this boundary.
|
|
231
|
+
*/
|
|
232
|
+
readonly accessibility?: import('./accessibility.js').AccessibilitySnapshot;
|
|
233
|
+
/**
|
|
234
|
+
* What the document that painted this said about its own components (ADR-0018).
|
|
235
|
+
*
|
|
236
|
+
* The field that makes a baseline self-describing, and the reason it is here
|
|
237
|
+
* rather than only in a history store. Separating the component that *caused* a
|
|
238
|
+
* change from the components the change merely moved needs both revisions, and
|
|
239
|
+
* a stored baseline is an image: without this, a run has one document and ranks
|
|
240
|
+
* by area — which
|
|
241
|
+
* [journal 0013](../../../../docs/context/journal/0013-observability.md)
|
|
242
|
+
* measured as backwards.
|
|
243
|
+
*
|
|
244
|
+
* Text, and small: one line per component boundary. It rides in the sidecar
|
|
245
|
+
* beside a PNG that dominates it, so a baseline carries its own explanation
|
|
246
|
+
* wherever it is copied, and a store or a transport that drops it degrades to
|
|
247
|
+
* ranking by area rather than to a wrong answer.
|
|
248
|
+
*
|
|
249
|
+
* Optional because a baseline written before this existed does not have it, and
|
|
250
|
+
* because a profile or a collector that supplies no snapshot cannot produce it.
|
|
251
|
+
* Absent means *unknown*, never *no components*.
|
|
252
|
+
*/
|
|
253
|
+
readonly components?: readonly ComponentHash[];
|
|
254
|
+
/**
|
|
255
|
+
* What inspection found in the document that painted this, as marks.
|
|
256
|
+
*
|
|
257
|
+
* The field that lets a later run say *this defect was already here* instead of
|
|
258
|
+
* printing every defect it finds with no indication of when it arrived. A
|
|
259
|
+
* finding is produced from one render with no baseline consulted — that is the
|
|
260
|
+
* point of having one — and the consequence is that the list is identical on
|
|
261
|
+
* the run that introduced a defect and on the two hundred runs after it. A
|
|
262
|
+
* reviewer reading `a control inside another control` has no way to tell an
|
|
263
|
+
* inherited condition from something they just wrote, and the two are different
|
|
264
|
+
* decisions.
|
|
265
|
+
*
|
|
266
|
+
* Marks rather than findings, from {@link findingMark}. The sentence, the
|
|
267
|
+
* landmark phrase and the source line are all rewritten by edits that do not
|
|
268
|
+
* touch the defect, so storing them would report a defect as new every time the
|
|
269
|
+
* copy beside it changed — and they are the bulk of the bytes, in a sidecar
|
|
270
|
+
* that is committed beside every baseline.
|
|
271
|
+
*
|
|
272
|
+
* **Absent means nothing inspected it, never that it was clean.** A baseline
|
|
273
|
+
* written before this existed, or by a collector that supplies no snapshot, has
|
|
274
|
+
* no list, and a run that read absence as `[]` would announce every standing
|
|
275
|
+
* defect in the suite as newly introduced on the first run after an upgrade.
|
|
276
|
+
*/
|
|
277
|
+
readonly findingMarks?: readonly string[];
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=document.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { digestValue } from './hash.js';
|
|
2
|
+
/**
|
|
3
|
+
* Content address of a document: *what is to be painted*.
|
|
4
|
+
*
|
|
5
|
+
* This is the deferral lever. Rendering is the expensive phase, and a document
|
|
6
|
+
* whose digest has been rendered before under the same identity has an image
|
|
7
|
+
* already — so the cheapest render is the one that is skipped. Nothing about the
|
|
8
|
+
* branch, the commit, or the file path enters this (Principle 4), so moving a
|
|
9
|
+
* story between files does not cost a re-render.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately covers `viewport` in full, `deviceScaleFactor` included: unlike
|
|
12
|
+
* the semantic key (ADR-0010), a raster genuinely differs at 2x and the whole
|
|
13
|
+
* purpose of this digest is to address the raster.
|
|
14
|
+
*
|
|
15
|
+
* **Limit, stated rather than discovered.** Declaration values arrive serialized
|
|
16
|
+
* by the acquiring engine's CSSOM — jsdom writes `rgb(18, 52, 86)` where the
|
|
17
|
+
* author wrote `#123456` — and this phase deliberately does not canonicalize
|
|
18
|
+
* them, because a collector that normalizes is a second ruleset versioned by
|
|
19
|
+
* nothing (ADR-0001). So two engines can address the same page differently. That
|
|
20
|
+
* makes a render cache *per acquiring engine*, which is a missed cache hit and
|
|
21
|
+
* never a wrong image.
|
|
22
|
+
*/
|
|
23
|
+
export function documentDigest(document) {
|
|
24
|
+
return digestValue({
|
|
25
|
+
documentVersion: document.documentVersion,
|
|
26
|
+
html: document.html,
|
|
27
|
+
frame: {
|
|
28
|
+
html: { ...document.frame.html },
|
|
29
|
+
body: { ...document.frame.body },
|
|
30
|
+
ancestors: document.frame.ancestors.map((element) => ({
|
|
31
|
+
tag: element.tag,
|
|
32
|
+
attributes: { ...element.attributes },
|
|
33
|
+
})),
|
|
34
|
+
...(document.frame.containerWidth !== undefined
|
|
35
|
+
? { containerWidth: document.frame.containerWidth }
|
|
36
|
+
: {}),
|
|
37
|
+
},
|
|
38
|
+
css: [...document.css],
|
|
39
|
+
viewport: { ...document.viewport },
|
|
40
|
+
inherited: { ...document.inherited },
|
|
41
|
+
fonts: [...document.fonts],
|
|
42
|
+
...(document.assets ? { assets: { ...document.assets } } : {}),
|
|
43
|
+
...(document.baseUrl !== undefined ? { baseUrl: document.baseUrl } : {}),
|
|
44
|
+
...(document.resources !== undefined
|
|
45
|
+
? {
|
|
46
|
+
resources: Object.fromEntries(Object.entries(document.resources).map(([url, resource]) => [
|
|
47
|
+
url,
|
|
48
|
+
{ ...resource },
|
|
49
|
+
])),
|
|
50
|
+
}
|
|
51
|
+
: {}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export function identityDigest(identity) {
|
|
55
|
+
return digestValue({
|
|
56
|
+
renderer: identity.renderer,
|
|
57
|
+
engine: identity.engine,
|
|
58
|
+
platform: identity.platform,
|
|
59
|
+
...(identity.stabilization !== undefined ? { stabilization: identity.stabilization } : {}),
|
|
60
|
+
...(identity.rasterization !== undefined ? { rasterization: identity.rasterization } : {}),
|
|
61
|
+
deviceScaleFactor: identity.deviceScaleFactor,
|
|
62
|
+
fonts: [...identity.fonts],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=document.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.js","sourceRoot":"","sources":["../../src/format/document.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAe,MAAM,WAAW,CAAC;AAoJrD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,cAAc,CAAC,QAAwB;IACrD,OAAO,WAAW,CAAC;QACjB,eAAe,EAAE,QAAQ,CAAC,eAAe;QACzC,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,KAAK,EAAE;YACL,IAAI,EAAE,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE;YAChC,IAAI,EAAE,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE;YAChC,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gBACpD,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,UAAU,EAAE,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE;aACtC,CAAC,CAAC;YACH,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,KAAK,SAAS;gBAC7C,CAAC,CAAC,EAAE,cAAc,EAAE,QAAQ,CAAC,KAAK,CAAC,cAAc,EAAE;gBACnD,CAAC,CAAC,EAAE,CAAC;SACR;QACD,GAAG,EAAE,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC;QACtB,QAAQ,EAAE,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE;QAClC,SAAS,EAAE,EAAE,GAAG,QAAQ,CAAC,SAAS,EAAE;QACpC,KAAK,EAAE,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC;QAC1B,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,GAAG,CAAC,QAAQ,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxE,GAAG,CAAC,QAAQ,CAAC,SAAS,KAAK,SAAS;YAClC,CAAC,CAAC;gBACE,SAAS,EAAE,MAAM,CAAC,WAAW,CAC3B,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC;oBAC1D,GAAG;oBACH,EAAE,GAAG,QAAQ,EAAE;iBAChB,CAAC,CACH;aACF;YACH,CAAC,CAAC,EAAE,CAAC;KACR,CAAC,CAAC;AACL,CAAC;AAyDD,MAAM,UAAU,cAAc,CAAC,QAAwB;IACrD,OAAO,WAAW,CAAC;QACjB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,GAAG,CAAC,QAAQ,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1F,GAAG,CAAC,QAAQ,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1F,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB;QAC7C,KAAK,EAAE,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC;KAC3B,CAAC,CAAC;AACL,CAAC","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\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 */\nexport interface Raster {\n readonly documentDigest: Digest;\n readonly identity: RenderIdentity;\n /** Device pixels. `width / viewport.width` is the scale, and is asserted. */\n readonly width: number;\n readonly height: number;\n /** PNG, base64. */\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"]}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type Digest } from './hash.js';
|
|
2
|
+
import type { ProfileId } from './profile.js';
|
|
3
|
+
/**
|
|
4
|
+
* The environment key: a hash of every render input that is not code.
|
|
5
|
+
*
|
|
6
|
+
* The system's central guarantee is *same hash ⇒ same render*. That guarantee is
|
|
7
|
+
* exactly as strong as this key's coverage, and no stronger — an uncovered input
|
|
8
|
+
* (a font that silently changed, an asset served from a URL whose bytes moved)
|
|
9
|
+
* produces two different renders under one key, which is a false `unchanged`.
|
|
10
|
+
*
|
|
11
|
+
* Adding a field here is a mass-invalidation event (spec §7.3) and must be
|
|
12
|
+
* treated as one. Leaving a field out is a correctness bug. When in doubt,
|
|
13
|
+
* include it: over-invalidation costs CI minutes, under-invalidation costs trust.
|
|
14
|
+
*/
|
|
15
|
+
export interface EnvironmentInputs {
|
|
16
|
+
/** Which collector, and therefore which dimensions exist at all (ADR-0002). */
|
|
17
|
+
readonly profile: ProfileId;
|
|
18
|
+
/** Engine identity. `jsdom@26.1.0`, `chromium@131.0.6778.33`. */
|
|
19
|
+
readonly engine: string;
|
|
20
|
+
/**
|
|
21
|
+
* Normalization ruleset version. Changing the rules changes every hash by
|
|
22
|
+
* design, so the ruleset is a render input like any other.
|
|
23
|
+
*/
|
|
24
|
+
readonly ruleset: string;
|
|
25
|
+
/** Computed-style allowlist version. Widening it is also mass invalidation. */
|
|
26
|
+
readonly allowlist: string;
|
|
27
|
+
readonly viewport: Viewport;
|
|
28
|
+
/**
|
|
29
|
+
* Fonts available at render time, as `family/weight/style/contentHash`.
|
|
30
|
+
* A font substitution changes metrics, and therefore geometry, without
|
|
31
|
+
* changing a single byte of code.
|
|
32
|
+
*/
|
|
33
|
+
readonly fonts: readonly string[];
|
|
34
|
+
/**
|
|
35
|
+
* Resolved media/feature conditions. ADR-0003 flattens `@media`, `@supports`,
|
|
36
|
+
* and `@container` away from the CSS text, so the conditions they were
|
|
37
|
+
* flattened against must live here or they vanish from the key entirely.
|
|
38
|
+
*/
|
|
39
|
+
readonly conditions: Readonly<Record<string, string | boolean | number>>;
|
|
40
|
+
/**
|
|
41
|
+
* External assets keyed by request URL, valued by content hash. URLs are not
|
|
42
|
+
* identities: the same `url(...)` can resolve to different bytes tomorrow.
|
|
43
|
+
*/
|
|
44
|
+
readonly assets: Readonly<Record<string, string>>;
|
|
45
|
+
/**
|
|
46
|
+
* Identity of the stabilization recipe the page was held still with, from
|
|
47
|
+
* `recipeDigest` — absent when the subject was observed untouched.
|
|
48
|
+
*
|
|
49
|
+
* A render input, and one of the more consequential ones: pinning animations
|
|
50
|
+
* changes `transform` and `opacity` on every animated node, so a baseline
|
|
51
|
+
* collected without it and a run collected with it disagree everywhere and
|
|
52
|
+
* agree about nothing. Without this field that disagreement arrives as a
|
|
53
|
+
* change with a component and a file attached — a confident, specific, wrong
|
|
54
|
+
* answer. With it, the two are different baselines and never meet.
|
|
55
|
+
*
|
|
56
|
+
* Optional rather than required because *not stabilizing* is a real state a
|
|
57
|
+
* caller may be in — a jsdom capture has nothing to hold still — and
|
|
58
|
+
* `undefined` is omitted from the canonical form, so the untouched case hashes
|
|
59
|
+
* as the absence it is rather than as an empty recipe.
|
|
60
|
+
*/
|
|
61
|
+
readonly stabilization?: Digest;
|
|
62
|
+
}
|
|
63
|
+
export interface Viewport {
|
|
64
|
+
readonly width: number;
|
|
65
|
+
readonly height: number;
|
|
66
|
+
readonly deviceScaleFactor: number;
|
|
67
|
+
/** `prefers-color-scheme`, and the reason a theme switch is not a code change. */
|
|
68
|
+
readonly colorScheme: 'light' | 'dark';
|
|
69
|
+
}
|
|
70
|
+
export interface EnvironmentKey {
|
|
71
|
+
/**
|
|
72
|
+
* Every render input, including the ones only pixels can see.
|
|
73
|
+
*
|
|
74
|
+
* The key a raster baseline must be stored under. Two machines agree on it only
|
|
75
|
+
* if they agree on device pixel ratio as well as everything else — which is why
|
|
76
|
+
* pixel-based tools end up inside a container.
|
|
77
|
+
*/
|
|
78
|
+
readonly digest: Digest;
|
|
79
|
+
/**
|
|
80
|
+
* The inputs that can reach the *semantic* representation.
|
|
81
|
+
*
|
|
82
|
+
* This is the key that matters economically. A raster baseline is machine-bound
|
|
83
|
+
* because rasterization is machine-bound, and the usual answer — put the whole
|
|
84
|
+
* pipeline in Docker — pays that cost on every subject of every build in order
|
|
85
|
+
* to stabilise the tier that decides almost nothing.
|
|
86
|
+
*
|
|
87
|
+
* The semantic representation is built from the box tree, and device pixel
|
|
88
|
+
* ratio cannot reach a box tree: layout is in CSS pixels, and a 2× render lays
|
|
89
|
+
* out identically. Measured, not assumed — a DPR change moves 3015 pixels and
|
|
90
|
+
* leaves structure and style byte-identical (journal 0012).
|
|
91
|
+
*
|
|
92
|
+
* So the semantic key omits it, and one semantic baseline is valid on a retina
|
|
93
|
+
* laptop, a non-retina CI runner, and a container alike. The container is then
|
|
94
|
+
* needed only for the raster residue, which the tiering already makes rare.
|
|
95
|
+
*
|
|
96
|
+
* What is *not* omitted: fonts, engine, viewport size, ruleset, allowlist, and
|
|
97
|
+
* the resolved conditions. Each of those genuinely changes the box tree, and
|
|
98
|
+
* dropping one to make baselines more portable would buy portability with
|
|
99
|
+
* false `unchanged` verdicts.
|
|
100
|
+
*/
|
|
101
|
+
readonly semanticDigest: Digest;
|
|
102
|
+
readonly inputs: EnvironmentInputs;
|
|
103
|
+
}
|
|
104
|
+
export declare function environmentKey(inputs: EnvironmentInputs): EnvironmentKey;
|
|
105
|
+
/**
|
|
106
|
+
* Explain why two environment keys differ, field by field.
|
|
107
|
+
*
|
|
108
|
+
* Mass invalidation is only survivable if the docket can collapse it to a single
|
|
109
|
+
* named root — "chromium 131→132", "ruleset v3" — and offer one-action
|
|
110
|
+
* re-baselining (spec §7.3). That collapse needs the specific field, so a diff of
|
|
111
|
+
* environments is a product feature, not a debugging aid.
|
|
112
|
+
*/
|
|
113
|
+
export declare function diffEnvironments(before: EnvironmentInputs, after: EnvironmentInputs): readonly EnvironmentDelta[];
|
|
114
|
+
export type EnvironmentField = 'profile' | 'engine' | 'ruleset' | 'allowlist' | 'viewport' | 'fonts' | 'conditions' | 'assets' | 'stabilization';
|
|
115
|
+
export interface EnvironmentDelta {
|
|
116
|
+
readonly field: EnvironmentField;
|
|
117
|
+
readonly from: string;
|
|
118
|
+
readonly to: string;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=environment.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { digestValue } from './hash.js';
|
|
2
|
+
export function environmentKey(inputs) {
|
|
3
|
+
const shared = {
|
|
4
|
+
profile: inputs.profile,
|
|
5
|
+
engine: inputs.engine,
|
|
6
|
+
ruleset: inputs.ruleset,
|
|
7
|
+
allowlist: inputs.allowlist,
|
|
8
|
+
fonts: [...inputs.fonts].sort(),
|
|
9
|
+
conditions: { ...inputs.conditions },
|
|
10
|
+
assets: { ...inputs.assets },
|
|
11
|
+
// In `shared`, so it reaches the semantic key too. That is the whole point:
|
|
12
|
+
// the recipe's first job is to stop an animation from moving `transform`,
|
|
13
|
+
// which is a value the semantic representation carries.
|
|
14
|
+
stabilization: inputs.stabilization,
|
|
15
|
+
};
|
|
16
|
+
// Layout depends on the viewport's size and colour scheme; rasterization also
|
|
17
|
+
// depends on its scale factor. Splitting the viewport is what makes the two
|
|
18
|
+
// keys differ, and it is the only difference between them.
|
|
19
|
+
const { deviceScaleFactor: _scale, ...layoutViewport } = inputs.viewport;
|
|
20
|
+
return {
|
|
21
|
+
digest: digestValue({ ...shared, viewport: { ...inputs.viewport } }),
|
|
22
|
+
semanticDigest: digestValue({ ...shared, viewport: layoutViewport }),
|
|
23
|
+
inputs,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Explain why two environment keys differ, field by field.
|
|
28
|
+
*
|
|
29
|
+
* Mass invalidation is only survivable if the docket can collapse it to a single
|
|
30
|
+
* named root — "chromium 131→132", "ruleset v3" — and offer one-action
|
|
31
|
+
* re-baselining (spec §7.3). That collapse needs the specific field, so a diff of
|
|
32
|
+
* environments is a product feature, not a debugging aid.
|
|
33
|
+
*/
|
|
34
|
+
export function diffEnvironments(before, after) {
|
|
35
|
+
const deltas = [];
|
|
36
|
+
const scalar = (field, a, b) => {
|
|
37
|
+
const from = JSON.stringify(a);
|
|
38
|
+
const to = JSON.stringify(b);
|
|
39
|
+
if (from !== to)
|
|
40
|
+
deltas.push({ field, from, to });
|
|
41
|
+
};
|
|
42
|
+
scalar('profile', before.profile, after.profile);
|
|
43
|
+
scalar('engine', before.engine, after.engine);
|
|
44
|
+
scalar('ruleset', before.ruleset, after.ruleset);
|
|
45
|
+
scalar('allowlist', before.allowlist, after.allowlist);
|
|
46
|
+
scalar('viewport', before.viewport, after.viewport);
|
|
47
|
+
scalar('fonts', [...before.fonts].sort(), [...after.fonts].sort());
|
|
48
|
+
scalar('conditions', before.conditions, after.conditions);
|
|
49
|
+
scalar('assets', before.assets, after.assets);
|
|
50
|
+
scalar('stabilization', before.stabilization, after.stabilization);
|
|
51
|
+
return deltas;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=environment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment.js","sourceRoot":"","sources":["../../src/format/environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAe,MAAM,WAAW,CAAC;AAqHrD,MAAM,UAAU,cAAc,CAAC,MAAyB;IACtD,MAAM,MAAM,GAAG;QACb,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE;QAC/B,UAAU,EAAE,EAAE,GAAG,MAAM,CAAC,UAAU,EAAE;QACpC,MAAM,EAAE,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE;QAC5B,4EAA4E;QAC5E,0EAA0E;QAC1E,wDAAwD;QACxD,aAAa,EAAE,MAAM,CAAC,aAAa;KACpC,CAAC;IAEF,8EAA8E;IAC9E,4EAA4E;IAC5E,2DAA2D;IAC3D,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC;IAEzE,OAAO;QACL,MAAM,EAAE,WAAW,CAAC,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QACpE,cAAc,EAAE,WAAW,CAAC,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;QACpE,MAAM;KACP,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAyB,EACzB,KAAwB;IAExB,MAAM,MAAM,GAAuB,EAAE,CAAC;IAEtC,MAAM,MAAM,GAAG,CAAC,KAAuB,EAAE,CAAU,EAAE,CAAU,EAAQ,EAAE;QACvE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,IAAI,KAAK,EAAE;YAAE,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IACpD,CAAC,CAAC;IAEF,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IACvD,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;IACpD,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACnE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IAC1D,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,aAAa,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;IAEnE,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import { digestValue, type Digest } from './hash.js';\nimport type { ProfileId } from './profile.js';\n\n/**\n * The environment key: a hash of every render input that is not code.\n *\n * The system's central guarantee is *same hash ⇒ same render*. That guarantee is\n * exactly as strong as this key's coverage, and no stronger — an uncovered input\n * (a font that silently changed, an asset served from a URL whose bytes moved)\n * produces two different renders under one key, which is a false `unchanged`.\n *\n * Adding a field here is a mass-invalidation event (spec §7.3) and must be\n * treated as one. Leaving a field out is a correctness bug. When in doubt,\n * include it: over-invalidation costs CI minutes, under-invalidation costs trust.\n */\nexport interface EnvironmentInputs {\n /** Which collector, and therefore which dimensions exist at all (ADR-0002). */\n readonly profile: ProfileId;\n\n /** Engine identity. `jsdom@26.1.0`, `chromium@131.0.6778.33`. */\n readonly engine: string;\n\n /**\n * Normalization ruleset version. Changing the rules changes every hash by\n * design, so the ruleset is a render input like any other.\n */\n readonly ruleset: string;\n\n /** Computed-style allowlist version. Widening it is also mass invalidation. */\n readonly allowlist: string;\n\n readonly viewport: Viewport;\n\n /**\n * Fonts available at render time, as `family/weight/style/contentHash`.\n * A font substitution changes metrics, and therefore geometry, without\n * changing a single byte of code.\n */\n readonly fonts: readonly string[];\n\n /**\n * Resolved media/feature conditions. ADR-0003 flattens `@media`, `@supports`,\n * and `@container` away from the CSS text, so the conditions they were\n * flattened against must live here or they vanish from the key entirely.\n */\n readonly conditions: Readonly<Record<string, string | boolean | number>>;\n\n /**\n * External assets keyed by request URL, valued by content hash. URLs are not\n * identities: the same `url(...)` can resolve to different bytes tomorrow.\n */\n readonly assets: Readonly<Record<string, string>>;\n\n /**\n * Identity of the stabilization recipe the page was held still with, from\n * `recipeDigest` — absent when the subject was observed untouched.\n *\n * A render input, and one of the more consequential ones: pinning animations\n * changes `transform` and `opacity` on every animated node, so a baseline\n * collected without it and a run collected with it disagree everywhere and\n * agree about nothing. Without this field that disagreement arrives as a\n * change with a component and a file attached — a confident, specific, wrong\n * answer. With it, the two are different baselines and never meet.\n *\n * Optional rather than required because *not stabilizing* is a real state a\n * caller may be in — a jsdom capture has nothing to hold still — and\n * `undefined` is omitted from the canonical form, so the untouched case hashes\n * as the absence it is rather than as an empty recipe.\n */\n readonly stabilization?: Digest;\n}\n\nexport interface Viewport {\n readonly width: number;\n readonly height: number;\n readonly deviceScaleFactor: number;\n /** `prefers-color-scheme`, and the reason a theme switch is not a code change. */\n readonly colorScheme: 'light' | 'dark';\n}\n\nexport interface EnvironmentKey {\n /**\n * Every render input, including the ones only pixels can see.\n *\n * The key a raster baseline must be stored under. Two machines agree on it only\n * if they agree on device pixel ratio as well as everything else — which is why\n * pixel-based tools end up inside a container.\n */\n readonly digest: Digest;\n\n /**\n * The inputs that can reach the *semantic* representation.\n *\n * This is the key that matters economically. A raster baseline is machine-bound\n * because rasterization is machine-bound, and the usual answer — put the whole\n * pipeline in Docker — pays that cost on every subject of every build in order\n * to stabilise the tier that decides almost nothing.\n *\n * The semantic representation is built from the box tree, and device pixel\n * ratio cannot reach a box tree: layout is in CSS pixels, and a 2× render lays\n * out identically. Measured, not assumed — a DPR change moves 3015 pixels and\n * leaves structure and style byte-identical (journal 0012).\n *\n * So the semantic key omits it, and one semantic baseline is valid on a retina\n * laptop, a non-retina CI runner, and a container alike. The container is then\n * needed only for the raster residue, which the tiering already makes rare.\n *\n * What is *not* omitted: fonts, engine, viewport size, ruleset, allowlist, and\n * the resolved conditions. Each of those genuinely changes the box tree, and\n * dropping one to make baselines more portable would buy portability with\n * false `unchanged` verdicts.\n */\n readonly semanticDigest: Digest;\n\n readonly inputs: EnvironmentInputs;\n}\n\nexport function environmentKey(inputs: EnvironmentInputs): EnvironmentKey {\n const shared = {\n profile: inputs.profile,\n engine: inputs.engine,\n ruleset: inputs.ruleset,\n allowlist: inputs.allowlist,\n fonts: [...inputs.fonts].sort(),\n conditions: { ...inputs.conditions },\n assets: { ...inputs.assets },\n // In `shared`, so it reaches the semantic key too. That is the whole point:\n // the recipe's first job is to stop an animation from moving `transform`,\n // which is a value the semantic representation carries.\n stabilization: inputs.stabilization,\n };\n\n // Layout depends on the viewport's size and colour scheme; rasterization also\n // depends on its scale factor. Splitting the viewport is what makes the two\n // keys differ, and it is the only difference between them.\n const { deviceScaleFactor: _scale, ...layoutViewport } = inputs.viewport;\n\n return {\n digest: digestValue({ ...shared, viewport: { ...inputs.viewport } }),\n semanticDigest: digestValue({ ...shared, viewport: layoutViewport }),\n inputs,\n };\n}\n\n/**\n * Explain why two environment keys differ, field by field.\n *\n * Mass invalidation is only survivable if the docket can collapse it to a single\n * named root — \"chromium 131→132\", \"ruleset v3\" — and offer one-action\n * re-baselining (spec §7.3). That collapse needs the specific field, so a diff of\n * environments is a product feature, not a debugging aid.\n */\nexport function diffEnvironments(\n before: EnvironmentInputs,\n after: EnvironmentInputs,\n): readonly EnvironmentDelta[] {\n const deltas: EnvironmentDelta[] = [];\n\n const scalar = (field: EnvironmentField, a: unknown, b: unknown): void => {\n const from = JSON.stringify(a);\n const to = JSON.stringify(b);\n if (from !== to) deltas.push({ field, from, to });\n };\n\n scalar('profile', before.profile, after.profile);\n scalar('engine', before.engine, after.engine);\n scalar('ruleset', before.ruleset, after.ruleset);\n scalar('allowlist', before.allowlist, after.allowlist);\n scalar('viewport', before.viewport, after.viewport);\n scalar('fonts', [...before.fonts].sort(), [...after.fonts].sort());\n scalar('conditions', before.conditions, after.conditions);\n scalar('assets', before.assets, after.assets);\n scalar('stabilization', before.stabilization, after.stabilization);\n\n return deltas;\n}\n\nexport type EnvironmentField =\n | 'profile'\n | 'engine'\n | 'ruleset'\n | 'allowlist'\n | 'viewport'\n | 'fonts'\n | 'conditions'\n | 'assets'\n | 'stabilization';\n\nexport interface EnvironmentDelta {\n readonly field: EnvironmentField;\n readonly from: string;\n readonly to: string;\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type CanonicalValue } from './canonical.js';
|
|
2
|
+
/**
|
|
3
|
+
* Content addressing (Principle 4).
|
|
4
|
+
*
|
|
5
|
+
* Hashes are the system's only identity. A baseline is addressed by what it *is*,
|
|
6
|
+
* never by the branch or commit it arrived on, which is why a rebase cannot
|
|
7
|
+
* invalidate one.
|
|
8
|
+
*
|
|
9
|
+
* Digests are prefixed with their algorithm and truncated to 128 bits. The prefix
|
|
10
|
+
* makes a future algorithm change a visible, greppable migration rather than a
|
|
11
|
+
* silent collision domain; 128 bits is far beyond collision risk for a corpus
|
|
12
|
+
* bounded by the number of subjects a repository has, and keeps `variance.lock`
|
|
13
|
+
* readable as a diff — a manifest a human refuses to read is a manifest nobody
|
|
14
|
+
* reviews.
|
|
15
|
+
*/
|
|
16
|
+
export type Digest = string;
|
|
17
|
+
/**
|
|
18
|
+
* Hash an already-canonical string.
|
|
19
|
+
*
|
|
20
|
+
* Synchronous, and must stay so. `propsDigest` runs inside the rendering page,
|
|
21
|
+
* where a prop may be a function or an element that cannot be serialized out —
|
|
22
|
+
* so the digest is taken where the value still exists. `crypto.subtle` is async
|
|
23
|
+
* and would colour the whole normalizer; see `sha256.ts`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function digestString(input: string): Digest;
|
|
26
|
+
/**
|
|
27
|
+
* Hash raw bytes — an image, a font file, anything whose identity is its octets.
|
|
28
|
+
*
|
|
29
|
+
* Same prefix and same truncation as every other digest here, so an asset hash
|
|
30
|
+
* and a render hash are the same kind of value and neither can be mistaken for
|
|
31
|
+
* the other's domain. What it is for is `EnvironmentInputs.assets`: a URL is not
|
|
32
|
+
* an identity, and the same `url(...)` can resolve to different bytes tomorrow.
|
|
33
|
+
*/
|
|
34
|
+
export declare function digestBytes(bytes: Uint8Array): Digest;
|
|
35
|
+
/** Canonicalize then hash. The only correct way to hash a structure. */
|
|
36
|
+
export declare function digestValue(value: CanonicalValue): Digest;
|
|
37
|
+
/**
|
|
38
|
+
* Combine digests under a domain label.
|
|
39
|
+
*
|
|
40
|
+
* The label prevents cross-domain collision: an environment key and a render hash
|
|
41
|
+
* built from identical component digests must not be equal, or an environment
|
|
42
|
+
* change could masquerade as a content match.
|
|
43
|
+
*/
|
|
44
|
+
export declare function digestCombine(domain: string, parts: readonly Digest[]): Digest;
|
|
45
|
+
//# sourceMappingURL=hash.d.ts.map
|