@variance-authority/report 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.
Files changed (53) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +258 -0
  4. package/dist/changelog-message.d.ts +51 -0
  5. package/dist/changelog-message.js +244 -0
  6. package/dist/changelog-message.js.map +1 -0
  7. package/dist/changelog.d.ts +231 -0
  8. package/dist/changelog.js +96 -0
  9. package/dist/changelog.js.map +1 -0
  10. package/dist/cluster.d.ts +112 -0
  11. package/dist/cluster.js +109 -0
  12. package/dist/cluster.js.map +1 -0
  13. package/dist/composition.d.ts +248 -0
  14. package/dist/composition.js +33 -0
  15. package/dist/composition.js.map +1 -0
  16. package/dist/declarations.d.ts +266 -0
  17. package/dist/declarations.js +212 -0
  18. package/dist/declarations.js.map +1 -0
  19. package/dist/file.d.ts +28 -0
  20. package/dist/file.js +151 -0
  21. package/dist/file.js.map +1 -0
  22. package/dist/finding-record.d.ts +64 -0
  23. package/dist/finding-record.js +14 -0
  24. package/dist/finding-record.js.map +1 -0
  25. package/dist/findings.d.ts +157 -0
  26. package/dist/findings.js +227 -0
  27. package/dist/findings.js.map +1 -0
  28. package/dist/format.d.ts +444 -0
  29. package/dist/format.js +2 -0
  30. package/dist/format.js.map +1 -0
  31. package/dist/history-records.d.ts +112 -0
  32. package/dist/history-records.js +16 -0
  33. package/dist/history-records.js.map +1 -0
  34. package/dist/index.d.ts +45 -0
  35. package/dist/index.js +35 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/intent.d.ts +168 -0
  38. package/dist/intent.js +214 -0
  39. package/dist/intent.js.map +1 -0
  40. package/dist/presentation-record.d.ts +66 -0
  41. package/dist/presentation-record.js +9 -0
  42. package/dist/presentation-record.js.map +1 -0
  43. package/dist/promotion.d.ts +86 -0
  44. package/dist/promotion.js +104 -0
  45. package/dist/promotion.js.map +1 -0
  46. package/dist/reach.d.ts +154 -0
  47. package/dist/reach.js +47 -0
  48. package/dist/reach.js.map +1 -0
  49. package/dist/variation.d.ts +58 -0
  50. package/dist/variation.js +2 -0
  51. package/dist/variation.js.map +1 -0
  52. package/mark.svg +30 -0
  53. package/package.json +48 -0
@@ -0,0 +1,86 @@
1
+ import type { ObservationRecord } from './format.js';
2
+ /**
3
+ * Whether a promotion would land, decided before anything is promoted.
4
+ *
5
+ * `accept` used to be the only thing that knew these rules, and it knew them by
6
+ * running: the answer to *what would this update record* was the update. That is
7
+ * fine for a person at a terminal and useless to anything asking beforehand — an
8
+ * agent proposing a command, a reader previewing a commit message — because the
9
+ * only way to find out was to do it.
10
+ *
11
+ * So the rules are a function over one observation and the command applies them
12
+ * rather than owning them. What that buys is the property the whole changelog
13
+ * argument rests on: a preview and the acceptance it previews cannot disagree,
14
+ * because there is one set of rules and both read it. A second copy that drifted
15
+ * would produce the worst available artifact here — a confident account of a
16
+ * baseline update that did not happen.
17
+ *
18
+ * Every refusal is the sentence `accept` prints, unchanged. The wording is part
19
+ * of the rule: it names what to do next, and an agent that gets a different
20
+ * sentence from the preview than from the command has to decide which one is the
21
+ * product.
22
+ */
23
+ /**
24
+ * What would happen to this subject, and why.
25
+ *
26
+ * Three outcomes rather than a boolean, because *already the baseline* is not a
27
+ * refusal and printing it as one turns the ordinary case — the two hundred and
28
+ * ninety-eight subjects a run did not change — into a wall of failures. It is
29
+ * also not a promotion: nothing is written, and a record that counted it would
30
+ * describe a baseline update nobody made.
31
+ */
32
+ export type Promotion = {
33
+ readonly kind: 'promotable';
34
+ /**
35
+ * The candidate image that would become the baseline, as the report named
36
+ * it.
37
+ *
38
+ * Carried on the answer rather than looked up again by the caller. A
39
+ * command that re-read `images.after` after being told a subject is
40
+ * promotable would be asking a second time whether the run left an image,
41
+ * and two readings of one fact are two answers waiting to differ.
42
+ */
43
+ readonly from: string;
44
+ } | {
45
+ readonly kind: 'already-baseline';
46
+ readonly because: string;
47
+ } | {
48
+ readonly kind: 'refused';
49
+ readonly because: string;
50
+ };
51
+ /**
52
+ * The rules, in the order they are asked.
53
+ *
54
+ * The order is load-bearing in one place: instability is asked *before* the
55
+ * verdict. A subject that read two ways and settled on `unchanged` is not a
56
+ * subject that did not change — `--flakes` reached the same baseline twice and
57
+ * got two answers — and answering "already the baseline" there hides the
58
+ * diagnostic that makes this refusable at all.
59
+ */
60
+ export declare function promotionOf(observation: ObservationRecord): Promotion;
61
+ /**
62
+ * Subjects a set of shapes can and cannot settle.
63
+ *
64
+ * `whole` is the safe bulk: every difference this subject has carries one of the
65
+ * named shapes, so promoting the shape here promotes nothing else. `partial` is
66
+ * the refusal that makes the bulk safe — the shape is present, something the
67
+ * shape does not name also moved, and accepting it there would baseline a
68
+ * difference nobody reviewed.
69
+ *
70
+ * A subject with no region at all is in neither list. It is not evidence about
71
+ * these shapes in either direction.
72
+ */
73
+ export declare function selectByShape(observations: readonly ObservationRecord[], shapes: ReadonlySet<string>): {
74
+ whole: readonly ObservationRecord[];
75
+ partial: readonly ObservationRecord[];
76
+ };
77
+ /**
78
+ * Why a shape present in this subject cannot settle it.
79
+ *
80
+ * Two different facts wearing one outcome, and they send a reader to different
81
+ * places: the run recorded another difference, or the run stopped recording. The
82
+ * second is not evidence that something else moved — it is the absence of
83
+ * evidence that nothing did, which is why it refuses in exactly the same way.
84
+ */
85
+ export declare function whyNotWhole(observation: ObservationRecord): string;
86
+ //# sourceMappingURL=promotion.d.ts.map
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The rules, in the order they are asked.
3
+ *
4
+ * The order is load-bearing in one place: instability is asked *before* the
5
+ * verdict. A subject that read two ways and settled on `unchanged` is not a
6
+ * subject that did not change — `--flakes` reached the same baseline twice and
7
+ * got two answers — and answering "already the baseline" there hides the
8
+ * diagnostic that makes this refusable at all.
9
+ */
10
+ export function promotionOf(observation) {
11
+ if (observation.unstable !== undefined && observation.unstable.absorbed === undefined) {
12
+ return {
13
+ kind: 'refused',
14
+ because: `${observation.unstable.because}. Accepting it would promote one of two readings ` +
15
+ 'as the baseline; fix what moves between them, or re-run once it is fixed',
16
+ };
17
+ }
18
+ if (observation.verdict === 'unchanged') {
19
+ return {
20
+ kind: 'already-baseline',
21
+ because: 'it did not change, so it already is the baseline; nothing to accept',
22
+ };
23
+ }
24
+ if (observation.alone?.reproduced === false) {
25
+ return {
26
+ kind: 'refused',
27
+ because: `${observation.alone.because}. Accepting it would make the leak the baseline; ` +
28
+ 'fix the subject that writes the shared state, or re-run once it is fixed',
29
+ };
30
+ }
31
+ const after = observation.images?.after;
32
+ if (after === undefined) {
33
+ return { kind: 'refused', because: noImage(observation) };
34
+ }
35
+ return { kind: 'promotable', from: after };
36
+ }
37
+ /**
38
+ * Subjects a set of shapes can and cannot settle.
39
+ *
40
+ * `whole` is the safe bulk: every difference this subject has carries one of the
41
+ * named shapes, so promoting the shape here promotes nothing else. `partial` is
42
+ * the refusal that makes the bulk safe — the shape is present, something the
43
+ * shape does not name also moved, and accepting it there would baseline a
44
+ * difference nobody reviewed.
45
+ *
46
+ * A subject with no region at all is in neither list. It is not evidence about
47
+ * these shapes in either direction.
48
+ */
49
+ export function selectByShape(observations, shapes) {
50
+ const whole = [];
51
+ const partial = [];
52
+ for (const observation of observations) {
53
+ if (observation.regions.length === 0)
54
+ continue;
55
+ const hits = observation.regions.filter((region) => region.fingerprint !== undefined && shapes.has(region.fingerprint));
56
+ if (hits.length === 0)
57
+ continue;
58
+ // A capped list is not a complete one. Regions the run found and chose not
59
+ // to record could be anything, so a subject whose evidence was truncated
60
+ // cannot support "this shape is the entire change" however its recorded
61
+ // regions look.
62
+ const capped = observation.truncated !== undefined && observation.truncated.regions > 0;
63
+ if (!capped && hits.length === observation.regions.length)
64
+ whole.push(observation);
65
+ else
66
+ partial.push(observation);
67
+ }
68
+ return { whole, partial };
69
+ }
70
+ /**
71
+ * Why a shape present in this subject cannot settle it.
72
+ *
73
+ * Two different facts wearing one outcome, and they send a reader to different
74
+ * places: the run recorded another difference, or the run stopped recording. The
75
+ * second is not evidence that something else moved — it is the absence of
76
+ * evidence that nothing did, which is why it refuses in exactly the same way.
77
+ */
78
+ export function whyNotWhole(observation) {
79
+ return observation.truncated !== undefined && observation.truncated.regions > 0
80
+ ? `this shape is present, but the run capped its region list ` +
81
+ `(${String(observation.truncated.regions)} more, ${String(observation.truncated.pixels)}px), so ` +
82
+ 'there is no evidence it is the whole change; accept this subject by name'
83
+ : 'this shape is present and something else changed too, so accepting it here ' +
84
+ 'would baseline that as well; accept this subject by name once you have read it';
85
+ }
86
+ /**
87
+ * Why a subject has no image, phrased around what to do next.
88
+ *
89
+ * The two causes are opposite and a single message would serve neither. A
90
+ * settlement means nothing was rendered *because nothing needed to be*; an
91
+ * incomparable baseline means the comparison was refused, and the fix is a
92
+ * decision about machines rather than about this subject.
93
+ */
94
+ function noImage(observation) {
95
+ if (observation.verdict === 'incomparable') {
96
+ return ('its baseline belongs to another machine, so the run refused to compare and produced ' +
97
+ 'no image. Delete that baseline and re-run here to record one for this machine, or ' +
98
+ 'run where the baseline was written — accepting across identities is the failure the ' +
99
+ 'partition exists to prevent');
100
+ }
101
+ return ('the run recorded no image for it. Re-run on this machine so the candidate exists; ' +
102
+ 'this command promotes an image that was already reviewed and never renders one');
103
+ }
104
+ //# sourceMappingURL=promotion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promotion.js","sourceRoot":"","sources":["../src/promotion.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ObservationRecord } from './format.js';\n\n/**\n * Whether a promotion would land, decided before anything is promoted.\n *\n * `accept` used to be the only thing that knew these rules, and it knew them by\n * running: the answer to *what would this update record* was the update. That is\n * fine for a person at a terminal and useless to anything asking beforehand — an\n * agent proposing a command, a reader previewing a commit message — because the\n * only way to find out was to do it.\n *\n * So the rules are a function over one observation and the command applies them\n * rather than owning them. What that buys is the property the whole changelog\n * argument rests on: a preview and the acceptance it previews cannot disagree,\n * because there is one set of rules and both read it. A second copy that drifted\n * would produce the worst available artifact here — a confident account of a\n * baseline update that did not happen.\n *\n * Every refusal is the sentence `accept` prints, unchanged. The wording is part\n * of the rule: it names what to do next, and an agent that gets a different\n * sentence from the preview than from the command has to decide which one is the\n * product.\n */\n\n/**\n * What would happen to this subject, and why.\n *\n * Three outcomes rather than a boolean, because *already the baseline* is not a\n * refusal and printing it as one turns the ordinary case — the two hundred and\n * ninety-eight subjects a run did not change — into a wall of failures. It is\n * also not a promotion: nothing is written, and a record that counted it would\n * describe a baseline update nobody made.\n */\nexport type Promotion =\n | {\n readonly kind: 'promotable';\n /**\n * The candidate image that would become the baseline, as the report named\n * it.\n *\n * Carried on the answer rather than looked up again by the caller. A\n * command that re-read `images.after` after being told a subject is\n * promotable would be asking a second time whether the run left an image,\n * and two readings of one fact are two answers waiting to differ.\n */\n readonly from: string;\n }\n | { readonly kind: 'already-baseline'; readonly because: string }\n | { readonly kind: 'refused'; readonly because: string };\n\n/**\n * The rules, in the order they are asked.\n *\n * The order is load-bearing in one place: instability is asked *before* the\n * verdict. A subject that read two ways and settled on `unchanged` is not a\n * subject that did not change — `--flakes` reached the same baseline twice and\n * got two answers — and answering \"already the baseline\" there hides the\n * diagnostic that makes this refusable at all.\n */\nexport function promotionOf(observation: ObservationRecord): Promotion {\n if (observation.unstable !== undefined && observation.unstable.absorbed === undefined) {\n return {\n kind: 'refused',\n because:\n `${observation.unstable.because}. Accepting it would promote one of two readings ` +\n 'as the baseline; fix what moves between them, or re-run once it is fixed',\n };\n }\n\n if (observation.verdict === 'unchanged') {\n return {\n kind: 'already-baseline',\n because: 'it did not change, so it already is the baseline; nothing to accept',\n };\n }\n\n if (observation.alone?.reproduced === false) {\n return {\n kind: 'refused',\n because:\n `${observation.alone.because}. Accepting it would make the leak the baseline; ` +\n 'fix the subject that writes the shared state, or re-run once it is fixed',\n };\n }\n\n const after = observation.images?.after;\n if (after === undefined) {\n return { kind: 'refused', because: noImage(observation) };\n }\n\n return { kind: 'promotable', from: after };\n}\n\n/**\n * Subjects a set of shapes can and cannot settle.\n *\n * `whole` is the safe bulk: every difference this subject has carries one of the\n * named shapes, so promoting the shape here promotes nothing else. `partial` is\n * the refusal that makes the bulk safe — the shape is present, something the\n * shape does not name also moved, and accepting it there would baseline a\n * difference nobody reviewed.\n *\n * A subject with no region at all is in neither list. It is not evidence about\n * these shapes in either direction.\n */\nexport function selectByShape(\n observations: readonly ObservationRecord[],\n shapes: ReadonlySet<string>,\n): { whole: readonly ObservationRecord[]; partial: readonly ObservationRecord[] } {\n const whole: ObservationRecord[] = [];\n const partial: ObservationRecord[] = [];\n\n for (const observation of observations) {\n if (observation.regions.length === 0) continue;\n\n const hits = observation.regions.filter(\n (region) => region.fingerprint !== undefined && shapes.has(region.fingerprint),\n );\n if (hits.length === 0) continue;\n\n // A capped list is not a complete one. Regions the run found and chose not\n // to record could be anything, so a subject whose evidence was truncated\n // cannot support \"this shape is the entire change\" however its recorded\n // regions look.\n const capped = observation.truncated !== undefined && observation.truncated.regions > 0;\n\n if (!capped && hits.length === observation.regions.length) whole.push(observation);\n else partial.push(observation);\n }\n\n return { whole, partial };\n}\n\n/**\n * Why a shape present in this subject cannot settle it.\n *\n * Two different facts wearing one outcome, and they send a reader to different\n * places: the run recorded another difference, or the run stopped recording. The\n * second is not evidence that something else moved — it is the absence of\n * evidence that nothing did, which is why it refuses in exactly the same way.\n */\nexport function whyNotWhole(observation: ObservationRecord): string {\n return observation.truncated !== undefined && observation.truncated.regions > 0\n ? `this shape is present, but the run capped its region list ` +\n `(${String(observation.truncated.regions)} more, ${String(observation.truncated.pixels)}px), so ` +\n 'there is no evidence it is the whole change; accept this subject by name'\n : 'this shape is present and something else changed too, so accepting it here ' +\n 'would baseline that as well; accept this subject by name once you have read it';\n}\n\n/**\n * Why a subject has no image, phrased around what to do next.\n *\n * The two causes are opposite and a single message would serve neither. A\n * settlement means nothing was rendered *because nothing needed to be*; an\n * incomparable baseline means the comparison was refused, and the fix is a\n * decision about machines rather than about this subject.\n */\nfunction noImage(observation: ObservationRecord): string {\n if (observation.verdict === 'incomparable') {\n return (\n 'its baseline belongs to another machine, so the run refused to compare and produced ' +\n 'no image. Delete that baseline and re-run here to record one for this machine, or ' +\n 'run where the baseline was written — accepting across identities is the failure the ' +\n 'partition exists to prevent'\n );\n }\n return (\n 'the run recorded no image for it. Re-run on this machine so the candidate exists; ' +\n 'this command promotes an image that was already reviewed and never renders one'\n );\n}\n"]}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * What the commit reaches, joined to what the run saw.
3
+ *
4
+ * Every other section of a report is an answer about *pixels*: this subject
5
+ * against its baseline, this suite against itself. This one is the answer about
6
+ * *cause*, and it is built from three things no diff tool holds at once — the
7
+ * paths git named, the file graph a scanner read off disk, and the component
8
+ * list each baseline recorded when it was painted.
9
+ *
10
+ * Walking the graph backwards from every changed file gives the set of
11
+ * components an edit can possibly have moved, and the trail that carried it:
12
+ * `app/src/tokens.css → app/src/components/ui/button.css → Button`. A subject
13
+ * renders components, so the same walk says, per subject, whether this commit
14
+ * could have touched it.
15
+ *
16
+ * ## The reason this is worth an artifact rather than a log line
17
+ *
18
+ * The same computation already exists one directory over as `--since`, where it
19
+ * is used to *skip* subjects and then discarded. Skipping is a saving; this is
20
+ * the finding. Crossed against the verdicts the run produced, reach sorts every
21
+ * subject into four states, and two of them are questions nobody else in this
22
+ * category can even ask:
23
+ *
24
+ * - **reached and changed** — the expected case, and the trail names the file.
25
+ * - **reached and unchanged** — the edit is inert here. No diff tool asserts
26
+ * anything about a green subject, so the surface an author *believed* they were
27
+ * changing and did not is invisible everywhere else.
28
+ * - **unreached and changed** — nothing in this commit reaches this subject, and
29
+ * it moved anyway. That is a flake, an input from outside the repository, or a
30
+ * hole in the graph, and it is a different bug in each case.
31
+ * - **unreached and unchanged** — counted, never listed.
32
+ *
33
+ * The states are not stored. They are `reached × verdict`, and the verdicts are
34
+ * already in the report; writing them down as a fifth field would be a second
35
+ * copy of a fact that can disagree with the first.
36
+ *
37
+ * ## Every refusal here is louder than an answer
38
+ *
39
+ * A graph that cannot place a changed file cannot say what that file reaches,
40
+ * and the difference between *this edit reaches nothing* and *this edit was not
41
+ * understood* is the difference between a finding and a lie. So `subjects` is
42
+ * absent rather than empty when the attribution could not be made, and `whole`
43
+ * carries the reason in the same words the selector would have used to refuse to
44
+ * narrow.
45
+ */
46
+ /** A component the diff reaches, and the chain that carried it there. */
47
+ export interface ReachedComponent {
48
+ readonly component: string;
49
+ /**
50
+ * The shortest chain from a seed to this component: seed first, component
51
+ * last, every step a repository-relative path but the last.
52
+ *
53
+ * Shortest because the traversal is breadth-first, so the explanation printed
54
+ * is the shortest true one rather than whichever a stack happened to unwind.
55
+ */
56
+ readonly trail: readonly string[];
57
+ /**
58
+ * Present when the chain does not begin at a file this diff named.
59
+ *
60
+ * The traversal seeds every file whose own imports could not be read, because
61
+ * an unreadable file may import the one that changed. A component reached only
62
+ * through such a file is reached by the *scan's blind spot*, not by the edit,
63
+ * and a trail that opened with a path nobody touched would read as an
64
+ * attribution.
65
+ */
66
+ readonly throughUnread?: string;
67
+ }
68
+ /** Whether this commit could have moved this subject, and through what. */
69
+ export interface SubjectReach {
70
+ readonly reached: boolean;
71
+ /**
72
+ * Components this subject's baseline records that the diff reaches, sorted.
73
+ *
74
+ * Empty exactly when `reached` is `false`.
75
+ */
76
+ readonly through: readonly string[];
77
+ /** The trail to the first of them. Absent when nothing was reached. */
78
+ readonly trail?: readonly string[];
79
+ /** One sentence, ready to print, either way it went. */
80
+ readonly because: string;
81
+ }
82
+ /** A file the graph holds but could not read the edges of. */
83
+ export interface ReachHole {
84
+ readonly file: string;
85
+ readonly because?: string;
86
+ }
87
+ /**
88
+ * What the commit reaches, as the run recorded it.
89
+ *
90
+ * The half of a comparison no comparison can produce. A diff says which files an
91
+ * author touched; the import graph says which components those files reach; the
92
+ * suite says which subjects render those components. Put together they answer the
93
+ * question a reviewer actually has in front of a changed screenshot — *did I do
94
+ * this?* — and the answer is occasionally **no**, which is the strongest signal on
95
+ * the page and one nothing that only compares pixels can ever emit.
96
+ *
97
+ * Every field is written so the report can say *unknown* as loudly as it says
98
+ * *yes* and *no*. `whole` is the walk's refusal to attribute, `unscanned` the
99
+ * files it could not parse, `opaque` the edges it could not follow, and
100
+ * `subjects` is absent rather than empty when none of it could be attributed —
101
+ * because a commit that was understood and reaches nothing supports the opposite
102
+ * decision to a commit nothing could be read from.
103
+ */
104
+ export interface ReachReport {
105
+ /** The ref the diff was taken against, in the operator's own words. */
106
+ readonly against: string;
107
+ /** Repository-relative paths the diff named. */
108
+ readonly changed: readonly string[];
109
+ /**
110
+ * Components the diff reaches, sorted by name.
111
+ *
112
+ * The whole answer, independent of any subject — a component here that appears
113
+ * in no subject's `through` is a component this suite does not watch, which is
114
+ * a coverage question rather than a review one.
115
+ */
116
+ readonly components: readonly ReachedComponent[];
117
+ /**
118
+ * Per planned subject, keyed by id. **Absent is not empty.**
119
+ *
120
+ * Absent means nothing here could be attributed and `whole` says why; every
121
+ * subject must then be read as possibly reached. Empty would mean the opposite
122
+ * — that the commit was understood and reaches none of them — and those two
123
+ * support opposite decisions.
124
+ *
125
+ * A subject whose baseline recorded no component list is simply not a key: the
126
+ * run does not know what it is made of, so it cannot say what reaches it.
127
+ */
128
+ readonly subjects?: Readonly<Record<string, SubjectReach>>;
129
+ /**
130
+ * Why nothing could be attributed, when nothing could.
131
+ *
132
+ * Present exactly when `subjects` is absent. A changed file under the scanned
133
+ * roots that the graph never saw, a diff entirely outside the graph, a diff
134
+ * that reaches no component at all: each of those looks identical to an edit
135
+ * that genuinely affects nothing, and only one of them is safe to act on.
136
+ */
137
+ readonly whole?: string;
138
+ /**
139
+ * Changed files under the scanned roots the graph does not hold.
140
+ *
141
+ * A gap in the scan rather than a file that affects nothing — the roots are the
142
+ * operator's own statement of where renders come from.
143
+ */
144
+ readonly unscanned?: readonly string[];
145
+ /**
146
+ * Files in the graph whose own edges could not be read, with the reason.
147
+ *
148
+ * Every one of them is traversed as though it changed. Reported because a
149
+ * reader who can see them can go and fix the scan; a reader who cannot sees an
150
+ * attribution that quietly covers more than the diff.
151
+ */
152
+ readonly opaque?: readonly ReachHole[];
153
+ }
154
+ //# sourceMappingURL=reach.d.ts.map
package/dist/reach.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * What the commit reaches, joined to what the run saw.
3
+ *
4
+ * Every other section of a report is an answer about *pixels*: this subject
5
+ * against its baseline, this suite against itself. This one is the answer about
6
+ * *cause*, and it is built from three things no diff tool holds at once — the
7
+ * paths git named, the file graph a scanner read off disk, and the component
8
+ * list each baseline recorded when it was painted.
9
+ *
10
+ * Walking the graph backwards from every changed file gives the set of
11
+ * components an edit can possibly have moved, and the trail that carried it:
12
+ * `app/src/tokens.css → app/src/components/ui/button.css → Button`. A subject
13
+ * renders components, so the same walk says, per subject, whether this commit
14
+ * could have touched it.
15
+ *
16
+ * ## The reason this is worth an artifact rather than a log line
17
+ *
18
+ * The same computation already exists one directory over as `--since`, where it
19
+ * is used to *skip* subjects and then discarded. Skipping is a saving; this is
20
+ * the finding. Crossed against the verdicts the run produced, reach sorts every
21
+ * subject into four states, and two of them are questions nobody else in this
22
+ * category can even ask:
23
+ *
24
+ * - **reached and changed** — the expected case, and the trail names the file.
25
+ * - **reached and unchanged** — the edit is inert here. No diff tool asserts
26
+ * anything about a green subject, so the surface an author *believed* they were
27
+ * changing and did not is invisible everywhere else.
28
+ * - **unreached and changed** — nothing in this commit reaches this subject, and
29
+ * it moved anyway. That is a flake, an input from outside the repository, or a
30
+ * hole in the graph, and it is a different bug in each case.
31
+ * - **unreached and unchanged** — counted, never listed.
32
+ *
33
+ * The states are not stored. They are `reached × verdict`, and the verdicts are
34
+ * already in the report; writing them down as a fifth field would be a second
35
+ * copy of a fact that can disagree with the first.
36
+ *
37
+ * ## Every refusal here is louder than an answer
38
+ *
39
+ * A graph that cannot place a changed file cannot say what that file reaches,
40
+ * and the difference between *this edit reaches nothing* and *this edit was not
41
+ * understood* is the difference between a finding and a lie. So `subjects` is
42
+ * absent rather than empty when the attribution could not be made, and `whole`
43
+ * carries the reason in the same words the selector would have used to refuse to
44
+ * narrow.
45
+ */
46
+ export {};
47
+ //# sourceMappingURL=reach.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reach.js","sourceRoot":"","sources":["../src/reach.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG","sourcesContent":["/**\n * What the commit reaches, joined to what the run saw.\n *\n * Every other section of a report is an answer about *pixels*: this subject\n * against its baseline, this suite against itself. This one is the answer about\n * *cause*, and it is built from three things no diff tool holds at once — the\n * paths git named, the file graph a scanner read off disk, and the component\n * list each baseline recorded when it was painted.\n *\n * Walking the graph backwards from every changed file gives the set of\n * components an edit can possibly have moved, and the trail that carried it:\n * `app/src/tokens.css → app/src/components/ui/button.css → Button`. A subject\n * renders components, so the same walk says, per subject, whether this commit\n * could have touched it.\n *\n * ## The reason this is worth an artifact rather than a log line\n *\n * The same computation already exists one directory over as `--since`, where it\n * is used to *skip* subjects and then discarded. Skipping is a saving; this is\n * the finding. Crossed against the verdicts the run produced, reach sorts every\n * subject into four states, and two of them are questions nobody else in this\n * category can even ask:\n *\n * - **reached and changed** — the expected case, and the trail names the file.\n * - **reached and unchanged** — the edit is inert here. No diff tool asserts\n * anything about a green subject, so the surface an author *believed* they were\n * changing and did not is invisible everywhere else.\n * - **unreached and changed** — nothing in this commit reaches this subject, and\n * it moved anyway. That is a flake, an input from outside the repository, or a\n * hole in the graph, and it is a different bug in each case.\n * - **unreached and unchanged** — counted, never listed.\n *\n * The states are not stored. They are `reached × verdict`, and the verdicts are\n * already in the report; writing them down as a fifth field would be a second\n * copy of a fact that can disagree with the first.\n *\n * ## Every refusal here is louder than an answer\n *\n * A graph that cannot place a changed file cannot say what that file reaches,\n * and the difference between *this edit reaches nothing* and *this edit was not\n * understood* is the difference between a finding and a lie. So `subjects` is\n * absent rather than empty when the attribution could not be made, and `whole`\n * carries the reason in the same words the selector would have used to refuse to\n * narrow.\n */\n\n/** A component the diff reaches, and the chain that carried it there. */\nexport interface ReachedComponent {\n readonly component: string;\n\n /**\n * The shortest chain from a seed to this component: seed first, component\n * last, every step a repository-relative path but the last.\n *\n * Shortest because the traversal is breadth-first, so the explanation printed\n * is the shortest true one rather than whichever a stack happened to unwind.\n */\n readonly trail: readonly string[];\n\n /**\n * Present when the chain does not begin at a file this diff named.\n *\n * The traversal seeds every file whose own imports could not be read, because\n * an unreadable file may import the one that changed. A component reached only\n * through such a file is reached by the *scan's blind spot*, not by the edit,\n * and a trail that opened with a path nobody touched would read as an\n * attribution.\n */\n readonly throughUnread?: string;\n}\n\n/** Whether this commit could have moved this subject, and through what. */\nexport interface SubjectReach {\n readonly reached: boolean;\n\n /**\n * Components this subject's baseline records that the diff reaches, sorted.\n *\n * Empty exactly when `reached` is `false`.\n */\n readonly through: readonly string[];\n\n /** The trail to the first of them. Absent when nothing was reached. */\n readonly trail?: readonly string[];\n\n /** One sentence, ready to print, either way it went. */\n readonly because: string;\n}\n\n/** A file the graph holds but could not read the edges of. */\nexport interface ReachHole {\n readonly file: string;\n readonly because?: string;\n}\n\n/**\n * What the commit reaches, as the run recorded it.\n *\n * The half of a comparison no comparison can produce. A diff says which files an\n * author touched; the import graph says which components those files reach; the\n * suite says which subjects render those components. Put together they answer the\n * question a reviewer actually has in front of a changed screenshot — *did I do\n * this?* — and the answer is occasionally **no**, which is the strongest signal on\n * the page and one nothing that only compares pixels can ever emit.\n *\n * Every field is written so the report can say *unknown* as loudly as it says\n * *yes* and *no*. `whole` is the walk's refusal to attribute, `unscanned` the\n * files it could not parse, `opaque` the edges it could not follow, and\n * `subjects` is absent rather than empty when none of it could be attributed —\n * because a commit that was understood and reaches nothing supports the opposite\n * decision to a commit nothing could be read from.\n */\nexport interface ReachReport {\n /** The ref the diff was taken against, in the operator's own words. */\n readonly against: string;\n\n /** Repository-relative paths the diff named. */\n readonly changed: readonly string[];\n\n /**\n * Components the diff reaches, sorted by name.\n *\n * The whole answer, independent of any subject — a component here that appears\n * in no subject's `through` is a component this suite does not watch, which is\n * a coverage question rather than a review one.\n */\n readonly components: readonly ReachedComponent[];\n\n /**\n * Per planned subject, keyed by id. **Absent is not empty.**\n *\n * Absent means nothing here could be attributed and `whole` says why; every\n * subject must then be read as possibly reached. Empty would mean the opposite\n * — that the commit was understood and reaches none of them — and those two\n * support opposite decisions.\n *\n * A subject whose baseline recorded no component list is simply not a key: the\n * run does not know what it is made of, so it cannot say what reaches it.\n */\n readonly subjects?: Readonly<Record<string, SubjectReach>>;\n\n /**\n * Why nothing could be attributed, when nothing could.\n *\n * Present exactly when `subjects` is absent. A changed file under the scanned\n * roots that the graph never saw, a diff entirely outside the graph, a diff\n * that reaches no component at all: each of those looks identical to an edit\n * that genuinely affects nothing, and only one of them is safe to act on.\n */\n readonly whole?: string;\n\n /**\n * Changed files under the scanned roots the graph does not hold.\n *\n * A gap in the scan rather than a file that affects nothing — the roots are the\n * operator's own statement of where renders come from.\n */\n readonly unscanned?: readonly string[];\n\n /**\n * Files in the graph whose own edges could not be read, with the reason.\n *\n * Every one of them is traversed as though it changed. Reported because a\n * reader who can see them can go and fix the scan; a reader who cannot sees an\n * attribution that quietly covers more than the diff.\n */\n readonly opaque?: readonly ReachHole[];\n}\n"]}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * One subject read against the subject it declares itself a variation of.
3
+ *
4
+ * A flattened `Variation` from `@variance-authority/core`, for the reason
5
+ * {@link ChurnRecord} is a copy rather than an import: a report reader must not
6
+ * have to install the comparison engine to open a file. The deltas themselves
7
+ * are not here — a report is read by something that wants a sentence, and the
8
+ * bands, the components and the digest are what a sentence is made of.
9
+ *
10
+ * **This is never a verdict.** A variation is a difference somebody declared on
11
+ * purpose; reporting it as a regression would be reporting a subject for
12
+ * existing. Nothing here reaches `exitFor`, `accept` or the baseline store.
13
+ */
14
+ export interface VariationRecord {
15
+ readonly subject: string;
16
+ /**
17
+ * The parent's subject id, once it resolved to a subject in this run.
18
+ *
19
+ * Absent when the declaration named something the run does not have — which is
20
+ * a state with its own sentence in `because`, and not the same claim as a
21
+ * variation that was compared and found identical.
22
+ */
23
+ readonly parent?: string;
24
+ /** `true` when both sides render to one hash: the variation changes nothing. */
25
+ readonly identical?: boolean;
26
+ /** Bands the difference falls in, in band order. Present with the comparison. */
27
+ readonly bands?: readonly string[];
28
+ /** Bands neither side's profile could decide. Absent is not "none". */
29
+ readonly unobserved?: readonly string[];
30
+ /** Components the difference was attributed to, causes first. */
31
+ readonly components?: readonly string[];
32
+ /**
33
+ * The identity of the difference itself, `variation/v1` over its deltas.
34
+ *
35
+ * Stable across a change that moved both subjects the same way, which is the
36
+ * whole reason a linked pair is worth more than two independent baselines: it
37
+ * distinguishes *this flag now does something else* from *everything moved*.
38
+ */
39
+ readonly digest?: string;
40
+ /**
41
+ * How the pair came to be a pair.
42
+ *
43
+ * `declared` is a tag somebody wrote. `named` is an inference from the ids: a
44
+ * subject whose name extends another subject's name is that subject plus an
45
+ * axis, which is how a suite that already encodes its variants in its names
46
+ * gets this for free — and is correct exactly as often as the naming
47
+ * convention is kept.
48
+ *
49
+ * On the record rather than derived from the presence of a tag, because the
50
+ * report is read by things that never saw the plan, and *a person said so* and
51
+ * *a name implied it* carry different weight in every sentence built from
52
+ * this.
53
+ */
54
+ readonly how?: 'declared' | 'named';
55
+ /** One sentence, ready to print, saying what this variation is or why it is not. */
56
+ readonly because: string;
57
+ }
58
+ //# sourceMappingURL=variation.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=variation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variation.js","sourceRoot":"","sources":["../src/variation.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * One subject read against the subject it declares itself a variation of.\n *\n * A flattened `Variation` from `@variance-authority/core`, for the reason\n * {@link ChurnRecord} is a copy rather than an import: a report reader must not\n * have to install the comparison engine to open a file. The deltas themselves\n * are not here — a report is read by something that wants a sentence, and the\n * bands, the components and the digest are what a sentence is made of.\n *\n * **This is never a verdict.** A variation is a difference somebody declared on\n * purpose; reporting it as a regression would be reporting a subject for\n * existing. Nothing here reaches `exitFor`, `accept` or the baseline store.\n */\nexport interface VariationRecord {\n readonly subject: string;\n\n /**\n * The parent's subject id, once it resolved to a subject in this run.\n *\n * Absent when the declaration named something the run does not have — which is\n * a state with its own sentence in `because`, and not the same claim as a\n * variation that was compared and found identical.\n */\n readonly parent?: string;\n\n /** `true` when both sides render to one hash: the variation changes nothing. */\n readonly identical?: boolean;\n\n /** Bands the difference falls in, in band order. Present with the comparison. */\n readonly bands?: readonly string[];\n\n /** Bands neither side's profile could decide. Absent is not \"none\". */\n readonly unobserved?: readonly string[];\n\n /** Components the difference was attributed to, causes first. */\n readonly components?: readonly string[];\n\n /**\n * The identity of the difference itself, `variation/v1` over its deltas.\n *\n * Stable across a change that moved both subjects the same way, which is the\n * whole reason a linked pair is worth more than two independent baselines: it\n * distinguishes *this flag now does something else* from *everything moved*.\n */\n readonly digest?: string;\n\n /**\n * How the pair came to be a pair.\n *\n * `declared` is a tag somebody wrote. `named` is an inference from the ids: a\n * subject whose name extends another subject's name is that subject plus an\n * axis, which is how a suite that already encodes its variants in its names\n * gets this for free — and is correct exactly as often as the naming\n * convention is kept.\n *\n * On the record rather than derived from the presence of a tag, because the\n * report is read by things that never saw the plan, and *a person said so* and\n * *a name implied it* carry different weight in every sentence built from\n * this.\n */\n readonly how?: 'declared' | 'named';\n\n /** One sentence, ready to print, saying what this variation is or why it is not. */\n readonly because: string;\n}\n"]}
package/mark.svg ADDED
@@ -0,0 +1,30 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 320" role="img" aria-labelledby="title desc">
2
+ <title id="title">Variance Authority mark</title>
3
+ <desc id="desc">Folded ribbon VA mark in charcoal and orange.</desc>
4
+
5
+ <defs>
6
+ <style>
7
+ .ribbon-dark {
8
+ fill: #24282A;
9
+ }
10
+
11
+ @media (prefers-color-scheme: dark) {
12
+ .ribbon-dark {
13
+ fill: #F3F4F6;
14
+ }
15
+ }
16
+ </style>
17
+ </defs>
18
+
19
+ <!-- Left descending ribbon -->
20
+ <path class="ribbon-dark" d="M64 54H159L256 266H160Z"/>
21
+
22
+ <!-- Right descending ribbon -->
23
+ <path class="ribbon-dark" d="M331 28H419L494 266H397Z"/>
24
+
25
+ <!-- Chosen / variant ribbon -->
26
+ <path d="M256 266L202 152L283 28H376L301 165Z" fill="#FF4A19"/>
27
+
28
+ <!-- Fold shadow -->
29
+ <path d="M202 152L256 266L301 165L264 103Z" fill="#D83A13" opacity="0.72"/>
30
+ </svg>
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@variance-authority/report",
3
+ "version": "0.1.0",
4
+ "description": "The shape a Variance Authority run leaves behind, so a person, a pull request and an agent read one format.",
5
+ "keywords": [
6
+ "variance-authority",
7
+ "report",
8
+ "json",
9
+ "html-report",
10
+ "ci"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Machine Garden",
14
+ "homepage": "https://variance-authority.dev#packages",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Variance-Authority/variance-authority.git",
18
+ "directory": "packages/report"
19
+ },
20
+ "bugs": "https://github.com/Variance-Authority/variance-authority/issues",
21
+ "engines": {
22
+ "node": ">=22"
23
+ },
24
+ "type": "module",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./file": {
31
+ "types": "./dist/file.d.ts",
32
+ "default": "./dist/file.js"
33
+ }
34
+ },
35
+ "main": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "files": [
38
+ "dist",
39
+ "!dist/**/*.d.ts.map",
40
+ "mark.svg"
41
+ ],
42
+ "scripts": {
43
+ "build": "tsc --build"
44
+ },
45
+ "dependencies": {
46
+ "@variance-authority/core": "^0.1.0"
47
+ }
48
+ }