@triscope/mcp 0.4.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/src/targets.ts ADDED
@@ -0,0 +1,163 @@
1
+ // Declarative "definition of done": evaluate per-camera convergence constraints
2
+ // from .claude/element-targets.json against captured stats, so an agent gets a
3
+ // machine-readable pass/fail instead of eyeballing screenshots. Pure + tested.
4
+
5
+ export interface Constraints {
6
+ /** Minimum SSIM vs the stored reference (needs a reference; else skipped). */
7
+ ssim?: number;
8
+ /** Maximum meanAbsDiff vs the stored reference (needs a reference; else skipped). */
9
+ meanAbsDiff?: number;
10
+ /** Allowed mean-luminance band (from the GPU probe — always available). */
11
+ luminance?: { min?: number; max?: number };
12
+ /** Allowed dynamic-range band (from the GPU probe). */
13
+ dynamicRange?: { min?: number; max?: number };
14
+ }
15
+
16
+ export interface CapturedStats {
17
+ ssim?: number;
18
+ meanAbsDiff?: number;
19
+ luminance?: number;
20
+ dynamicRange?: number;
21
+ }
22
+
23
+ export interface TargetResult {
24
+ camera: string;
25
+ constraint: string;
26
+ op: '>=' | '<=' | 'range' | 'present';
27
+ expected: unknown;
28
+ actual: unknown;
29
+ pass: boolean;
30
+ skipped?: boolean;
31
+ reason?: string;
32
+ }
33
+
34
+ export interface TargetReport {
35
+ checked: number;
36
+ passed: number;
37
+ allPassed: boolean;
38
+ results: TargetResult[];
39
+ }
40
+
41
+ export function evaluateTargets(
42
+ targetsByCamera: Record<string, Constraints>,
43
+ capturedByCamera: Record<string, CapturedStats>,
44
+ ): TargetReport {
45
+ const results: TargetResult[] = [];
46
+
47
+ for (const [camera, c] of Object.entries(targetsByCamera ?? {})) {
48
+ const cap = capturedByCamera?.[camera];
49
+ if (!cap) {
50
+ results.push({
51
+ camera,
52
+ constraint: 'capture',
53
+ op: 'present',
54
+ expected: 'captured view',
55
+ actual: undefined,
56
+ pass: false,
57
+ skipped: true,
58
+ reason: 'camera not captured',
59
+ });
60
+ continue;
61
+ }
62
+ if (typeof c.ssim === 'number') {
63
+ if (typeof cap.ssim === 'number') {
64
+ results.push({
65
+ camera,
66
+ constraint: 'ssim',
67
+ op: '>=',
68
+ expected: c.ssim,
69
+ actual: cap.ssim,
70
+ pass: cap.ssim >= c.ssim,
71
+ });
72
+ } else {
73
+ results.push({
74
+ camera,
75
+ constraint: 'ssim',
76
+ op: '>=',
77
+ expected: c.ssim,
78
+ actual: undefined,
79
+ pass: true,
80
+ skipped: true,
81
+ reason: 'no reference image — set_reference to enable',
82
+ });
83
+ }
84
+ }
85
+ if (typeof c.meanAbsDiff === 'number') {
86
+ if (typeof cap.meanAbsDiff === 'number') {
87
+ results.push({
88
+ camera,
89
+ constraint: 'meanAbsDiff',
90
+ op: '<=',
91
+ expected: c.meanAbsDiff,
92
+ actual: cap.meanAbsDiff,
93
+ pass: cap.meanAbsDiff <= c.meanAbsDiff,
94
+ });
95
+ } else {
96
+ results.push({
97
+ camera,
98
+ constraint: 'meanAbsDiff',
99
+ op: '<=',
100
+ expected: c.meanAbsDiff,
101
+ actual: undefined,
102
+ pass: true,
103
+ skipped: true,
104
+ reason: 'no reference image — set_reference to enable',
105
+ });
106
+ }
107
+ }
108
+ pushBand(results, camera, 'luminance', c.luminance, cap.luminance);
109
+ pushBand(results, camera, 'dynamicRange', c.dynamicRange, cap.dynamicRange);
110
+ }
111
+
112
+ const evaluated = results.filter((r) => !r.skipped);
113
+ const passed = evaluated.filter((r) => r.pass).length;
114
+ return {
115
+ checked: evaluated.length,
116
+ passed,
117
+ allPassed: evaluated.every((r) => r.pass),
118
+ results,
119
+ };
120
+ }
121
+
122
+ function pushBand(
123
+ out: TargetResult[],
124
+ camera: string,
125
+ constraint: string,
126
+ band: { min?: number; max?: number } | undefined,
127
+ actual: number | undefined,
128
+ ): void {
129
+ if (!band) return;
130
+ if (typeof actual !== 'number') {
131
+ out.push({
132
+ camera,
133
+ constraint,
134
+ op: 'range',
135
+ expected: band,
136
+ actual: undefined,
137
+ pass: true,
138
+ skipped: true,
139
+ reason: 'probe unavailable',
140
+ });
141
+ return;
142
+ }
143
+ if (typeof band.min === 'number') {
144
+ out.push({
145
+ camera,
146
+ constraint: `${constraint}.min`,
147
+ op: '>=',
148
+ expected: band.min,
149
+ actual,
150
+ pass: actual >= band.min,
151
+ });
152
+ }
153
+ if (typeof band.max === 'number') {
154
+ out.push({
155
+ camera,
156
+ constraint: `${constraint}.max`,
157
+ op: '<=',
158
+ expected: band.max,
159
+ actual,
160
+ pass: actual <= band.max,
161
+ });
162
+ }
163
+ }