@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,31 @@
|
|
|
1
|
+
import type { Tool } from './tool.js';
|
|
2
|
+
/**
|
|
3
|
+
* `variance_changes` — the review, sized before it is started.
|
|
4
|
+
*
|
|
5
|
+
* Every other tool here answers about a *subject*. This one refuses to, and the
|
|
6
|
+
* refusal is the point: forty changed stories are not forty questions, and an
|
|
7
|
+
* agent that walks them one at a time will spend forty tool calls learning what
|
|
8
|
+
* one call can say — that three things happened, one of them explains
|
|
9
|
+
* thirty-one of the subjects, and it is a token edit in `Button`.
|
|
10
|
+
*
|
|
11
|
+
* That reshaping is the difference between an agent that reviews a suite and an
|
|
12
|
+
* agent that transcribes one.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Distinct changes in a run, most decidable first.
|
|
16
|
+
*
|
|
17
|
+
* Leads with the count of *decisions* rather than the count of subjects, and
|
|
18
|
+
* then with the exact command that settles each one — because the failure this
|
|
19
|
+
* tool exists to prevent is not an agent that cannot find the information. It is
|
|
20
|
+
* an agent that finds it, summarises forty subjects into a paragraph, and leaves
|
|
21
|
+
* the operator exactly where they started.
|
|
22
|
+
*
|
|
23
|
+
* The distinction between `subjects` and `settles` is carried into the text on
|
|
24
|
+
* every line, and it is the one thing here that must not be smoothed over: a
|
|
25
|
+
* shape appearing beside something else cannot be accepted there without
|
|
26
|
+
* promoting a difference nobody reviewed. `variance accept --shape` refuses
|
|
27
|
+
* those by name; saying so here means the agent proposes a command that will
|
|
28
|
+
* work rather than one that will be refused.
|
|
29
|
+
*/
|
|
30
|
+
export declare const changes: Tool;
|
|
31
|
+
//# sourceMappingURL=changes.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { clusterChanges, describeClustering } from '@variance-authority/report';
|
|
2
|
+
/**
|
|
3
|
+
* `variance_changes` — the review, sized before it is started.
|
|
4
|
+
*
|
|
5
|
+
* Every other tool here answers about a *subject*. This one refuses to, and the
|
|
6
|
+
* refusal is the point: forty changed stories are not forty questions, and an
|
|
7
|
+
* agent that walks them one at a time will spend forty tool calls learning what
|
|
8
|
+
* one call can say — that three things happened, one of them explains
|
|
9
|
+
* thirty-one of the subjects, and it is a token edit in `Button`.
|
|
10
|
+
*
|
|
11
|
+
* That reshaping is the difference between an agent that reviews a suite and an
|
|
12
|
+
* agent that transcribes one.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Distinct changes in a run, most decidable first.
|
|
16
|
+
*
|
|
17
|
+
* Leads with the count of *decisions* rather than the count of subjects, and
|
|
18
|
+
* then with the exact command that settles each one — because the failure this
|
|
19
|
+
* tool exists to prevent is not an agent that cannot find the information. It is
|
|
20
|
+
* an agent that finds it, summarises forty subjects into a paragraph, and leaves
|
|
21
|
+
* the operator exactly where they started.
|
|
22
|
+
*
|
|
23
|
+
* The distinction between `subjects` and `settles` is carried into the text on
|
|
24
|
+
* every line, and it is the one thing here that must not be smoothed over: a
|
|
25
|
+
* shape appearing beside something else cannot be accepted there without
|
|
26
|
+
* promoting a difference nobody reviewed. `variance accept --shape` refuses
|
|
27
|
+
* those by name; saying so here means the agent proposes a command that will
|
|
28
|
+
* work rather than one that will be refused.
|
|
29
|
+
*/
|
|
30
|
+
export const changes = {
|
|
31
|
+
name: 'variance_changes',
|
|
32
|
+
description: 'Group this run’s changed subjects into the distinct changes behind them, most ' +
|
|
33
|
+
'decidable first. A design-token edit touching forty stories is one change, not forty. ' +
|
|
34
|
+
'Each names the component responsible, the subjects it reached, the subjects where it is ' +
|
|
35
|
+
'the whole change (and can therefore be settled in one action), and the shape digest to ' +
|
|
36
|
+
'pass to `variance accept --shape`. Ask this before asking about any individual subject.',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
component: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
description: 'Optional. Only changes attributed to this component.',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
},
|
|
47
|
+
run(report, input) {
|
|
48
|
+
const wanted = typeof input?.['component'] === 'string' ? input['component'] : undefined;
|
|
49
|
+
const changed = report.observations.filter((observation) => observation.verdict === 'changed').length;
|
|
50
|
+
const clustering = clusterChanges(report.observations);
|
|
51
|
+
const selected = wanted === undefined
|
|
52
|
+
? clustering.changes
|
|
53
|
+
: clustering.changes.filter((change) => change.component === wanted);
|
|
54
|
+
if (wanted !== undefined && selected.length === 0) {
|
|
55
|
+
// Two claims kept apart. "No change was attributed to `Button`" and "this
|
|
56
|
+
// run grouped nothing at all" lead an agent to different next moves, and
|
|
57
|
+
// collapsing them into one sentence sends it down the wrong one.
|
|
58
|
+
return clustering.changes.length === 0
|
|
59
|
+
? 'This run has no grouped changes at all, so nothing can be attributed to ' +
|
|
60
|
+
`${wanted} or to anything else.`
|
|
61
|
+
: `No change in this run is attributed to ${wanted}. Components with changes: ` +
|
|
62
|
+
named(clustering.changes).join(', ');
|
|
63
|
+
}
|
|
64
|
+
if (selected.length === 0) {
|
|
65
|
+
return clustering.ungrouped.length === 0
|
|
66
|
+
? describeClustering(clustering, changed)
|
|
67
|
+
: `${describeClustering(clustering, changed)}. Ungrouped subjects have no difference ` +
|
|
68
|
+
'shape, which means the run compared without a document — the ephemeral mode, or a ' +
|
|
69
|
+
`raster-only path: ${clustering.ungrouped.join(', ')}`;
|
|
70
|
+
}
|
|
71
|
+
return [
|
|
72
|
+
wanted === undefined
|
|
73
|
+
? describeClustering(clustering, changed)
|
|
74
|
+
: `${selected.length} change(s) attributed to ${wanted}`,
|
|
75
|
+
'',
|
|
76
|
+
...selected.map(render),
|
|
77
|
+
...(clustering.ungrouped.length > 0 && wanted === undefined
|
|
78
|
+
? [
|
|
79
|
+
'',
|
|
80
|
+
`Not grouped (${clustering.ungrouped.length}): ${clustering.ungrouped.join(', ')}`,
|
|
81
|
+
' These carry no difference shape, so the run compared without a document. ' +
|
|
82
|
+
'They have to be reviewed one at a time.',
|
|
83
|
+
]
|
|
84
|
+
: []),
|
|
85
|
+
].join('\n');
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
function render(change) {
|
|
89
|
+
const head = change.component ?? '(grouped by pixel shape; no component resolved)';
|
|
90
|
+
const where = change.file === undefined ? '' : ` ${change.file}\n`;
|
|
91
|
+
const reach = change.subjects.length === change.settles.length
|
|
92
|
+
? ` reaches ${change.subjects.length} subject(s), and is the whole change in all of them\n`
|
|
93
|
+
: ` reaches ${change.subjects.length} subject(s); it is the whole change in ` +
|
|
94
|
+
`${change.settles.length}\n` +
|
|
95
|
+
` in the other ${change.subjects.length - change.settles.length}, something else ` +
|
|
96
|
+
'also moved, so accepting this shape there would promote a difference nobody reviewed\n';
|
|
97
|
+
// The command, spelled out. An agent that has to derive it will sometimes
|
|
98
|
+
// derive it wrong, and the digest is the one part of this answer that cannot
|
|
99
|
+
// be guessed from anything else in the report.
|
|
100
|
+
const action = change.settles.length === 0
|
|
101
|
+
? ' no subject can be settled by this shape alone\n'
|
|
102
|
+
: ` variance accept --shape ${change.fingerprint}\n`;
|
|
103
|
+
return (`${change.cause ? '' : '[collateral] '}${head}\n` +
|
|
104
|
+
where +
|
|
105
|
+
reach +
|
|
106
|
+
` ${change.pixels} pixel(s): ${preview(change.subjects)}\n` +
|
|
107
|
+
action);
|
|
108
|
+
}
|
|
109
|
+
/** The first few subjects, because a reader wants examples and not a manifest. */
|
|
110
|
+
function preview(subjects) {
|
|
111
|
+
return subjects.length <= 4
|
|
112
|
+
? subjects.join(', ')
|
|
113
|
+
: `${subjects.slice(0, 4).join(', ')}, and ${subjects.length - 4} more`;
|
|
114
|
+
}
|
|
115
|
+
function named(changes) {
|
|
116
|
+
return [
|
|
117
|
+
...new Set(changes.map((change) => change.component).filter((name) => name !== undefined)),
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=changes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changes.js","sourceRoot":"","sources":["../../src/tools/changes.ts"],"names":[],"mappings":"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","sourcesContent":["import { clusterChanges, describeClustering, type Change } from '@variance-authority/report';\nimport type { Tool } from './tool.js';\n\n/**\n * `variance_changes` — the review, sized before it is started.\n *\n * Every other tool here answers about a *subject*. This one refuses to, and the\n * refusal is the point: forty changed stories are not forty questions, and an\n * agent that walks them one at a time will spend forty tool calls learning what\n * one call can say — that three things happened, one of them explains\n * thirty-one of the subjects, and it is a token edit in `Button`.\n *\n * That reshaping is the difference between an agent that reviews a suite and an\n * agent that transcribes one.\n */\n\n/**\n * Distinct changes in a run, most decidable first.\n *\n * Leads with the count of *decisions* rather than the count of subjects, and\n * then with the exact command that settles each one — because the failure this\n * tool exists to prevent is not an agent that cannot find the information. It is\n * an agent that finds it, summarises forty subjects into a paragraph, and leaves\n * the operator exactly where they started.\n *\n * The distinction between `subjects` and `settles` is carried into the text on\n * every line, and it is the one thing here that must not be smoothed over: a\n * shape appearing beside something else cannot be accepted there without\n * promoting a difference nobody reviewed. `variance accept --shape` refuses\n * those by name; saying so here means the agent proposes a command that will\n * work rather than one that will be refused.\n */\nexport const changes: Tool = {\n name: 'variance_changes',\n description:\n 'Group this run’s changed subjects into the distinct changes behind them, most ' +\n 'decidable first. A design-token edit touching forty stories is one change, not forty. ' +\n 'Each names the component responsible, the subjects it reached, the subjects where it is ' +\n 'the whole change (and can therefore be settled in one action), and the shape digest to ' +\n 'pass to `variance accept --shape`. Ask this before asking about any individual subject.',\n inputSchema: {\n type: 'object',\n properties: {\n component: {\n type: 'string',\n description: 'Optional. Only changes attributed to this component.',\n },\n },\n additionalProperties: false,\n },\n\n run(report, input) {\n const wanted = typeof input?.['component'] === 'string' ? input['component'] : undefined;\n\n const changed = report.observations.filter(\n (observation) => observation.verdict === 'changed',\n ).length;\n\n const clustering = clusterChanges(report.observations);\n const selected =\n wanted === undefined\n ? clustering.changes\n : clustering.changes.filter((change) => change.component === wanted);\n\n if (wanted !== undefined && selected.length === 0) {\n // Two claims kept apart. \"No change was attributed to `Button`\" and \"this\n // run grouped nothing at all\" lead an agent to different next moves, and\n // collapsing them into one sentence sends it down the wrong one.\n return clustering.changes.length === 0\n ? 'This run has no grouped changes at all, so nothing can be attributed to ' +\n `${wanted} or to anything else.`\n : `No change in this run is attributed to ${wanted}. Components with changes: ` +\n named(clustering.changes).join(', ');\n }\n\n if (selected.length === 0) {\n return clustering.ungrouped.length === 0\n ? describeClustering(clustering, changed)\n : `${describeClustering(clustering, changed)}. Ungrouped subjects have no difference ` +\n 'shape, which means the run compared without a document — the ephemeral mode, or a ' +\n `raster-only path: ${clustering.ungrouped.join(', ')}`;\n }\n\n return [\n wanted === undefined\n ? describeClustering(clustering, changed)\n : `${selected.length} change(s) attributed to ${wanted}`,\n '',\n ...selected.map(render),\n ...(clustering.ungrouped.length > 0 && wanted === undefined\n ? [\n '',\n `Not grouped (${clustering.ungrouped.length}): ${clustering.ungrouped.join(', ')}`,\n ' These carry no difference shape, so the run compared without a document. ' +\n 'They have to be reviewed one at a time.',\n ]\n : []),\n ].join('\\n');\n },\n};\n\nfunction render(change: Change): string {\n const head = change.component ?? '(grouped by pixel shape; no component resolved)';\n const where = change.file === undefined ? '' : ` ${change.file}\\n`;\n\n const reach =\n change.subjects.length === change.settles.length\n ? ` reaches ${change.subjects.length} subject(s), and is the whole change in all of them\\n`\n : ` reaches ${change.subjects.length} subject(s); it is the whole change in ` +\n `${change.settles.length}\\n` +\n ` in the other ${change.subjects.length - change.settles.length}, something else ` +\n 'also moved, so accepting this shape there would promote a difference nobody reviewed\\n';\n\n // The command, spelled out. An agent that has to derive it will sometimes\n // derive it wrong, and the digest is the one part of this answer that cannot\n // be guessed from anything else in the report.\n const action =\n change.settles.length === 0\n ? ' no subject can be settled by this shape alone\\n'\n : ` variance accept --shape ${change.fingerprint}\\n`;\n\n return (\n `${change.cause ? '' : '[collateral] '}${head}\\n` +\n where +\n reach +\n ` ${change.pixels} pixel(s): ${preview(change.subjects)}\\n` +\n action\n );\n}\n\n/** The first few subjects, because a reader wants examples and not a manifest. */\nfunction preview(subjects: readonly string[]): string {\n return subjects.length <= 4\n ? subjects.join(', ')\n : `${subjects.slice(0, 4).join(', ')}, and ${subjects.length - 4} more`;\n}\n\nfunction named(changes: readonly Change[]): readonly string[] {\n return [\n ...new Set(\n changes.map((change) => change.component).filter((name): name is string => name !== undefined),\n ),\n ];\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Tool } from './tool.js';
|
|
2
|
+
/**
|
|
3
|
+
* The suite's graph: what it is built from, what repeats, and what moved.
|
|
4
|
+
*
|
|
5
|
+
* Ordered by what an agent acts on rather than by what is interesting.
|
|
6
|
+
* Unexplained movements lead, because they are the only entries here that are a
|
|
7
|
+
* *finding*; explained ones are folded to one line per cause, for the same
|
|
8
|
+
* reason `variance_changes` refuses to list forty subjects; and the shared
|
|
9
|
+
* renderings come last, because they change how a review is *sized* rather than
|
|
10
|
+
* what it concludes.
|
|
11
|
+
*/
|
|
12
|
+
export declare const composition: Tool;
|
|
13
|
+
//# sourceMappingURL=composition.d.ts.map
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `variance_composition` — the suite as one graph, and the flakes it shortlists.
|
|
3
|
+
*
|
|
4
|
+
* The other axis. Every other tool in this package answers about *one subject
|
|
5
|
+
* against its baseline*: two revisions, one thing. This one answers about the
|
|
6
|
+
* run's subjects compared to **each other**, at one commit, which is the
|
|
7
|
+
* question a suite of examples has always implied and never written down:
|
|
8
|
+
*
|
|
9
|
+
* > A visual-regression example is a component built from components. The
|
|
10
|
+
* > example *is* a component, at a boundary; the same component appears again,
|
|
11
|
+
* > with the same or different props, inside larger examples.
|
|
12
|
+
*
|
|
13
|
+
* It earns a tool of its own rather than a paragraph in `variance_summary`
|
|
14
|
+
* because it changes what an agent does next twice over. `variance_changes`
|
|
15
|
+
* turns forty changed subjects into three decisions; this turns a decision into
|
|
16
|
+
* the component and the caller behind it, and then says which movements
|
|
17
|
+
* *nothing in the run explains* — the only place in this package where a flake
|
|
18
|
+
* is named, and the only one that can name it, because naming one needs the
|
|
19
|
+
* places the same component **held**.
|
|
20
|
+
*
|
|
21
|
+
* Not `variance_trace_component`, which reads the regions of changed subjects
|
|
22
|
+
* and is therefore blind to every component that did not move. The control
|
|
23
|
+
* group is the half of a flake report that makes the other half worth reading,
|
|
24
|
+
* so this reads every boundary in every subject instead.
|
|
25
|
+
*/
|
|
26
|
+
/** Echoes printed before the list is cut. The rest are counted, never dropped in silence. */
|
|
27
|
+
const MAX_ECHOES_SHOWN = 10;
|
|
28
|
+
/**
|
|
29
|
+
* The suite's graph: what it is built from, what repeats, and what moved.
|
|
30
|
+
*
|
|
31
|
+
* Ordered by what an agent acts on rather than by what is interesting.
|
|
32
|
+
* Unexplained movements lead, because they are the only entries here that are a
|
|
33
|
+
* *finding*; explained ones are folded to one line per cause, for the same
|
|
34
|
+
* reason `variance_changes` refuses to list forty subjects; and the shared
|
|
35
|
+
* renderings come last, because they change how a review is *sized* rather than
|
|
36
|
+
* what it concludes.
|
|
37
|
+
*/
|
|
38
|
+
export const composition = {
|
|
39
|
+
name: 'variance_composition',
|
|
40
|
+
description: 'The run’s subjects compared to each other at one commit rather than to their baselines: ' +
|
|
41
|
+
'which components the suite is built from, which renderings appear in more than one subject ' +
|
|
42
|
+
'(so two diffs are one thing to review), which components render two ways from one input, ' +
|
|
43
|
+
'and — for everything that moved — whether an edited file, a moved token or an edited caller ' +
|
|
44
|
+
'explains it. Movements nothing explains are named here: `flake` where the subject also ' +
|
|
45
|
+
'failed to read the same way twice, `suspect` where nobody has read it twice yet. Pass ' +
|
|
46
|
+
'`component` for one component’s census entry, the subjects it holds in, and the examples ' +
|
|
47
|
+
'that watch it. Ask this when a change has no obvious author, when you need the component ' +
|
|
48
|
+
'behind a set of diffs, or before calling anything flaky.',
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
component: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: 'Optional. One component’s entry in the graph, instead of the whole suite.',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
},
|
|
59
|
+
run(report, input) {
|
|
60
|
+
const composed = report.composition;
|
|
61
|
+
if (composed === undefined)
|
|
62
|
+
return ABSENT;
|
|
63
|
+
const wanted = typeof input?.['component'] === 'string' ? input['component'] : undefined;
|
|
64
|
+
return wanted === undefined ? whole(composed) : one(composed, wanted);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The absence, said as an absence.
|
|
69
|
+
*
|
|
70
|
+
* `composition` is missing from a report whose collection produced no semantic
|
|
71
|
+
* snapshots, and rendering that as an empty graph would answer *this suite
|
|
72
|
+
* shares nothing* — a different claim, and a false one. Absent is not empty
|
|
73
|
+
* here for the same reason it is not empty anywhere else in this system.
|
|
74
|
+
*/
|
|
75
|
+
const ABSENT = 'This run composed nothing, which is not the same as it finding nothing. No subject supplied ' +
|
|
76
|
+
'a semantic snapshot — a raster-only or ephemeral run compares images and never sees a ' +
|
|
77
|
+
'component boundary — so there was nothing to join. Nothing here says the suite shares no ' +
|
|
78
|
+
'components; it says this run cannot tell.';
|
|
79
|
+
function whole(composed) {
|
|
80
|
+
const unexplained = composed.movements.filter((movement) => movement.cause === 'unexplained');
|
|
81
|
+
const explained = composed.movements.filter((movement) => movement.cause !== 'unexplained');
|
|
82
|
+
return [
|
|
83
|
+
headline(composed, unexplained.length),
|
|
84
|
+
unexplainedSection(unexplained),
|
|
85
|
+
explainedSection(explained),
|
|
86
|
+
divergenceSection(composed.divergences),
|
|
87
|
+
echoSection(composed),
|
|
88
|
+
orphanSection(composed.components),
|
|
89
|
+
]
|
|
90
|
+
.filter((section) => section !== '')
|
|
91
|
+
.join('\n\n');
|
|
92
|
+
}
|
|
93
|
+
function one(composed, wanted) {
|
|
94
|
+
const entry = composed.components.find((each) => each.component === wanted);
|
|
95
|
+
if (entry === undefined)
|
|
96
|
+
return missing(composed, wanted);
|
|
97
|
+
const movements = composed.movements.filter((movement) => movement.component === wanted);
|
|
98
|
+
return [
|
|
99
|
+
census(entry),
|
|
100
|
+
unexplainedSection(movements.filter((movement) => movement.cause === 'unexplained')),
|
|
101
|
+
explainedSection(movements.filter((movement) => movement.cause !== 'unexplained')),
|
|
102
|
+
divergenceSection(composed.divergences.filter((each) => each.component === wanted)),
|
|
103
|
+
echoesFor(composed.echoes.filter((echo) => echo.component === wanted)),
|
|
104
|
+
]
|
|
105
|
+
.filter((section) => section !== '')
|
|
106
|
+
.join('\n\n');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The census, or the one sentence that replaces it when there is nothing to count.
|
|
110
|
+
*
|
|
111
|
+
* A run whose subjects carry no component provenance — a fixture built with
|
|
112
|
+
* `createElement`, a page served without source stamping — would otherwise get a
|
|
113
|
+
* census of zeros and an empty `components:` list, which reads as a broken
|
|
114
|
+
* report rather than as an answered question. Every other section here already
|
|
115
|
+
* suppresses itself when empty; this one could not, because it is also the only
|
|
116
|
+
* place the reader learns that attribution was unavailable rather than clean.
|
|
117
|
+
*/
|
|
118
|
+
function headline(composed, unexplained) {
|
|
119
|
+
if (composed.components.length === 0) {
|
|
120
|
+
return (`no component named in ${composed.subjects.length} subject(s) — these subjects carry no ` +
|
|
121
|
+
'component provenance, so a change in them can be located in the image but not ' +
|
|
122
|
+
'attributed to what rendered it');
|
|
123
|
+
}
|
|
124
|
+
const echoes = composed.echoes.length + (composed.truncated?.echoes ?? 0);
|
|
125
|
+
const movements = composed.movements.length === 0
|
|
126
|
+
? 'nothing moved'
|
|
127
|
+
: `${composed.movements.length} movement(s), ${unexplained} of them unexplained`;
|
|
128
|
+
return (`${composed.components.length} component(s) across ${composed.subjects.length} subject(s) — ` +
|
|
129
|
+
`${echoes} shared rendering(s), ${composed.divergences.length} divergence(s), ${movements}\n` +
|
|
130
|
+
` components: ${preview(composed.components.map((entry) => entry.component), 20)}`);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The finding, and the two things it is allowed to be called.
|
|
134
|
+
*
|
|
135
|
+
* `flake` and `suspect` are not degrees of confidence in the same claim. One is
|
|
136
|
+
* a subject that disagreed with itself *and* has no explanation; the other is a
|
|
137
|
+
* subject nobody has read twice, which is a shortlist entry — the scarce
|
|
138
|
+
* resource a sweep spends, pointed somewhere better than plan order.
|
|
139
|
+
*/
|
|
140
|
+
function unexplainedSection(movements) {
|
|
141
|
+
if (movements.length === 0)
|
|
142
|
+
return '';
|
|
143
|
+
const flakes = movements.filter((movement) => movement.standing === 'flake').length;
|
|
144
|
+
return [
|
|
145
|
+
`unexplained (${movements.length}) — no edited file, moved token, edited caller or ` +
|
|
146
|
+
'contradiction in this run accounts for these' +
|
|
147
|
+
(flakes === 0 ? '' : `; ${flakes} in subjects already proven unstable`),
|
|
148
|
+
'',
|
|
149
|
+
...movements.map(renderUnexplained),
|
|
150
|
+
].join('\n');
|
|
151
|
+
}
|
|
152
|
+
function renderUnexplained(movement) {
|
|
153
|
+
const standing = movement.standing === 'flake'
|
|
154
|
+
? ' [flake] the subject also failed to read the same way twice in this run, so both ' +
|
|
155
|
+
'halves of the sentence are present\n'
|
|
156
|
+
: movement.standing === 'suspect'
|
|
157
|
+
? ' [suspect] nothing has read this subject twice, so this is a shortlist entry and ' +
|
|
158
|
+
'not a verdict — `variance run --flakes` is what settles it\n'
|
|
159
|
+
: '';
|
|
160
|
+
const held = movement.held.length === 0
|
|
161
|
+
? ' no control: it renders nowhere else in this run with these inputs, which weakens ' +
|
|
162
|
+
'this rather than strengthening it\n'
|
|
163
|
+
: ` held in ${movement.held.length} other place(s): ${preview(movement.held)}\n`;
|
|
164
|
+
return (`${movement.subject} · ${movement.component}${bands(movement.bands)}\n` +
|
|
165
|
+
` ${movement.because}\n` +
|
|
166
|
+
standing +
|
|
167
|
+
held +
|
|
168
|
+
alsoIn(movement));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* One line per cause, not one per subject.
|
|
172
|
+
*
|
|
173
|
+
* A token edit reaching eleven subjects is eleven movement records carrying one
|
|
174
|
+
* sentence between them, and printing all eleven is the failure
|
|
175
|
+
* `variance_changes` exists to prevent, arrived at from the other direction.
|
|
176
|
+
* Grouped on the `because`, which names the evidence, so two movements share a
|
|
177
|
+
* line only when they share the actual reason.
|
|
178
|
+
*/
|
|
179
|
+
function explainedSection(movements) {
|
|
180
|
+
if (movements.length === 0)
|
|
181
|
+
return '';
|
|
182
|
+
const groups = new Map();
|
|
183
|
+
for (const movement of movements) {
|
|
184
|
+
const head = `${movement.component} [${movement.cause}] — ${movement.because}`;
|
|
185
|
+
const group = groups.get(head);
|
|
186
|
+
if (group === undefined)
|
|
187
|
+
groups.set(head, { head, subjects: [movement.subject] });
|
|
188
|
+
else
|
|
189
|
+
group.subjects.push(movement.subject);
|
|
190
|
+
}
|
|
191
|
+
return [
|
|
192
|
+
`explained (${groups.size} cause(s) across ${movements.length} movement(s))`,
|
|
193
|
+
'',
|
|
194
|
+
...[...groups.values()].map((group) => `${group.head}\n ${group.subjects.length} subject(s): ${preview(group.subjects)}`),
|
|
195
|
+
].join('\n');
|
|
196
|
+
}
|
|
197
|
+
function divergenceSection(divergences) {
|
|
198
|
+
if (divergences.length === 0)
|
|
199
|
+
return '';
|
|
200
|
+
return [
|
|
201
|
+
`diverging at this commit (${divergences.length}) — one input, more than one rendering. Not ` +
|
|
202
|
+
'a regression: there is no baseline anywhere in this. Either something outside the props ' +
|
|
203
|
+
'decides part of the output — a token, a theme, an ancestor’s cascade — or the reading is ' +
|
|
204
|
+
'not repeatable.',
|
|
205
|
+
'',
|
|
206
|
+
...divergences.map((divergence) => `${divergence.component}${bands(divergence.bands)} — ${divergence.renderings.length} ` +
|
|
207
|
+
`rendering(s) from one props digest\n` +
|
|
208
|
+
// One line per rendering, widest first. The split is the finding: the
|
|
209
|
+
// subjects on the short line are the ones that disagree with the rest,
|
|
210
|
+
// and they are what a reader opens. A single flat list of every subject
|
|
211
|
+
// involved says a divergence happened and refuses to say where.
|
|
212
|
+
divergence.renderings
|
|
213
|
+
.map((subjects, index) => ` ${subjects.length} subject(s): ${preview(subjects)}` +
|
|
214
|
+
// The parting, indented under the rendering it explains. This is
|
|
215
|
+
// the answer to the question the rest of the section only poses —
|
|
216
|
+
// an agent reading this has the moved input by name and never has
|
|
217
|
+
// to fetch two subjects and diff them.
|
|
218
|
+
partingLines(divergence, index))
|
|
219
|
+
.join('\n')),
|
|
220
|
+
].join('\n');
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The parting for one rendering, indented, or nothing.
|
|
224
|
+
*
|
|
225
|
+
* Nothing when the run kept no documents — silence rather than a line saying so,
|
|
226
|
+
* because this section is already explicit that a divergence has two possible
|
|
227
|
+
* causes, and a per-rendering "not read" under every row would be the same
|
|
228
|
+
* caveat repeated once per subject.
|
|
229
|
+
*/
|
|
230
|
+
function partingLines(divergence, rendering) {
|
|
231
|
+
const parting = divergence.partings?.find((entry) => entry.rendering === rendering);
|
|
232
|
+
if (parting === undefined)
|
|
233
|
+
return '';
|
|
234
|
+
return parting.lines.map((line) => `\n ${line}`).join('');
|
|
235
|
+
}
|
|
236
|
+
function echoSection(composed) {
|
|
237
|
+
const dropped = composed.truncated?.echoes ?? 0;
|
|
238
|
+
const total = composed.echoes.length + dropped;
|
|
239
|
+
if (total === 0)
|
|
240
|
+
return '';
|
|
241
|
+
const shown = composed.echoes.slice(0, MAX_ECHOES_SHOWN);
|
|
242
|
+
const hidden = composed.echoes.length - shown.length;
|
|
243
|
+
return [
|
|
244
|
+
`shared renderings (${total}) — the same component producing the same output in more than ` +
|
|
245
|
+
'one subject. Two diffs over one of these are one thing to review, and an example among ' +
|
|
246
|
+
'them is the narrow subject to review it in.',
|
|
247
|
+
'',
|
|
248
|
+
...shown.map(renderEcho),
|
|
249
|
+
...(hidden === 0
|
|
250
|
+
? []
|
|
251
|
+
: [`${hidden} more are in the report; ask with \`component\` to see one component’s.`]),
|
|
252
|
+
...(dropped === 0
|
|
253
|
+
? []
|
|
254
|
+
: [
|
|
255
|
+
`${dropped} more were never written to the report: the run caps this list, and what ` +
|
|
256
|
+
'the cap left out is counted here rather than passed off as coverage.',
|
|
257
|
+
]),
|
|
258
|
+
].join('\n');
|
|
259
|
+
}
|
|
260
|
+
function echoesFor(echoes) {
|
|
261
|
+
if (echoes.length === 0)
|
|
262
|
+
return '';
|
|
263
|
+
return [`shared renderings (${echoes.length})`, '', ...echoes.map(renderEcho)].join('\n');
|
|
264
|
+
}
|
|
265
|
+
function renderEcho(echo) {
|
|
266
|
+
const example = echo.example === undefined
|
|
267
|
+
? ' no example among them: every subject sharing this rendering is a larger one'
|
|
268
|
+
: ` example: ${echo.example}`;
|
|
269
|
+
return (`${echo.component} · ${echo.subjects.length} subject(s), ${echo.sites} site(s)\n` +
|
|
270
|
+
`${example}\n` +
|
|
271
|
+
` ${preview(echo.subjects)}`);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* The components the suite watches only through something else.
|
|
275
|
+
*
|
|
276
|
+
* The gap a reviewer is usually looking for, and it is a list rather than a
|
|
277
|
+
* count because the names are the work: a component with no narrow example is
|
|
278
|
+
* reviewed through whatever page happens to contain it, and a change to it that
|
|
279
|
+
* the page's own noise absorbs is a change nothing in the suite would catch.
|
|
280
|
+
*/
|
|
281
|
+
function orphanSection(components) {
|
|
282
|
+
const orphans = components.filter((entry) => entry.examples.length === 0);
|
|
283
|
+
if (orphans.length === 0)
|
|
284
|
+
return '';
|
|
285
|
+
return (`no example of their own (${orphans.length}): ` +
|
|
286
|
+
`${preview(orphans.map((entry) => entry.component), 12)}\n` +
|
|
287
|
+
' These appear only inside larger subjects, so a change to one is reviewed through ' +
|
|
288
|
+
'whatever page happens to contain it.');
|
|
289
|
+
}
|
|
290
|
+
function census(entry) {
|
|
291
|
+
const shape = entry.renderings === entry.variants
|
|
292
|
+
? `${entry.variants} input(s), ${entry.renderings} rendering(s): here, its output is a ` +
|
|
293
|
+
'function of its props'
|
|
294
|
+
: entry.renderings > entry.variants
|
|
295
|
+
? `${entry.variants} input(s) produced ${entry.renderings} rendering(s): its output is ` +
|
|
296
|
+
'not a function of its props alone'
|
|
297
|
+
: `${entry.variants} input(s) produced ${entry.renderings} rendering(s): some of those ` +
|
|
298
|
+
'inputs make no difference to what it renders';
|
|
299
|
+
return [
|
|
300
|
+
`${entry.component} — ${entry.instances} boundary(ies) in ${entry.subjects.length} subject(s)`,
|
|
301
|
+
entry.examples.length === 0
|
|
302
|
+
? ' no example of its own: it appears only inside larger subjects'
|
|
303
|
+
: ` example(s): ${entry.examples.join(', ')}`,
|
|
304
|
+
` within: ${list(entry.within)}`,
|
|
305
|
+
// Two upward edges because they answer different questions. `within` is
|
|
306
|
+
// where the boundary sits, which is often a presentational wrapper that
|
|
307
|
+
// knows nothing about it; `created by` is who wrote the element, which is
|
|
308
|
+
// the file whose edit changed this component's inputs.
|
|
309
|
+
` created by: ${list(entry.createdBy)}${entry.createdBy.length === 0
|
|
310
|
+
? ' — either nothing mounted it, or this was a production build, where the owner is gone'
|
|
311
|
+
: ''}`,
|
|
312
|
+
` renders: ${list(entry.renders)}`,
|
|
313
|
+
` tokens: ${list(entry.tokens)}`,
|
|
314
|
+
` ${shape}`,
|
|
315
|
+
...(entry.renderings === entry.variants
|
|
316
|
+
? []
|
|
317
|
+
: [
|
|
318
|
+
' (an input here is a props digest, which excludes `children` — a pair that the ' +
|
|
319
|
+
'children could explain is refused rather than reported)',
|
|
320
|
+
]),
|
|
321
|
+
` subjects: ${preview(entry.subjects, 8)}`,
|
|
322
|
+
].join('\n');
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* A name with no census entry, and the two very different reasons for that.
|
|
326
|
+
*
|
|
327
|
+
* A component that renders nothing but other components owns no DOM node, is a
|
|
328
|
+
* boundary nowhere, and has no entry — while being exactly the file a reviewer
|
|
329
|
+
* has to open, because it is where the props are written. Measured on
|
|
330
|
+
* `examples/todomvc`: `TodoFooter` is in no entry and is the `createdBy` of
|
|
331
|
+
* every `Chip`. Answering that with "no such component" would send an agent
|
|
332
|
+
* looking for a typo in the one name that would have explained the run.
|
|
333
|
+
*/
|
|
334
|
+
function missing(composed, wanted) {
|
|
335
|
+
const mounted = composed.components
|
|
336
|
+
.filter((entry) => entry.createdBy.includes(wanted))
|
|
337
|
+
.map((entry) => entry.component);
|
|
338
|
+
if (mounted.length > 0) {
|
|
339
|
+
return (`\`${wanted}\` is a boundary nowhere in this suite, so it has no census entry: it renders ` +
|
|
340
|
+
'other components and authors no DOM node of its own. It is still in the graph, as the ' +
|
|
341
|
+
`component that mounted ${mounted.length}: ${preview(mounted, 12)} — and it is usually ` +
|
|
342
|
+
'the file to open, because it is where their props are written.');
|
|
343
|
+
}
|
|
344
|
+
const names = composed.components.map((entry) => entry.component);
|
|
345
|
+
return (`This run composed no component named \`${wanted}\`, and nothing it did compose names it as ` +
|
|
346
|
+
`a creator. It has ${names.length}: ${preview(names, 20)}`);
|
|
347
|
+
}
|
|
348
|
+
/** Empty means *not known* — a name-only comparison — never *no band*. */
|
|
349
|
+
function bands(bands) {
|
|
350
|
+
return bands.length === 0 ? '' : ` (${bands.join(', ')})`;
|
|
351
|
+
}
|
|
352
|
+
function alsoIn(movement) {
|
|
353
|
+
return movement.alsoIn.length === 0
|
|
354
|
+
? ''
|
|
355
|
+
: ` also moved in ${movement.alsoIn.length}: ${preview(movement.alsoIn)}\n`;
|
|
356
|
+
}
|
|
357
|
+
function list(values) {
|
|
358
|
+
return values.length === 0 ? '(none)' : values.join(', ');
|
|
359
|
+
}
|
|
360
|
+
/** The first few, because a reader wants examples and not a manifest. */
|
|
361
|
+
function preview(values, limit = 4) {
|
|
362
|
+
return values.length <= limit
|
|
363
|
+
? values.join(', ')
|
|
364
|
+
: `${values.slice(0, limit).join(', ')}, and ${values.length - limit} more`;
|
|
365
|
+
}
|
|
366
|
+
//# sourceMappingURL=composition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition.js","sourceRoot":"","sources":["../../src/tools/composition.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ComponentRecord,\n CompositionReport,\n DivergenceRecord,\n EchoRecord,\n MovementRecord,\n} from '@variance-authority/report';\nimport type { Tool } from './tool.js';\n\n/**\n * `variance_composition` — the suite as one graph, and the flakes it shortlists.\n *\n * The other axis. Every other tool in this package answers about *one subject\n * against its baseline*: two revisions, one thing. This one answers about the\n * run's subjects compared to **each other**, at one commit, which is the\n * question a suite of examples has always implied and never written down:\n *\n * > A visual-regression example is a component built from components. The\n * > example *is* a component, at a boundary; the same component appears again,\n * > with the same or different props, inside larger examples.\n *\n * It earns a tool of its own rather than a paragraph in `variance_summary`\n * because it changes what an agent does next twice over. `variance_changes`\n * turns forty changed subjects into three decisions; this turns a decision into\n * the component and the caller behind it, and then says which movements\n * *nothing in the run explains* — the only place in this package where a flake\n * is named, and the only one that can name it, because naming one needs the\n * places the same component **held**.\n *\n * Not `variance_trace_component`, which reads the regions of changed subjects\n * and is therefore blind to every component that did not move. The control\n * group is the half of a flake report that makes the other half worth reading,\n * so this reads every boundary in every subject instead.\n */\n\n/** Echoes printed before the list is cut. The rest are counted, never dropped in silence. */\nconst MAX_ECHOES_SHOWN = 10;\n\n/**\n * The suite's graph: what it is built from, what repeats, and what moved.\n *\n * Ordered by what an agent acts on rather than by what is interesting.\n * Unexplained movements lead, because they are the only entries here that are a\n * *finding*; explained ones are folded to one line per cause, for the same\n * reason `variance_changes` refuses to list forty subjects; and the shared\n * renderings come last, because they change how a review is *sized* rather than\n * what it concludes.\n */\nexport const composition: Tool = {\n name: 'variance_composition',\n description:\n 'The run’s subjects compared to each other at one commit rather than to their baselines: ' +\n 'which components the suite is built from, which renderings appear in more than one subject ' +\n '(so two diffs are one thing to review), which components render two ways from one input, ' +\n 'and — for everything that moved — whether an edited file, a moved token or an edited caller ' +\n 'explains it. Movements nothing explains are named here: `flake` where the subject also ' +\n 'failed to read the same way twice, `suspect` where nobody has read it twice yet. Pass ' +\n '`component` for one component’s census entry, the subjects it holds in, and the examples ' +\n 'that watch it. Ask this when a change has no obvious author, when you need the component ' +\n 'behind a set of diffs, or before calling anything flaky.',\n inputSchema: {\n type: 'object',\n properties: {\n component: {\n type: 'string',\n description: 'Optional. One component’s entry in the graph, instead of the whole suite.',\n },\n },\n additionalProperties: false,\n },\n\n run(report, input) {\n const composed = report.composition;\n if (composed === undefined) return ABSENT;\n\n const wanted = typeof input?.['component'] === 'string' ? input['component'] : undefined;\n return wanted === undefined ? whole(composed) : one(composed, wanted);\n },\n};\n\n/**\n * The absence, said as an absence.\n *\n * `composition` is missing from a report whose collection produced no semantic\n * snapshots, and rendering that as an empty graph would answer *this suite\n * shares nothing* — a different claim, and a false one. Absent is not empty\n * here for the same reason it is not empty anywhere else in this system.\n */\nconst ABSENT =\n 'This run composed nothing, which is not the same as it finding nothing. No subject supplied ' +\n 'a semantic snapshot — a raster-only or ephemeral run compares images and never sees a ' +\n 'component boundary — so there was nothing to join. Nothing here says the suite shares no ' +\n 'components; it says this run cannot tell.';\n\nfunction whole(composed: CompositionReport): string {\n const unexplained = composed.movements.filter((movement) => movement.cause === 'unexplained');\n const explained = composed.movements.filter((movement) => movement.cause !== 'unexplained');\n\n return [\n headline(composed, unexplained.length),\n unexplainedSection(unexplained),\n explainedSection(explained),\n divergenceSection(composed.divergences),\n echoSection(composed),\n orphanSection(composed.components),\n ]\n .filter((section) => section !== '')\n .join('\\n\\n');\n}\n\nfunction one(composed: CompositionReport, wanted: string): string {\n const entry = composed.components.find((each) => each.component === wanted);\n if (entry === undefined) return missing(composed, wanted);\n\n const movements = composed.movements.filter((movement) => movement.component === wanted);\n\n return [\n census(entry),\n unexplainedSection(movements.filter((movement) => movement.cause === 'unexplained')),\n explainedSection(movements.filter((movement) => movement.cause !== 'unexplained')),\n divergenceSection(composed.divergences.filter((each) => each.component === wanted)),\n echoesFor(composed.echoes.filter((echo) => echo.component === wanted)),\n ]\n .filter((section) => section !== '')\n .join('\\n\\n');\n}\n\n/**\n * The census, or the one sentence that replaces it when there is nothing to count.\n *\n * A run whose subjects carry no component provenance — a fixture built with\n * `createElement`, a page served without source stamping — would otherwise get a\n * census of zeros and an empty `components:` list, which reads as a broken\n * report rather than as an answered question. Every other section here already\n * suppresses itself when empty; this one could not, because it is also the only\n * place the reader learns that attribution was unavailable rather than clean.\n */\nfunction headline(composed: CompositionReport, unexplained: number): string {\n if (composed.components.length === 0) {\n return (\n `no component named in ${composed.subjects.length} subject(s) — these subjects carry no ` +\n 'component provenance, so a change in them can be located in the image but not ' +\n 'attributed to what rendered it'\n );\n }\n\n const echoes = composed.echoes.length + (composed.truncated?.echoes ?? 0);\n const movements =\n composed.movements.length === 0\n ? 'nothing moved'\n : `${composed.movements.length} movement(s), ${unexplained} of them unexplained`;\n\n return (\n `${composed.components.length} component(s) across ${composed.subjects.length} subject(s) — ` +\n `${echoes} shared rendering(s), ${composed.divergences.length} divergence(s), ${movements}\\n` +\n ` components: ${preview(composed.components.map((entry) => entry.component), 20)}`\n );\n}\n\n/**\n * The finding, and the two things it is allowed to be called.\n *\n * `flake` and `suspect` are not degrees of confidence in the same claim. One is\n * a subject that disagreed with itself *and* has no explanation; the other is a\n * subject nobody has read twice, which is a shortlist entry — the scarce\n * resource a sweep spends, pointed somewhere better than plan order.\n */\nfunction unexplainedSection(movements: readonly MovementRecord[]): string {\n if (movements.length === 0) return '';\n\n const flakes = movements.filter((movement) => movement.standing === 'flake').length;\n\n return [\n `unexplained (${movements.length}) — no edited file, moved token, edited caller or ` +\n 'contradiction in this run accounts for these' +\n (flakes === 0 ? '' : `; ${flakes} in subjects already proven unstable`),\n '',\n ...movements.map(renderUnexplained),\n ].join('\\n');\n}\n\nfunction renderUnexplained(movement: MovementRecord): string {\n const standing =\n movement.standing === 'flake'\n ? ' [flake] the subject also failed to read the same way twice in this run, so both ' +\n 'halves of the sentence are present\\n'\n : movement.standing === 'suspect'\n ? ' [suspect] nothing has read this subject twice, so this is a shortlist entry and ' +\n 'not a verdict — `variance run --flakes` is what settles it\\n'\n : '';\n\n const held =\n movement.held.length === 0\n ? ' no control: it renders nowhere else in this run with these inputs, which weakens ' +\n 'this rather than strengthening it\\n'\n : ` held in ${movement.held.length} other place(s): ${preview(movement.held)}\\n`;\n\n return (\n `${movement.subject} · ${movement.component}${bands(movement.bands)}\\n` +\n ` ${movement.because}\\n` +\n standing +\n held +\n alsoIn(movement)\n );\n}\n\n/**\n * One line per cause, not one per subject.\n *\n * A token edit reaching eleven subjects is eleven movement records carrying one\n * sentence between them, and printing all eleven is the failure\n * `variance_changes` exists to prevent, arrived at from the other direction.\n * Grouped on the `because`, which names the evidence, so two movements share a\n * line only when they share the actual reason.\n */\nfunction explainedSection(movements: readonly MovementRecord[]): string {\n if (movements.length === 0) return '';\n\n const groups = new Map<string, { readonly head: string; readonly subjects: string[] }>();\n for (const movement of movements) {\n const head = `${movement.component} [${movement.cause}] — ${movement.because}`;\n const group = groups.get(head);\n if (group === undefined) groups.set(head, { head, subjects: [movement.subject] });\n else group.subjects.push(movement.subject);\n }\n\n return [\n `explained (${groups.size} cause(s) across ${movements.length} movement(s))`,\n '',\n ...[...groups.values()].map(\n (group) => `${group.head}\\n ${group.subjects.length} subject(s): ${preview(group.subjects)}`,\n ),\n ].join('\\n');\n}\n\nfunction divergenceSection(divergences: readonly DivergenceRecord[]): string {\n if (divergences.length === 0) return '';\n\n return [\n `diverging at this commit (${divergences.length}) — one input, more than one rendering. Not ` +\n 'a regression: there is no baseline anywhere in this. Either something outside the props ' +\n 'decides part of the output — a token, a theme, an ancestor’s cascade — or the reading is ' +\n 'not repeatable.',\n '',\n ...divergences.map(\n (divergence) =>\n `${divergence.component}${bands(divergence.bands)} — ${divergence.renderings.length} ` +\n `rendering(s) from one props digest\\n` +\n // One line per rendering, widest first. The split is the finding: the\n // subjects on the short line are the ones that disagree with the rest,\n // and they are what a reader opens. A single flat list of every subject\n // involved says a divergence happened and refuses to say where.\n divergence.renderings\n .map(\n (subjects, index) =>\n ` ${subjects.length} subject(s): ${preview(subjects)}` +\n // The parting, indented under the rendering it explains. This is\n // the answer to the question the rest of the section only poses —\n // an agent reading this has the moved input by name and never has\n // to fetch two subjects and diff them.\n partingLines(divergence, index),\n )\n .join('\\n'),\n ),\n ].join('\\n');\n}\n\n/**\n * The parting for one rendering, indented, or nothing.\n *\n * Nothing when the run kept no documents — silence rather than a line saying so,\n * because this section is already explicit that a divergence has two possible\n * causes, and a per-rendering \"not read\" under every row would be the same\n * caveat repeated once per subject.\n */\nfunction partingLines(divergence: DivergenceRecord, rendering: number): string {\n const parting = divergence.partings?.find((entry) => entry.rendering === rendering);\n if (parting === undefined) return '';\n return parting.lines.map((line) => `\\n ${line}`).join('');\n}\n\nfunction echoSection(composed: CompositionReport): string {\n const dropped = composed.truncated?.echoes ?? 0;\n const total = composed.echoes.length + dropped;\n if (total === 0) return '';\n\n const shown = composed.echoes.slice(0, MAX_ECHOES_SHOWN);\n const hidden = composed.echoes.length - shown.length;\n\n return [\n `shared renderings (${total}) — the same component producing the same output in more than ` +\n 'one subject. Two diffs over one of these are one thing to review, and an example among ' +\n 'them is the narrow subject to review it in.',\n '',\n ...shown.map(renderEcho),\n ...(hidden === 0\n ? []\n : [`${hidden} more are in the report; ask with \\`component\\` to see one component’s.`]),\n ...(dropped === 0\n ? []\n : [\n `${dropped} more were never written to the report: the run caps this list, and what ` +\n 'the cap left out is counted here rather than passed off as coverage.',\n ]),\n ].join('\\n');\n}\n\nfunction echoesFor(echoes: readonly EchoRecord[]): string {\n if (echoes.length === 0) return '';\n return [`shared renderings (${echoes.length})`, '', ...echoes.map(renderEcho)].join('\\n');\n}\n\nfunction renderEcho(echo: EchoRecord): string {\n const example =\n echo.example === undefined\n ? ' no example among them: every subject sharing this rendering is a larger one'\n : ` example: ${echo.example}`;\n\n return (\n `${echo.component} · ${echo.subjects.length} subject(s), ${echo.sites} site(s)\\n` +\n `${example}\\n` +\n ` ${preview(echo.subjects)}`\n );\n}\n\n/**\n * The components the suite watches only through something else.\n *\n * The gap a reviewer is usually looking for, and it is a list rather than a\n * count because the names are the work: a component with no narrow example is\n * reviewed through whatever page happens to contain it, and a change to it that\n * the page's own noise absorbs is a change nothing in the suite would catch.\n */\nfunction orphanSection(components: readonly ComponentRecord[]): string {\n const orphans = components.filter((entry) => entry.examples.length === 0);\n if (orphans.length === 0) return '';\n\n return (\n `no example of their own (${orphans.length}): ` +\n `${preview(orphans.map((entry) => entry.component), 12)}\\n` +\n ' These appear only inside larger subjects, so a change to one is reviewed through ' +\n 'whatever page happens to contain it.'\n );\n}\n\nfunction census(entry: ComponentRecord): string {\n const shape =\n entry.renderings === entry.variants\n ? `${entry.variants} input(s), ${entry.renderings} rendering(s): here, its output is a ` +\n 'function of its props'\n : entry.renderings > entry.variants\n ? `${entry.variants} input(s) produced ${entry.renderings} rendering(s): its output is ` +\n 'not a function of its props alone'\n : `${entry.variants} input(s) produced ${entry.renderings} rendering(s): some of those ` +\n 'inputs make no difference to what it renders';\n\n return [\n `${entry.component} — ${entry.instances} boundary(ies) in ${entry.subjects.length} subject(s)`,\n entry.examples.length === 0\n ? ' no example of its own: it appears only inside larger subjects'\n : ` example(s): ${entry.examples.join(', ')}`,\n ` within: ${list(entry.within)}`,\n // Two upward edges because they answer different questions. `within` is\n // where the boundary sits, which is often a presentational wrapper that\n // knows nothing about it; `created by` is who wrote the element, which is\n // the file whose edit changed this component's inputs.\n ` created by: ${list(entry.createdBy)}${\n entry.createdBy.length === 0\n ? ' — either nothing mounted it, or this was a production build, where the owner is gone'\n : ''\n }`,\n ` renders: ${list(entry.renders)}`,\n ` tokens: ${list(entry.tokens)}`,\n ` ${shape}`,\n ...(entry.renderings === entry.variants\n ? []\n : [\n ' (an input here is a props digest, which excludes `children` — a pair that the ' +\n 'children could explain is refused rather than reported)',\n ]),\n ` subjects: ${preview(entry.subjects, 8)}`,\n ].join('\\n');\n}\n\n/**\n * A name with no census entry, and the two very different reasons for that.\n *\n * A component that renders nothing but other components owns no DOM node, is a\n * boundary nowhere, and has no entry — while being exactly the file a reviewer\n * has to open, because it is where the props are written. Measured on\n * `examples/todomvc`: `TodoFooter` is in no entry and is the `createdBy` of\n * every `Chip`. Answering that with \"no such component\" would send an agent\n * looking for a typo in the one name that would have explained the run.\n */\nfunction missing(composed: CompositionReport, wanted: string): string {\n const mounted = composed.components\n .filter((entry) => entry.createdBy.includes(wanted))\n .map((entry) => entry.component);\n\n if (mounted.length > 0) {\n return (\n `\\`${wanted}\\` is a boundary nowhere in this suite, so it has no census entry: it renders ` +\n 'other components and authors no DOM node of its own. It is still in the graph, as the ' +\n `component that mounted ${mounted.length}: ${preview(mounted, 12)} — and it is usually ` +\n 'the file to open, because it is where their props are written.'\n );\n }\n\n const names = composed.components.map((entry) => entry.component);\n return (\n `This run composed no component named \\`${wanted}\\`, and nothing it did compose names it as ` +\n `a creator. It has ${names.length}: ${preview(names, 20)}`\n );\n}\n\n/** Empty means *not known* — a name-only comparison — never *no band*. */\nfunction bands(bands: readonly string[]): string {\n return bands.length === 0 ? '' : ` (${bands.join(', ')})`;\n}\n\nfunction alsoIn(movement: MovementRecord): string {\n return movement.alsoIn.length === 0\n ? ''\n : ` also moved in ${movement.alsoIn.length}: ${preview(movement.alsoIn)}\\n`;\n}\n\nfunction list(values: readonly string[]): string {\n return values.length === 0 ? '(none)' : values.join(', ');\n}\n\n/** The first few, because a reader wants examples and not a manifest. */\nfunction preview(values: readonly string[], limit = 4): string {\n return values.length <= limit\n ? values.join(', ')\n : `${values.slice(0, limit).join(', ')}, and ${values.length - limit} more`;\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Tool } from './tool.js';
|
|
2
|
+
/**
|
|
3
|
+
* `variance_describe`, and the region format nothing else prints.
|
|
4
|
+
*
|
|
5
|
+
* `regionLine` stays with the tool because the region list is this answer's whole
|
|
6
|
+
* substance, and everything around it is an ordering decision that belongs to
|
|
7
|
+
* this tool rather than to regions in general: the leak warning above the list
|
|
8
|
+
* because it changes what every line below means, the findings before it because
|
|
9
|
+
* an `unchanged` subject can carry nothing else, and the truncation count after
|
|
10
|
+
* it because a capped list that does not say so reads as a complete one.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* One subject, in full: what changed, where it is, and which file to open.
|
|
14
|
+
*
|
|
15
|
+
* The tool an agent calls after the summary, and the one that has to be complete
|
|
16
|
+
* — an agent that has to ask three more questions to locate an edit will guess
|
|
17
|
+
* instead.
|
|
18
|
+
*/
|
|
19
|
+
export declare const describe: Tool;
|
|
20
|
+
//# sourceMappingURL=describe.d.ts.map
|