@sealab/mcp-server 1.0.3 → 1.0.4

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.
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ // ---------------------------------------------------------------------------
3
+ // Cart-manifest cross-check for placement projection.
4
+ //
5
+ // Ground-truth fix for under-segmentation: a placement may only exist for a
6
+ // real AI cart item, and its real-world size must match that item's catalog
7
+ // dimensions. This anchors the drawing to the manifest — you cannot stretch one
8
+ // 18" cabinet's box to cover a 36" run, and you cannot invent boxes for
9
+ // cabinets that were never configured. Combined with one-placement-per-item, a
10
+ // run can only hold as many cabinets as there are real items of the right size.
11
+ //
12
+ // Pure (no IO): the handler fetches session/cart state and passes the items in.
13
+ // ---------------------------------------------------------------------------
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.buildCartLookup = buildCartLookup;
16
+ exports.crossCheckPlacementsAgainstCart = crossCheckPlacementsAgainstCart;
17
+ const DEFAULT_WIDTH_TOLERANCE = 1.0;
18
+ const DEFAULT_HEIGHT_TOLERANCE = 1.5;
19
+ const DEFAULT_DEPTH_TOLERANCE = 1.5;
20
+ function stringifyId(value) {
21
+ if (value === null || value === undefined) {
22
+ return undefined;
23
+ }
24
+ const text = String(value).trim();
25
+ return text.length > 0 ? text : undefined;
26
+ }
27
+ function toFiniteNumber(value) {
28
+ if (typeof value === 'number' && Number.isFinite(value)) {
29
+ return value;
30
+ }
31
+ if (typeof value === 'string' && value.trim() !== '') {
32
+ const parsed = Number(value);
33
+ return Number.isFinite(parsed) ? parsed : undefined;
34
+ }
35
+ return undefined;
36
+ }
37
+ function isRecord(value) {
38
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
39
+ }
40
+ /** Read a cabinet dimension from the item itself or any of its payloads. */
41
+ function extractCartItemDimension(item, key) {
42
+ const direct = toFiniteNumber(item[key]);
43
+ if (direct !== undefined) {
44
+ return direct;
45
+ }
46
+ for (const payload of [item.orderReadyPayload, item.itemPayload, item.originalItemPayload]) {
47
+ if (isRecord(payload)) {
48
+ const value = toFiniteNumber(payload[key]);
49
+ if (value !== undefined) {
50
+ return value;
51
+ }
52
+ }
53
+ }
54
+ return undefined;
55
+ }
56
+ function buildCartLookup(cartItems) {
57
+ const byId = new Map();
58
+ const byPositionName = new Map();
59
+ for (const item of cartItems) {
60
+ const itemId = stringifyId(item.id ?? item.aiCartItemId);
61
+ const positionName = stringifyId(item.positionName);
62
+ if (itemId) {
63
+ byId.set(itemId, item);
64
+ }
65
+ if (positionName) {
66
+ byPositionName.set(positionName, item);
67
+ }
68
+ }
69
+ return { byId, byPositionName };
70
+ }
71
+ function findMatchingCartItem(spec, lookup) {
72
+ return ((spec.aiCartItemId ? lookup.byId.get(spec.aiCartItemId) : undefined) ??
73
+ lookup.byPositionName.get(spec.positionName));
74
+ }
75
+ function cartItemKey(item) {
76
+ return stringifyId(item.id ?? item.aiCartItemId) ?? stringifyId(item.positionName) ?? '';
77
+ }
78
+ /**
79
+ * Cross-check projected placement specs against the real AI cart manifest.
80
+ * Returns blocking issues (refuse the write) and soft warnings.
81
+ */
82
+ function crossCheckPlacementsAgainstCart(specs, cartItems, options = {}) {
83
+ const widthTol = options.widthToleranceInches ?? DEFAULT_WIDTH_TOLERANCE;
84
+ const heightTol = options.heightToleranceInches ?? DEFAULT_HEIGHT_TOLERANCE;
85
+ const depthTol = options.depthToleranceInches ?? DEFAULT_DEPTH_TOLERANCE;
86
+ const lookup = buildCartLookup(cartItems);
87
+ const blocking = [];
88
+ const warnings = [];
89
+ const usedItemKeys = new Map(); // cart item key -> first positionName that used it
90
+ let matchedCount = 0;
91
+ for (const spec of specs) {
92
+ const item = findMatchingCartItem(spec, lookup);
93
+ if (!item) {
94
+ blocking.push({
95
+ positionName: spec.positionName,
96
+ code: 'no_matching_cart_item',
97
+ message: `Placement "${spec.positionName}" does not match any active AI cart item by aiCartItemId or positionName. ` +
98
+ 'Create the AI cart item first (one physical cabinet = one cart item) and reference its backend id/positionName. ' +
99
+ 'Do not place cabinets that are not in the manifest.',
100
+ });
101
+ continue;
102
+ }
103
+ matchedCount += 1;
104
+ const key = cartItemKey(item);
105
+ const priorPositionName = usedItemKeys.get(key);
106
+ if (priorPositionName !== undefined && priorPositionName !== spec.positionName) {
107
+ blocking.push({
108
+ positionName: spec.positionName,
109
+ code: 'duplicate_cart_item_reference',
110
+ message: `Placements "${priorPositionName}" and "${spec.positionName}" both resolve to the same AI cart item. ` +
111
+ 'One physical cabinet can have only one placement per canvas. If this is really two cabinets, create a ' +
112
+ 'second AI cart item; if it is one cabinet, use a single placement.',
113
+ });
114
+ }
115
+ else if (priorPositionName === undefined) {
116
+ usedItemKeys.set(key, spec.positionName);
117
+ }
118
+ const cartWidth = extractCartItemDimension(item, 'width');
119
+ if (cartWidth === undefined) {
120
+ warnings.push({
121
+ positionName: spec.positionName,
122
+ code: 'cart_item_dimensions_unknown',
123
+ message: `AI cart item for "${spec.positionName}" has no readable width; could not verify the placement width ` +
124
+ 'against the manifest. Confirm the cart item is configured with real catalog dimensions.',
125
+ });
126
+ }
127
+ else if (Math.abs(cartWidth - spec.widthInches) > widthTol) {
128
+ blocking.push({
129
+ positionName: spec.positionName,
130
+ code: 'width_mismatch',
131
+ message: `Placement "${spec.positionName}" width ${spec.widthInches}" does not match the AI cart item width ` +
132
+ `${cartWidth}" (tolerance ${widthTol}"). A box wider than its real cabinet usually means a run was lumped ` +
133
+ 'into one box; split it into one cabinet per item, or correct the cart item width.',
134
+ });
135
+ }
136
+ if (spec.heightInches !== undefined) {
137
+ const cartHeight = extractCartItemDimension(item, 'height');
138
+ if (cartHeight !== undefined && Math.abs(cartHeight - spec.heightInches) > heightTol) {
139
+ blocking.push({
140
+ positionName: spec.positionName,
141
+ code: 'height_mismatch',
142
+ message: `Placement "${spec.positionName}" height ${spec.heightInches}" does not match the AI cart item height ` +
143
+ `${cartHeight}" (tolerance ${heightTol}"). For base cabinets use the body height (excluding toekick and ` +
144
+ 'countertop) and make the cart item height match.',
145
+ });
146
+ }
147
+ }
148
+ if (spec.depthInches !== undefined) {
149
+ const cartDepth = extractCartItemDimension(item, 'depth');
150
+ if (cartDepth !== undefined && Math.abs(cartDepth - spec.depthInches) > depthTol) {
151
+ blocking.push({
152
+ positionName: spec.positionName,
153
+ code: 'depth_mismatch',
154
+ message: `Plan footprint "${spec.positionName}" depth ${spec.depthInches}" does not match the AI cart item depth ` +
155
+ `${cartDepth}" (tolerance ${depthTol}").`,
156
+ });
157
+ }
158
+ }
159
+ }
160
+ if (options.reportCoverage) {
161
+ const referencedKeys = new Set();
162
+ for (const spec of specs) {
163
+ const item = findMatchingCartItem(spec, lookup);
164
+ if (item) {
165
+ referencedKeys.add(cartItemKey(item));
166
+ }
167
+ }
168
+ for (const item of cartItems) {
169
+ const key = cartItemKey(item);
170
+ if (key && !referencedKeys.has(key)) {
171
+ const positionName = stringifyId(item.positionName) ?? key;
172
+ warnings.push({
173
+ positionName,
174
+ code: 'cart_item_without_placement',
175
+ message: `Active AI cart item "${positionName}" has no placement in this write. If it belongs on this canvas, ` +
176
+ 'add it; if it only appears on another view, this is expected.',
177
+ });
178
+ }
179
+ }
180
+ }
181
+ return { matchedCount, blocking, warnings };
182
+ }
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ // ---------------------------------------------------------------------------
3
+ // Elevation run segmentation.
4
+ //
5
+ // Second root cause of bad placement (after raw pixel-guessing): the agent
6
+ // does not reliably decide HOW MANY physical cabinets a dimensioned run holds,
7
+ // or WHAT front type each is. It draws one rectangle around a whole run — e.g.
8
+ // a 3'-0" "EQ A | EQ A" upper run that is actually two double-door cabinets gets
9
+ // treated as a single cabinet.
10
+ //
11
+ // This module makes segmentation explicit and checkable. The agent enumerates
12
+ // each module of a run with its real width and declared front type; this code
13
+ // tiles them contiguously (computing each module's along-wall start so there is
14
+ // exactly one box per cabinet) and emits warnings when the declared layout is
15
+ // implausible or contradicts the dimension chain the agent says it read. The
16
+ // act of filling in per-module front types + widths forces the single-vs-double
17
+ // and one-vs-many reasoning the model was skipping.
18
+ //
19
+ // Pure (no IO) so it can be unit-tested directly.
20
+ // ---------------------------------------------------------------------------
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.segmentElevationRun = segmentElevationRun;
23
+ const DEFAULT_MAX_SINGLE_CABINET_WIDTH = 48;
24
+ const DEFAULT_REVEAL = 0;
25
+ // Industry-typical door sizing used only to raise warnings, never hard errors.
26
+ const SINGLE_DOOR_MAX_WIDTH = 24;
27
+ const DOUBLE_DOOR_MIN_WIDTH = 12;
28
+ const RUN_TOTAL_TOLERANCE = 0.51;
29
+ /**
30
+ * Tile a run's modules left-to-right and collect segmentation warnings.
31
+ * This never throws; structural requirements (non-empty modules, positive
32
+ * widths, required frontType) are enforced by the Zod schema at the tool layer.
33
+ */
34
+ function segmentElevationRun(run) {
35
+ const reveal = run.revealBetweenModulesInches ?? DEFAULT_REVEAL;
36
+ const maxSingle = run.maxSingleCabinetWidthInches ?? DEFAULT_MAX_SINGLE_CABINET_WIDTH;
37
+ const modules = [];
38
+ let cursor = run.runStartInches;
39
+ for (let i = 0; i < run.modules.length; i += 1) {
40
+ const moduleSpec = run.modules[i];
41
+ const start = cursor;
42
+ const end = start + moduleSpec.widthInches;
43
+ modules.push({ ...moduleSpec, horizontalStartInches: start, horizontalEndInches: end });
44
+ cursor = end + reveal;
45
+ }
46
+ const modulesWidthInches = run.modules.reduce((sum, m) => sum + m.widthInches, 0);
47
+ const occupiedSpanInches = modules.length > 0
48
+ ? modules[modules.length - 1].horizontalEndInches - modules[0].horizontalStartInches
49
+ : 0;
50
+ const warnings = collectSegmentationWarnings(run, modules, modulesWidthInches, maxSingle);
51
+ return { modules, modulesWidthInches, occupiedSpanInches, warnings };
52
+ }
53
+ function collectSegmentationWarnings(run, modules, modulesWidthInches, maxSingle) {
54
+ const warnings = [];
55
+ // Under-segmentation: a single very wide module is the classic "one box for
56
+ // multiple cabinets" failure.
57
+ for (const m of modules) {
58
+ if (m.widthInches > maxSingle) {
59
+ warnings.push(`Module "${m.positionName}" is ${m.widthInches}" wide, above the ${maxSingle}" single-cabinet threshold. ` +
60
+ 'Verify this is one physical cabinet and not several lumped into one box; if the dimension chain subdivides ' +
61
+ 'this span, split it into separate cabinets with their own cart items and placements.');
62
+ }
63
+ }
64
+ // Front-type vs width plausibility.
65
+ for (const m of modules) {
66
+ if (m.frontType === 'single_door' && m.widthInches > SINGLE_DOOR_MAX_WIDTH) {
67
+ warnings.push(`Module "${m.positionName}" is declared single_door but is ${m.widthInches}" wide (> ${SINGLE_DOOR_MAX_WIDTH}"). ` +
68
+ 'A single door this wide is unusual; confirm it is not a double_door cabinet or two separate cabinets.');
69
+ }
70
+ if ((m.frontType === 'double_door' || m.frontType === 'pair_of_doors') && m.widthInches < DOUBLE_DOOR_MIN_WIDTH) {
71
+ warnings.push(`Module "${m.positionName}" is declared ${m.frontType} but is only ${m.widthInches}" wide (< ${DOUBLE_DOOR_MIN_WIDTH}"). ` +
72
+ 'Two doors in a span this narrow is unusual; confirm the front type.');
73
+ }
74
+ }
75
+ // Declared subdivisions vs module count: the secondary dimension chain
76
+ // ("EQ A | EQ A") is explicit segmentation evidence in the drawing.
77
+ if (run.declaredSubdivisionCount !== undefined && run.declaredSubdivisionCount > modules.length) {
78
+ warnings.push(`You read ${run.declaredSubdivisionCount} equal subdivisions in the dimension chain but provided only ` +
79
+ `${modules.length} module(s). Confirm whether each subdivision is a separate cabinet (split it) or just ` +
80
+ 'multiple doors within one cabinet (keep one module and set the front type accordingly).');
81
+ }
82
+ // Tiled width vs the run's primary dimension string.
83
+ if (run.expectedRunTotalInches !== undefined) {
84
+ const reveal = run.revealBetweenModulesInches ?? DEFAULT_REVEAL;
85
+ const totalWithReveals = modulesWidthInches + reveal * Math.max(0, modules.length - 1);
86
+ const delta = run.expectedRunTotalInches - totalWithReveals;
87
+ if (Math.abs(delta) > RUN_TOTAL_TOLERANCE) {
88
+ warnings.push(`Module widths${reveal ? ' plus reveals' : ''} total ${round2(totalWithReveals)}" but the run dimension is ` +
89
+ `${run.expectedRunTotalInches}" (off by ${round2(delta)}"). Re-check module count, widths, or reveal: ` +
90
+ (delta > 0
91
+ ? 'the run has unaccounted width — a cabinet or filler may be missing.'
92
+ : 'the modules overflow the run — a width is too large or there are too many cabinets.'));
93
+ }
94
+ }
95
+ return warnings;
96
+ }
97
+ function round2(value) {
98
+ return Math.round(value * 100) / 100;
99
+ }
@@ -412,6 +412,8 @@ const DeleteSavedCartArticlesSchema = zod_1.z.object({
412
412
  });
413
413
  const CreateOrderSchema = zod_1.z.object({
414
414
  articles: flexArray(exports.ArticleItemSchema).describe('Line items to order.'),
415
+ projectId: zod_1.z.number().int().positive().optional().describe('Optional existing project ID to place this order into. Use list_projects or create_project first. ' +
416
+ 'When provided, the backend copies that project name/address and pricing snapshot to the order.'),
415
417
  projectName: zod_1.z.string().describe('Project name for this order'),
416
418
  purchaseOrder: zod_1.z.string().describe('Purchase order number'),
417
419
  projectAddress: ProjectAddressSchema.optional().describe('Project address (where cabinets will be installed)'),
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ // ---------------------------------------------------------------------------
3
+ // Deterministic AIDrawingTool placement projection.
4
+ //
5
+ // Root cause of inaccurate cabinet placement: the agent was asked to eyeball
6
+ // raw centerX/centerY/width/height pixel rectangles directly from a drawing.
7
+ // Vision-language models are unreliable at precise pixel localization, so boxes
8
+ // drifted, overlapped, doubled, and overshot — especially across rows of
9
+ // near-identical modules where per-box error accumulates.
10
+ //
11
+ // This module removes that guesswork. The agent establishes ONE pixels-per-inch
12
+ // calibration per canvas (from a visible dimension string + the floor/Z0 line +
13
+ // one origin reference) and supplies each cabinet's REAL inch dimensions and
14
+ // position. Every placement rectangle is then computed from the same scale, so
15
+ // the entire layout is internally consistent and free of cumulative drift.
16
+ //
17
+ // These functions are pure (no IO) so they can be unit-tested directly.
18
+ // ---------------------------------------------------------------------------
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.projectElevationPlacement = projectElevationPlacement;
21
+ exports.projectPlanPlacement = projectPlanPlacement;
22
+ exports.projectPlanWallAnchoredPlacement = projectPlanWallAnchoredPlacement;
23
+ exports.isProjectedPlacementOutsideImage = isProjectedPlacementOutsideImage;
24
+ const PIXEL_PRECISION = 2;
25
+ function roundPx(value) {
26
+ const factor = 10 ** PIXEL_PRECISION;
27
+ return Math.round(value * factor) / factor;
28
+ }
29
+ function horizontalSign(direction) {
30
+ return direction === 'right' ? 1 : -1;
31
+ }
32
+ function elevationVerticalSign(direction) {
33
+ // Height-above-floor grows upward; screen Y grows downward.
34
+ // 'up' positive => increasing height moves toward smaller pixel Y.
35
+ return direction === 'up' ? -1 : 1;
36
+ }
37
+ function planVerticalSign(direction) {
38
+ return direction === 'down' ? 1 : -1;
39
+ }
40
+ function elevationPixelX(horizontalInches, cal) {
41
+ return (cal.horizontalOriginPx.x +
42
+ horizontalSign(cal.horizontalAxisPositiveDirection) *
43
+ (horizontalInches - cal.horizontalOriginCoordinateInches) *
44
+ cal.horizontalPixelsPerInch);
45
+ }
46
+ function elevationPixelY(heightAboveFloorInches, cal) {
47
+ return (cal.floorZ0Px +
48
+ elevationVerticalSign(cal.verticalAxisPositiveDirection) *
49
+ heightAboveFloorInches *
50
+ cal.verticalPixelsPerInch);
51
+ }
52
+ /** Project one elevation cabinet face spec into an image-pixel rectangle. */
53
+ function projectElevationPlacement(spec, cal) {
54
+ const bottomFromFloor = spec.bottomFromFloorInches ?? 0;
55
+ const leftPx = elevationPixelX(spec.horizontalStartInches, cal);
56
+ const rightPx = elevationPixelX(spec.horizontalStartInches + spec.widthInches, cal);
57
+ const bottomPx = elevationPixelY(bottomFromFloor, cal);
58
+ const topPx = elevationPixelY(bottomFromFloor + spec.heightInches, cal);
59
+ return {
60
+ centerX: roundPx((leftPx + rightPx) / 2),
61
+ centerY: roundPx((topPx + bottomPx) / 2),
62
+ width: roundPx(Math.abs(rightPx - leftPx)),
63
+ height: roundPx(Math.abs(bottomPx - topPx)),
64
+ rotation: 0,
65
+ };
66
+ }
67
+ function planPixelX(xInches, cal) {
68
+ return cal.roomOriginPx.x + horizontalSign(cal.xPositiveDirection) * xInches * cal.pixelsPerInch;
69
+ }
70
+ function planPixelY(yInches, cal) {
71
+ return cal.roomOriginPx.y + planVerticalSign(cal.yPositiveDirection) * yInches * cal.pixelsPerInch;
72
+ }
73
+ /** Project one plan cabinet footprint spec into an image-pixel rectangle. */
74
+ function projectPlanPlacement(spec, cal) {
75
+ const rotation = ((spec.rotation ?? 0) % 360 + 360) % 360;
76
+ const rotatedQuarterTurn = rotation === 90 || rotation === 270;
77
+ const inchesAlongX = rotatedQuarterTurn ? spec.depthInches : spec.widthInches;
78
+ const inchesAlongY = rotatedQuarterTurn ? spec.widthInches : spec.depthInches;
79
+ return {
80
+ centerX: roundPx(planPixelX(spec.centerXInches, cal)),
81
+ centerY: roundPx(planPixelY(spec.centerYInches, cal)),
82
+ width: roundPx(inchesAlongX * cal.pixelsPerInch),
83
+ height: roundPx(inchesAlongY * cal.pixelsPerInch),
84
+ // The rectangle is axis-aligned to the room/image; orientation is captured
85
+ // by the dimension swap above, so the rendered box rotation stays 0.
86
+ rotation: 0,
87
+ };
88
+ }
89
+ /**
90
+ * Project a wall-anchored plan footprint. The back edge lands exactly on
91
+ * wallBackEdgeInches; the box extends widthInches along the wall and depthInches
92
+ * into the room. The rendered rectangle is axis-aligned (rotation 0).
93
+ */
94
+ function projectPlanWallAnchoredPlacement(spec, cal) {
95
+ const sign = spec.depthDirection === 'positive' ? 1 : -1;
96
+ const alongCenter = spec.alongWallStartInches + spec.widthInches / 2;
97
+ const depthCenter = spec.wallBackEdgeInches + sign * (spec.depthInches / 2);
98
+ const centerXInches = spec.depthAxis === 'x' ? depthCenter : alongCenter;
99
+ const centerYInches = spec.depthAxis === 'y' ? depthCenter : alongCenter;
100
+ const inchesAlongX = spec.depthAxis === 'x' ? spec.depthInches : spec.widthInches;
101
+ const inchesAlongY = spec.depthAxis === 'y' ? spec.depthInches : spec.widthInches;
102
+ return {
103
+ centerX: roundPx(planPixelX(centerXInches, cal)),
104
+ centerY: roundPx(planPixelY(centerYInches, cal)),
105
+ width: roundPx(inchesAlongX * cal.pixelsPerInch),
106
+ height: roundPx(inchesAlongY * cal.pixelsPerInch),
107
+ rotation: 0,
108
+ };
109
+ }
110
+ /** True when any part of the rectangle falls outside the calibration image. */
111
+ function isProjectedPlacementOutsideImage(placement, imageSizePx) {
112
+ const halfWidth = placement.width / 2;
113
+ const halfHeight = placement.height / 2;
114
+ const minX = placement.centerX - halfWidth;
115
+ const maxX = placement.centerX + halfWidth;
116
+ const minY = placement.centerY - halfHeight;
117
+ const maxY = placement.centerY + halfHeight;
118
+ return minX < 0 || minY < 0 || maxX > imageSizePx.width || maxY > imageSizePx.height;
119
+ }