opentakeoff-mcp 0.9.44 → 0.9.45

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.
Files changed (2) hide show
  1. package/dist/server-core.js +80 -10
  2. package/package.json +1 -1
@@ -2678,7 +2678,7 @@ function clusterRows(spans) {
2678
2678
  }
2679
2679
  var rowY = (r) => r.reduce((s, t) => s + t.y, 0) / r.length;
2680
2680
  var ROOM_HEADERS = ["ROOM", "NO", "NUMBER", "NAME", "FLOOR", "BASE", "WALL", "WALLS", "NORTH", "SOUTH", "EAST", "WEST", "CEILING", "WAINSCOT", "REMARKS", "CLG", "HT", "BLDG", "BUILDING"];
2681
- var FINISH_HEADERS = ["CODE", "MARK", "MATERIAL", "MANUFACTURER", "PRODUCT", "STYLE", "COLOR", "SIZE", "REMARKS", "DESCRIPTION", "PATTERN"];
2681
+ var FINISH_HEADERS = ["CODE", "MARK", "SYMBOL", "MATERIAL", "MANUFACTURER", "PRODUCT", "STYLE", "COLOR", "SIZE", "REMARKS", "DESCRIPTION", "PATTERN", "COMMENTS"];
2682
2682
  var headerLabel = (s, vocab) => {
2683
2683
  for (const w of norm(s).split(/[^A-Z]+/)) if (w && vocab.includes(w)) return w;
2684
2684
  return null;
@@ -2695,10 +2695,57 @@ function findHeaderRow(rows, vocab, required, minHits) {
2695
2695
  }
2696
2696
  }
2697
2697
  if (anchors.length < minHits || !required.some((r) => seen.has(r))) continue;
2698
- return { anchors: anchors.sort((a, b) => a.x - b.x), rowIndex: i };
2698
+ return { anchors: subTierAnchors(rows, i, anchors.sort((a, b) => a.x - b.x), vocab), rowIndex: i };
2699
2699
  }
2700
2700
  return null;
2701
2701
  }
2702
+ var SUB_LABEL_RE = /^[A-Z0-9][A-Z0-9.\/-]{0,5}$/;
2703
+ function parentLabelOver(rows, hdrIdx, gx0, gx1, vocab) {
2704
+ const width = Math.max(gx1 - gx0, 1);
2705
+ for (let j = hdrIdx - 1; j >= 0 && j >= hdrIdx - 2; j--) {
2706
+ for (const t of rows[j]) {
2707
+ if (Math.min(t.x + (t.w || 0), gx1) - Math.max(t.x, gx0) <= width * 0.3) continue;
2708
+ const lbl = headerLabel(t.str, vocab);
2709
+ if (lbl) return lbl;
2710
+ }
2711
+ }
2712
+ return null;
2713
+ }
2714
+ function subTierAnchors(rows, hdrIdx, anchors, vocab) {
2715
+ const lo = anchors[0].x, hi = anchors[anchors.length - 1].x;
2716
+ const loose = rows[hdrIdx].filter((t) => !headerLabel(t.str, vocab) && SUB_LABEL_RE.test(norm(t.str))).filter((t) => t.x + (t.w || 0) / 2 > lo && t.x + (t.w || 0) / 2 < hi).sort((a, b) => a.x - b.x);
2717
+ if (loose.length < 2) return anchors;
2718
+ const mid = (t) => t.x + (t.w || 0) / 2;
2719
+ const gaps = loose.slice(1).map((t, i) => mid(t) - mid(loose[i])).sort((a, b) => a - b);
2720
+ const med = gaps[gaps.length >> 1] || 1;
2721
+ const runs = [];
2722
+ let run2 = [loose[0]];
2723
+ for (let i = 1; i < loose.length; i++) {
2724
+ if (mid(loose[i]) - mid(loose[i - 1]) > med * 3) {
2725
+ runs.push(run2);
2726
+ run2 = [];
2727
+ }
2728
+ run2.push(loose[i]);
2729
+ }
2730
+ runs.push(run2);
2731
+ const out = anchors.slice();
2732
+ const used = new Set(anchors.map((a) => a.label));
2733
+ for (const r of runs) {
2734
+ if (r.length < 2) continue;
2735
+ const last = r[r.length - 1];
2736
+ const parent = parentLabelOver(rows, hdrIdx, r[0].x, last.x + (last.w || 0), vocab);
2737
+ if (!parent) continue;
2738
+ const pitch = r.length > 1 ? r.slice(1).map((t, i) => mid(t) - mid(r[i])).sort((a, b) => a - b)[r.length - 1 >> 1] : 0;
2739
+ for (const t of r) {
2740
+ const label = `${parent} ${norm(t.str)}`;
2741
+ if (used.has(label)) continue;
2742
+ used.add(label);
2743
+ const c = mid(t);
2744
+ out.push(pitch > 0 ? { label, x: c, x0: c - pitch / 2, x1: c + pitch / 2 } : { label, x: c });
2745
+ }
2746
+ }
2747
+ return out.sort((a, b) => a.x - b.x);
2748
+ }
2702
2749
  function findRotatedHeader(vert, vocab, required, minHits) {
2703
2750
  const cands = vert.map((sp) => ({ sp, label: headerLabel(sp.str, vocab) })).filter((c) => !!c.label).sort((a, b) => a.sp.x - b.sp.x);
2704
2751
  let band = [];
@@ -2733,18 +2780,35 @@ function findRotatedHeader(vert, vocab, required, minHits) {
2733
2780
  return band.length ? flush() : null;
2734
2781
  }
2735
2782
  var nearestAnchor = (x, anchors) => {
2736
- let best = anchors[0];
2737
- for (const a of anchors) if (Math.abs(a.x - x) < Math.abs(best.x - x)) best = a;
2738
- return best.label;
2783
+ let inside2 = null;
2784
+ for (const a of anchors) {
2785
+ if (a.x0 == null || a.x1 == null || x < a.x0 || x > a.x1) continue;
2786
+ if (!inside2 || Math.abs(a.x - x) < Math.abs(inside2.x - x)) inside2 = a;
2787
+ }
2788
+ if (inside2) return inside2.label;
2789
+ let best = null;
2790
+ for (const a of anchors) {
2791
+ if (a.x0 != null) continue;
2792
+ if (!best || Math.abs(a.x - x) < Math.abs(best.x - x)) best = a;
2793
+ }
2794
+ return (best ?? anchors[0]).label;
2739
2795
  };
2796
+ var WIDE_LAST = /* @__PURE__ */ new Set(["REMARKS", "DESCRIPTION", "NOTES"]);
2740
2797
  function bandLimits(anchors) {
2741
2798
  const gaps = anchors.slice(1).map((a, i) => a.x - anchors[i].x).sort((a, b) => a - b);
2742
2799
  const medGap = gaps.length ? gaps[gaps.length >> 1] : 150;
2743
- return { x0: anchors[0].x - Math.max(80, medGap / 2), x1: anchors[anchors.length - 1].x + Math.max(300, medGap * 3), medGap };
2800
+ const last = anchors[anchors.length - 1];
2801
+ const rightMargin = WIDE_LAST.has(last.label) ? Math.max(300, medGap * 3) : Math.max(120, medGap);
2802
+ return { x0: anchors[0].x - Math.max(80, medGap / 2), x1: last.x + rightMargin, medGap };
2744
2803
  }
2745
2804
  var CODE_RE = /^[A-Z]{1,4}(-?[A-Z0-9]{1,4})?$/;
2746
2805
  var ROW_KEY_RE = /^\d{1,3}[A-Z]{0,2}$/;
2747
2806
  var QUALIFIED_KEY_RE = /^([A-Z]{1,2})-(\d{1,3}[A-Z]{0,2})$/;
2807
+ var OTHER_FAMILY_RE = /\b(DOOR|WINDOW|PARTITION|EQUIPMENT|HARDWARE|LOUVER|SIGNAGE|LIGHTING|LUMINAIRE|PLUMBING|MECHANICAL|ELECTRICAL|STOREFRONT|GLAZING|CASEWORK|MILLWORK|APPLIANCE)S?\b/;
2808
+ var isNonFinishSchedule = (title) => {
2809
+ const u = norm(title);
2810
+ return OTHER_FAMILY_RE.test(u) && !/\b(FINISH|MATERIAL)S?\b/.test(u);
2811
+ };
2748
2812
  function rowKeyOf(raw, kind, buildings) {
2749
2813
  const key = norm(raw).replace(/[^A-Z0-9-]/g, "");
2750
2814
  if (kind === "finish") return CODE_RE.test(key) ? { key } : null;
@@ -2833,7 +2897,7 @@ function extractTable(sheet, kind, opts = {}) {
2833
2897
  const vert = sheet.spans.filter(isVertical);
2834
2898
  const rows = clusterRows(horiz);
2835
2899
  const vocab = kind === "room-finish" ? ROOM_HEADERS : FINISH_HEADERS;
2836
- const required = kind === "room-finish" ? ["FLOOR", "BASE"] : ["CODE", "MARK"];
2900
+ const required = kind === "room-finish" ? ["FLOOR", "BASE"] : ["CODE", "MARK", "SYMBOL"];
2837
2901
  const minHits = kind === "room-finish" ? 4 : 3;
2838
2902
  let anchors;
2839
2903
  let headerSpans;
@@ -2943,7 +3007,7 @@ function roomTags(sheet, opts = {}) {
2943
3007
  const dy = b[1] - cb[3];
2944
3008
  if (dy < -hgt * 0.2 || dy > hgt * 2.2) continue;
2945
3009
  if (cb[2] < b[0] - hgt || cb[0] > b[2] + hgt) continue;
2946
- if (!/^[A-Z][A-Z .\/&-]{2,}$/.test(norm(cand.str))) continue;
3010
+ if (!/^[A-Z][A-Z .'’\/&-]{2,}$/.test(norm(cand.str))) continue;
2947
3011
  if (dy < best) {
2948
3012
  best = dy;
2949
3013
  name = cand.str.trim();
@@ -3022,6 +3086,10 @@ function buildSheetGraph(sheets) {
3022
3086
  for (const kind of ["room-finish", "finish"]) {
3023
3087
  const t = extractTable(s, kind, { buildings, deltas: deltasBySheet.get(s.key) });
3024
3088
  if (!t) continue;
3089
+ if (kind === "finish" && t.title && isNonFinishSchedule(t.title.text)) {
3090
+ notes.push(`${s.key}: "${t.title.text}" names another schedule family, not a finish/material schedule \u2014 its ${t.rows.length} rows are NOT indexed as finish definitions`);
3091
+ continue;
3092
+ }
3025
3093
  const titleB = t.title ? buildingMentions(t.title.text) : [];
3026
3094
  const b = titleB.length === 1 ? titleB[0] : ctxBySheet.get(s.key);
3027
3095
  if (b) t.building = b;
@@ -3103,6 +3171,7 @@ function buildSheetGraph(sheets) {
3103
3171
  return { available: true, sheets: outSheets, rooms, tables, callouts, buildings: [...buildings].sort(), revisions, notes };
3104
3172
  }
3105
3173
  var SURFACE_HEADERS = ["FLOOR", "BASE", "WALL", "WALLS", "NORTH", "SOUTH", "EAST", "WEST", "CEILING", "WAINSCOT"];
3174
+ var surfaceRank = (label) => SURFACE_HEADERS.indexOf(label.split(" ")[0]);
3106
3175
  function resolveTag(graph, tag) {
3107
3176
  const t = norm(tag).replace(/\s+/g, "");
3108
3177
  const q = t.match(QUALIFIED_KEY_RE);
@@ -3171,7 +3240,8 @@ function resolveTag(graph, tag) {
3171
3240
  const finishes = [];
3172
3241
  const sources = [{ sheet: r.sheet, text: `${tab.title?.text || "room-finish schedule"} row ${r.key}`, bbox: r.cells[Object.keys(r.cells)[0]]?.bbox || tab.region }];
3173
3242
  if (room) sources.unshift({ sheet: room.sheet, text: `${room.name ? room.name + " " : ""}${room.tag}`.trim(), bbox: room.bbox });
3174
- for (const surface of SURFACE_HEADERS) {
3243
+ const surfaces = Object.keys(r.cells).filter((k) => surfaceRank(k) >= 0).sort((a, b) => surfaceRank(a) - surfaceRank(b) || a.localeCompare(b));
3244
+ for (const surface of surfaces) {
3175
3245
  const cell = r.cells[surface];
3176
3246
  if (!cell || !cell.text.trim()) continue;
3177
3247
  const code = norm(cell.text).replace(/[^A-Z0-9-]/g, "");
@@ -10848,7 +10918,7 @@ function applyStagedTools(server, registered) {
10848
10918
  // package.json
10849
10919
  var package_default = {
10850
10920
  name: "opentakeoff-mcp",
10851
- version: "0.9.44",
10921
+ version: "0.9.45",
10852
10922
  mcpName: "io.github.Kentucky-ai/opentakeoff",
10853
10923
  type: "module",
10854
10924
  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.44",
3
+ "version": "0.9.45",
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.",