@variance-authority/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/artifact.d.ts +47 -0
- package/dist/artifact.js +2 -0
- package/dist/artifact.js.map +1 -0
- package/dist/attribute/because.d.ts +43 -0
- package/dist/attribute/because.js +64 -0
- package/dist/attribute/because.js.map +1 -0
- package/dist/attribute/boundary.d.ts +191 -0
- package/dist/attribute/boundary.js +328 -0
- package/dist/attribute/boundary.js.map +1 -0
- package/dist/attribute/call-site.d.ts +96 -0
- package/dist/attribute/call-site.js +183 -0
- package/dist/attribute/call-site.js.map +1 -0
- package/dist/attribute/component-hash.d.ts +188 -0
- package/dist/attribute/component-hash.js +312 -0
- package/dist/attribute/component-hash.js.map +1 -0
- package/dist/attribute/composition-fixture.d.ts +18 -0
- package/dist/attribute/composition-fixture.js +53 -0
- package/dist/attribute/composition-fixture.js.map +1 -0
- package/dist/attribute/composition.d.ts +182 -0
- package/dist/attribute/composition.js +169 -0
- package/dist/attribute/composition.js.map +1 -0
- package/dist/attribute/control.d.ts +43 -0
- package/dist/attribute/control.js +54 -0
- package/dist/attribute/control.js.map +1 -0
- package/dist/attribute/coverage.d.ts +99 -0
- package/dist/attribute/coverage.js +86 -0
- package/dist/attribute/coverage.js.map +1 -0
- package/dist/attribute/divergence.d.ts +102 -0
- package/dist/attribute/divergence.js +152 -0
- package/dist/attribute/divergence.js.map +1 -0
- package/dist/attribute/index.d.ts +43 -0
- package/dist/attribute/index.js +29 -0
- package/dist/attribute/index.js.map +1 -0
- package/dist/attribute/instability.d.ts +89 -0
- package/dist/attribute/instability.js +183 -0
- package/dist/attribute/instability.js.map +1 -0
- package/dist/attribute/instances.d.ts +145 -0
- package/dist/attribute/instances.js +140 -0
- package/dist/attribute/instances.js.map +1 -0
- package/dist/attribute/locate.d.ts +22 -0
- package/dist/attribute/locate.js +100 -0
- package/dist/attribute/locate.js.map +1 -0
- package/dist/attribute/mask.d.ts +156 -0
- package/dist/attribute/mask.js +201 -0
- package/dist/attribute/mask.js.map +1 -0
- package/dist/attribute/movement.d.ts +211 -0
- package/dist/attribute/movement.js +181 -0
- package/dist/attribute/movement.js.map +1 -0
- package/dist/attribute/region.d.ts +188 -0
- package/dist/attribute/region.js +144 -0
- package/dist/attribute/region.js.map +1 -0
- package/dist/attribute/source-map.d.ts +98 -0
- package/dist/attribute/source-map.js +264 -0
- package/dist/attribute/source-map.js.map +1 -0
- package/dist/attribute/source.d.ts +77 -0
- package/dist/attribute/source.js +95 -0
- package/dist/attribute/source.js.map +1 -0
- package/dist/attribute/stack.d.ts +80 -0
- package/dist/attribute/stack.js +108 -0
- package/dist/attribute/stack.js.map +1 -0
- package/dist/compare/band.d.ts +85 -0
- package/dist/compare/band.js +70 -0
- package/dist/compare/band.js.map +1 -0
- package/dist/compare/cascade.d.ts +45 -0
- package/dist/compare/cascade.js +71 -0
- package/dist/compare/cascade.js.map +1 -0
- package/dist/compare/derive.d.ts +78 -0
- package/dist/compare/derive.js +39 -0
- package/dist/compare/derive.js.map +1 -0
- package/dist/compare/diff/attribution.d.ts +23 -0
- package/dist/compare/diff/attribution.js +244 -0
- package/dist/compare/diff/attribution.js.map +1 -0
- package/dist/compare/diff/compare-nodes.d.ts +15 -0
- package/dist/compare/diff/compare-nodes.js +227 -0
- package/dist/compare/diff/compare-nodes.js.map +1 -0
- package/dist/compare/diff/components.d.ts +20 -0
- package/dist/compare/diff/components.js +76 -0
- package/dist/compare/diff/components.js.map +1 -0
- package/dist/compare/diff/delta.d.ts +165 -0
- package/dist/compare/diff/delta.js +12 -0
- package/dist/compare/diff/delta.js.map +1 -0
- package/dist/compare/diff/index.d.ts +69 -0
- package/dist/compare/diff/index.js +101 -0
- package/dist/compare/diff/index.js.map +1 -0
- package/dist/compare/diff/match.d.ts +37 -0
- package/dist/compare/diff/match.js +167 -0
- package/dist/compare/diff/match.js.map +1 -0
- package/dist/compare/explain.d.ts +23 -0
- package/dist/compare/explain.js +190 -0
- package/dist/compare/explain.js.map +1 -0
- package/dist/compare/holding-diff.d.ts +25 -0
- package/dist/compare/holding-diff.js +74 -0
- package/dist/compare/holding-diff.js.map +1 -0
- package/dist/compare/impact.d.ts +56 -0
- package/dist/compare/impact.js +124 -0
- package/dist/compare/impact.js.map +1 -0
- package/dist/compare/index.d.ts +30 -0
- package/dist/compare/index.js +22 -0
- package/dist/compare/index.js.map +1 -0
- package/dist/compare/instance.d.ts +30 -0
- package/dist/compare/instance.js +145 -0
- package/dist/compare/instance.js.map +1 -0
- package/dist/compare/observability.d.ts +49 -0
- package/dist/compare/observability.js +57 -0
- package/dist/compare/observability.js.map +1 -0
- package/dist/compare/parting.d.ts +179 -0
- package/dist/compare/parting.js +244 -0
- package/dist/compare/parting.js.map +1 -0
- package/dist/compare/slice.d.ts +74 -0
- package/dist/compare/slice.js +79 -0
- package/dist/compare/slice.js.map +1 -0
- package/dist/compare/value.d.ts +60 -0
- package/dist/compare/value.js +111 -0
- package/dist/compare/value.js.map +1 -0
- package/dist/format/accessibility.d.ts +27 -0
- package/dist/format/accessibility.js +17 -0
- package/dist/format/accessibility.js.map +1 -0
- package/dist/format/canonical.d.ts +38 -0
- package/dist/format/canonical.js +65 -0
- package/dist/format/canonical.js.map +1 -0
- package/dist/format/capture.d.ts +212 -0
- package/dist/format/capture.js +2 -0
- package/dist/format/capture.js.map +1 -0
- package/dist/format/document.d.ts +279 -0
- package/dist/format/document.js +65 -0
- package/dist/format/document.js.map +1 -0
- package/dist/format/environment.d.ts +120 -0
- package/dist/format/environment.js +53 -0
- package/dist/format/environment.js.map +1 -0
- package/dist/format/hash.d.ts +45 -0
- package/dist/format/hash.js +41 -0
- package/dist/format/hash.js.map +1 -0
- package/dist/format/holding.d.ts +134 -0
- package/dist/format/holding.js +2 -0
- package/dist/format/holding.js.map +1 -0
- package/dist/format/index.d.ts +39 -0
- package/dist/format/index.js +25 -0
- package/dist/format/index.js.map +1 -0
- package/dist/format/intervention.d.ts +87 -0
- package/dist/format/intervention.js +11 -0
- package/dist/format/intervention.js.map +1 -0
- package/dist/format/profile.d.ts +59 -0
- package/dist/format/profile.js +66 -0
- package/dist/format/profile.js.map +1 -0
- package/dist/format/provenance.d.ts +179 -0
- package/dist/format/provenance.js +222 -0
- package/dist/format/provenance.js.map +1 -0
- package/dist/format/sha256.d.ts +34 -0
- package/dist/format/sha256.js +152 -0
- package/dist/format/sha256.js.map +1 -0
- package/dist/format/snapshot.d.ts +263 -0
- package/dist/format/snapshot.js +2 -0
- package/dist/format/snapshot.js.map +1 -0
- package/dist/format/stabilize.d.ts +190 -0
- package/dist/format/stabilize.js +333 -0
- package/dist/format/stabilize.js.map +1 -0
- package/dist/format/tier.d.ts +19 -0
- package/dist/format/tier.js +11 -0
- package/dist/format/tier.js.map +1 -0
- package/dist/format/value.d.ts +115 -0
- package/dist/format/value.js +182 -0
- package/dist/format/value.js.map +1 -0
- package/dist/format/wiring.d.ts +115 -0
- package/dist/format/wiring.js +60 -0
- package/dist/format/wiring.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/judge/docket.d.ts +85 -0
- package/dist/judge/docket.js +162 -0
- package/dist/judge/docket.js.map +1 -0
- package/dist/judge/fingerprint.d.ts +53 -0
- package/dist/judge/fingerprint.js +144 -0
- package/dist/judge/fingerprint.js.map +1 -0
- package/dist/judge/ignore.d.ts +206 -0
- package/dist/judge/ignore.js +237 -0
- package/dist/judge/ignore.js.map +1 -0
- package/dist/judge/index.d.ts +44 -0
- package/dist/judge/index.js +34 -0
- package/dist/judge/index.js.map +1 -0
- package/dist/judge/inspect.d.ts +148 -0
- package/dist/judge/inspect.js +208 -0
- package/dist/judge/inspect.js.map +1 -0
- package/dist/judge/intent.d.ts +146 -0
- package/dist/judge/intent.js +150 -0
- package/dist/judge/intent.js.map +1 -0
- package/dist/judge/locale.d.ts +111 -0
- package/dist/judge/locale.js +247 -0
- package/dist/judge/locale.js.map +1 -0
- package/dist/judge/scope.d.ts +41 -0
- package/dist/judge/scope.js +47 -0
- package/dist/judge/scope.js.map +1 -0
- package/dist/judge/sensitivity.d.ts +202 -0
- package/dist/judge/sensitivity.js +152 -0
- package/dist/judge/sensitivity.js.map +1 -0
- package/dist/judge/trail.d.ts +122 -0
- package/dist/judge/trail.js +96 -0
- package/dist/judge/trail.js.map +1 -0
- package/dist/judge/verdict.d.ts +39 -0
- package/dist/judge/verdict.js +32 -0
- package/dist/judge/verdict.js.map +1 -0
- package/dist/plan/index.d.ts +12 -0
- package/dist/plan/index.js +11 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/plan/tool.d.ts +85 -0
- package/dist/plan/tool.js +87 -0
- package/dist/plan/tool.js.map +1 -0
- package/dist/relate/graph.d.ts +190 -0
- package/dist/relate/graph.js +234 -0
- package/dist/relate/graph.js.map +1 -0
- package/dist/relate/index.d.ts +16 -0
- package/dist/relate/index.js +16 -0
- package/dist/relate/index.js.map +1 -0
- package/dist/relate/merkle.d.ts +110 -0
- package/dist/relate/merkle.js +246 -0
- package/dist/relate/merkle.js.map +1 -0
- package/dist/relate/reach.d.ts +65 -0
- package/dist/relate/reach.js +111 -0
- package/dist/relate/reach.js.map +1 -0
- package/dist/relate/records.d.ts +129 -0
- package/dist/relate/records.js +112 -0
- package/dist/relate/records.js.map +1 -0
- package/dist/rules/index.d.ts +19 -0
- package/dist/rules/index.js +17 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/normalize/alias.d.ts +50 -0
- package/dist/rules/normalize/alias.js +111 -0
- package/dist/rules/normalize/alias.js.map +1 -0
- package/dist/rules/normalize/cascade.d.ts +85 -0
- package/dist/rules/normalize/cascade.js +324 -0
- package/dist/rules/normalize/cascade.js.map +1 -0
- package/dist/rules/normalize/color.d.ts +30 -0
- package/dist/rules/normalize/color.js +185 -0
- package/dist/rules/normalize/color.js.map +1 -0
- package/dist/rules/normalize/fixture.d.ts +83 -0
- package/dist/rules/normalize/fixture.js +74 -0
- package/dist/rules/normalize/fixture.js.map +1 -0
- package/dist/rules/normalize/index.d.ts +39 -0
- package/dist/rules/normalize/index.js +268 -0
- package/dist/rules/normalize/index.js.map +1 -0
- package/dist/rules/normalize/project.d.ts +42 -0
- package/dist/rules/normalize/project.js +83 -0
- package/dist/rules/normalize/project.js.map +1 -0
- package/dist/rules/normalize/shorthand.d.ts +38 -0
- package/dist/rules/normalize/shorthand.js +388 -0
- package/dist/rules/normalize/shorthand.js.map +1 -0
- package/dist/rules/normalize/value.d.ts +23 -0
- package/dist/rules/normalize/value.js +162 -0
- package/dist/rules/normalize/value.js.map +1 -0
- package/dist/rules/normalize/wrapper.d.ts +16 -0
- package/dist/rules/normalize/wrapper.js +137 -0
- package/dist/rules/normalize/wrapper.js.map +1 -0
- package/dist/rules/ruleset.d.ts +93 -0
- package/dist/rules/ruleset.js +175 -0
- package/dist/rules/ruleset.js.map +1 -0
- package/mark.svg +30 -0
- package/package.json +69 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { aggregateImpact } from '../compare/impact.js';
|
|
2
|
+
import { componentsOf } from '../compare/diff/components.js';
|
|
3
|
+
import { impactTag } from '../compare/diff/delta.js';
|
|
4
|
+
import { fingerprintOfRoot } from './fingerprint.js';
|
|
5
|
+
import { appliesToSubject, isExpired, isUnder } from './scope.js';
|
|
6
|
+
/**
|
|
7
|
+
* Check a rule before it can absorb anything.
|
|
8
|
+
*
|
|
9
|
+
* Returns the problems rather than throwing, because a config carrying three bad
|
|
10
|
+
* rules should report three, and because the CLI's config parser wants to attach
|
|
11
|
+
* its own file and key to each one.
|
|
12
|
+
*/
|
|
13
|
+
export function validateIgnoreRule(rule, options = {}) {
|
|
14
|
+
const problems = [];
|
|
15
|
+
if (rule.id.trim() === '')
|
|
16
|
+
problems.push('needs an id');
|
|
17
|
+
if (rule.reason.trim() === '') {
|
|
18
|
+
problems.push('needs a reason: an ignore nobody can evaluate later is one nobody will ever remove');
|
|
19
|
+
}
|
|
20
|
+
// `hasPlace` is the caller's business because a place is resolved outside this
|
|
21
|
+
// package: a selector needs a DOM. The rule this enforces is the same either
|
|
22
|
+
// way — a rule that names no place and no shape absorbs everything its bands
|
|
23
|
+
// cover in every subject it lists, which is a tolerance.
|
|
24
|
+
const concrete = (options.hasPlace ?? false) ||
|
|
25
|
+
rule.whole === true ||
|
|
26
|
+
(rule.fingerprints !== undefined && rule.fingerprints.length > 0);
|
|
27
|
+
if (!concrete) {
|
|
28
|
+
problems.push(`"${rule.id}" names neither a place nor a shape, so there is nothing for it to be scoped ` +
|
|
29
|
+
'to; an ignore that is only a band is a tolerance');
|
|
30
|
+
}
|
|
31
|
+
if (rule.whole === true && rule.bands === undefined) {
|
|
32
|
+
// `whole` with no band is every difference in the subject, which is not an
|
|
33
|
+
// ignore — it is switching the subject off, and `subjects.exclude` says that
|
|
34
|
+
// in a word a reader cannot mistake.
|
|
35
|
+
problems.push(`"${rule.id}" covers the whole subject and narrows nothing, which silences it entirely; ` +
|
|
36
|
+
'exclude the subject if that is the intent');
|
|
37
|
+
}
|
|
38
|
+
if (rule.until !== undefined && Number.isNaN(Date.parse(rule.until))) {
|
|
39
|
+
problems.push(`until must be an ISO date, not "${rule.until}"`);
|
|
40
|
+
}
|
|
41
|
+
return problems;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The shape of one difference: its kind, its band, and which property moved.
|
|
45
|
+
*
|
|
46
|
+
* Everything positional and everything valued is deliberately absent. Two deltas
|
|
47
|
+
* with the same shape are the same *kind of thing happening*, which is the
|
|
48
|
+
* equivalence a flake needs and the one a coordinate cannot express.
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* Field separator inside a digest input.
|
|
52
|
+
*
|
|
53
|
+
* A byte that cannot occur in a component name, a property or a band, so no two
|
|
54
|
+
* different shapes can concatenate into the same string. A space would collide
|
|
55
|
+
* the moment a value contained one, which is the collision `digestCombine` uses
|
|
56
|
+
* the same character to avoid.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Apply ignores to a run's diffs, and account for every difference absorbed.
|
|
60
|
+
*
|
|
61
|
+
* Runs after `diffSnapshots` and before `buildDocket`: the docket's arithmetic is
|
|
62
|
+
* about which causes a reviewer signs off on, and a cause nobody is going to look
|
|
63
|
+
* at should not be in it. Everything removed here is counted, attributed to the
|
|
64
|
+
* rule that removed it, and reported.
|
|
65
|
+
*/
|
|
66
|
+
export function applyIgnores(diffs, rules, options = {}) {
|
|
67
|
+
if (rules.length === 0) {
|
|
68
|
+
return { diffs, register: emptyRegister() };
|
|
69
|
+
}
|
|
70
|
+
const now = options.now;
|
|
71
|
+
const expired = rules.filter((rule) => isExpired(rule, now)).map((rule) => rule.id);
|
|
72
|
+
const live = rules.filter((rule) => !isExpired(rule, now));
|
|
73
|
+
const tallies = new Map();
|
|
74
|
+
const fullyAbsorbed = [];
|
|
75
|
+
const filtered = [];
|
|
76
|
+
for (const diff of diffs) {
|
|
77
|
+
if (diff.identical || diff.deltas.length === 0) {
|
|
78
|
+
filtered.push(diff);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const sites = options.sites?.[diff.subjectId] ?? [];
|
|
82
|
+
const applicable = live.filter((rule) => appliesToSubject(rule, diff.subjectId));
|
|
83
|
+
if (applicable.length === 0) {
|
|
84
|
+
filtered.push(diff);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// Fingerprints are per root, so they are resolved once per root rather than
|
|
88
|
+
// once per delta: a root's shape is a property of the whole group, and asking
|
|
89
|
+
// it repeatedly would make the digest cost quadratic in a wide change set.
|
|
90
|
+
const fingerprints = new Map();
|
|
91
|
+
for (const root of diff.roots) {
|
|
92
|
+
const digest = fingerprintOfRoot(root);
|
|
93
|
+
for (const delta of root.deltas)
|
|
94
|
+
fingerprints.set(delta, digest);
|
|
95
|
+
}
|
|
96
|
+
const absorbedBy = new Map();
|
|
97
|
+
for (const delta of diff.deltas) {
|
|
98
|
+
const rule = applicable.find((candidate) => absorbs(candidate, delta, sites, fingerprints.get(delta)));
|
|
99
|
+
if (rule !== undefined)
|
|
100
|
+
absorbedBy.set(delta, rule);
|
|
101
|
+
}
|
|
102
|
+
if (absorbedBy.size === 0) {
|
|
103
|
+
filtered.push(diff);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const [delta, rule] of absorbedBy) {
|
|
107
|
+
const tally = tallies.get(rule.id) ?? newTally(rule);
|
|
108
|
+
tallies.set(rule.id, tally);
|
|
109
|
+
tally.deltas += 1;
|
|
110
|
+
tally.subjects.add(diff.subjectId);
|
|
111
|
+
tally.bands.add(delta.band);
|
|
112
|
+
const digest = fingerprints.get(delta);
|
|
113
|
+
if (digest !== undefined)
|
|
114
|
+
tally.fingerprints.add(digest);
|
|
115
|
+
}
|
|
116
|
+
const kept = diff.deltas.filter((delta) => !absorbedBy.has(delta));
|
|
117
|
+
if (kept.length === 0)
|
|
118
|
+
fullyAbsorbed.push(diff.subjectId);
|
|
119
|
+
filtered.push(rebuild(diff, kept, absorbedBy));
|
|
120
|
+
}
|
|
121
|
+
const absorbed = [...tallies.values()]
|
|
122
|
+
.map(finalizeTally)
|
|
123
|
+
.sort((a, b) => b.deltas - a.deltas || (a.rule < b.rule ? -1 : a.rule > b.rule ? 1 : 0));
|
|
124
|
+
const dead = rules
|
|
125
|
+
.filter((rule) => !tallies.has(rule.id) && !expired.includes(rule.id))
|
|
126
|
+
.map((rule) => rule.id);
|
|
127
|
+
return {
|
|
128
|
+
diffs: filtered,
|
|
129
|
+
register: {
|
|
130
|
+
absorbed,
|
|
131
|
+
dead,
|
|
132
|
+
expired,
|
|
133
|
+
fullyAbsorbed,
|
|
134
|
+
totalAbsorbed: absorbed.reduce((sum, entry) => sum + entry.deltas, 0),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The register as the paragraph a reader is owed.
|
|
140
|
+
*
|
|
141
|
+
* Absorption first, because that is what changed the verdict; dead and expired
|
|
142
|
+
* rules after it, because those are what the operator has to do something about.
|
|
143
|
+
* A register with nothing in it returns the empty string rather than a cheerful
|
|
144
|
+
* line — a run with no ignores should read exactly as it did before this existed.
|
|
145
|
+
*/
|
|
146
|
+
export function summarizeIgnores(register) {
|
|
147
|
+
const lines = [];
|
|
148
|
+
if (register.totalAbsorbed > 0) {
|
|
149
|
+
lines.push(`${register.totalAbsorbed} difference(s) absorbed by ${register.absorbed.length} ignore(s).`);
|
|
150
|
+
for (const entry of register.absorbed) {
|
|
151
|
+
lines.push(` ${entry.rule}: ${entry.deltas} in ${entry.subjects.length} subject(s), ` +
|
|
152
|
+
`${entry.bands.join('/')} — ${entry.reason}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (register.fullyAbsorbed.length > 0) {
|
|
156
|
+
lines.push(` ${register.fullyAbsorbed.length} subject(s) differed and were entirely ignored: ` +
|
|
157
|
+
`${register.fullyAbsorbed.slice(0, 3).join(', ')}` +
|
|
158
|
+
(register.fullyAbsorbed.length > 3 ? ` (+${register.fullyAbsorbed.length - 3} more)` : ''));
|
|
159
|
+
}
|
|
160
|
+
for (const rule of register.expired) {
|
|
161
|
+
lines.push(` [expired] ${rule} — past its date; it absorbed nothing and is now reporting`);
|
|
162
|
+
}
|
|
163
|
+
for (const rule of register.dead) {
|
|
164
|
+
lines.push(` [dead] ${rule} — absorbed nothing this run; either it is no longer needed, or it ` +
|
|
165
|
+
'stopped matching and something is being reported that you believe is silenced');
|
|
166
|
+
}
|
|
167
|
+
return lines.join('\n');
|
|
168
|
+
}
|
|
169
|
+
function newTally(rule) {
|
|
170
|
+
return {
|
|
171
|
+
rule,
|
|
172
|
+
deltas: 0,
|
|
173
|
+
subjects: new Set(),
|
|
174
|
+
bands: new Set(),
|
|
175
|
+
fingerprints: new Set(),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function finalizeTally(tally) {
|
|
179
|
+
return {
|
|
180
|
+
rule: tally.rule.id,
|
|
181
|
+
reason: tally.rule.reason,
|
|
182
|
+
deltas: tally.deltas,
|
|
183
|
+
subjects: [...tally.subjects],
|
|
184
|
+
bands: [...tally.bands],
|
|
185
|
+
fingerprints: [...tally.fingerprints],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function emptyRegister() {
|
|
189
|
+
return { absorbed: [], dead: [], expired: [], fullyAbsorbed: [], totalAbsorbed: 0 };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Whether a rule absorbs one delta.
|
|
193
|
+
*
|
|
194
|
+
* Order matters only for cost. A rule absorbs when it names *this* place or
|
|
195
|
+
* *this* shape and the band, if it named any, is one of them — so a rule that
|
|
196
|
+
* names nothing absorbs nothing, which is the property that keeps a config typo
|
|
197
|
+
* from silencing a suite.
|
|
198
|
+
*/
|
|
199
|
+
function absorbs(rule, delta, sites, fingerprint) {
|
|
200
|
+
if (rule.bands !== undefined && !rule.bands.includes(delta.band))
|
|
201
|
+
return false;
|
|
202
|
+
// Checked after the band and before any place, which is the order that makes
|
|
203
|
+
// it safe: `whole` is only ever reached by a rule that has already narrowed to
|
|
204
|
+
// the bands it absorbs, so "the whole subject" never means "everything in it".
|
|
205
|
+
if (rule.whole === true)
|
|
206
|
+
return true;
|
|
207
|
+
if (fingerprint !== undefined && rule.fingerprints?.includes(fingerprint))
|
|
208
|
+
return true;
|
|
209
|
+
return sites.some((site) => site.rule === rule.id && isUnder(delta.path, site.path));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Rebuild a diff around the deltas that survived.
|
|
213
|
+
*
|
|
214
|
+
* Roots keep their own band and cause — filtering deltas out of a root does not
|
|
215
|
+
* change what caused the rest of them — but a root left with nothing is dropped,
|
|
216
|
+
* because a cause with no observed effect is not a review item. Components and
|
|
217
|
+
* aggregate impact are recomputed from the survivors by the same functions that
|
|
218
|
+
* built them, so a filtered diff cannot disagree with an unfiltered one about
|
|
219
|
+
* what a given set of deltas implies.
|
|
220
|
+
*/
|
|
221
|
+
function rebuild(diff, kept, absorbedBy) {
|
|
222
|
+
const roots = [];
|
|
223
|
+
for (const root of diff.roots) {
|
|
224
|
+
const deltas = root.deltas.filter((delta) => !absorbedBy.has(delta));
|
|
225
|
+
if (deltas.length === 0)
|
|
226
|
+
continue;
|
|
227
|
+
roots.push(deltas.length === root.deltas.length ? root : { ...root, deltas });
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
...diff,
|
|
231
|
+
deltas: kept,
|
|
232
|
+
roots,
|
|
233
|
+
components: componentsOf(kept, roots),
|
|
234
|
+
impact: aggregateImpact(kept.map(impactTag)),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=ignore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ignore.js","sourceRoot":"","sources":["../../src/judge/ignore.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Band } from '../compare/band.js';\nimport { aggregateImpact } from '../compare/impact.js';\nimport { componentsOf } from '../compare/diff/components.js';\nimport { impactTag, type Delta, type Root } from '../compare/diff/delta.js';\nimport type { SemanticDiff } from '../compare/diff/index.js';\nimport type { Digest } from '../format/hash.js';\nimport { fingerprintOfRoot } from './fingerprint.js';\nimport type { IgnoreSite } from '../format/snapshot.js';\nimport { appliesToSubject, isExpired, isUnder } from './scope.js';\n\nexport type { IgnoreSite } from '../format/snapshot.js';\n\n/**\n * Ignores, and the accounting that stops one becoming a blind spot (spec 0024).\n *\n * The README refuses tolerances, and it is right to: a tolerance is an anonymous\n * number that hides whatever fits underneath it, chosen by whoever wrote the\n * default. An ignore is the opposite of that in every respect that matters, and\n * the difference is enforced here rather than asserted in a document:\n *\n * - it names a **place** or a **shape**, never a magnitude;\n * - it carries a **reason**, and a rule without one is refused;\n * - it says how much it absorbed, in how many subjects, every run;\n * - it is **reported when it absorbs nothing**, because an ignore that outlived\n * its flake is a hole in the suite nobody can see;\n * - it never produces `unchanged`. A subject whose every difference was absorbed\n * is a subject that was *ignored*, and the two words are not interchangeable.\n *\n * That last rule is ADR-0002's, applied to a second reason for not having looked.\n * A profile that cannot observe a band reports `unobserved` rather than passing\n * it; an operator who declined to look at a region gets the same treatment, for\n * the same reason — a green subject that is indistinguishable from a genuinely\n * unchanged one makes every count on this page decorative.\n *\n * ## Two ways to name what is not the subject\n *\n * **By place.** A subtree, resolved once by whatever held the document, arrives\n * here as an {@link IgnoreSite}: a path, and the box it occupied. One declaration\n * serves both tiers — the semantic tier drops deltas under the path, the raster\n * tier subtracts the box from the change mask — because two declarations would\n * let the tiers disagree about what the subject is, and a region that is ignored\n * semantically and compared on pixels reports `unexplained`, the highest severity\n * in the system, for something the operator already excluded.\n *\n * **By shape.** A {@link fingerprintOfRoot} is a digest of a difference with its\n * position and its values removed, so the same artifact anywhere in any subject\n * digests the same. An ignore keyed on one silences a known flake *without*\n * blinding the image it appears in: a different regression in the same place has\n * a different shape and is still reported. A coordinate mask cannot make that\n * distinction and never will, which is why no rule here accepts one: a rectangle\n * is a place that stops covering the thing it was drawn around the first time the\n * layout moves, and a selector is a place that does not.\n *\n * ## What is not here\n *\n * A band on its own. `bands` narrows a rule that already names a place or a\n * shape; a rule carrying only a band would be a tolerance wearing an ignore's\n * clothes, and {@link validateIgnoreRule} refuses it. Which bands *block* is a\n * different question with a different answer — `Policy.blocking` in `intent.ts`.\n */\n\nexport interface IgnoreRule {\n /** Stable name, used in the config, in the report, and by `IgnoreSite.rule`. */\n readonly id: string;\n\n /**\n * Why this is not the subject. Required.\n *\n * Not documentation. Six months on, the only question anyone asks about an\n * ignore is whether it is still true, and a rule that cannot answer it gets\n * kept out of superstition. Carried into the report so the answer is in front\n * of whoever is reading the failure it did not absorb.\n */\n readonly reason: string;\n\n /**\n * Subjects this applies to. `*` matches any run of characters. Absent means all.\n *\n * Narrow by default is the wrong default here and the right one to offer: a\n * flake that appears in one story should be silenced in one story, and an\n * operator who genuinely means \"everywhere\" can say so by omission — visibly,\n * in the same file.\n */\n readonly subjects?: readonly string[];\n\n /**\n * Difference shapes this absorbs. See {@link fingerprintOfRoot}.\n *\n * The form to prefer. It survives layout changes, applies across subjects\n * without listing them, and leaves the rest of the image being tested.\n */\n readonly fingerprints?: readonly Digest[];\n\n /** Narrows what the rule absorbs where it already applies. Never on its own. */\n readonly bands?: readonly Band[];\n\n /**\n * The place is the whole subject.\n *\n * The one way a rule may carry `bands` and no selector, and it has to be said\n * rather than inferred from their absence — because \"no place\" and \"every\n * place\" are the two readings of the same missing field, and one of them is the\n * tolerance this mechanism exists to refuse.\n *\n * Set by {@link import('./sensitivity.js').asIgnore} and by nothing else. A\n * sensitivity declares what a subject *is asserted on*, so its place genuinely\n * is the subject: \"this route asserts on layout\" absorbs every token-band\n * difference in it, wherever in it they land. The config parser does not accept\n * this key, so a hand-written ignore still cannot reach it.\n */\n readonly whole?: boolean;\n\n /**\n * ISO date after which this stops absorbing and starts being reported.\n *\n * Declared rather than inferred, so the default lifetime of a blind spot is\n * \"until somebody decides again\" instead of \"forever\". An expired rule is not\n * an error — the run proceeds and the differences it used to absorb come back,\n * which is the point.\n */\n readonly until?: string;\n}\n\n/** What one rule absorbed in one run. */\nexport interface AbsorbedByRule {\n readonly rule: string;\n readonly reason: string;\n readonly deltas: number;\n readonly subjects: readonly string[];\n readonly bands: readonly Band[];\n\n /**\n * The shapes this rule actually absorbed.\n *\n * The most useful field for an operator holding a place-scoped rule: it names\n * what that place was covering, so a whole excluded subtree can be narrowed to\n * the one shape that is actually noisy without anyone guessing a digest.\n */\n readonly fingerprints: readonly Digest[];\n}\n\nexport interface IgnoreRegister {\n readonly absorbed: readonly AbsorbedByRule[];\n\n /**\n * Rules that absorbed nothing this run.\n *\n * Not a warning about the config file — a report about the suite. Either the\n * flake is fixed and the rule is a hole that should close, or the rule stopped\n * matching and something is being silently reported that the operator believes\n * is silenced. Both are worth a line.\n */\n readonly dead: readonly string[];\n\n /** Rules past their `until` date. They absorbed nothing, by construction. */\n readonly expired: readonly string[];\n\n /**\n * Subjects whose every difference was absorbed.\n *\n * The list that must never be folded into \"unchanged\". These are subjects that\n * differed and were not looked at.\n */\n readonly fullyAbsorbed: readonly string[];\n\n readonly totalAbsorbed: number;\n}\n\nexport interface IgnoreOutcome {\n /** The diffs with absorbed deltas removed, in the order they were given. */\n readonly diffs: readonly SemanticDiff[];\n readonly register: IgnoreRegister;\n}\n\nexport interface IgnoreOptions {\n /** Sites per subject id, as resolved against each candidate document. */\n readonly sites?: Readonly<Record<string, readonly IgnoreSite[]>>;\n\n /**\n * Today, as an ISO date, for `until`. Supplied rather than read.\n *\n * `core` has no clock — a package that reads one cannot be tested for what it\n * does on the day a rule expires, which is the only day the field matters.\n */\n readonly now?: string;\n}\n\n/**\n * Check a rule before it can absorb anything.\n *\n * Returns the problems rather than throwing, because a config carrying three bad\n * rules should report three, and because the CLI's config parser wants to attach\n * its own file and key to each one.\n */\nexport function validateIgnoreRule(\n rule: IgnoreRule,\n options: { readonly hasPlace?: boolean } = {},\n): readonly string[] {\n const problems: string[] = [];\n\n if (rule.id.trim() === '') problems.push('needs an id');\n if (rule.reason.trim() === '') {\n problems.push(\n 'needs a reason: an ignore nobody can evaluate later is one nobody will ever remove',\n );\n }\n\n // `hasPlace` is the caller's business because a place is resolved outside this\n // package: a selector needs a DOM. The rule this enforces is the same either\n // way — a rule that names no place and no shape absorbs everything its bands\n // cover in every subject it lists, which is a tolerance.\n const concrete =\n (options.hasPlace ?? false) ||\n rule.whole === true ||\n (rule.fingerprints !== undefined && rule.fingerprints.length > 0);\n\n if (!concrete) {\n problems.push(\n `\"${rule.id}\" names neither a place nor a shape, so there is nothing for it to be scoped ` +\n 'to; an ignore that is only a band is a tolerance',\n );\n }\n\n if (rule.whole === true && rule.bands === undefined) {\n // `whole` with no band is every difference in the subject, which is not an\n // ignore — it is switching the subject off, and `subjects.exclude` says that\n // in a word a reader cannot mistake.\n problems.push(\n `\"${rule.id}\" covers the whole subject and narrows nothing, which silences it entirely; ` +\n 'exclude the subject if that is the intent',\n );\n }\n\n if (rule.until !== undefined && Number.isNaN(Date.parse(rule.until))) {\n problems.push(`until must be an ISO date, not \"${rule.until}\"`);\n }\n\n return problems;\n}\n\n/**\n * The shape of one difference: its kind, its band, and which property moved.\n *\n * Everything positional and everything valued is deliberately absent. Two deltas\n * with the same shape are the same *kind of thing happening*, which is the\n * equivalence a flake needs and the one a coordinate cannot express.\n */\n/**\n * Field separator inside a digest input.\n *\n * A byte that cannot occur in a component name, a property or a band, so no two\n * different shapes can concatenate into the same string. A space would collide\n * the moment a value contained one, which is the collision `digestCombine` uses\n * the same character to avoid.\n */\n/**\n * Apply ignores to a run's diffs, and account for every difference absorbed.\n *\n * Runs after `diffSnapshots` and before `buildDocket`: the docket's arithmetic is\n * about which causes a reviewer signs off on, and a cause nobody is going to look\n * at should not be in it. Everything removed here is counted, attributed to the\n * rule that removed it, and reported.\n */\nexport function applyIgnores(\n diffs: readonly SemanticDiff[],\n rules: readonly IgnoreRule[],\n options: IgnoreOptions = {},\n): IgnoreOutcome {\n if (rules.length === 0) {\n return { diffs, register: emptyRegister() };\n }\n\n const now = options.now;\n const expired = rules.filter((rule) => isExpired(rule, now)).map((rule) => rule.id);\n const live = rules.filter((rule) => !isExpired(rule, now));\n\n const tallies = new Map<string, Tally>();\n const fullyAbsorbed: string[] = [];\n const filtered: SemanticDiff[] = [];\n\n for (const diff of diffs) {\n if (diff.identical || diff.deltas.length === 0) {\n filtered.push(diff);\n continue;\n }\n\n const sites = options.sites?.[diff.subjectId] ?? [];\n const applicable = live.filter((rule) => appliesToSubject(rule, diff.subjectId));\n\n if (applicable.length === 0) {\n filtered.push(diff);\n continue;\n }\n\n // Fingerprints are per root, so they are resolved once per root rather than\n // once per delta: a root's shape is a property of the whole group, and asking\n // it repeatedly would make the digest cost quadratic in a wide change set.\n const fingerprints = new Map<Delta, Digest>();\n for (const root of diff.roots) {\n const digest = fingerprintOfRoot(root);\n for (const delta of root.deltas) fingerprints.set(delta, digest);\n }\n\n const absorbedBy = new Map<Delta, IgnoreRule>();\n\n for (const delta of diff.deltas) {\n const rule = applicable.find((candidate) =>\n absorbs(candidate, delta, sites, fingerprints.get(delta)),\n );\n if (rule !== undefined) absorbedBy.set(delta, rule);\n }\n\n if (absorbedBy.size === 0) {\n filtered.push(diff);\n continue;\n }\n\n for (const [delta, rule] of absorbedBy) {\n const tally = tallies.get(rule.id) ?? newTally(rule);\n tallies.set(rule.id, tally);\n tally.deltas += 1;\n tally.subjects.add(diff.subjectId);\n tally.bands.add(delta.band);\n const digest = fingerprints.get(delta);\n if (digest !== undefined) tally.fingerprints.add(digest);\n }\n\n const kept = diff.deltas.filter((delta) => !absorbedBy.has(delta));\n if (kept.length === 0) fullyAbsorbed.push(diff.subjectId);\n\n filtered.push(rebuild(diff, kept, absorbedBy));\n }\n\n const absorbed = [...tallies.values()]\n .map(finalizeTally)\n .sort(\n (a, b) => b.deltas - a.deltas || (a.rule < b.rule ? -1 : a.rule > b.rule ? 1 : 0),\n );\n\n const dead = rules\n .filter((rule) => !tallies.has(rule.id) && !expired.includes(rule.id))\n .map((rule) => rule.id);\n\n return {\n diffs: filtered,\n register: {\n absorbed,\n dead,\n expired,\n fullyAbsorbed,\n totalAbsorbed: absorbed.reduce((sum, entry) => sum + entry.deltas, 0),\n },\n };\n}\n\n/**\n * The register as the paragraph a reader is owed.\n *\n * Absorption first, because that is what changed the verdict; dead and expired\n * rules after it, because those are what the operator has to do something about.\n * A register with nothing in it returns the empty string rather than a cheerful\n * line — a run with no ignores should read exactly as it did before this existed.\n */\nexport function summarizeIgnores(register: IgnoreRegister): string {\n const lines: string[] = [];\n\n if (register.totalAbsorbed > 0) {\n lines.push(\n `${register.totalAbsorbed} difference(s) absorbed by ${register.absorbed.length} ignore(s).`,\n );\n for (const entry of register.absorbed) {\n lines.push(\n ` ${entry.rule}: ${entry.deltas} in ${entry.subjects.length} subject(s), ` +\n `${entry.bands.join('/')} — ${entry.reason}`,\n );\n }\n }\n\n if (register.fullyAbsorbed.length > 0) {\n lines.push(\n ` ${register.fullyAbsorbed.length} subject(s) differed and were entirely ignored: ` +\n `${register.fullyAbsorbed.slice(0, 3).join(', ')}` +\n (register.fullyAbsorbed.length > 3 ? ` (+${register.fullyAbsorbed.length - 3} more)` : ''),\n );\n }\n\n for (const rule of register.expired) {\n lines.push(` [expired] ${rule} — past its date; it absorbed nothing and is now reporting`);\n }\n\n for (const rule of register.dead) {\n lines.push(\n ` [dead] ${rule} — absorbed nothing this run; either it is no longer needed, or it ` +\n 'stopped matching and something is being reported that you believe is silenced',\n );\n }\n\n return lines.join('\\n');\n}\n\ninterface Tally {\n readonly rule: IgnoreRule;\n deltas: number;\n readonly subjects: Set<string>;\n readonly bands: Set<Band>;\n readonly fingerprints: Set<Digest>;\n}\n\nfunction newTally(rule: IgnoreRule): Tally {\n return {\n rule,\n deltas: 0,\n subjects: new Set(),\n bands: new Set(),\n fingerprints: new Set(),\n };\n}\n\nfunction finalizeTally(tally: Tally): AbsorbedByRule {\n return {\n rule: tally.rule.id,\n reason: tally.rule.reason,\n deltas: tally.deltas,\n subjects: [...tally.subjects],\n bands: [...tally.bands],\n fingerprints: [...tally.fingerprints],\n };\n}\n\nfunction emptyRegister(): IgnoreRegister {\n return { absorbed: [], dead: [], expired: [], fullyAbsorbed: [], totalAbsorbed: 0 };\n}\n\n/**\n * Whether a rule absorbs one delta.\n *\n * Order matters only for cost. A rule absorbs when it names *this* place or\n * *this* shape and the band, if it named any, is one of them — so a rule that\n * names nothing absorbs nothing, which is the property that keeps a config typo\n * from silencing a suite.\n */\nfunction absorbs(\n rule: IgnoreRule,\n delta: Delta,\n sites: readonly IgnoreSite[],\n fingerprint: Digest | undefined,\n): boolean {\n if (rule.bands !== undefined && !rule.bands.includes(delta.band)) return false;\n\n // Checked after the band and before any place, which is the order that makes\n // it safe: `whole` is only ever reached by a rule that has already narrowed to\n // the bands it absorbs, so \"the whole subject\" never means \"everything in it\".\n if (rule.whole === true) return true;\n\n if (fingerprint !== undefined && rule.fingerprints?.includes(fingerprint)) return true;\n\n return sites.some((site) => site.rule === rule.id && isUnder(delta.path, site.path));\n}\n\n/**\n * Rebuild a diff around the deltas that survived.\n *\n * Roots keep their own band and cause — filtering deltas out of a root does not\n * change what caused the rest of them — but a root left with nothing is dropped,\n * because a cause with no observed effect is not a review item. Components and\n * aggregate impact are recomputed from the survivors by the same functions that\n * built them, so a filtered diff cannot disagree with an unfiltered one about\n * what a given set of deltas implies.\n */\nfunction rebuild(\n diff: SemanticDiff,\n kept: readonly Delta[],\n absorbedBy: ReadonlyMap<Delta, IgnoreRule>,\n): SemanticDiff {\n const roots: Root[] = [];\n\n for (const root of diff.roots) {\n const deltas = root.deltas.filter((delta) => !absorbedBy.has(delta));\n if (deltas.length === 0) continue;\n roots.push(deltas.length === root.deltas.length ? root : { ...root, deltas });\n }\n\n return {\n ...diff,\n deltas: kept,\n roots,\n components: componentsOf(kept, roots),\n impact: aggregateImpact(kept.map(impactTag)),\n };\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@variance-authority/core/judge` — from what moved to whether anyone should mind.
|
|
3
|
+
*
|
|
4
|
+
* Verdicts and their ordering, intent claims and their adjudication, and the
|
|
5
|
+
* docket a reader is handed. This is where policy lives, and it is deliberately
|
|
6
|
+
* the last group and the smallest: everything above it produces facts, and a
|
|
7
|
+
* team that disagrees with the policy replaces this and keeps the rest.
|
|
8
|
+
*
|
|
9
|
+
* `UNOBSERVED` is the group's whole argument in one export. A band a profile
|
|
10
|
+
* cannot see is not a band that passed, and the type system will not let the two
|
|
11
|
+
* be spelled the same way.
|
|
12
|
+
*
|
|
13
|
+
* `inspect` is the one member that needs no baseline. Everything else here
|
|
14
|
+
* decides between two snapshots; it decides about one, which is the only way a
|
|
15
|
+
* defect present on the *first* run is ever reported rather than approved into
|
|
16
|
+
* the baseline.
|
|
17
|
+
*
|
|
18
|
+
* `ignore` is the group's second refusal to conflate two things. A difference the
|
|
19
|
+
* operator declined to look at is not a difference that did not happen, so an
|
|
20
|
+
* absorbed delta is counted, attributed to the rule that absorbed it, and the
|
|
21
|
+
* rule is reported when it absorbs nothing — the same treatment `UNOBSERVED`
|
|
22
|
+
* gives a band nobody could see.
|
|
23
|
+
*/
|
|
24
|
+
export type { Verdict, BandOutcome } from './verdict.js';
|
|
25
|
+
export { UNOBSERVED, severityOf, blocks } from './verdict.js';
|
|
26
|
+
export { adjudicate, summarizeAdjudication } from './intent.js';
|
|
27
|
+
export type { Intent, IntentClaim, Policy, Adjudication, Adjudicated } from './intent.js';
|
|
28
|
+
export { buildDocket, summarize } from './docket.js';
|
|
29
|
+
export type { Docket, DocketEntry, DocketOptions } from './docket.js';
|
|
30
|
+
export { findingMark, inspect, summarizeFindings } from './inspect.js';
|
|
31
|
+
export type { Finding, FindingRule, InspectionReportOptions } from './inspect.js';
|
|
32
|
+
export { applyIgnores, summarizeIgnores, validateIgnoreRule } from './ignore.js';
|
|
33
|
+
export { isUnder, matchesGlob, appliesToSubject, isExpired } from './scope.js';
|
|
34
|
+
export type { Scoped } from './scope.js';
|
|
35
|
+
export { fingerprintOfRoot, fingerprintOfMask, shapeOfDelta } from './fingerprint.js';
|
|
36
|
+
export type { MaskFingerprintOptions } from './fingerprint.js';
|
|
37
|
+
export type { IgnoreRule, IgnoreOptions, IgnoreOutcome, IgnoreRegister, AbsorbedByRule, } from './ignore.js';
|
|
38
|
+
export { applySensitivity, absorbsEntirely, asIgnore, bandsOf, relaxes, summarizeSensitivity, } from './sensitivity.js';
|
|
39
|
+
export type { Level, SensitivityRule, SensitivityOutcome, SensitivityRegister, Relaxation, } from './sensitivity.js';
|
|
40
|
+
export { startTrail, record, progress, summarizeTrail } from './trail.js';
|
|
41
|
+
export type { Trail, TrailStep, Progress } from './trail.js';
|
|
42
|
+
export { compareLocales } from './locale.js';
|
|
43
|
+
export type { LocaleComparison, LocaleOptions, Uncompared } from './locale.js';
|
|
44
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@variance-authority/core/judge` — from what moved to whether anyone should mind.
|
|
3
|
+
*
|
|
4
|
+
* Verdicts and their ordering, intent claims and their adjudication, and the
|
|
5
|
+
* docket a reader is handed. This is where policy lives, and it is deliberately
|
|
6
|
+
* the last group and the smallest: everything above it produces facts, and a
|
|
7
|
+
* team that disagrees with the policy replaces this and keeps the rest.
|
|
8
|
+
*
|
|
9
|
+
* `UNOBSERVED` is the group's whole argument in one export. A band a profile
|
|
10
|
+
* cannot see is not a band that passed, and the type system will not let the two
|
|
11
|
+
* be spelled the same way.
|
|
12
|
+
*
|
|
13
|
+
* `inspect` is the one member that needs no baseline. Everything else here
|
|
14
|
+
* decides between two snapshots; it decides about one, which is the only way a
|
|
15
|
+
* defect present on the *first* run is ever reported rather than approved into
|
|
16
|
+
* the baseline.
|
|
17
|
+
*
|
|
18
|
+
* `ignore` is the group's second refusal to conflate two things. A difference the
|
|
19
|
+
* operator declined to look at is not a difference that did not happen, so an
|
|
20
|
+
* absorbed delta is counted, attributed to the rule that absorbed it, and the
|
|
21
|
+
* rule is reported when it absorbs nothing — the same treatment `UNOBSERVED`
|
|
22
|
+
* gives a band nobody could see.
|
|
23
|
+
*/
|
|
24
|
+
export { UNOBSERVED, severityOf, blocks } from './verdict.js';
|
|
25
|
+
export { adjudicate, summarizeAdjudication } from './intent.js';
|
|
26
|
+
export { buildDocket, summarize } from './docket.js';
|
|
27
|
+
export { findingMark, inspect, summarizeFindings } from './inspect.js';
|
|
28
|
+
export { applyIgnores, summarizeIgnores, validateIgnoreRule } from './ignore.js';
|
|
29
|
+
export { isUnder, matchesGlob, appliesToSubject, isExpired } from './scope.js';
|
|
30
|
+
export { fingerprintOfRoot, fingerprintOfMask, shapeOfDelta } from './fingerprint.js';
|
|
31
|
+
export { applySensitivity, absorbsEntirely, asIgnore, bandsOf, relaxes, summarizeSensitivity, } from './sensitivity.js';
|
|
32
|
+
export { startTrail, record, progress, summarizeTrail } from './trail.js';
|
|
33
|
+
export { compareLocales } from './locale.js';
|
|
34
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/judge/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE9D,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAGhE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGvE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE/E,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAUtF,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,QAAQ,EACR,OAAO,EACP,OAAO,EACP,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG1E,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC","sourcesContent":["/**\n * `@variance-authority/core/judge` — from what moved to whether anyone should mind.\n *\n * Verdicts and their ordering, intent claims and their adjudication, and the\n * docket a reader is handed. This is where policy lives, and it is deliberately\n * the last group and the smallest: everything above it produces facts, and a\n * team that disagrees with the policy replaces this and keeps the rest.\n *\n * `UNOBSERVED` is the group's whole argument in one export. A band a profile\n * cannot see is not a band that passed, and the type system will not let the two\n * be spelled the same way.\n *\n * `inspect` is the one member that needs no baseline. Everything else here\n * decides between two snapshots; it decides about one, which is the only way a\n * defect present on the *first* run is ever reported rather than approved into\n * the baseline.\n *\n * `ignore` is the group's second refusal to conflate two things. A difference the\n * operator declined to look at is not a difference that did not happen, so an\n * absorbed delta is counted, attributed to the rule that absorbed it, and the\n * rule is reported when it absorbs nothing — the same treatment `UNOBSERVED`\n * gives a band nobody could see.\n */\n\nexport type { Verdict, BandOutcome } from './verdict.js';\nexport { UNOBSERVED, severityOf, blocks } from './verdict.js';\n\nexport { adjudicate, summarizeAdjudication } from './intent.js';\nexport type { Intent, IntentClaim, Policy, Adjudication, Adjudicated } from './intent.js';\n\nexport { buildDocket, summarize } from './docket.js';\nexport type { Docket, DocketEntry, DocketOptions } from './docket.js';\n\nexport { findingMark, inspect, summarizeFindings } from './inspect.js';\nexport type { Finding, FindingRule, InspectionReportOptions } from './inspect.js';\n\nexport { applyIgnores, summarizeIgnores, validateIgnoreRule } from './ignore.js';\nexport { isUnder, matchesGlob, appliesToSubject, isExpired } from './scope.js';\nexport type { Scoped } from './scope.js';\nexport { fingerprintOfRoot, fingerprintOfMask, shapeOfDelta } from './fingerprint.js';\nexport type { MaskFingerprintOptions } from './fingerprint.js';\nexport type {\n IgnoreRule,\n IgnoreOptions,\n IgnoreOutcome,\n IgnoreRegister,\n AbsorbedByRule,\n} from './ignore.js';\n\nexport {\n applySensitivity,\n absorbsEntirely,\n asIgnore,\n bandsOf,\n relaxes,\n summarizeSensitivity,\n} from './sensitivity.js';\nexport type {\n Level,\n SensitivityRule,\n SensitivityOutcome,\n SensitivityRegister,\n Relaxation,\n} from './sensitivity.js';\n\nexport { startTrail, record, progress, summarizeTrail } from './trail.js';\nexport type { Trail, TrailStep, Progress } from './trail.js';\n\nexport { compareLocales } from './locale.js';\nexport type { LocaleComparison, LocaleOptions, Uncompared } from './locale.js';\n"]}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Band } from '../compare/band.js';
|
|
2
|
+
import type { OwnerFrame, SourceLocation } from '../format/provenance.js';
|
|
3
|
+
import type { NodePath, SemanticSnapshot } from '../format/snapshot.js';
|
|
4
|
+
import { type SourceIndex } from '../attribute/source.js';
|
|
5
|
+
/**
|
|
6
|
+
* What a render says without a baseline.
|
|
7
|
+
*
|
|
8
|
+
* Two producers, one `Finding` type. `inspect` reads a single snapshot;
|
|
9
|
+
* `compareLocales` reads two renders of one subject in different languages,
|
|
10
|
+
* which is also not a regression comparison — both renders are correct. Sharing
|
|
11
|
+
* the type is what lets the report, the CLI and the MCP tools carry both without
|
|
12
|
+
* knowing which produced what.
|
|
13
|
+
*
|
|
14
|
+
* The rest of this comment is about `inspect`, which reads one snapshot with no
|
|
15
|
+
* baseline and no second run.
|
|
16
|
+
*
|
|
17
|
+
* Everything else in this package answers *what changed*, which requires two of
|
|
18
|
+
* something. That framing has a blind spot the whole category shares: **a defect
|
|
19
|
+
* that was present on the first run is invisible to a comparison forever.** A
|
|
20
|
+
* button that never had an accessible name compares equal to itself on every run
|
|
21
|
+
* until someone happens to edit it, and a picture of it is a perfectly good
|
|
22
|
+
* picture. Approving the first baseline approves the defect.
|
|
23
|
+
*
|
|
24
|
+
* These rules read the normalized snapshot and report defects in it. Consequences
|
|
25
|
+
* worth stating, because they are the practical argument for having the document
|
|
26
|
+
* rather than an image of it:
|
|
27
|
+
*
|
|
28
|
+
* - **A fresh checkout with zero baselines is already useful.** The expensive
|
|
29
|
+
* part of adopting a visual-regression tool is that it says nothing until it
|
|
30
|
+
* has a history; this half says something on the first run.
|
|
31
|
+
* - **Every finding names a component and a file**, through the same provenance
|
|
32
|
+
* chain a delta uses. A rule engine that reports a DOM path reports where the
|
|
33
|
+
* symptom is; this reports whose JSX wrote it.
|
|
34
|
+
* - **They band as `a11y`**, so a project that writes `blocking: ['a11y']` gets
|
|
35
|
+
* both halves under one policy: regressions found by comparison, and defects
|
|
36
|
+
* found by inspection.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately *not* an axe-core reimplementation. Axe runs against a live DOM
|
|
39
|
+
* with computed visibility, contrast and focus order, and does dozens of things
|
|
40
|
+
* this cannot. What is here is the subset a normalized snapshot can decide
|
|
41
|
+
* without guessing — and the subset it can decide **offline, from a stored
|
|
42
|
+
* artifact, months later**, which is a thing axe cannot do at all.
|
|
43
|
+
*
|
|
44
|
+
* There is no severity field. A rule that needs one to be tolerable is a rule
|
|
45
|
+
* whose condition is too broad, and the fix is a better condition or no rule.
|
|
46
|
+
*
|
|
47
|
+
* **Why there is no contrast rule, stated here so nobody adds one badly later.**
|
|
48
|
+
* The snapshot carries a resolved `color` and `background-color` per node, so a
|
|
49
|
+
* check looks like four lines and would be wrong: the background a glyph is
|
|
50
|
+
* actually painted on is whatever is behind it, which is a stacking question a
|
|
51
|
+
* layout engine answers and a document does not. A node with a transparent
|
|
52
|
+
* background over a dark ancestor, an image, a gradient, or a positioned sibling
|
|
53
|
+
* all read as "background-color: rgba(0,0,0,0)" here. A rule that is right most
|
|
54
|
+
* of the time about accessibility is worse than no rule: it gets disabled after
|
|
55
|
+
* the second false alarm, and takes the four that work with it. Spec 0009
|
|
56
|
+
* records the same for focus order and anything about motion.
|
|
57
|
+
*/
|
|
58
|
+
export type FindingRule =
|
|
59
|
+
/** An interactive control with no accessible name. Nothing announces it. */
|
|
60
|
+
'control-without-name'
|
|
61
|
+
/** A meaningful image with neither a name nor an explicit `alt=""`. */
|
|
62
|
+
| 'image-without-alt'
|
|
63
|
+
/** Heading levels jump forward by more than one. */
|
|
64
|
+
| 'heading-level-skipped'
|
|
65
|
+
/** A control inside another control. Only one of them is reachable. */
|
|
66
|
+
| 'nested-interactive'
|
|
67
|
+
/** An id reference that resolves to nothing inside this subject. */
|
|
68
|
+
| 'dangling-reference'
|
|
69
|
+
/** A control whose accessible name does not contain its visible label. */
|
|
70
|
+
| 'label-mismatch'
|
|
71
|
+
/** Two landmarks of one role that nothing tells apart. */
|
|
72
|
+
| 'duplicate-landmark'
|
|
73
|
+
/** A table with no header cells. */
|
|
74
|
+
| 'table-without-headers'
|
|
75
|
+
/** `tabindex` above zero, which reorders focus for the whole page. */
|
|
76
|
+
| 'positive-tabindex'
|
|
77
|
+
/**
|
|
78
|
+
* The same string in two locales. From `compareLocales`, not from `inspect` —
|
|
79
|
+
* one render cannot know whether its text was translated.
|
|
80
|
+
*/
|
|
81
|
+
| 'untranslated'
|
|
82
|
+
/** A box outside the box that contains it, in one locale and not the other. */
|
|
83
|
+
| 'overflows-container';
|
|
84
|
+
/**
|
|
85
|
+
* The identity of a finding, as a string two runs can be compared on.
|
|
86
|
+
*
|
|
87
|
+
* A finding is *the same finding* when the same rule fires on the same node. Not
|
|
88
|
+
* the same sentence: `what` quotes the text it found, so a copy edit beside a
|
|
89
|
+
* control with no accessible name would read as the old defect going away and a
|
|
90
|
+
* new one arriving in the same place. And not the same component either — a
|
|
91
|
+
* component renders in many places, and every one of them would collapse to one.
|
|
92
|
+
*
|
|
93
|
+
* Small on purpose. This is what a baseline carries so that the next run can say
|
|
94
|
+
* whether a defect it found was already there, and a baseline sidecar rides
|
|
95
|
+
* beside a PNG in a tracked directory: a mark is a few dozen bytes and a stored
|
|
96
|
+
* `Finding` is a paragraph. It can only answer membership, which is the only
|
|
97
|
+
* question the next run is allowed to ask of it — the same restraint
|
|
98
|
+
* `Described.components` is written under.
|
|
99
|
+
*/
|
|
100
|
+
export declare function findingMark(finding: {
|
|
101
|
+
readonly rule: string;
|
|
102
|
+
readonly path: string;
|
|
103
|
+
}): string;
|
|
104
|
+
export interface Finding {
|
|
105
|
+
readonly rule: FindingRule;
|
|
106
|
+
/**
|
|
107
|
+
* The band this finding would block under.
|
|
108
|
+
*
|
|
109
|
+
* Carried so that inspection and comparison answer to one policy. A team that
|
|
110
|
+
* blocks `a11y` should not have to discover that it blocks regressions and not
|
|
111
|
+
* defects.
|
|
112
|
+
*/
|
|
113
|
+
readonly band: Band;
|
|
114
|
+
readonly path: NodePath;
|
|
115
|
+
/** One sentence, naming the thing rather than the rule. */
|
|
116
|
+
readonly what: string;
|
|
117
|
+
/** Spoken outside-in, from `locate` — `main → region "Todos" → item 2 of 3`. */
|
|
118
|
+
readonly where?: string;
|
|
119
|
+
/** The component whose JSX created the node, when provenance reached it. */
|
|
120
|
+
readonly component?: string;
|
|
121
|
+
readonly owners?: readonly OwnerFrame[];
|
|
122
|
+
/**
|
|
123
|
+
* The line that wrote *this element*, when a JSX runtime recorded one.
|
|
124
|
+
*
|
|
125
|
+
* Different from — and better than — resolving `component` through the source
|
|
126
|
+
* index, which answers with where the component is *declared*. A defect is
|
|
127
|
+
* rarely at a declaration: a button with no accessible name is a specific
|
|
128
|
+
* element on a specific line inside that component, and that line is the edit.
|
|
129
|
+
* The index stays as the fallback, because it needs no build change.
|
|
130
|
+
*
|
|
131
|
+
* Requires `@variance-authority/jsx-source` in the transform. Absent otherwise,
|
|
132
|
+
* and absence is normal.
|
|
133
|
+
*/
|
|
134
|
+
readonly source?: SourceLocation;
|
|
135
|
+
}
|
|
136
|
+
export declare function inspect(snapshot: SemanticSnapshot): readonly Finding[];
|
|
137
|
+
export interface InspectionReportOptions {
|
|
138
|
+
/** Component → file, so a finding names an edit rather than an identifier. */
|
|
139
|
+
readonly source?: SourceIndex;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Findings as the thing a reviewer or an agent reads.
|
|
143
|
+
*
|
|
144
|
+
* Same shape as `summarizeAdjudication`: what, where, which file. A finding a
|
|
145
|
+
* reader has to go and locate is a finding that gets skipped.
|
|
146
|
+
*/
|
|
147
|
+
export declare function summarizeFindings(findings: readonly Finding[], options?: InspectionReportOptions): string;
|
|
148
|
+
//# sourceMappingURL=inspect.d.ts.map
|