opentakeoff-mcp 0.9.61 → 0.9.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -172,7 +172,7 @@ reads the tool list once. ([#230](https://github.com/Kentucky-ai/opentakeoff/iss
172
172
  | `resolve_tag` | ONE room tag → its room-finish schedule row → each code's finish/material definition, every edge cited (sheet + literal text + bbox). Refusal over guessing: `unresolved` comes back with a reason, never as silence. A delta/REV marker on the answering row rides the result as `revisions`—the codes are the post-revision answer, and you're told the ink changed. |
173
173
  | `find_schedule` | Locate a schedule table by kind ("room finish", "material")—sheet, title, headers, row count, a `view_sheet`-ready region, and `revised_rows` when delta/REV-marked rows exist. |
174
174
  | `sheet_context` | The region's STRUCTURE in one frame: classified vector segments (endpoints as drawn, meta byte per segment), text spans with bboxes, and hatch-family instances with content-derived ids—same pattern spec ⇒ same id anywhere on the sheet, so plan↔legend matching is `id === id`. Decimation is declared and counted on every reply: `kept + dropped === total_in_region`, cap applies longest-first so walls survive. |
175
- | `view_sheet` | The agent's eyes: render the sheet (or an image-px crop) to PNG. `overlay` burns committed shapes in (solid = human-affirmed, dashed = unreviewed) to verify geometry landed; `grid` burns in a calibrated 1-ft/5-ft measuring grid with foot labels (`"auto"` from the set scale, or the drawing scale like `"1/4"`) so dimensions are counted off cells, not guessed. |
175
+ | `view_sheet` | The agent's eyes: render the sheet (or an image-px crop) to PNG. `overlay` burns committed shapes in (solid = human-affirmed, dashed = unreviewed) to verify geometry landed; `grid` burns in a calibrated 1-ft/5-ft measuring grid with foot labels (`"auto"` from the set scale, or the drawing scale like `"1/4"`) so dimensions are counted off cells, not guessed; `marks` (#297) burns disclosure layers in — `question` (withheld placements, orange ?-circle), `struck` (rejections, magenta struck ×), `ring` (the sweep's seed, violet double ring) — in colors off the common CAD pens, so what a reply names, the picture shows. |
176
176
 
177
177
  ### The agent revises its own work
178
178
 
@@ -4638,6 +4638,64 @@ function sharedRuns(ringA, ringB, opts) {
4638
4638
  }
4639
4639
  return runs;
4640
4640
  }
4641
+ var TRANSITION_DEFAULTS = { max_gap_in: 12, min_run_in: 12 };
4642
+ var round22 = (n) => +n.toFixed(2);
4643
+ var round12 = (n) => +n.toFixed(1);
4644
+ function dropCollinear(path5, tol = 1e-6) {
4645
+ if (path5.length < 3) return path5.map((p) => [p[0], p[1]]);
4646
+ const out = [[path5[0][0], path5[0][1]]];
4647
+ for (let i = 1; i < path5.length - 1; i++) {
4648
+ const a = out[out.length - 1], b = path5[i], c = path5[i + 1];
4649
+ const abx = b[0] - a[0], aby = b[1] - a[1];
4650
+ const acx = c[0] - a[0], acy = c[1] - a[1];
4651
+ const len = Math.hypot(acx, acy);
4652
+ const off = len > 0 ? Math.abs(abx * acy - aby * acx) / len : Infinity;
4653
+ if (off > tol) out.push([b[0], b[1]]);
4654
+ }
4655
+ out.push([path5[path5.length - 1][0], path5[path5.length - 1][1]]);
4656
+ return out;
4657
+ }
4658
+ function deriveTransitionRuns(a, b, sheets, opts = {}) {
4659
+ const maxGapIn = opts.max_gap_in ?? TRANSITION_DEFAULTS.max_gap_in;
4660
+ const minRunIn = opts.min_run_in ?? TRANSITION_DEFAULTS.min_run_in;
4661
+ const runs = [], withheld = [];
4662
+ if (!(maxGapIn > 0) || !(minRunIn > 0)) return { runs, withheld };
4663
+ for (const [key, frame] of sheets) {
4664
+ if (!(frame.upp > 0) || !(frame.widthPx > 0) || !(frame.heightPx > 0)) continue;
4665
+ const onA = a.shapes.filter((s) => s.sheet_id === key);
4666
+ const onB = b.shapes.filter((s) => s.sheet_id === key);
4667
+ if (!onA.length || !onB.length) continue;
4668
+ const pxPerFt = 1 / frame.upp;
4669
+ const toPx = (s) => s.verts_norm.map(([x, y]) => [x * frame.widthPx, y * frame.heightPx]);
4670
+ const runOpts = {
4671
+ step_px: Math.max(1, pxPerFt * 0.25),
4672
+ // a quarter-foot walk — finer than any transition matters
4673
+ touch_px: pxPerFt * (1 / 12),
4674
+ // within an inch: one open space, not two rooms
4675
+ max_gap_px: pxPerFt * (maxGapIn / 12),
4676
+ min_len_px: pxPerFt * (minRunIn / 12)
4677
+ };
4678
+ for (const ra of onA) {
4679
+ for (const rb of onB) {
4680
+ if (ra.id === rb.id) continue;
4681
+ for (const r of sharedRuns(toPx(ra), toPx(rb), runOpts)) {
4682
+ const row = {
4683
+ sheet_id: key,
4684
+ between_shape_ids: [ra.id, rb.id],
4685
+ between: [a.tag, b.tag],
4686
+ length_lf: round22(r.length_px * frame.upp),
4687
+ gap_in: round12(r.gap_px * frame.upp * 12),
4688
+ at: [Math.round(r.at[0]), Math.round(r.at[1])],
4689
+ verts_norm: dropCollinear(r.path).map(([x, y]) => [x / frame.widthPx, y / frame.heightPx])
4690
+ };
4691
+ if (r.kind === "wall") withheld.push({ ...row, reason: "wall_separated" });
4692
+ else runs.push(row);
4693
+ }
4694
+ }
4695
+ }
4696
+ }
4697
+ return { runs, withheld };
4698
+ }
4641
4699
 
4642
4700
  // ../web/src/lib/cutout.js
4643
4701
  import { polygon as turfPolygon, featureCollection } from "@turf/helpers";
@@ -4950,7 +5008,7 @@ function applyApprovalCommand(approvals, cmd) {
4950
5008
  }
4951
5009
 
4952
5010
  // ../web/src/lib/num.js
4953
- var round22 = (n) => Math.round((n + Number.EPSILON) * 100) / 100;
5011
+ var round23 = (n) => Math.round((n + Number.EPSILON) * 100) / 100;
4954
5012
 
4955
5013
  // ../web/src/lib/conditionColumns.js
4956
5014
  var visible = (v) => typeof v === "string" && v.trim() ? v : "";
@@ -5025,8 +5083,8 @@ function conditionTotals(conditions, shapes, ctx = null) {
5025
5083
  const per = Math.max(0, Number(m.per) || 0);
5026
5084
  const basisVal = m.basis === "linear" ? lf : m.basis === "count" ? ea : m.basis === "seam_lf" ? seam : total;
5027
5085
  let qty = per > 0 ? basisVal / per : 0;
5028
- qty = m.round === false ? round22(qty) : Math.ceil(qty - 1e-9);
5029
- return { name: m.name, unit: m.unit || "", per, basis: m.basis || "area", round: m.round !== false, note: m.note || "", basis_qty: round22(basisVal), qty };
5086
+ qty = m.round === false ? round23(qty) : Math.ceil(qty - 1e-9);
5087
+ return { name: m.name, unit: m.unit || "", per, basis: m.basis || "area", round: m.round !== false, note: m.note || "", basis_qty: round23(basisVal), qty };
5030
5088
  });
5031
5089
  return {
5032
5090
  id: c.id,
@@ -5037,19 +5095,19 @@ function conditionTotals(conditions, shapes, ctx = null) {
5037
5095
  multiplier: mult,
5038
5096
  waste_pct: waste,
5039
5097
  shape_count: cs.length,
5040
- floor_sf: round22(floor),
5041
- wall_sf: round22(wall),
5042
- border_sf: round22(border),
5043
- lf: round22(lf),
5098
+ floor_sf: round23(floor),
5099
+ wall_sf: round23(wall),
5100
+ border_sf: round23(border),
5101
+ lf: round23(lf),
5044
5102
  ea,
5045
- total_sf: round22(total),
5103
+ total_sf: round23(total),
5046
5104
  // waste-adjusted (order quantities)
5047
- floor_sf_net: round22(floor * w),
5048
- wall_sf_net: round22(wall * w),
5049
- border_sf_net: round22(border * w),
5050
- lf_net: round22(lf * w),
5051
- total_sf_net: round22(total * w),
5052
- sy_net: round22(total * w / 9),
5105
+ floor_sf_net: round23(floor * w),
5106
+ wall_sf_net: round23(wall * w),
5107
+ border_sf_net: round23(border * w),
5108
+ lf_net: round23(lf * w),
5109
+ total_sf_net: round23(total * w),
5110
+ sy_net: round23(total * w / 9),
5053
5111
  materials
5054
5112
  };
5055
5113
  });
@@ -5098,11 +5156,11 @@ function hasMultipliers(bySheet) {
5098
5156
  function roundSheetRow(r) {
5099
5157
  return {
5100
5158
  ...r,
5101
- floor_sf: round22(r.floor_sf),
5102
- wall_sf: round22(r.wall_sf),
5103
- border_sf: round22(r.border_sf),
5104
- lf: round22(r.lf),
5105
- ea: round22(r.ea)
5159
+ floor_sf: round23(r.floor_sf),
5160
+ wall_sf: round23(r.wall_sf),
5161
+ border_sf: round23(r.border_sf),
5162
+ lf: round23(r.lf),
5163
+ ea: round23(r.ea)
5106
5164
  };
5107
5165
  }
5108
5166
  function materialsSummary(rows) {
@@ -5113,17 +5171,17 @@ function materialsSummary(rows) {
5113
5171
  cur.qty += m.qty;
5114
5172
  map.set(key, cur);
5115
5173
  }
5116
- return [...map.values()].map((x) => ({ ...x, qty: round22(x.qty) }));
5174
+ return [...map.values()].map((x) => ({ ...x, qty: round23(x.qty) }));
5117
5175
  }
5118
5176
  function grandTotals(rows) {
5119
5177
  const sum = (k) => rows.reduce((n, r) => n + (r[k] || 0), 0);
5120
5178
  return {
5121
- total_sf: round22(sum("total_sf")),
5122
- total_sf_net: round22(sum("total_sf_net")),
5123
- lf: round22(sum("lf")),
5124
- lf_net: round22(sum("lf_net")),
5179
+ total_sf: round23(sum("total_sf")),
5180
+ total_sf_net: round23(sum("total_sf_net")),
5181
+ lf: round23(sum("lf")),
5182
+ lf_net: round23(sum("lf_net")),
5125
5183
  ea: sum("ea"),
5126
- sy_net: round22(sum("sy_net"))
5184
+ sy_net: round23(sum("sy_net"))
5127
5185
  };
5128
5186
  }
5129
5187
  function reportJson({ projectName = "", rows = [], bySheet = [], scaleInfo = [], markups = [], rfis = [], sheetLabel = null, conditionColumns = [], attrsByCond = null, shapeLabels = [], byLabel = [], displayUnits = "imperial", rollGoods = [] }) {
@@ -5621,7 +5679,7 @@ function computeRollTakeoff(conditions, shapes, dimsFor, uppFor) {
5621
5679
  orderFt,
5622
5680
  rollCount: rollLayoutRollCount(layout.strips),
5623
5681
  oversize: layout.strips.some((s) => s.overRoll),
5624
- qty: round22(rollQtyForUnit(orderFt, config.rollWidthFt, unit)),
5682
+ qty: round23(rollQtyForUnit(orderFt, config.rollWidthFt, unit)),
5625
5683
  unit,
5626
5684
  cutCount: layout.strips.length,
5627
5685
  seamLf: seamLfForStrips(layout.strips),
@@ -5674,16 +5732,16 @@ function rollReportRows(rollByCond, rows) {
5674
5732
  roll_length_ft: ri.config.rollLengthFt,
5675
5733
  direction: ri.direction,
5676
5734
  cuts: ri.cutCount,
5677
- order_lf: round22(ri.orderFt * mult),
5735
+ order_lf: round23(ri.orderFt * mult),
5678
5736
  rolls: ri.rollCount * mult,
5679
- order_qty: round22(ri.qty * mult),
5737
+ order_qty: round23(ri.qty * mult),
5680
5738
  order_unit: ri.unit,
5681
5739
  oversize: ri.oversize,
5682
5740
  // seam_lf APPENDS last (the additive-only convention the roll_goods
5683
5741
  // block already follows): the figured weld-rod / seam-tape length, ×N
5684
5742
  // like every other reported quantity. 0 is a real answer — a layout
5685
5743
  // whose rooms each fit one strip has no seams to weld.
5686
- seam_lf: round22((ri.seamLf || 0) * mult)
5744
+ seam_lf: round23((ri.seamLf || 0) * mult)
5687
5745
  });
5688
5746
  }
5689
5747
  return out;
@@ -5790,6 +5848,59 @@ function drawShapes(ctx, toCanvas, shapes, sheetW, sheetH, longEdge) {
5790
5848
  }
5791
5849
  ctx.setLineDash([]);
5792
5850
  }
5851
+ var MARK_QUESTION = "#ff8c00";
5852
+ var MARK_STRUCK = "#e10ee1";
5853
+ var MARK_RING = "#7a00e6";
5854
+ function polyCircle(ctx, x, y, r) {
5855
+ ctx.beginPath();
5856
+ for (let i = 0; i <= 16; i++) {
5857
+ const a = i / 16 * 2 * Math.PI;
5858
+ const px = x + r * Math.cos(a), py = y + r * Math.sin(a);
5859
+ if (i === 0) ctx.moveTo(px, py);
5860
+ else ctx.lineTo(px, py);
5861
+ }
5862
+ ctx.stroke();
5863
+ }
5864
+ function drawMarks(ctx, toCanvas, marks, longEdge) {
5865
+ const r = Math.max(7, longEdge / 110);
5866
+ const w = Math.max(1.6, longEdge / 600);
5867
+ ctx.setLineDash([]);
5868
+ ctx.lineWidth = w;
5869
+ let drawn = 0;
5870
+ for (const [mx, my] of marks.question ?? []) {
5871
+ const [x, y] = toCanvas(mx, my);
5872
+ ctx.strokeStyle = MARK_QUESTION;
5873
+ polyCircle(ctx, x, y, r);
5874
+ ctx.fillStyle = MARK_QUESTION;
5875
+ ctx.font = `bold ${Math.round(r * 1.4)}px sans-serif`;
5876
+ ctx.fillText("?", x - r * 0.35, y + r * 0.5);
5877
+ drawn++;
5878
+ }
5879
+ for (const [mx, my] of marks.struck ?? []) {
5880
+ const [x, y] = toCanvas(mx, my);
5881
+ ctx.strokeStyle = MARK_STRUCK;
5882
+ const m = r * 0.8;
5883
+ ctx.beginPath();
5884
+ ctx.moveTo(x - m, y - m);
5885
+ ctx.lineTo(x + m, y + m);
5886
+ ctx.moveTo(x - m, y + m);
5887
+ ctx.lineTo(x + m, y - m);
5888
+ ctx.stroke();
5889
+ ctx.beginPath();
5890
+ ctx.moveTo(x - r * 1.2, y);
5891
+ ctx.lineTo(x + r * 1.2, y);
5892
+ ctx.stroke();
5893
+ drawn++;
5894
+ }
5895
+ for (const [mx, my] of marks.ring ?? []) {
5896
+ const [x, y] = toCanvas(mx, my);
5897
+ ctx.strokeStyle = MARK_RING;
5898
+ polyCircle(ctx, x, y, r);
5899
+ polyCircle(ctx, x, y, r * 0.6);
5900
+ drawn++;
5901
+ }
5902
+ return drawn;
5903
+ }
5793
5904
 
5794
5905
  // src/session.ts
5795
5906
  var SNAP_CELL = 24;
@@ -6035,9 +6146,11 @@ var Session = class _Session {
6035
6146
  }
6036
6147
  const ppf = gridPxPerFoot(opts.grid, s.upp);
6037
6148
  const sheetShapes = this.shapes.filter((x) => x.sheet_id === s.key);
6149
+ let marksDrawn = 0;
6038
6150
  const { png, width, height, zoom } = await s.page.renderRegionPng(r, px, (ctx, toCanvas) => {
6039
6151
  if (ppf) drawGrid(ctx, toCanvas, r, ppf);
6040
6152
  if (opts.overlay) drawShapes(ctx, toCanvas, sheetShapes, s.widthPx, s.heightPx, px);
6153
+ if (opts.marks) marksDrawn = drawMarks(ctx, toCanvas, opts.marks, px);
6041
6154
  });
6042
6155
  return {
6043
6156
  png,
@@ -6050,6 +6163,7 @@ var Session = class _Session {
6050
6163
  zoom: +zoom.toFixed(4),
6051
6164
  overlay: !!opts.overlay,
6052
6165
  ...opts.overlay ? { shapes_drawn: sheetShapes.length } : {},
6166
+ ...opts.marks ? { marks_drawn: marksDrawn } : {},
6053
6167
  grid_px_per_foot: ppf ? round2(ppf) : 0
6054
6168
  }
6055
6169
  };
@@ -7097,26 +7211,40 @@ var Session = class _Session {
7097
7211
  const s = this.sheet(key);
7098
7212
  if (s.upp == null) throw new UserError(`${key} has no scale \u2014 a transition is a real length, so set_scale first (${this.scaleGate(s)})`);
7099
7213
  }
7100
- const committed = [], withheld = [];
7214
+ const frames = /* @__PURE__ */ new Map();
7101
7215
  for (const key of sheetsInPlay) {
7102
7216
  const s = this.sheet(key);
7103
- const upp = s.upp;
7104
- const pxPerFt = 1 / upp;
7105
- const toPx = (sh) => sh.verts_norm.map(([x, y]) => [x * s.widthPx, y * s.heightPx]);
7106
- const onSheetA = fa.filter((x) => x.sheet_id === key), onSheetB = fb.filter((x) => x.sheet_id === key);
7107
- for (const ra of onSheetA) {
7108
- for (const rb of onSheetB) {
7109
- const runs = sharedRuns(toPx(ra), toPx(rb), {
7110
- step_px: Math.max(1, pxPerFt * 0.25),
7111
- // a quarter-foot walk — finer than any transition matters
7112
- touch_px: pxPerFt * (1 / 12),
7113
- // within an inch: one open space, not two rooms
7114
- max_gap_px: pxPerFt * (maxGapIn / 12),
7115
- min_len_px: pxPerFt * (minRunIn / 12)
7116
- });
7117
- for (const r of runs) this.recordRun(s, r, upp, opts.condition, ra.id, rb.id, a.finish_tag, b.finish_tag, committed, withheld);
7118
- }
7119
- }
7217
+ frames.set(key, { widthPx: s.widthPx, heightPx: s.heightPx, upp: s.upp });
7218
+ }
7219
+ const src = (sh) => ({ id: sh.id, sheet_id: sh.sheet_id, verts_norm: sh.verts_norm });
7220
+ const derived = deriveTransitionRuns(
7221
+ { tag: a.finish_tag, shapes: fa.map(src) },
7222
+ { tag: b.finish_tag, shapes: fb.map(src) },
7223
+ frames,
7224
+ { max_gap_in: maxGapIn, min_run_in: minRunIn }
7225
+ );
7226
+ const committed = [], withheld = [];
7227
+ for (const w of derived.withheld) {
7228
+ withheld.push({
7229
+ sheet: w.sheet_id,
7230
+ between_shape_ids: w.between_shape_ids,
7231
+ length_lf: w.length_lf,
7232
+ gap_in: w.gap_in,
7233
+ at: w.at,
7234
+ reason: "wall_separated",
7235
+ detail: `${a.finish_tag} and ${b.finish_tag} run ${w.length_lf} LF apart across ${w.gap_in}" of wall \u2014 adjacent rooms, not a butt joint. If a door opens here the transition is a threshold at the door, which this cannot see.`
7236
+ });
7237
+ }
7238
+ for (const r of derived.runs) {
7239
+ const s = this.sheet(r.sheet_id);
7240
+ const pathPx = r.verts_norm.map(([x, y]) => [x * s.widthPx, y * s.heightPx]);
7241
+ const shape = this.commit(s, opts.condition, "linear", pathPx, { area_sf: 0, perimeter_lf: r.length_lf }, {
7242
+ method: "agent_v1",
7243
+ actor: "agent",
7244
+ reviewed: false,
7245
+ derived: { between_shape_ids: r.between_shape_ids, between: [a.finish_tag, b.finish_tag], case: "butt", gap_in: r.gap_in }
7246
+ });
7247
+ committed.push({ sheet: r.sheet_id, between_shape_ids: r.between_shape_ids, length_lf: r.length_lf, gap_in: r.gap_in, at: r.at, shape_id: shape.id });
7120
7248
  }
7121
7249
  if (committed.length) this.flushCommits("derive_transitions");
7122
7250
  return {
@@ -7130,23 +7258,6 @@ var Session = class _Session {
7130
7258
  note: withheld.length ? `${withheld.length} run(s) are adjacency ACROSS A WALL, not a butt joint \u2014 the transition there is a threshold in the doorway, and the trace record does not say where the doorway is. view_sheet each \`at\` and place them with measure_line / place_count.` : "Every run was a butt joint inside one open space. Verify with view_sheet overlay:true before trusting the total."
7131
7259
  };
7132
7260
  }
7133
- /** One shared run → committed transition, or a disclosed question. */
7134
- recordRun(s, r, upp, condition, aId, bId, aTag, bTag, committed, withheld) {
7135
- const length_lf = round2(r.length_px * upp);
7136
- const gap_in = round1(r.gap_px * upp * 12);
7137
- const row = { sheet: s.key, between_shape_ids: [aId, bId], length_lf, gap_in, at: [Math.round(r.at[0]), Math.round(r.at[1])] };
7138
- if (r.kind === "wall") {
7139
- withheld.push({ ...row, reason: "wall_separated", detail: `${aTag} and ${bTag} run ${length_lf} LF apart across ${gap_in}" of wall \u2014 adjacent rooms, not a butt joint. If a door opens here the transition is a threshold at the door, which this cannot see.` });
7140
- return;
7141
- }
7142
- const shape = this.commit(s, condition, "linear", r.path, { area_sf: 0, perimeter_lf: length_lf }, {
7143
- method: "agent_v1",
7144
- actor: "agent",
7145
- reviewed: false,
7146
- derived: { between_shape_ids: [aId, bId], between: [aTag, bTag], case: "butt", gap_in }
7147
- });
7148
- committed.push({ ...row, shape_id: shape.id });
7149
- }
7150
7261
  /** Count markers — the canvas's Count tool (commitCount): one point, one EA,
7151
7262
  * computed {count: 1}, NO scale required (EA is scale-free; the canvas's
7152
7263
  * recompute skips count shapes for the same reason). One shape per point,
@@ -7421,6 +7532,12 @@ var Session = class _Session {
7421
7532
  if (opts.commit && !opts.condition) {
7422
7533
  throw new UserError("commit: true needs a condition \u2014 the finish tag the match markers count under (e.g. 'FD-1').");
7423
7534
  }
7535
+ if (opts.commitSeed && !opts.commit) {
7536
+ throw new UserError("commit_seed: true needs commit: true \u2014 the seed joins the same one-undo-step batch as the matches.");
7537
+ }
7538
+ if (opts.commitSeed && scope === "set") {
7539
+ throw new UserError("commit_seed applies to sheet scope only \u2014 in a set-wide sweep the seed may sit on a detail or legend sheet, where it is a reference drawing. If the seed instance is installed work, place_count it on its sheet explicitly.");
7540
+ }
7424
7541
  const geo = await this.ensureGeometry(s);
7425
7542
  if (!geo.segs.length) {
7426
7543
  throw new UserError("This sheet has no vector linework (likely a scan) \u2014 symbol matching reads the drawn segments; raster fallback not yet available in the MCP server.");
@@ -7473,16 +7590,23 @@ var Session = class _Session {
7473
7590
  if (!s.spans) s.spans = textSpans(s.page);
7474
7591
  const lbl = this.sweepLabels(s.spans, geo, fp.center, res.matches, res.withheld);
7475
7592
  let committed2;
7476
- if (opts.commit && res.matches.length) {
7477
- committed2 = this.placeCount(name, res.matches.map((m) => m.at), {
7593
+ if (opts.commit && (res.matches.length || opts.commitSeed)) {
7594
+ const points = [...opts.commitSeed ? [fp.center] : [], ...res.matches.map((m) => m.at)];
7595
+ const seedOrigin = {
7596
+ method: "symbol_sweep",
7597
+ actor: "agent",
7598
+ reviewed: false,
7599
+ symbol: { score: 1, rotation: 0, mirrored: false, seed: { source: "instance", sheet: s.key } }
7600
+ };
7601
+ committed2 = this.placeCount(name, points, {
7478
7602
  condition: opts.condition,
7479
7603
  tool: "symbol_sweep",
7480
- origins: res.matches.map((m) => ({
7604
+ origins: [...opts.commitSeed ? [seedOrigin] : [], ...res.matches.map((m) => ({
7481
7605
  method: "symbol_sweep",
7482
7606
  actor: "agent",
7483
7607
  reviewed: false,
7484
7608
  symbol: { score: m.score, rotation: m.rotation, mirrored: m.mirrored, seed: { source: "instance", sheet: s.key } }
7485
- }))
7609
+ }))]
7486
7610
  });
7487
7611
  }
7488
7612
  const labelNote2 = _Session.sweepLabelNote(lbl);
@@ -7501,11 +7625,13 @@ var Session = class _Session {
7501
7625
  committed: committed2.committed,
7502
7626
  shape_ids: committed2.shape_ids,
7503
7627
  condition: committed2.condition,
7504
- ea_total: committed2.ea_total
7628
+ ea_total: committed2.ea_total,
7629
+ ...opts.commitSeed ? { seed_committed: true } : {}
7505
7630
  } : {},
7506
7631
  ...(() => {
7507
7632
  const parts = [];
7508
- if (opts.commit && !res.matches.length) parts.push("commit requested but nothing cleared the bar \u2014 no shapes were committed.");
7633
+ if (opts.commit && !res.matches.length && !opts.commitSeed) parts.push("commit requested but nothing cleared the bar \u2014 no shapes were committed.");
7634
+ if (committed2 && !opts.commitSeed) parts.push(`The seed instance at (${round1(fp.center[0])}, ${round1(fp.center[1])}) is NOT in this count \u2014 if it is installed work, re-run with commit_seed: true or place_count it.`);
7509
7635
  if (labelNote2) parts.push(labelNote2);
7510
7636
  return parts.length ? { note: parts.join(" ") } : {};
7511
7637
  })(),
@@ -9188,6 +9314,7 @@ var symbolSweepOutput = {
9188
9314
  rejected: z.array(sweepRejected).optional().describe("Sheet scope only. Placements the geometry accepted and a counter-example refused (#259) \u2014 NEVER counted in found, and never silent: each says which negative did it and what it saw. Reinstate one by hand with place_count at its `at` if you disagree"),
9189
9315
  negatives: sweepNegatives.optional().describe("What each `exclude` rect was read as, in the order you passed them (#259)"),
9190
9316
  rejected_total: z.number().int().optional().describe("Set scope: placements counter-examples rejected across every swept sheet"),
9317
+ seed_committed: z.boolean().optional().describe("Present when commit_seed: true minted the seed instance into the batch (#296) \u2014 ea_total then includes it"),
9191
9318
  lum_gate: sweepLumGate.optional().describe("Sheet scope only. The stated stroke-luminance gate's accounting (#260): the tolerance, the seed's own luminance band, and every placement the geometry would have committed that the pen pulled under the bar \u2014 NEVER counted in found, never silent. Set scope accounts per sheet in sheets[]"),
9192
9319
  candidates: sweepCandidates.optional().describe("Sheet scope only \u2014 set scope accounts per sheet in sheets[]"),
9193
9320
  complete: z.boolean().describe("True when every proposed placement was scored (every swept sheet, in set scope) and the count is a total. FALSE MEANS THE COUNT IS A FLOOR \u2014 acknowledge it before trusting found (#261)"),
@@ -11503,12 +11630,13 @@ function registerTools(realServer, session) {
11503
11630
  outputSchema: placeCountOutput
11504
11631
  }, run("place_count", (a) => session.placeCount(a.sheet, a.points, { condition: a.condition })));
11505
11632
  server.registerTool("symbol_sweep", {
11506
- description: `Find EVERY instance of a repeated plan symbol from ONE example \u2014 drains, thresholds, fixtures, transition markers: marquee a tight seed_rect around a single instance and the vector linework is searched for every other placement of that same segment cluster. Deterministic geometry, not vision: each placement scores as the length-weighted fraction of the seed's segments reproduced within tolerance_px, under translation plus 0/90/180/270 rotation and mirroring (symbols rotate on plans \u2014 both ON by default; turn them off to pin orientation). Score \u2265 0.92 is a match; the 0.75\u20130.92 band comes back in \`withheld\` with a reason \u2014 a near-match is a question you answer by LOOKING (view_sheet at its \`at\`), never a silent commit and never a silent drop. The seed's own location is reported in \`seed\` and never double-committed. Every proposed placement is scored up to a hard work ceiling sized for pathological sheets, and the reply says which it was: complete true means the count is a total; complete false (with candidates.dropped > 0) means the count is a FLOOR \u2014 some placements were never scored \u2014 so tighten the seed rect around more distinctive geometry rather than trusting it as a total. Marquee discipline: the rect must hug ONE instance \u2014 only segments FULLY inside it define the symbol, so a loose rect that swallows wall linework fingerprints the wall, not the symbol. scope "set" sweeps the WHOLE working set, counting on PLAN-role sheets only (the sheet graph decides): a symbol drawn in a detail, legend, or schedule is a reference drawing and never counts itself \u2014 which is also how you seed from one: marquee the assembly on the detail sheet and its plan-sheet occurrences are counted while the detail stays excluded (the exclusion disclosed in \`skipped\`, per-sheet results with per-sheet caps and wall-clock in \`sheets\`). Scale across sheets: the fingerprint is size-true and is never scale-SEARCHED, so a detail drawn at 1-1/2" = 1'-0" is 12\xD7 the size of the same mark on a 1/8" plan \u2014 when BOTH sheets have a scale set, the exact ratio is computed from them and the seed is resized before matching (reported per sheet as \`scaled\`); when a scale is missing, the sweep runs at 1:1 and SAYS so (\`scale_assumed\`), because an unknown ratio plus a zero count is not evidence of absence. Seeding from a detail/legend/schedule sheet REFUSES outright until both scales are set \u2014 that is the case where an unstated ratio silently finds nothing. commit: true (requires condition) commits every match center as an EA count marker through the same path as place_count \u2014 the whole sweep (set-wide included) is ONE undo step, each marker carries origin.method "symbol_sweep" with its score, transform, and seed source, and withheld placements are NEVER committed. The COUNT is scale-free (EA), but matching across sheets of different scales is not \u2014 set_scale on the sheets involved is what turns the ratio from an assumption into arithmetic. Counter-examples (#259): drafting reuses one generic shape for different devices \u2014 a wall-mounted data outlet drawn as a plain triangle, the flush-floor variant the SAME triangle inside a square, keynote callouts a triangle with a letter in it \u2014 so the seed legitimately matches things you do not mean, and seeding more geometry only works where the drawing offers more to capture. \`exclude\` takes rects around instances you do NOT mean, marqueed exactly like the seed. You never choose a mechanism; the rect's contents decide, because both are the same gesture: a rect holding EXTRA linework beyond the seed rejects placements where that extra linework is present too (the box, the letter), and a rect holding no extra linework of its own is read as the line running THROUGH it \u2014 a bare ceiling-grid tile whose grid line a real fixture, drawn over it, would BREAK. That second mechanic is not expressible as a seed: only segments fully INSIDE a rect define a symbol, and background structure is long by nature. Every rejection is disclosed in rejected[] \u2014 which negative, what fraction of its evidence was found, and the placement \u2014 and NEVER counted in found: an exclusion is a judgement, so look at it and reinstate any you disagree with using place_count at its \`at\`. A counter-example that holds no instance of the seed, or holds the seed with nothing extra, is REFUSED rather than silently doing nothing. Stroke luminance (#260): a flattened export strips the layer tree and flattens every pen, but the file still STATES stroke color \u2014 a black fixture outline over a grey ceiling grid is unambiguous there even when the geometry is identical (two empty 2 ft grid tiles reproduce a 2\xD74 fixture's outline exactly). luminance_tolerance (0\u2013254) gates on it: a sheet segment only answers for a seed segment when their stroke luminances are within the stated tolerance (Rec. 709, 0 = black, 255 = white; 32\u201364 separates black from grey without touching anti-aliasing wobble). OPT-IN and disclosed, in the spirit of tolerance_px \u2014 omitted, sweeps score exactly as before; stated, the reply's lum_gate says the seed's own luminance band and names every placement the geometry would have committed and the pen did not, so you can LOOK at what a stated gate cost. Prefer geometry (a counter-example, a tighter seed) where the drawing offers it \u2014 color is the fallback for exports where nothing else survived. Labels (#308): for a LABELED family \u2014 fixtures, tagged equipment, keyed devices \u2014 the drawing already names every instance, and the sweep reads those names: a fixture token written beside a placement, or connected to it by a drawn leader line (leader-following arms only on multi-pen sheets, where the annotation pen separates from the work), comes back as \`label\` + \`label_via\` on the row, and the seed's own tag rides \`seed.label\`. Disclosure in both directions, never a recount: a committed match with NO label while the family is labeled was counted on shape alone (measured case: two 0.97 matches that were valve internals, not drains \u2014 LOOK at those first), a withheld row carrying the seed's own tag is the drawing vouching for a near-miss (look, then place_count), and a withheld row named a DIFFERENT tag is a sibling fixture answered, not a missed count. After any batch commit, LOOK at what landed \u2014 view_sheet {overlay: true} over the swept area \u2014 and audit the markers against the drawing before trusting the EA total. ${COORDS}`,
11633
+ description: `Find EVERY instance of a repeated plan symbol from ONE example \u2014 drains, thresholds, fixtures, transition markers: marquee a tight seed_rect around a single instance and the vector linework is searched for every other placement of that same segment cluster. Deterministic geometry, not vision: each placement scores as the length-weighted fraction of the seed's segments reproduced within tolerance_px, under translation plus 0/90/180/270 rotation and mirroring (symbols rotate on plans \u2014 both ON by default; turn them off to pin orientation). Score \u2265 0.92 is a match; the 0.75\u20130.92 band comes back in \`withheld\` with a reason \u2014 a near-match is a question you answer by LOOKING (view_sheet at its \`at\`), never a silent commit and never a silent drop. The seed's own location is reported in \`seed\` and never double-committed. Every proposed placement is scored up to a hard work ceiling sized for pathological sheets, and the reply says which it was: complete true means the count is a total; complete false (with candidates.dropped > 0) means the count is a FLOOR \u2014 some placements were never scored \u2014 so tighten the seed rect around more distinctive geometry rather than trusting it as a total. Marquee discipline: the rect must hug ONE instance \u2014 only segments FULLY inside it define the symbol, so a loose rect that swallows wall linework fingerprints the wall, not the symbol. scope "set" sweeps the WHOLE working set, counting on PLAN-role sheets only (the sheet graph decides): a symbol drawn in a detail, legend, or schedule is a reference drawing and never counts itself \u2014 which is also how you seed from one: marquee the assembly on the detail sheet and its plan-sheet occurrences are counted while the detail stays excluded (the exclusion disclosed in \`skipped\`, per-sheet results with per-sheet caps and wall-clock in \`sheets\`). Scale across sheets: the fingerprint is size-true and is never scale-SEARCHED, so a detail drawn at 1-1/2" = 1'-0" is 12\xD7 the size of the same mark on a 1/8" plan \u2014 when BOTH sheets have a scale set, the exact ratio is computed from them and the seed is resized before matching (reported per sheet as \`scaled\`); when a scale is missing, the sweep runs at 1:1 and SAYS so (\`scale_assumed\`), because an unknown ratio plus a zero count is not evidence of absence. Seeding from a detail/legend/schedule sheet REFUSES outright until both scales are set \u2014 that is the case where an unstated ratio silently finds nothing. commit: true (requires condition) commits every match center as an EA count marker through the same path as place_count \u2014 the whole sweep (set-wide included) is ONE undo step, each marker carries origin.method "symbol_sweep" with its score, transform, and seed source, and withheld placements are NEVER committed. The SEED instance is not in that count (#296) \u2014 in sheet scope it is almost always installed work, so pass commit_seed: true to mint it into the same batch (the reply reminds you whenever a sheet-scope commit leaves it out; ea_total one short of the hand tally is exactly this). The COUNT is scale-free (EA), but matching across sheets of different scales is not \u2014 set_scale on the sheets involved is what turns the ratio from an assumption into arithmetic. Counter-examples (#259): drafting reuses one generic shape for different devices \u2014 a wall-mounted data outlet drawn as a plain triangle, the flush-floor variant the SAME triangle inside a square, keynote callouts a triangle with a letter in it \u2014 so the seed legitimately matches things you do not mean, and seeding more geometry only works where the drawing offers more to capture. \`exclude\` takes rects around instances you do NOT mean, marqueed exactly like the seed. You never choose a mechanism; the rect's contents decide, because both are the same gesture: a rect holding EXTRA linework beyond the seed rejects placements where that extra linework is present too (the box, the letter), and a rect holding no extra linework of its own is read as the line running THROUGH it \u2014 a bare ceiling-grid tile whose grid line a real fixture, drawn over it, would BREAK. That second mechanic is not expressible as a seed: only segments fully INSIDE a rect define a symbol, and background structure is long by nature. Every rejection is disclosed in rejected[] \u2014 which negative, what fraction of its evidence was found, and the placement \u2014 and NEVER counted in found: an exclusion is a judgement, so look at it and reinstate any you disagree with using place_count at its \`at\`. A counter-example that holds no instance of the seed, or holds the seed with nothing extra, is REFUSED rather than silently doing nothing. Stroke luminance (#260): a flattened export strips the layer tree and flattens every pen, but the file still STATES stroke color \u2014 a black fixture outline over a grey ceiling grid is unambiguous there even when the geometry is identical (two empty 2 ft grid tiles reproduce a 2\xD74 fixture's outline exactly). luminance_tolerance (0\u2013254) gates on it: a sheet segment only answers for a seed segment when their stroke luminances are within the stated tolerance (Rec. 709, 0 = black, 255 = white; 32\u201364 separates black from grey without touching anti-aliasing wobble). OPT-IN and disclosed, in the spirit of tolerance_px \u2014 omitted, sweeps score exactly as before; stated, the reply's lum_gate says the seed's own luminance band and names every placement the geometry would have committed and the pen did not, so you can LOOK at what a stated gate cost. Prefer geometry (a counter-example, a tighter seed) where the drawing offers it \u2014 color is the fallback for exports where nothing else survived. Labels (#308): for a LABELED family \u2014 fixtures, tagged equipment, keyed devices \u2014 the drawing already names every instance, and the sweep reads those names: a fixture token written beside a placement, or connected to it by a drawn leader line (leader-following arms only on multi-pen sheets, where the annotation pen separates from the work), comes back as \`label\` + \`label_via\` on the row, and the seed's own tag rides \`seed.label\`. Disclosure in both directions, never a recount: a committed match with NO label while the family is labeled was counted on shape alone (measured case: two 0.97 matches that were valve internals, not drains \u2014 LOOK at those first), a withheld row carrying the seed's own tag is the drawing vouching for a near-miss (look, then place_count), and a withheld row named a DIFFERENT tag is a sibling fixture answered, not a missed count. After any batch commit, LOOK at what landed \u2014 view_sheet {overlay: true} over the swept area \u2014 and audit the markers against the drawing before trusting the EA total. ${COORDS}`,
11507
11634
  inputSchema: {
11508
11635
  sheet: z2.string().describe("The sheet the seed rect sits on \u2014 in scope 'set' it may be ANY sheet (a detail/legend seed sheet is fingerprint source only, never counted)"),
11509
11636
  seed_rect: z2.tuple([pointSchema, pointSchema]).describe("Marquee around ONE example instance, [[x0,y0],[x1,y1]] in image px \u2014 tight: segments fully inside define the symbol"),
11510
11637
  condition: z2.string().optional().describe("Finish tag to commit match markers under (minted on first use), e.g. 'FD-1'. Required when commit is true"),
11511
11638
  commit: z2.boolean().default(false).describe("Commit every MATCH center as one EA count marker (withheld placements never commit)"),
11639
+ commit_seed: z2.boolean().default(false).describe("Sheet scope + commit only (#296): also commit the SEED instance \u2014 in sheet scope the seed is almost always installed work, and a count that excludes it bids one short. Joins the same one-undo-step batch, origin score 1. Refused in set scope, where a detail/legend seed is a reference drawing"),
11512
11640
  scope: z2.enum(["sheet", "set"]).default("sheet").describe('"sheet" = this sheet only; "set" = every PLAN-role sheet in the working set (needs a text layer for the sheet graph; non-plan sheets are excluded and disclosed)'),
11513
11641
  rotations: z2.boolean().default(true).describe("Also match 90/180/270-rotated placements"),
11514
11642
  mirror: z2.boolean().default(true).describe("Also match mirrored placements"),
@@ -11526,7 +11654,8 @@ function registerTools(realServer, session) {
11526
11654
  mirror: a.mirror,
11527
11655
  tolerancePx: a.tolerance_px,
11528
11656
  exclude: a.exclude,
11529
- luminanceTolerance: a.luminance_tolerance
11657
+ luminanceTolerance: a.luminance_tolerance,
11658
+ commitSeed: a.commit_seed
11530
11659
  })));
11531
11660
  server.registerTool("sweep_schedule_row", {
11532
11661
  description: `Take off a schedule row's mark from the row itself \u2014 the estimator's own gesture: a transition type sometimes exists only as a schedule row plus tag markers scattered across the plan sheets, and this tool mints the condition FROM the row and finds every occurrence. Pass the row's key (e.g. 'T1') and the tool (1) reads the row from the set's schedule tables (the sheet_graph/find_schedule machinery \u2014 the row is the condition's cited source), (2) anchors a geometric fingerprint on the marker the tag is DRAWN as on a plan sheet (a deterministic pad ladder around the tag text; where the tag occurs more than once the fingerprint must recur at a second occurrence before it is trusted \u2014 \`anchor.corroborated\`), and (3) sweeps every PLAN-role sheet for it. The count is geometry AND text agreeing: drafting reuses one bubble shape across many marks, so a match counts ONLY when the row's own tag sits within the marker footprint (its bbox rides the match as \`tag_at\` evidence); a match labeled with a SIBLING row's tag is excluded and says whose it is, an unlabeled match is withheld as a question, and a tag drawn with no matching marker is disclosed as text_only. REFUSAL over guessing, with the reason and the fix: no such row; the same key in two tables (ambiguous); a tag drawn on no plan sheet; no repeatable marker linework around the tag \u2014 a fingerprint is never guessed from text alone (the fallback is always: marquee one instance with symbol_sweep). commit: true commits the counted matches as EA markers under the row's own key \u2014 one undo step for the whole set-wide sweep, every marker carrying origin.assignment {source: "schedule"} plus the anchor and row citation on origin.symbol.seed. The COUNT is scale-free (EA), but matching is not: where the anchor sheet and a target sheet both carry a scale, the marker is resized by their exact ratio before matching (\`scaled\` per sheet), and where one does not, the sweep runs at 1:1 and discloses it (\`scale_assumed\`) rather than reporting a confident zero. After committing, LOOK: view_sheet {overlay: true} over each swept sheet. ${COORDS}`,
@@ -11779,19 +11908,24 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
11779
11908
  outputSchema: findTextOutput
11780
11909
  }, run("find_text", (a) => session.findText(a.sheet, a.q, { region: a.region, limit: a.limit })));
11781
11910
  server.registerTool("view_sheet", {
11782
- description: `SEE the sheet \u2014 render the page (or a crop of it) to a PNG image. This is your eyes on the plan, so CROP, DON'T SQUINT: the render downsamples to the px budget (\u22642000 long side), which on an E-size sheet is ~4 sheet pixels per returned pixel \u2014 a full-sheet render finds WHERE things are, and only a tight region crop can tell you what the linework and labels actually say. Never audit a trace or read a dimension off a full-sheet render. region is in image px \u2014 the same space as every other tool \u2014 so a feature at pixel (ix, iy) of the returned image sits at x = region_x0 + ix \xD7 (region_x1 \u2212 region_x0) / img_w (same for y), and those coordinates go straight into one_click, measure_polygon, or read_sheet_text. overlay:true burns the session's committed shapes into the render (human-affirmed ink solid red, unreviewed machine shapes dashed blue) \u2014 render again after committing to verify your geometry landed where you intended, and sanity-check what you see: a fixture-sized ring where a room should be means the seed landed inside a stall or casework; an outsized ring means the flood escaped through an opening. To MEASURE rather than guess, pass grid: a calibrated measuring grid is burned in \u2014 thin lines every 1 ft, heavy blue every 5 ft, foot labels along the crop edges, feet counted from the crop's top-left corner. Count grid cells between walls exactly like an estimator scaling a plan; never derive a dimension by eye when the grid can give it to you. grid "auto" uses the sheet's set scale; before set_scale, pass the drawing scale read off the title block as inches-per-foot \u2014 "1/4" for a 1/4" = 1'-0" plan, "3/16", "0.25". Rendering needs the optional native canvas (@napi-rs/canvas); where it isn't installed this tool errors cleanly and every other tool still works. ${COORDS}`,
11911
+ description: `SEE the sheet \u2014 render the page (or a crop of it) to a PNG image. This is your eyes on the plan, so CROP, DON'T SQUINT: the render downsamples to the px budget (\u22642000 long side), which on an E-size sheet is ~4 sheet pixels per returned pixel \u2014 a full-sheet render finds WHERE things are, and only a tight region crop can tell you what the linework and labels actually say. Never audit a trace or read a dimension off a full-sheet render. region is in image px \u2014 the same space as every other tool \u2014 so a feature at pixel (ix, iy) of the returned image sits at x = region_x0 + ix \xD7 (region_x1 \u2212 region_x0) / img_w (same for y), and those coordinates go straight into one_click, measure_polygon, or read_sheet_text. overlay:true burns the session's committed shapes into the render (human-affirmed ink solid red, unreviewed machine shapes dashed blue) \u2014 render again after committing to verify your geometry landed where you intended, and sanity-check what you see: a fixture-sized ring where a room should be means the seed landed inside a stall or casework; an outsized ring means the flood escaped through an opening. To MEASURE rather than guess, pass grid: a calibrated measuring grid is burned in \u2014 thin lines every 1 ft, heavy blue every 5 ft, foot labels along the crop edges, feet counted from the crop's top-left corner. Count grid cells between walls exactly like an estimator scaling a plan; never derive a dimension by eye when the grid can give it to you. grid "auto" uses the sheet's set scale; before set_scale, pass the drawing scale read off the title block as inches-per-foot \u2014 "1/4" for a 1/4" = 1'-0" plan, "3/16", "0.25". marks (#297) burns DISCLOSURE layers into the render, so what a reply names, the picture shows: pass the coordinate lists a tool disclosed \u2014 question: withheld placements (orange ?-circles), struck: rejections a counter-example or luminance gate refused (magenta struck \xD7), ring: reference points like the sweep's own seed (violet double ring). The colors sit deliberately off the common CAD pens so they cannot vanish into color-plotted work. An overlay audit without marks shows only committed ink \u2014 the validation trap where 37 disclosed near-misses read as "it missed them". Rendering needs the optional native canvas (@napi-rs/canvas); where it isn't installed this tool errors cleanly and every other tool still works. ${COORDS}`,
11783
11912
  inputSchema: {
11784
11913
  sheet: z2.string(),
11785
11914
  region: z2.object({ x0: z2.number(), y0: z2.number(), x1: z2.number(), y1: z2.number() }).optional().describe("Crop rect in image px (origin top-left, y down); omit for the full sheet"),
11786
11915
  px: z2.number().int().min(200).max(2e3).optional().describe("Long-side pixel budget of the returned image (default 1400) \u2014 small region + high px = readable dimension strings"),
11787
11916
  overlay: z2.boolean().optional().describe("Burn committed shapes into the render (solid = human-affirmed, dashed = unreviewed)"),
11788
- grid: z2.string().optional().describe(`Burn in a calibrated 1-ft/5-ft measuring grid: "auto" = the sheet's set scale; otherwise the drawing scale as inches-per-foot, e.g. "1/4", "3/16", "0.25"`)
11917
+ grid: z2.string().optional().describe(`Burn in a calibrated 1-ft/5-ft measuring grid: "auto" = the sheet's set scale; otherwise the drawing scale as inches-per-foot, e.g. "1/4", "3/16", "0.25"`),
11918
+ marks: z2.object({
11919
+ question: z2.array(pointSchema).optional().describe("Open questions \u2014 withheld placements, spots to look at. Orange ?-in-circle"),
11920
+ struck: z2.array(pointSchema).optional().describe("Refusals \u2014 rejected[] placements, lum_gate.at. Magenta struck \xD7"),
11921
+ ring: z2.array(pointSchema).optional().describe("Reference points \u2014 the sweep's seed.center, an anchor. Violet double ring")
11922
+ }).optional().describe("Disclosure marks to burn into the render (#297): what the reply names, the picture shows. Coordinates in image px")
11789
11923
  }
11790
11924
  }, async (a) => {
11791
11925
  const startedAt = process.hrtime.bigint();
11792
11926
  let reply;
11793
11927
  try {
11794
- const { png, meta } = await session.viewSheet(a.sheet, { region: a.region, px: a.px, overlay: a.overlay, grid: a.grid });
11928
+ const { png, meta } = await session.viewSheet(a.sheet, { region: a.region, px: a.px, overlay: a.overlay, grid: a.grid, marks: a.marks });
11795
11929
  reply = okImage(png, meta);
11796
11930
  } catch (e) {
11797
11931
  reply = fail(e);
@@ -12077,7 +12211,7 @@ function nameTheStageInRefusals(server) {
12077
12211
  // package.json
12078
12212
  var package_default = {
12079
12213
  name: "opentakeoff-mcp",
12080
- version: "0.9.61",
12214
+ version: "0.9.63",
12081
12215
  mcpName: "io.github.Kentucky-ai/opentakeoff",
12082
12216
  type: "module",
12083
12217
  description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opentakeoff-mcp",
3
- "version": "0.9.61",
3
+ "version": "0.9.63",
4
4
  "mcpName": "io.github.Kentucky-ai/opentakeoff",
5
5
  "type": "module",
6
6
  "description": "OpenTakeoff MCP server — drive the takeoff engine from your MCP client over stdio.",