@variance-authority/core 0.1.0 → 0.2.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 +192 -0
- package/README.md +51 -9
- package/dist/attribute/closure.d.ts +105 -0
- package/dist/attribute/closure.js +180 -0
- package/dist/attribute/closure.js.map +1 -0
- package/dist/attribute/composition.d.ts +5 -3
- package/dist/attribute/composition.js.map +1 -1
- package/dist/attribute/divergence.js +7 -1
- package/dist/attribute/divergence.js.map +1 -1
- package/dist/attribute/index.d.ts +5 -1
- package/dist/attribute/index.js +3 -1
- package/dist/attribute/index.js.map +1 -1
- package/dist/attribute/lexicon.d.ts +88 -0
- package/dist/attribute/lexicon.js +150 -0
- package/dist/attribute/lexicon.js.map +1 -0
- package/dist/attribute/source.d.ts +19 -2
- package/dist/attribute/source.js +12 -0
- package/dist/attribute/source.js.map +1 -1
- package/dist/compare/derive.d.ts +17 -0
- package/dist/compare/derive.js.map +1 -1
- package/dist/compare/diff/index.d.ts +25 -1
- package/dist/compare/diff/index.js +8 -5
- package/dist/compare/diff/index.js.map +1 -1
- package/dist/compare/explain.d.ts +1 -1
- package/dist/compare/explain.js +15 -8
- package/dist/compare/explain.js.map +1 -1
- package/dist/compare/index.d.ts +2 -2
- package/dist/compare/index.js +1 -1
- package/dist/compare/index.js.map +1 -1
- package/dist/compare/observability.d.ts +25 -1
- package/dist/compare/observability.js +54 -0
- package/dist/compare/observability.js.map +1 -1
- package/dist/compare/parting.d.ts +24 -2
- package/dist/compare/parting.js +14 -9
- package/dist/compare/parting.js.map +1 -1
- package/dist/compare/slice.d.ts +15 -2
- package/dist/compare/slice.js +6 -2
- package/dist/compare/slice.js.map +1 -1
- package/dist/format/capture.d.ts +15 -0
- package/dist/format/capture.js +30 -1
- package/dist/format/capture.js.map +1 -1
- package/dist/format/document.d.ts +65 -5
- package/dist/format/document.js +34 -0
- package/dist/format/document.js.map +1 -1
- package/dist/format/hash.d.ts +26 -0
- package/dist/format/hash.js +41 -0
- package/dist/format/hash.js.map +1 -1
- package/dist/format/index.d.ts +4 -3
- package/dist/format/index.js +3 -2
- package/dist/format/index.js.map +1 -1
- package/dist/format/sha256.d.ts +10 -2
- package/dist/format/sha256.js +42 -9
- package/dist/format/sha256.js.map +1 -1
- package/dist/format/stabilize.d.ts +15 -0
- package/dist/format/stabilize.js +22 -1
- package/dist/format/stabilize.js.map +1 -1
- package/dist/index.d.ts +3 -11
- package/dist/index.js +3 -11
- package/dist/index.js.map +1 -1
- package/dist/relate/graph.d.ts +26 -6
- package/dist/relate/graph.js +14 -7
- package/dist/relate/graph.js.map +1 -1
- package/dist/relate/index.d.ts +2 -2
- package/dist/relate/index.js +1 -1
- package/dist/relate/index.js.map +1 -1
- package/dist/relate/merkle.d.ts +8 -5
- package/dist/relate/merkle.js +8 -6
- package/dist/relate/merkle.js.map +1 -1
- package/dist/relate/reach.d.ts +19 -5
- package/dist/relate/reach.js +10 -8
- package/dist/relate/reach.js.map +1 -1
- package/dist/relate/records.d.ts +29 -4
- package/dist/relate/records.js +60 -7
- package/dist/relate/records.js.map +1 -1
- package/dist/segment/index.d.ts +123 -0
- package/dist/segment/index.js +265 -0
- package/dist/segment/index.js.map +1 -0
- package/dist/share/index.d.ts +148 -0
- package/dist/share/index.js +190 -0
- package/dist/share/index.js.map +1 -0
- package/package.json +10 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/attribute/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;
|
|
1
|
+
{"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/attribute/source.ts"],"names":[],"mappings":"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","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), so building one is\n * a caller's job; 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 four are shapes a scan of the source text matched by name.\n * `engine` is the one that is not a match at all: the browser was asked where\n * the function it rendered was compiled from, and that position was mapped\n * back to the file. It names the component that ran, whatever it was called.\n */\n readonly via: 'function' | 'const' | 'class' | 'declared' | '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/**\n * Extract component declarations from one file's source.\n *\n * A regex scan, and the limits are worth stating rather than discovering. It\n * finds exported and local declarations in the three shapes React components are\n * written in, and it will miss a component produced by a factory, assigned\n * dynamically, or re-exported under another name. It can also match a function\n * that merely looks like a component — capitalised, declared at top level — and\n * name a non-component in a report.\n *\n * Both failures are survivable in a way a wrong file path would not be: a miss\n * degrades the report to the component name, which is what it said before, and a\n * false positive can only appear if attribution already named that identifier.\n * Parsing properly is the right answer eventually; it is not worth a parser\n * dependency to find out whether the link is useful.\n */\nexport function indexSource(file: string, contents: string): SourceIndex {\n const found: Record<string, SourceRef[]> = {};\n const lines = contents.split('\\n');\n\n const patterns: readonly { readonly re: RegExp; readonly via: SourceRef['via'] }[] = [\n { re: /^\\s*(?:export\\s+)?(?:default\\s+)?function\\s+([A-Z][A-Za-z0-9_]*)/, via: 'function' },\n { re: /^\\s*(?:export\\s+)?(?:const|let)\\s+([A-Z][A-Za-z0-9_]*)\\s*[:=]/, via: 'const' },\n { re: /^\\s*(?:export\\s+)?(?:default\\s+)?class\\s+([A-Z][A-Za-z0-9_]*)/, via: 'class' },\n ];\n\n for (const [index, line] of lines.entries()) {\n for (const { re, via } of patterns) {\n const match = re.exec(line);\n if (!match) continue;\n\n const name = match[1]!;\n (found[name] ??= []).push({ file, line: index + 1, via });\n break;\n }\n }\n\n return found;\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"]}
|
package/dist/compare/derive.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type Digest } from '../format/hash.js';
|
|
|
2
2
|
import type { SemanticSnapshot } from '../format/snapshot.js';
|
|
3
3
|
import type { EnvironmentDelta } from '../format/environment.js';
|
|
4
4
|
import { type Band } from './band.js';
|
|
5
|
+
import type { Observability } from './observability.js';
|
|
5
6
|
import type { AggregateImpact } from './impact.js';
|
|
6
7
|
import { type ChangedComponent, type Delta, type Root } from './diff/index.js';
|
|
7
8
|
/**
|
|
@@ -41,6 +42,22 @@ export interface Variation {
|
|
|
41
42
|
* to refuse (ADR-0002).
|
|
42
43
|
*/
|
|
43
44
|
readonly unobserved: readonly Band[];
|
|
45
|
+
/**
|
|
46
|
+
* How completely each band was decided, weaker side winning.
|
|
47
|
+
*
|
|
48
|
+
* A variation is the one comparison that crosses profiles on purpose, so this
|
|
49
|
+
* is where the weaker-side rule has work to do: a dark story read in Chromium
|
|
50
|
+
* against a light parent read in JSDOM is only as well observed as the JSDOM
|
|
51
|
+
* side, band by band.
|
|
52
|
+
*/
|
|
53
|
+
readonly observability: Readonly<Record<Band, Observability>>;
|
|
54
|
+
/**
|
|
55
|
+
* Bands decided on less than the evidence the band is made of.
|
|
56
|
+
*
|
|
57
|
+
* The pair is alike *there* in a narrower sense than elsewhere, and a report
|
|
58
|
+
* that prints one word for both senses is the difference nobody can see.
|
|
59
|
+
*/
|
|
60
|
+
readonly narrowed: readonly Band[];
|
|
44
61
|
/**
|
|
45
62
|
* Render inputs that differ between the two sides.
|
|
46
63
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"derive.js","sourceRoot":"","sources":["../../src/compare/derive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAe,MAAM,mBAAmB,CAAC;AAG5E,OAAO,EAAE,KAAK,EAAa,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"derive.js","sourceRoot":"","sources":["../../src/compare/derive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAe,MAAM,mBAAmB,CAAC;AAG5E,OAAO,EAAE,KAAK,EAAa,MAAM,WAAW,CAAC;AAG7C,OAAO,EAAE,YAAY,EAAgD,MAAM,iBAAiB,CAAC;AAyF7F;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,MAAwB,EAAE,OAAyB;IACjF,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAElE,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE;QAC3B,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE;QACzB,GAAG,QAAQ;QACX,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC9C,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,MAAM,CAAC;KAC5C,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAS,kBAAkB,CAAC,MAAwB;IAClD,OAAO,aAAa,CAClB,cAAc,EACd,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACnB,WAAW,CAAC;QACV,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,EAAE,EAAE,KAAK,CAAC,EAAE;KACb,CAAC,CACH,CACF,CAAC;AACJ,CAAC","sourcesContent":["import { digestCombine, digestValue, type Digest } from '../format/hash.js';\nimport type { SemanticSnapshot } from '../format/snapshot.js';\nimport type { EnvironmentDelta } from '../format/environment.js';\nimport { BANDS, type Band } from './band.js';\nimport type { Observability } from './observability.js';\nimport type { AggregateImpact } from './impact.js';\nimport { compareTrees, type ChangedComponent, type Delta, type Root } from './diff/index.js';\n\n/**\n * One subject read as a variation of another.\n *\n * The same arithmetic a verdict is made of, pointed at a pair of subjects\n * somebody linked on purpose: a dark story against its light parent, a mobile\n * viewport against the desktop one, a story behind a feature flag against the\n * story it was copied from. Nothing here is compared against a baseline and\n * nothing here is a verdict — a variation *is* a difference, so reporting it as a\n * regression would be reporting the subject for existing.\n *\n * What it answers is the question a second case cannot: **a new story under a\n * flag is a new baseline and an empty diff, and everybody has to open two\n * pictures to see what the flag does.** Linked, the difference is a value with an\n * identity, and the identity is the useful part — see {@link Variation.digest}.\n */\nexport interface Variation {\n /** The variation's own subject id. */\n readonly subject: string;\n /** The subject it declares itself a variation of. */\n readonly parent: string;\n\n /** `true` when the two renders hash identically — the flag changed nothing. */\n readonly identical: boolean;\n\n readonly deltas: readonly Delta[];\n readonly roots: readonly Root[];\n readonly components: readonly ChangedComponent[];\n readonly impact: AggregateImpact;\n\n /** Bands the difference falls in, in band order. Empty when identical. */\n readonly bands: readonly Band[];\n\n /**\n * Bands **either** side's profile could not decide.\n *\n * Union rather than intersection, and never dropped: a jsdom side has no\n * geometry, and a variation that reported no geometry difference because\n * nothing could measure one would be the false `unchanged` this project exists\n * to refuse (ADR-0002).\n */\n readonly unobserved: readonly Band[];\n\n /**\n * How completely each band was decided, weaker side winning.\n *\n * A variation is the one comparison that crosses profiles on purpose, so this\n * is where the weaker-side rule has work to do: a dark story read in Chromium\n * against a light parent read in JSDOM is only as well observed as the JSDOM\n * side, band by band.\n */\n readonly observability: Readonly<Record<Band, Observability>>;\n\n /**\n * Bands decided on less than the evidence the band is made of.\n *\n * The pair is alike *there* in a narrower sense than elsewhere, and a report\n * that prints one word for both senses is the difference nobody can see.\n */\n readonly narrowed: readonly Band[];\n\n /**\n * Render inputs that differ between the two sides.\n *\n * Reported, never a refusal. A variation is frequently a *deliberate*\n * environment difference — a viewport, a colour scheme — and the field that\n * would make `diffSnapshots` refuse the pair is here the thing being examined.\n */\n readonly environmentDeltas: readonly EnvironmentDelta[];\n\n /**\n * The identity of the difference itself: `variation/v1` over the deltas.\n *\n * The field that makes a linked pair worth more than two pictures. A parent and\n * its variation both move by the same edit — a token changed, both re-render,\n * both go red — and this digest does not move, because what the variation *adds*\n * to its parent is unchanged. So the reviewer's question stops being \"did these\n * two subjects change\" and becomes \"did what this flag does change\", which is\n * the question they were trying to ask.\n *\n * Positions and values are in it and owner chains are not. An owner frame\n * carries a props digest per frame, so hashing it would make the identity move\n * whenever any component above the change was handed a different object —\n * which is a fact about the render, not about the difference.\n */\n readonly digest: Digest;\n}\n\n/**\n * Compare a variation against the subject it derives from.\n *\n * Refuses nothing. Two subject ids are the input this function exists for, and\n * two profiles or two environments are reported as part of the answer — see\n * `compareTrees` for why the refusals live with the caller that owes them rather\n * than with the arithmetic.\n */\nexport function deriveVariation(parent: SemanticSnapshot, variant: SemanticSnapshot): Variation {\n const compared = compareTrees(parent, variant);\n const moved = new Set(compared.deltas.map((delta) => delta.band));\n\n return {\n subject: variant.subject.id,\n parent: parent.subject.id,\n ...compared,\n bands: BANDS.filter((band) => moved.has(band)),\n digest: digestOfDifference(compared.deltas),\n };\n}\n\n/**\n * `variation/v1` over what each delta *is*, in the order the comparison produced.\n *\n * Order is the comparison's, which is a function of the two trees and of nothing\n * else — sorting here would cost a walk to buy stability the input already has.\n */\nfunction digestOfDifference(deltas: readonly Delta[]): Digest {\n return digestCombine(\n 'variation/v1',\n deltas.map((delta) =>\n digestValue({\n kind: delta.kind,\n band: delta.band,\n path: delta.path,\n property: delta.property,\n from: delta.from,\n to: delta.to,\n }),\n ),\n );\n}\n"]}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Band } from '../band.js';
|
|
2
2
|
import { type AggregateImpact } from '../impact.js';
|
|
3
3
|
import { type EnvironmentDelta } from '../../format/environment.js';
|
|
4
4
|
import type { SemanticSnapshot } from '../../format/snapshot.js';
|
|
5
|
+
import { type Observability } from '../observability.js';
|
|
5
6
|
import { type ChangedComponent, type Delta, type Root } from './delta.js';
|
|
6
7
|
export type { ChangedComponent, Delta, Root, RootKind } from './delta.js';
|
|
7
8
|
export { matchTrees } from './match.js';
|
|
@@ -30,6 +31,16 @@ export interface SemanticDiff {
|
|
|
30
31
|
* failing to observe it.
|
|
31
32
|
*/
|
|
32
33
|
readonly impact: AggregateImpact;
|
|
34
|
+
/**
|
|
35
|
+
* How completely each band was decided, weaker side winning.
|
|
36
|
+
*
|
|
37
|
+
* The primitive the two lists below are views of. Carried rather than reduced
|
|
38
|
+
* to them on the way out, because `unchanged` is one word for two readings of
|
|
39
|
+
* different strength and a caller with a stricter policy than ours has to be
|
|
40
|
+
* able to tell them apart without re-deriving the profile arithmetic — which
|
|
41
|
+
* is the version of this that drifts.
|
|
42
|
+
*/
|
|
43
|
+
readonly observability: Readonly<Record<Band, Observability>>;
|
|
33
44
|
/**
|
|
34
45
|
* Bands this profile could not observe.
|
|
35
46
|
*
|
|
@@ -37,6 +48,19 @@ export interface SemanticDiff {
|
|
|
37
48
|
* fine" under a profile with no layout engine (ADR-0002).
|
|
38
49
|
*/
|
|
39
50
|
readonly unobserved: readonly Band[];
|
|
51
|
+
/**
|
|
52
|
+
* Bands decided on less than the evidence the band is made of.
|
|
53
|
+
*
|
|
54
|
+
* `unobserved`'s quiet half. A band here returned an answer, and the answer is
|
|
55
|
+
* narrower than the word carrying it: a `token` band decided `declared-only`
|
|
56
|
+
* compared what an author wrote and not what an engine resolved, so
|
|
57
|
+
* `padding: 1rem` is `1rem` on both sides and a root font-size that moved
|
|
58
|
+
* underneath it is outside the comparison. Absent from the unobserved list on
|
|
59
|
+
* purpose — the band *was* decided — and named here so the qualification
|
|
60
|
+
* reaches whoever reads the verdict instead of stopping at the profile that
|
|
61
|
+
* produced it.
|
|
62
|
+
*/
|
|
63
|
+
readonly narrowed: readonly Band[];
|
|
40
64
|
}
|
|
41
65
|
/**
|
|
42
66
|
* Compare two snapshots of one subject.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { BANDS } from '../band.js';
|
|
2
1
|
import { aggregateImpact } from '../impact.js';
|
|
3
2
|
import { diffEnvironments } from '../../format/environment.js';
|
|
4
|
-
import {
|
|
3
|
+
import { narrowedBands, sharedObservability, unobservedBands, } from '../observability.js';
|
|
5
4
|
import { impactTag } from './delta.js';
|
|
6
5
|
import { matchTrees } from './match.js';
|
|
7
6
|
import { compareNodes, wholeNode } from './compare-nodes.js';
|
|
@@ -43,9 +42,9 @@ export function diffSnapshots(baseline, candidate) {
|
|
|
43
42
|
* costs it nothing and changes none of its answers.
|
|
44
43
|
*/
|
|
45
44
|
export function compareTrees(baseline, candidate) {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
45
|
+
const observability = sharedObservability(candidate.profile, baseline.profile);
|
|
46
|
+
const unobserved = unobservedBands(observability);
|
|
47
|
+
const narrowed = narrowedBands(observability);
|
|
49
48
|
const environmentDeltas = diffEnvironments(baseline.environment.inputs, candidate.environment.inputs);
|
|
50
49
|
if (baseline.renderHash === candidate.renderHash) {
|
|
51
50
|
return {
|
|
@@ -55,7 +54,9 @@ export function compareTrees(baseline, candidate) {
|
|
|
55
54
|
roots: [],
|
|
56
55
|
components: [],
|
|
57
56
|
impact: 'paint',
|
|
57
|
+
observability,
|
|
58
58
|
unobserved,
|
|
59
|
+
narrowed,
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
const matching = matchTrees(baseline.root, candidate.root);
|
|
@@ -95,7 +96,9 @@ export function compareTrees(baseline, candidate) {
|
|
|
95
96
|
roots,
|
|
96
97
|
components: componentsOf(located, roots),
|
|
97
98
|
impact: aggregateImpact(located.map(impactTag)),
|
|
99
|
+
observability,
|
|
98
100
|
unobserved,
|
|
101
|
+
narrowed,
|
|
99
102
|
};
|
|
100
103
|
}
|
|
101
104
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/compare/diff/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { BANDS, type Band } from '../band.js';\nimport { aggregateImpact, type AggregateImpact } from '../impact.js';\nimport { diffEnvironments, type EnvironmentDelta } from '../../format/environment.js';\nimport type { SemanticSnapshot } from '../../format/snapshot.js';\nimport { decidesBand, observableBands } from '../observability.js';\nimport { impactTag, type ChangedComponent, type Delta, type Root } from './delta.js';\nimport { matchTrees } from './match.js';\nimport { compareNodes, wholeNode } from './compare-nodes.js';\nimport { attribute } from './attribution.js';\nimport { componentsOf } from './components.js';\nimport { locate } from '../../attribute/locate.js';\n\nexport type { ChangedComponent, Delta, Root, RootKind } from './delta.js';\nexport { matchTrees } from './match.js';\nexport type { Matching } from './match.js';\n\nexport interface SemanticDiff {\n readonly subjectId: string;\n /** `true` when both render hashes agree — nothing below need be consulted. */\n readonly identical: boolean;\n\n readonly environmentDeltas: readonly EnvironmentDelta[];\n readonly deltas: readonly Delta[];\n\n /** One entry per explanation. This is what a docket renders and approves. */\n readonly roots: readonly Root[];\n\n /**\n * Components implicated, separated into causes and collateral.\n *\n * \"Eleven components changed\" reads like eleven problems. \"`Button` changed,\n * and ten components render it\" reads like one, which is what it is.\n */\n readonly components: readonly ChangedComponent[];\n\n /**\n * Whether anything in this change set can move a box.\n *\n * `paint` or `composite` means nothing reflowed and no geometric collateral is\n * possible — a conclusion available without a layout engine, which is how a\n * profile that cannot measure still rules movement out instead of merely\n * failing to observe it.\n */\n readonly impact: AggregateImpact;\n\n /**\n * Bands this profile could not observe.\n *\n * Reported so that \"no geometry deltas\" is never mistaken for \"geometry is\n * fine\" under a profile with no layout engine (ADR-0002).\n */\n readonly unobserved: readonly Band[];\n}\n\n/**\n * Compare two snapshots of one subject.\n *\n * @throws {Error} when the snapshots come from different subjects or different\n * observation profiles. Neither is a large diff — they are a category error, and\n * returning deltas for them would let a JSDOM run appear to satisfy a Chromium\n * baseline while blind to every geometry change in it.\n */\nexport function diffSnapshots(\n baseline: SemanticSnapshot,\n candidate: SemanticSnapshot,\n): SemanticDiff {\n if (baseline.subject.id !== candidate.subject.id) {\n throw new Error(\n `refusing to diff different subjects: ${baseline.subject.id} vs ${candidate.subject.id}`,\n );\n }\n\n if (baseline.profile.id !== candidate.profile.id) {\n throw new Error(\n `refusing to diff across observation profiles: ${baseline.profile.id} vs ${candidate.profile.id}`,\n );\n }\n\n return { subjectId: candidate.subject.id, ...compareTrees(baseline, candidate) };\n}\n\n/** Everything a {@link SemanticDiff} is except the subject it is about. */\nexport type TreeComparison = Omit<SemanticDiff, 'subjectId'>;\n\n/**\n * The comparison itself, after whatever refusals the caller owes.\n *\n * Split out because two callers owe different refusals over the same arithmetic.\n * `diffSnapshots` produces a **verdict** and must refuse two subjects, because a\n * verdict that crossed them would let one subject's baseline stand in for\n * another's. `deriveVariation` produces an **explanation** of a difference\n * somebody declared on purpose — a dark story against its light parent — where\n * two subject ids are the entire point and a refusal would be the bug.\n *\n * What is not negotiable either way is the observability rule: a band is\n * `unobserved` unless **both** sides could decide it, so a difference that only\n * one profile could have seen is never reported as no difference (ADR-0002).\n * Under `diffSnapshots` the two profiles are already identical, so the union\n * costs it nothing and changes none of its answers.\n */\nexport function compareTrees(\n baseline: SemanticSnapshot,\n candidate: SemanticSnapshot,\n): TreeComparison {\n const here = observableBands(candidate.profile);\n const there = observableBands(baseline.profile);\n const unobserved: Band[] = BANDS.filter(\n (band) => !decidesBand(band, here[band]) || !decidesBand(band, there[band]),\n );\n\n const environmentDeltas = diffEnvironments(\n baseline.environment.inputs,\n candidate.environment.inputs,\n );\n\n if (baseline.renderHash === candidate.renderHash) {\n return {\n identical: true,\n environmentDeltas,\n deltas: [],\n roots: [],\n components: [],\n impact: 'paint',\n unobserved,\n };\n }\n\n const matching = matchTrees(baseline.root, candidate.root);\n const deltas: Delta[] = [];\n\n for (const node of matching.added) {\n deltas.push(wholeNode('node-added', node));\n }\n for (const node of matching.removed) {\n deltas.push(wholeNode('node-removed', node));\n }\n\n for (const [before, after] of matching.pairs) {\n if (matching.moved.has(before)) {\n deltas.push(wholeNode('node-moved', after));\n }\n compareNodes(before, after, deltas, candidate.profile.layout && baseline.profile.layout);\n }\n\n // Orientation is attached after the deltas exist, in one pass over the\n // candidate tree. Doing it during comparison would locate against whichever\n // tree that comparison happened to be holding, and a removal must be located\n // in the tree it was removed *from* — see below.\n const located = deltas.map((delta) => {\n const tree = delta.kind === 'node-removed' ? baseline.root : candidate.root;\n const where = locate(tree, delta.path);\n\n return where.where === ''\n ? delta\n : {\n ...delta,\n where: where.where,\n ...(where.region !== undefined ? { region: where.region } : {}),\n };\n });\n\n const roots = attribute(located, matching, environmentDeltas);\n\n return {\n identical: false,\n environmentDeltas,\n deltas: located,\n roots,\n components: componentsOf(located, roots),\n impact: aggregateImpact(located.map(impactTag)),\n unobserved,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/compare/diff/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Band } from '../band.js';\nimport { aggregateImpact, type AggregateImpact } from '../impact.js';\nimport { diffEnvironments, type EnvironmentDelta } from '../../format/environment.js';\nimport type { SemanticSnapshot } from '../../format/snapshot.js';\nimport {\n narrowedBands,\n sharedObservability,\n unobservedBands,\n type Observability,\n} from '../observability.js';\nimport { impactTag, type ChangedComponent, type Delta, type Root } from './delta.js';\nimport { matchTrees } from './match.js';\nimport { compareNodes, wholeNode } from './compare-nodes.js';\nimport { attribute } from './attribution.js';\nimport { componentsOf } from './components.js';\nimport { locate } from '../../attribute/locate.js';\n\nexport type { ChangedComponent, Delta, Root, RootKind } from './delta.js';\nexport { matchTrees } from './match.js';\nexport type { Matching } from './match.js';\n\nexport interface SemanticDiff {\n readonly subjectId: string;\n /** `true` when both render hashes agree — nothing below need be consulted. */\n readonly identical: boolean;\n\n readonly environmentDeltas: readonly EnvironmentDelta[];\n readonly deltas: readonly Delta[];\n\n /** One entry per explanation. This is what a docket renders and approves. */\n readonly roots: readonly Root[];\n\n /**\n * Components implicated, separated into causes and collateral.\n *\n * \"Eleven components changed\" reads like eleven problems. \"`Button` changed,\n * and ten components render it\" reads like one, which is what it is.\n */\n readonly components: readonly ChangedComponent[];\n\n /**\n * Whether anything in this change set can move a box.\n *\n * `paint` or `composite` means nothing reflowed and no geometric collateral is\n * possible — a conclusion available without a layout engine, which is how a\n * profile that cannot measure still rules movement out instead of merely\n * failing to observe it.\n */\n readonly impact: AggregateImpact;\n\n /**\n * How completely each band was decided, weaker side winning.\n *\n * The primitive the two lists below are views of. Carried rather than reduced\n * to them on the way out, because `unchanged` is one word for two readings of\n * different strength and a caller with a stricter policy than ours has to be\n * able to tell them apart without re-deriving the profile arithmetic — which\n * is the version of this that drifts.\n */\n readonly observability: Readonly<Record<Band, Observability>>;\n\n /**\n * Bands this profile could not observe.\n *\n * Reported so that \"no geometry deltas\" is never mistaken for \"geometry is\n * fine\" under a profile with no layout engine (ADR-0002).\n */\n readonly unobserved: readonly Band[];\n\n /**\n * Bands decided on less than the evidence the band is made of.\n *\n * `unobserved`'s quiet half. A band here returned an answer, and the answer is\n * narrower than the word carrying it: a `token` band decided `declared-only`\n * compared what an author wrote and not what an engine resolved, so\n * `padding: 1rem` is `1rem` on both sides and a root font-size that moved\n * underneath it is outside the comparison. Absent from the unobserved list on\n * purpose — the band *was* decided — and named here so the qualification\n * reaches whoever reads the verdict instead of stopping at the profile that\n * produced it.\n */\n readonly narrowed: readonly Band[];\n}\n\n/**\n * Compare two snapshots of one subject.\n *\n * @throws {Error} when the snapshots come from different subjects or different\n * observation profiles. Neither is a large diff — they are a category error, and\n * returning deltas for them would let a JSDOM run appear to satisfy a Chromium\n * baseline while blind to every geometry change in it.\n */\nexport function diffSnapshots(\n baseline: SemanticSnapshot,\n candidate: SemanticSnapshot,\n): SemanticDiff {\n if (baseline.subject.id !== candidate.subject.id) {\n throw new Error(\n `refusing to diff different subjects: ${baseline.subject.id} vs ${candidate.subject.id}`,\n );\n }\n\n if (baseline.profile.id !== candidate.profile.id) {\n throw new Error(\n `refusing to diff across observation profiles: ${baseline.profile.id} vs ${candidate.profile.id}`,\n );\n }\n\n return { subjectId: candidate.subject.id, ...compareTrees(baseline, candidate) };\n}\n\n/** Everything a {@link SemanticDiff} is except the subject it is about. */\nexport type TreeComparison = Omit<SemanticDiff, 'subjectId'>;\n\n/**\n * The comparison itself, after whatever refusals the caller owes.\n *\n * Split out because two callers owe different refusals over the same arithmetic.\n * `diffSnapshots` produces a **verdict** and must refuse two subjects, because a\n * verdict that crossed them would let one subject's baseline stand in for\n * another's. `deriveVariation` produces an **explanation** of a difference\n * somebody declared on purpose — a dark story against its light parent — where\n * two subject ids are the entire point and a refusal would be the bug.\n *\n * What is not negotiable either way is the observability rule: a band is\n * `unobserved` unless **both** sides could decide it, so a difference that only\n * one profile could have seen is never reported as no difference (ADR-0002).\n * Under `diffSnapshots` the two profiles are already identical, so the union\n * costs it nothing and changes none of its answers.\n */\nexport function compareTrees(\n baseline: SemanticSnapshot,\n candidate: SemanticSnapshot,\n): TreeComparison {\n const observability = sharedObservability(candidate.profile, baseline.profile);\n const unobserved = unobservedBands(observability);\n const narrowed = narrowedBands(observability);\n\n const environmentDeltas = diffEnvironments(\n baseline.environment.inputs,\n candidate.environment.inputs,\n );\n\n if (baseline.renderHash === candidate.renderHash) {\n return {\n identical: true,\n environmentDeltas,\n deltas: [],\n roots: [],\n components: [],\n impact: 'paint',\n observability,\n unobserved,\n narrowed,\n };\n }\n\n const matching = matchTrees(baseline.root, candidate.root);\n const deltas: Delta[] = [];\n\n for (const node of matching.added) {\n deltas.push(wholeNode('node-added', node));\n }\n for (const node of matching.removed) {\n deltas.push(wholeNode('node-removed', node));\n }\n\n for (const [before, after] of matching.pairs) {\n if (matching.moved.has(before)) {\n deltas.push(wholeNode('node-moved', after));\n }\n compareNodes(before, after, deltas, candidate.profile.layout && baseline.profile.layout);\n }\n\n // Orientation is attached after the deltas exist, in one pass over the\n // candidate tree. Doing it during comparison would locate against whichever\n // tree that comparison happened to be holding, and a removal must be located\n // in the tree it was removed *from* — see below.\n const located = deltas.map((delta) => {\n const tree = delta.kind === 'node-removed' ? baseline.root : candidate.root;\n const where = locate(tree, delta.path);\n\n return where.where === ''\n ? delta\n : {\n ...delta,\n where: where.where,\n ...(where.region !== undefined ? { region: where.region } : {}),\n };\n });\n\n const roots = attribute(located, matching, environmentDeltas);\n\n return {\n identical: false,\n environmentDeltas,\n deltas: located,\n roots,\n components: componentsOf(located, roots),\n impact: aggregateImpact(located.map(impactTag)),\n observability,\n unobserved,\n narrowed,\n };\n}\n"]}
|
|
@@ -8,7 +8,7 @@ import type { Parting } from './parting.js';
|
|
|
8
8
|
* ```text
|
|
9
9
|
* a <div> rendered <p> on one side and <span> on the other
|
|
10
10
|
* Summary was handed a different `total`
|
|
11
|
-
* Cart chose differently — useState #2
|
|
11
|
+
* Cart chose differently — useState #2 changed, and Summary's `total` follows
|
|
12
12
|
* ```
|
|
13
13
|
*
|
|
14
14
|
* The first is a diff. The second localises. Only the third names a cause, and
|
package/dist/compare/explain.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* ```text
|
|
8
8
|
* a <div> rendered <p> on one side and <span> on the other
|
|
9
9
|
* Summary was handed a different `total`
|
|
10
|
-
* Cart chose differently — useState #2
|
|
10
|
+
* Cart chose differently — useState #2 changed, and Summary's `total` follows
|
|
11
11
|
* ```
|
|
12
12
|
*
|
|
13
13
|
* The first is a diff. The second localises. Only the third names a cause, and
|
|
@@ -68,9 +68,9 @@ function sentence(boundary) {
|
|
|
68
68
|
const { component, inputs } = boundary;
|
|
69
69
|
switch (boundary.rung) {
|
|
70
70
|
case 'stateful':
|
|
71
|
-
return `${component} chose differently — ${list(inputs.filter(isHook))}
|
|
71
|
+
return `${component} chose differently — ${list(inputs.filter(isHook))} changed`;
|
|
72
72
|
case 'external':
|
|
73
|
-
return `${component} read a different external store — ${list(inputs.filter(isHook))}
|
|
73
|
+
return `${component} read a different external store — ${list(inputs.filter(isHook))} changed`;
|
|
74
74
|
case 'provided':
|
|
75
75
|
return `${component} was given a different ${list(inputs.filter((i) => i.kind === 'context'))}`;
|
|
76
76
|
case 'handed':
|
|
@@ -83,6 +83,11 @@ function sentence(boundary) {
|
|
|
83
83
|
`${list(inputs.filter((i) => i.kind === 'inherited'))} — an ancestor declared it`);
|
|
84
84
|
case 'undetermined':
|
|
85
85
|
return `${component} rendered differently from inputs that all agreed — nondeterministic`;
|
|
86
|
+
// Not an accusation. The two readings are two places, and the sentence says
|
|
87
|
+
// where to look — outwards, at what put it there — rather than at the
|
|
88
|
+
// component, which received nothing that could have decided this.
|
|
89
|
+
case 'placed':
|
|
90
|
+
return `${component} rendered differently where it stands — its context decided this`;
|
|
86
91
|
case 'unread':
|
|
87
92
|
return `${component} rendered differently, and what it holds could not be read`;
|
|
88
93
|
case 'unpaired':
|
|
@@ -149,17 +154,19 @@ function slice(kind) {
|
|
|
149
154
|
case 'settled':
|
|
150
155
|
return 'settled — the component tree, its inputs and its output all held';
|
|
151
156
|
case 'variation':
|
|
152
|
-
return 'variation — an input
|
|
157
|
+
return 'variation — an input changed and the page followed';
|
|
153
158
|
case 'flake':
|
|
154
|
-
return 'flake — every input agreed, the component tree held, and the page
|
|
159
|
+
return 'flake — every input agreed, the component tree held, and the page changed anyway';
|
|
160
|
+
case 'placed':
|
|
161
|
+
return 'placed — every input agreed, and the two readings are of two places';
|
|
155
162
|
case 'reshaped':
|
|
156
163
|
return 'reshaped — the component tree is a different tree and the page followed';
|
|
157
164
|
case 'refactor':
|
|
158
|
-
return 'refactor — the component tree
|
|
165
|
+
return 'refactor — the component tree changed and the page did not';
|
|
159
166
|
case 'absorbed':
|
|
160
|
-
return 'absorbed — an input
|
|
167
|
+
return 'absorbed — an input changed and the page did not';
|
|
161
168
|
case 'unread':
|
|
162
|
-
return 'unread — the page
|
|
169
|
+
return 'unread — the page changed and what would explain it was not read';
|
|
163
170
|
}
|
|
164
171
|
}
|
|
165
172
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"explain.js","sourceRoot":"","sources":["../../src/compare/explain.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Parting, PartedBoundary, MovedInput } from './parting.js';\nimport type { PartingSlice } from './slice.js';\n\n/**\n * A parting, spoken.\n *\n * The rung a run reached is only worth what a reader takes from it, and the\n * three sentences below are three different products:\n *\n * ```text\n * a <div> rendered <p> on one side and <span> on the other\n * Summary was handed a different `total`\n * Cart chose differently — useState #2 moved, and Summary's `total` follows\n * ```\n *\n * The first is a diff. The second localises. Only the third names a cause, and\n * it is the only one somebody can act on without opening the component and\n * guessing. So the shape here is an **origin and its manifestations**, not a\n * flat list: an origin line, then the boundaries and deltas downstream of it,\n * indented. A collateral boundary with no origin above it gets a line of its own\n * — that is a parting this run could not trace further, and hiding it under\n * something else would overstate what was found.\n */\nexport function explainParting(parting: Parting): readonly string[] {\n const boundaries = parting.boundaries;\n const headline = slice(parting.slice);\n\n if (boundaries === undefined) {\n return [headline, ' no framework boundary was read, so nothing can be said about why'];\n }\n if (boundaries.length === 0) {\n return [headline, ' every boundary read held its inputs and its output'];\n }\n\n const origins = parting.origins ?? [];\n const claimed = new Set<PartedBoundary>();\n const lines: string[] = [headline];\n\n for (const origin of origins) {\n if (claimed.has(origin)) continue;\n claimed.add(origin);\n const downstream = boundaries.filter(\n (other) => !claimed.has(other) && under(origin.path, other.path),\n );\n for (const other of downstream) claimed.add(other);\n\n lines.push(sentence(origin));\n lines.push(...deltaLines(origin, boundaries));\n\n // The avalanche. One input at a fork can put a boundary on every component\n // beneath it, and enumerating them buries the one line worth reading under\n // its own consequences. Past the cap the fan-out is stated as a size —\n // pointing at the fork is the finding, and the boundary list is still there\n // for a caller that wants to walk it.\n if (downstream.length > FANOUT) {\n const deltas = downstream.reduce((total, other) => total + other.deltas, 0);\n lines.push(\n ` manifests across ${downstream.length} boundaries below it` +\n (deltas > 0 ? `, ${deltas} delta${deltas === 1 ? '' : 's'} in all` : ''),\n );\n } else {\n for (const other of downstream) {\n lines.push(` manifests as ${sentence(other)}`);\n lines.push(...deltaLines(other, boundaries).map((line) => ` ${line}`));\n }\n }\n }\n\n for (const rest of boundaries) {\n if (claimed.has(rest)) continue;\n lines.push(sentence(rest));\n lines.push(...deltaLines(rest, boundaries));\n }\n\n return lines;\n}\n\nfunction sentence(boundary: PartedBoundary): string {\n const { component, inputs } = boundary;\n\n switch (boundary.rung) {\n case 'stateful':\n return `${component} chose differently — ${list(inputs.filter(isHook))} moved`;\n case 'external':\n return `${component} read a different external store — ${list(inputs.filter(isHook))} moved`;\n case 'provided':\n return `${component} was given a different ${list(inputs.filter((i) => i.kind === 'context'))}`;\n case 'handed':\n return `${component} was handed a different ${list(inputs.filter((i) => i.kind === 'prop'))}`;\n // Phrased at the ancestor rather than at the component, because that is\n // where the reader has to go. `Price` did nothing; something it is standing\n // inside declared a value it never declares for itself.\n case 'inherited':\n return (\n `${component} inherited a different ` +\n `${list(inputs.filter((i) => i.kind === 'inherited'))} — an ancestor declared it`\n );\n case 'undetermined':\n return `${component} rendered differently from inputs that all agreed — nondeterministic`;\n case 'unread':\n return `${component} rendered differently, and what it holds could not be read`;\n case 'unpaired':\n return `${component} is present on one side only`;\n }\n}\n\n/** What `holdingOf` writes for a context React could not name. */\nconst ANONYMOUS = '(anonymous)';\n\nfunction isHook(input: MovedInput): boolean {\n return input.kind === 'hook';\n}\n\n/**\n * Name the moved inputs, hooks by call position.\n *\n * `useState #2` rather than `useState` because a component with four `useState`\n * calls is the ordinary case, and a sentence naming the hook without saying\n * which one sends the reader back to counting — the work this module exists to\n * have already done.\n */\nfunction list(inputs: readonly MovedInput[]): string {\n const named = inputs.map((input) => {\n if (input.index !== undefined) return `${input.name} #${input.index}`;\n // A `createContext` call with no `displayName` is the common case, and\n // \"a different `(anonymous)`\" names the gap in React's metadata rather than\n // the thing that moved.\n return input.name === ANONYMOUS ? 'context value' : `\\`${input.name}\\``;\n });\n if (named.length === 0) return 'input';\n if (named.length === 1) return named[0] as string;\n return `${named.slice(0, -1).join(', ')} and ${named[named.length - 1] as string}`;\n}\n\n/**\n * The deltas at a boundary, and the one case worth saying nothing about.\n *\n * A boundary owning no delta is only *quiet* if nothing under it owns one\n * either. Otherwise the deltas are real and belong to a nested boundary, whose\n * own line already reports them — and \"and rendered the same anyway\" directly\n * beneath \"chose differently\" reads as a contradiction rather than as the\n * ownership statement it is.\n */\nfunction deltaLines(\n boundary: PartedBoundary,\n all: readonly PartedBoundary[],\n): readonly string[] {\n if (boundary.deltas === 0) {\n const moved = all.some((other) => under(boundary.path, other.path) && other.deltas > 0);\n return moved ? [] : [' and rendered the same anyway'];\n }\n const bands = boundary.bands.join(', ');\n const count = `${boundary.deltas} delta${boundary.deltas === 1 ? '' : 's'} here (${bands})`;\n return [` ${count}${named(boundary.moved)}`];\n}\n\nconst SHOWN = 4;\n\n/** Above this many boundaries under one origin, the fan-out is the finding. */\nconst FANOUT = 3;\n\n/**\n * The triage line, first, before anything about which input moved.\n *\n * Deliberately a whole sentence rather than the bare word: `refactor` alone\n * reads as a label somebody applied, and the clause is the evidence for it.\n */\nfunction slice(kind: PartingSlice): string {\n switch (kind) {\n case 'settled':\n return 'settled — the component tree, its inputs and its output all held';\n case 'variation':\n return 'variation — an input moved and the page followed';\n case 'flake':\n return 'flake — every input agreed, the component tree held, and the page moved anyway';\n case 'reshaped':\n return 'reshaped — the component tree is a different tree and the page followed';\n case 'refactor':\n return 'refactor — the component tree moved and the page did not';\n case 'absorbed':\n return 'absorbed — an input moved and the page did not';\n case 'unread':\n return 'unread — the page moved and what would explain it was not read';\n }\n}\n\n/**\n * The properties that moved, truncated where a list stops being a sentence.\n *\n * Four because `padding` expands to four longhands and a reader who has seen\n * the first learns nothing from the rest. What was dropped is counted rather\n * than elided — a list that quietly ends reads as the whole list.\n */\nfunction named(moved: readonly string[] | undefined): string {\n if (moved === undefined || moved.length === 0) return '';\n if (moved.length <= SHOWN) return ` — ${moved.join(', ')}`;\n return ` — ${moved.slice(0, SHOWN).join(', ')} and ${moved.length - SHOWN} more`;\n}\n\n/**\n * Whether one path encloses another, strictly.\n *\n * Segment-aware: `0/1` must not swallow `0/10`, and a plain `startsWith` would\n * make every tenth sibling collateral of its second.\n */\nfunction under(ancestor: string, path: string): boolean {\n if (ancestor === path) return false;\n return ancestor === '' || path.startsWith(`${ancestor}/`);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"explain.js","sourceRoot":"","sources":["../../src/compare/explain.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Parting, PartedBoundary, MovedInput } from './parting.js';\nimport type { PartingSlice } from './slice.js';\n\n/**\n * A parting, spoken.\n *\n * The rung a run reached is only worth what a reader takes from it, and the\n * three sentences below are three different products:\n *\n * ```text\n * a <div> rendered <p> on one side and <span> on the other\n * Summary was handed a different `total`\n * Cart chose differently — useState #2 changed, and Summary's `total` follows\n * ```\n *\n * The first is a diff. The second localises. Only the third names a cause, and\n * it is the only one somebody can act on without opening the component and\n * guessing. So the shape here is an **origin and its manifestations**, not a\n * flat list: an origin line, then the boundaries and deltas downstream of it,\n * indented. A collateral boundary with no origin above it gets a line of its own\n * — that is a parting this run could not trace further, and hiding it under\n * something else would overstate what was found.\n */\nexport function explainParting(parting: Parting): readonly string[] {\n const boundaries = parting.boundaries;\n const headline = slice(parting.slice);\n\n if (boundaries === undefined) {\n return [headline, ' no framework boundary was read, so nothing can be said about why'];\n }\n if (boundaries.length === 0) {\n return [headline, ' every boundary read held its inputs and its output'];\n }\n\n const origins = parting.origins ?? [];\n const claimed = new Set<PartedBoundary>();\n const lines: string[] = [headline];\n\n for (const origin of origins) {\n if (claimed.has(origin)) continue;\n claimed.add(origin);\n const downstream = boundaries.filter(\n (other) => !claimed.has(other) && under(origin.path, other.path),\n );\n for (const other of downstream) claimed.add(other);\n\n lines.push(sentence(origin));\n lines.push(...deltaLines(origin, boundaries));\n\n // The avalanche. One input at a fork can put a boundary on every component\n // beneath it, and enumerating them buries the one line worth reading under\n // its own consequences. Past the cap the fan-out is stated as a size —\n // pointing at the fork is the finding, and the boundary list is still there\n // for a caller that wants to walk it.\n if (downstream.length > FANOUT) {\n const deltas = downstream.reduce((total, other) => total + other.deltas, 0);\n lines.push(\n ` manifests across ${downstream.length} boundaries below it` +\n (deltas > 0 ? `, ${deltas} delta${deltas === 1 ? '' : 's'} in all` : ''),\n );\n } else {\n for (const other of downstream) {\n lines.push(` manifests as ${sentence(other)}`);\n lines.push(...deltaLines(other, boundaries).map((line) => ` ${line}`));\n }\n }\n }\n\n for (const rest of boundaries) {\n if (claimed.has(rest)) continue;\n lines.push(sentence(rest));\n lines.push(...deltaLines(rest, boundaries));\n }\n\n return lines;\n}\n\nfunction sentence(boundary: PartedBoundary): string {\n const { component, inputs } = boundary;\n\n switch (boundary.rung) {\n case 'stateful':\n return `${component} chose differently — ${list(inputs.filter(isHook))} changed`;\n case 'external':\n return `${component} read a different external store — ${list(inputs.filter(isHook))} changed`;\n case 'provided':\n return `${component} was given a different ${list(inputs.filter((i) => i.kind === 'context'))}`;\n case 'handed':\n return `${component} was handed a different ${list(inputs.filter((i) => i.kind === 'prop'))}`;\n // Phrased at the ancestor rather than at the component, because that is\n // where the reader has to go. `Price` did nothing; something it is standing\n // inside declared a value it never declares for itself.\n case 'inherited':\n return (\n `${component} inherited a different ` +\n `${list(inputs.filter((i) => i.kind === 'inherited'))} — an ancestor declared it`\n );\n case 'undetermined':\n return `${component} rendered differently from inputs that all agreed — nondeterministic`;\n // Not an accusation. The two readings are two places, and the sentence says\n // where to look — outwards, at what put it there — rather than at the\n // component, which received nothing that could have decided this.\n case 'placed':\n return `${component} rendered differently where it stands — its context decided this`;\n case 'unread':\n return `${component} rendered differently, and what it holds could not be read`;\n case 'unpaired':\n return `${component} is present on one side only`;\n }\n}\n\n/** What `holdingOf` writes for a context React could not name. */\nconst ANONYMOUS = '(anonymous)';\n\nfunction isHook(input: MovedInput): boolean {\n return input.kind === 'hook';\n}\n\n/**\n * Name the moved inputs, hooks by call position.\n *\n * `useState #2` rather than `useState` because a component with four `useState`\n * calls is the ordinary case, and a sentence naming the hook without saying\n * which one sends the reader back to counting — the work this module exists to\n * have already done.\n */\nfunction list(inputs: readonly MovedInput[]): string {\n const named = inputs.map((input) => {\n if (input.index !== undefined) return `${input.name} #${input.index}`;\n // A `createContext` call with no `displayName` is the common case, and\n // \"a different `(anonymous)`\" names the gap in React's metadata rather than\n // the thing that moved.\n return input.name === ANONYMOUS ? 'context value' : `\\`${input.name}\\``;\n });\n if (named.length === 0) return 'input';\n if (named.length === 1) return named[0] as string;\n return `${named.slice(0, -1).join(', ')} and ${named[named.length - 1] as string}`;\n}\n\n/**\n * The deltas at a boundary, and the one case worth saying nothing about.\n *\n * A boundary owning no delta is only *quiet* if nothing under it owns one\n * either. Otherwise the deltas are real and belong to a nested boundary, whose\n * own line already reports them — and \"and rendered the same anyway\" directly\n * beneath \"chose differently\" reads as a contradiction rather than as the\n * ownership statement it is.\n */\nfunction deltaLines(\n boundary: PartedBoundary,\n all: readonly PartedBoundary[],\n): readonly string[] {\n if (boundary.deltas === 0) {\n const moved = all.some((other) => under(boundary.path, other.path) && other.deltas > 0);\n return moved ? [] : [' and rendered the same anyway'];\n }\n const bands = boundary.bands.join(', ');\n const count = `${boundary.deltas} delta${boundary.deltas === 1 ? '' : 's'} here (${bands})`;\n return [` ${count}${named(boundary.moved)}`];\n}\n\nconst SHOWN = 4;\n\n/** Above this many boundaries under one origin, the fan-out is the finding. */\nconst FANOUT = 3;\n\n/**\n * The triage line, first, before anything about which input moved.\n *\n * Deliberately a whole sentence rather than the bare word: `refactor` alone\n * reads as a label somebody applied, and the clause is the evidence for it.\n */\nfunction slice(kind: PartingSlice): string {\n switch (kind) {\n case 'settled':\n return 'settled — the component tree, its inputs and its output all held';\n case 'variation':\n return 'variation — an input changed and the page followed';\n case 'flake':\n return 'flake — every input agreed, the component tree held, and the page changed anyway';\n case 'placed':\n return 'placed — every input agreed, and the two readings are of two places';\n case 'reshaped':\n return 'reshaped — the component tree is a different tree and the page followed';\n case 'refactor':\n return 'refactor — the component tree changed and the page did not';\n case 'absorbed':\n return 'absorbed — an input changed and the page did not';\n case 'unread':\n return 'unread — the page changed and what would explain it was not read';\n }\n}\n\n/**\n * The properties that moved, truncated where a list stops being a sentence.\n *\n * Four because `padding` expands to four longhands and a reader who has seen\n * the first learns nothing from the rest. What was dropped is counted rather\n * than elided — a list that quietly ends reads as the whole list.\n */\nfunction named(moved: readonly string[] | undefined): string {\n if (moved === undefined || moved.length === 0) return '';\n if (moved.length <= SHOWN) return ` — ${moved.join(', ')}`;\n return ` — ${moved.slice(0, SHOWN).join(', ')} and ${moved.length - SHOWN} more`;\n}\n\n/**\n * Whether one path encloses another, strictly.\n *\n * Segment-aware: `0/1` must not swallow `0/10`, and a plain `startsWith` would\n * make every tenth sibling collateral of its second.\n */\nfunction under(ancestor: string, path: string): boolean {\n if (ancestor === path) return false;\n return ancestor === '' || path.startsWith(`${ancestor}/`);\n}\n"]}
|
package/dist/compare/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { impactOf, canReflow, aggregateImpact } from './impact.js';
|
|
|
15
15
|
export type { PropertyImpact, AggregateImpact } from './impact.js';
|
|
16
16
|
export type { Band, DeltaKind } from './band.js';
|
|
17
17
|
export { BANDS, bandOf, loudestBand } from './band.js';
|
|
18
|
-
export { observableBands, decidesBand } from './observability.js';
|
|
18
|
+
export { observableBands, decidesBand, weaker, sharedObservability, unobservedBands, narrowedBands, } from './observability.js';
|
|
19
19
|
export type { Observability } from './observability.js';
|
|
20
20
|
export { deriveVariation } from './derive.js';
|
|
21
21
|
export type { Variation } from './derive.js';
|
|
@@ -23,7 +23,7 @@ export { partingOf } from './parting.js';
|
|
|
23
23
|
export { boundarySnapshot } from './instance.js';
|
|
24
24
|
export { sliceOf, sameTree } from './slice.js';
|
|
25
25
|
export type { PartingSlice } from './slice.js';
|
|
26
|
-
export type { Parting, PartedBoundary, PartingRung, MovedInput, } from './parting.js';
|
|
26
|
+
export type { Parting, PartedBoundary, PartingRung, PartingPlace, MovedInput, } from './parting.js';
|
|
27
27
|
export { explainParting } from './explain.js';
|
|
28
28
|
export { compareValues, fingerprintOfValueDelta } from './value.js';
|
|
29
29
|
export type { ValueChange, ValueDelta } from './value.js';
|
package/dist/compare/index.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
export { diffSnapshots, matchTrees } from './diff/index.js';
|
|
13
13
|
export { impactOf, canReflow, aggregateImpact } from './impact.js';
|
|
14
14
|
export { BANDS, bandOf, loudestBand } from './band.js';
|
|
15
|
-
export { observableBands, decidesBand } from './observability.js';
|
|
15
|
+
export { observableBands, decidesBand, weaker, sharedObservability, unobservedBands, narrowedBands, } from './observability.js';
|
|
16
16
|
export { deriveVariation } from './derive.js';
|
|
17
17
|
export { partingOf } from './parting.js';
|
|
18
18
|
export { boundarySnapshot } from './instance.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/compare/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAU5D,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAInE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEvD,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/compare/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAU5D,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAInE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEvD,OAAO,EACL,eAAe,EACf,WAAW,EACX,MAAM,EACN,mBAAmB,EACnB,eAAe,EACf,aAAa,GACd,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAU/C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC","sourcesContent":["/**\n * `@variance-authority/core/compare` — two snapshots become a list of deltas.\n *\n * Tree matching, the delta vocabulary, the bands a delta falls into, and what a\n * changed property is capable of doing to a layout. No verdict: this group says\n * *what moved*, and refuses to say whether anyone should mind.\n *\n * The split from `judge` is the load-bearing one. A comparison that also decided\n * severity could not be reused by a caller with a different policy, and every\n * team has a different policy.\n */\n\nexport { diffSnapshots, matchTrees } from './diff/index.js';\nexport type {\n SemanticDiff,\n Delta,\n Root,\n RootKind,\n ChangedComponent,\n Matching,\n} from './diff/index.js';\n\nexport { impactOf, canReflow, aggregateImpact } from './impact.js';\nexport type { PropertyImpact, AggregateImpact } from './impact.js';\n\nexport type { Band, DeltaKind } from './band.js';\nexport { BANDS, bandOf, loudestBand } from './band.js';\n\nexport {\n observableBands,\n decidesBand,\n weaker,\n sharedObservability,\n unobservedBands,\n narrowedBands,\n} from './observability.js';\nexport type { Observability } from './observability.js';\n\nexport { deriveVariation } from './derive.js';\nexport type { Variation } from './derive.js';\n\nexport { partingOf } from './parting.js';\nexport { boundarySnapshot } from './instance.js';\nexport { sliceOf, sameTree } from './slice.js';\nexport type { PartingSlice } from './slice.js';\nexport type {\n Parting,\n PartedBoundary,\n PartingRung,\n PartingPlace,\n MovedInput,\n} from './parting.js';\n\nexport { explainParting } from './explain.js';\n\nexport { compareValues, fingerprintOfValueDelta } from './value.js';\nexport type { ValueChange, ValueDelta } from './value.js';\n"]}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* settled is the comparison layer's question.
|
|
10
10
|
*/
|
|
11
11
|
import type { ObservationProfile } from '../format/profile.js';
|
|
12
|
-
import type
|
|
12
|
+
import { type Band } from './band.js';
|
|
13
13
|
/** How completely a profile can decide one band. */
|
|
14
14
|
export type Observability = 'full' | 'structural-only' | 'declared-only' | 'none';
|
|
15
15
|
/**
|
|
@@ -46,4 +46,28 @@ export declare function observableBands(profile: ObservationProfile): Record<Ban
|
|
|
46
46
|
* narrower one than a resolved cascade.
|
|
47
47
|
*/
|
|
48
48
|
export declare function decidesBand(band: Band, level: Observability): boolean;
|
|
49
|
+
export declare function weaker(a: Observability, b: Observability): Observability;
|
|
50
|
+
/** The weaker of two profiles' readings, band by band. */
|
|
51
|
+
export declare function sharedObservability(a: ObservationProfile, b: ObservationProfile): Record<Band, Observability>;
|
|
52
|
+
/**
|
|
53
|
+
* Bands a comparison could not decide at all.
|
|
54
|
+
*
|
|
55
|
+
* Derived from {@link sharedObservability} rather than computed beside it: a band
|
|
56
|
+
* is unobserved exactly when the weaker of the two levels does not decide it, and
|
|
57
|
+
* two expressions of one rule are two things that can disagree.
|
|
58
|
+
*/
|
|
59
|
+
export declare function unobservedBands(levels: Readonly<Record<Band, Observability>>): readonly Band[];
|
|
60
|
+
/**
|
|
61
|
+
* Bands that were decided, but on less than the evidence the band is made of.
|
|
62
|
+
*
|
|
63
|
+
* The half of observability that has no verdict to hide behind. `unobserved` is
|
|
64
|
+
* loud — a band with no answer is reported as having none — while a band decided
|
|
65
|
+
* `declared-only` returns `unchanged`, in the same word a resolved cascade
|
|
66
|
+
* returns, over a narrower question: JSDOM compares what an author wrote, so
|
|
67
|
+
* `padding: 1rem` stays `1rem` and a root font-size that moved underneath it is
|
|
68
|
+
* not in the comparison. That is a real answer and the reason the level decides
|
|
69
|
+
* its band at all — but it is not the answer the word `unchanged` promises, and
|
|
70
|
+
* a reader given the word without the level cannot tell which one they have.
|
|
71
|
+
*/
|
|
72
|
+
export declare function narrowedBands(levels: Readonly<Record<Band, Observability>>): readonly Band[];
|
|
49
73
|
//# sourceMappingURL=observability.d.ts.map
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* describes a collector's capability; mapping that capability onto what can be
|
|
9
9
|
* settled is the comparison layer's question.
|
|
10
10
|
*/
|
|
11
|
+
import { BANDS } from './band.js';
|
|
11
12
|
/**
|
|
12
13
|
* Bands a profile is able to decide. A band this returns `none` for can never
|
|
13
14
|
* resolve to `unchanged` under that profile — it resolves to `unobserved`.
|
|
@@ -54,4 +55,57 @@ export function observableBands(profile) {
|
|
|
54
55
|
export function decidesBand(band, level) {
|
|
55
56
|
return band === 'geometry' ? level === 'full' : level !== 'none';
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* How completely a band was decided when two profiles both had to decide it.
|
|
60
|
+
*
|
|
61
|
+
* The weaker reading wins, which is the same union rule `unobserved` is built
|
|
62
|
+
* from carried one value further. A pair is only as well observed as its blinder
|
|
63
|
+
* side, and reporting the better side's level would describe a reading nobody
|
|
64
|
+
* took.
|
|
65
|
+
*
|
|
66
|
+
* The two partial levels never meet: `structural-only` is a geometry answer and
|
|
67
|
+
* `declared-only` a token one, and a band produces at most one of them. So the
|
|
68
|
+
* ranking needs no tie-break between them, and a tie is two spellings of one
|
|
69
|
+
* level.
|
|
70
|
+
*/
|
|
71
|
+
const RANK = {
|
|
72
|
+
none: 0,
|
|
73
|
+
'structural-only': 1,
|
|
74
|
+
'declared-only': 1,
|
|
75
|
+
full: 2,
|
|
76
|
+
};
|
|
77
|
+
export function weaker(a, b) {
|
|
78
|
+
return RANK[a] <= RANK[b] ? a : b;
|
|
79
|
+
}
|
|
80
|
+
/** The weaker of two profiles' readings, band by band. */
|
|
81
|
+
export function sharedObservability(a, b) {
|
|
82
|
+
const here = observableBands(a);
|
|
83
|
+
const there = observableBands(b);
|
|
84
|
+
return Object.fromEntries(BANDS.map((band) => [band, weaker(here[band], there[band])]));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Bands a comparison could not decide at all.
|
|
88
|
+
*
|
|
89
|
+
* Derived from {@link sharedObservability} rather than computed beside it: a band
|
|
90
|
+
* is unobserved exactly when the weaker of the two levels does not decide it, and
|
|
91
|
+
* two expressions of one rule are two things that can disagree.
|
|
92
|
+
*/
|
|
93
|
+
export function unobservedBands(levels) {
|
|
94
|
+
return BANDS.filter((band) => !decidesBand(band, levels[band]));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Bands that were decided, but on less than the evidence the band is made of.
|
|
98
|
+
*
|
|
99
|
+
* The half of observability that has no verdict to hide behind. `unobserved` is
|
|
100
|
+
* loud — a band with no answer is reported as having none — while a band decided
|
|
101
|
+
* `declared-only` returns `unchanged`, in the same word a resolved cascade
|
|
102
|
+
* returns, over a narrower question: JSDOM compares what an author wrote, so
|
|
103
|
+
* `padding: 1rem` stays `1rem` and a root font-size that moved underneath it is
|
|
104
|
+
* not in the comparison. That is a real answer and the reason the level decides
|
|
105
|
+
* its band at all — but it is not the answer the word `unchanged` promises, and
|
|
106
|
+
* a reader given the word without the level cannot tell which one they have.
|
|
107
|
+
*/
|
|
108
|
+
export function narrowedBands(levels) {
|
|
109
|
+
return BANDS.filter((band) => decidesBand(band, levels[band]) && levels[band] !== 'full');
|
|
110
|
+
}
|
|
57
111
|
//# sourceMappingURL=observability.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observability.js","sourceRoot":"","sources":["../../src/compare/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"observability.js","sourceRoot":"","sources":["../../src/compare/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,KAAK,EAAa,MAAM,WAAW,CAAC;AAK7C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,eAAe,CAAC,OAA2B;IACzD,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;QACxC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,MAAM;QACjF,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,MAAM;QACxF,0EAA0E;QAC1E,gDAAgD;QAChD,OAAO,EAAE,MAAM;QACf,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;KAC1C,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,IAAU,EAAE,KAAoB;IAC1D,OAAO,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC;AACnE,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,IAAI,GAA4C;IACpD,IAAI,EAAE,CAAC;IACP,iBAAiB,EAAE,CAAC;IACpB,eAAe,EAAE,CAAC;IAClB,IAAI,EAAE,CAAC;CACR,CAAC;AAEF,MAAM,UAAU,MAAM,CAAC,CAAgB,EAAE,CAAgB;IACvD,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,mBAAmB,CACjC,CAAqB,EACrB,CAAqB;IAErB,MAAM,IAAI,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;IAEjC,OAAO,MAAM,CAAC,WAAW,CACvB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAC9B,CAAC;AACnC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,MAA6C;IAC3E,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,MAA6C;IACzE,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,CAAC;AAC5F,CAAC","sourcesContent":["/**\n * What a profile can decide, band by band.\n *\n * This lives in `compare` rather than beside `ObservationProfile` in `format`\n * because it is a statement about *bands*, and a band classifies a delta. Core's\n * entrypoints run format → rules → compare → attribute → judge → plan, and a\n * `format` module importing `Band` would point that arrow backwards. The profile\n * describes a collector's capability; mapping that capability onto what can be\n * settled is the comparison layer's question.\n */\n\nimport type { ObservationProfile } from '../format/profile.js';\nimport { BANDS, type Band } from './band.js';\n\n/** How completely a profile can decide one band. */\nexport type Observability = 'full' | 'structural-only' | 'declared-only' | 'none';\n\n/**\n * Bands a profile is able to decide. A band this returns `none` for can never\n * resolve to `unchanged` under that profile — it resolves to `unobserved`.\n *\n * JSDOM sees structural geometry (nodes appearing, disappearing, reordering)\n * because that is tree shape, but not metric geometry (moves, resizes) because\n * that requires an engine. `geometry` is therefore only partially observable,\n * and partial observation of a blocking band is reported, never assumed away.\n *\n * The two bands that need no engine are worth stating rather than leaving to be\n * inferred: **`a11y` and `content` are fully decidable in the unit-test\n * process.** A role, an accessible name, an ARIA state and a text node are facts\n * about a document, so a dropped `aria-label` and a changed string are settled at\n * the cheapest tier this project has — no browser, no screenshot, no baseline\n * image. That is the concrete form of `docs/comparison.md` §3.3: the category no\n * raster comparison can reach is also the category that costs least to reach.\n *\n * The return type is `Record<Band, …>` on purpose. Declaring a new band and\n * forgetting to say whether a profile can observe it is a compile error, not a\n * silent `undefined` that a truthiness check reads as \"no\".\n */\nexport function observableBands(profile: ObservationProfile): Record<Band, Observability> {\n return {\n a11y: profile.ariaTree ? 'full' : 'none',\n geometry: profile.layout ? 'full' : profile.ariaTree ? 'structural-only' : 'none',\n token: profile.computedStyle ? 'full' : profile.declaredStyle ? 'declared-only' : 'none',\n // Text is the one band no collector can fail at: having a document is the\n // precondition for producing a snapshot at all.\n content: 'full',\n texture: profile.raster ? 'full' : 'none',\n };\n}\n\n/**\n * Whether a level is enough to settle its band, or whether the band must be\n * reported `unobserved`.\n *\n * The asymmetry between `geometry` and everything else is ADR-0002 and is\n * deliberate. `structural-only` geometry is *not* enough: a profile that sees\n * tree shape but not metrics would report a box that moved as `unchanged`, which\n * is the one failure this product must never produce. `declared-only` style *is*\n * enough — an author's declaration is a real answer about the token band, just a\n * narrower one than a resolved cascade.\n */\nexport function decidesBand(band: Band, level: Observability): boolean {\n return band === 'geometry' ? level === 'full' : level !== 'none';\n}\n\n/**\n * How completely a band was decided when two profiles both had to decide it.\n *\n * The weaker reading wins, which is the same union rule `unobserved` is built\n * from carried one value further. A pair is only as well observed as its blinder\n * side, and reporting the better side's level would describe a reading nobody\n * took.\n *\n * The two partial levels never meet: `structural-only` is a geometry answer and\n * `declared-only` a token one, and a band produces at most one of them. So the\n * ranking needs no tie-break between them, and a tie is two spellings of one\n * level.\n */\nconst RANK: Readonly<Record<Observability, number>> = {\n none: 0,\n 'structural-only': 1,\n 'declared-only': 1,\n full: 2,\n};\n\nexport function weaker(a: Observability, b: Observability): Observability {\n return RANK[a] <= RANK[b] ? a : b;\n}\n\n/** The weaker of two profiles' readings, band by band. */\nexport function sharedObservability(\n a: ObservationProfile,\n b: ObservationProfile,\n): Record<Band, Observability> {\n const here = observableBands(a);\n const there = observableBands(b);\n\n return Object.fromEntries(\n BANDS.map((band) => [band, weaker(here[band], there[band])]),\n ) as Record<Band, Observability>;\n}\n\n/**\n * Bands a comparison could not decide at all.\n *\n * Derived from {@link sharedObservability} rather than computed beside it: a band\n * is unobserved exactly when the weaker of the two levels does not decide it, and\n * two expressions of one rule are two things that can disagree.\n */\nexport function unobservedBands(levels: Readonly<Record<Band, Observability>>): readonly Band[] {\n return BANDS.filter((band) => !decidesBand(band, levels[band]));\n}\n\n/**\n * Bands that were decided, but on less than the evidence the band is made of.\n *\n * The half of observability that has no verdict to hide behind. `unobserved` is\n * loud — a band with no answer is reported as having none — while a band decided\n * `declared-only` returns `unchanged`, in the same word a resolved cascade\n * returns, over a narrower question: JSDOM compares what an author wrote, so\n * `padding: 1rem` stays `1rem` and a root font-size that moved underneath it is\n * not in the comparison. That is a real answer and the reason the level decides\n * its band at all — but it is not the answer the word `unchanged` promises, and\n * a reader given the word without the level cannot tell which one they have.\n */\nexport function narrowedBands(levels: Readonly<Record<Band, Observability>>): readonly Band[] {\n return BANDS.filter((band) => decidesBand(band, levels[band]) && levels[band] !== 'full');\n}\n"]}
|
|
@@ -28,7 +28,8 @@ import { type PartingSlice } from './slice.js';
|
|
|
28
28
|
* change. A flake is the case where nothing readable moved at all
|
|
29
29
|
* ({@link PartingRung} `undetermined`), and it is only nameable because
|
|
30
30
|
* `unread` is a rung of its own: an accusation of nondeterminism may only be
|
|
31
|
-
* made about inputs somebody actually read (ADR-0002).
|
|
31
|
+
* made about inputs somebody actually read (ADR-0002). It is also only nameable
|
|
32
|
+
* when the two readings were taken at one address — see {@link PartingPlace}.
|
|
32
33
|
*
|
|
33
34
|
* Not `attribute/composition.ts`'s `Divergence`, which is the same suspicion
|
|
34
35
|
* arrived at from the other side: one component rendering two ways from one
|
|
@@ -150,8 +151,29 @@ export type PartingRung =
|
|
|
150
151
|
| 'unread'
|
|
151
152
|
/** Every input was read, every input agreed, and the output moved anyway. */
|
|
152
153
|
| 'undetermined'
|
|
154
|
+
/**
|
|
155
|
+
* The same, between two readings taken in different places. Where a component
|
|
156
|
+
* sits is decided by the boxes around it and is handed to it as nothing, so
|
|
157
|
+
* two instances that agreed on every input and landed apart have contradicted
|
|
158
|
+
* nothing. {@link PartingPlace} is how a caller says which it is asking.
|
|
159
|
+
*/
|
|
160
|
+
| 'placed'
|
|
153
161
|
/** The boundary exists on one side only. */
|
|
154
162
|
| 'unpaired';
|
|
163
|
+
/**
|
|
164
|
+
* Whether two readings were taken at one address.
|
|
165
|
+
*
|
|
166
|
+
* `same` is a subject read twice — across two revisions, or across two moments
|
|
167
|
+
* of one scenario. There, an output that moved with every input holding is the
|
|
168
|
+
* accusation this system is careful about: the reading is not repeatable.
|
|
169
|
+
*
|
|
170
|
+
* `elsewhere` is two instances lifted out of two subjects at one commit, which
|
|
171
|
+
* is `attribute/divergence.ts`' whole shape. The same evidence means something
|
|
172
|
+
* different there: position, and everything a measured box decides downstream of
|
|
173
|
+
* it, is a function of the context an instance was mounted in, not of the props
|
|
174
|
+
* it received. Calling that nondeterminism blames a component for its page.
|
|
175
|
+
*/
|
|
176
|
+
export type PartingPlace = 'same' | 'elsewhere';
|
|
155
177
|
export interface MovedInput {
|
|
156
178
|
readonly kind: 'prop' | 'context' | 'hook' | 'inherited';
|
|
157
179
|
/**
|
|
@@ -175,5 +197,5 @@ export interface MovedInput {
|
|
|
175
197
|
* mismatch, for `compareTrees`' stated reason: this produces an *explanation*,
|
|
176
198
|
* not a verdict, and two arms of an experiment are two subjects on purpose.
|
|
177
199
|
*/
|
|
178
|
-
export declare function partingOf(baseline: SemanticSnapshot, candidate: SemanticSnapshot): Parting;
|
|
200
|
+
export declare function partingOf(baseline: SemanticSnapshot, candidate: SemanticSnapshot, place?: PartingPlace): Parting;
|
|
179
201
|
//# sourceMappingURL=parting.d.ts.map
|