@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,212 @@
1
+ /**
2
+ * What the config *declared*, and what each declaration did in this run.
3
+ *
4
+ * The two ledgers a run keeps about its own settings. An ignore says *part of
5
+ * this page is not my subject*; a sensitivity says *assert on this much of it*.
6
+ * Both make a run less observant on purpose, and both are only safe because
7
+ * every run counts what they absorbed — a declaration that stopped being needed
8
+ * is invisible without a count of zero, and a mask that outlives its cause grows
9
+ * quietly over a real regression.
10
+ *
11
+ * They live here rather than with the command that computes them for the reason
12
+ * the rest of this package exists: they have several readers. The CLI folds them,
13
+ * a terminal prints them, an HTML page tabulates them, and a review service
14
+ * stores them so the audit survives the machine the run happened on. A shape
15
+ * owned by the first of those bends towards a terminal.
16
+ *
17
+ * ## One decision, taken once
18
+ *
19
+ * A rule's *state* — spent, mistyped, expired, or simply working — is a reading
20
+ * of the counts, not another count. It is taken by {@link ignoreState} and
21
+ * {@link sensitivityState} and nowhere else, because a second reader that
22
+ * re-derived it would eventually disagree: the first HTML report to try shipped
23
+ * `pixels === 0` as the whole test for *dead*, which calls every rule in a fresh
24
+ * checkout dead, on the run that proves least about any of them.
25
+ */
26
+ /** The single reading of an ignore's counts. Every surface asks this one. */
27
+ export function ignoreState(entry) {
28
+ if (entry.expired)
29
+ return 'expired';
30
+ if (entry.unwornTags.length > 0)
31
+ return 'unworn';
32
+ if (entry.unresolved)
33
+ return 'unresolved';
34
+ if (entry.pixels === 0)
35
+ return entry.comparedIn === 0 ? 'untested' : 'dead';
36
+ return 'live';
37
+ }
38
+ /** The single reading of a sensitivity's counts. */
39
+ export function sensitivityState(entry) {
40
+ if (entry.unscoped)
41
+ return 'unscoped';
42
+ return entry.absorbed.length === 0 ? 'dead' : 'live';
43
+ }
44
+ /**
45
+ * Whether a state names something to do about the config.
46
+ *
47
+ * `untested` is deliberately not one. It is the absence of evidence, and a
48
+ * report that flagged it would ask an operator to act on a run that measured
49
+ * nothing — which is how audits stop being read.
50
+ */
51
+ export function isActionable(state) {
52
+ return state !== 'live' && state !== 'untested';
53
+ }
54
+ /* --- what a surface says about a rule ------------------------------------- */
55
+ /**
56
+ * The sentence behind an ignore's state.
57
+ *
58
+ * Here rather than in a renderer for the reason the state itself is here: an
59
+ * HTML table, a terminal and a review page all have to answer *why is this rule
60
+ * marked* and there is one answer. Two of them writing their own is how a report
61
+ * and a service come to disagree about the same run in front of the same person.
62
+ *
63
+ * The near-miss on `unworn` is the part worth carrying: the likeliest cause of a
64
+ * tag nothing wears is a typo, and naming the tags that *are* worn turns a
65
+ * report into a fix.
66
+ */
67
+ export function ignoreSays(entry, ledger) {
68
+ switch (ignoreState(entry)) {
69
+ case 'expired':
70
+ return 'Past its date. The differences it absorbed are being reported again.';
71
+ case 'unworn': {
72
+ const worn = ledger.vocabulary.length === 0
73
+ ? ''
74
+ : ` Tags worn in this run: ${ledger.vocabulary.join(', ')}.`;
75
+ return (`No subject in this run wears ${entry.unwornTags.join(', ')}, ` +
76
+ `so the rule applied nowhere.${worn}`);
77
+ }
78
+ case 'unresolved':
79
+ return ('Its selector matched nothing in any subject. Either it is no longer needed, or it ' +
80
+ 'stopped matching and something you believe is silenced is being reported.');
81
+ case 'untested':
82
+ return (`It excluded a subtree in ${String(entry.subjects)} subject(s), none of which was ` +
83
+ 'compared this run. Nothing here says whether it is still needed.');
84
+ case 'dead':
85
+ return (`It excluded a subtree in ${String(entry.subjects)} subject(s), ` +
86
+ `${String(entry.comparedIn)} of them compared, and absorbed nothing.`);
87
+ default:
88
+ return `Absorbed ${String(entry.pixels)} pixel(s) across ${String(entry.subjects)} subject(s).`;
89
+ }
90
+ }
91
+ /**
92
+ * The run-level line under the ignore table, vocabulary included.
93
+ *
94
+ * The last clause is the one that has to be there. With no vocabulary the
95
+ * unworn-tag check did not run at all, and a footer that said nothing about it
96
+ * would read as every tag having been checked and every tag having been found.
97
+ *
98
+ * The first clause counts the rules that *absorbed*, over the rules that were
99
+ * declared, and the two are only the same number on a run where every rule
100
+ * worked. Counting declarations there put the whole run's pixels behind the size
101
+ * of the config: two rules, one of them matching nothing, read as *647 pixels
102
+ * absorbed by 2 rules* directly above a table saying one of them resolved
103
+ * nowhere. The table is the audit; the footer must not contradict it.
104
+ */
105
+ export function ignoreTotals(ledger) {
106
+ const parts = [
107
+ `${ledger.totalPixels.toLocaleString('en-US')} pixel(s) absorbed by ${ignoreShare(ledger)}`,
108
+ ];
109
+ if (ledger.fullyIgnored.length > 0) {
110
+ parts.push(`${String(ledger.fullyIgnored.length)} subject(s) differed only there`);
111
+ }
112
+ parts.push(ledger.vocabulary.length === 0
113
+ ? 'no subject in this run declared a tag, so no tag was checked'
114
+ : `checked against ${String(ledger.vocabulary.length)} declared tag(s)`);
115
+ return parts.join(' · ');
116
+ }
117
+ /** The sentence behind a sensitivity's state. */
118
+ export function sensitivitySays(entry) {
119
+ switch (sensitivityState(entry)) {
120
+ case 'unscoped':
121
+ return ('It matched no subject this run planned. Check the subjects and tags it names — this is ' +
122
+ 'a typo rather than a policy that has outlived its cause.');
123
+ case 'dead':
124
+ return (`It was in scope for ${String(entry.scoped)} subject(s) and decided none of them. ` +
125
+ 'Nothing here needed relaxing.');
126
+ default:
127
+ return (`It decided ${String(entry.absorbed.length)} of ${String(entry.scoped)} ` +
128
+ 'subject(s) in scope.');
129
+ }
130
+ }
131
+ /**
132
+ * The run-level line under the sensitivity table.
133
+ *
134
+ * Rules that decided something, over rules declared — the same reading as
135
+ * {@link ignoreTotals}, and for the same reason. *0 subjects not asserted on in
136
+ * full, by 1 rule* is a sentence with a rule in it that did nothing.
137
+ */
138
+ export function sensitivityTotals(ledger) {
139
+ return (`${String(ledger.totalAbsorbed)} subject(s) not asserted on in full, ` +
140
+ `by ${sensitivityShare(ledger)}`);
141
+ }
142
+ /**
143
+ * How many of the declared ignores absorbed anything, over how many were written.
144
+ *
145
+ * A phrase rather than a number because there are four footers printing it — an
146
+ * HTML report, a review page and two terminal summaries — and the arithmetic is
147
+ * the part that has to be identical between them. Whether a rule counts here is
148
+ * the same reading {@link ignoreState} takes, and a surface that re-derived it
149
+ * would eventually take a different one.
150
+ */
151
+ export function ignoreShare(ledger) {
152
+ return over(ledger.rules.filter((rule) => rule.pixels > 0).length, ledger.rules.length);
153
+ }
154
+ /** The same reading for sensitivities: rules that decided a verdict, over rules written. */
155
+ export function sensitivityShare(ledger) {
156
+ return over(ledger.rules.filter((rule) => rule.absorbed.length > 0).length, ledger.rules.length);
157
+ }
158
+ /**
159
+ * *n rule(s)* when every declaration did the thing, *n of m* when they did not.
160
+ *
161
+ * The bare count is kept for the ordinary case because *2 of 2 rule(s)* invites a
162
+ * reader to go looking for the one that is missing.
163
+ */
164
+ function over(did, declared) {
165
+ return did === declared
166
+ ? `${String(did)} rule(s)`
167
+ : `${String(did)} of ${String(declared)} rule(s)`;
168
+ }
169
+ /**
170
+ * The bands a rule absorbed, or the fact that they were not kept.
171
+ *
172
+ * `undefined` is the third answer, and it is the whole reason this is a function
173
+ * rather than a field read. A rule that decided verdicts with no band recorded is
174
+ * not a rule that absorbed nothing — it is a report written before the bands were
175
+ * kept, or by a writer that dropped them, and an empty list says the first.
176
+ */
177
+ export function absorbedBands(entry) {
178
+ if (entry.absorbed.length === 0)
179
+ return [];
180
+ return entry.bands.length === 0 ? undefined : entry.bands;
181
+ }
182
+ /**
183
+ * What decided a green subject, in the words of whatever decided it.
184
+ *
185
+ * `unsaid` is the case this exists for. A subject reported `ignored` whose
186
+ * record carries neither block says *a declaration absorbed this* and does not
187
+ * say which — and both obvious renderings of that are false. Printing nothing
188
+ * reads as *nothing was absorbed*; printing `0 px` reads as *a rule absorbed
189
+ * nothing*. So the absence is a state, and every surface has to render it.
190
+ *
191
+ * It is reachable two ways and they are worth telling apart when reading a bug:
192
+ * an older report written before the blocks were kept, and a store that dropped
193
+ * them on the way in. The second is the one that turns a report and a service
194
+ * into two different accounts of one run.
195
+ */
196
+ export function greenBecause(entry) {
197
+ if (entry.verdict !== 'ignored') {
198
+ const boxes = entry.ignored?.boxes ?? 0;
199
+ return boxes === 0 ? { kind: 'measured' } : { kind: 'masked', boxes };
200
+ }
201
+ if (entry.relaxed !== undefined) {
202
+ const { rule, level, bands } = entry.relaxed;
203
+ return { kind: 'relaxed', rule, level, bands };
204
+ }
205
+ const rules = Object.entries(entry.ignored?.byRule ?? {})
206
+ .filter(([, pixels]) => pixels > 0)
207
+ .map(([rule]) => rule);
208
+ return rules.length === 0
209
+ ? { kind: 'unsaid' }
210
+ : { kind: 'absorbed', rules, pixels: entry.ignored?.pixels ?? 0 };
211
+ }
212
+ //# sourceMappingURL=declarations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declarations.js","sourceRoot":"","sources":["../src/declarations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the config *declared*, and what each declaration did in this run.\n *\n * The two ledgers a run keeps about its own settings. An ignore says *part of\n * this page is not my subject*; a sensitivity says *assert on this much of it*.\n * Both make a run less observant on purpose, and both are only safe because\n * every run counts what they absorbed — a declaration that stopped being needed\n * is invisible without a count of zero, and a mask that outlives its cause grows\n * quietly over a real regression.\n *\n * They live here rather than with the command that computes them for the reason\n * the rest of this package exists: they have several readers. The CLI folds them,\n * a terminal prints them, an HTML page tabulates them, and a review service\n * stores them so the audit survives the machine the run happened on. A shape\n * owned by the first of those bends towards a terminal.\n *\n * ## One decision, taken once\n *\n * A rule's *state* — spent, mistyped, expired, or simply working — is a reading\n * of the counts, not another count. It is taken by {@link ignoreState} and\n * {@link sensitivityState} and nowhere else, because a second reader that\n * re-derived it would eventually disagree: the first HTML report to try shipped\n * `pixels === 0` as the whole test for *dead*, which calls every rule in a fresh\n * checkout dead, on the run that proves least about any of them.\n */\n\n/** What one ignore rule did this run. */\nexport interface IgnoreUsage {\n readonly rule: string;\n readonly reason: string;\n\n /** Changed pixels this rule absorbed across the run. */\n readonly pixels: number;\n\n /** Subjects where it excluded something, whether or not it absorbed anything. */\n readonly subjects: number;\n\n /**\n * Subjects where it excluded something **and a comparison happened**.\n *\n * The denominator that stops \"absorbed nothing\" from being an accusation. A\n * subject that is `new`, `incomparable`, or settled from a digest compared no\n * pixels, so an ignore over it had nothing to absorb — which says nothing at\n * all about whether the rule is still needed. Without this a fresh checkout\n * reported every ignore in the config as dead.\n */\n readonly comparedIn: number;\n\n /** Subjects where it excluded something and absorbed nothing there. */\n readonly inertIn: number;\n\n /** `true` when it never resolved to a place in any subject. */\n readonly unresolved: boolean;\n\n /**\n * Tags the rule names that no subject in this run wears.\n *\n * The only defence a tag has. A misspelled *key* is refused by name because\n * the config's objects are closed; a misspelled *tag* is a legal word that\n * simply matches nothing, and the rule then silently applies nowhere while the\n * operator reads their config and believes it applies somewhere. So the words\n * nothing answered to are named, with the near-misses that were present, and\n * an empty list is the ordinary case rather than the interesting one.\n */\n readonly unwornTags: readonly string[];\n\n /** `true` when it is past `until` and no longer absorbing. */\n readonly expired: boolean;\n}\n\n/** Every ignore rule the config named, and what the run as a whole absorbed. */\nexport interface IgnoreLedger {\n readonly rules: readonly IgnoreUsage[];\n\n /**\n * Rules that absorbed nothing anywhere this run.\n *\n * The list an operator is meant to act on. Named separately rather than left\n * to be derived, because a derivation nobody writes is a report nobody reads.\n */\n readonly dead: readonly string[];\n\n /** Subjects whose only differences were absorbed. Green, and not `unchanged`. */\n readonly fullyIgnored: readonly string[];\n\n readonly totalPixels: number;\n\n /** Every tag worn by a subject this run planned, for the near-miss hint. */\n readonly vocabulary: readonly string[];\n}\n\n/** What one sensitivity rule did this run. */\nexport interface SensitivityUsage {\n readonly rule: string;\n readonly reason: string;\n readonly level: string;\n\n /** Subjects this rule was in scope for, whether or not it absorbed them. */\n readonly scoped: number;\n\n /** Subjects whose verdict it decided. */\n readonly absorbed: readonly string[];\n\n /** Bands it absorbed, across every subject it decided. */\n readonly bands: readonly string[];\n\n /**\n * `true` when the rule matched no subject this run planned.\n *\n * A different failure from absorbing nothing, and worth its own word: a rule\n * naming `route/*` in a project whose subjects are all `story:*` is a typo,\n * not a policy that has outlived its cause.\n */\n readonly unscoped: boolean;\n}\n\n/** Every sensitivity rule the config named, and how much they relaxed. */\nexport interface SensitivityLedger {\n readonly rules: readonly SensitivityUsage[];\n readonly totalAbsorbed: number;\n}\n\n/**\n * What an ignore rule turned out to be, read from what it did.\n *\n * - `expired` — past its date. The differences it used to absorb are back.\n * - `unworn` — it names a tag no subject in this run wears, so it applied nowhere.\n * - `unresolved` — its selector matched no element in any subject.\n * - `untested` — it excluded a subtree, and nothing it covered was compared.\n * - `dead` — it excluded a subtree in subjects that *were* compared, and took nothing.\n * - `live` — it absorbed something.\n *\n * The order is the order the checks run in, and it matters: an expired rule that\n * also names a mistyped tag is expired, because that is the fact that explains\n * the other. `untested` and `dead` are the pair worth keeping apart — the first\n * is an absence of evidence and the second is evidence of absence, and a report\n * that spells both *dead* tells an operator to delete a rule on the run that\n * proves least about it.\n */\nexport type IgnoreState = 'expired' | 'unworn' | 'unresolved' | 'untested' | 'dead' | 'live';\n\n/**\n * What a sensitivity rule turned out to be.\n *\n * - `unscoped` — it matched no subject this run planned.\n * - `dead` — it was in scope and decided nothing; nothing here needed relaxing.\n * - `live` — it decided at least one verdict.\n */\nexport type SensitivityState = 'unscoped' | 'dead' | 'live';\n\n/** The single reading of an ignore's counts. Every surface asks this one. */\nexport function ignoreState(entry: IgnoreUsage): IgnoreState {\n if (entry.expired) return 'expired';\n if (entry.unwornTags.length > 0) return 'unworn';\n if (entry.unresolved) return 'unresolved';\n if (entry.pixels === 0) return entry.comparedIn === 0 ? 'untested' : 'dead';\n return 'live';\n}\n\n/** The single reading of a sensitivity's counts. */\nexport function sensitivityState(entry: SensitivityUsage): SensitivityState {\n if (entry.unscoped) return 'unscoped';\n return entry.absorbed.length === 0 ? 'dead' : 'live';\n}\n\n/**\n * Whether a state names something to do about the config.\n *\n * `untested` is deliberately not one. It is the absence of evidence, and a\n * report that flagged it would ask an operator to act on a run that measured\n * nothing — which is how audits stop being read.\n */\nexport function isActionable(state: IgnoreState | SensitivityState): boolean {\n return state !== 'live' && state !== 'untested';\n}\n\n/* --- what a surface says about a rule ------------------------------------- */\n\n/**\n * The sentence behind an ignore's state.\n *\n * Here rather than in a renderer for the reason the state itself is here: an\n * HTML table, a terminal and a review page all have to answer *why is this rule\n * marked* and there is one answer. Two of them writing their own is how a report\n * and a service come to disagree about the same run in front of the same person.\n *\n * The near-miss on `unworn` is the part worth carrying: the likeliest cause of a\n * tag nothing wears is a typo, and naming the tags that *are* worn turns a\n * report into a fix.\n */\nexport function ignoreSays(entry: IgnoreUsage, ledger: IgnoreLedger): string {\n switch (ignoreState(entry)) {\n case 'expired':\n return 'Past its date. The differences it absorbed are being reported again.';\n case 'unworn': {\n const worn =\n ledger.vocabulary.length === 0\n ? ''\n : ` Tags worn in this run: ${ledger.vocabulary.join(', ')}.`;\n return (\n `No subject in this run wears ${entry.unwornTags.join(', ')}, ` +\n `so the rule applied nowhere.${worn}`\n );\n }\n case 'unresolved':\n return (\n 'Its selector matched nothing in any subject. Either it is no longer needed, or it ' +\n 'stopped matching and something you believe is silenced is being reported.'\n );\n case 'untested':\n return (\n `It excluded a subtree in ${String(entry.subjects)} subject(s), none of which was ` +\n 'compared this run. Nothing here says whether it is still needed.'\n );\n case 'dead':\n return (\n `It excluded a subtree in ${String(entry.subjects)} subject(s), ` +\n `${String(entry.comparedIn)} of them compared, and absorbed nothing.`\n );\n default:\n return `Absorbed ${String(entry.pixels)} pixel(s) across ${String(entry.subjects)} subject(s).`;\n }\n}\n\n/**\n * The run-level line under the ignore table, vocabulary included.\n *\n * The last clause is the one that has to be there. With no vocabulary the\n * unworn-tag check did not run at all, and a footer that said nothing about it\n * would read as every tag having been checked and every tag having been found.\n *\n * The first clause counts the rules that *absorbed*, over the rules that were\n * declared, and the two are only the same number on a run where every rule\n * worked. Counting declarations there put the whole run's pixels behind the size\n * of the config: two rules, one of them matching nothing, read as *647 pixels\n * absorbed by 2 rules* directly above a table saying one of them resolved\n * nowhere. The table is the audit; the footer must not contradict it.\n */\nexport function ignoreTotals(ledger: IgnoreLedger): string {\n const parts = [\n `${ledger.totalPixels.toLocaleString('en-US')} pixel(s) absorbed by ${ignoreShare(ledger)}`,\n ];\n if (ledger.fullyIgnored.length > 0) {\n parts.push(`${String(ledger.fullyIgnored.length)} subject(s) differed only there`);\n }\n parts.push(\n ledger.vocabulary.length === 0\n ? 'no subject in this run declared a tag, so no tag was checked'\n : `checked against ${String(ledger.vocabulary.length)} declared tag(s)`,\n );\n return parts.join(' · ');\n}\n\n/** The sentence behind a sensitivity's state. */\nexport function sensitivitySays(entry: SensitivityUsage): string {\n switch (sensitivityState(entry)) {\n case 'unscoped':\n return (\n 'It matched no subject this run planned. Check the subjects and tags it names — this is ' +\n 'a typo rather than a policy that has outlived its cause.'\n );\n case 'dead':\n return (\n `It was in scope for ${String(entry.scoped)} subject(s) and decided none of them. ` +\n 'Nothing here needed relaxing.'\n );\n default:\n return (\n `It decided ${String(entry.absorbed.length)} of ${String(entry.scoped)} ` +\n 'subject(s) in scope.'\n );\n }\n}\n\n/**\n * The run-level line under the sensitivity table.\n *\n * Rules that decided something, over rules declared — the same reading as\n * {@link ignoreTotals}, and for the same reason. *0 subjects not asserted on in\n * full, by 1 rule* is a sentence with a rule in it that did nothing.\n */\nexport function sensitivityTotals(ledger: SensitivityLedger): string {\n return (\n `${String(ledger.totalAbsorbed)} subject(s) not asserted on in full, ` +\n `by ${sensitivityShare(ledger)}`\n );\n}\n\n/**\n * How many of the declared ignores absorbed anything, over how many were written.\n *\n * A phrase rather than a number because there are four footers printing it — an\n * HTML report, a review page and two terminal summaries — and the arithmetic is\n * the part that has to be identical between them. Whether a rule counts here is\n * the same reading {@link ignoreState} takes, and a surface that re-derived it\n * would eventually take a different one.\n */\nexport function ignoreShare(ledger: IgnoreLedger): string {\n return over(ledger.rules.filter((rule) => rule.pixels > 0).length, ledger.rules.length);\n}\n\n/** The same reading for sensitivities: rules that decided a verdict, over rules written. */\nexport function sensitivityShare(ledger: SensitivityLedger): string {\n return over(ledger.rules.filter((rule) => rule.absorbed.length > 0).length, ledger.rules.length);\n}\n\n/**\n * *n rule(s)* when every declaration did the thing, *n of m* when they did not.\n *\n * The bare count is kept for the ordinary case because *2 of 2 rule(s)* invites a\n * reader to go looking for the one that is missing.\n */\nfunction over(did: number, declared: number): string {\n return did === declared\n ? `${String(did)} rule(s)`\n : `${String(did)} of ${String(declared)} rule(s)`;\n}\n\n/**\n * The bands a rule absorbed, or the fact that they were not kept.\n *\n * `undefined` is the third answer, and it is the whole reason this is a function\n * rather than a field read. A rule that decided verdicts with no band recorded is\n * not a rule that absorbed nothing — it is a report written before the bands were\n * kept, or by a writer that dropped them, and an empty list says the first.\n */\nexport function absorbedBands(entry: SensitivityUsage): readonly string[] | undefined {\n if (entry.absorbed.length === 0) return [];\n return entry.bands.length === 0 ? undefined : entry.bands;\n}\n\n/* --- what made one subject green ------------------------------------------ */\n\n/**\n * The part of an observation that says why it came back green.\n *\n * Structural rather than the whole `ObservationRecord`, because the second\n * caller is a review service whose subjects arrive from a database and carry\n * only what was stored. Writing the relation over the fields it actually reads\n * is what lets both of them ask the same question, and stops the answer from\n * being re-derived on the far side of the wire.\n */\nexport interface GreenSubject {\n readonly verdict: 'unchanged' | 'changed' | 'new' | 'incomparable' | 'ignored';\n //\n // Both are written `| undefined` as well as optional, because the second\n // caller reads its subjects out of a database and declares them by indexed\n // access — which puts `undefined` in the property's type rather than in its\n // optionality. Under `exactOptionalPropertyTypes` those are different types,\n // and this is the one place the two spellings have to meet.\n readonly ignored?:\n | {\n readonly pixels: number;\n readonly boxes: number;\n readonly byRule: Readonly<Record<string, number>>;\n }\n | undefined;\n readonly relaxed?:\n | { readonly rule: string; readonly level: string; readonly bands: readonly string[] }\n | undefined;\n}\n\n/**\n * Why one subject is green.\n *\n * - `measured` — nothing differed, and no declaration stood over it.\n * - `masked` — nothing differed, and a rule was watching anyway. Its boxes\n * caught nothing here, which is how a mask starts outliving its cause.\n * - `relaxed` — it differed, and every band that moved is one this subject is\n * not asserted on.\n * - `absorbed` — it differed, and every differing pixel fell inside an excluded\n * subtree. The rules that took them are named.\n * - `unsaid` — it is `ignored` and the record does not say what did it.\n */\nexport type Green =\n | { readonly kind: 'measured' }\n | { readonly kind: 'masked'; readonly boxes: number }\n | {\n readonly kind: 'relaxed';\n readonly rule: string;\n readonly level: string;\n readonly bands: readonly string[];\n }\n | { readonly kind: 'absorbed'; readonly rules: readonly string[]; readonly pixels: number }\n | { readonly kind: 'unsaid' };\n\n/**\n * What decided a green subject, in the words of whatever decided it.\n *\n * `unsaid` is the case this exists for. A subject reported `ignored` whose\n * record carries neither block says *a declaration absorbed this* and does not\n * say which — and both obvious renderings of that are false. Printing nothing\n * reads as *nothing was absorbed*; printing `0 px` reads as *a rule absorbed\n * nothing*. So the absence is a state, and every surface has to render it.\n *\n * It is reachable two ways and they are worth telling apart when reading a bug:\n * an older report written before the blocks were kept, and a store that dropped\n * them on the way in. The second is the one that turns a report and a service\n * into two different accounts of one run.\n */\nexport function greenBecause(entry: GreenSubject): Green {\n if (entry.verdict !== 'ignored') {\n const boxes = entry.ignored?.boxes ?? 0;\n return boxes === 0 ? { kind: 'measured' } : { kind: 'masked', boxes };\n }\n\n if (entry.relaxed !== undefined) {\n const { rule, level, bands } = entry.relaxed;\n return { kind: 'relaxed', rule, level, bands };\n }\n\n const rules = Object.entries(entry.ignored?.byRule ?? {})\n .filter(([, pixels]) => pixels > 0)\n .map(([rule]) => rule);\n\n return rules.length === 0\n ? { kind: 'unsaid' }\n : { kind: 'absorbed', rules, pixels: entry.ignored?.pixels ?? 0 };\n}\n"]}
package/dist/file.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { RunReport } from './format.js';
2
+ /**
3
+ * The run report as a file — the one thing here that needs a disk.
4
+ *
5
+ * Its own module and its own entrypoint because the *format* is the contract and
6
+ * the disk is an implementation of it. A run happening on a pinned machine in CI
7
+ * and questions being asked on a laptop is exactly why this artifact exists; a
8
+ * consumer who moves it some other way — an object store, a PR comment, a socket
9
+ * — wants the shapes and not this.
10
+ */
11
+ export declare function writeRunReport(path: string, report: RunReport): Promise<void>;
12
+ /**
13
+ * Read a run report, refusing anything that is not one.
14
+ *
15
+ * The version check is not ceremony. These tools answer questions an agent then
16
+ * edits code on, and a silently-misparsed report produces confident answers about
17
+ * fields that were never there.
18
+ *
19
+ * `notObserved` gets the same treatment for a sharper reason: it is the field a
20
+ * summary claims a clean run *from*, so a malformed entry that survived parsing
21
+ * would be counted as neither a failure nor an exclusion and would silently stop
22
+ * holding the run open. The cost is that a report from a future writer with a
23
+ * third kind is refused outright rather than partly understood — which is the
24
+ * intended trade, because partly understanding a coverage list is exactly the
25
+ * failure this field exists to prevent.
26
+ */
27
+ export declare function readRunReport(path: string): Promise<RunReport>;
28
+ //# sourceMappingURL=file.d.ts.map
package/dist/file.js ADDED
@@ -0,0 +1,151 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ /**
4
+ * The run report as a file — the one thing here that needs a disk.
5
+ *
6
+ * Its own module and its own entrypoint because the *format* is the contract and
7
+ * the disk is an implementation of it. A run happening on a pinned machine in CI
8
+ * and questions being asked on a laptop is exactly why this artifact exists; a
9
+ * consumer who moves it some other way — an object store, a PR comment, a socket
10
+ * — wants the shapes and not this.
11
+ */
12
+ export async function writeRunReport(path, report) {
13
+ await mkdir(dirname(path), { recursive: true });
14
+ await writeFile(path, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
15
+ }
16
+ /**
17
+ * Read a run report, refusing anything that is not one.
18
+ *
19
+ * The version check is not ceremony. These tools answer questions an agent then
20
+ * edits code on, and a silently-misparsed report produces confident answers about
21
+ * fields that were never there.
22
+ *
23
+ * `notObserved` gets the same treatment for a sharper reason: it is the field a
24
+ * summary claims a clean run *from*, so a malformed entry that survived parsing
25
+ * would be counted as neither a failure nor an exclusion and would silently stop
26
+ * holding the run open. The cost is that a report from a future writer with a
27
+ * third kind is refused outright rather than partly understood — which is the
28
+ * intended trade, because partly understanding a coverage list is exactly the
29
+ * failure this field exists to prevent.
30
+ */
31
+ export async function readRunReport(path) {
32
+ const parsed = JSON.parse(await readFile(path, 'utf8'));
33
+ if (parsed.runVersion !== 1) {
34
+ throw new Error(`${path} is not a variance-authority run report (runVersion=${String(parsed.runVersion)})`);
35
+ }
36
+ if (!Array.isArray(parsed.observations)) {
37
+ throw new Error(`${path} has no observations array`);
38
+ }
39
+ if (parsed.notObserved !== undefined)
40
+ checkNotObserved(path, parsed.notObserved);
41
+ parsed.observations.forEach((observation, index) => {
42
+ const presentation = observation.signals?.presentation;
43
+ if (presentation !== undefined)
44
+ checkPresentation(path, index, presentation);
45
+ });
46
+ return parsed;
47
+ }
48
+ function checkPresentation(path, index, value) {
49
+ const signal = value;
50
+ const where = `${path}: observations[${index}].signals.presentation`;
51
+ if (signal.verdict === 'incomparable') {
52
+ if (typeof signal.because !== 'string') {
53
+ throw new Error(`${where} is incomparable without a \`because\``);
54
+ }
55
+ if ('effects' in signal || 'information' in signal) {
56
+ throw new Error(`${where} is incomparable but claims comparable evidence`);
57
+ }
58
+ if ((signal.before !== undefined && typeof signal.before !== 'string') ||
59
+ (signal.after !== undefined && typeof signal.after !== 'string')) {
60
+ throw new Error(`${where} has a presentation digest that is not a string`);
61
+ }
62
+ return;
63
+ }
64
+ if (signal.verdict !== 'unchanged' && signal.verdict !== 'changed') {
65
+ throw new Error(`${where}.verdict is not unchanged, changed, or incomparable`);
66
+ }
67
+ if (typeof signal.before !== 'string' || typeof signal.after !== 'string') {
68
+ throw new Error(`${where} has no before and after presentation digests`);
69
+ }
70
+ if (!Array.isArray(signal.effects))
71
+ throw new Error(`${where}.effects is not an array`);
72
+ checkInformation(where, signal.information);
73
+ signal.effects.forEach((effect, effectIndex) => {
74
+ checkEffect(`${where}.effects[${effectIndex}]`, effect);
75
+ });
76
+ }
77
+ function checkInformation(where, value) {
78
+ const information = value;
79
+ if (information === undefined || typeof information.contentPreserved !== 'boolean') {
80
+ throw new Error(`${where}.information has no content-preservation reading`);
81
+ }
82
+ for (const name of ['characters', 'elements', 'repeatedObjects']) {
83
+ const row = information[name];
84
+ if (row === undefined ||
85
+ typeof row.before !== 'number' ||
86
+ typeof row.after !== 'number' ||
87
+ typeof row.delta !== 'number') {
88
+ throw new Error(`${where}.information.${name} has no before, after, and delta`);
89
+ }
90
+ }
91
+ }
92
+ function checkEffect(where, value) {
93
+ const effect = value;
94
+ if (typeof effect.rule !== 'string' || typeof effect.owner !== 'string') {
95
+ throw new Error(`${where} has no rule and owner`);
96
+ }
97
+ if ((effect.pattern !== undefined && typeof effect.pattern !== 'string') ||
98
+ (effect.contract !== undefined && typeof effect.contract !== 'string')) {
99
+ throw new Error(`${where} has a pattern or contract that is not a string`);
100
+ }
101
+ if (!Array.isArray(effect.nodes) || !effect.nodes.every((node) => typeof node === 'string')) {
102
+ throw new Error(`${where}.nodes is not a string array`);
103
+ }
104
+ if (!['introduced', 'resolved', 'persisted'].includes(effect.transition ?? '')) {
105
+ throw new Error(`${where}.transition is not introduced, resolved, or persisted`);
106
+ }
107
+ const hasBefore = effect.before !== undefined;
108
+ const hasAfter = effect.after !== undefined;
109
+ if ((effect.transition === 'introduced' && (hasBefore || !hasAfter)) ||
110
+ (effect.transition === 'resolved' && (!hasBefore || hasAfter)) ||
111
+ (effect.transition === 'persisted' && (!hasBefore || !hasAfter))) {
112
+ throw new Error(`${where} does not carry the evidence its transition requires`);
113
+ }
114
+ if (effect.before !== undefined)
115
+ checkEvidence(`${where}.before`, effect.before);
116
+ if (effect.after !== undefined)
117
+ checkEvidence(`${where}.after`, effect.after);
118
+ }
119
+ function checkEvidence(where, value) {
120
+ const evidence = value;
121
+ if (typeof evidence.finding !== 'string' ||
122
+ evidence.measurements === null ||
123
+ typeof evidence.measurements !== 'object' ||
124
+ Array.isArray(evidence.measurements)) {
125
+ throw new Error(`${where} has no finding and measurement object`);
126
+ }
127
+ if (!Object.values(evidence.measurements)
128
+ .every((measurement) => typeof measurement === 'string' || typeof measurement === 'number')) {
129
+ throw new Error(`${where}.measurements contains a value that is not a string or number`);
130
+ }
131
+ }
132
+ function checkNotObserved(path, value) {
133
+ if (!Array.isArray(value)) {
134
+ throw new Error(`${path} has a \`notObserved\` field that is not an array`);
135
+ }
136
+ value.forEach((entry, index) => {
137
+ const row = entry;
138
+ if (typeof row.subject !== 'string' || typeof row.because !== 'string') {
139
+ throw new Error(`${path}: notObserved[${index}] has no \`subject\` and \`because\``);
140
+ }
141
+ if (row.kind !== 'excluded' && row.kind !== 'failed' && row.kind !== 'unreached') {
142
+ // Not defaulted. Guessing `excluded` would turn a coverage hole into a
143
+ // decision somebody made, guessing `failed` would turn every deliberate
144
+ // exclusion into a permanently red build, and guessing `unreached` would
145
+ // credit the run with reasoning it never did.
146
+ throw new Error(`${path}: notObserved[${index}].kind is ${JSON.stringify(row.kind)}, ` +
147
+ 'which is none of "excluded", "failed" or "unreached"');
148
+ }
149
+ });
150
+ }
151
+ //# sourceMappingURL=file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.js","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC;;;;;;;;GAQG;AAEH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,MAAiB;IAClE,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,SAAS,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY;IAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAuB,CAAC;IAE9E,IAAI,MAAM,CAAC,UAAU,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,uDAAuD,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAC3F,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,4BAA4B,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS;QAAE,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IACjF,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,KAAK,EAAE,EAAE;QACjD,MAAM,YAAY,GAAG,WAAW,CAAC,OAAO,EAAE,YAAY,CAAC;QACvD,IAAI,YAAY,KAAK,SAAS;YAAE,iBAAiB,CAAC,IAAI,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC;IAC/E,CAAC,CAAC,CAAC;IAEH,OAAO,MAAmB,CAAC;AAC7B,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAY,EAAE,KAAa,EAAE,KAAc;IACpE,MAAM,MAAM,GAAG,KAAoE,CAAC;IACpF,MAAM,KAAK,GAAG,GAAG,IAAI,kBAAkB,KAAK,wBAAwB,CAAC;IACrE,IAAI,MAAM,CAAC,OAAO,KAAK,cAAc,EAAE,CAAC;QACtC,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YACvC,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,wCAAwC,CAAC,CAAC;QACpE,CAAC;QACD,IAAI,SAAS,IAAI,MAAM,IAAI,aAAa,IAAI,MAAM,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,iDAAiD,CAAC,CAAC;QAC7E,CAAC;QACD,IACE,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC;YAClE,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,EAChE,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,iDAAiD,CAAC,CAAC;QAC7E,CAAC;QACD,OAAO;IACT,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,KAAK,WAAW,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,qDAAqD,CAAC,CAAC;IACjF,CAAC;IACD,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC1E,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,+CAA+C,CAAC,CAAC;IAC3E,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,0BAA0B,CAAC,CAAC;IACxF,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IAC5C,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE;QAC7C,WAAW,CAAC,GAAG,KAAK,YAAY,WAAW,GAAG,EAAE,MAAM,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAa,EAAE,KAAc;IACrD,MAAM,WAAW,GAAG,KAA4C,CAAC;IACjE,IAAI,WAAW,KAAK,SAAS,IAAI,OAAO,WAAW,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACnF,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,kDAAkD,CAAC,CAAC;IAC9E,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,CAAC,YAAY,EAAE,UAAU,EAAE,iBAAiB,CAAC,EAAE,CAAC;QACjE,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAwC,CAAC;QACrE,IACE,GAAG,KAAK,SAAS;YACjB,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ;YAC9B,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ;YAC7B,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,EAC7B,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,gBAAgB,IAAI,kCAAkC,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,KAAa,EAAE,KAAc;IAChD,MAAM,MAAM,GAAG,KAA0C,CAAC;IAC1D,IAAI,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,wBAAwB,CAAC,CAAC;IACpD,CAAC;IACD,IACE,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC;QACpE,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,EACtE,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,iDAAiD,CAAC,CAAC;IAC7E,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,EAAE,CAAC;QAC5F,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,8BAA8B,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,CAAC,CAAC,YAAY,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,CAAC;QAC/E,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,uDAAuD,CAAC,CAAC;IACnF,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC;IAC9C,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC;IAC5C,IACE,CAAC,MAAM,CAAC,UAAU,KAAK,YAAY,IAAI,CAAC,SAAS,IAAI,CAAC,QAAQ,CAAC,CAAC;QAChE,CAAC,MAAM,CAAC,UAAU,KAAK,UAAU,IAAI,CAAC,CAAC,SAAS,IAAI,QAAQ,CAAC,CAAC;QAC9D,CAAC,MAAM,CAAC,UAAU,KAAK,WAAW,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,QAAQ,CAAC,CAAC,EAChE,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,sDAAsD,CAAC,CAAC;IAClF,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;QAAE,aAAa,CAAC,GAAG,KAAK,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACjF,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;QAAE,aAAa,CAAC,GAAG,KAAK,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;AAChF,CAAC;AAED,SAAS,aAAa,CAAC,KAAa,EAAE,KAAc;IAClD,MAAM,QAAQ,GAAG,KAAgC,CAAC;IAClD,IACE,OAAO,QAAQ,CAAC,OAAO,KAAK,QAAQ;QACpC,QAAQ,CAAC,YAAY,KAAK,IAAI;QAC9B,OAAO,QAAQ,CAAC,YAAY,KAAK,QAAQ;QACzC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,EACpC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,wCAAwC,CAAC,CAAC;IACpE,CAAC;IACD,IACE,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAuC,CAAC;SAC7D,KAAK,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,OAAO,WAAW,KAAK,QAAQ,IAAI,OAAO,WAAW,KAAK,QAAQ,CAAC,EAC7F,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,+DAA+D,CAAC,CAAC;IAC3F,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAY,EAAE,KAAc;IACpD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,mDAAmD,CAAC,CAAC;IAC9E,CAAC;IAEA,KAA4B,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QACrD,MAAM,GAAG,GAAG,KAA6B,CAAC;QAE1C,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YACvE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,iBAAiB,KAAK,sCAAsC,CAAC,CAAC;QACvF,CAAC;QACD,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YACjF,uEAAuE;YACvE,wEAAwE;YACxE,yEAAyE;YACzE,8CAA8C;YAC9C,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,iBAAiB,KAAK,aAAa,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI;gBACpE,sDAAsD,CACzD,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport type {\n NotObserved,\n PresentationEffectRecord,\n PresentationSignalRecord,\n RunReport,\n} from './format.js';\n\n/**\n * The run report as a file — the one thing here that needs a disk.\n *\n * Its own module and its own entrypoint because the *format* is the contract and\n * the disk is an implementation of it. A run happening on a pinned machine in CI\n * and questions being asked on a laptop is exactly why this artifact exists; a\n * consumer who moves it some other way — an object store, a PR comment, a socket\n * — wants the shapes and not this.\n */\n\nexport async function writeRunReport(path: string, report: RunReport): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(report, null, 2)}\\n`, 'utf8');\n}\n\n/**\n * Read a run report, refusing anything that is not one.\n *\n * The version check is not ceremony. These tools answer questions an agent then\n * edits code on, and a silently-misparsed report produces confident answers about\n * fields that were never there.\n *\n * `notObserved` gets the same treatment for a sharper reason: it is the field a\n * summary claims a clean run *from*, so a malformed entry that survived parsing\n * would be counted as neither a failure nor an exclusion and would silently stop\n * holding the run open. The cost is that a report from a future writer with a\n * third kind is refused outright rather than partly understood — which is the\n * intended trade, because partly understanding a coverage list is exactly the\n * failure this field exists to prevent.\n */\nexport async function readRunReport(path: string): Promise<RunReport> {\n const parsed = JSON.parse(await readFile(path, 'utf8')) as Partial<RunReport>;\n\n if (parsed.runVersion !== 1) {\n throw new Error(\n `${path} is not a variance-authority run report (runVersion=${String(parsed.runVersion)})`,\n );\n }\n if (!Array.isArray(parsed.observations)) {\n throw new Error(`${path} has no observations array`);\n }\n if (parsed.notObserved !== undefined) checkNotObserved(path, parsed.notObserved);\n parsed.observations.forEach((observation, index) => {\n const presentation = observation.signals?.presentation;\n if (presentation !== undefined) checkPresentation(path, index, presentation);\n });\n\n return parsed as RunReport;\n}\n\nfunction checkPresentation(path: string, index: number, value: unknown): void {\n const signal = value as Partial<PresentationSignalRecord> & Record<string, unknown>;\n const where = `${path}: observations[${index}].signals.presentation`;\n if (signal.verdict === 'incomparable') {\n if (typeof signal.because !== 'string') {\n throw new Error(`${where} is incomparable without a \\`because\\``);\n }\n if ('effects' in signal || 'information' in signal) {\n throw new Error(`${where} is incomparable but claims comparable evidence`);\n }\n if (\n (signal.before !== undefined && typeof signal.before !== 'string') ||\n (signal.after !== undefined && typeof signal.after !== 'string')\n ) {\n throw new Error(`${where} has a presentation digest that is not a string`);\n }\n return;\n }\n if (signal.verdict !== 'unchanged' && signal.verdict !== 'changed') {\n throw new Error(`${where}.verdict is not unchanged, changed, or incomparable`);\n }\n if (typeof signal.before !== 'string' || typeof signal.after !== 'string') {\n throw new Error(`${where} has no before and after presentation digests`);\n }\n if (!Array.isArray(signal.effects)) throw new Error(`${where}.effects is not an array`);\n checkInformation(where, signal.information);\n signal.effects.forEach((effect, effectIndex) => {\n checkEffect(`${where}.effects[${effectIndex}]`, effect);\n });\n}\n\nfunction checkInformation(where: string, value: unknown): void {\n const information = value as Record<string, unknown> | undefined;\n if (information === undefined || typeof information.contentPreserved !== 'boolean') {\n throw new Error(`${where}.information has no content-preservation reading`);\n }\n for (const name of ['characters', 'elements', 'repeatedObjects']) {\n const row = information[name] as Record<string, unknown> | undefined;\n if (\n row === undefined ||\n typeof row.before !== 'number' ||\n typeof row.after !== 'number' ||\n typeof row.delta !== 'number'\n ) {\n throw new Error(`${where}.information.${name} has no before, after, and delta`);\n }\n }\n}\n\nfunction checkEffect(where: string, value: unknown): void {\n const effect = value as Partial<PresentationEffectRecord>;\n if (typeof effect.rule !== 'string' || typeof effect.owner !== 'string') {\n throw new Error(`${where} has no rule and owner`);\n }\n if (\n (effect.pattern !== undefined && typeof effect.pattern !== 'string') ||\n (effect.contract !== undefined && typeof effect.contract !== 'string')\n ) {\n throw new Error(`${where} has a pattern or contract that is not a string`);\n }\n if (!Array.isArray(effect.nodes) || !effect.nodes.every((node) => typeof node === 'string')) {\n throw new Error(`${where}.nodes is not a string array`);\n }\n if (!['introduced', 'resolved', 'persisted'].includes(effect.transition ?? '')) {\n throw new Error(`${where}.transition is not introduced, resolved, or persisted`);\n }\n const hasBefore = effect.before !== undefined;\n const hasAfter = effect.after !== undefined;\n if (\n (effect.transition === 'introduced' && (hasBefore || !hasAfter)) ||\n (effect.transition === 'resolved' && (!hasBefore || hasAfter)) ||\n (effect.transition === 'persisted' && (!hasBefore || !hasAfter))\n ) {\n throw new Error(`${where} does not carry the evidence its transition requires`);\n }\n if (effect.before !== undefined) checkEvidence(`${where}.before`, effect.before);\n if (effect.after !== undefined) checkEvidence(`${where}.after`, effect.after);\n}\n\nfunction checkEvidence(where: string, value: unknown): void {\n const evidence = value as Record<string, unknown>;\n if (\n typeof evidence.finding !== 'string' ||\n evidence.measurements === null ||\n typeof evidence.measurements !== 'object' ||\n Array.isArray(evidence.measurements)\n ) {\n throw new Error(`${where} has no finding and measurement object`);\n }\n if (\n !Object.values(evidence.measurements as Record<string, unknown>)\n .every((measurement) => typeof measurement === 'string' || typeof measurement === 'number')\n ) {\n throw new Error(`${where}.measurements contains a value that is not a string or number`);\n }\n}\n\nfunction checkNotObserved(path: string, value: unknown): void {\n if (!Array.isArray(value)) {\n throw new Error(`${path} has a \\`notObserved\\` field that is not an array`);\n }\n\n (value as readonly unknown[]).forEach((entry, index) => {\n const row = entry as Partial<NotObserved>;\n\n if (typeof row.subject !== 'string' || typeof row.because !== 'string') {\n throw new Error(`${path}: notObserved[${index}] has no \\`subject\\` and \\`because\\``);\n }\n if (row.kind !== 'excluded' && row.kind !== 'failed' && row.kind !== 'unreached') {\n // Not defaulted. Guessing `excluded` would turn a coverage hole into a\n // decision somebody made, guessing `failed` would turn every deliberate\n // exclusion into a permanently red build, and guessing `unreached` would\n // credit the run with reasoning it never did.\n throw new Error(\n `${path}: notObserved[${index}].kind is ${JSON.stringify(row.kind)}, ` +\n 'which is none of \"excluded\", \"failed\" or \"unreached\"',\n );\n }\n });\n}\n"]}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A defect the run found by reading one render, and when it arrived.
3
+ *
4
+ * Its own file because two of its fields are about provenance rather than about
5
+ * the defect, and they are the two a surface gets wrong by omission. A finding is
6
+ * produced with no baseline consulted — that is the half a comparison
7
+ * structurally cannot produce — and the consequence is that the same list is
8
+ * printed on the run that introduced a defect and on every run after it, under a
9
+ * heading that does not say what kind of defect it is. `band` and `standing` are
10
+ * what make the list sayable and datable, and the rules for reading them absent
11
+ * are written on them.
12
+ */
13
+ /**
14
+ * One defect in a render, flattened for the report.
15
+ *
16
+ * Same fields a `RegionRecord` carries and for the same reason: what, where,
17
+ * whose, which file. The owner chain is dropped — it is an in-memory structure
18
+ * with a props digest per frame, and a report is read by something that wants a
19
+ * sentence.
20
+ */
21
+ export interface FindingRecord {
22
+ /** e.g. `control-without-name`. Stable, so an ignore list can name one. */
23
+ readonly rule: string;
24
+ /**
25
+ * The band this defect would block under — `a11y`, `geometry`, `content`.
26
+ *
27
+ * What makes the list sayable. Nine of the eleven rules are accessibility and
28
+ * two are not, so a panel headed *Accessibility* would be wrong about
29
+ * `untranslated` and `overflows-container`, and a panel headed nothing at all
30
+ * leaves a reader to infer the subject of the report from the rule slugs. The
31
+ * band is the axis the project already blocks on (`blocking: ['a11y']`), which
32
+ * makes the heading a reader sees and the policy that stops their merge the
33
+ * same word.
34
+ *
35
+ * Optional because a report written before this existed carries none, and a
36
+ * renderer must say *unclassified* rather than filing it under the first band.
37
+ */
38
+ readonly band?: string;
39
+ /**
40
+ * Whether the baseline this render was measured against carried this too.
41
+ *
42
+ * `true` is *you inherited this*; `false` is *this arrived with the change you
43
+ * are reviewing*. Inspection reads one render with no baseline, which is the
44
+ * whole reason it can see a defect a comparison never will — and the cost is
45
+ * that its output is identical on the run that introduced a defect and on the
46
+ * two hundred runs after it. A reviewer with no answer here reads the same list
47
+ * every time and stops reading it.
48
+ *
49
+ * **Absent is neither.** It means nothing recorded what the baseline contained:
50
+ * no baseline yet, a baseline written before `Raster.findingMarks` existed, or
51
+ * a store that does not carry it. A surface that printed absent as `false`
52
+ * would announce every standing defect in the suite as newly introduced, on the
53
+ * first run after an upgrade, to the person least able to check.
54
+ */
55
+ readonly standing?: boolean;
56
+ /** One sentence, naming the thing rather than the rule. */
57
+ readonly what: string;
58
+ readonly path: string;
59
+ /** Landmark phrase, e.g. `main → list item 2 of 3`. */
60
+ readonly where?: string;
61
+ readonly component?: string;
62
+ readonly file?: string;
63
+ }
64
+ //# sourceMappingURL=finding-record.d.ts.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A defect the run found by reading one render, and when it arrived.
3
+ *
4
+ * Its own file because two of its fields are about provenance rather than about
5
+ * the defect, and they are the two a surface gets wrong by omission. A finding is
6
+ * produced with no baseline consulted — that is the half a comparison
7
+ * structurally cannot produce — and the consequence is that the same list is
8
+ * printed on the run that introduced a defect and on every run after it, under a
9
+ * heading that does not say what kind of defect it is. `band` and `standing` are
10
+ * what make the list sayable and datable, and the rules for reading them absent
11
+ * are written on them.
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=finding-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finding-record.js","sourceRoot":"","sources":["../src/finding-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG","sourcesContent":["/**\n * A defect the run found by reading one render, and when it arrived.\n *\n * Its own file because two of its fields are about provenance rather than about\n * the defect, and they are the two a surface gets wrong by omission. A finding is\n * produced with no baseline consulted — that is the half a comparison\n * structurally cannot produce — and the consequence is that the same list is\n * printed on the run that introduced a defect and on every run after it, under a\n * heading that does not say what kind of defect it is. `band` and `standing` are\n * what make the list sayable and datable, and the rules for reading them absent\n * are written on them.\n */\n\n/**\n * One defect in a render, flattened for the report.\n *\n * Same fields a `RegionRecord` carries and for the same reason: what, where,\n * whose, which file. The owner chain is dropped — it is an in-memory structure\n * with a props digest per frame, and a report is read by something that wants a\n * sentence.\n */\nexport interface FindingRecord {\n /** e.g. `control-without-name`. Stable, so an ignore list can name one. */\n readonly rule: string;\n\n /**\n * The band this defect would block under — `a11y`, `geometry`, `content`.\n *\n * What makes the list sayable. Nine of the eleven rules are accessibility and\n * two are not, so a panel headed *Accessibility* would be wrong about\n * `untranslated` and `overflows-container`, and a panel headed nothing at all\n * leaves a reader to infer the subject of the report from the rule slugs. The\n * band is the axis the project already blocks on (`blocking: ['a11y']`), which\n * makes the heading a reader sees and the policy that stops their merge the\n * same word.\n *\n * Optional because a report written before this existed carries none, and a\n * renderer must say *unclassified* rather than filing it under the first band.\n */\n readonly band?: string;\n\n /**\n * Whether the baseline this render was measured against carried this too.\n *\n * `true` is *you inherited this*; `false` is *this arrived with the change you\n * are reviewing*. Inspection reads one render with no baseline, which is the\n * whole reason it can see a defect a comparison never will — and the cost is\n * that its output is identical on the run that introduced a defect and on the\n * two hundred runs after it. A reviewer with no answer here reads the same list\n * every time and stops reading it.\n *\n * **Absent is neither.** It means nothing recorded what the baseline contained:\n * no baseline yet, a baseline written before `Raster.findingMarks` existed, or\n * a store that does not carry it. A surface that printed absent as `false`\n * would announce every standing defect in the suite as newly introduced, on the\n * first run after an upgrade, to the person least able to check.\n */\n readonly standing?: boolean;\n /** One sentence, naming the thing rather than the rule. */\n readonly what: string;\n readonly path: string;\n /** Landmark phrase, e.g. `main → list item 2 of 3`. */\n readonly where?: string;\n readonly component?: string;\n readonly file?: string;\n}\n"]}