@variance-authority/mcp 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 +89 -0
- package/LICENSE +21 -0
- package/README.md +407 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.js +32 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/observability-subject.d.ts +22 -0
- package/dist/observability-subject.js +2 -0
- package/dist/observability-subject.js.map +1 -0
- package/dist/presentation.d.ts +6 -0
- package/dist/presentation.js +64 -0
- package/dist/presentation.js.map +1 -0
- package/dist/protocol.d.ts +82 -0
- package/dist/protocol.js +186 -0
- package/dist/protocol.js.map +1 -0
- package/dist/server.d.ts +71 -0
- package/dist/server.js +103 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/adjudicate.d.ts +25 -0
- package/dist/tools/adjudicate.js +122 -0
- package/dist/tools/adjudicate.js.map +1 -0
- package/dist/tools/attention.d.ts +13 -0
- package/dist/tools/attention.js +151 -0
- package/dist/tools/attention.js.map +1 -0
- package/dist/tools/changelog.d.ts +38 -0
- package/dist/tools/changelog.js +201 -0
- package/dist/tools/changelog.js.map +1 -0
- package/dist/tools/changes.d.ts +31 -0
- package/dist/tools/changes.js +120 -0
- package/dist/tools/changes.js.map +1 -0
- package/dist/tools/composition.d.ts +13 -0
- package/dist/tools/composition.js +366 -0
- package/dist/tools/composition.js.map +1 -0
- package/dist/tools/describe.d.ts +20 -0
- package/dist/tools/describe.js +279 -0
- package/dist/tools/describe.js.map +1 -0
- package/dist/tools/diff.d.ts +20 -0
- package/dist/tools/diff.js +85 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/explain-verdict.d.ts +25 -0
- package/dist/tools/explain-verdict.js +92 -0
- package/dist/tools/explain-verdict.js.map +1 -0
- package/dist/tools/findings.d.ts +40 -0
- package/dist/tools/findings.js +130 -0
- package/dist/tools/findings.js.map +1 -0
- package/dist/tools/narrowing.d.ts +26 -0
- package/dist/tools/narrowing.js +38 -0
- package/dist/tools/narrowing.js.map +1 -0
- package/dist/tools/observability.d.ts +7 -0
- package/dist/tools/observability.js +199 -0
- package/dist/tools/observability.js.map +1 -0
- package/dist/tools/presentations.d.ts +6 -0
- package/dist/tools/presentations.js +74 -0
- package/dist/tools/presentations.js.map +1 -0
- package/dist/tools/run-signals.d.ts +12 -0
- package/dist/tools/run-signals.js +115 -0
- package/dist/tools/run-signals.js.map +1 -0
- package/dist/tools/scenarios.d.ts +6 -0
- package/dist/tools/scenarios.js +62 -0
- package/dist/tools/scenarios.js.map +1 -0
- package/dist/tools/source-tests.d.ts +5 -0
- package/dist/tools/source-tests.js +95 -0
- package/dist/tools/source-tests.js.map +1 -0
- package/dist/tools/subject.d.ts +45 -0
- package/dist/tools/subject.js +57 -0
- package/dist/tools/subject.js.map +1 -0
- package/dist/tools/summary.d.ts +31 -0
- package/dist/tools/summary.js +436 -0
- package/dist/tools/summary.js.map +1 -0
- package/dist/tools/test-signals.d.ts +16 -0
- package/dist/tools/test-signals.js +106 -0
- package/dist/tools/test-signals.js.map +1 -0
- package/dist/tools/tool.d.ts +64 -0
- package/dist/tools/tool.js +9 -0
- package/dist/tools/tool.js.map +1 -0
- package/dist/tools/trace-component.d.ts +17 -0
- package/dist/tools/trace-component.js +50 -0
- package/dist/tools/trace-component.js.map +1 -0
- package/dist/tools/vantage-lines.d.ts +44 -0
- package/dist/tools/vantage-lines.js +66 -0
- package/dist/tools/vantage-lines.js.map +1 -0
- package/dist/tools/variations.d.ts +27 -0
- package/dist/tools/variations.js +125 -0
- package/dist/tools/variations.js.map +1 -0
- package/dist/tools.d.ts +120 -0
- package/dist/tools.js +127 -0
- package/dist/tools.js.map +1 -0
- package/mark.svg +30 -0
- package/package.json +64 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { AGE_WORDS, ageOf, byArrival, byBand, carriedLine, findingTotals, mixedAges, } from '@variance-authority/report';
|
|
2
|
+
import { describePresentation } from '../presentation.js';
|
|
3
|
+
import { subjectOf, unobserved } from './subject.js';
|
|
4
|
+
/**
|
|
5
|
+
* `variance_describe`, and the region format nothing else prints.
|
|
6
|
+
*
|
|
7
|
+
* `regionLine` stays with the tool because the region list is this answer's whole
|
|
8
|
+
* substance, and everything around it is an ordering decision that belongs to
|
|
9
|
+
* this tool rather than to regions in general: the leak warning above the list
|
|
10
|
+
* because it changes what every line below means, the findings before it because
|
|
11
|
+
* an `unchanged` subject can carry nothing else, and the truncation count after
|
|
12
|
+
* it because a capped list that does not say so reads as a complete one.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* One subject, in full: what changed, where it is, and which file to open.
|
|
16
|
+
*
|
|
17
|
+
* The tool an agent calls after the summary, and the one that has to be complete
|
|
18
|
+
* — an agent that has to ask three more questions to locate an edit will guess
|
|
19
|
+
* instead.
|
|
20
|
+
*/
|
|
21
|
+
export const describe = {
|
|
22
|
+
name: 'variance_describe',
|
|
23
|
+
description: 'Everything known about one subject: the ranked regions, the component each belongs to, ' +
|
|
24
|
+
'a landmark description of where it is on the page, the source file to edit, and each ' +
|
|
25
|
+
"region's shape fingerprint — which is what `variance accept --shape` and an `ignore` " +
|
|
26
|
+
'rule are keyed on when a difference is not a code problem. Causes are listed before ' +
|
|
27
|
+
'collateral. A subject the run did not observe is answered with why, not refused.',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: { subject: { type: 'string', description: 'Subject id from variance_summary.' } },
|
|
31
|
+
required: ['subject'],
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
},
|
|
34
|
+
run(report, input) {
|
|
35
|
+
const located = subjectOf(report, input);
|
|
36
|
+
if (!located.observed)
|
|
37
|
+
return unobserved(located.entry);
|
|
38
|
+
const { observation } = located;
|
|
39
|
+
const lines = [
|
|
40
|
+
`[${label(observation)}] ${observation.subject}`,
|
|
41
|
+
observation.because,
|
|
42
|
+
// Ahead of the leak note, because it disqualifies that one too. A clean
|
|
43
|
+
// world re-collection was never taken on this subject — its answer would
|
|
44
|
+
// have been a comparison between two readings that do not agree anyway —
|
|
45
|
+
// so there is no order-dependence claim here to place second.
|
|
46
|
+
...(observation.unstable !== undefined && observation.unstable.absorbed === undefined
|
|
47
|
+
? [
|
|
48
|
+
`NOT A COMPONENT CHANGE: ${observation.unstable.because}. The regions below are`,
|
|
49
|
+
'the difference between one of those two readings and the baseline, so which',
|
|
50
|
+
'ones appear here is decided by a race. Do not review them and do not accept',
|
|
51
|
+
'this subject. Fix what moves between two readings of the same page' +
|
|
52
|
+
(observation.unstable.components.length === 0
|
|
53
|
+
? ', starting from the subject itself — no snapshot was collected, so nothing'
|
|
54
|
+
+ ' could name the component that moved.'
|
|
55
|
+
: `, starting at ${unstableAt(observation.unstable.components)}` +
|
|
56
|
+
(observation.unstable.bands.length === 0
|
|
57
|
+
? '.'
|
|
58
|
+
: ` (${observation.unstable.bands.join(', ')}).`)),
|
|
59
|
+
]
|
|
60
|
+
: []),
|
|
61
|
+
// What the record says, directly under the instruction it qualifies. An
|
|
62
|
+
// agent handed "fix what moves between two readings" acts differently when
|
|
63
|
+
// the answer is "this has fired in eleven of the last twelve sweeps" than
|
|
64
|
+
// when it is "twice in March, and nine sweeps have been clean since" — the
|
|
65
|
+
// second is a fix that already landed, and rewriting it is a day spent
|
|
66
|
+
// re-solving somebody's solved problem.
|
|
67
|
+
...(observation.unstable !== undefined ? recurrence(report, observation.subject) : []),
|
|
68
|
+
// The experiment, spelled out. An agent that has just been told to fix
|
|
69
|
+
// what moves between two readings needs a way to find out whether it did,
|
|
70
|
+
// and the alternative it reaches for otherwise is a full run — three
|
|
71
|
+
// hundred subjects and a browser, to answer a question about one of them.
|
|
72
|
+
...(observation.unstable !== undefined && observation.unstable.absorbed === undefined
|
|
73
|
+
? [
|
|
74
|
+
'VERIFY A FIX WITH:',
|
|
75
|
+
` variance run --subjects '${observation.subject}' --flakes`,
|
|
76
|
+
'which reads this subject twice and nothing else, and exits 1 while the two',
|
|
77
|
+
'readings still disagree — even when every verdict is green.',
|
|
78
|
+
]
|
|
79
|
+
: []),
|
|
80
|
+
// The same movement, inside the boundary the subject declared. Said rather
|
|
81
|
+
// than suppressed: an agent that later sees this subject go green wants to
|
|
82
|
+
// know a level was doing work, and the rule's name is what makes that
|
|
83
|
+
// auditable. It carries no instruction, because there is nothing to do.
|
|
84
|
+
...(observation.unstable?.absorbed !== undefined
|
|
85
|
+
? [
|
|
86
|
+
`read differently between two readings in ${(observation.unstable.bands ?? []).join(', ')}, ` +
|
|
87
|
+
`and \`${observation.unstable.absorbed.rule}\` asserts on ` +
|
|
88
|
+
`${observation.unstable.absorbed.level} — so none of it is asserted on here. ` +
|
|
89
|
+
'Working as declared; the regions below are the comparison, not the movement.',
|
|
90
|
+
]
|
|
91
|
+
: []),
|
|
92
|
+
// Placed under the verdict, because it changes what every line below it
|
|
93
|
+
// means. The regions are still correct — those pixels really did move, in
|
|
94
|
+
// those components — but they are the shape of a leak rather than the shape
|
|
95
|
+
// of an edit, and an agent that reads the region list first starts editing a
|
|
96
|
+
// component whose source nobody changed.
|
|
97
|
+
...((observation.unstable === undefined || observation.unstable.absorbed !== undefined) &&
|
|
98
|
+
observation.alone?.reproduced === false
|
|
99
|
+
? [
|
|
100
|
+
`NOT A COMPONENT CHANGE: ${observation.alone?.because}. The regions below are real`,
|
|
101
|
+
'but they are what the leak did, not what an edit did. Do not change these',
|
|
102
|
+
'components. Find the subject that writes the state this one reads by bisecting',
|
|
103
|
+
'run order — the run cannot name it, because module-level state is invisible to',
|
|
104
|
+
'anything a rendered document can observe about itself.',
|
|
105
|
+
]
|
|
106
|
+
: []),
|
|
107
|
+
...(observation.missingFonts !== undefined && observation.missingFonts.length > 0
|
|
108
|
+
? [
|
|
109
|
+
`warning: the renderer lacked ${observation.missingFonts.join(', ')}; ` +
|
|
110
|
+
'these images are of a substituted font and their metrics are not the product’s',
|
|
111
|
+
]
|
|
112
|
+
: []),
|
|
113
|
+
];
|
|
114
|
+
// What the record says about the components this subject's change was
|
|
115
|
+
// attributed to. Placed after the verdict and before the regions, because it
|
|
116
|
+
// changes how the region list should be read: a component that has caused an
|
|
117
|
+
// approved change in eleven of the last forty runs is a component whose next
|
|
118
|
+
// change is unremarkable, and one that has never moved before is the opposite.
|
|
119
|
+
lines.push(...componentChurn(report, observation));
|
|
120
|
+
// Findings before regions. A subject can be `unchanged` and still carry
|
|
121
|
+
// them, in which case they are the only thing this tool has to say, and a
|
|
122
|
+
// reader who stopped at "nothing changed" would never reach them.
|
|
123
|
+
if (observation.findings !== undefined && observation.findings.length > 0) {
|
|
124
|
+
lines.push('', findingTotals(observation.findings) + ':', ...findingLines(observation.findings));
|
|
125
|
+
}
|
|
126
|
+
if (observation.signals?.presentation !== undefined) {
|
|
127
|
+
lines.push('', ...describePresentation(observation.signals.presentation));
|
|
128
|
+
}
|
|
129
|
+
if (observation.regions.length === 0)
|
|
130
|
+
return lines.join('\n');
|
|
131
|
+
lines.push('', ...observation.regions.map(regionLine));
|
|
132
|
+
if (observation.truncated !== undefined && observation.truncated.regions > 0) {
|
|
133
|
+
// Never a silent cap: a truncated list that does not say so reads as
|
|
134
|
+
// complete coverage, and the reader has no way to tell the difference.
|
|
135
|
+
lines.push(`+${observation.truncated.regions} smaller region(s) not listed ` +
|
|
136
|
+
`(${observation.truncated.pixels}px)`);
|
|
137
|
+
}
|
|
138
|
+
if (observation.images !== undefined) {
|
|
139
|
+
lines.push('', 'images: ' +
|
|
140
|
+
Object.entries(observation.images)
|
|
141
|
+
.map(([kind, path]) => `${kind}=${path}`)
|
|
142
|
+
.join(' '));
|
|
143
|
+
}
|
|
144
|
+
return lines.join('\n');
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* What the subject *is*, which is not always its verdict.
|
|
149
|
+
*
|
|
150
|
+
* Three words over one, and the order is the precedence: instability disqualifies
|
|
151
|
+
* the clean-world answer, which in turn re-reads the verdict. All three subjects
|
|
152
|
+
* are `changed` — the pixels did move — and all three need different work, so a
|
|
153
|
+
* reader given the verdict alone acts on the wrong one two times out of three.
|
|
154
|
+
*/
|
|
155
|
+
function label(observation) {
|
|
156
|
+
if (observation.unstable !== undefined && observation.unstable.absorbed === undefined) {
|
|
157
|
+
return 'unstable';
|
|
158
|
+
}
|
|
159
|
+
if (observation.alone?.reproduced === false)
|
|
160
|
+
return 'order-dependent';
|
|
161
|
+
return observation.verdict;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* How often the components named here have changed before.
|
|
165
|
+
*
|
|
166
|
+
* Only the causes, and only the ones the record answered for. A component this
|
|
167
|
+
* report has no entry for is not a component that has never changed — it is one
|
|
168
|
+
* nobody asked about, either because no store answered or because the run capped
|
|
169
|
+
* how many it asked. Silence is therefore silent rather than reassuring: nothing
|
|
170
|
+
* is printed for it, and the run's own warnings carry the reason.
|
|
171
|
+
*/
|
|
172
|
+
function componentChurn(report, observation) {
|
|
173
|
+
if (report.churn === undefined)
|
|
174
|
+
return [];
|
|
175
|
+
const named = [
|
|
176
|
+
...new Set(observation.regions
|
|
177
|
+
.filter((region) => region.cause && region.component !== undefined)
|
|
178
|
+
.map((region) => region.component)),
|
|
179
|
+
];
|
|
180
|
+
const lines = named.flatMap((component) => {
|
|
181
|
+
const record = report.churn?.[component];
|
|
182
|
+
return record === undefined ? [] : [` ${record.because}`];
|
|
183
|
+
});
|
|
184
|
+
return lines.length === 0 ? [] : ['', 'HOW OFTEN THESE COMPONENTS CHANGE:', ...lines];
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* How often this has happened before, or the fact that nobody was asked.
|
|
188
|
+
*
|
|
189
|
+
* The absent arm is not politeness. Two readings put a floor under flakiness and
|
|
190
|
+
* never a ceiling, so "no record answered" and "this has never happened" are
|
|
191
|
+
* different sentences with different next actions — and only one of them is
|
|
192
|
+
* something this report can support.
|
|
193
|
+
*/
|
|
194
|
+
function recurrence(report, subject) {
|
|
195
|
+
const record = report.flakiness?.[subject];
|
|
196
|
+
if (record === undefined) {
|
|
197
|
+
return [
|
|
198
|
+
'No history record answered for this subject, so nothing here says whether it has',
|
|
199
|
+
'happened before. Absence of a record is not a first occurrence.',
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
const named = record.causes
|
|
203
|
+
.map((cause) => [cause.component, cause.band].filter((part) => part !== undefined).join(' '))
|
|
204
|
+
.filter((label) => label !== '')
|
|
205
|
+
.slice(0, 3);
|
|
206
|
+
return [
|
|
207
|
+
`OVER THE RECORDED WINDOW: ${record.because}.` +
|
|
208
|
+
(named.length === 0 ? '' : ` Seen in: ${named.join(', ')}.`),
|
|
209
|
+
...(record.sweepsSince > 0 && record.occurrences > 1
|
|
210
|
+
? [
|
|
211
|
+
'It has been quiet for the last few sweeps, so check whether a fix already landed',
|
|
212
|
+
'before writing another one.',
|
|
213
|
+
]
|
|
214
|
+
: []),
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
/** The unstable components as `Name file:line`, which is what an editor opens. */
|
|
218
|
+
function unstableAt(components) {
|
|
219
|
+
return components
|
|
220
|
+
.map((component) => component.file === undefined ? component.name : `${component.name} ${component.file}`)
|
|
221
|
+
.join(', ');
|
|
222
|
+
}
|
|
223
|
+
function regionLine(region) {
|
|
224
|
+
const head = region.unattributed === true
|
|
225
|
+
? `unattributed — a region no box contained, which usually means the scale or origin was wrong`
|
|
226
|
+
: (region.component ?? region.path ?? 'unknown');
|
|
227
|
+
return [
|
|
228
|
+
` ${region.cause ? 'cause ' : 'collateral'} ${region.pixels}px ` +
|
|
229
|
+
`at ${region.x},${region.y} ${region.width}×${region.height} — ${head}`,
|
|
230
|
+
region.where !== undefined ? ` in ${region.where}` : null,
|
|
231
|
+
region.file !== undefined ? ` ${region.file}` : null,
|
|
232
|
+
// The digest, on the region it describes. An agent's two non-code responses
|
|
233
|
+
// to a recurring difference are `variance accept --shape <it>` and an
|
|
234
|
+
// `ignore` rule keyed on it, and neither is reachable without the value —
|
|
235
|
+
// printing it in a different section would make an agent guess which line it
|
|
236
|
+
// belonged to.
|
|
237
|
+
region.fingerprint !== undefined ? ` shape ${region.fingerprint}` : null,
|
|
238
|
+
]
|
|
239
|
+
.filter((line) => line !== null)
|
|
240
|
+
.join('\n');
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* The defects under the band each was filed as, dated where the record says.
|
|
244
|
+
*
|
|
245
|
+
* The same two questions the review page and the HTML report answer above their
|
|
246
|
+
* own lists, and the same fold answering them — an agent reading a flat list of
|
|
247
|
+
* eleven rules has to know which of them the project blocks on and which of them
|
|
248
|
+
* it inherited, and neither is derivable from the rule name.
|
|
249
|
+
*
|
|
250
|
+
* The band heads a group rather than prefixing a row because most renders carry
|
|
251
|
+
* one band, and a word repeated down a column is a word a reader stops seeing.
|
|
252
|
+
*
|
|
253
|
+
* Above the bands is the split the review page and the HTML report draw: what
|
|
254
|
+
* this change brought, then — under its own count — what it did not. There is no
|
|
255
|
+
* folding to do in text, so the separation is an order and a line, and the order
|
|
256
|
+
* is the part that matters. An agent handed twenty inherited defects first will
|
|
257
|
+
* either fix twenty or fix none.
|
|
258
|
+
*/
|
|
259
|
+
function findingLines(findings) {
|
|
260
|
+
const { arrived, rest, dated } = byArrival(findings);
|
|
261
|
+
if (!dated)
|
|
262
|
+
return bandLines(findings);
|
|
263
|
+
const carried = carriedLine(findings);
|
|
264
|
+
return [
|
|
265
|
+
...(arrived.length === 0 ? [] : bandLines(arrived)),
|
|
266
|
+
...(carried === undefined ? [] : [` ${carried} — separate work`, ...bandLines(rest)]),
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
/** One list, banded, dated per row only where the rows disagree. */
|
|
270
|
+
function bandLines(findings) {
|
|
271
|
+
const dated = mixedAges(findings);
|
|
272
|
+
return byBand(findings).flatMap((group) => [
|
|
273
|
+
` ${group.title}`,
|
|
274
|
+
...group.findings.map((finding) => ` [${finding.rule}] ${finding.what}` +
|
|
275
|
+
(dated ? ` — ${AGE_WORDS[ageOf(finding)]}` : '') +
|
|
276
|
+
(finding.file === undefined ? '' : `\n ${finding.file}`)),
|
|
277
|
+
]);
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=describe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"describe.js","sourceRoot":"","sources":["../../src/tools/describe.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AGE_WORDS,\n ageOf,\n byArrival,\n byBand,\n carriedLine,\n findingTotals,\n mixedAges,\n} from '@variance-authority/report';\nimport type {\n FindingRecord,\n FlakinessRecord,\n RegionRecord,\n RunReport,\n} from '@variance-authority/report';\nimport { describePresentation } from '../presentation.js';\nimport { subjectOf, unobserved } from './subject.js';\nimport type { Tool } from './tool.js';\n\n/**\n * `variance_describe`, and the region format nothing else prints.\n *\n * `regionLine` stays with the tool because the region list is this answer's whole\n * substance, and everything around it is an ordering decision that belongs to\n * this tool rather than to regions in general: the leak warning above the list\n * because it changes what every line below means, the findings before it because\n * an `unchanged` subject can carry nothing else, and the truncation count after\n * it because a capped list that does not say so reads as a complete one.\n */\n\n/**\n * One subject, in full: what changed, where it is, and which file to open.\n *\n * The tool an agent calls after the summary, and the one that has to be complete\n * — an agent that has to ask three more questions to locate an edit will guess\n * instead.\n */\nexport const describe: Tool = {\n name: 'variance_describe',\n description:\n 'Everything known about one subject: the ranked regions, the component each belongs to, ' +\n 'a landmark description of where it is on the page, the source file to edit, and each ' +\n \"region's shape fingerprint — which is what `variance accept --shape` and an `ignore` \" +\n 'rule are keyed on when a difference is not a code problem. Causes are listed before ' +\n 'collateral. A subject the run did not observe is answered with why, not refused.',\n inputSchema: {\n type: 'object',\n properties: { subject: { type: 'string', description: 'Subject id from variance_summary.' } },\n required: ['subject'],\n additionalProperties: false,\n },\n\n run(report, input) {\n const located = subjectOf(report, input);\n if (!located.observed) return unobserved(located.entry);\n const { observation } = located;\n\n const lines = [\n `[${label(observation)}] ${observation.subject}`,\n observation.because,\n // Ahead of the leak note, because it disqualifies that one too. A clean\n // world re-collection was never taken on this subject — its answer would\n // have been a comparison between two readings that do not agree anyway —\n // so there is no order-dependence claim here to place second.\n ...(observation.unstable !== undefined && observation.unstable.absorbed === undefined\n ? [\n `NOT A COMPONENT CHANGE: ${observation.unstable.because}. The regions below are`,\n 'the difference between one of those two readings and the baseline, so which',\n 'ones appear here is decided by a race. Do not review them and do not accept',\n 'this subject. Fix what moves between two readings of the same page' +\n (observation.unstable.components.length === 0\n ? ', starting from the subject itself — no snapshot was collected, so nothing'\n + ' could name the component that moved.'\n : `, starting at ${unstableAt(observation.unstable.components)}` +\n (observation.unstable.bands.length === 0\n ? '.'\n : ` (${observation.unstable.bands.join(', ')}).`)),\n ]\n : []),\n // What the record says, directly under the instruction it qualifies. An\n // agent handed \"fix what moves between two readings\" acts differently when\n // the answer is \"this has fired in eleven of the last twelve sweeps\" than\n // when it is \"twice in March, and nine sweeps have been clean since\" — the\n // second is a fix that already landed, and rewriting it is a day spent\n // re-solving somebody's solved problem.\n ...(observation.unstable !== undefined ? recurrence(report, observation.subject) : []),\n // The experiment, spelled out. An agent that has just been told to fix\n // what moves between two readings needs a way to find out whether it did,\n // and the alternative it reaches for otherwise is a full run — three\n // hundred subjects and a browser, to answer a question about one of them.\n ...(observation.unstable !== undefined && observation.unstable.absorbed === undefined\n ? [\n 'VERIFY A FIX WITH:',\n ` variance run --subjects '${observation.subject}' --flakes`,\n 'which reads this subject twice and nothing else, and exits 1 while the two',\n 'readings still disagree — even when every verdict is green.',\n ]\n : []),\n // The same movement, inside the boundary the subject declared. Said rather\n // than suppressed: an agent that later sees this subject go green wants to\n // know a level was doing work, and the rule's name is what makes that\n // auditable. It carries no instruction, because there is nothing to do.\n ...(observation.unstable?.absorbed !== undefined\n ? [\n `read differently between two readings in ${(observation.unstable.bands ?? []).join(', ')}, ` +\n `and \\`${observation.unstable.absorbed.rule}\\` asserts on ` +\n `${observation.unstable.absorbed.level} — so none of it is asserted on here. ` +\n 'Working as declared; the regions below are the comparison, not the movement.',\n ]\n : []),\n // Placed under the verdict, because it changes what every line below it\n // means. The regions are still correct — those pixels really did move, in\n // those components — but they are the shape of a leak rather than the shape\n // of an edit, and an agent that reads the region list first starts editing a\n // component whose source nobody changed.\n ...((observation.unstable === undefined || observation.unstable.absorbed !== undefined) &&\n observation.alone?.reproduced === false\n ? [\n `NOT A COMPONENT CHANGE: ${observation.alone?.because}. The regions below are real`,\n 'but they are what the leak did, not what an edit did. Do not change these',\n 'components. Find the subject that writes the state this one reads by bisecting',\n 'run order — the run cannot name it, because module-level state is invisible to',\n 'anything a rendered document can observe about itself.',\n ]\n : []),\n ...(observation.missingFonts !== undefined && observation.missingFonts.length > 0\n ? [\n `warning: the renderer lacked ${observation.missingFonts.join(', ')}; ` +\n 'these images are of a substituted font and their metrics are not the product’s',\n ]\n : []),\n ];\n\n // What the record says about the components this subject's change was\n // attributed to. Placed after the verdict and before the regions, because it\n // changes how the region list should be read: a component that has caused an\n // approved change in eleven of the last forty runs is a component whose next\n // change is unremarkable, and one that has never moved before is the opposite.\n lines.push(...componentChurn(report, observation));\n\n // Findings before regions. A subject can be `unchanged` and still carry\n // them, in which case they are the only thing this tool has to say, and a\n // reader who stopped at \"nothing changed\" would never reach them.\n if (observation.findings !== undefined && observation.findings.length > 0) {\n lines.push('', findingTotals(observation.findings) + ':', ...findingLines(observation.findings));\n }\n\n if (observation.signals?.presentation !== undefined) {\n lines.push('', ...describePresentation(observation.signals.presentation));\n }\n\n if (observation.regions.length === 0) return lines.join('\\n');\n\n lines.push('', ...observation.regions.map(regionLine));\n\n if (observation.truncated !== undefined && observation.truncated.regions > 0) {\n // Never a silent cap: a truncated list that does not say so reads as\n // complete coverage, and the reader has no way to tell the difference.\n lines.push(\n `+${observation.truncated.regions} smaller region(s) not listed ` +\n `(${observation.truncated.pixels}px)`,\n );\n }\n\n if (observation.images !== undefined) {\n lines.push(\n '',\n 'images: ' +\n Object.entries(observation.images)\n .map(([kind, path]) => `${kind}=${path}`)\n .join(' '),\n );\n }\n\n return lines.join('\\n');\n },\n};\n\n/**\n * What the subject *is*, which is not always its verdict.\n *\n * Three words over one, and the order is the precedence: instability disqualifies\n * the clean-world answer, which in turn re-reads the verdict. All three subjects\n * are `changed` — the pixels did move — and all three need different work, so a\n * reader given the verdict alone acts on the wrong one two times out of three.\n */\nfunction label(observation: {\n readonly verdict: string;\n readonly unstable?: { readonly absorbed?: unknown };\n readonly alone?: { readonly reproduced: boolean };\n}): string {\n if (observation.unstable !== undefined && observation.unstable.absorbed === undefined) {\n return 'unstable';\n }\n if (observation.alone?.reproduced === false) return 'order-dependent';\n return observation.verdict;\n}\n\n/**\n * How often the components named here have changed before.\n *\n * Only the causes, and only the ones the record answered for. A component this\n * report has no entry for is not a component that has never changed — it is one\n * nobody asked about, either because no store answered or because the run capped\n * how many it asked. Silence is therefore silent rather than reassuring: nothing\n * is printed for it, and the run's own warnings carry the reason.\n */\nfunction componentChurn(\n report: RunReport,\n observation: { readonly regions: readonly RegionRecord[] },\n): readonly string[] {\n if (report.churn === undefined) return [];\n\n const named = [\n ...new Set(\n observation.regions\n .filter((region) => region.cause && region.component !== undefined)\n .map((region) => region.component as string),\n ),\n ];\n\n const lines = named.flatMap((component) => {\n const record = report.churn?.[component];\n return record === undefined ? [] : [` ${record.because}`];\n });\n\n return lines.length === 0 ? [] : ['', 'HOW OFTEN THESE COMPONENTS CHANGE:', ...lines];\n}\n\n/**\n * How often this has happened before, or the fact that nobody was asked.\n *\n * The absent arm is not politeness. Two readings put a floor under flakiness and\n * never a ceiling, so \"no record answered\" and \"this has never happened\" are\n * different sentences with different next actions — and only one of them is\n * something this report can support.\n */\nfunction recurrence(report: RunReport, subject: string): readonly string[] {\n const record = report.flakiness?.[subject];\n\n if (record === undefined) {\n return [\n 'No history record answered for this subject, so nothing here says whether it has',\n 'happened before. Absence of a record is not a first occurrence.',\n ];\n }\n\n const named = record.causes\n .map((cause: FlakinessRecord['causes'][number]) =>\n [cause.component, cause.band].filter((part) => part !== undefined).join(' '),\n )\n .filter((label: string) => label !== '')\n .slice(0, 3);\n\n return [\n `OVER THE RECORDED WINDOW: ${record.because}.` +\n (named.length === 0 ? '' : ` Seen in: ${named.join(', ')}.`),\n ...(record.sweepsSince > 0 && record.occurrences > 1\n ? [\n 'It has been quiet for the last few sweeps, so check whether a fix already landed',\n 'before writing another one.',\n ]\n : []),\n ];\n}\n\n/** The unstable components as `Name file:line`, which is what an editor opens. */\nfunction unstableAt(\n components: readonly { readonly name: string; readonly file?: string }[],\n): string {\n return components\n .map((component) =>\n component.file === undefined ? component.name : `${component.name} ${component.file}`,\n )\n .join(', ');\n}\n\nfunction regionLine(region: RegionRecord): string {\n const head = region.unattributed === true\n ? `unattributed — a region no box contained, which usually means the scale or origin was wrong`\n : (region.component ?? region.path ?? 'unknown');\n\n return [\n ` ${region.cause ? 'cause ' : 'collateral'} ${region.pixels}px ` +\n `at ${region.x},${region.y} ${region.width}×${region.height} — ${head}`,\n region.where !== undefined ? ` in ${region.where}` : null,\n region.file !== undefined ? ` ${region.file}` : null,\n // The digest, on the region it describes. An agent's two non-code responses\n // to a recurring difference are `variance accept --shape <it>` and an\n // `ignore` rule keyed on it, and neither is reachable without the value —\n // printing it in a different section would make an agent guess which line it\n // belonged to.\n region.fingerprint !== undefined ? ` shape ${region.fingerprint}` : null,\n ]\n .filter((line): line is string => line !== null)\n .join('\\n');\n}\n\n/**\n * The defects under the band each was filed as, dated where the record says.\n *\n * The same two questions the review page and the HTML report answer above their\n * own lists, and the same fold answering them — an agent reading a flat list of\n * eleven rules has to know which of them the project blocks on and which of them\n * it inherited, and neither is derivable from the rule name.\n *\n * The band heads a group rather than prefixing a row because most renders carry\n * one band, and a word repeated down a column is a word a reader stops seeing.\n *\n * Above the bands is the split the review page and the HTML report draw: what\n * this change brought, then — under its own count — what it did not. There is no\n * folding to do in text, so the separation is an order and a line, and the order\n * is the part that matters. An agent handed twenty inherited defects first will\n * either fix twenty or fix none.\n */\nfunction findingLines(findings: readonly FindingRecord[]): readonly string[] {\n const { arrived, rest, dated } = byArrival(findings);\n if (!dated) return bandLines(findings);\n\n const carried = carriedLine(findings);\n\n return [\n ...(arrived.length === 0 ? [] : bandLines(arrived)),\n ...(carried === undefined ? [] : [` ${carried} — separate work`, ...bandLines(rest)]),\n ];\n}\n\n/** One list, banded, dated per row only where the rows disagree. */\nfunction bandLines(findings: readonly FindingRecord[]): readonly string[] {\n const dated = mixedAges(findings);\n\n return byBand(findings).flatMap((group) => [\n ` ${group.title}`,\n ...group.findings.map(\n (finding) =>\n ` [${finding.rule}] ${finding.what}` +\n (dated ? ` — ${AGE_WORDS[ageOf(finding)]}` : '') +\n (finding.file === undefined ? '' : `\\n ${finding.file}`),\n ),\n ]);\n}\n\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Tool } from './tool.js';
|
|
2
|
+
export type StateDifference = {
|
|
3
|
+
readonly kind: 'added';
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly value: unknown;
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: 'removed';
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly value: unknown;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: 'changed';
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly before: unknown;
|
|
14
|
+
readonly after: unknown;
|
|
15
|
+
};
|
|
16
|
+
/** Compare two JSON-compatible states without assigning meaning to either one. */
|
|
17
|
+
export declare function diffState(before: unknown, after: unknown): readonly StateDifference[];
|
|
18
|
+
/** The one stateful tool every served subject receives. */
|
|
19
|
+
export declare const diff: Tool<unknown>;
|
|
20
|
+
//# sourceMappingURL=diff.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { NO_ARGS } from './tool.js';
|
|
2
|
+
/** Compare two JSON-compatible states without assigning meaning to either one. */
|
|
3
|
+
export function diffState(before, after) {
|
|
4
|
+
const differences = [];
|
|
5
|
+
visit('$', before, after, differences);
|
|
6
|
+
return differences;
|
|
7
|
+
}
|
|
8
|
+
/** The one stateful tool every served subject receives. */
|
|
9
|
+
export const diff = {
|
|
10
|
+
name: 'variance_diff',
|
|
11
|
+
description: 'Compare the current state with the state held from the previous successful MCP tool invocation. ' +
|
|
12
|
+
'The first invocation records state and has nothing to compare.',
|
|
13
|
+
inputSchema: NO_ARGS,
|
|
14
|
+
run(current, _input, invocation) {
|
|
15
|
+
if (invocation?.previous === undefined) {
|
|
16
|
+
return 'No previous MCP invocation state. The current state is now remembered.';
|
|
17
|
+
}
|
|
18
|
+
const differences = diffState(invocation.previous, current);
|
|
19
|
+
if (differences.length === 0) {
|
|
20
|
+
return 'The current state matches the previous MCP invocation.';
|
|
21
|
+
}
|
|
22
|
+
return [
|
|
23
|
+
`The current state differs from the previous MCP invocation at ${String(differences.length)} path(s).`,
|
|
24
|
+
...differences.map(describe),
|
|
25
|
+
].join('\n');
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
function visit(path, before, after, differences) {
|
|
29
|
+
if (Object.is(before, after))
|
|
30
|
+
return;
|
|
31
|
+
if (isRecord(before) && isRecord(after)) {
|
|
32
|
+
const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
const next = `${path}${property(key)}`;
|
|
35
|
+
if (!(key in before)) {
|
|
36
|
+
differences.push({ kind: 'added', path: next, value: after[key] });
|
|
37
|
+
}
|
|
38
|
+
else if (!(key in after)) {
|
|
39
|
+
differences.push({ kind: 'removed', path: next, value: before[key] });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
visit(next, before[key], after[key], differences);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
48
|
+
const length = Math.max(before.length, after.length);
|
|
49
|
+
for (let index = 0; index < length; index += 1) {
|
|
50
|
+
const next = `${path}[${String(index)}]`;
|
|
51
|
+
if (index >= before.length) {
|
|
52
|
+
differences.push({ kind: 'added', path: next, value: after[index] });
|
|
53
|
+
}
|
|
54
|
+
else if (index >= after.length) {
|
|
55
|
+
differences.push({ kind: 'removed', path: next, value: before[index] });
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
visit(next, before[index], after[index], differences);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
differences.push({ kind: 'changed', path, before, after });
|
|
64
|
+
}
|
|
65
|
+
function isRecord(value) {
|
|
66
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
function property(key) {
|
|
69
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
|
|
70
|
+
}
|
|
71
|
+
function describe(difference) {
|
|
72
|
+
switch (difference.kind) {
|
|
73
|
+
case 'added':
|
|
74
|
+
return `+ ${difference.path}: ${valueOf(difference.value)}`;
|
|
75
|
+
case 'removed':
|
|
76
|
+
return `- ${difference.path}: ${valueOf(difference.value)}`;
|
|
77
|
+
case 'changed':
|
|
78
|
+
return `~ ${difference.path}: ${valueOf(difference.before)} -> ${valueOf(difference.after)}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function valueOf(value) {
|
|
82
|
+
const encoded = JSON.stringify(value);
|
|
83
|
+
return encoded === undefined ? String(value) : encoded;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/tools/diff.ts"],"names":[],"mappings":"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","sourcesContent":["import { NO_ARGS, type Tool } from './tool.js';\n\nexport type StateDifference =\n | { readonly kind: 'added'; readonly path: string; readonly value: unknown }\n | { readonly kind: 'removed'; readonly path: string; readonly value: unknown }\n | {\n readonly kind: 'changed';\n readonly path: string;\n readonly before: unknown;\n readonly after: unknown;\n };\n\n/** Compare two JSON-compatible states without assigning meaning to either one. */\nexport function diffState(before: unknown, after: unknown): readonly StateDifference[] {\n const differences: StateDifference[] = [];\n visit('$', before, after, differences);\n return differences;\n}\n\n/** The one stateful tool every served subject receives. */\nexport const diff: Tool<unknown> = {\n name: 'variance_diff',\n description:\n 'Compare the current state with the state held from the previous successful MCP tool invocation. ' +\n 'The first invocation records state and has nothing to compare.',\n inputSchema: NO_ARGS,\n run(current, _input, invocation) {\n if (invocation?.previous === undefined) {\n return 'No previous MCP invocation state. The current state is now remembered.';\n }\n\n const differences = diffState(invocation.previous, current);\n if (differences.length === 0) {\n return 'The current state matches the previous MCP invocation.';\n }\n\n return [\n `The current state differs from the previous MCP invocation at ${String(differences.length)} path(s).`,\n ...differences.map(describe),\n ].join('\\n');\n },\n};\n\nfunction visit(\n path: string,\n before: unknown,\n after: unknown,\n differences: StateDifference[],\n): void {\n if (Object.is(before, after)) return;\n\n if (isRecord(before) && isRecord(after)) {\n const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();\n for (const key of keys) {\n const next = `${path}${property(key)}`;\n if (!(key in before)) {\n differences.push({ kind: 'added', path: next, value: after[key] });\n } else if (!(key in after)) {\n differences.push({ kind: 'removed', path: next, value: before[key] });\n } else {\n visit(next, before[key], after[key], differences);\n }\n }\n return;\n }\n\n if (Array.isArray(before) && Array.isArray(after)) {\n const length = Math.max(before.length, after.length);\n for (let index = 0; index < length; index += 1) {\n const next = `${path}[${String(index)}]`;\n if (index >= before.length) {\n differences.push({ kind: 'added', path: next, value: after[index] });\n } else if (index >= after.length) {\n differences.push({ kind: 'removed', path: next, value: before[index] });\n } else {\n visit(next, before[index], after[index], differences);\n }\n }\n return;\n }\n\n differences.push({ kind: 'changed', path, before, after });\n}\n\nfunction isRecord(value: unknown): value is Readonly<Record<string, unknown>> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction property(key: string): string {\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;\n}\n\nfunction describe(difference: StateDifference): string {\n switch (difference.kind) {\n case 'added':\n return `+ ${difference.path}: ${valueOf(difference.value)}`;\n case 'removed':\n return `- ${difference.path}: ${valueOf(difference.value)}`;\n case 'changed':\n return `~ ${difference.path}: ${valueOf(difference.before)} -> ${valueOf(difference.after)}`;\n }\n}\n\nfunction valueOf(value: unknown): string {\n const encoded = JSON.stringify(value);\n return encoded === undefined ? String(value) : encoded;\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Tool } from './tool.js';
|
|
2
|
+
/**
|
|
3
|
+
* `variance_explain_verdict` — the answers that are not about the code.
|
|
4
|
+
*
|
|
5
|
+
* Kept apart from `variance_describe` although both start by locating a subject.
|
|
6
|
+
* `describe` answers about pixels that moved; this one answers about a comparison
|
|
7
|
+
* that never happened, and its whole job is to end with "there is no edit to
|
|
8
|
+
* make here" — a sentence a region list cannot say, and one that stops being
|
|
9
|
+
* said the moment the two tools share a body and a caller reaches for the
|
|
10
|
+
* regions because they are already in scope.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Why a subject has no comparison.
|
|
14
|
+
*
|
|
15
|
+
* `incomparable` and `new` are the two verdicts an agent will otherwise treat as
|
|
16
|
+
* failures and try to fix in code, which is exactly wrong: neither is about the
|
|
17
|
+
* code. This makes the reason legible enough to act on — or to decide not to.
|
|
18
|
+
*
|
|
19
|
+
* A subject with no observation at all is the third such case and the worst one
|
|
20
|
+
* to refuse. "Unknown subject" tells an agent the subject does not exist, so it
|
|
21
|
+
* stops asking; the truth is that the subject exists and nothing is known about
|
|
22
|
+
* it, which is the opposite conclusion.
|
|
23
|
+
*/
|
|
24
|
+
export declare const explain: Tool;
|
|
25
|
+
//# sourceMappingURL=explain-verdict.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { subjectOf, unobserved } from './subject.js';
|
|
2
|
+
/**
|
|
3
|
+
* `variance_explain_verdict` — the answers that are not about the code.
|
|
4
|
+
*
|
|
5
|
+
* Kept apart from `variance_describe` although both start by locating a subject.
|
|
6
|
+
* `describe` answers about pixels that moved; this one answers about a comparison
|
|
7
|
+
* that never happened, and its whole job is to end with "there is no edit to
|
|
8
|
+
* make here" — a sentence a region list cannot say, and one that stops being
|
|
9
|
+
* said the moment the two tools share a body and a caller reaches for the
|
|
10
|
+
* regions because they are already in scope.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Why a subject has no comparison.
|
|
14
|
+
*
|
|
15
|
+
* `incomparable` and `new` are the two verdicts an agent will otherwise treat as
|
|
16
|
+
* failures and try to fix in code, which is exactly wrong: neither is about the
|
|
17
|
+
* code. This makes the reason legible enough to act on — or to decide not to.
|
|
18
|
+
*
|
|
19
|
+
* A subject with no observation at all is the third such case and the worst one
|
|
20
|
+
* to refuse. "Unknown subject" tells an agent the subject does not exist, so it
|
|
21
|
+
* stops asking; the truth is that the subject exists and nothing is known about
|
|
22
|
+
* it, which is the opposite conclusion.
|
|
23
|
+
*/
|
|
24
|
+
export const explain = {
|
|
25
|
+
name: 'variance_explain_verdict',
|
|
26
|
+
description: 'Why a subject was not compared. `incomparable` means a baseline exists but another ' +
|
|
27
|
+
'machine rendered it; `new` means none exists; a subject in the coverage list was never ' +
|
|
28
|
+
'observed at all. None is a code problem — call this before attempting a fix.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: { subject: { type: 'string' } },
|
|
32
|
+
required: ['subject'],
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
},
|
|
35
|
+
run(report, input) {
|
|
36
|
+
const located = subjectOf(report, input);
|
|
37
|
+
if (!located.observed) {
|
|
38
|
+
return [
|
|
39
|
+
unobserved(located.entry),
|
|
40
|
+
'',
|
|
41
|
+
nothingCompared(located.entry.kind),
|
|
42
|
+
].join('\n');
|
|
43
|
+
}
|
|
44
|
+
const { observation } = located;
|
|
45
|
+
switch (observation.verdict) {
|
|
46
|
+
case 'incomparable':
|
|
47
|
+
return [
|
|
48
|
+
observation.because,
|
|
49
|
+
'',
|
|
50
|
+
'This is not a code change and cannot be fixed in code. Either run on the machine ' +
|
|
51
|
+
'that wrote the baseline, re-record the baseline on this one, or use an ephemeral ' +
|
|
52
|
+
'comparison, which renders both sides here and needs no stored image at all.',
|
|
53
|
+
].join('\n');
|
|
54
|
+
case 'new':
|
|
55
|
+
return [
|
|
56
|
+
observation.because,
|
|
57
|
+
'',
|
|
58
|
+
'Nothing has regressed; there is no baseline to regress from. Record one, ' +
|
|
59
|
+
'or compare ephemerally against the previous revision.',
|
|
60
|
+
].join('\n');
|
|
61
|
+
case 'unchanged':
|
|
62
|
+
return `${observation.subject} was compared and did not change: ${observation.because}`;
|
|
63
|
+
case 'ignored':
|
|
64
|
+
return [
|
|
65
|
+
observation.because,
|
|
66
|
+
'',
|
|
67
|
+
'This subject changed. Every changed pixel landed inside a region the project ' +
|
|
68
|
+
'excluded, so the run is green and nothing here was reviewed. That is a ' +
|
|
69
|
+
'configuration decision, not a fact about the code: if the change should have ' +
|
|
70
|
+
'been caught, the ignore is too wide.',
|
|
71
|
+
].join('\n');
|
|
72
|
+
case 'changed':
|
|
73
|
+
return `${observation.subject} was compared and changed. Call variance_describe for the regions.`;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
/** What an agent should do about a subject nothing was compared for. */
|
|
78
|
+
function nothingCompared(kind) {
|
|
79
|
+
switch (kind) {
|
|
80
|
+
case 'excluded':
|
|
81
|
+
return ('Nothing was compared, so nothing is known about this subject. There is no code ' +
|
|
82
|
+
'change to make here; if it should be watched, change the exclusion.');
|
|
83
|
+
case 'unreached':
|
|
84
|
+
return ('Nothing was compared, and nothing needed to be: the run resolved that this ' +
|
|
85
|
+
'change cannot reach this subject. There is no code change to make here.');
|
|
86
|
+
default:
|
|
87
|
+
return ('Nothing was compared, so nothing is known about this subject — an absent ' +
|
|
88
|
+
'observation is not an unchanged one. Fix whatever stopped the run from seeing ' +
|
|
89
|
+
'it before treating any part of this run as a pass for this subject.');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=explain-verdict.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explain-verdict.js","sourceRoot":"","sources":["../../src/tools/explain-verdict.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAGrD;;;;;;;;;GASG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,OAAO,GAAS;IAC3B,IAAI,EAAE,0BAA0B;IAChC,WAAW,EACT,qFAAqF;QACrF,yFAAyF;QACzF,8EAA8E;IAChF,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;QAC3C,QAAQ,EAAE,CAAC,SAAS,CAAC;QACrB,oBAAoB,EAAE,KAAK;KAC5B;IAED,GAAG,CAAC,MAAM,EAAE,KAAK;QACf,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAEzC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YACtB,OAAO;gBACL,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC;gBACzB,EAAE;gBACF,eAAe,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;aACpC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACf,CAAC;QAED,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC;QAEhC,QAAQ,WAAW,CAAC,OAAO,EAAE,CAAC;YAC5B,KAAK,cAAc;gBACjB,OAAO;oBACL,WAAW,CAAC,OAAO;oBACnB,EAAE;oBACF,mFAAmF;wBACjF,mFAAmF;wBACnF,6EAA6E;iBAChF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACf,KAAK,KAAK;gBACR,OAAO;oBACL,WAAW,CAAC,OAAO;oBACnB,EAAE;oBACF,2EAA2E;wBACzE,uDAAuD;iBAC1D,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACf,KAAK,WAAW;gBACd,OAAO,GAAG,WAAW,CAAC,OAAO,qCAAqC,WAAW,CAAC,OAAO,EAAE,CAAC;YAC1F,KAAK,SAAS;gBACZ,OAAO;oBACL,WAAW,CAAC,OAAO;oBACnB,EAAE;oBACF,+EAA+E;wBAC7E,yEAAyE;wBACzE,+EAA+E;wBAC/E,sCAAsC;iBACzC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACf,KAAK,SAAS;gBACZ,OAAO,GAAG,WAAW,CAAC,OAAO,oEAAoE,CAAC;QACtG,CAAC;IACH,CAAC;CACF,CAAC;AAEF,wEAAwE;AACxE,SAAS,eAAe,CAAC,IAAyB;IAChD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU;YACb,OAAO,CACL,iFAAiF;gBACjF,qEAAqE,CACtE,CAAC;QACJ,KAAK,WAAW;YACd,OAAO,CACL,6EAA6E;gBAC7E,yEAAyE,CAC1E,CAAC;QACJ;YACE,OAAO,CACL,2EAA2E;gBAC3E,gFAAgF;gBAChF,qEAAqE,CACtE,CAAC;IACN,CAAC;AACH,CAAC","sourcesContent":["import type { NotObserved } from '@variance-authority/report';\nimport { subjectOf, unobserved } from './subject.js';\nimport type { Tool } from './tool.js';\n\n/**\n * `variance_explain_verdict` — the answers that are not about the code.\n *\n * Kept apart from `variance_describe` although both start by locating a subject.\n * `describe` answers about pixels that moved; this one answers about a comparison\n * that never happened, and its whole job is to end with \"there is no edit to\n * make here\" — a sentence a region list cannot say, and one that stops being\n * said the moment the two tools share a body and a caller reaches for the\n * regions because they are already in scope.\n */\n\n/**\n * Why a subject has no comparison.\n *\n * `incomparable` and `new` are the two verdicts an agent will otherwise treat as\n * failures and try to fix in code, which is exactly wrong: neither is about the\n * code. This makes the reason legible enough to act on — or to decide not to.\n *\n * A subject with no observation at all is the third such case and the worst one\n * to refuse. \"Unknown subject\" tells an agent the subject does not exist, so it\n * stops asking; the truth is that the subject exists and nothing is known about\n * it, which is the opposite conclusion.\n */\nexport const explain: Tool = {\n name: 'variance_explain_verdict',\n description:\n 'Why a subject was not compared. `incomparable` means a baseline exists but another ' +\n 'machine rendered it; `new` means none exists; a subject in the coverage list was never ' +\n 'observed at all. None is a code problem — call this before attempting a fix.',\n inputSchema: {\n type: 'object',\n properties: { subject: { type: 'string' } },\n required: ['subject'],\n additionalProperties: false,\n },\n\n run(report, input) {\n const located = subjectOf(report, input);\n\n if (!located.observed) {\n return [\n unobserved(located.entry),\n '',\n nothingCompared(located.entry.kind),\n ].join('\\n');\n }\n\n const { observation } = located;\n\n switch (observation.verdict) {\n case 'incomparable':\n return [\n observation.because,\n '',\n 'This is not a code change and cannot be fixed in code. Either run on the machine ' +\n 'that wrote the baseline, re-record the baseline on this one, or use an ephemeral ' +\n 'comparison, which renders both sides here and needs no stored image at all.',\n ].join('\\n');\n case 'new':\n return [\n observation.because,\n '',\n 'Nothing has regressed; there is no baseline to regress from. Record one, ' +\n 'or compare ephemerally against the previous revision.',\n ].join('\\n');\n case 'unchanged':\n return `${observation.subject} was compared and did not change: ${observation.because}`;\n case 'ignored':\n return [\n observation.because,\n '',\n 'This subject changed. Every changed pixel landed inside a region the project ' +\n 'excluded, so the run is green and nothing here was reviewed. That is a ' +\n 'configuration decision, not a fact about the code: if the change should have ' +\n 'been caught, the ignore is too wide.',\n ].join('\\n');\n case 'changed':\n return `${observation.subject} was compared and changed. Call variance_describe for the regions.`;\n }\n },\n};\n\n/** What an agent should do about a subject nothing was compared for. */\nfunction nothingCompared(kind: NotObserved['kind']): string {\n switch (kind) {\n case 'excluded':\n return (\n 'Nothing was compared, so nothing is known about this subject. There is no code ' +\n 'change to make here; if it should be watched, change the exclusion.'\n );\n case 'unreached':\n return (\n 'Nothing was compared, and nothing needed to be: the run resolved that this ' +\n 'change cannot reach this subject. There is no code change to make here.'\n );\n default:\n return (\n 'Nothing was compared, so nothing is known about this subject — an absent ' +\n 'observation is not an unchanged one. Fix whatever stopped the run from seeing ' +\n 'it before treating any part of this run as a pass for this subject.'\n );\n }\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Tool } from './tool.js';
|
|
2
|
+
/**
|
|
3
|
+
* `variance_findings` — the one answer here that reads no comparison.
|
|
4
|
+
*
|
|
5
|
+
* Alone in a module because nothing else in this package groups by anything but
|
|
6
|
+
* a subject. This tool inverts that twice — by rule, then by the place the rule
|
|
7
|
+
* fired — and both inversions exist for the same reason the docket does: the
|
|
8
|
+
* unit a reader acts on is an edit, not an occurrence.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Defects in the render itself, which no comparison could have reported.
|
|
12
|
+
*
|
|
13
|
+
* A separate tool rather than part of `variance_describe` because it answers a
|
|
14
|
+
* different question. `describe` answers "what did this change do"; this answers
|
|
15
|
+
* "what is wrong with this component regardless of whether anyone touched it" —
|
|
16
|
+
* and the second is the one a comparison structurally cannot reach, since a
|
|
17
|
+
* control that never had an accessible name compares equal to itself forever.
|
|
18
|
+
*
|
|
19
|
+
* Grouped by rule rather than by subject. One missing `alt` in twelve stories is
|
|
20
|
+
* one edit to one component, and listing it twelve times under twelve subject
|
|
21
|
+
* headings is the same fatigue the docket exists to prevent.
|
|
22
|
+
*
|
|
23
|
+
* ## Two things an agent cannot act on this list without
|
|
24
|
+
*
|
|
25
|
+
* **What kind of defect it is.** Most of these rules are accessibility and the
|
|
26
|
+
* description used to say all of them were, which is wrong about the two that
|
|
27
|
+
* are not — a string the locale pass never translated is a content defect, and
|
|
28
|
+
* a box past its container's edge is a layout one. The band is the same word the
|
|
29
|
+
* project blocks on, so the heading a reader sees and the policy that stops
|
|
30
|
+
* their merge are the same word.
|
|
31
|
+
*
|
|
32
|
+
* **Whether it is theirs.** A defect the baseline carried too is somebody else's
|
|
33
|
+
* afternoon, and an agent told twelve defects with no dating will either fix
|
|
34
|
+
* twelve or fix none. The date is only ever printed from a record; nothing here
|
|
35
|
+
* infers it from an absence — and the rules holding a defect this change brought
|
|
36
|
+
* are listed before the rules holding only inherited ones, so an agent that reads
|
|
37
|
+
* the first group and stops has read the part it can act on.
|
|
38
|
+
*/
|
|
39
|
+
export declare const findings: Tool;
|
|
40
|
+
//# sourceMappingURL=findings.d.ts.map
|