@bridge_gpt/mcp-server 0.2.18 → 0.2.19
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/CONDUCTOR.md +75 -0
- package/README.md +2 -2
- package/build/agent-capabilities/probe-context.js +13 -3
- package/build/agent-capabilities/probes.js +262 -11
- package/build/agent-capabilities/reporter.js +1 -0
- package/build/agents.generated.js +1 -1
- package/build/backend-warnings.js +44 -0
- package/build/claude-settings.js +129 -0
- package/build/commands.generated.js +1 -0
- package/build/conductor/bridge-api-client.js +7 -7
- package/build/conductor/cli.js +65 -12
- package/build/conductor/deny-enforcement-preflight.js +96 -0
- package/build/conductor/doctor.js +183 -2
- package/build/conductor/epic-reconcile.js +9 -1
- package/build/conductor/epic-runtime.js +403 -43
- package/build/conductor/epic-state.js +7 -0
- package/build/conductor/errors.js +115 -3
- package/build/conductor/event-accessors.js +28 -10
- package/build/conductor/merge-ledger.js +6 -4
- package/build/conductor/pr-ci-producer.js +17 -2
- package/build/conductor/producer-ledger.js +1 -1
- package/build/conductor/store.js +161 -18
- package/build/conductor/supervisor-merge.js +32 -5
- package/build/conductor/taxonomy.js +8 -0
- package/build/conductor/tools.js +28 -6
- package/build/conductor/worker-ledger-cli.js +244 -0
- package/build/conductor-bin.js +1884 -6917
- package/build/doctor.js +8 -0
- package/build/executor/cli.js +229 -0
- package/build/executor/credentials.js +65 -0
- package/build/executor/deps.js +117 -0
- package/build/executor/env.js +79 -0
- package/build/executor/heartbeat.js +59 -0
- package/build/executor/http-client.js +131 -0
- package/build/executor/index.js +10 -0
- package/build/executor/job-errors.js +55 -0
- package/build/executor/job-log-registry.js +110 -0
- package/build/executor/job-runner.js +688 -0
- package/build/executor/job-types.js +60 -0
- package/build/executor/merge-job.js +155 -0
- package/build/executor/observation.js +123 -0
- package/build/executor/permissions.js +79 -0
- package/build/executor/preflight.js +144 -0
- package/build/executor/process.js +81 -0
- package/build/executor/prompt-spec.js +235 -0
- package/build/executor/results.js +134 -0
- package/build/executor/resume-pre-spawn.js +179 -0
- package/build/executor/runner.js +98 -0
- package/build/executor/terminal-mutation.js +34 -0
- package/build/executor/test-clock.js +109 -0
- package/build/executor/types.js +18 -0
- package/build/executor/verdict-artifact.js +53 -0
- package/build/executor/viewer-tabs.js +78 -0
- package/build/executor/watch-cli.js +113 -0
- package/build/executor/worker-command.js +106 -0
- package/build/executor/worker-finalization.js +97 -0
- package/build/executor/worker-log.js +92 -0
- package/build/executor/worktree-gc.js +134 -0
- package/build/executor/worktree-inspection.js +86 -0
- package/build/executor/worktree.js +103 -0
- package/build/index.js +11222 -8544
- package/build/mcp-invoke.js +19 -3
- package/build/mcp-provisioning.js +31 -25
- package/build/mcp-registration-doctor.js +27 -7
- package/build/mcp-server-invocation.js +152 -0
- package/build/pipelines.generated.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/sfcc/reads-site-preference.js +52 -19
- package/build/start-tickets-conductor.js +25 -93
- package/build/start-tickets-prereqs.js +152 -1
- package/build/start-tickets.js +96 -158
- package/build/version.generated.js +1 -1
- package/build/visual-diff-worker.js +313 -0
- package/build/visual-diff.js +632 -0
- package/build/worktree-core.js +202 -0
- package/package.json +8 -4
- package/public/css/main.min.css +39 -0
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +7924 -1
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +2 -1
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* visual-diff-worker.ts — deterministic PNG pixel-diff engine (BAPI-563).
|
|
3
|
+
*
|
|
4
|
+
* This module runs the CPU-heavy part of the `visual_diff` MCP tool: decode two
|
|
5
|
+
* PNG buffers with pngjs, apply mask boxes, diff them with pixelmatch (with an
|
|
6
|
+
* anti-aliasing-tolerant threshold so OS font rendering never inflates the
|
|
7
|
+
* mismatch), and derive coarse diff regions + a heatmap. Keeping this off the
|
|
8
|
+
* MCP server's event loop is why it is spawned as a worker thread from
|
|
9
|
+
* `visual-diff.ts` (see computeVisualDiffInWorker), but the actual logic lives
|
|
10
|
+
* in the exported pure function `computePngVisualDiff` so it can be unit-tested
|
|
11
|
+
* directly and reused inline as a fallback when worker spawning is unavailable.
|
|
12
|
+
*
|
|
13
|
+
* Dimensions are NEVER rescaled: a comp/render size mismatch is a hard signal
|
|
14
|
+
* (`dimension_match: false`) surfaced via a union canvas, not silently resized
|
|
15
|
+
* away.
|
|
16
|
+
*/
|
|
17
|
+
import { PNG } from "pngjs";
|
|
18
|
+
import pixelmatch from "pixelmatch";
|
|
19
|
+
import { isMainThread, parentPort, workerData } from "worker_threads";
|
|
20
|
+
/** Anti-aliasing-tolerant pixelmatch color-distance threshold (0..1). */
|
|
21
|
+
export const PIXELMATCH_COLOR_THRESHOLD = 0.1;
|
|
22
|
+
/** Default pass budget (percent) — never 0%, since AA/font rendering makes a
|
|
23
|
+
* true 0% mismatch impossible. Callers override via the `threshold` param. */
|
|
24
|
+
export const DEFAULT_PASS_MISMATCH_PCT = 2;
|
|
25
|
+
/** Maximum number of coarse diff regions returned (deterministic cap). */
|
|
26
|
+
export const MAX_DIFF_REGIONS = 10;
|
|
27
|
+
function decodePng(buffer, label) {
|
|
28
|
+
try {
|
|
29
|
+
const png = PNG.sync.read(Buffer.from(buffer));
|
|
30
|
+
if (!Number.isInteger(png.width) ||
|
|
31
|
+
!Number.isInteger(png.height) ||
|
|
32
|
+
png.width <= 0 ||
|
|
33
|
+
png.height <= 0) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: "IMAGE_DECODE_FAILED",
|
|
37
|
+
status: 422,
|
|
38
|
+
message: `Decoded ${label} PNG has invalid dimensions.`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { width: png.width, height: png.height, data: png.data };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Never surface a raw pngjs stack trace to the caller.
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: "IMAGE_DECODE_FAILED",
|
|
48
|
+
status: 422,
|
|
49
|
+
message: `Failed to decode ${label} image as PNG.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extract the top-left overlapW×overlapH region of a decoded RGBA image into a
|
|
55
|
+
* fresh contiguous RGBA buffer (no scaling).
|
|
56
|
+
*/
|
|
57
|
+
function extractOverlap(src, srcW, overlapW, overlapH) {
|
|
58
|
+
const out = new Uint8Array(overlapW * overlapH * 4);
|
|
59
|
+
for (let y = 0; y < overlapH; y++) {
|
|
60
|
+
const srcRow = y * srcW * 4;
|
|
61
|
+
const dstRow = y * overlapW * 4;
|
|
62
|
+
out.set(src.subarray(srcRow, srcRow + overlapW * 4), dstRow);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/** Build a union-sized boolean grid marking pixels covered by any mask box. */
|
|
67
|
+
function buildMaskGrid(boxes, unionW, unionH) {
|
|
68
|
+
const grid = new Uint8Array(unionW * unionH);
|
|
69
|
+
for (const box of boxes) {
|
|
70
|
+
const x0 = Math.max(0, Math.floor(box.x));
|
|
71
|
+
const y0 = Math.max(0, Math.floor(box.y));
|
|
72
|
+
const x1 = Math.min(unionW, Math.floor(box.x + box.width));
|
|
73
|
+
const y1 = Math.min(unionH, Math.floor(box.y + box.height));
|
|
74
|
+
for (let y = y0; y < y1; y++) {
|
|
75
|
+
for (let x = x0; x < x1; x++) {
|
|
76
|
+
grid[y * unionW + x] = 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return grid;
|
|
81
|
+
}
|
|
82
|
+
/** Fill masked cells (within the overlap region) with opaque black in-place. */
|
|
83
|
+
function applyMaskToOverlap(buf, overlapW, overlapH, maskGrid, unionW) {
|
|
84
|
+
for (let y = 0; y < overlapH; y++) {
|
|
85
|
+
for (let x = 0; x < overlapW; x++) {
|
|
86
|
+
if (maskGrid[y * unionW + x] === 1) {
|
|
87
|
+
const off = (y * overlapW + x) * 4;
|
|
88
|
+
buf[off] = 0;
|
|
89
|
+
buf[off + 1] = 0;
|
|
90
|
+
buf[off + 2] = 0;
|
|
91
|
+
buf[off + 3] = 255;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Group contiguous differing pixels into coarse bounding boxes via an iterative
|
|
98
|
+
* 4-connectivity flood fill. Deterministic: regions are sorted by pixel count
|
|
99
|
+
* descending, then y, then x, and capped at `maxRegions`.
|
|
100
|
+
*/
|
|
101
|
+
function extractDiffRegions(mask, width, height, maxRegions) {
|
|
102
|
+
const visited = new Uint8Array(width * height);
|
|
103
|
+
const regions = [];
|
|
104
|
+
const stack = [];
|
|
105
|
+
for (let start = 0; start < mask.length; start++) {
|
|
106
|
+
if (mask[start] === 0 || visited[start] === 1)
|
|
107
|
+
continue;
|
|
108
|
+
let minX = width;
|
|
109
|
+
let minY = height;
|
|
110
|
+
let maxX = -1;
|
|
111
|
+
let maxY = -1;
|
|
112
|
+
let pixels = 0;
|
|
113
|
+
stack.length = 0;
|
|
114
|
+
stack.push(start);
|
|
115
|
+
visited[start] = 1;
|
|
116
|
+
while (stack.length > 0) {
|
|
117
|
+
const idx = stack.pop();
|
|
118
|
+
const x = idx % width;
|
|
119
|
+
const y = (idx - x) / width;
|
|
120
|
+
pixels++;
|
|
121
|
+
if (x < minX)
|
|
122
|
+
minX = x;
|
|
123
|
+
if (y < minY)
|
|
124
|
+
minY = y;
|
|
125
|
+
if (x > maxX)
|
|
126
|
+
maxX = x;
|
|
127
|
+
if (y > maxY)
|
|
128
|
+
maxY = y;
|
|
129
|
+
// 4-connected neighbors.
|
|
130
|
+
if (x > 0) {
|
|
131
|
+
const n = idx - 1;
|
|
132
|
+
if (mask[n] === 1 && visited[n] === 0) {
|
|
133
|
+
visited[n] = 1;
|
|
134
|
+
stack.push(n);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (x < width - 1) {
|
|
138
|
+
const n = idx + 1;
|
|
139
|
+
if (mask[n] === 1 && visited[n] === 0) {
|
|
140
|
+
visited[n] = 1;
|
|
141
|
+
stack.push(n);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (y > 0) {
|
|
145
|
+
const n = idx - width;
|
|
146
|
+
if (mask[n] === 1 && visited[n] === 0) {
|
|
147
|
+
visited[n] = 1;
|
|
148
|
+
stack.push(n);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (y < height - 1) {
|
|
152
|
+
const n = idx + width;
|
|
153
|
+
if (mask[n] === 1 && visited[n] === 0) {
|
|
154
|
+
visited[n] = 1;
|
|
155
|
+
stack.push(n);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
regions.push({
|
|
160
|
+
x: minX,
|
|
161
|
+
y: minY,
|
|
162
|
+
width: maxX - minX + 1,
|
|
163
|
+
height: maxY - minY + 1,
|
|
164
|
+
pixels,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
regions.sort((a, b) => {
|
|
168
|
+
if (b.pixels !== a.pixels)
|
|
169
|
+
return b.pixels - a.pixels;
|
|
170
|
+
if (a.y !== b.y)
|
|
171
|
+
return a.y - b.y;
|
|
172
|
+
return a.x - b.x;
|
|
173
|
+
});
|
|
174
|
+
return regions.slice(0, Math.max(0, maxRegions));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Pure deterministic PNG visual diff. Given two PNG buffers plus mask boxes and
|
|
178
|
+
* thresholds, returns mismatch percent, dimension-match signal, coarse diff
|
|
179
|
+
* regions, and a base64 heatmap — or a structured decode error. Never throws a
|
|
180
|
+
* raw stack to the caller.
|
|
181
|
+
*/
|
|
182
|
+
export function computePngVisualDiff(input) {
|
|
183
|
+
const comp = decodePng(input.compPngBuffer, "comp");
|
|
184
|
+
if ("ok" in comp && comp.ok === false)
|
|
185
|
+
return comp;
|
|
186
|
+
const render = decodePng(input.renderPngBuffer, "render");
|
|
187
|
+
if ("ok" in render && render.ok === false)
|
|
188
|
+
return render;
|
|
189
|
+
const compImg = comp;
|
|
190
|
+
const renderImg = render;
|
|
191
|
+
const dimensionMatch = compImg.width === renderImg.width && compImg.height === renderImg.height;
|
|
192
|
+
const unionW = Math.max(compImg.width, renderImg.width);
|
|
193
|
+
const unionH = Math.max(compImg.height, renderImg.height);
|
|
194
|
+
const overlapW = Math.min(compImg.width, renderImg.width);
|
|
195
|
+
const overlapH = Math.min(compImg.height, renderImg.height);
|
|
196
|
+
const maskGrid = buildMaskGrid(input.maskBoxes ?? [], unionW, unionH);
|
|
197
|
+
// Diff the OVERLAP region with pixelmatch (both images have real, opaque
|
|
198
|
+
// pixels there, so alpha-blending never distorts the comparison). Dimensions
|
|
199
|
+
// are NEVER rescaled: the non-overlap region is handled explicitly below.
|
|
200
|
+
const output = new Uint8Array(unionW * unionH * 4);
|
|
201
|
+
const diffMask = new Uint8Array(unionW * unionH);
|
|
202
|
+
let differingPixels = 0;
|
|
203
|
+
if (overlapW > 0 && overlapH > 0) {
|
|
204
|
+
const compOverlap = extractOverlap(compImg.data, compImg.width, overlapW, overlapH);
|
|
205
|
+
const renderOverlap = extractOverlap(renderImg.data, renderImg.width, overlapW, overlapH);
|
|
206
|
+
applyMaskToOverlap(compOverlap, overlapW, overlapH, maskGrid, unionW);
|
|
207
|
+
applyMaskToOverlap(renderOverlap, overlapW, overlapH, maskGrid, unionW);
|
|
208
|
+
const overlapOut = new Uint8Array(overlapW * overlapH * 4);
|
|
209
|
+
try {
|
|
210
|
+
differingPixels = pixelmatch(compOverlap, renderOverlap, overlapOut, overlapW, overlapH, {
|
|
211
|
+
threshold: input.pixelmatchColorThreshold,
|
|
212
|
+
// includeAA: false => anti-aliased pixels are detected and NOT counted
|
|
213
|
+
// as diffs, so OS font rendering does not inflate the mismatch.
|
|
214
|
+
includeAA: false,
|
|
215
|
+
diffColor: [255, 0, 0],
|
|
216
|
+
diffColorAlt: [255, 0, 0],
|
|
217
|
+
aaColor: [255, 255, 0],
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
223
|
+
error: "DIFF_FAILED",
|
|
224
|
+
status: 500,
|
|
225
|
+
message: "Pixel comparison failed.",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// Copy the overlap heatmap into the union output's top-left; differing
|
|
229
|
+
// pixels are drawn pure red ([255,0,0]).
|
|
230
|
+
for (let y = 0; y < overlapH; y++) {
|
|
231
|
+
for (let x = 0; x < overlapW; x++) {
|
|
232
|
+
const so = (y * overlapW + x) * 4;
|
|
233
|
+
const uo = (y * unionW + x) * 4;
|
|
234
|
+
output[uo] = overlapOut[so];
|
|
235
|
+
output[uo + 1] = overlapOut[so + 1];
|
|
236
|
+
output[uo + 2] = overlapOut[so + 2];
|
|
237
|
+
output[uo + 3] = 255;
|
|
238
|
+
if (overlapOut[so] === 255 && overlapOut[so + 1] === 0 && overlapOut[so + 2] === 0) {
|
|
239
|
+
diffMask[y * unionW + x] = 1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Non-overlap region: a union cell in-bounds for exactly ONE image is a hard
|
|
245
|
+
// dimension-mismatch difference (the render has area the comp does not account
|
|
246
|
+
// for, or vice versa) and is forced-counted — never rescaled away. Cells
|
|
247
|
+
// covered by a mask box are ignored.
|
|
248
|
+
for (let y = 0; y < unionH; y++) {
|
|
249
|
+
for (let x = 0; x < unionW; x++) {
|
|
250
|
+
const inComp = x < compImg.width && y < compImg.height;
|
|
251
|
+
const inRender = x < renderImg.width && y < renderImg.height;
|
|
252
|
+
if (inComp === inRender)
|
|
253
|
+
continue; // overlap (handled) or both-out (empty)
|
|
254
|
+
if (maskGrid[y * unionW + x] === 1)
|
|
255
|
+
continue;
|
|
256
|
+
const off = (y * unionW + x) * 4;
|
|
257
|
+
output[off] = 255;
|
|
258
|
+
output[off + 1] = 0;
|
|
259
|
+
output[off + 2] = 0;
|
|
260
|
+
output[off + 3] = 255;
|
|
261
|
+
diffMask[y * unionW + x] = 1;
|
|
262
|
+
differingPixels++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const diffRegions = extractDiffRegions(diffMask, unionW, unionH, input.maxRegions ?? MAX_DIFF_REGIONS);
|
|
266
|
+
const totalPixels = unionW * unionH;
|
|
267
|
+
const mismatchPct = totalPixels > 0 ? (differingPixels / totalPixels) * 100 : 0;
|
|
268
|
+
const passed = dimensionMatch && mismatchPct <= input.passMismatchPct;
|
|
269
|
+
let heatmapBase64;
|
|
270
|
+
try {
|
|
271
|
+
const png = new PNG({ width: unionW, height: unionH });
|
|
272
|
+
png.data = Buffer.from(output);
|
|
273
|
+
heatmapBase64 = PNG.sync.write(png).toString("base64");
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
error: "DIFF_FAILED",
|
|
279
|
+
status: 500,
|
|
280
|
+
message: "Failed to encode diff heatmap.",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
ok: true,
|
|
285
|
+
mismatch_pct: mismatchPct,
|
|
286
|
+
dimension_match: dimensionMatch,
|
|
287
|
+
passed,
|
|
288
|
+
diff_regions: diffRegions,
|
|
289
|
+
comp_dimensions: { width: compImg.width, height: compImg.height },
|
|
290
|
+
render_dimensions: { width: renderImg.width, height: renderImg.height },
|
|
291
|
+
differing_pixels: differingPixels,
|
|
292
|
+
total_pixels: totalPixels,
|
|
293
|
+
heatmap_base64: heatmapBase64,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
// Worker-thread entrypoint: when spawned via new Worker(...), read the input
|
|
297
|
+
// from workerData, run the pure diff, and post the structured result back. Any
|
|
298
|
+
// unexpected failure is returned as a structured DIFF_FAILED envelope rather
|
|
299
|
+
// than crashing the worker.
|
|
300
|
+
if (!isMainThread && parentPort) {
|
|
301
|
+
try {
|
|
302
|
+
const result = computePngVisualDiff(workerData);
|
|
303
|
+
parentPort.postMessage(result);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
parentPort.postMessage({
|
|
307
|
+
ok: false,
|
|
308
|
+
error: "DIFF_FAILED",
|
|
309
|
+
status: 500,
|
|
310
|
+
message: `Diff worker failed: ${err instanceof Error ? err.message : "unknown error"}`,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|