@sealab/mcp-server 1.0.2 → 1.0.3

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,2080 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.aiDrawingTools = exports.AiCabinetCoordinatesDeleteInputSchema = exports.AiCabinetCoordinatesPatchInputListSchema = exports.AiCabinetCoordinatesReplaceInputSchema = exports.AiCabinetCoordinatePatchInputSchema = exports.AiCabinetCoordinateInputSchema = exports.AiCanvasCabinetPlacementsDeleteInputSchema = exports.AiCanvasCabinetPlacementsPatchInputSchema = exports.AiCanvasCabinetPlacementsByCanvasInputSchema = exports.AiCanvasCabinetPlacementsUpsertInputSchema = exports.AiCanvasCabinetPlacementsReplaceInputSchema = exports.AiCanvasCabinetPlacementPatchSchema = exports.AiCanvasCabinetPlacementInputSchema = exports.AiCanvasImageDeleteInputSchema = exports.AiCanvasImageUploadInputSchema = exports.AiCanvasImageInputSchema = exports.AiCanvasMetadataInputSchema = exports.LinkAiCartToDrawingSessionInputSchema = exports.AiDrawingSessionReviewRenderInputSchema = exports.AiReduxCartImportInputSchema = exports.AiOrderReadyPayloadClearInputSchema = exports.AiOrderReadyPayloadSetInputSchema = exports.AiOrderReadyPayloadReadInputSchema = exports.DeleteAiCabinetInputSchema = exports.ConfigureAiCabinetInputSchema = exports.DuplicateAiCabinetInputSchema = exports.AiCartCreateInputSchema = exports.AiCartSnapshotInputSchema = exports.AiCartItemPatchSchema = exports.AiCartItemInputSchema = exports.AiCartItemIdParamSchema = exports.AiCartIdParamSchema = exports.AiDrawingSessionCreateInputSchema = exports.AiCanvasMetaSchema = exports.AiCanvasProjectionCalibrationSchema = void 0;
40
+ exports.getAiDrawingState = getAiDrawingState;
41
+ exports.renderAiDrawingSessionReviewTool = renderAiDrawingSessionReviewTool;
42
+ exports.createAiDrawingSession = createAiDrawingSession;
43
+ exports.linkAiCartToDrawingSession = linkAiCartToDrawingSession;
44
+ exports.upsertAiCanvasMetadata = upsertAiCanvasMetadata;
45
+ exports.upsertAiCanvasImage = upsertAiCanvasImage;
46
+ exports.uploadAiCanvasImage = uploadAiCanvasImage;
47
+ exports.deleteAiCanvasImage = deleteAiCanvasImage;
48
+ exports.getAiCanvasCabinetPlacements = getAiCanvasCabinetPlacements;
49
+ exports.getAiCanvasCabinetPlacementsByCanvas = getAiCanvasCabinetPlacementsByCanvas;
50
+ exports.replaceAiCanvasCabinetPlacements = replaceAiCanvasCabinetPlacements;
51
+ exports.upsertAiCanvasCabinetPlacements = upsertAiCanvasCabinetPlacements;
52
+ exports.patchAiCanvasCabinetPlacements = patchAiCanvasCabinetPlacements;
53
+ exports.deleteAiCanvasCabinetPlacements = deleteAiCanvasCabinetPlacements;
54
+ exports.replaceAiCabinetCoordinates = replaceAiCabinetCoordinates;
55
+ exports.patchAiCabinetCoordinates = patchAiCabinetCoordinates;
56
+ exports.deleteAiCabinetCoordinates = deleteAiCabinetCoordinates;
57
+ exports.createAiCart = createAiCart;
58
+ exports.getAiCart = getAiCart;
59
+ exports.getAiCartBySession = getAiCartBySession;
60
+ exports.syncAiCartSnapshot = syncAiCartSnapshot;
61
+ exports.patchAiCartItem = patchAiCartItem;
62
+ exports.deleteAiCartItem = deleteAiCartItem;
63
+ exports.duplicateAiCabinet = duplicateAiCabinet;
64
+ exports.configureAiCabinet = configureAiCabinet;
65
+ exports.deleteAiCabinet = deleteAiCabinet;
66
+ exports.getAiCartItemOrderReadyPayload = getAiCartItemOrderReadyPayload;
67
+ exports.setAiCartItemOrderReadyPayload = setAiCartItemOrderReadyPayload;
68
+ exports.clearAiCartItemOrderReadyPayload = clearAiCartItemOrderReadyPayload;
69
+ exports.importReduxCartItemsToAiCart = importReduxCartItemsToAiCart;
70
+ const zod_1 = require("zod");
71
+ const api_client_1 = require("../client/api-client");
72
+ const orders_1 = require("./orders");
73
+ const fs = __importStar(require("fs"));
74
+ const path = __importStar(require("path"));
75
+ const form_data_1 = __importDefault(require("form-data"));
76
+ const ai_drawing_session_review_renderer_1 = require("./ai-drawing-session-review-renderer");
77
+ const LEGACY_DRAWING_TOOL_NAME = 'the legacy frontend drawing route';
78
+ const STORED_CARTS_TERM = 'stored carts';
79
+ const AI_CART_TOOL_BOUNDARY = 'AIDrawingTool is backend-owned and isolated. These tools do not mutate the Redux cart, ' +
80
+ STORED_CARTS_TERM +
81
+ ', or ' +
82
+ LEGACY_DRAWING_TOOL_NAME +
83
+ '. The AI cart is one physical cabinet manifest for the whole AI drawing session, not one manifest per canvas. positionName is the physical cabinet identity and must remain stable across plan and elevation placements. backend aiCartItemId is canonical after creation.';
84
+ const AI_DRAWING_VISUAL_REVIEW_COMPLETION_GUIDANCE = 'Before reporting completion after canvas, AI cart, or placement writes, call render_ai_drawing_session_review, inspect every returned full-canvas PNG with native vision, apply the visual QA checklist and every returned sourceBoundaryReviewTasks[] item against the actual source drawing pixels, correct wrong placements with existing AI-only placement tools, rerender, and document PASS/FAIL/UNCERTAIN per canvas and placement. A placement may be PASS only when the agent writes explicit LEFT/RIGHT/TOP/BOTTOM boundary evidence from visible source linework; JSON validity is not visual correctness, and bbox non-overlap or bbox adjacency is not visual correctness. If the user asked to create an AI drawing session/cart from drawings, do not stop at a setup-only session with uploaded backgrounds: infer visible cabinet/catalog items from the drawings, create the AI cart items, place every visible cabinet/object on the relevant canvases, and only leave items unplaced when the user explicitly asked for setup-only or a view is genuinely ambiguous and reported UNCERTAIN.';
85
+ const AI_DRAWING_VISUAL_QA_CHECKLIST = 'Visual QA checklist for every rendered review PNG: compare each rectangle against the visible drawing, not just labels; source drawing pixels are the authority and generated bbox relationships are never proof by themselves; every PASS must cite visible LEFT, RIGHT, TOP, and BOTTOM source linework; each cabinet box must sit inside the actual cabinet face or plan footprint boundaries; reject boxes that overlap other cabinets, appliances, fixtures, counters, toe kicks, walls, dimension strings, or blank/context areas; reject shifted boxes that extend beyond wood/front boundaries or include refrigerator/range/dishwasher/sink appliance surfaces unless the AI cart item is explicitly an appliance panel/opening; verify the same physical cabinet uses the same positionName/aiCartItemId across plan and elevation while plan footprints and elevation front faces stay on their own canvases; verify no obvious visible cabinet is missing; verify no side/return/context-only face is treated as a primary cabinet; verify labels do not hide boundary errors; when any item is ambiguous, mark it unresolved or ask the user instead of declaring the review correct.';
86
+ const AI_ARCHITECTURAL_DRAWING_INTERPRETATION_GUIDANCE = 'Architectural drawing interpretation contract: use native vision and reasoning on the actual drawing images before writing MCP state. Identify each physical cabinet/object bbox from visible boundaries, scale callouts, known dimensions, and repeated modules. Differentiate base cabinets, uppers/wall cabinets, tall/pantry units, fillers, appliance panels/openings, drawer stacks, single-door fronts, and double-door fronts because door/front segmentation changes bbox boundaries and catalog matching. Toekicks are separate plinth objects in the Sealab catalog: create separate AI cart items and separate orange plinth/toekick bboxes for them when present. On any front elevation with base or sink-base cabinets, explicitly inspect the lower support/toekick band: if a visible plinth/toekick band exists, it must be represented as a separate plinth AI cart item and orange bbox before the review can pass. Do not include toekicks or countertops in front-facing cabinet bboxes. Do not include toekicks or countertops in base cabinet height; use called-out scale plus known cabinet/body dimensions to infer the cabinet body height exclusive of plinth/toekick and countertop. A drawing-derived base or sink-base item cannot be marked READY unless drawingClassificationEvidence.baseCabinetBodyHeightEvidence documents the measured cabinet body height, the top/bottom boundary evidence, and explicit exclusion of both countertop and toekick_plinth; the orderReadyPayload height must match that body-height evidence. Cabinet front-face bboxes must not extend below the Z0/floor reference line or into floors, walls, ceilings, counters, labels, dimension strings, or blank/context areas. If a cabinet bbox captures floor, wall, ceiling, countertop, or a toekick/plinth, the visual review must fail and the placement must be patched. Review overlays use blue for cabinet front-facing bboxes and orange for plinth/toekick bboxes.';
87
+ const AI_CART_ORDER_READY_CREATION_GUIDANCE = 'When the agent creates or syncs AI cart items that did not come from a rich frontend cart snapshot, it must run the same configuration discovery flow used for normal saved-cart/order creation before creating the item manifest: for each serialNumber, call get_article_context, then get_article_options, check get_my_saved_settings/get_saved_settings_presets when saved settings could answer user preferences, present the applicable options, and ask the user for every missing order-critical choice. If the drawing has no explicit material/configuration callouts, stop and ask the user to choose exact options or a saved setting/preset before syncing cabinet items. Only after that flow should the agent call set_ai_cart_item_order_ready_payload with orderReadyStatus READY and a full ArticleItemSchema-compatible payload. If required choices are still missing, do not create an unconfigured cabinet manifest; keep only non-item drawing/canvas setup until the user answers. Creating positionName, serialNumber, width, height, and depth is not enough for a cart-ready cabinet. READY order-ready payloads automatically mirror into the browser Redux checkout cart while the matching AI drawing session is loaded; there is no normal Seed AI Cart or Apply to cart button step. ' +
88
+ AI_ARCHITECTURAL_DRAWING_INTERPRETATION_GUIDANCE +
89
+ ' ' +
90
+ AI_DRAWING_VISUAL_REVIEW_COMPLETION_GUIDANCE;
91
+ const AI_AUTOMATIC_CART_MIRROR_GUIDANCE = 'Phase 14 automatic mirror lifecycle: backend AI cart remains the persistent planner state and browser Redux cart remains checkout state. READY AI cart items with validated ArticleItemSchema-compatible orderReadyPayload values automatically upsert into the browser Redux checkout cart while that session is loaded. Non-ready statuses such as NEEDS_AGENT_CONFIGURATION, BLOCKED, STALE, SKIPPED, or NOT_PREPARED must be explicit and user-actionable with messages. Do not tell users to press Seed AI Cart or Apply to cart as the normal workflow. Use catalog/configuration/order tools and exact user-confirmed options; never guess order-critical material, edge, drawer, hinge, shelf, back-panel, joint, or saved setting values. For AI cart items, the normal path is the same as saved-cart/order preparation: call get_article_context and get_article_options for each article, consult get_my_saved_settings/get_saved_settings_presets when preferences may already exist, ask the user for missing required options, then write READY only with a full validated payload. These MCP tools do not submit orders, generate CAD/XML, mutate stored carts, or write production coordinate tables.';
92
+ const AI_DRAWING_TOOL_BOUNDARY = 'AIDrawingTool is backend-owned and isolated. These tools do not mutate the Redux cart, ' +
93
+ STORED_CARTS_TERM +
94
+ ', or ' +
95
+ LEGACY_DRAWING_TOOL_NAME +
96
+ '. Coordinates use canonical room/global inches: x is cabinet center X in room/global plan coordinates, y is cabinet center Y in room/global plan coordinates, z is finished-floor-to-cabinet-bottom height, and rotation is wall orientation 0/90/180/270 degrees. Agents must never write image-local pixels or image-local elevation coordinates into canonical x/y/z.';
97
+ const AI_CANONICAL_PROJECTION_GUIDANCE = 'Plan projects canonical x/y with cabinet width/depth. North/south elevations project canonical x/z with cabinet width/height. East/west elevations project canonical y/z with cabinet width/height. Rotation 0 belongs to north, 90 to east, 180 to south, and 270 to west.';
98
+ const AI_CANVAS_CALIBRATION_GUIDANCE = 'canvasMeta is flexible, but canvasMeta.projectionCalibration is not free-form notes. Only include projectionCalibration when providing the complete canonical coordinate projection schemaVersion ai-canvas-calibration/v1. For direct IMAGE_PIXELS per-canvas placements, omit projectionCalibration unless canonical room-inch projection is actually known. Put sourceFile, imageSizePx, plan/elevation marker association, orientation evidence, z0 notes, floor-line notes, and other visual-analysis notes in other canvasMeta keys outside projectionCalibration.';
99
+ const AI_REVIEW_TOKEN_GUIDANCE = 'Native vision on the actual source plan/elevation images is the source of truth for drawing interpretation. Run native image analysis first to identify real cabinet faces/footprints, appliances, fixtures, returns, exclusions, and plan/elevation binding; then use MCP tools only to persist the reviewed session, cart, placements, and review artifacts. MCP extraction, reviewed overlay, association objects, catalog labels, and evidence JSON are optional trace/diagnostic metadata only. Do not let raw extracted candidates, tool labels, evidence JSON, schema validity, or MCP-derived guesses override what is visibly in the drawing.';
100
+ const AI_DRAWING_SCALE_ANCHOR_GUIDANCE = 'Use visible dimensions as scale anchors before persisting placements: read explicit dimension strings, known catalog widths/heights, appliance/opening dimensions, and repeated cabinet modules; compute pixels-per-inch per canvas when possible; use that scale to sanity-check other cabinet sizes and gaps. Do not resize boxes away from visible boundaries just to force a dimension, but if a box dimension is inconsistent with nearby explicit dimensions or known cabinet/article size, reinspect the source image and correct or ask the user before persisting.';
101
+ const AI_CANVAS_PLACEMENT_GUIDANCE = 'These are backend-owned AIDrawingTool per-canvas cabinet placements using canvas-local visual rectangles in original image pixels. Placement writes require the session to have an active linked AI cart and each placement must link to an active AI cart item by aiCartItemId or exact positionName. After create_ai_cart/link_ai_cart_to_drawing_session/sync_ai_cart_snapshot, read back get_ai_drawing_state or get_ai_cart_by_session and use the returned backend aiCartItemId values before writing placements. If cart sync failed or activeCart/items are missing, stop and fix the cart flow; do not call placement tools. width/height in these placement tools are visual rectangle pixel dimensions, not cabinet inch dimensions. Plan rectangles are footprints; elevation rectangles are primary front faces. Reuse the same aiCartItemId/positionName for the same physical cabinet across canvases. A plan placement does not make the cabinet appear on an elevation: every visible cabinet face on north/south/east/west must also get its own placement on that elevation canvas using the same aiCartItemId/positionName, without creating another AI cart item. Do not derive plan placements from elevation boxes or elevation placements from plan boxes. Do not rely on filenames, compass words, object order, or catalog labels alone; use native vision and user confirmation when ambiguous. ' +
102
+ AI_ARCHITECTURAL_DRAWING_INTERPRETATION_GUIDANCE +
103
+ ' ' +
104
+ AI_DRAWING_SCALE_ANCHOR_GUIDANCE +
105
+ ' After writes, render and inspect the stored session, correct bad placements, and rerender. These tools do not mutate Redux cart, stored carts, production coordinates, CAD/XML, or the legacy frontend drawing route. ' +
106
+ AI_DRAWING_VISUAL_QA_CHECKLIST +
107
+ ' ' +
108
+ AI_DRAWING_VISUAL_REVIEW_COMPLETION_GUIDANCE +
109
+ ' ' +
110
+ AI_REVIEW_TOKEN_GUIDANCE;
111
+ const AI_CANVAS_PLACEMENT_ROTATION_GUIDANCE = 'Nonzero placement rotation is allowed only on plan. Elevation canvases are front-facing and must use rotation 0.';
112
+ const AI_CABINET_OPERATION_GUIDANCE = 'These are AI-native backend operations for one physical cabinet item in the AIDrawingTool AI cart. The AI cart is one physical cabinet manifest shared across plan and elevation canvases. Plan and elevation placements for the same cabinet must use the same shared aiCartItemId and positionName. Do not create separate physical AI cart items for plan versus elevation views; use per-canvas placement tools for placement geometry when representing the same cabinet on another canvas. duplicate_ai_cabinet creates a new physical cabinet. configure_ai_cabinet mutates backend AI cart item fields and linked AI-only placement dimensions. delete_ai_cabinet removes the backend AI cabinet and linked AI-only placements. ' +
113
+ AI_ARCHITECTURAL_DRAWING_INTERPRETATION_GUIDANCE +
114
+ ' ' +
115
+ AI_DRAWING_VISUAL_REVIEW_COMPLETION_GUIDANCE +
116
+ ' These tools do not mutate the Redux cart, stored carts, production coordinate tables, CAD/XML, or any external production target.';
117
+ const AI_ORDER_READY_GUIDANCE = 'These tools store AI-only order-ready article payloads on backend-owned AIDrawingTool AI cart items for browser automatic mirroring. serialNumber, positionName, width, height, and depth alone are not order-ready. Before READY, use catalog/configuration/saved setting tools, ask the user for every required missing order-critical choice, use exact option strings, and pass ArticleItemSchema/order-contract validation. Drawing evidence JSON is optional trace metadata and is not visual certification; correctness should be verified with rendered visual review. ' +
118
+ AI_ARCHITECTURAL_DRAWING_INTERPRETATION_GUIDANCE +
119
+ ' ' +
120
+ AI_REVIEW_TOKEN_GUIDANCE +
121
+ ' ' +
122
+ AI_DRAWING_VISUAL_REVIEW_COMPLETION_GUIDANCE +
123
+ ' ' +
124
+ AI_AUTOMATIC_CART_MIRROR_GUIDANCE;
125
+ const AI_REDUX_IMPORT_GUIDANCE = 'Import one or more existing rich browser Redux checkout cart items into the active backend-owned AIDrawingTool AI cart as physical AI cart items. This MCP tool is only for real existing browser checkout cart items that are already ArticleItemSchema/order-ready; do not use it to seed AI cart items from drawing takeoff, overlay output, catalog serials, dimensions, or other agent-created planner guesses. Provide the full original Redux cart item payload, not a thin serial/dimensions-only object. Use frontendItemId/originalFrontendItemId or stable source identity fields for idempotency; the backend must not rely on positionName alone. MCP imports must be READY and order-ready. If an item is not order-ready, do not import it through this tool; ask the user for configuration first, then use sync_ai_cart_snapshot with USER_CONFIRMED_ORDER_READY or set_ai_cart_item_order_ready_payload after backend ids exist. Imported READY items can then participate in the automatic mirror lifecycle. ' +
126
+ AI_DRAWING_VISUAL_REVIEW_COMPLETION_GUIDANCE +
127
+ ' ' +
128
+ AI_AUTOMATIC_CART_MIRROR_GUIDANCE;
129
+ const AI_DRAWING_VISUAL_REVIEW_GUIDANCE = 'Render local review PNG artifacts from the actual stored backend AIDrawingTool session state after writing or updating AI cart items and per-canvas placements. The output includes full-canvas review PNGs only; no per-placement crop artifacts are generated. Agents must open every returned full-canvas PNG path with native vision, apply the visual QA checklist to each canvas, and complete every returned sourceBoundaryReviewTasks[] item against the actual source drawing pixels. For each placement, PASS requires a written boundary-evidence sentence naming visible source linework for LEFT, RIGHT, TOP, and BOTTOM and listing excluded non-object context; otherwise mark FAIL or UNCERTAIN. Do not use bbox non-overlap, bbox adjacency, labels, dimensions, or evidence prose as proof that a base cabinet, plinth/toekick, floor/Z0, countertop, plan footprint, or cabinet face boundary is visually correct. Correct bad placements through the existing AI-only placement tools, render again/rerender, and only then report completion. JSON validity and structurally valid placement payloads are not visual correctness. Completion is blocked until the agent records PASS/FAIL/UNCERTAIN for every rendered canvas and placement with required boundary evidence, patches failures, rerenders, and explicitly documents any remaining unresolved issues. ' +
130
+ AI_ARCHITECTURAL_DRAWING_INTERPRETATION_GUIDANCE +
131
+ ' ' +
132
+ AI_DRAWING_VISUAL_QA_CHECKLIST +
133
+ ' ' +
134
+ AI_DRAWING_SCALE_ANCHOR_GUIDANCE +
135
+ ' ' +
136
+ AI_REVIEW_TOKEN_GUIDANCE +
137
+ ' This tool reads session state and writes only local review artifacts; it does not create sessions, upload images, mutate AI cart items, write order-ready payloads, import browser cart items, submit orders, or call legacy canvas tools.';
138
+ const AI_DRAWING_REVIEW_ASSET_BASE_URL = (process.env.SEALAB_ASSET_BASE_URL || process.env.REACT_APP_S3_BASE_URL || 'https://www.thesealab.com').replace(/\/+$/, '');
139
+ // ---------------------------------------------------------------------------
140
+ // Schemas
141
+ // ---------------------------------------------------------------------------
142
+ const GetAiDrawingStateSchema = zod_1.z.object({
143
+ sessionId: zod_1.z.string().uuid().describe('UUID of the backend-owned AIDrawingTool session. This reads the isolated AI drawing state, not the existing frontend drawing tool state.'),
144
+ });
145
+ const AiDrawingSessionIdParamSchema = zod_1.z.object({
146
+ sessionId: zod_1.z.string().uuid().describe('UUID of the backend-owned AIDrawingTool session.'),
147
+ }).strict();
148
+ const CanvasTypeSchema = zod_1.z.enum(['plan', 'north', 'south', 'east', 'west']).describe('AI drawing canvas type. These values are independent from the existing frontend drawing route.');
149
+ const ImageTypeSchema = zod_1.z.enum(['background', 'snapshot']).describe('AI drawing image type accepted by the backend AI drawing service.');
150
+ const IMAGE_MIME_TYPES = {
151
+ '.png': 'image/png',
152
+ '.jpg': 'image/jpeg',
153
+ '.jpeg': 'image/jpeg',
154
+ '.webp': 'image/webp',
155
+ };
156
+ const CoordinateRotationSchema = zod_1.z.union([
157
+ zod_1.z.literal(0),
158
+ zod_1.z.literal(90),
159
+ zod_1.z.literal(180),
160
+ zod_1.z.literal(270),
161
+ ]).describe('AI coordinate rotation in degrees: 0 north, 90 east, 180 south, 270 west.');
162
+ const CoordinateSourceSchema = zod_1.z.enum(['AI', 'MCP', 'BROWSER']).describe('Source metadata for an isolated AI drawing coordinate update.');
163
+ const PlacementSourceSchema = zod_1.z.enum(['AI', 'MCP', 'BROWSER']).describe('Source metadata for an isolated AI canvas placement update.');
164
+ const PlacementSpaceSchema = zod_1.z.literal('IMAGE_PIXELS').describe('Placement coordinate space. IMAGE_PIXELS means original canvas image pixels or the stored blank canvas basis.');
165
+ const PlanMarkerSideSchema = zod_1.z.enum(['left', 'right', 'up', 'down']);
166
+ const DrawingViewPlaneRoleSchema = zod_1.z.enum([
167
+ 'primary_elevation_face',
168
+ 'return_side_face',
169
+ 'context_anchor',
170
+ 'plan_footprint',
171
+ 'needs_user_confirmation',
172
+ ]);
173
+ const DrawingFrontFaceFeatureSchema = zod_1.z.enum([
174
+ 'door_drawer_fronts',
175
+ 'single_door_front',
176
+ 'double_door_front',
177
+ 'drawer_stack_front',
178
+ 'front_handles_or_pulls',
179
+ 'appliance_front_or_handles',
180
+ 'open_shelf_front',
181
+ 'sink_basin_or_faucet_on_primary_face',
182
+ 'front_panel_boundaries',
183
+ 'toekick_plinth',
184
+ 'countertop_context',
185
+ 'z0_floor_line',
186
+ 'side_panel_or_return_visible',
187
+ 'context_only',
188
+ 'user_confirmed_front_face',
189
+ ]);
190
+ const DrawingReviewedObjectTypeSchema = zod_1.z.enum([
191
+ 'base_cabinet',
192
+ 'wall_cabinet',
193
+ 'tall_cabinet',
194
+ 'drawer_stack',
195
+ 'sink_base',
196
+ 'appliance_panel',
197
+ 'appliance_opening',
198
+ 'filler',
199
+ 'plinth',
200
+ 'countertop',
201
+ 'wall',
202
+ 'floor',
203
+ 'ceiling',
204
+ 'unknown',
205
+ ]);
206
+ const DrawingReviewedObjectEvidenceSchema = zod_1.z.object({
207
+ objectId: zod_1.z.string().min(1),
208
+ canvasType: CanvasTypeSchema,
209
+ objectType: DrawingReviewedObjectTypeSchema,
210
+ reviewStatus: zod_1.z.literal('reviewed_cabinet'),
211
+ viewPlaneRole: DrawingViewPlaneRoleSchema,
212
+ frontFaceFeatures: zod_1.z.array(DrawingFrontFaceFeatureSchema).default([]),
213
+ reviewToken: zod_1.z.string().min(20).optional().describe('Optional trace token from a reviewed drawing artifact. Trace metadata only.'),
214
+ bbox: zod_1.z.object({
215
+ x: zod_1.z.number().finite(),
216
+ y: zod_1.z.number().finite(),
217
+ width: zod_1.z.number().finite().positive(),
218
+ height: zod_1.z.number().finite().positive(),
219
+ }).strict().optional(),
220
+ evidence: zod_1.z.string().min(20),
221
+ }).strict().describe('Optional structured reviewed object evidence from MCP drawing-analysis/review artifacts. Trace metadata only; MCP does not certify visual semantics.');
222
+ const DrawingBboxPxSchema = zod_1.z.object({
223
+ x: zod_1.z.number().finite(),
224
+ y: zod_1.z.number().finite(),
225
+ width: zod_1.z.number().finite().positive(),
226
+ height: zod_1.z.number().finite().positive(),
227
+ }).strict();
228
+ const BaseCabinetBodyHeightEvidenceSchema = zod_1.z.object({
229
+ cabinetBodyHeightInches: zod_1.z.number().finite().positive().describe('Measured or inferred base cabinet body height in inches, excluding countertop and toekick/plinth.'),
230
+ measurementBasis: zod_1.z.enum([
231
+ 'explicit_dimension_callout',
232
+ 'called_out_scale_and_known_dimension',
233
+ 'known_catalog_body_height',
234
+ 'user_confirmed',
235
+ ]).describe('How the agent established the base cabinet body height without including countertop or plinth/toekick.'),
236
+ scaleEvidence: zod_1.z.string().min(20).describe('Specific drawing scale, known dimension, repeated module, or user-confirmed basis used for the height measurement.'),
237
+ topBoundaryEvidence: zod_1.z.string().min(20).describe('Evidence for the top of the base cabinet body. Must explain how countertop height/thickness was excluded.'),
238
+ bottomBoundaryEvidence: zod_1.z.string().min(20).describe('Evidence for the bottom of the base cabinet body. Must explain how toekick/plinth height and Z0/floor were excluded.'),
239
+ excludedElements: zod_1.z.array(zod_1.z.enum([
240
+ 'toekick_plinth',
241
+ 'countertop',
242
+ 'floor',
243
+ 'wall',
244
+ 'ceiling',
245
+ 'dimension_text',
246
+ 'blank_context',
247
+ ])).min(2).describe('Elements explicitly excluded from the base cabinet body height and bbox.'),
248
+ plinthDecision: zod_1.z.enum([
249
+ 'separate_plinth_item_created',
250
+ 'no_visible_plinth',
251
+ 'user_confirmed_no_plinth',
252
+ ]).describe('Agent decision for the toekick/plinth band. If visible, it must be a separate plinth item and orange placement.'),
253
+ countertopDecision: zod_1.z.enum([
254
+ 'excluded_visible_countertop',
255
+ 'no_visible_countertop',
256
+ 'user_confirmed_no_countertop',
257
+ ]).describe('Agent decision for countertop geometry. Visible countertops must be excluded from the cabinet body height.'),
258
+ z0Evidence: zod_1.z.string().min(20).describe('Evidence that the cabinet body bbox/height does not extend below the finished-floor Z0 reference into floor/wall/ceiling context.'),
259
+ cabinetBodyBboxPx: DrawingBboxPxSchema.optional().describe('Optional blue cabinet body bbox used for the base item, excluding countertop and toekick/plinth.'),
260
+ plinthBboxPx: DrawingBboxPxSchema.optional().describe('Optional orange plinth/toekick bbox if a visible plinth/toekick was separated.'),
261
+ countertopBboxPx: DrawingBboxPxSchema.optional().describe('Optional countertop context bbox used only to document exclusion from the base cabinet body.'),
262
+ evidence: zod_1.z.string().min(20).describe('Plain-language audit explanation of why the persisted base height excludes countertop and plinth/toekick.'),
263
+ }).strict().superRefine((value, ctx) => {
264
+ if (!value.excludedElements.includes('toekick_plinth')) {
265
+ ctx.addIssue({
266
+ code: zod_1.z.ZodIssueCode.custom,
267
+ path: ['excludedElements'],
268
+ message: 'Base cabinet body height evidence must explicitly exclude toekick_plinth.',
269
+ });
270
+ }
271
+ if (!value.excludedElements.includes('countertop')) {
272
+ ctx.addIssue({
273
+ code: zod_1.z.ZodIssueCode.custom,
274
+ path: ['excludedElements'],
275
+ message: 'Base cabinet body height evidence must explicitly exclude countertop.',
276
+ });
277
+ }
278
+ if (value.plinthDecision === 'separate_plinth_item_created' && !value.plinthBboxPx) {
279
+ ctx.addIssue({
280
+ code: zod_1.z.ZodIssueCode.custom,
281
+ path: ['plinthBboxPx'],
282
+ message: 'separate_plinth_item_created requires the separated plinth/toekick bbox evidence.',
283
+ });
284
+ }
285
+ }).describe('Required measurement evidence for drawing-derived base/sink cabinet READY payloads. This is the agent contract that forces base height to exclude countertop and toekick/plinth.');
286
+ const OrderReadyStatusSchema = zod_1.z.enum([
287
+ 'NOT_PREPARED',
288
+ 'READY',
289
+ 'NEEDS_USER_INPUT',
290
+ 'NEEDS_AGENT_CONFIGURATION',
291
+ 'BLOCKED',
292
+ 'SKIPPED',
293
+ 'STALE',
294
+ ]).describe('AI-only order-ready validation status for one backend AI cart item.');
295
+ const OrderReadySourceSchema = zod_1.z.enum(['AI', 'MCP', 'BROWSER']).default('MCP').describe('Source metadata for the AI-only order-ready payload update.');
296
+ const OrderReadyMessageSchema = zod_1.z.object({
297
+ code: zod_1.z.string().min(1).optional(),
298
+ severity: zod_1.z.enum(['info', 'warning', 'error', 'blocker']).default('error'),
299
+ field: zod_1.z.string().min(1).optional(),
300
+ message: zod_1.z.string().min(1),
301
+ }).strict().describe('Structured order-ready validation message for MCP/frontend display.');
302
+ const AxisHorizontalDirectionSchema = zod_1.z.enum(['left', 'right']);
303
+ const AxisVerticalDirectionSchema = zod_1.z.enum(['up', 'down']);
304
+ const CalibrationSchemaVersionSchema = zod_1.z.literal('ai-canvas-calibration/v1');
305
+ const CalibrationUnitSchema = zod_1.z.literal('inches');
306
+ const ImageSizePxSchema = zod_1.z.object({
307
+ width: zod_1.z.number().positive().describe('Positive image width in pixels.'),
308
+ height: zod_1.z.number().positive().describe('Positive image height in pixels.'),
309
+ }).strict();
310
+ const PixelPointSchema = zod_1.z.object({
311
+ x: zod_1.z.number().finite().describe('Image pixel X coordinate.'),
312
+ y: zod_1.z.number().finite().describe('Image pixel Y coordinate.'),
313
+ }).strict();
314
+ const PlanProjectionCalibrationSchema = zod_1.z.object({
315
+ schemaVersion: CalibrationSchemaVersionSchema,
316
+ canvasType: zod_1.z.literal('plan'),
317
+ imageSizePx: ImageSizePxSchema,
318
+ unit: CalibrationUnitSchema,
319
+ pixelsPerInch: zod_1.z.number().positive().describe('Positive pixels per room inch for plan projection.'),
320
+ roomOriginPx: PixelPointSchema.describe('Image pixel reference point for room/global coordinate origin.'),
321
+ roomAxis: zod_1.z.object({
322
+ xPositiveDirection: AxisHorizontalDirectionSchema,
323
+ yPositiveDirection: AxisVerticalDirectionSchema,
324
+ }).strict(),
325
+ roomSizeInches: zod_1.z.object({
326
+ width: zod_1.z.number().positive().optional(),
327
+ depth: zod_1.z.number().positive().optional(),
328
+ }).strict().optional(),
329
+ displayTransform: zod_1.z.record(zod_1.z.unknown()).optional().describe('Optional UI-only zoom/pan metadata. It is not projection calibration.'),
330
+ }).strict().describe('Plan projection calibration for canonical room/global AI coordinates.');
331
+ const ElevationVisibilityRuleSchema = zod_1.z.object({
332
+ mode: zod_1.z.literal('rotation'),
333
+ rotations: zod_1.z.array(CoordinateRotationSchema).optional().describe('Allowed wall rotations for this elevation. Use 0 north, 90 east, 180 south, 270 west.'),
334
+ visibleRotations: zod_1.z.array(CoordinateRotationSchema).optional().describe('Backend-compatible allowed wall rotations for this elevation. Use 0 north, 90 east, 180 south, 270 west.'),
335
+ includePositionNames: zod_1.z.array(zod_1.z.string().min(1)).optional(),
336
+ excludePositionNames: zod_1.z.array(zod_1.z.string().min(1)).optional(),
337
+ }).strict().superRefine((value, ctx) => {
338
+ if (!value.rotations && !value.visibleRotations) {
339
+ ctx.addIssue({
340
+ code: zod_1.z.ZodIssueCode.custom,
341
+ message: 'visibilityRule requires rotations or visibleRotations',
342
+ path: ['rotations'],
343
+ });
344
+ }
345
+ });
346
+ const ElevationCanvasTypeSchema = zod_1.z.enum(['north', 'south', 'east', 'west']);
347
+ const AiDrawingItemClassificationEvidenceSchema = zod_1.z.object({
348
+ sourceCanvases: zod_1.z.array(CanvasTypeSchema).min(1).describe('Canvases actually used to classify this physical item.'),
349
+ primaryCanvasType: CanvasTypeSchema.describe('Canvas that directly shows the cabinet geometry used to classify this item.'),
350
+ classificationBasis: zod_1.z.enum([
351
+ 'same_canvas_visible_geometry',
352
+ 'plan_elevation_association',
353
+ 'user_confirmed',
354
+ ]).describe('Basis for semantic classification. Plan symbols from unrelated elevation/detail regions are not valid.'),
355
+ semanticTypeEvidence: zod_1.z.string().min(20).describe('Human-readable same-canvas or user-confirmed evidence for the item type, such as base, upper, pantry/tall, sink base, appliance surround, or filler.'),
356
+ primaryViewPlaneRole: DrawingViewPlaneRoleSchema.optional().describe('View-plane role for the primary classification canvas. Elevation-derived items must be primary_elevation_face; return_side_face/context_anchor objects are not physical items for that elevation.'),
357
+ frontFaceEvidence: zod_1.z.string().min(20).optional().describe('Required visual evidence for elevation-derived primary_elevation_face items, such as visible doors, drawers, appliance front/handles, or front cabinet face boundaries.'),
358
+ frontFaceFeatures: zod_1.z.array(DrawingFrontFaceFeatureSchema).optional().describe('Required for elevation-derived items. Declare concrete visual signals visible on the primary face. side_panel_or_return_visible/context_only are exclusion signals, not placement evidence.'),
359
+ sourceElevationObject: DrawingReviewedObjectEvidenceSchema.optional().describe('Required for elevation-derived item classifications. Must cite the reviewed elevation object that directly supports the physical item.'),
360
+ baseCabinetBodyHeightEvidence: BaseCabinetBodyHeightEvidenceSchema.optional().describe('Required when a drawing-derived base or sink-base item is marked READY. Documents the measured body height excluding countertop and toekick/plinth.'),
361
+ sourcePlanObject: DrawingReviewedObjectEvidenceSchema.optional().describe('Required for plan_elevation_association classifications. Must cite the exact reviewed selected plan object.'),
362
+ elevationCanvasType: ElevationCanvasTypeSchema.optional(),
363
+ detailNumber: zod_1.z.string().min(1).optional(),
364
+ planMarkerSide: PlanMarkerSideSchema.optional(),
365
+ selectedPlanObjectIds: zod_1.z.array(zod_1.z.string().min(1)).optional(),
366
+ anchorPlanObjectIds: zod_1.z.array(zod_1.z.string().min(1)).optional(),
367
+ selectedPlanObjectBboxPx: zod_1.z.object({
368
+ x: zod_1.z.number().finite(),
369
+ y: zod_1.z.number().finite(),
370
+ width: zod_1.z.number().finite().positive(),
371
+ height: zod_1.z.number().finite().positive(),
372
+ }).strict().optional(),
373
+ rejectedConflictingEvidence: zod_1.z.array(zod_1.z.string().min(1)).default([]).describe('Plan-only symbols/anchors/callouts rejected because they belong to another elevation/detail marker side.'),
374
+ }).strict().describe('Optional trace metadata for the model-visible drawing interpretation. MCP accepts it for audit/debugging but does not certify visual semantics.');
375
+ const PlacementViewAssociationEvidenceSchema = zod_1.z.object({
376
+ basis: zod_1.z.enum(['plan_elevation_marker_review', 'same_canvas_reviewed_plan_object', 'user_confirmed', 'plan_only_no_elevation']).describe('Optional trace note for how this plan placement was associated to the physical cabinet/wall view.'),
377
+ elevationCanvasType: ElevationCanvasTypeSchema.optional().describe('Elevation canvas this plan placement is associated with, when applicable.'),
378
+ detailNumber: zod_1.z.string().min(1).optional().describe('Elevation/detail marker number used for plan/elevation binding, when applicable.'),
379
+ planMarkerSide: PlanMarkerSideSchema.optional().describe('Side of the plan detail marker used for binding, when applicable.'),
380
+ selectedPlanObjectIds: zod_1.z.array(zod_1.z.string().min(1)).optional().describe('Optional plan object ids selected during analysis. Trace metadata only.'),
381
+ anchorPlanObjectIds: zod_1.z.array(zod_1.z.string().min(1)).optional().describe('Optional contextual plan anchor ids. Do not use this to block REF/refrigerator footprints; REF can be the selected placement object when matched elevation vision confirms an appliance/surround condition.'),
382
+ selectedPlanObjectBboxPx: zod_1.z.object({
383
+ x: zod_1.z.number().finite(),
384
+ y: zod_1.z.number().finite(),
385
+ width: zod_1.z.number().finite().positive(),
386
+ height: zod_1.z.number().finite().positive(),
387
+ }).strict().optional().describe('Optional pixel bbox of a selected plan object used during analysis. Trace metadata only.'),
388
+ selectedPlanObjectRole: zod_1.z.enum([
389
+ 'physical_plan_footprint',
390
+ 'appliance_footprint',
391
+ 'dashed_overhead_only',
392
+ 'context_anchor',
393
+ 'annotation_or_clearance',
394
+ 'needs_user_confirmation',
395
+ ]).optional().describe('Optional structured role of the selected plan object. Trace metadata only.'),
396
+ userExplicitConfirmation: zod_1.z.boolean().optional().describe('Optional trace flag for user confirmation.'),
397
+ userConfirmationText: zod_1.z.string().min(20).optional().describe('Optional user-confirmation trace text.'),
398
+ sourcePlanObject: DrawingReviewedObjectEvidenceSchema.optional().describe('Optional reviewed plan object selected during analysis. Trace metadata only.'),
399
+ sharedAnchors: zod_1.z.array(zod_1.z.string().min(1)).optional().describe('Shared semantic anchors used for the association, such as appliance_label:REF.'),
400
+ conflictingPlanAnchorsRejected: zod_1.z.array(zod_1.z.string().min(1)).optional().describe('Plan-only anchors that were rejected for this elevation association, such as fixture_symbol:SINK.'),
401
+ evidence: zod_1.z.string().optional().describe('Optional plain-language trace note for this placement.'),
402
+ }).strict().describe('Optional trace metadata for a placement. MCP does not certify plan/elevation visual semantics from this object.');
403
+ const ElevationProjectionCalibrationBaseSchema = zod_1.z.object({
404
+ schemaVersion: CalibrationSchemaVersionSchema,
405
+ canvasType: ElevationCanvasTypeSchema,
406
+ imageSizePx: ImageSizePxSchema,
407
+ unit: CalibrationUnitSchema,
408
+ horizontalPixelsPerInch: zod_1.z.number().positive(),
409
+ verticalPixelsPerInch: zod_1.z.number().positive(),
410
+ floorZ0Px: zod_1.z.number().finite(),
411
+ wallType: ElevationCanvasTypeSchema,
412
+ wallPlaneCoordinateInches: zod_1.z.number().finite(),
413
+ horizontalOriginPx: PixelPointSchema,
414
+ horizontalOriginCoordinateInches: zod_1.z.number().finite(),
415
+ horizontalAxisPositiveDirection: AxisHorizontalDirectionSchema,
416
+ verticalAxisPositiveDirection: AxisVerticalDirectionSchema,
417
+ visibilityRule: ElevationVisibilityRuleSchema,
418
+ displayTransform: zod_1.z.record(zod_1.z.unknown()).optional().describe('Optional UI-only zoom/pan metadata. It is not projection calibration.'),
419
+ }).strict();
420
+ const ElevationProjectionCalibrationSchema = ElevationProjectionCalibrationBaseSchema.superRefine((value, ctx) => {
421
+ if (value.wallType !== value.canvasType) {
422
+ ctx.addIssue({
423
+ code: zod_1.z.ZodIssueCode.custom,
424
+ message: 'wallType must match canvasType',
425
+ path: ['wallType'],
426
+ });
427
+ }
428
+ }).describe('Elevation projection calibration for canonical room/global AI coordinates.');
429
+ exports.AiCanvasProjectionCalibrationSchema = zod_1.z.union([
430
+ PlanProjectionCalibrationSchema,
431
+ ElevationProjectionCalibrationSchema,
432
+ ]).describe('Canonical AIDrawingTool projection calibration stored at canvasMeta.projectionCalibration.');
433
+ exports.AiCanvasMetaSchema = zod_1.z.record(zod_1.z.unknown()).superRefine((canvasMeta, ctx) => {
434
+ const projectionCalibration = canvasMeta.projectionCalibration;
435
+ if (projectionCalibration === undefined) {
436
+ return;
437
+ }
438
+ const result = exports.AiCanvasProjectionCalibrationSchema.safeParse(projectionCalibration);
439
+ if (!result.success) {
440
+ for (const issue of result.error.issues) {
441
+ ctx.addIssue({
442
+ ...issue,
443
+ path: ['projectionCalibration', ...issue.path],
444
+ });
445
+ }
446
+ }
447
+ }).describe('Flexible AI canvas metadata. If projectionCalibration is present, it must follow ai-canvas-calibration/v1. Other legacy metadata keys remain flexible during transition.');
448
+ exports.AiDrawingSessionCreateInputSchema = zod_1.z.object({
449
+ sourceType: zod_1.z.enum(['REDUX_SNAPSHOT', 'ORDER', 'SAVED_CART']).default('REDUX_SNAPSHOT').describe('Session source metadata. Stored draft cart references are metadata only and are not the live AI drawing cart.'),
450
+ sourceOrderId: zod_1.z.string().optional().describe('Optional source order id metadata.'),
451
+ sourceSavedCartId: zod_1.z.number().int().optional().describe('Optional source stored cart id metadata only.'),
452
+ activeAiCartId: zod_1.z.string().uuid().optional().describe('Optional active backend-owned AI cart UUID to link at session creation.'),
453
+ }).strict().describe('Create an isolated backend-owned AIDrawingTool session. Owner fields are supplied by backend authentication, not by MCP input.');
454
+ exports.AiCartIdParamSchema = zod_1.z.object({
455
+ cartId: zod_1.z.string().uuid().describe('UUID of the isolated backend-owned AIDrawingTool AI cart. This is not the frontend Redux cart and not a stored draft cart.'),
456
+ }).strict();
457
+ exports.AiCartItemIdParamSchema = exports.AiCartIdParamSchema.extend({
458
+ itemId: zod_1.z.string().uuid().describe('UUID of the backend canonical AI cart item. This backend aiCartItemId is the primary cart item identity after creation.'),
459
+ }).strict();
460
+ exports.AiCartItemInputSchema = zod_1.z.object({
461
+ frontendItemId: zod_1.z.string().optional().describe('Optional source frontend item id retained only as trace metadata. It is not canonical identity.'),
462
+ originalFrontendItemId: zod_1.z.string().optional().describe('Optional original source frontend item id retained when an input quantity expands into physical AI cart items.'),
463
+ serialNumber: zod_1.z.string().optional().describe('Optional catalog serial number. It identifies the cabinet catalog item only, not the cart item identity.'),
464
+ positionName: zod_1.z.string().min(1).describe('Required physical cabinet identity. It must be unique within an AI cart after backend expansion and must be reused by every plan/elevation placement that represents this same cabinet.'),
465
+ originalPositionName: zod_1.z.string().optional().describe('Optional original position name retained as source metadata when backend expansion creates physical item instances.'),
466
+ groupId: zod_1.z.string().optional().describe('Optional source grouping id retained as metadata.'),
467
+ quantity: zod_1.z.number().int().positive().default(1).describe('Positive source quantity. Quantity greater than 1 is accepted for snapshot input and the backend expands it into separate physical AI cart items with unique positionName values.'),
468
+ originalQuantity: zod_1.z.number().int().positive().optional().describe('Optional original source quantity. If omitted, the backend can preserve the input quantity as original quantity.'),
469
+ width: zod_1.z.number().positive().optional().describe('Optional cabinet width in inches.'),
470
+ height: zod_1.z.number().positive().optional().describe('Optional cabinet height in inches.'),
471
+ depth: zod_1.z.number().positive().optional().describe('Optional cabinet depth in inches.'),
472
+ displayName: zod_1.z.string().optional().describe('Optional cabinet display name for UI and agent context.'),
473
+ drawingClassificationEvidence: AiDrawingItemClassificationEvidenceSchema.optional().describe('Optional drawing interpretation trace metadata. MCP stores it for audit/debugging but does not validate visual semantics.'),
474
+ itemPayload: zod_1.z.record(zod_1.z.unknown()).optional().describe('Optional full original cart item payload object. If omitted, the MCP tool will synthesize a compact provenance payload from the item identity, catalog, dimensions, quantity, and display fields so the backend originalItemPayload requirement is still satisfied.'),
475
+ }).strict().describe('AI cart item snapshot input for the isolated backend-owned AIDrawingTool AI cart. The snapshot must be one physical cabinet manifest for the whole session, not separate plan/elevation manifests. positionName is physical cabinet identity; backend aiCartItemId becomes canonical after creation; frontendItemId is trace metadata only; serialNumber is catalog identity only. For agent-created cabinets, creating this row is not enough: after backend ids are returned, write an order-ready payload/status for this item with set_ai_cart_item_order_ready_payload. This schema is separate from the Redux cart, stored draft carts, and the legacy frontend drawing route.');
476
+ exports.AiCartItemPatchSchema = zod_1.z.object({
477
+ frontendItemId: zod_1.z.string().optional().describe('Optional trace metadata update. Not canonical identity.'),
478
+ originalFrontendItemId: zod_1.z.string().optional().describe('Optional original source frontend item id metadata update.'),
479
+ serialNumber: zod_1.z.string().optional().describe('Optional catalog identity update. Not cart item identity.'),
480
+ positionName: zod_1.z.string().min(1).optional().describe('Optional drawing identity update. Backend validation requires it to remain unique within the AI cart.'),
481
+ originalPositionName: zod_1.z.string().optional().describe('Optional original source position name metadata update.'),
482
+ groupId: zod_1.z.string().optional().describe('Optional source grouping metadata update.'),
483
+ quantity: zod_1.z.number().int().positive().optional().describe('Optional physical instance quantity update. Backend AI cart items represent physical cabinet instances, so service validation permits only quantity 1.'),
484
+ originalQuantity: zod_1.z.number().int().positive().optional().describe('Optional original source quantity metadata update.'),
485
+ width: zod_1.z.number().positive().optional().describe('Optional cabinet width in inches.'),
486
+ height: zod_1.z.number().positive().optional().describe('Optional cabinet height in inches.'),
487
+ depth: zod_1.z.number().positive().optional().describe('Optional cabinet depth in inches.'),
488
+ displayName: zod_1.z.string().optional().describe('Optional cabinet display name update.'),
489
+ itemPayload: zod_1.z.record(zod_1.z.unknown()).optional().describe('Optional replacement for the preserved original cart item payload object. Future tools should serialize it for backend storage.'),
490
+ }).strict().describe('AI cart item patch input for isolated backend-owned AIDrawingTool AI cart items. Unknown fields are rejected so future tools cannot accidentally mutate unsupported state.');
491
+ const AiCartSnapshotOrderConfigurationWorkflowSchema = zod_1.z.object({
492
+ mode: zod_1.z.literal('USER_CONFIRMED_ORDER_READY').describe('Required workflow gate before syncing agent-created AI cart items. The agent already ran configuration discovery, asked the user for exact order-critical options or a saved setting, and has enough confirmed inputs to write READY payloads after backend ids are returned.'),
493
+ evidence: zod_1.z.string().min(1).describe('Brief human-readable audit note for the configuration workflow. Prose is not used as a hard correctness gate; actual READY payloads are validated separately by ArticleItemSchema/order-contract checks.'),
494
+ checkedSerialNumbers: zod_1.z.array(zod_1.z.string().min(1)).min(1).optional().describe('Catalog serialNumbers for which configuration context/options were checked before syncing the AI cart snapshot.'),
495
+ }).strict().describe('Pre-sync workflow gate for agent-created AIDrawingTool AI cart items.');
496
+ exports.AiCartSnapshotInputSchema = exports.AiCartIdParamSchema.extend({
497
+ sourceSnapshotPayload: zod_1.z.string().optional().describe('Optional serialized full source cart snapshot for backend preservation.'),
498
+ items: zod_1.z.array(exports.AiCartItemInputSchema).default([]).describe('AI cart snapshot items. Provide each physical cabinet once for the whole AI drawing session. Do not create separate cart items for the plan view and elevation views of the same cabinet. First snapshot into a new or empty AI cart is allowed; a later snapshot must not silently overwrite cart state after AI or MCP edits. After creating agent-created items, use returned backend aiCartItemId values to write order-ready payload/status records for each item.'),
499
+ orderConfigurationWorkflow: AiCartSnapshotOrderConfigurationWorkflowSchema.optional(),
500
+ }).describe('Full AI cart snapshot input for future MCP tools. It targets only the isolated backend-owned AIDrawingTool AI cart and persists one physical cabinet manifest for the session. Visual interpretation belongs to native vision and rendered review; this schema validates persistence shape only.');
501
+ exports.AiCartCreateInputSchema = zod_1.z.object({
502
+ aiDrawingSessionId: zod_1.z.string().uuid().optional().describe('Optional UUID of the AI drawing session that owns this backend AI cart.'),
503
+ sourceType: zod_1.z.enum(['REDUX_SNAPSHOT', 'ORDER', 'SAVED_CART']).default('REDUX_SNAPSHOT').describe('Source metadata for the initial AI cart. This is metadata only and does not make stored draft carts the live AI cart.'),
504
+ sourceOrderId: zod_1.z.string().optional().describe('Optional source order id metadata.'),
505
+ sourceSavedCartId: zod_1.z.number().int().optional().describe('Optional source stored cart id metadata only.'),
506
+ sourceSnapshotPayload: zod_1.z.string().optional().describe('Optional serialized source cart snapshot for backend preservation.'),
507
+ orderConfigurationWorkflow: AiCartSnapshotOrderConfigurationWorkflowSchema.optional().describe('Optional audit note for user/configuration workflow. It is not forwarded to the backend create-cart endpoint.'),
508
+ }).strict();
509
+ const PatchAiCartItemInputSchema = exports.AiCartItemIdParamSchema.extend({
510
+ patch: exports.AiCartItemPatchSchema.describe('Patch fields for one backend-owned AI cart item.'),
511
+ }).strict();
512
+ const AiCabinetOperationItemParamSchema = AiDrawingSessionIdParamSchema.extend({
513
+ itemId: zod_1.z.string().uuid().describe('UUID of the physical backend AI cart item in the session active AI cart.'),
514
+ }).strict();
515
+ exports.DuplicateAiCabinetInputSchema = AiCabinetOperationItemParamSchema.extend({
516
+ positionName: zod_1.z.string().min(1).optional().describe('Optional unique positionName for the new physical cabinet. If omitted, the backend generates a copy name.'),
517
+ sourceCanvasType: CanvasTypeSchema.optional().describe('Optional selected source canvas whose placement should be copied for the duplicate. This maps to the backend selectedCanvasType field.'),
518
+ sourcePlacementPositionName: zod_1.z.string().min(1).optional().describe('Optional source placement positionName to copy from the selected canvas. Defaults to the source item positionName in the backend.'),
519
+ placementOffsetX: zod_1.z.number().finite().optional().describe('Optional X offset for the copied selected-canvas placement.'),
520
+ placementOffsetY: zod_1.z.number().finite().optional().describe('Optional Y offset for the copied selected-canvas placement.'),
521
+ source: PlacementSourceSchema.optional(),
522
+ }).strict().describe('Duplicate one physical backend AI cabinet item and optionally copy one selected-canvas placement. ' +
523
+ AI_CABINET_OPERATION_GUIDANCE);
524
+ const ConfigurePayloadSchema = zod_1.z.union([zod_1.z.string().min(1), zod_1.z.record(zod_1.z.unknown())]);
525
+ exports.ConfigureAiCabinetInputSchema = AiCabinetOperationItemParamSchema.extend({
526
+ positionName: zod_1.z.string().min(1).optional().describe('Optional rename for the physical cabinet identity. The backend enforces uniqueness in the session active AI cart and updates linked AI-only placements.'),
527
+ frontendItemId: zod_1.z.string().optional().describe('Optional trace metadata update. Not physical identity.'),
528
+ originalFrontendItemId: zod_1.z.string().optional().describe('Optional original source frontend item id metadata update.'),
529
+ serialNumber: zod_1.z.string().optional().describe('Optional catalog identity update. Not physical AI cart item identity.'),
530
+ originalPositionName: zod_1.z.string().optional().describe('Optional original source position name metadata update.'),
531
+ groupId: zod_1.z.string().optional().describe('Optional source grouping metadata update.'),
532
+ quantity: zod_1.z.literal(1).optional().describe('Optional physical instance quantity. Backend AI cart items represent one physical cabinet, so only 1 is valid.'),
533
+ originalQuantity: zod_1.z.number().int().positive().optional().describe('Optional original source quantity metadata update.'),
534
+ width: zod_1.z.number().positive().optional().describe('Optional cabinet width in inches. Linked plan placements resize width/depth; linked elevation placements resize width/height.'),
535
+ height: zod_1.z.number().positive().optional().describe('Optional cabinet height in inches. Linked elevation placements use this as visual height.'),
536
+ depth: zod_1.z.number().positive().optional().describe('Optional cabinet depth in inches. Linked plan placements use this as visual height.'),
537
+ displayName: zod_1.z.string().optional().describe('Optional cabinet display name update.'),
538
+ originalItemPayload: ConfigurePayloadSchema.optional().describe('Optional preserved source payload. Objects are serialized before sending to the backend.'),
539
+ source: PlacementSourceSchema.optional(),
540
+ }).strict().describe('Configure one physical backend AI cabinet item and resize linked AI-only placements when dimensions change. ' +
541
+ AI_CABINET_OPERATION_GUIDANCE);
542
+ exports.DeleteAiCabinetInputSchema = AiCabinetOperationItemParamSchema.describe('Delete one physical backend AI cabinet item and all linked AI-only per-canvas placements. ' +
543
+ AI_CABINET_OPERATION_GUIDANCE);
544
+ exports.AiOrderReadyPayloadReadInputSchema = AiCabinetOperationItemParamSchema.describe('Read the AI-only order-ready payload/status for one backend AI cart item. ' +
545
+ AI_ORDER_READY_GUIDANCE);
546
+ exports.AiOrderReadyPayloadSetInputSchema = AiCabinetOperationItemParamSchema.extend({
547
+ orderReadyStatus: OrderReadyStatusSchema,
548
+ orderReadyPayload: zod_1.z.record(zod_1.z.unknown()).optional().describe('Agent-prepared order-ready article payload. Required and validated against the MCP ArticleItemSchema when orderReadyStatus is READY.'),
549
+ orderReadyMessages: zod_1.z.array(OrderReadyMessageSchema).optional().describe('Structured validation messages. Use this for NEEDS_AGENT_CONFIGURATION, BLOCKED, STALE, or other non-ready states.'),
550
+ orderReadySource: OrderReadySourceSchema.optional(),
551
+ drawingClassificationEvidence: AiDrawingItemClassificationEvidenceSchema.optional().describe('Optional drawing interpretation trace metadata. MCP does not certify visual semantics from this object.'),
552
+ }).strict().superRefine((value, ctx) => {
553
+ if (value.orderReadyStatus === 'READY') {
554
+ const result = orders_1.ArticleItemSchema.safeParse(value.orderReadyPayload);
555
+ if (!result.success || !hasOrderConfigurationEvidence(value.orderReadyPayload)) {
556
+ ctx.addIssue({
557
+ code: zod_1.z.ZodIssueCode.custom,
558
+ path: ['orderReadyPayload'],
559
+ message: 'READY requires an ArticleItemSchema-compatible payload with order-critical configuration evidence; arbitrary objects or thin AI item fields are not order-ready.',
560
+ });
561
+ }
562
+ if (result.success) {
563
+ addBaseCabinetBodyHeightEvidenceIssues(value.orderReadyPayload, value.drawingClassificationEvidence, ctx);
564
+ }
565
+ }
566
+ if (value.orderReadyStatus === 'NOT_PREPARED' && value.orderReadyPayload !== undefined) {
567
+ ctx.addIssue({
568
+ code: zod_1.z.ZodIssueCode.custom,
569
+ path: ['orderReadyPayload'],
570
+ message: 'NOT_PREPARED must not include orderReadyPayload. Use clear_ai_cart_item_order_ready_payload to reset.',
571
+ });
572
+ }
573
+ }).describe('Set the AI-only order-ready payload/status for one backend AI cart item. ' +
574
+ AI_ORDER_READY_GUIDANCE);
575
+ exports.AiOrderReadyPayloadClearInputSchema = AiCabinetOperationItemParamSchema.describe('Clear the AI-only order-ready payload/status for one backend AI cart item. ' +
576
+ AI_ORDER_READY_GUIDANCE);
577
+ const AiReduxCartImportStatusSchema = zod_1.z.literal('READY').describe('MCP imports must be existing order-ready browser Redux cart items. Non-ready planner/takeoff items must not be imported through MCP.');
578
+ const AiReduxCartImportItemSchema = zod_1.z.object({
579
+ frontendItemId: zod_1.z.string().min(1).optional().describe('Source browser Redux cart item id for idempotent import. Provide when available.'),
580
+ originalFrontendItemId: zod_1.z.string().min(1).optional().describe('Original source browser Redux cart item id when different from frontendItemId.'),
581
+ positionName: zod_1.z.string().min(1).describe('Position name from the full Redux cart item payload.'),
582
+ originalPositionName: zod_1.z.string().min(1).optional(),
583
+ serialNumber: zod_1.z.string().min(1).describe('Catalog/article serial number from the Redux cart item.'),
584
+ displayName: zod_1.z.string().min(1).optional(),
585
+ groupId: zod_1.z.string().min(1).optional(),
586
+ quantity: zod_1.z.number().int().positive().default(1).describe('Source quantity. Backend import expands quantity greater than 1 into physical AI cart item instances.'),
587
+ originalQuantity: zod_1.z.number().int().positive().optional(),
588
+ width: zod_1.z.number().positive().describe('Cabinet width from the Redux cart item payload.'),
589
+ height: zod_1.z.number().positive().describe('Cabinet height from the Redux cart item payload.'),
590
+ depth: zod_1.z.number().positive().describe('Cabinet depth from the Redux cart item payload.'),
591
+ originalItemPayload: zod_1.z.record(zod_1.z.unknown()).describe('Full original browser Redux checkout cart item payload. Do not send thin serial/dimensions-only objects.'),
592
+ orderReadyPayload: zod_1.z.record(zod_1.z.unknown()).optional().describe('Optional prepared order-ready ArticleItemSchema-compatible payload. Required when orderReadyStatus is READY unless originalItemPayload itself is order-ready.'),
593
+ orderReadyStatus: AiReduxCartImportStatusSchema.optional(),
594
+ orderReadyMessages: zod_1.z.array(OrderReadyMessageSchema).optional(),
595
+ orderReadySource: OrderReadySourceSchema.optional(),
596
+ orderReadyRevision: zod_1.z.number().int().nonnegative().optional(),
597
+ aiDrawingSessionId: zod_1.z.string().uuid().optional().describe('Optional existing mirror provenance session id.'),
598
+ aiCartId: zod_1.z.string().uuid().optional().describe('Optional existing mirror provenance AI cart id.'),
599
+ aiCartItemId: zod_1.z.string().uuid().optional().describe('Optional existing mirror provenance AI cart item id.'),
600
+ aiMirrorStatus: zod_1.z.string().min(1).optional(),
601
+ aiMirrorSource: zod_1.z.string().min(1).optional(),
602
+ }).strict().superRefine((value, ctx) => {
603
+ if (!hasCoreReduxPayloadFields(value.originalItemPayload)) {
604
+ ctx.addIssue({
605
+ code: zod_1.z.ZodIssueCode.custom,
606
+ path: ['originalItemPayload'],
607
+ message: 'originalItemPayload must be the full Redux cart item object with positionName, serialNumber, quantity, width, height, and depth.',
608
+ });
609
+ }
610
+ const originalIsOrderReady = orders_1.ArticleItemSchema.safeParse(value.originalItemPayload).success &&
611
+ hasOrderConfigurationEvidence(value.originalItemPayload);
612
+ const suppliedReadyPayloadIsOrderReady = orders_1.ArticleItemSchema.safeParse(value.orderReadyPayload).success &&
613
+ hasOrderConfigurationEvidence(value.orderReadyPayload);
614
+ if (value.orderReadyStatus === 'READY' && !suppliedReadyPayloadIsOrderReady && !originalIsOrderReady) {
615
+ ctx.addIssue({
616
+ code: zod_1.z.ZodIssueCode.custom,
617
+ path: ['orderReadyPayload'],
618
+ message: 'READY import requires orderReadyPayload or originalItemPayload to satisfy ArticleItemSchema with order-critical configuration evidence.',
619
+ });
620
+ }
621
+ if (value.orderReadyStatus !== 'READY') {
622
+ ctx.addIssue({
623
+ code: zod_1.z.ZodIssueCode.custom,
624
+ path: ['orderReadyStatus'],
625
+ message: 'MCP import requires orderReadyStatus READY. Do not use import_redux_cart_items_to_ai_cart for drawing-takeoff or unconfigured planner items.',
626
+ });
627
+ }
628
+ if (!suppliedReadyPayloadIsOrderReady && !originalIsOrderReady) {
629
+ ctx.addIssue({
630
+ code: zod_1.z.ZodIssueCode.custom,
631
+ path: ['originalItemPayload'],
632
+ message: 'MCP import requires originalItemPayload or orderReadyPayload to satisfy ArticleItemSchema with order-critical configuration evidence.',
633
+ });
634
+ }
635
+ }).describe('One rich Redux checkout cart item import candidate for the backend-owned AIDrawingTool AI cart. ' +
636
+ AI_REDUX_IMPORT_GUIDANCE);
637
+ exports.AiReduxCartImportInputSchema = AiDrawingSessionIdParamSchema.extend({
638
+ items: zod_1.z.array(AiReduxCartImportItemSchema).min(1).describe('One or more full Redux cart items to import idempotently into the active AI drawing cart.'),
639
+ }).strict().describe('Import rich Redux checkout cart items into the active backend-owned AIDrawingTool AI cart. ' +
640
+ AI_REDUX_IMPORT_GUIDANCE);
641
+ exports.AiDrawingSessionReviewRenderInputSchema = AiDrawingSessionIdParamSchema.extend({
642
+ canvasTypes: zod_1.z.array(CanvasTypeSchema).optional().describe('Optional list of AI drawing canvases to render. When omitted, the tool renders the active canvases returned by the stored AI drawing state.'),
643
+ outputDir: zod_1.z.string().min(1).refine((value) => value.trim().length > 0, {
644
+ message: 'outputDir must not be blank',
645
+ }).describe('Local directory where the MCP server should write review PNG artifacts and an optional JSON manifest.'),
646
+ includeManifest: zod_1.z.boolean().optional().default(false).describe('When true, also write a JSON manifest next to the review PNG artifacts.'),
647
+ }).strict().describe('Render read-only AI drawing session visual review artifacts. ' + AI_DRAWING_VISUAL_REVIEW_GUIDANCE);
648
+ exports.LinkAiCartToDrawingSessionInputSchema = AiDrawingSessionIdParamSchema.extend({
649
+ cartId: zod_1.z.string().uuid().describe('UUID of the backend-owned AI cart to link as the session active cart.'),
650
+ }).strict();
651
+ exports.AiCanvasMetadataInputSchema = AiDrawingSessionIdParamSchema.extend({
652
+ canvasType: CanvasTypeSchema,
653
+ canvasMeta: exports.AiCanvasMetaSchema.optional().describe('Optional AI canvas metadata payload. canvasMeta itself is flexible. Use canvasMeta.projectionCalibration only for the strict canonical coordinate projection schema; do not put free-form notes there. For IMAGE_PIXELS placement-only workflows, store notes in other canvasMeta keys and omit projectionCalibration.'),
654
+ z0Reference: zod_1.z.record(zod_1.z.unknown()).optional().describe('Optional Z0/floor reference metadata payload.'),
655
+ verticalCalibration: zod_1.z.record(zod_1.z.unknown()).optional().describe('Optional vertical calibration metadata payload.'),
656
+ calibrationOffsets: zod_1.z.record(zod_1.z.unknown()).optional().describe('Optional calibration offset metadata payload.'),
657
+ elevationVisibility: zod_1.z.record(zod_1.z.unknown()).optional().describe('Optional elevation visibility metadata payload.'),
658
+ fixedElevationPositions: zod_1.z.record(zod_1.z.unknown()).optional().describe('Optional fixed elevation position metadata payload.'),
659
+ elevationCalibrated: zod_1.z.record(zod_1.z.unknown()).optional().describe('Optional elevation calibration state metadata payload.'),
660
+ globalElevationOffsets: zod_1.z.record(zod_1.z.unknown()).optional().describe('Optional global elevation offset metadata payload.'),
661
+ calibratedCabinets: zod_1.z.record(zod_1.z.unknown()).optional().describe('Optional calibrated cabinet metadata payload.'),
662
+ activeCanvases: zod_1.z.array(CanvasTypeSchema).optional().describe('Optional active AI canvas list.'),
663
+ }).strict().describe('Upsert metadata for one isolated backend-owned AIDrawingTool canvas. This does not mutate legacy drawing metadata. ' +
664
+ AI_CANVAS_CALIBRATION_GUIDANCE);
665
+ exports.AiCanvasImageInputSchema = AiDrawingSessionIdParamSchema.extend({
666
+ canvasType: CanvasTypeSchema,
667
+ imageType: ImageTypeSchema,
668
+ s3Key: zod_1.z.string().min(1).describe('Required S3 key or backend image reference for the AI drawing canvas image.'),
669
+ }).strict().describe('Upsert an image reference for one isolated backend-owned AIDrawingTool canvas.');
670
+ exports.AiCanvasImageUploadInputSchema = AiDrawingSessionIdParamSchema.extend({
671
+ canvasType: CanvasTypeSchema,
672
+ imageType: ImageTypeSchema.default('background'),
673
+ imagePath: zod_1.z.string().min(1).describe('Absolute path to a local image file readable by the MCP server. Supported formats: .png, .jpg, .jpeg, .webp. The MCP server uploads the actual bytes to the AI backend; do not use upsert_ai_canvas_image for local file paths.'),
674
+ }).strict().describe('Upload a local image file into backend-owned AIDrawingTool storage for one canvas.');
675
+ exports.AiCanvasImageDeleteInputSchema = AiDrawingSessionIdParamSchema.extend({
676
+ canvasType: CanvasTypeSchema,
677
+ imageType: ImageTypeSchema,
678
+ }).strict().describe('Delete one image reference for one isolated backend-owned AIDrawingTool canvas. If the stored backend image is an AI-owned object under ai-drawings/{sessionId}/, the backend may delete that S3 object; arbitrary external URL references are not S3-deleted.');
679
+ exports.AiCanvasCabinetPlacementInputSchema = zod_1.z.object({
680
+ aiCartItemId: zod_1.z.string().uuid().optional().describe('Optional backend AI cart item UUID. Required unless positionName exactly matches an active AI cart item positionName for backend auto-linking.'),
681
+ positionName: zod_1.z.string().min(1).describe('Required physical AI cart item positionName and per-canvas placement identity. This must match the linked aiCartItemId when provided. Do not use aggregate footprint/reference labels such as P_*_FOOTPRINT as cabinet placements.'),
682
+ centerX: zod_1.z.number().finite().describe('Required placement center X in the selected canvas coordinate space.'),
683
+ centerY: zod_1.z.number().finite().describe('Required placement center Y in the selected canvas coordinate space.'),
684
+ width: zod_1.z.number().finite().positive().describe('Required positive visual rectangle width in the selected canvas coordinate space.'),
685
+ height: zod_1.z.number().finite().positive().describe('Required positive visual rectangle height in the selected canvas coordinate space.'),
686
+ rotation: zod_1.z.number().finite().optional().describe('Optional visual rectangle rotation for this canvas. Backend defaults to 0 when omitted.'),
687
+ placementSpace: PlacementSpaceSchema.optional(),
688
+ basisImageWidthPx: zod_1.z.number().finite().positive().optional().describe('Optional positive width of the source image or blank canvas basis used for this placement.'),
689
+ basisImageHeightPx: zod_1.z.number().finite().positive().optional().describe('Optional positive height of the source image or blank canvas basis used for this placement.'),
690
+ source: PlacementSourceSchema.optional(),
691
+ viewAssociationEvidence: PlacementViewAssociationEvidenceSchema.optional().describe('Optional plan/elevation association trace metadata. It is not sent to the backend placement DTO.'),
692
+ }).strict().describe('One AIDrawingTool per-canvas cabinet placement rectangle. ' + AI_CANVAS_PLACEMENT_GUIDANCE);
693
+ exports.AiCanvasCabinetPlacementPatchSchema = zod_1.z.object({
694
+ aiCartItemId: zod_1.z.string().uuid().optional().describe('Optional backend AI cart item UUID association update. Required for unlinked legacy placements unless positionName exactly matches an active AI cart item.'),
695
+ positionName: zod_1.z.string().min(1).describe('Required per-canvas placement identity to patch.'),
696
+ centerX: zod_1.z.number().finite().optional().describe('Optional placement center X update in the selected canvas coordinate space.'),
697
+ centerY: zod_1.z.number().finite().optional().describe('Optional placement center Y update in the selected canvas coordinate space.'),
698
+ width: zod_1.z.number().finite().positive().optional().describe('Optional positive visual rectangle width update.'),
699
+ height: zod_1.z.number().finite().positive().optional().describe('Optional positive visual rectangle height update.'),
700
+ rotation: zod_1.z.number().finite().optional().describe('Optional visual rectangle rotation update for this canvas.'),
701
+ placementSpace: PlacementSpaceSchema.optional(),
702
+ basisImageWidthPx: zod_1.z.number().finite().positive().optional().describe('Optional positive source image or blank canvas basis width update.'),
703
+ basisImageHeightPx: zod_1.z.number().finite().positive().optional().describe('Optional positive source image or blank canvas basis height update.'),
704
+ source: PlacementSourceSchema.optional(),
705
+ viewAssociationEvidence: PlacementViewAssociationEvidenceSchema.optional().describe('Optional plan/elevation association trace metadata for placement geometry patches. It is not sent to the backend placement DTO.'),
706
+ }).strict().describe('Patch input for one AIDrawingTool per-canvas cabinet placement rectangle. ' + AI_CANVAS_PLACEMENT_GUIDANCE);
707
+ exports.AiCanvasCabinetPlacementsReplaceInputSchema = AiDrawingSessionIdParamSchema.extend({
708
+ canvasType: CanvasTypeSchema,
709
+ placements: zod_1.z.array(exports.AiCanvasCabinetPlacementInputSchema).min(1).describe('Nonempty replacement placements for exactly this canvas. This must not replace placements on any other canvas. Use delete_ai_canvas_cabinet_placements for selected removals.'),
710
+ }).strict().superRefine((input, ctx) => validatePlacementOperationForCanvas(input.canvasType, input.placements, ctx)).describe('Replace one selected canvas placement set for backend-owned AIDrawingTool. ' + AI_CANVAS_PLACEMENT_GUIDANCE + ' ' + AI_CANVAS_PLACEMENT_ROTATION_GUIDANCE);
711
+ exports.AiCanvasCabinetPlacementsUpsertInputSchema = AiDrawingSessionIdParamSchema.extend({
712
+ canvasType: CanvasTypeSchema,
713
+ placements: zod_1.z.array(exports.AiCanvasCabinetPlacementInputSchema).min(1).describe('Nonempty create-or-update placements for exactly this canvas, keyed by positionName. This must not replace or delete placements omitted from the request.'),
714
+ }).strict().superRefine((input, ctx) => validatePlacementOperationForCanvas(input.canvasType, input.placements, ctx)).describe('Upsert selected placement rectangles for one backend-owned AIDrawingTool canvas. ' + AI_CANVAS_PLACEMENT_GUIDANCE + ' ' + AI_CANVAS_PLACEMENT_ROTATION_GUIDANCE);
715
+ exports.AiCanvasCabinetPlacementsByCanvasInputSchema = AiDrawingSessionIdParamSchema.extend({
716
+ canvasType: CanvasTypeSchema,
717
+ }).strict().describe('Read placement rectangles for one backend-owned AIDrawingTool canvas. ' + AI_CANVAS_PLACEMENT_GUIDANCE);
718
+ exports.AiCanvasCabinetPlacementsPatchInputSchema = AiDrawingSessionIdParamSchema.extend({
719
+ canvasType: CanvasTypeSchema,
720
+ placements: zod_1.z.array(exports.AiCanvasCabinetPlacementPatchSchema).min(1).describe('Patch operations for exactly this canvas, keyed by positionName.'),
721
+ }).strict().superRefine((input, ctx) => validatePlacementOperationForCanvas(input.canvasType, input.placements, ctx)).describe('Patch selected placements for one backend-owned AIDrawingTool canvas. ' + AI_CANVAS_PLACEMENT_GUIDANCE + ' ' + AI_CANVAS_PLACEMENT_ROTATION_GUIDANCE);
722
+ exports.AiCanvasCabinetPlacementsDeleteInputSchema = AiDrawingSessionIdParamSchema.extend({
723
+ canvasType: CanvasTypeSchema,
724
+ positionNames: zod_1.z.array(zod_1.z.string().min(1)).min(1).describe('Nonempty list of per-canvas placement positionName values to delete from exactly this canvas.'),
725
+ }).strict().describe('Delete selected placements from one backend-owned AIDrawingTool canvas. ' + AI_CANVAS_PLACEMENT_GUIDANCE);
726
+ const AiCoordinateDerivationEvidenceSchema = zod_1.z.object({
727
+ basis: zod_1.z.enum(['validated_plan_placement', 'user_confirmed']).describe('How this canonical x/y/z coordinate was derived. MCP must not write canonical coordinates from guessed elevation rectangles.'),
728
+ sourceCanvasType: zod_1.z.literal('plan').describe('Canonical x/y coordinates must be derived from validated plan placement geometry, not elevation-local pixels.'),
729
+ planPlacementPositionName: zod_1.z.string().min(1).optional().describe('Position name of the validated plan placement used to derive this coordinate.'),
730
+ planPlacementEvidence: PlacementViewAssociationEvidenceSchema.optional().describe('Plan placement evidence used for the coordinate derivation. Required unless basis is explicit user_confirmed.'),
731
+ userExplicitConfirmation: zod_1.z.boolean().optional(),
732
+ userConfirmationText: zod_1.z.string().min(20).optional(),
733
+ evidence: zod_1.z.string().min(20),
734
+ }).strict().superRefine((value, ctx) => {
735
+ if (value.basis === 'validated_plan_placement') {
736
+ if (!value.planPlacementPositionName || !value.planPlacementEvidence) {
737
+ ctx.addIssue({
738
+ code: zod_1.z.ZodIssueCode.custom,
739
+ path: ['planPlacementEvidence'],
740
+ message: 'validated_plan_placement coordinate writes require planPlacementPositionName and planPlacementEvidence.',
741
+ });
742
+ }
743
+ }
744
+ if (value.basis === 'user_confirmed') {
745
+ const confirmation = `${value.userConfirmationText ?? ''} ${value.evidence}`.toLowerCase();
746
+ if (!value.userExplicitConfirmation || !value.userConfirmationText) {
747
+ ctx.addIssue({
748
+ code: zod_1.z.ZodIssueCode.custom,
749
+ path: ['userConfirmationText'],
750
+ message: 'user_confirmed coordinate writes require explicit user confirmation text for the exact coordinate/placement.',
751
+ });
752
+ }
753
+ if (/\b(provided|uploaded|supplied)\b.*\b(drawings?|images?)\b|\basked\b.*\b(verify|analy[sz]e)\b/.test(confirmation)) {
754
+ ctx.addIssue({
755
+ code: zod_1.z.ZodIssueCode.custom,
756
+ path: ['userConfirmationText'],
757
+ message: 'Generic statements that the user supplied drawings or asked the agent to verify are not coordinate confirmation.',
758
+ });
759
+ }
760
+ }
761
+ });
762
+ function validatePlacementRotationsForCanvas(canvasType, placements, ctx) {
763
+ if (canvasType === 'plan') {
764
+ return;
765
+ }
766
+ placements.forEach((placement, index) => {
767
+ if (placement.rotation !== undefined && placement.rotation !== 0) {
768
+ ctx.addIssue({
769
+ code: zod_1.z.ZodIssueCode.custom,
770
+ path: ['placements', index, 'rotation'],
771
+ message: AI_CANVAS_PLACEMENT_ROTATION_GUIDANCE,
772
+ });
773
+ }
774
+ });
775
+ }
776
+ function validatePlacementOperationForCanvas(canvasType, placements, ctx) {
777
+ validatePlacementRotationsForCanvas(canvasType, placements, ctx);
778
+ }
779
+ exports.AiCabinetCoordinateInputSchema = zod_1.z.object({
780
+ positionName: zod_1.z.string().min(1).describe('Required AI drawing coordinate identity. It is unique within an AI drawing session.'),
781
+ aiCartItemId: zod_1.z.string().uuid().optional().describe('Optional canonical backend AI cart item UUID associated with this coordinate.'),
782
+ x: zod_1.z.number().finite().describe('Canonical cabinet center X in room/global plan coordinates, in inches. Never use image-local pixels or elevation-local X here.'),
783
+ y: zod_1.z.number().finite().describe('Canonical cabinet center Y in room/global plan coordinates, in inches. Never use image-local pixels or elevation-local horizontal coordinates here.'),
784
+ z: zod_1.z.number().finite().default(0).describe('Canonical height from finished floor to cabinet bottom, in inches.'),
785
+ rotation: CoordinateRotationSchema.default(0),
786
+ source: CoordinateSourceSchema.optional(),
787
+ coordinateEvidence: AiCoordinateDerivationEvidenceSchema.optional().describe('Optional canonical coordinate trace metadata.'),
788
+ }).strict().describe('AI cabinet coordinate input using canonical room/global inches for the AIDrawingTool coordinate contract. ' +
789
+ AI_CANONICAL_PROJECTION_GUIDANCE);
790
+ exports.AiCabinetCoordinatePatchInputSchema = zod_1.z.object({
791
+ positionName: zod_1.z.string().min(1).describe('Required AI drawing coordinate identity to patch.'),
792
+ aiCartItemId: zod_1.z.string().uuid().optional().describe('Optional canonical backend AI cart item UUID association update.'),
793
+ x: zod_1.z.number().finite().optional().describe('Optional canonical cabinet center X update in room/global plan coordinates, in inches. Never use image-local pixels or elevation-local X.'),
794
+ y: zod_1.z.number().finite().optional().describe('Optional canonical cabinet center Y update in room/global plan coordinates, in inches. Never use image-local pixels or elevation-local horizontal coordinates.'),
795
+ z: zod_1.z.number().finite().optional().describe('Optional canonical finished-floor-to-cabinet-bottom height update, in inches.'),
796
+ rotation: CoordinateRotationSchema.optional(),
797
+ source: CoordinateSourceSchema.optional(),
798
+ coordinateEvidence: AiCoordinateDerivationEvidenceSchema.optional().describe('Optional canonical coordinate trace metadata for coordinate patches.'),
799
+ }).strict().describe('AI cabinet coordinate patch input. Unknown fields are rejected so tools cannot accidentally mutate unsupported state. ' +
800
+ AI_CANONICAL_PROJECTION_GUIDANCE);
801
+ exports.AiCabinetCoordinatesReplaceInputSchema = AiDrawingSessionIdParamSchema.extend({
802
+ coordinates: zod_1.z.array(exports.AiCabinetCoordinateInputSchema).default([]).describe('Full replacement AI coordinate list for the session.'),
803
+ }).strict();
804
+ exports.AiCabinetCoordinatesPatchInputListSchema = AiDrawingSessionIdParamSchema.extend({
805
+ coordinates: zod_1.z.array(exports.AiCabinetCoordinatePatchInputSchema).min(1).describe('AI coordinate patch operations keyed by positionName.'),
806
+ }).strict();
807
+ exports.AiCabinetCoordinatesDeleteInputSchema = AiDrawingSessionIdParamSchema.extend({
808
+ positionNames: zod_1.z.array(zod_1.z.string().min(1)).min(1).describe('AI coordinate position names to delete.'),
809
+ }).strict();
810
+ // ---------------------------------------------------------------------------
811
+ // Handlers
812
+ // ---------------------------------------------------------------------------
813
+ async function getAiDrawingState(input) {
814
+ try {
815
+ const { data } = await api_client_1.aiDrawingClient.get(`/sessions/${input.sessionId}/state`);
816
+ return JSON.stringify(data, null, 2);
817
+ }
818
+ catch (error) {
819
+ try {
820
+ (0, api_client_1.handleAxiosError)(error);
821
+ }
822
+ catch (e) {
823
+ return e.message;
824
+ }
825
+ return 'Unexpected error fetching AI drawing state.';
826
+ }
827
+ }
828
+ async function renderAiDrawingSessionReviewTool(input) {
829
+ try {
830
+ const { data } = await api_client_1.aiDrawingClient.get(`/sessions/${input.sessionId}/state`);
831
+ const imagePathByCanvas = await materializeReviewBackgroundImages(data, input.outputDir);
832
+ const renderOptions = {
833
+ outputDir: input.outputDir,
834
+ canvases: input.canvasTypes,
835
+ writeJsonManifest: input.includeManifest ?? false,
836
+ };
837
+ if (Object.keys(imagePathByCanvas).length > 0) {
838
+ renderOptions.imagePathByCanvas = imagePathByCanvas;
839
+ }
840
+ const manifest = (0, ai_drawing_session_review_renderer_1.renderAiDrawingSessionReview)(data, renderOptions);
841
+ const sourceBoundaryReviewTasks = manifest.canvases.flatMap((canvas) => canvas.sourceBoundaryReviewTasks);
842
+ const reviewImages = manifest.canvases
843
+ .filter((canvas) => Boolean(canvas.outputImagePath))
844
+ .map((canvas) => ({
845
+ canvasType: canvas.canvasType,
846
+ outputImagePath: canvas.outputImagePath,
847
+ imageWidth: canvas.imageWidth,
848
+ imageHeight: canvas.imageHeight,
849
+ renderedPlacementCount: canvas.renderedPlacementCount,
850
+ sourceBoundaryReviewTaskCount: canvas.sourceBoundaryReviewTasks.length,
851
+ issueCount: canvas.issues.length,
852
+ }));
853
+ return JSON.stringify({
854
+ sessionId: input.sessionId,
855
+ outputDir: input.outputDir,
856
+ visualReviewRequired: true,
857
+ visualReviewStatus: manifest.visualReviewStatus,
858
+ completionBlockedUntil: manifest.completionBlockedUntil,
859
+ machineValidationScope: 'Renderer issues include deterministic structural checks plus blocking source_boundary_review_required tasks. aggregateIssueCount 0 is not visual approval and does not prove cabinet boundaries are correct. sourceBoundaryReviewTasks must be checked against the full source image with native vision.',
860
+ completionPolicy: 'Open every reviewImages[].outputImagePath with native vision. Resolve every source_boundary_review_required issue by completing every sourceBoundaryReviewTasks[] item against the visible source drawing pixels, not generated bbox relationships. Record PASS/FAIL/UNCERTAIN for each canvas and placement. PASS requires explicit LEFT/RIGHT/TOP/BOTTOM source-linework evidence using each task passRequiresEvidenceFormat; if any side cannot be named from visible drawing geometry, mark FAIL or UNCERTAIN. Fix any overlap, shifted box, missing placement, wrong object, wrong canvas, wrong-surface placement, wrong plan footprint, wrong cabinet face, wrong plinth/toekick, wrong floor/Z0 boundary, or wrong countertop boundary, rerender, and only then report completion or document unresolved issues.',
861
+ visualQaChecklist: manifest.visualInspectionChecklist,
862
+ reviewImages,
863
+ sourceBoundaryReviewTasks,
864
+ canvases: manifest.canvases.map((canvas) => ({
865
+ canvasType: canvas.canvasType,
866
+ outputImagePath: canvas.outputImagePath,
867
+ imageWidth: canvas.imageWidth,
868
+ imageHeight: canvas.imageHeight,
869
+ renderedPlacementCount: canvas.renderedPlacementCount,
870
+ sourceBoundaryReviewTasks: canvas.sourceBoundaryReviewTasks,
871
+ issues: canvas.issues,
872
+ })),
873
+ perCanvasIssues: Object.fromEntries(manifest.canvases.map((canvas) => [canvas.canvasType, canvas.issues])),
874
+ aggregateIssueCount: manifest.issues.length,
875
+ issues: manifest.issues,
876
+ manifestPath: manifest.manifestPath,
877
+ }, null, 2);
878
+ }
879
+ catch (error) {
880
+ try {
881
+ (0, api_client_1.handleAxiosError)(error);
882
+ }
883
+ catch (e) {
884
+ return e.message;
885
+ }
886
+ return `Unexpected error rendering AI drawing session review: ${formatUnknownError(error)}`;
887
+ }
888
+ }
889
+ async function materializeReviewBackgroundImages(state, outputDir) {
890
+ const stateRecord = isRecord(state) ? state : {};
891
+ const images = Array.isArray(stateRecord.canvasImages) ? stateRecord.canvasImages : [];
892
+ const paths = {};
893
+ for (const image of images) {
894
+ if (!isRecord(image)) {
895
+ continue;
896
+ }
897
+ const canvasType = typeof image.canvasType === 'string' && isAiDrawingReviewCanvasType(image.canvasType)
898
+ ? image.canvasType
899
+ : null;
900
+ const imageType = typeof image.imageType === 'string' ? image.imageType.toLowerCase() : '';
901
+ const reference = stringifyReviewImageReference(image.imagePath ?? image.path ?? image.s3Key);
902
+ if (!canvasType || imageType !== 'background' || !reference) {
903
+ continue;
904
+ }
905
+ if (isReadableLocalPng(reference)) {
906
+ paths[canvasType] = normalizeFileSchemePath(reference);
907
+ continue;
908
+ }
909
+ const remoteUrl = resolveReviewBackgroundUrl(reference);
910
+ if (!remoteUrl || path.extname(new URL(remoteUrl).pathname).toLowerCase() !== '.png') {
911
+ continue;
912
+ }
913
+ const downloadedPath = await downloadReviewBackground(remoteUrl, outputDir, canvasType);
914
+ if (downloadedPath) {
915
+ paths[canvasType] = downloadedPath;
916
+ }
917
+ }
918
+ return paths;
919
+ }
920
+ async function downloadReviewBackground(remoteUrl, outputDir, canvasType) {
921
+ try {
922
+ fs.mkdirSync(outputDir, { recursive: true });
923
+ const response = await fetch(remoteUrl);
924
+ if (!response.ok) {
925
+ return undefined;
926
+ }
927
+ const contentType = response.headers.get('content-type') ?? '';
928
+ if (contentType && !contentType.toLowerCase().includes('image/png')) {
929
+ return undefined;
930
+ }
931
+ const bytes = Buffer.from(await response.arrayBuffer());
932
+ if (!isPngBuffer(bytes)) {
933
+ return undefined;
934
+ }
935
+ const outputPath = path.join(outputDir, `ai-drawing-session-review-background-${canvasType}.png`);
936
+ fs.writeFileSync(outputPath, bytes);
937
+ return outputPath;
938
+ }
939
+ catch {
940
+ return undefined;
941
+ }
942
+ }
943
+ function resolveReviewBackgroundUrl(reference) {
944
+ if (/^https?:\/\//i.test(reference)) {
945
+ return reference;
946
+ }
947
+ if (/^(data|blob):/i.test(reference) || path.isAbsolute(normalizeFileSchemePath(reference))) {
948
+ return undefined;
949
+ }
950
+ const normalizedReference = reference.replace(/^\/+/, '');
951
+ return `${AI_DRAWING_REVIEW_ASSET_BASE_URL}/${normalizedReference}`;
952
+ }
953
+ function isReadableLocalPng(reference) {
954
+ const localPath = normalizeFileSchemePath(reference);
955
+ return path.isAbsolute(localPath) && path.extname(localPath).toLowerCase() === '.png' && fs.existsSync(localPath);
956
+ }
957
+ function normalizeFileSchemePath(reference) {
958
+ return reference.startsWith('file://')
959
+ ? decodeURIComponent(reference.slice('file://'.length))
960
+ : reference;
961
+ }
962
+ function stringifyReviewImageReference(value) {
963
+ if (value === null || value === undefined) {
964
+ return undefined;
965
+ }
966
+ const text = String(value).trim();
967
+ return text.length > 0 ? text : undefined;
968
+ }
969
+ function isAiDrawingReviewCanvasType(value) {
970
+ return ['plan', 'north', 'south', 'east', 'west'].includes(value);
971
+ }
972
+ function isRecord(value) {
973
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
974
+ }
975
+ function isPngBuffer(bytes) {
976
+ return bytes.length >= 8 &&
977
+ bytes[0] === 0x89 &&
978
+ bytes[1] === 0x50 &&
979
+ bytes[2] === 0x4e &&
980
+ bytes[3] === 0x47 &&
981
+ bytes[4] === 0x0d &&
982
+ bytes[5] === 0x0a &&
983
+ bytes[6] === 0x1a &&
984
+ bytes[7] === 0x0a;
985
+ }
986
+ async function createAiDrawingSession(input) {
987
+ try {
988
+ const { data } = await api_client_1.aiDrawingClient.post('/sessions', input);
989
+ return JSON.stringify(data, null, 2);
990
+ }
991
+ catch (error) {
992
+ try {
993
+ (0, api_client_1.handleAxiosError)(error);
994
+ }
995
+ catch (e) {
996
+ return e.message;
997
+ }
998
+ return 'Unexpected error creating AI drawing session.';
999
+ }
1000
+ }
1001
+ async function linkAiCartToDrawingSession(input) {
1002
+ try {
1003
+ const { data } = await api_client_1.aiDrawingClient.put(`/sessions/${input.sessionId}/active-cart/${input.cartId}`);
1004
+ return JSON.stringify(data, null, 2);
1005
+ }
1006
+ catch (error) {
1007
+ try {
1008
+ (0, api_client_1.handleAxiosError)(error);
1009
+ }
1010
+ catch (e) {
1011
+ return e.message;
1012
+ }
1013
+ return 'Unexpected error linking AI cart to AI drawing session.';
1014
+ }
1015
+ }
1016
+ async function upsertAiCanvasMetadata(input) {
1017
+ try {
1018
+ const { data } = await api_client_1.aiDrawingClient.put(`/sessions/${input.sessionId}/metadata/${input.canvasType}`, removeUndefined({
1019
+ canvasType: input.canvasType,
1020
+ canvasMeta: input.canvasMeta,
1021
+ z0Reference: input.z0Reference,
1022
+ verticalCalibration: input.verticalCalibration,
1023
+ calibrationOffsets: input.calibrationOffsets,
1024
+ elevationVisibility: input.elevationVisibility,
1025
+ fixedElevationPositions: input.fixedElevationPositions,
1026
+ elevationCalibrated: input.elevationCalibrated,
1027
+ globalElevationOffsets: input.globalElevationOffsets,
1028
+ calibratedCabinets: input.calibratedCabinets,
1029
+ activeCanvases: input.activeCanvases,
1030
+ }));
1031
+ return JSON.stringify(data, null, 2);
1032
+ }
1033
+ catch (error) {
1034
+ try {
1035
+ (0, api_client_1.handleAxiosError)(error);
1036
+ }
1037
+ catch (e) {
1038
+ return e.message;
1039
+ }
1040
+ return 'Unexpected error upserting AI canvas metadata.';
1041
+ }
1042
+ }
1043
+ async function upsertAiCanvasImage(input) {
1044
+ try {
1045
+ const { data } = await api_client_1.aiDrawingClient.put(`/sessions/${input.sessionId}/images/${input.canvasType}/${input.imageType}`, {
1046
+ canvasType: input.canvasType,
1047
+ imageType: input.imageType,
1048
+ s3Key: input.s3Key,
1049
+ });
1050
+ return JSON.stringify(data, null, 2);
1051
+ }
1052
+ catch (error) {
1053
+ try {
1054
+ (0, api_client_1.handleAxiosError)(error);
1055
+ }
1056
+ catch (e) {
1057
+ return e.message;
1058
+ }
1059
+ return 'Unexpected error upserting AI canvas image.';
1060
+ }
1061
+ }
1062
+ async function uploadAiCanvasImage(input) {
1063
+ const extension = path.extname(input.imagePath).toLowerCase();
1064
+ const mimeType = IMAGE_MIME_TYPES[extension];
1065
+ if (!mimeType) {
1066
+ return `Unsupported file extension "${extension}". Supported: .png, .jpg, .jpeg, .webp`;
1067
+ }
1068
+ if (!fs.existsSync(input.imagePath)) {
1069
+ return `File not found at "${input.imagePath}"`;
1070
+ }
1071
+ const form = new form_data_1.default();
1072
+ form.append('file', fs.createReadStream(input.imagePath), {
1073
+ filename: path.basename(input.imagePath),
1074
+ contentType: mimeType,
1075
+ });
1076
+ try {
1077
+ const { data } = await api_client_1.aiDrawingClient.post(`/sessions/${input.sessionId}/images/${input.canvasType}/${input.imageType}/upload`, form, { headers: form.getHeaders() });
1078
+ return JSON.stringify(data, null, 2);
1079
+ }
1080
+ catch (error) {
1081
+ try {
1082
+ (0, api_client_1.handleAxiosError)(error);
1083
+ }
1084
+ catch (e) {
1085
+ return e.message;
1086
+ }
1087
+ return 'Unexpected error uploading AI canvas image.';
1088
+ }
1089
+ }
1090
+ async function deleteAiCanvasImage(input) {
1091
+ try {
1092
+ const { data } = await api_client_1.aiDrawingClient.delete(`/sessions/${input.sessionId}/images/${input.canvasType}/${input.imageType}`);
1093
+ return JSON.stringify(data, null, 2);
1094
+ }
1095
+ catch (error) {
1096
+ try {
1097
+ (0, api_client_1.handleAxiosError)(error);
1098
+ }
1099
+ catch (e) {
1100
+ return e.message;
1101
+ }
1102
+ return 'Unexpected error deleting AI canvas image.';
1103
+ }
1104
+ }
1105
+ async function getAiCanvasCabinetPlacements(input) {
1106
+ try {
1107
+ const { data } = await api_client_1.aiDrawingClient.get(`/sessions/${input.sessionId}/placements`);
1108
+ return JSON.stringify(data, null, 2);
1109
+ }
1110
+ catch (error) {
1111
+ try {
1112
+ (0, api_client_1.handleAxiosError)(error);
1113
+ }
1114
+ catch (e) {
1115
+ return e.message;
1116
+ }
1117
+ return 'Unexpected error fetching AI canvas cabinet placements.';
1118
+ }
1119
+ }
1120
+ async function getAiCanvasCabinetPlacementsByCanvas(input) {
1121
+ try {
1122
+ const { data } = await api_client_1.aiDrawingClient.get(`/sessions/${input.sessionId}/placements/${input.canvasType}`);
1123
+ return JSON.stringify(data, null, 2);
1124
+ }
1125
+ catch (error) {
1126
+ try {
1127
+ (0, api_client_1.handleAxiosError)(error);
1128
+ }
1129
+ catch (e) {
1130
+ return e.message;
1131
+ }
1132
+ return 'Unexpected error fetching AI canvas cabinet placements by canvas.';
1133
+ }
1134
+ }
1135
+ async function replaceAiCanvasCabinetPlacements(input) {
1136
+ try {
1137
+ const { data } = await api_client_1.aiDrawingClient.put(`/sessions/${input.sessionId}/placements/${input.canvasType}`, {
1138
+ placements: input.placements.map(toBackendPlacementPayload),
1139
+ });
1140
+ return JSON.stringify(data, null, 2);
1141
+ }
1142
+ catch (error) {
1143
+ try {
1144
+ (0, api_client_1.handleAxiosError)(error);
1145
+ }
1146
+ catch (e) {
1147
+ return e.message;
1148
+ }
1149
+ return 'Unexpected error replacing AI canvas cabinet placements.';
1150
+ }
1151
+ }
1152
+ async function upsertAiCanvasCabinetPlacements(input) {
1153
+ try {
1154
+ const { data } = await api_client_1.aiDrawingClient.post(`/sessions/${input.sessionId}/placements/${input.canvasType}`, {
1155
+ placements: input.placements.map(toBackendPlacementPayload),
1156
+ });
1157
+ return JSON.stringify(data, null, 2);
1158
+ }
1159
+ catch (error) {
1160
+ try {
1161
+ (0, api_client_1.handleAxiosError)(error);
1162
+ }
1163
+ catch (e) {
1164
+ return e.message;
1165
+ }
1166
+ return 'Unexpected error upserting AI canvas cabinet placements.';
1167
+ }
1168
+ }
1169
+ async function patchAiCanvasCabinetPlacements(input) {
1170
+ try {
1171
+ const { data } = await api_client_1.aiDrawingClient.patch(`/sessions/${input.sessionId}/placements/${input.canvasType}`, {
1172
+ placements: input.placements.map(toBackendPlacementPatchPayload),
1173
+ });
1174
+ return JSON.stringify(data, null, 2);
1175
+ }
1176
+ catch (error) {
1177
+ try {
1178
+ (0, api_client_1.handleAxiosError)(error);
1179
+ }
1180
+ catch (e) {
1181
+ return e.message;
1182
+ }
1183
+ return 'Unexpected error patching AI canvas cabinet placements.';
1184
+ }
1185
+ }
1186
+ async function deleteAiCanvasCabinetPlacements(input) {
1187
+ try {
1188
+ const { data } = await api_client_1.aiDrawingClient.post(`/sessions/${input.sessionId}/placements/${input.canvasType}/delete`, {
1189
+ positionNames: input.positionNames,
1190
+ });
1191
+ return JSON.stringify(data, null, 2);
1192
+ }
1193
+ catch (error) {
1194
+ try {
1195
+ (0, api_client_1.handleAxiosError)(error);
1196
+ }
1197
+ catch (e) {
1198
+ return e.message;
1199
+ }
1200
+ return 'Unexpected error deleting AI canvas cabinet placements.';
1201
+ }
1202
+ }
1203
+ async function replaceAiCabinetCoordinates(input) {
1204
+ try {
1205
+ const { data } = await api_client_1.aiDrawingClient.put(`/sessions/${input.sessionId}/coordinates`, {
1206
+ coordinates: input.coordinates.map(toBackendCoordinatePayload),
1207
+ });
1208
+ return JSON.stringify(data, null, 2);
1209
+ }
1210
+ catch (error) {
1211
+ try {
1212
+ (0, api_client_1.handleAxiosError)(error);
1213
+ }
1214
+ catch (e) {
1215
+ return e.message;
1216
+ }
1217
+ return 'Unexpected error replacing AI cabinet coordinates.';
1218
+ }
1219
+ }
1220
+ async function patchAiCabinetCoordinates(input) {
1221
+ try {
1222
+ const { data } = await api_client_1.aiDrawingClient.patch(`/sessions/${input.sessionId}/coordinates`, {
1223
+ coordinates: input.coordinates.map(toBackendCoordinatePatchPayload),
1224
+ });
1225
+ return JSON.stringify(data, null, 2);
1226
+ }
1227
+ catch (error) {
1228
+ try {
1229
+ (0, api_client_1.handleAxiosError)(error);
1230
+ }
1231
+ catch (e) {
1232
+ return e.message;
1233
+ }
1234
+ return 'Unexpected error patching AI cabinet coordinates.';
1235
+ }
1236
+ }
1237
+ async function deleteAiCabinetCoordinates(input) {
1238
+ try {
1239
+ const { data } = await api_client_1.aiDrawingClient.post(`/sessions/${input.sessionId}/coordinates/delete`, {
1240
+ positionNames: input.positionNames,
1241
+ });
1242
+ return JSON.stringify(data, null, 2);
1243
+ }
1244
+ catch (error) {
1245
+ try {
1246
+ (0, api_client_1.handleAxiosError)(error);
1247
+ }
1248
+ catch (e) {
1249
+ return e.message;
1250
+ }
1251
+ return 'Unexpected error deleting AI cabinet coordinates.';
1252
+ }
1253
+ }
1254
+ async function createAiCart(input) {
1255
+ const parsedInput = exports.AiCartCreateInputSchema.safeParse(input);
1256
+ if (!parsedInput.success) {
1257
+ return `Invalid input: ${parsedInput.error.message}`;
1258
+ }
1259
+ try {
1260
+ const { orderConfigurationWorkflow, ...backendInput } = parsedInput.data;
1261
+ const { data } = await api_client_1.aiDrawingClient.post('/carts', backendInput);
1262
+ return JSON.stringify(data, null, 2);
1263
+ }
1264
+ catch (error) {
1265
+ try {
1266
+ (0, api_client_1.handleAxiosError)(error);
1267
+ }
1268
+ catch (e) {
1269
+ return e.message;
1270
+ }
1271
+ return 'Unexpected error creating AI cart.';
1272
+ }
1273
+ }
1274
+ async function getAiCart(input) {
1275
+ try {
1276
+ const { data } = await api_client_1.aiDrawingClient.get(`/carts/${input.cartId}`);
1277
+ return JSON.stringify(data, null, 2);
1278
+ }
1279
+ catch (error) {
1280
+ try {
1281
+ (0, api_client_1.handleAxiosError)(error);
1282
+ }
1283
+ catch (e) {
1284
+ return e.message;
1285
+ }
1286
+ return 'Unexpected error fetching AI cart.';
1287
+ }
1288
+ }
1289
+ async function getAiCartBySession(input) {
1290
+ try {
1291
+ const { data } = await api_client_1.aiDrawingClient.get(`/carts/session/${input.sessionId}`);
1292
+ return JSON.stringify(data, null, 2);
1293
+ }
1294
+ catch (error) {
1295
+ try {
1296
+ (0, api_client_1.handleAxiosError)(error);
1297
+ }
1298
+ catch (e) {
1299
+ return e.message;
1300
+ }
1301
+ return 'Unexpected error fetching AI cart by session.';
1302
+ }
1303
+ }
1304
+ async function syncAiCartSnapshot(input) {
1305
+ const parsedInput = exports.AiCartSnapshotInputSchema.safeParse(input);
1306
+ if (!parsedInput.success) {
1307
+ return `Invalid input: ${parsedInput.error.message}`;
1308
+ }
1309
+ try {
1310
+ const { data } = await api_client_1.aiDrawingClient.put(`/carts/${parsedInput.data.cartId}/snapshot`, {
1311
+ sourceSnapshotPayload: parsedInput.data.sourceSnapshotPayload,
1312
+ items: parsedInput.data.items.map(toBackendCartItemPayload),
1313
+ });
1314
+ return JSON.stringify(data, null, 2);
1315
+ }
1316
+ catch (error) {
1317
+ try {
1318
+ (0, api_client_1.handleAxiosError)(error);
1319
+ }
1320
+ catch (e) {
1321
+ return e.message;
1322
+ }
1323
+ return 'Unexpected error syncing AI cart snapshot.';
1324
+ }
1325
+ }
1326
+ async function patchAiCartItem(input) {
1327
+ try {
1328
+ const { data } = await api_client_1.aiDrawingClient.patch(`/carts/${input.cartId}/items/${input.itemId}`, toBackendCartItemPatchPayload(input.patch));
1329
+ return JSON.stringify(data, null, 2);
1330
+ }
1331
+ catch (error) {
1332
+ try {
1333
+ (0, api_client_1.handleAxiosError)(error);
1334
+ }
1335
+ catch (e) {
1336
+ return e.message;
1337
+ }
1338
+ return 'Unexpected error patching AI cart item.';
1339
+ }
1340
+ }
1341
+ async function deleteAiCartItem(input) {
1342
+ try {
1343
+ const { data } = await api_client_1.aiDrawingClient.delete(`/carts/${input.cartId}/items/${input.itemId}`);
1344
+ return JSON.stringify(data, null, 2);
1345
+ }
1346
+ catch (error) {
1347
+ try {
1348
+ (0, api_client_1.handleAxiosError)(error);
1349
+ }
1350
+ catch (e) {
1351
+ return e.message;
1352
+ }
1353
+ return 'Unexpected error deleting AI cart item.';
1354
+ }
1355
+ }
1356
+ async function duplicateAiCabinet(input) {
1357
+ try {
1358
+ const { data } = await api_client_1.aiDrawingClient.post(`/sessions/${input.sessionId}/cart-items/${input.itemId}/duplicate`, toBackendDuplicateCabinetPayload(input));
1359
+ return JSON.stringify(data, null, 2);
1360
+ }
1361
+ catch (error) {
1362
+ try {
1363
+ (0, api_client_1.handleAxiosError)(error);
1364
+ }
1365
+ catch (e) {
1366
+ return e.message;
1367
+ }
1368
+ return 'Unexpected error duplicating AI cabinet.';
1369
+ }
1370
+ }
1371
+ async function configureAiCabinet(input) {
1372
+ try {
1373
+ const { data } = await api_client_1.aiDrawingClient.patch(`/sessions/${input.sessionId}/cart-items/${input.itemId}/configure`, toBackendConfigureCabinetPayload(input));
1374
+ return JSON.stringify(data, null, 2);
1375
+ }
1376
+ catch (error) {
1377
+ try {
1378
+ (0, api_client_1.handleAxiosError)(error);
1379
+ }
1380
+ catch (e) {
1381
+ return e.message;
1382
+ }
1383
+ return 'Unexpected error configuring AI cabinet.';
1384
+ }
1385
+ }
1386
+ async function deleteAiCabinet(input) {
1387
+ try {
1388
+ const { data } = await api_client_1.aiDrawingClient.delete(`/sessions/${input.sessionId}/cart-items/${input.itemId}`);
1389
+ return JSON.stringify(data, null, 2);
1390
+ }
1391
+ catch (error) {
1392
+ try {
1393
+ (0, api_client_1.handleAxiosError)(error);
1394
+ }
1395
+ catch (e) {
1396
+ return e.message;
1397
+ }
1398
+ return 'Unexpected error deleting AI cabinet.';
1399
+ }
1400
+ }
1401
+ async function getAiCartItemOrderReadyPayload(input) {
1402
+ try {
1403
+ const { data } = await api_client_1.aiDrawingClient.get(`/sessions/${input.sessionId}/cart-items/${input.itemId}/order-ready-payload`);
1404
+ return JSON.stringify(data, null, 2);
1405
+ }
1406
+ catch (error) {
1407
+ try {
1408
+ (0, api_client_1.handleAxiosError)(error);
1409
+ }
1410
+ catch (e) {
1411
+ return e.message;
1412
+ }
1413
+ return 'Unexpected error fetching AI cart item order-ready payload.';
1414
+ }
1415
+ }
1416
+ async function setAiCartItemOrderReadyPayload(input) {
1417
+ const validation = validateOrderReadyPayloadInput(input);
1418
+ if (!validation.success) {
1419
+ return validation.message;
1420
+ }
1421
+ try {
1422
+ const { data } = await api_client_1.aiDrawingClient.put(`/sessions/${input.sessionId}/cart-items/${input.itemId}/order-ready-payload`, toBackendOrderReadyPayload(input));
1423
+ return JSON.stringify(data, null, 2);
1424
+ }
1425
+ catch (error) {
1426
+ try {
1427
+ (0, api_client_1.handleAxiosError)(error);
1428
+ }
1429
+ catch (e) {
1430
+ return e.message;
1431
+ }
1432
+ return 'Unexpected error setting AI cart item order-ready payload.';
1433
+ }
1434
+ }
1435
+ async function clearAiCartItemOrderReadyPayload(input) {
1436
+ try {
1437
+ const { data } = await api_client_1.aiDrawingClient.delete(`/sessions/${input.sessionId}/cart-items/${input.itemId}/order-ready-payload`);
1438
+ return JSON.stringify(data, null, 2);
1439
+ }
1440
+ catch (error) {
1441
+ try {
1442
+ (0, api_client_1.handleAxiosError)(error);
1443
+ }
1444
+ catch (e) {
1445
+ return e.message;
1446
+ }
1447
+ return 'Unexpected error clearing AI cart item order-ready payload.';
1448
+ }
1449
+ }
1450
+ async function importReduxCartItemsToAiCart(input) {
1451
+ try {
1452
+ const { data } = await api_client_1.aiDrawingClient.post(`/sessions/${input.sessionId}/cart-items/import-redux`, toBackendReduxCartImportPayload(input));
1453
+ return JSON.stringify(data, null, 2);
1454
+ }
1455
+ catch (error) {
1456
+ try {
1457
+ (0, api_client_1.handleAxiosError)(error);
1458
+ }
1459
+ catch (e) {
1460
+ return e.message;
1461
+ }
1462
+ return 'Unexpected error importing Redux cart items into AI cart.';
1463
+ }
1464
+ }
1465
+ function synthesizeAiCartItemOriginalPayload(item) {
1466
+ return removeUndefined({
1467
+ frontendItemId: item.frontendItemId,
1468
+ originalFrontendItemId: item.originalFrontendItemId,
1469
+ serialNumber: item.serialNumber,
1470
+ positionName: item.positionName,
1471
+ originalPositionName: item.originalPositionName,
1472
+ groupId: item.groupId,
1473
+ quantity: item.quantity,
1474
+ originalQuantity: item.originalQuantity,
1475
+ width: item.width,
1476
+ height: item.height,
1477
+ depth: item.depth,
1478
+ displayName: item.displayName,
1479
+ });
1480
+ }
1481
+ function toOriginalItemPayloadJson(item) {
1482
+ return JSON.stringify(removeUndefined({
1483
+ ...synthesizeAiCartItemOriginalPayload(item),
1484
+ ...(item.itemPayload ?? {}),
1485
+ __aiDrawingClassificationEvidence: item.drawingClassificationEvidence,
1486
+ }));
1487
+ }
1488
+ function toBackendCartItemPayload(item) {
1489
+ return removeUndefined({
1490
+ frontendItemId: item.frontendItemId,
1491
+ originalFrontendItemId: item.originalFrontendItemId,
1492
+ serialNumber: item.serialNumber,
1493
+ positionName: item.positionName,
1494
+ originalPositionName: item.originalPositionName,
1495
+ groupId: item.groupId,
1496
+ quantity: item.quantity,
1497
+ originalQuantity: item.originalQuantity,
1498
+ width: item.width,
1499
+ height: item.height,
1500
+ depth: item.depth,
1501
+ displayName: item.displayName,
1502
+ originalItemPayload: toOriginalItemPayloadJson(item),
1503
+ });
1504
+ }
1505
+ function toBackendCartItemPatchPayload(patch) {
1506
+ return removeUndefined({
1507
+ frontendItemId: patch.frontendItemId,
1508
+ originalFrontendItemId: patch.originalFrontendItemId,
1509
+ serialNumber: patch.serialNumber,
1510
+ positionName: patch.positionName,
1511
+ originalPositionName: patch.originalPositionName,
1512
+ groupId: patch.groupId,
1513
+ quantity: patch.quantity,
1514
+ originalQuantity: patch.originalQuantity,
1515
+ width: patch.width,
1516
+ height: patch.height,
1517
+ depth: patch.depth,
1518
+ displayName: patch.displayName,
1519
+ originalItemPayload: patch.itemPayload === undefined ? undefined : JSON.stringify(patch.itemPayload),
1520
+ });
1521
+ }
1522
+ function toBackendDuplicateCabinetPayload(input) {
1523
+ return removeUndefined({
1524
+ positionName: input.positionName,
1525
+ selectedCanvasType: input.sourceCanvasType,
1526
+ sourcePlacementPositionName: input.sourcePlacementPositionName,
1527
+ placementOffsetX: input.placementOffsetX,
1528
+ placementOffsetY: input.placementOffsetY,
1529
+ source: input.source,
1530
+ });
1531
+ }
1532
+ function toBackendConfigureCabinetPayload(input) {
1533
+ return removeUndefined({
1534
+ frontendItemId: input.frontendItemId,
1535
+ originalFrontendItemId: input.originalFrontendItemId,
1536
+ serialNumber: input.serialNumber,
1537
+ positionName: input.positionName,
1538
+ originalPositionName: input.originalPositionName,
1539
+ groupId: input.groupId,
1540
+ quantity: input.quantity,
1541
+ originalQuantity: input.originalQuantity,
1542
+ width: input.width,
1543
+ height: input.height,
1544
+ depth: input.depth,
1545
+ displayName: input.displayName,
1546
+ originalItemPayload: serializePayload(input.originalItemPayload),
1547
+ source: input.source,
1548
+ });
1549
+ }
1550
+ function validateOrderReadyPayloadInput(input) {
1551
+ if (input.orderReadyStatus !== 'READY') {
1552
+ return { success: true };
1553
+ }
1554
+ const result = orders_1.ArticleItemSchema.safeParse(input.orderReadyPayload);
1555
+ if (result.success && hasOrderConfigurationEvidence(input.orderReadyPayload)) {
1556
+ const baseCabinetHeightIssue = getBaseCabinetBodyHeightValidationMessage(input.orderReadyPayload, input.drawingClassificationEvidence);
1557
+ if (baseCabinetHeightIssue) {
1558
+ return {
1559
+ success: false,
1560
+ message: 'READY rejected: base/sink cabinet height evidence is required for drawing-derived order-ready payloads. ' +
1561
+ baseCabinetHeightIssue,
1562
+ };
1563
+ }
1564
+ return { success: true };
1565
+ }
1566
+ const issueText = result.success
1567
+ ? 'payload: order-critical material/configuration fields are missing'
1568
+ : result.error.issues.map((issue) => `${issue.path.join('.') || 'payload'}: ${issue.message}`).join('; ');
1569
+ return {
1570
+ success: false,
1571
+ message: 'READY rejected: orderReadyPayload must satisfy the MCP ArticleItemSchema/order-ready article contract. ' +
1572
+ 'serialNumber, positionName, width, height, and depth alone are not enough; use get_article_context, get_article_options, catalog lookup, saved settings where applicable, and exact user-confirmed configuration option strings before writing READY. ' +
1573
+ issueText,
1574
+ };
1575
+ }
1576
+ function toBackendOrderReadyPayload(input) {
1577
+ return removeUndefined({
1578
+ orderReadyPayload: input.orderReadyPayload,
1579
+ orderReadyStatus: input.orderReadyStatus,
1580
+ orderReadyMessages: input.orderReadyMessages,
1581
+ orderReadySource: input.orderReadySource ?? 'MCP',
1582
+ });
1583
+ }
1584
+ function toBackendReduxCartImportPayload(input) {
1585
+ return {
1586
+ items: input.items.map((item) => removeUndefined({
1587
+ frontendItemId: item.frontendItemId,
1588
+ originalFrontendItemId: item.originalFrontendItemId,
1589
+ positionName: item.positionName,
1590
+ originalPositionName: item.originalPositionName,
1591
+ serialNumber: item.serialNumber,
1592
+ displayName: item.displayName,
1593
+ groupId: item.groupId,
1594
+ quantity: item.quantity,
1595
+ originalQuantity: item.originalQuantity,
1596
+ width: item.width,
1597
+ height: item.height,
1598
+ depth: item.depth,
1599
+ originalItemPayload: item.originalItemPayload,
1600
+ orderReadyPayload: item.orderReadyPayload,
1601
+ orderReadyStatus: item.orderReadyStatus,
1602
+ orderReadyMessages: item.orderReadyMessages,
1603
+ orderReadySource: item.orderReadySource,
1604
+ orderReadyRevision: item.orderReadyRevision,
1605
+ aiDrawingSessionId: item.aiDrawingSessionId,
1606
+ aiCartId: item.aiCartId,
1607
+ aiCartItemId: item.aiCartItemId,
1608
+ aiMirrorStatus: item.aiMirrorStatus,
1609
+ aiMirrorSource: item.aiMirrorSource,
1610
+ })),
1611
+ };
1612
+ }
1613
+ function hasCoreReduxPayloadFields(payload) {
1614
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
1615
+ return false;
1616
+ }
1617
+ const record = payload;
1618
+ return hasNonBlankValue(record.positionName) &&
1619
+ hasNonBlankValue(record.serialNumber) &&
1620
+ hasPositiveNumberValue(record.quantity) &&
1621
+ hasPositiveNumberValue(record.width) &&
1622
+ hasPositiveNumberValue(record.height) &&
1623
+ hasPositiveNumberValue(record.depth);
1624
+ }
1625
+ function hasNonBlankValue(value) {
1626
+ return typeof value === 'string' && value.trim().length > 0;
1627
+ }
1628
+ function hasPositiveNumberValue(value) {
1629
+ if (typeof value === 'number') {
1630
+ return Number.isFinite(value) && value > 0;
1631
+ }
1632
+ if (typeof value === 'string') {
1633
+ const parsed = Number(value);
1634
+ return Number.isFinite(parsed) && parsed > 0;
1635
+ }
1636
+ return false;
1637
+ }
1638
+ function addBaseCabinetBodyHeightEvidenceIssues(orderReadyPayload, drawingClassificationEvidence, ctx) {
1639
+ const message = getBaseCabinetBodyHeightValidationMessage(orderReadyPayload, drawingClassificationEvidence);
1640
+ if (!message) {
1641
+ return;
1642
+ }
1643
+ const path = message.startsWith('READY base/sink cabinet height')
1644
+ ? ['orderReadyPayload', 'height']
1645
+ : ['drawingClassificationEvidence', 'baseCabinetBodyHeightEvidence'];
1646
+ ctx.addIssue({
1647
+ code: zod_1.z.ZodIssueCode.custom,
1648
+ path,
1649
+ message,
1650
+ });
1651
+ }
1652
+ function getBaseCabinetBodyHeightValidationMessage(orderReadyPayload, drawingClassificationEvidence) {
1653
+ if (!isBaseOrSinkCabinetPayload(orderReadyPayload, drawingClassificationEvidence)) {
1654
+ return null;
1655
+ }
1656
+ const evidenceRecord = isPlainRecord(drawingClassificationEvidence)
1657
+ ? drawingClassificationEvidence
1658
+ : undefined;
1659
+ const bodyHeightEvidence = evidenceRecord?.baseCabinetBodyHeightEvidence;
1660
+ const evidenceResult = BaseCabinetBodyHeightEvidenceSchema.safeParse(bodyHeightEvidence);
1661
+ if (!evidenceResult.success) {
1662
+ return 'READY base/sink cabinet payloads from drawing takeoff require baseCabinetBodyHeightEvidence documenting body height with countertop and toekick_plinth excluded.';
1663
+ }
1664
+ const payloadHeight = getPositiveNumberField(orderReadyPayload, 'height');
1665
+ if (payloadHeight === null) {
1666
+ return null;
1667
+ }
1668
+ const measuredBodyHeight = evidenceResult.data.cabinetBodyHeightInches;
1669
+ if (Math.abs(payloadHeight - measuredBodyHeight) > 0.125) {
1670
+ return `READY base/sink cabinet height (${payloadHeight}) must match baseCabinetBodyHeightEvidence.cabinetBodyHeightInches (${measuredBodyHeight}) within 1/8". ` +
1671
+ 'Do not include countertop or toekick/plinth height in the base cabinet height.';
1672
+ }
1673
+ return null;
1674
+ }
1675
+ function isBaseOrSinkCabinetPayload(orderReadyPayload, drawingClassificationEvidence) {
1676
+ const payload = isPlainRecord(orderReadyPayload) ? orderReadyPayload : {};
1677
+ const searchableValues = [
1678
+ payload.serialNumber,
1679
+ payload.positionName,
1680
+ payload.displayName,
1681
+ ...collectSemanticStrings(drawingClassificationEvidence),
1682
+ ]
1683
+ .filter((value) => (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'))
1684
+ .map((value) => String(value).toLowerCase());
1685
+ return searchableValues.some((value) => (value === 'base' ||
1686
+ value === 'base_cabinet' ||
1687
+ value === 'sink_base' ||
1688
+ value.includes('base cabinet') ||
1689
+ value.includes('sink base') ||
1690
+ value.includes('base_cabinet') ||
1691
+ value.includes('sink_base') ||
1692
+ /^bc[_-]/.test(value) ||
1693
+ /^sb\d*/.test(value) ||
1694
+ /^base([_\s.-]|$)/.test(value) ||
1695
+ /(^|[^a-z])base([^a-z]|$)/.test(value)));
1696
+ }
1697
+ function getPositiveNumberField(value, fieldName) {
1698
+ if (!isPlainRecord(value)) {
1699
+ return null;
1700
+ }
1701
+ const fieldValue = value[fieldName];
1702
+ if (typeof fieldValue === 'number') {
1703
+ return Number.isFinite(fieldValue) && fieldValue > 0 ? fieldValue : null;
1704
+ }
1705
+ if (typeof fieldValue === 'string') {
1706
+ const parsed = Number(fieldValue);
1707
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
1708
+ }
1709
+ return null;
1710
+ }
1711
+ function collectSemanticStrings(value, depth = 0) {
1712
+ if (value === null || value === undefined || depth > 4) {
1713
+ return [];
1714
+ }
1715
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
1716
+ return [value];
1717
+ }
1718
+ if (Array.isArray(value)) {
1719
+ return value.flatMap((item) => collectSemanticStrings(item, depth + 1));
1720
+ }
1721
+ if (isPlainRecord(value)) {
1722
+ return Object.values(value).flatMap((item) => collectSemanticStrings(item, depth + 1));
1723
+ }
1724
+ return [];
1725
+ }
1726
+ function isPlainRecord(value) {
1727
+ return !!value && typeof value === 'object' && !Array.isArray(value);
1728
+ }
1729
+ function hasOrderConfigurationEvidence(payload) {
1730
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
1731
+ return false;
1732
+ }
1733
+ const record = payload;
1734
+ const configurationKeys = [
1735
+ 'caseMaterial',
1736
+ 'frontMaterial',
1737
+ 'innerCaseMaterial',
1738
+ 'backPanelMaterial',
1739
+ 'caseEdge',
1740
+ 'frontEdge',
1741
+ 'edgeBandingType',
1742
+ 'drawerType',
1743
+ 'jointMethod',
1744
+ 'hingePlate',
1745
+ 'backPanel',
1746
+ 'numOfShelves',
1747
+ 'gapTop',
1748
+ 'gapBottom',
1749
+ 'gapLeft',
1750
+ 'gapRight',
1751
+ 'gapCenter',
1752
+ 'topDrwrHeightValue',
1753
+ 'leftCornerWidth',
1754
+ 'rightCornerDepth',
1755
+ 'excludeFronts',
1756
+ 'includeLegLevelers',
1757
+ 'settingsName',
1758
+ ];
1759
+ return configurationKeys.some((key) => {
1760
+ const value = record[key];
1761
+ if (typeof value === 'string') {
1762
+ return value.trim().length > 0;
1763
+ }
1764
+ return value !== undefined && value !== null;
1765
+ });
1766
+ }
1767
+ function serializePayload(payload) {
1768
+ if (payload === undefined) {
1769
+ return undefined;
1770
+ }
1771
+ return typeof payload === 'string' ? payload : JSON.stringify(payload);
1772
+ }
1773
+ function toBackendCoordinatePayload(coordinate) {
1774
+ return removeUndefined({
1775
+ positionName: coordinate.positionName,
1776
+ aiCartItemId: coordinate.aiCartItemId,
1777
+ x: coordinate.x,
1778
+ y: coordinate.y,
1779
+ z: coordinate.z,
1780
+ rotation: coordinate.rotation,
1781
+ source: coordinate.source,
1782
+ });
1783
+ }
1784
+ function toBackendCoordinatePatchPayload(coordinate) {
1785
+ return removeUndefined({
1786
+ positionName: coordinate.positionName,
1787
+ aiCartItemId: coordinate.aiCartItemId,
1788
+ x: coordinate.x,
1789
+ y: coordinate.y,
1790
+ z: coordinate.z,
1791
+ rotation: coordinate.rotation,
1792
+ source: coordinate.source,
1793
+ });
1794
+ }
1795
+ function toBackendPlacementPayload(placement) {
1796
+ return removeUndefined({
1797
+ aiCartItemId: placement.aiCartItemId,
1798
+ positionName: placement.positionName,
1799
+ centerX: placement.centerX,
1800
+ centerY: placement.centerY,
1801
+ width: placement.width,
1802
+ height: placement.height,
1803
+ rotation: placement.rotation,
1804
+ placementSpace: placement.placementSpace,
1805
+ basisImageWidthPx: placement.basisImageWidthPx,
1806
+ basisImageHeightPx: placement.basisImageHeightPx,
1807
+ source: placement.source,
1808
+ });
1809
+ }
1810
+ function toBackendPlacementPatchPayload(placement) {
1811
+ return removeUndefined({
1812
+ aiCartItemId: placement.aiCartItemId,
1813
+ positionName: placement.positionName,
1814
+ centerX: placement.centerX,
1815
+ centerY: placement.centerY,
1816
+ width: placement.width,
1817
+ height: placement.height,
1818
+ rotation: placement.rotation,
1819
+ placementSpace: placement.placementSpace,
1820
+ basisImageWidthPx: placement.basisImageWidthPx,
1821
+ basisImageHeightPx: placement.basisImageHeightPx,
1822
+ source: placement.source,
1823
+ });
1824
+ }
1825
+ function removeUndefined(payload) {
1826
+ return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined));
1827
+ }
1828
+ function formatUnknownError(error) {
1829
+ return error instanceof Error ? error.message : String(error);
1830
+ }
1831
+ // ---------------------------------------------------------------------------
1832
+ // Exports
1833
+ // ---------------------------------------------------------------------------
1834
+ exports.aiDrawingTools = [
1835
+ {
1836
+ name: 'get_ai_drawing_state',
1837
+ description: 'Retrieve the combined state for an isolated backend-owned AIDrawingTool session. ' +
1838
+ 'Returns session metadata, active AI cart data, canvas metadata, canvas images, cabinet coordinates, active canvases, and revisions. ' +
1839
+ 'This tool reads only the AI drawing session API and is separate from the existing frontend drawing tool canvas tools.',
1840
+ inputSchema: GetAiDrawingStateSchema,
1841
+ handler: getAiDrawingState,
1842
+ },
1843
+ {
1844
+ name: 'render_ai_drawing_session_review',
1845
+ description: 'Render per-canvas visual review PNGs from an existing backend-owned AIDrawingTool session. ' +
1846
+ AI_DRAWING_VISUAL_REVIEW_GUIDANCE,
1847
+ inputSchema: exports.AiDrawingSessionReviewRenderInputSchema,
1848
+ handler: renderAiDrawingSessionReviewTool,
1849
+ },
1850
+ {
1851
+ name: 'create_ai_drawing_session',
1852
+ description: 'Create an isolated backend-owned AIDrawingTool session. ' +
1853
+ AI_DRAWING_TOOL_BOUNDARY +
1854
+ ' Owner fields are taken from backend authentication, not MCP input.',
1855
+ inputSchema: exports.AiDrawingSessionCreateInputSchema,
1856
+ handler: createAiDrawingSession,
1857
+ },
1858
+ {
1859
+ name: 'link_ai_cart_to_drawing_session',
1860
+ description: 'Link a backend-owned AI cart as the active cart for an isolated AIDrawingTool session. ' +
1861
+ AI_DRAWING_TOOL_BOUNDARY,
1862
+ inputSchema: exports.LinkAiCartToDrawingSessionInputSchema,
1863
+ handler: linkAiCartToDrawingSession,
1864
+ },
1865
+ {
1866
+ name: 'upsert_ai_canvas_metadata',
1867
+ description: 'Create or update metadata for one backend-owned AIDrawingTool canvas. ' +
1868
+ AI_CANVAS_CALIBRATION_GUIDANCE +
1869
+ ' ' +
1870
+ AI_DRAWING_VISUAL_REVIEW_COMPLETION_GUIDANCE +
1871
+ ' ' +
1872
+ AI_DRAWING_TOOL_BOUNDARY,
1873
+ inputSchema: exports.AiCanvasMetadataInputSchema,
1874
+ handler: upsertAiCanvasMetadata,
1875
+ },
1876
+ {
1877
+ name: 'upsert_ai_canvas_image',
1878
+ description: 'Create or update an existing public/S3 image reference for one backend-owned AIDrawingTool canvas. Do not use this for local filesystem paths; use upload_ai_canvas_image for local image files. ' +
1879
+ AI_DRAWING_VISUAL_REVIEW_COMPLETION_GUIDANCE +
1880
+ ' ' +
1881
+ AI_DRAWING_TOOL_BOUNDARY,
1882
+ inputSchema: exports.AiCanvasImageInputSchema,
1883
+ handler: upsertAiCanvasImage,
1884
+ },
1885
+ {
1886
+ name: 'upload_ai_canvas_image',
1887
+ description: 'Upload a local image file into backend-owned AIDrawingTool S3 storage for one canvas, then store the returned AI-owned S3 key in AI drawing state. Use this when the image is a local file such as an overlay preview PNG. ' +
1888
+ AI_DRAWING_VISUAL_REVIEW_COMPLETION_GUIDANCE +
1889
+ ' ' +
1890
+ AI_DRAWING_TOOL_BOUNDARY,
1891
+ inputSchema: exports.AiCanvasImageUploadInputSchema,
1892
+ handler: uploadAiCanvasImage,
1893
+ },
1894
+ {
1895
+ name: 'delete_ai_canvas_image',
1896
+ description: 'Delete one backend-owned AIDrawingTool canvas image reference. The backend deletes the S3 object only when the stored key is AI-owned under ai-drawings/{sessionId}/; arbitrary external image references are removed from AI state but not S3-deleted. ' +
1897
+ AI_DRAWING_VISUAL_REVIEW_COMPLETION_GUIDANCE +
1898
+ ' ' +
1899
+ AI_DRAWING_TOOL_BOUNDARY,
1900
+ inputSchema: exports.AiCanvasImageDeleteInputSchema,
1901
+ handler: deleteAiCanvasImage,
1902
+ },
1903
+ {
1904
+ name: 'get_ai_canvas_cabinet_placements',
1905
+ description: 'Read all backend-owned AIDrawingTool per-canvas cabinet placements for one session. ' +
1906
+ AI_CANVAS_PLACEMENT_GUIDANCE,
1907
+ inputSchema: AiDrawingSessionIdParamSchema,
1908
+ handler: getAiCanvasCabinetPlacements,
1909
+ },
1910
+ {
1911
+ name: 'get_ai_canvas_cabinet_placements_by_canvas',
1912
+ description: 'Read backend-owned AIDrawingTool cabinet placements for one specific canvas. ' +
1913
+ AI_CANVAS_PLACEMENT_GUIDANCE,
1914
+ inputSchema: exports.AiCanvasCabinetPlacementsByCanvasInputSchema,
1915
+ handler: getAiCanvasCabinetPlacementsByCanvas,
1916
+ },
1917
+ {
1918
+ name: 'replace_ai_canvas_cabinet_placements',
1919
+ description: 'Replace backend-owned AIDrawingTool cabinet placements for exactly one canvas. This replacement must not delete or rewrite any other canvas. ' +
1920
+ AI_CANVAS_PLACEMENT_GUIDANCE,
1921
+ inputSchema: exports.AiCanvasCabinetPlacementsReplaceInputSchema,
1922
+ handler: replaceAiCanvasCabinetPlacements,
1923
+ },
1924
+ {
1925
+ name: 'upsert_ai_canvas_cabinet_placements',
1926
+ description: 'Create or update backend-owned AIDrawingTool cabinet placements for exactly one canvas by positionName without deleting omitted placements. ' +
1927
+ AI_CANVAS_PLACEMENT_GUIDANCE,
1928
+ inputSchema: exports.AiCanvasCabinetPlacementsUpsertInputSchema,
1929
+ handler: upsertAiCanvasCabinetPlacements,
1930
+ },
1931
+ {
1932
+ name: 'patch_ai_canvas_cabinet_placements',
1933
+ description: 'Patch backend-owned AIDrawingTool cabinet placements for exactly one canvas by positionName. ' +
1934
+ AI_CANVAS_PLACEMENT_GUIDANCE,
1935
+ inputSchema: exports.AiCanvasCabinetPlacementsPatchInputSchema,
1936
+ handler: patchAiCanvasCabinetPlacements,
1937
+ },
1938
+ {
1939
+ name: 'delete_ai_canvas_cabinet_placements',
1940
+ description: 'Delete selected backend-owned AIDrawingTool cabinet placements from exactly one canvas by positionName. ' +
1941
+ AI_CANVAS_PLACEMENT_GUIDANCE,
1942
+ inputSchema: exports.AiCanvasCabinetPlacementsDeleteInputSchema,
1943
+ handler: deleteAiCanvasCabinetPlacements,
1944
+ },
1945
+ {
1946
+ name: 'replace_ai_cabinet_coordinates',
1947
+ description: 'Replace all backend-owned AIDrawingTool cabinet coordinates for a session. ' +
1948
+ AI_CANONICAL_PROJECTION_GUIDANCE +
1949
+ ' ' +
1950
+ AI_DRAWING_TOOL_BOUNDARY,
1951
+ inputSchema: exports.AiCabinetCoordinatesReplaceInputSchema,
1952
+ handler: replaceAiCabinetCoordinates,
1953
+ },
1954
+ {
1955
+ name: 'patch_ai_cabinet_coordinates',
1956
+ description: 'Patch backend-owned AIDrawingTool cabinet coordinates by positionName. ' +
1957
+ AI_CANONICAL_PROJECTION_GUIDANCE +
1958
+ ' ' +
1959
+ AI_DRAWING_TOOL_BOUNDARY,
1960
+ inputSchema: exports.AiCabinetCoordinatesPatchInputListSchema,
1961
+ handler: patchAiCabinetCoordinates,
1962
+ },
1963
+ {
1964
+ name: 'delete_ai_cabinet_coordinates',
1965
+ description: 'Delete backend-owned AIDrawingTool cabinet coordinates by positionName. ' +
1966
+ AI_CANONICAL_PROJECTION_GUIDANCE +
1967
+ ' ' +
1968
+ AI_DRAWING_TOOL_BOUNDARY,
1969
+ inputSchema: exports.AiCabinetCoordinatesDeleteInputSchema,
1970
+ handler: deleteAiCabinetCoordinates,
1971
+ },
1972
+ {
1973
+ name: 'create_ai_cart',
1974
+ description: 'Create a backend-owned AI cart for AIDrawingTool. ' +
1975
+ AI_CART_TOOL_BOUNDARY +
1976
+ ' ' +
1977
+ AI_CART_ORDER_READY_CREATION_GUIDANCE +
1978
+ ' This creates only the isolated AI cart, not a frontend cart or stored draft cart.',
1979
+ inputSchema: exports.AiCartCreateInputSchema,
1980
+ handler: createAiCart,
1981
+ },
1982
+ {
1983
+ name: 'get_ai_cart',
1984
+ description: 'Read one backend-owned AIDrawingTool AI cart by cart UUID. ' +
1985
+ AI_CART_TOOL_BOUNDARY,
1986
+ inputSchema: exports.AiCartIdParamSchema,
1987
+ handler: getAiCart,
1988
+ },
1989
+ {
1990
+ name: 'get_ai_cart_by_session',
1991
+ description: 'Read the backend-owned AIDrawingTool AI cart linked to a session UUID. ' +
1992
+ AI_CART_TOOL_BOUNDARY,
1993
+ inputSchema: AiDrawingSessionIdParamSchema,
1994
+ handler: getAiCartBySession,
1995
+ },
1996
+ {
1997
+ name: 'sync_ai_cart_snapshot',
1998
+ description: 'Store a full snapshot into a backend-owned AIDrawingTool AI cart. ' +
1999
+ 'Use this to persist the model-reviewed physical cabinet manifest after native vision has interpreted the drawings. orderConfigurationWorkflow is optional audit metadata; visual semantics are not certified by this schema. When the agent intends items to become READY/order-ready checkout payloads, it must still call get_article_context and get_article_options for each selected catalog serial, check get_my_saved_settings/get_saved_settings_presets when saved settings could satisfy preferences, and ask the user to choose exact configuration options or a saved setting before writing READY order-ready payloads. ' +
2000
+ 'Build this snapshot from one deduplicated physical cabinet manifest across all canvases; the plan is only a top-view placement of the same cabinets shown in the elevations. ' +
2001
+ AI_REVIEW_TOKEN_GUIDANCE +
2002
+ ' ' +
2003
+ 'First snapshot into an empty or new AI cart is allowed; the backend rejects silent replacement after AI or MCP edits. ' +
2004
+ 'Quantity greater than 1 expands in the backend into separate physical item instances. ' +
2005
+ AI_CART_ORDER_READY_CREATION_GUIDANCE +
2006
+ ' ' +
2007
+ AI_CART_TOOL_BOUNDARY,
2008
+ inputSchema: exports.AiCartSnapshotInputSchema,
2009
+ handler: syncAiCartSnapshot,
2010
+ },
2011
+ {
2012
+ name: 'patch_ai_cart_item',
2013
+ description: 'Patch one backend-owned AIDrawingTool AI cart item by canonical backend aiCartItemId. ' +
2014
+ 'If positionName is patched, backend validation requires it to remain unique in the AI cart. ' +
2015
+ 'If the patch changes serialNumber, dimensions, displayName, or any order-relevant field, refresh that item with set_ai_cart_item_order_ready_payload or mark it NEEDS_AGENT_CONFIGURATION/BLOCKED. ' +
2016
+ AI_DRAWING_VISUAL_REVIEW_COMPLETION_GUIDANCE +
2017
+ ' ' +
2018
+ AI_CART_TOOL_BOUNDARY,
2019
+ inputSchema: PatchAiCartItemInputSchema,
2020
+ handler: patchAiCartItem,
2021
+ },
2022
+ {
2023
+ name: 'delete_ai_cart_item',
2024
+ description: 'Delete one backend-owned AIDrawingTool AI cart item by canonical backend aiCartItemId. ' +
2025
+ AI_DRAWING_VISUAL_REVIEW_COMPLETION_GUIDANCE +
2026
+ ' ' +
2027
+ AI_CART_TOOL_BOUNDARY,
2028
+ inputSchema: exports.AiCartItemIdParamSchema,
2029
+ handler: deleteAiCartItem,
2030
+ },
2031
+ {
2032
+ name: 'duplicate_ai_cabinet',
2033
+ description: 'Duplicate one backend-owned AIDrawingTool physical AI cabinet item. ' +
2034
+ AI_CABINET_OPERATION_GUIDANCE,
2035
+ inputSchema: exports.DuplicateAiCabinetInputSchema,
2036
+ handler: duplicateAiCabinet,
2037
+ },
2038
+ {
2039
+ name: 'configure_ai_cabinet',
2040
+ description: 'Configure one backend-owned AIDrawingTool physical AI cabinet item and synchronize linked AI-only placement dimensions. ' +
2041
+ AI_CABINET_OPERATION_GUIDANCE,
2042
+ inputSchema: exports.ConfigureAiCabinetInputSchema,
2043
+ handler: configureAiCabinet,
2044
+ },
2045
+ {
2046
+ name: 'delete_ai_cabinet',
2047
+ description: 'Delete one backend-owned AIDrawingTool physical AI cabinet item and linked AI-only placements. ' +
2048
+ AI_CABINET_OPERATION_GUIDANCE,
2049
+ inputSchema: exports.DeleteAiCabinetInputSchema,
2050
+ handler: deleteAiCabinet,
2051
+ },
2052
+ {
2053
+ name: 'get_ai_cart_item_order_ready_payload',
2054
+ description: 'Read the AI-only order-ready article payload and validation status stored on one backend-owned AIDrawingTool AI cart item. ' +
2055
+ AI_ORDER_READY_GUIDANCE,
2056
+ inputSchema: exports.AiOrderReadyPayloadReadInputSchema,
2057
+ handler: getAiCartItemOrderReadyPayload,
2058
+ },
2059
+ {
2060
+ name: 'set_ai_cart_item_order_ready_payload',
2061
+ description: 'Write the AI-only order-ready article payload and validation status for one backend-owned AIDrawingTool AI cart item. ' +
2062
+ AI_ORDER_READY_GUIDANCE,
2063
+ inputSchema: exports.AiOrderReadyPayloadSetInputSchema,
2064
+ handler: setAiCartItemOrderReadyPayload,
2065
+ },
2066
+ {
2067
+ name: 'clear_ai_cart_item_order_ready_payload',
2068
+ description: 'Clear the AI-only order-ready article payload and reset validation status for one backend-owned AIDrawingTool AI cart item. ' +
2069
+ AI_ORDER_READY_GUIDANCE,
2070
+ inputSchema: exports.AiOrderReadyPayloadClearInputSchema,
2071
+ handler: clearAiCartItemOrderReadyPayload,
2072
+ },
2073
+ {
2074
+ name: 'import_redux_cart_items_to_ai_cart',
2075
+ description: 'Import rich browser Redux checkout cart items into the active backend-owned AIDrawingTool AI cart without a Seed AI Cart button. ' +
2076
+ AI_REDUX_IMPORT_GUIDANCE,
2077
+ inputSchema: exports.AiReduxCartImportInputSchema,
2078
+ handler: importReduxCartItemsToAiCart,
2079
+ },
2080
+ ];