@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.
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/changelog-message.d.ts +51 -0
- package/dist/changelog-message.js +244 -0
- package/dist/changelog-message.js.map +1 -0
- package/dist/changelog.d.ts +231 -0
- package/dist/changelog.js +96 -0
- package/dist/changelog.js.map +1 -0
- package/dist/cluster.d.ts +112 -0
- package/dist/cluster.js +109 -0
- package/dist/cluster.js.map +1 -0
- package/dist/composition.d.ts +248 -0
- package/dist/composition.js +33 -0
- package/dist/composition.js.map +1 -0
- package/dist/declarations.d.ts +266 -0
- package/dist/declarations.js +212 -0
- package/dist/declarations.js.map +1 -0
- package/dist/file.d.ts +28 -0
- package/dist/file.js +151 -0
- package/dist/file.js.map +1 -0
- package/dist/finding-record.d.ts +64 -0
- package/dist/finding-record.js +14 -0
- package/dist/finding-record.js.map +1 -0
- package/dist/findings.d.ts +157 -0
- package/dist/findings.js +227 -0
- package/dist/findings.js.map +1 -0
- package/dist/format.d.ts +444 -0
- package/dist/format.js +2 -0
- package/dist/format.js.map +1 -0
- package/dist/history-records.d.ts +112 -0
- package/dist/history-records.js +16 -0
- package/dist/history-records.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/intent.d.ts +168 -0
- package/dist/intent.js +214 -0
- package/dist/intent.js.map +1 -0
- package/dist/presentation-record.d.ts +66 -0
- package/dist/presentation-record.js +9 -0
- package/dist/presentation-record.js.map +1 -0
- package/dist/promotion.d.ts +86 -0
- package/dist/promotion.js +104 -0
- package/dist/promotion.js.map +1 -0
- package/dist/reach.d.ts +154 -0
- package/dist/reach.js +47 -0
- package/dist/reach.js.map +1 -0
- package/dist/variation.d.ts +58 -0
- package/dist/variation.js +2 -0
- package/dist/variation.js.map +1 -0
- package/mark.svg +30 -0
- package/package.json +48 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a list of defects is labelled, and how it is dated — once, for every surface.
|
|
3
|
+
*
|
|
4
|
+
* Two questions a reader asks of a findings panel, and until this file neither
|
|
5
|
+
* had an answer anywhere:
|
|
6
|
+
*
|
|
7
|
+
* **What kind of report is this?** The rows are rule slugs and clauses —
|
|
8
|
+
* `nested-interactive`, `label-mismatch` — under a heading that says `Findings`
|
|
9
|
+
* or, on the HTML report, nothing at all. A reader who does not already know what
|
|
10
|
+
* the inspector checks has to infer the subject of the report from the names of
|
|
11
|
+
* its rules. The honest label is not one word: nine of the eleven rules band
|
|
12
|
+
* `a11y` and two do not, so a panel headed *Accessibility* would be wrong about
|
|
13
|
+
* the box that overflows its container and the string that was never translated.
|
|
14
|
+
* The band is already on the record and is the axis a project blocks on
|
|
15
|
+
* (`blocking: ['a11y']`), so the heading a reader sees and the policy that stops
|
|
16
|
+
* their merge are the same word by construction.
|
|
17
|
+
*
|
|
18
|
+
* **Is this mine?** A finding is read from one render with no baseline consulted,
|
|
19
|
+
* which is the half a comparison structurally cannot produce — and the cost is
|
|
20
|
+
* that the same list prints on the run that introduced a defect and on the two
|
|
21
|
+
* hundred runs after it. `FindingRecord.standing` carries the answer when a
|
|
22
|
+
* baseline recorded one; the rules for reading it absent are the whole reason
|
|
23
|
+
* this is a shared fold rather than a ternary at each call site. Absent is *not
|
|
24
|
+
* dated*, never *new*: a surface that guessed would announce every inherited
|
|
25
|
+
* defect in a suite as freshly introduced, on the first run after an upgrade, to
|
|
26
|
+
* the person least equipped to check.
|
|
27
|
+
*
|
|
28
|
+
* **Which of them is this reviewer's problem right now?** The two above are
|
|
29
|
+
* readings; this one is an order, and it is the one the surfaces got wrong for
|
|
30
|
+
* longest. A panel that opens on a total and then counts the dates as equal
|
|
31
|
+
* clauses is telling somebody halfway through a button restyle about twenty-two
|
|
32
|
+
* defects they inherited and cannot fix today, before it tells them about the
|
|
33
|
+
* twenty they just caused. [`byArrival`](#byArrival) is the split, and every
|
|
34
|
+
* surface leads with `arrived` and folds the rest behind a count.
|
|
35
|
+
*
|
|
36
|
+
* One fold, many renderers. Five surfaces print these sentences — an HTML report,
|
|
37
|
+
* a review page and three terminal answers — and the part that has to be
|
|
38
|
+
* identical between them is the reading, not the markup.
|
|
39
|
+
*/
|
|
40
|
+
import type { FindingRecord } from './finding-record.js';
|
|
41
|
+
/**
|
|
42
|
+
* The heading for a band, or the one honest heading for a record without one.
|
|
43
|
+
*
|
|
44
|
+
* `Unclassified` rather than a guess. A report written before the band was
|
|
45
|
+
* carried has none, and filing those under `Accessibility` would put the two
|
|
46
|
+
* rules that are not accessibility under a heading that says they are — which is
|
|
47
|
+
* the mislabelling this whole file exists to end, arriving through the default.
|
|
48
|
+
*/
|
|
49
|
+
export declare function bandTitle(band: string | undefined): string;
|
|
50
|
+
/** Whether the baseline carried this defect too, as one of three states. */
|
|
51
|
+
export type Age = 'standing' | 'new' | 'undated';
|
|
52
|
+
/**
|
|
53
|
+
* Which of the three a record is, with absent kept out of the other two.
|
|
54
|
+
*
|
|
55
|
+
* The whole reading is the `undefined` branch. `standing` is only ever set from
|
|
56
|
+
* evidence — the render's document was byte-for-byte the baseline's, or the
|
|
57
|
+
* baseline recorded the marks to cross against — so a record without it is one
|
|
58
|
+
* nothing dated, not one dated *no*. Every surface goes through here rather than
|
|
59
|
+
* writing `finding.standing === true ? … : …`, because that ternary is correct
|
|
60
|
+
* on the two states it names and silently wrong on the third, and it is the
|
|
61
|
+
* third that arrives on the first run after an upgrade.
|
|
62
|
+
*/
|
|
63
|
+
export declare function ageOf(finding: FindingRecord): Age;
|
|
64
|
+
/** The phrase that goes on the row. Short: it sits beside a component and a file. */
|
|
65
|
+
export declare const AGE_WORDS: Readonly<Record<Age, string>>;
|
|
66
|
+
/** The same three states at length, for a `title` a reader can hover for the why. */
|
|
67
|
+
export declare const AGE_WHY: Readonly<Record<Age, string>>;
|
|
68
|
+
/** One band's worth of defects, in the order the bands are reported. */
|
|
69
|
+
export interface Grouped {
|
|
70
|
+
readonly band: string | undefined;
|
|
71
|
+
readonly title: string;
|
|
72
|
+
readonly findings: readonly FindingRecord[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The defects, grouped under the heading each one belongs to.
|
|
76
|
+
*
|
|
77
|
+
* Loudest band first, from core's own order, so the accessibility defects lead a
|
|
78
|
+
* mixed list rather than sorting alphabetically under `Content`. A band nothing
|
|
79
|
+
* in this render fired is not an empty group — it is not a group, because a
|
|
80
|
+
* heading over nothing reads as a category that was checked and found clean, and
|
|
81
|
+
* these rules do not check bands, they check rules.
|
|
82
|
+
*/
|
|
83
|
+
export declare function byBand(findings: readonly FindingRecord[]): readonly Grouped[];
|
|
84
|
+
/** Two piles and the one fact that says whether the split may be acted on. */
|
|
85
|
+
export interface Arrival {
|
|
86
|
+
/** Recorded as absent from the baseline: this change is what put them here. */
|
|
87
|
+
readonly arrived: readonly FindingRecord[];
|
|
88
|
+
/** Everything the record does not put on this change — carried, or never dated. */
|
|
89
|
+
readonly rest: readonly FindingRecord[];
|
|
90
|
+
/** Whether anything in the list carries a date, which is what makes the split a claim. */
|
|
91
|
+
readonly dated: boolean;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The defects, split by whether the record puts them on this change.
|
|
95
|
+
*
|
|
96
|
+
* The one axis a reviewer mid-change is actually working along, and until this
|
|
97
|
+
* function no surface ordered by it. Every panel led with a total and the method
|
|
98
|
+
* — *42 defects read from this render, with no baseline compared* — and then
|
|
99
|
+
* counted the two dates as equal clauses, which hands somebody reviewing a
|
|
100
|
+
* button restyle twenty-two inherited defects in the same breath as the twenty
|
|
101
|
+
* they just caused. They cannot act on the twenty-two today. Printing them first
|
|
102
|
+
* is how a panel teaches a reviewer to stop reading it.
|
|
103
|
+
*
|
|
104
|
+
* Two piles, and `undated` is in neither by name. It goes with the rest because
|
|
105
|
+
* the rest is defined by what it is *not* — not recorded as arriving with this
|
|
106
|
+
* change — which is exactly true of a finding nothing dated, and stays true when
|
|
107
|
+
* the record improves. Calling that pile *inherited* would be the guess this
|
|
108
|
+
* whole axis exists to refuse.
|
|
109
|
+
*
|
|
110
|
+
* {@link Arrival.dated} is what decides whether a surface may fold at all. When
|
|
111
|
+
* nothing carries a date, `arrived` is empty and `rest` is the whole list, and a
|
|
112
|
+
* renderer that folded on that would hide every defect in the render behind a
|
|
113
|
+
* summary — on exactly the run where there is no baseline to have inherited them
|
|
114
|
+
* from.
|
|
115
|
+
*/
|
|
116
|
+
export declare function byArrival(findings: readonly FindingRecord[]): Arrival;
|
|
117
|
+
/**
|
|
118
|
+
* The sentence that goes first, which is about this change and nothing else.
|
|
119
|
+
*
|
|
120
|
+
* Three answers, and the third is not a count. A run with no dating on it cannot
|
|
121
|
+
* say *nothing arrived* — that is a claim about a baseline nobody read — so it
|
|
122
|
+
* says what it does know, which is that the question has no answer here.
|
|
123
|
+
*/
|
|
124
|
+
export declare function arrivalLine(findings: readonly FindingRecord[]): string;
|
|
125
|
+
/**
|
|
126
|
+
* The rest, counted once, for the summary a surface folds them behind.
|
|
127
|
+
*
|
|
128
|
+
* `undefined` when there is nothing to fold — either the list is all arrivals, or
|
|
129
|
+
* nothing is dated and the split would be an invention. The two states are
|
|
130
|
+
* counted apart even here: *already in the baseline* is a fact about a record
|
|
131
|
+
* somebody wrote, and *the baseline does not account for* is the absence of one,
|
|
132
|
+
* and a reviewer deciding whether to open the fold is entitled to know which.
|
|
133
|
+
*/
|
|
134
|
+
export declare function carriedLine(findings: readonly FindingRecord[]): string | undefined;
|
|
135
|
+
/** Why nothing in the list moved the verdict. Method, so it goes last. */
|
|
136
|
+
export declare function methodLine(findings: readonly FindingRecord[]): string;
|
|
137
|
+
/**
|
|
138
|
+
* Whether the rows in one list disagree about their date.
|
|
139
|
+
*
|
|
140
|
+
* The gate on drawing a date per row, and it is per *list* rather than per
|
|
141
|
+
* report. A column of rows all saying `arrived with this change`, under a heading
|
|
142
|
+
* that already says it, is a word a reader stops seeing — and once a surface
|
|
143
|
+
* splits arrivals from the rest, every list it draws is uniform unless the fold
|
|
144
|
+
* happens to hold both a dated and an undated row. That case is the only one
|
|
145
|
+
* where the row is carrying the finding rather than repeating the heading.
|
|
146
|
+
*/
|
|
147
|
+
export declare function mixedAges(findings: readonly FindingRecord[]): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* The whole opening, for a surface with one line to spend rather than a panel.
|
|
150
|
+
*
|
|
151
|
+
* Same three parts in the same order the panels draw them: what arrived, what was
|
|
152
|
+
* already here, and last the method — which is the sentence that explains why
|
|
153
|
+
* none of this moved the verdict, and which used to be first because it explains
|
|
154
|
+
* the list rather than because anybody needed it before the count.
|
|
155
|
+
*/
|
|
156
|
+
export declare function findingTotals(findings: readonly FindingRecord[]): string;
|
|
157
|
+
//# sourceMappingURL=findings.d.ts.map
|
package/dist/findings.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a list of defects is labelled, and how it is dated — once, for every surface.
|
|
3
|
+
*
|
|
4
|
+
* Two questions a reader asks of a findings panel, and until this file neither
|
|
5
|
+
* had an answer anywhere:
|
|
6
|
+
*
|
|
7
|
+
* **What kind of report is this?** The rows are rule slugs and clauses —
|
|
8
|
+
* `nested-interactive`, `label-mismatch` — under a heading that says `Findings`
|
|
9
|
+
* or, on the HTML report, nothing at all. A reader who does not already know what
|
|
10
|
+
* the inspector checks has to infer the subject of the report from the names of
|
|
11
|
+
* its rules. The honest label is not one word: nine of the eleven rules band
|
|
12
|
+
* `a11y` and two do not, so a panel headed *Accessibility* would be wrong about
|
|
13
|
+
* the box that overflows its container and the string that was never translated.
|
|
14
|
+
* The band is already on the record and is the axis a project blocks on
|
|
15
|
+
* (`blocking: ['a11y']`), so the heading a reader sees and the policy that stops
|
|
16
|
+
* their merge are the same word by construction.
|
|
17
|
+
*
|
|
18
|
+
* **Is this mine?** A finding is read from one render with no baseline consulted,
|
|
19
|
+
* which is the half a comparison structurally cannot produce — and the cost is
|
|
20
|
+
* that the same list prints on the run that introduced a defect and on the two
|
|
21
|
+
* hundred runs after it. `FindingRecord.standing` carries the answer when a
|
|
22
|
+
* baseline recorded one; the rules for reading it absent are the whole reason
|
|
23
|
+
* this is a shared fold rather than a ternary at each call site. Absent is *not
|
|
24
|
+
* dated*, never *new*: a surface that guessed would announce every inherited
|
|
25
|
+
* defect in a suite as freshly introduced, on the first run after an upgrade, to
|
|
26
|
+
* the person least equipped to check.
|
|
27
|
+
*
|
|
28
|
+
* **Which of them is this reviewer's problem right now?** The two above are
|
|
29
|
+
* readings; this one is an order, and it is the one the surfaces got wrong for
|
|
30
|
+
* longest. A panel that opens on a total and then counts the dates as equal
|
|
31
|
+
* clauses is telling somebody halfway through a button restyle about twenty-two
|
|
32
|
+
* defects they inherited and cannot fix today, before it tells them about the
|
|
33
|
+
* twenty they just caused. [`byArrival`](#byArrival) is the split, and every
|
|
34
|
+
* surface leads with `arrived` and folds the rest behind a count.
|
|
35
|
+
*
|
|
36
|
+
* One fold, many renderers. Five surfaces print these sentences — an HTML report,
|
|
37
|
+
* a review page and three terminal answers — and the part that has to be
|
|
38
|
+
* identical between them is the reading, not the markup.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* What a reader is told the row is about, in the order the bands are reported.
|
|
42
|
+
*
|
|
43
|
+
* Typed against core's `Band` rather than reading core's `BANDS` array, which
|
|
44
|
+
* would be the same list at the cost of a runtime edge from this package into the
|
|
45
|
+
* comparison engine — and this module is bundled into a browser page. The type is
|
|
46
|
+
* the coupling that matters: a band added to core fails to compile here until
|
|
47
|
+
* somebody writes the word a reviewer will read, which is the failure worth
|
|
48
|
+
* having. Insertion order is loudest first, matching `BANDS`, so a mixed list
|
|
49
|
+
* leads with the accessibility defects rather than sorting under `Content`.
|
|
50
|
+
*/
|
|
51
|
+
const TITLES = {
|
|
52
|
+
a11y: 'Accessibility',
|
|
53
|
+
geometry: 'Layout',
|
|
54
|
+
token: 'Style',
|
|
55
|
+
content: 'Content',
|
|
56
|
+
texture: 'Rendering',
|
|
57
|
+
};
|
|
58
|
+
/** The reporting order, from the one list that has to name every band anyway. */
|
|
59
|
+
const ORDER = Object.keys(TITLES);
|
|
60
|
+
/**
|
|
61
|
+
* The heading for a band, or the one honest heading for a record without one.
|
|
62
|
+
*
|
|
63
|
+
* `Unclassified` rather than a guess. A report written before the band was
|
|
64
|
+
* carried has none, and filing those under `Accessibility` would put the two
|
|
65
|
+
* rules that are not accessibility under a heading that says they are — which is
|
|
66
|
+
* the mislabelling this whole file exists to end, arriving through the default.
|
|
67
|
+
*/
|
|
68
|
+
export function bandTitle(band) {
|
|
69
|
+
if (band === undefined)
|
|
70
|
+
return 'Unclassified';
|
|
71
|
+
return TITLES[band] ?? band;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Which of the three a record is, with absent kept out of the other two.
|
|
75
|
+
*
|
|
76
|
+
* The whole reading is the `undefined` branch. `standing` is only ever set from
|
|
77
|
+
* evidence — the render's document was byte-for-byte the baseline's, or the
|
|
78
|
+
* baseline recorded the marks to cross against — so a record without it is one
|
|
79
|
+
* nothing dated, not one dated *no*. Every surface goes through here rather than
|
|
80
|
+
* writing `finding.standing === true ? … : …`, because that ternary is correct
|
|
81
|
+
* on the two states it names and silently wrong on the third, and it is the
|
|
82
|
+
* third that arrives on the first run after an upgrade.
|
|
83
|
+
*/
|
|
84
|
+
export function ageOf(finding) {
|
|
85
|
+
if (finding.standing === undefined)
|
|
86
|
+
return 'undated';
|
|
87
|
+
return finding.standing ? 'standing' : 'new';
|
|
88
|
+
}
|
|
89
|
+
/** The phrase that goes on the row. Short: it sits beside a component and a file. */
|
|
90
|
+
export const AGE_WORDS = {
|
|
91
|
+
new: 'arrived with this change',
|
|
92
|
+
standing: 'already in the baseline',
|
|
93
|
+
undated: 'not dated',
|
|
94
|
+
};
|
|
95
|
+
/** The same three states at length, for a `title` a reader can hover for the why. */
|
|
96
|
+
export const AGE_WHY = {
|
|
97
|
+
new: 'The baseline recorded what was found in it, and this was not among it.',
|
|
98
|
+
standing: 'The baseline carried this defect too. Approving this change does not approve ' +
|
|
99
|
+
'the defect, and fixing it is separate work.',
|
|
100
|
+
undated: 'Nothing recorded what the baseline contained — there is no baseline yet, or it ' +
|
|
101
|
+
'was written before findings were kept beside it. This is not a claim that the ' +
|
|
102
|
+
'defect is new.',
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* The defects, grouped under the heading each one belongs to.
|
|
106
|
+
*
|
|
107
|
+
* Loudest band first, from core's own order, so the accessibility defects lead a
|
|
108
|
+
* mixed list rather than sorting alphabetically under `Content`. A band nothing
|
|
109
|
+
* in this render fired is not an empty group — it is not a group, because a
|
|
110
|
+
* heading over nothing reads as a category that was checked and found clean, and
|
|
111
|
+
* these rules do not check bands, they check rules.
|
|
112
|
+
*/
|
|
113
|
+
export function byBand(findings) {
|
|
114
|
+
const groups = new Map();
|
|
115
|
+
for (const finding of findings) {
|
|
116
|
+
const key = finding.band ?? undefined;
|
|
117
|
+
groups.set(key, [...(groups.get(key) ?? []), finding]);
|
|
118
|
+
}
|
|
119
|
+
const known = ORDER.filter((band) => groups.has(band)).map((band) => ({ band, title: bandTitle(band), findings: groups.get(band) ?? [] }));
|
|
120
|
+
const rest = [...groups.entries()]
|
|
121
|
+
.filter(([band]) => band === undefined || !ORDER.includes(band))
|
|
122
|
+
.map(([band, found]) => ({ band, title: bandTitle(band), findings: found }));
|
|
123
|
+
return [...known, ...rest];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The defects, split by whether the record puts them on this change.
|
|
127
|
+
*
|
|
128
|
+
* The one axis a reviewer mid-change is actually working along, and until this
|
|
129
|
+
* function no surface ordered by it. Every panel led with a total and the method
|
|
130
|
+
* — *42 defects read from this render, with no baseline compared* — and then
|
|
131
|
+
* counted the two dates as equal clauses, which hands somebody reviewing a
|
|
132
|
+
* button restyle twenty-two inherited defects in the same breath as the twenty
|
|
133
|
+
* they just caused. They cannot act on the twenty-two today. Printing them first
|
|
134
|
+
* is how a panel teaches a reviewer to stop reading it.
|
|
135
|
+
*
|
|
136
|
+
* Two piles, and `undated` is in neither by name. It goes with the rest because
|
|
137
|
+
* the rest is defined by what it is *not* — not recorded as arriving with this
|
|
138
|
+
* change — which is exactly true of a finding nothing dated, and stays true when
|
|
139
|
+
* the record improves. Calling that pile *inherited* would be the guess this
|
|
140
|
+
* whole axis exists to refuse.
|
|
141
|
+
*
|
|
142
|
+
* {@link Arrival.dated} is what decides whether a surface may fold at all. When
|
|
143
|
+
* nothing carries a date, `arrived` is empty and `rest` is the whole list, and a
|
|
144
|
+
* renderer that folded on that would hide every defect in the render behind a
|
|
145
|
+
* summary — on exactly the run where there is no baseline to have inherited them
|
|
146
|
+
* from.
|
|
147
|
+
*/
|
|
148
|
+
export function byArrival(findings) {
|
|
149
|
+
return {
|
|
150
|
+
arrived: findings.filter((finding) => ageOf(finding) === 'new'),
|
|
151
|
+
rest: findings.filter((finding) => ageOf(finding) !== 'new'),
|
|
152
|
+
dated: findings.some((finding) => ageOf(finding) !== 'undated'),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The sentence that goes first, which is about this change and nothing else.
|
|
157
|
+
*
|
|
158
|
+
* Three answers, and the third is not a count. A run with no dating on it cannot
|
|
159
|
+
* say *nothing arrived* — that is a claim about a baseline nobody read — so it
|
|
160
|
+
* says what it does know, which is that the question has no answer here.
|
|
161
|
+
*/
|
|
162
|
+
export function arrivalLine(findings) {
|
|
163
|
+
const { arrived, dated } = byArrival(findings);
|
|
164
|
+
if (arrived.length > 0)
|
|
165
|
+
return `${plural(arrived.length, 'defect')} arrived with this change`;
|
|
166
|
+
if (dated)
|
|
167
|
+
return 'Nothing here arrived with this change';
|
|
168
|
+
return 'Nothing recorded what the baseline held, so none of them can be dated to this change';
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The rest, counted once, for the summary a surface folds them behind.
|
|
172
|
+
*
|
|
173
|
+
* `undefined` when there is nothing to fold — either the list is all arrivals, or
|
|
174
|
+
* nothing is dated and the split would be an invention. The two states are
|
|
175
|
+
* counted apart even here: *already in the baseline* is a fact about a record
|
|
176
|
+
* somebody wrote, and *the baseline does not account for* is the absence of one,
|
|
177
|
+
* and a reviewer deciding whether to open the fold is entitled to know which.
|
|
178
|
+
*/
|
|
179
|
+
export function carriedLine(findings) {
|
|
180
|
+
const { rest, dated } = byArrival(findings);
|
|
181
|
+
if (!dated || rest.length === 0)
|
|
182
|
+
return undefined;
|
|
183
|
+
const held = rest.filter((finding) => ageOf(finding) === 'standing').length;
|
|
184
|
+
const undated = rest.length - held;
|
|
185
|
+
return [
|
|
186
|
+
held === 0 ? undefined : `${String(held)} already in the baseline`,
|
|
187
|
+
undated === 0 ? undefined : `${String(undated)} the baseline does not account for`,
|
|
188
|
+
]
|
|
189
|
+
.filter((clause) => clause !== undefined)
|
|
190
|
+
.join(', ');
|
|
191
|
+
}
|
|
192
|
+
/** Why nothing in the list moved the verdict. Method, so it goes last. */
|
|
193
|
+
export function methodLine(findings) {
|
|
194
|
+
return `${plural(findings.length, 'defect')} read from this render, with no baseline compared`;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Whether the rows in one list disagree about their date.
|
|
198
|
+
*
|
|
199
|
+
* The gate on drawing a date per row, and it is per *list* rather than per
|
|
200
|
+
* report. A column of rows all saying `arrived with this change`, under a heading
|
|
201
|
+
* that already says it, is a word a reader stops seeing — and once a surface
|
|
202
|
+
* splits arrivals from the rest, every list it draws is uniform unless the fold
|
|
203
|
+
* happens to hold both a dated and an undated row. That case is the only one
|
|
204
|
+
* where the row is carrying the finding rather than repeating the heading.
|
|
205
|
+
*/
|
|
206
|
+
export function mixedAges(findings) {
|
|
207
|
+
return new Set(findings.map(ageOf)).size > 1;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The whole opening, for a surface with one line to spend rather than a panel.
|
|
211
|
+
*
|
|
212
|
+
* Same three parts in the same order the panels draw them: what arrived, what was
|
|
213
|
+
* already here, and last the method — which is the sentence that explains why
|
|
214
|
+
* none of this moved the verdict, and which used to be first because it explains
|
|
215
|
+
* the list rather than because anybody needed it before the count.
|
|
216
|
+
*/
|
|
217
|
+
export function findingTotals(findings) {
|
|
218
|
+
if (findings.length === 0)
|
|
219
|
+
return methodLine(findings);
|
|
220
|
+
return [arrivalLine(findings), carriedLine(findings), methodLine(findings)]
|
|
221
|
+
.filter((part) => part !== undefined)
|
|
222
|
+
.join(' · ');
|
|
223
|
+
}
|
|
224
|
+
function plural(count, noun) {
|
|
225
|
+
return `${String(count)} ${noun}${count === 1 ? '' : 's'}`;
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=findings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"findings.js","sourceRoot":"","sources":["../src/findings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * How a list of defects is labelled, and how it is dated — once, for every surface.\n *\n * Two questions a reader asks of a findings panel, and until this file neither\n * had an answer anywhere:\n *\n * **What kind of report is this?** The rows are rule slugs and clauses —\n * `nested-interactive`, `label-mismatch` — under a heading that says `Findings`\n * or, on the HTML report, nothing at all. A reader who does not already know what\n * the inspector checks has to infer the subject of the report from the names of\n * its rules. The honest label is not one word: nine of the eleven rules band\n * `a11y` and two do not, so a panel headed *Accessibility* would be wrong about\n * the box that overflows its container and the string that was never translated.\n * The band is already on the record and is the axis a project blocks on\n * (`blocking: ['a11y']`), so the heading a reader sees and the policy that stops\n * their merge are the same word by construction.\n *\n * **Is this mine?** A finding is read from one render with no baseline consulted,\n * which is the half a comparison structurally cannot produce — and the cost is\n * that the same list prints on the run that introduced a defect and on the two\n * hundred runs after it. `FindingRecord.standing` carries the answer when a\n * baseline recorded one; the rules for reading it absent are the whole reason\n * this is a shared fold rather than a ternary at each call site. Absent is *not\n * dated*, never *new*: a surface that guessed would announce every inherited\n * defect in a suite as freshly introduced, on the first run after an upgrade, to\n * the person least equipped to check.\n *\n * **Which of them is this reviewer's problem right now?** The two above are\n * readings; this one is an order, and it is the one the surfaces got wrong for\n * longest. A panel that opens on a total and then counts the dates as equal\n * clauses is telling somebody halfway through a button restyle about twenty-two\n * defects they inherited and cannot fix today, before it tells them about the\n * twenty they just caused. [`byArrival`](#byArrival) is the split, and every\n * surface leads with `arrived` and folds the rest behind a count.\n *\n * One fold, many renderers. Five surfaces print these sentences — an HTML report,\n * a review page and three terminal answers — and the part that has to be\n * identical between them is the reading, not the markup.\n */\n\nimport type { Band } from '@variance-authority/core';\nimport type { FindingRecord } from './finding-record.js';\n\n/**\n * What a reader is told the row is about, in the order the bands are reported.\n *\n * Typed against core's `Band` rather than reading core's `BANDS` array, which\n * would be the same list at the cost of a runtime edge from this package into the\n * comparison engine — and this module is bundled into a browser page. The type is\n * the coupling that matters: a band added to core fails to compile here until\n * somebody writes the word a reviewer will read, which is the failure worth\n * having. Insertion order is loudest first, matching `BANDS`, so a mixed list\n * leads with the accessibility defects rather than sorting under `Content`.\n */\nconst TITLES: Readonly<Record<Band, string>> = {\n a11y: 'Accessibility',\n geometry: 'Layout',\n token: 'Style',\n content: 'Content',\n texture: 'Rendering',\n};\n\n/** The reporting order, from the one list that has to name every band anyway. */\nconst ORDER: readonly string[] = Object.keys(TITLES);\n\n/**\n * The heading for a band, or the one honest heading for a record without one.\n *\n * `Unclassified` rather than a guess. A report written before the band was\n * carried has none, and filing those under `Accessibility` would put the two\n * rules that are not accessibility under a heading that says they are — which is\n * the mislabelling this whole file exists to end, arriving through the default.\n */\nexport function bandTitle(band: string | undefined): string {\n if (band === undefined) return 'Unclassified';\n return (TITLES as Readonly<Record<string, string>>)[band] ?? band;\n}\n\n/** Whether the baseline carried this defect too, as one of three states. */\nexport type Age = 'standing' | 'new' | 'undated';\n\n/**\n * Which of the three a record is, with absent kept out of the other two.\n *\n * The whole reading is the `undefined` branch. `standing` is only ever set from\n * evidence — the render's document was byte-for-byte the baseline's, or the\n * baseline recorded the marks to cross against — so a record without it is one\n * nothing dated, not one dated *no*. Every surface goes through here rather than\n * writing `finding.standing === true ? … : …`, because that ternary is correct\n * on the two states it names and silently wrong on the third, and it is the\n * third that arrives on the first run after an upgrade.\n */\nexport function ageOf(finding: FindingRecord): Age {\n if (finding.standing === undefined) return 'undated';\n return finding.standing ? 'standing' : 'new';\n}\n\n/** The phrase that goes on the row. Short: it sits beside a component and a file. */\nexport const AGE_WORDS: Readonly<Record<Age, string>> = {\n new: 'arrived with this change',\n standing: 'already in the baseline',\n undated: 'not dated',\n};\n\n/** The same three states at length, for a `title` a reader can hover for the why. */\nexport const AGE_WHY: Readonly<Record<Age, string>> = {\n new: 'The baseline recorded what was found in it, and this was not among it.',\n standing:\n 'The baseline carried this defect too. Approving this change does not approve ' +\n 'the defect, and fixing it is separate work.',\n undated:\n 'Nothing recorded what the baseline contained — there is no baseline yet, or it ' +\n 'was written before findings were kept beside it. This is not a claim that the ' +\n 'defect is new.',\n};\n\n/** One band's worth of defects, in the order the bands are reported. */\nexport interface Grouped {\n readonly band: string | undefined;\n readonly title: string;\n readonly findings: readonly FindingRecord[];\n}\n\n/**\n * The defects, grouped under the heading each one belongs to.\n *\n * Loudest band first, from core's own order, so the accessibility defects lead a\n * mixed list rather than sorting alphabetically under `Content`. A band nothing\n * in this render fired is not an empty group — it is not a group, because a\n * heading over nothing reads as a category that was checked and found clean, and\n * these rules do not check bands, they check rules.\n */\nexport function byBand(findings: readonly FindingRecord[]): readonly Grouped[] {\n const groups = new Map<string | undefined, FindingRecord[]>();\n for (const finding of findings) {\n const key = finding.band ?? undefined;\n groups.set(key, [...(groups.get(key) ?? []), finding]);\n }\n\n const known = ORDER.filter((band) => groups.has(band)).map(\n (band): Grouped => ({ band, title: bandTitle(band), findings: groups.get(band) ?? [] }),\n );\n const rest = [...groups.entries()]\n .filter(([band]) => band === undefined || !ORDER.includes(band))\n .map(([band, found]): Grouped => ({ band, title: bandTitle(band), findings: found }));\n\n return [...known, ...rest];\n}\n\n/** Two piles and the one fact that says whether the split may be acted on. */\nexport interface Arrival {\n /** Recorded as absent from the baseline: this change is what put them here. */\n readonly arrived: readonly FindingRecord[];\n /** Everything the record does not put on this change — carried, or never dated. */\n readonly rest: readonly FindingRecord[];\n /** Whether anything in the list carries a date, which is what makes the split a claim. */\n readonly dated: boolean;\n}\n\n/**\n * The defects, split by whether the record puts them on this change.\n *\n * The one axis a reviewer mid-change is actually working along, and until this\n * function no surface ordered by it. Every panel led with a total and the method\n * — *42 defects read from this render, with no baseline compared* — and then\n * counted the two dates as equal clauses, which hands somebody reviewing a\n * button restyle twenty-two inherited defects in the same breath as the twenty\n * they just caused. They cannot act on the twenty-two today. Printing them first\n * is how a panel teaches a reviewer to stop reading it.\n *\n * Two piles, and `undated` is in neither by name. It goes with the rest because\n * the rest is defined by what it is *not* — not recorded as arriving with this\n * change — which is exactly true of a finding nothing dated, and stays true when\n * the record improves. Calling that pile *inherited* would be the guess this\n * whole axis exists to refuse.\n *\n * {@link Arrival.dated} is what decides whether a surface may fold at all. When\n * nothing carries a date, `arrived` is empty and `rest` is the whole list, and a\n * renderer that folded on that would hide every defect in the render behind a\n * summary — on exactly the run where there is no baseline to have inherited them\n * from.\n */\nexport function byArrival(findings: readonly FindingRecord[]): Arrival {\n return {\n arrived: findings.filter((finding) => ageOf(finding) === 'new'),\n rest: findings.filter((finding) => ageOf(finding) !== 'new'),\n dated: findings.some((finding) => ageOf(finding) !== 'undated'),\n };\n}\n\n/**\n * The sentence that goes first, which is about this change and nothing else.\n *\n * Three answers, and the third is not a count. A run with no dating on it cannot\n * say *nothing arrived* — that is a claim about a baseline nobody read — so it\n * says what it does know, which is that the question has no answer here.\n */\nexport function arrivalLine(findings: readonly FindingRecord[]): string {\n const { arrived, dated } = byArrival(findings);\n\n if (arrived.length > 0) return `${plural(arrived.length, 'defect')} arrived with this change`;\n if (dated) return 'Nothing here arrived with this change';\n return 'Nothing recorded what the baseline held, so none of them can be dated to this change';\n}\n\n/**\n * The rest, counted once, for the summary a surface folds them behind.\n *\n * `undefined` when there is nothing to fold — either the list is all arrivals, or\n * nothing is dated and the split would be an invention. The two states are\n * counted apart even here: *already in the baseline* is a fact about a record\n * somebody wrote, and *the baseline does not account for* is the absence of one,\n * and a reviewer deciding whether to open the fold is entitled to know which.\n */\nexport function carriedLine(findings: readonly FindingRecord[]): string | undefined {\n const { rest, dated } = byArrival(findings);\n if (!dated || rest.length === 0) return undefined;\n\n const held = rest.filter((finding) => ageOf(finding) === 'standing').length;\n const undated = rest.length - held;\n\n return [\n held === 0 ? undefined : `${String(held)} already in the baseline`,\n undated === 0 ? undefined : `${String(undated)} the baseline does not account for`,\n ]\n .filter((clause): clause is string => clause !== undefined)\n .join(', ');\n}\n\n/** Why nothing in the list moved the verdict. Method, so it goes last. */\nexport function methodLine(findings: readonly FindingRecord[]): string {\n return `${plural(findings.length, 'defect')} read from this render, with no baseline compared`;\n}\n\n/**\n * Whether the rows in one list disagree about their date.\n *\n * The gate on drawing a date per row, and it is per *list* rather than per\n * report. A column of rows all saying `arrived with this change`, under a heading\n * that already says it, is a word a reader stops seeing — and once a surface\n * splits arrivals from the rest, every list it draws is uniform unless the fold\n * happens to hold both a dated and an undated row. That case is the only one\n * where the row is carrying the finding rather than repeating the heading.\n */\nexport function mixedAges(findings: readonly FindingRecord[]): boolean {\n return new Set(findings.map(ageOf)).size > 1;\n}\n\n/**\n * The whole opening, for a surface with one line to spend rather than a panel.\n *\n * Same three parts in the same order the panels draw them: what arrived, what was\n * already here, and last the method — which is the sentence that explains why\n * none of this moved the verdict, and which used to be first because it explains\n * the list rather than because anybody needed it before the count.\n */\nexport function findingTotals(findings: readonly FindingRecord[]): string {\n if (findings.length === 0) return methodLine(findings);\n\n return [arrivalLine(findings), carriedLine(findings), methodLine(findings)]\n .filter((part): part is string => part !== undefined)\n .join(' · ');\n}\n\nfunction plural(count: number, noun: string): string {\n return `${String(count)} ${noun}${count === 1 ? '' : 's'}`;\n}\n"]}
|