@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,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
@@ -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"]}