opentakeoff-mcp 0.9.45 → 0.9.47
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/dist/server-core.js +233 -32
- package/package.json +1 -1
package/dist/server-core.js
CHANGED
|
@@ -2677,34 +2677,98 @@ function clusterRows(spans) {
|
|
|
2677
2677
|
return rows.map((r) => r.sort((a, b) => a.x - b.x));
|
|
2678
2678
|
}
|
|
2679
2679
|
var rowY = (r) => r.reduce((s, t) => s + t.y, 0) / r.length;
|
|
2680
|
-
var
|
|
2680
|
+
var SURFACE_WORDS = /* @__PURE__ */ new Set(["FLOOR", "BASE", "WALL", "WALLS", "CEILING", "NORTH", "SOUTH", "EAST", "WEST", "WAINSCOT"]);
|
|
2681
|
+
var ROOM_HEADERS = ["ROOM", "NO", "NUMBER", "NAME", "MARK", "LOCATION", "FLOOR", "BASE", "WALL", "WALLS", "NORTH", "SOUTH", "EAST", "WEST", "CEILING", "WAINSCOT", "REMARKS", "CLG", "HT", "HEIGHT", "FINISH", "CASEWORK", "CABINET", "COUNTER", "COUNTERTOP", "BLDG", "BUILDING"];
|
|
2681
2682
|
var FINISH_HEADERS = ["CODE", "MARK", "SYMBOL", "MATERIAL", "MANUFACTURER", "PRODUCT", "STYLE", "COLOR", "SIZE", "REMARKS", "DESCRIPTION", "PATTERN", "COMMENTS"];
|
|
2682
|
-
var headerLabel = (s, vocab) =>
|
|
2683
|
-
|
|
2684
|
-
|
|
2683
|
+
var headerLabel = (s, vocab) => headerLabels(s, vocab)[0] ?? null;
|
|
2684
|
+
var headerLabels = (s, vocab) => {
|
|
2685
|
+
const out = [];
|
|
2686
|
+
for (const w of norm(s).split(/[^A-Z]+/)) if (w && vocab.includes(w) && !out.includes(w)) out.push(w);
|
|
2687
|
+
return out;
|
|
2688
|
+
};
|
|
2689
|
+
function headerHits(row, vocab) {
|
|
2690
|
+
const out = [];
|
|
2691
|
+
for (const t of row) {
|
|
2692
|
+
const w = headerLabel(t.str, vocab);
|
|
2693
|
+
if (w) out.push({ label: w, span: t });
|
|
2694
|
+
}
|
|
2695
|
+
return out.sort((a, b) => a.span.x - b.span.x);
|
|
2696
|
+
}
|
|
2697
|
+
var qualifies = (hits, required, minHits) => {
|
|
2698
|
+
const seen = new Set(hits.map((h) => h.label));
|
|
2699
|
+
return seen.size >= minHits && required.some((r) => seen.has(r));
|
|
2685
2700
|
};
|
|
2686
2701
|
function findHeaderRow(rows, vocab, required, minHits) {
|
|
2687
2702
|
for (let i = 0; i < rows.length; i++) {
|
|
2703
|
+
let hits = headerHits(rows[i], vocab);
|
|
2704
|
+
if (!qualifies(hits, required, minHits)) continue;
|
|
2705
|
+
let idx = i;
|
|
2706
|
+
for (; ; ) {
|
|
2707
|
+
let next = -1;
|
|
2708
|
+
for (let j = idx + 1; j < Math.min(idx + 4, rows.length); j++) {
|
|
2709
|
+
const h = headerHits(rows[j], vocab);
|
|
2710
|
+
const ratio = h.length / Math.max(1, rows[j].length);
|
|
2711
|
+
if (qualifies(h, required, minHits) && h.length > hits.length && ratio >= 0.6) {
|
|
2712
|
+
next = j;
|
|
2713
|
+
break;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
if (next < 0) break;
|
|
2717
|
+
idx = next;
|
|
2718
|
+
hits = headerHits(rows[idx], vocab);
|
|
2719
|
+
}
|
|
2720
|
+
const dup = /* @__PURE__ */ new Set();
|
|
2721
|
+
const once = /* @__PURE__ */ new Set();
|
|
2722
|
+
for (const h of hits) (once.has(h.label) ? dup : once).add(h.label);
|
|
2688
2723
|
const anchors = [];
|
|
2689
|
-
const
|
|
2690
|
-
for (
|
|
2691
|
-
const
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2724
|
+
const used = /* @__PURE__ */ new Set();
|
|
2725
|
+
for (let j = 0; j < hits.length; j++) {
|
|
2726
|
+
const h = hits[j];
|
|
2727
|
+
let label = h.label;
|
|
2728
|
+
if (dup.has(h.label) && !SURFACE_WORDS.has(h.label)) {
|
|
2729
|
+
const hi = j + 1 < hits.length ? hits[j + 1].span.x : Infinity;
|
|
2730
|
+
const parent = parentLabelOver(rows, idx, i, h.span.x, hi, vocab);
|
|
2731
|
+
if (parent && parent !== h.label) label = `${parent} ${h.label}`;
|
|
2732
|
+
}
|
|
2733
|
+
if (used.has(label)) {
|
|
2734
|
+
const alt = headerLabels(h.span.str, vocab).find((w) => !used.has(w));
|
|
2735
|
+
if (alt) label = alt;
|
|
2736
|
+
}
|
|
2737
|
+
if (used.has(label)) continue;
|
|
2738
|
+
used.add(label);
|
|
2739
|
+
anchors.push({ label, x: h.span.x + (h.span.w || 0) / 2 });
|
|
2740
|
+
}
|
|
2741
|
+
if (anchors.length < minHits) continue;
|
|
2742
|
+
if (idx > i) {
|
|
2743
|
+
const lo = Math.min(...anchors.map((a) => a.x)), hi = Math.max(...anchors.map((a) => a.x));
|
|
2744
|
+
for (let j = i; j < idx; j++) {
|
|
2745
|
+
for (const h of headerHits(rows[j], vocab)) {
|
|
2746
|
+
const cx = h.span.x + (h.span.w || 0) / 2;
|
|
2747
|
+
if (cx >= lo && cx <= hi) continue;
|
|
2748
|
+
if (used.has(h.label)) continue;
|
|
2749
|
+
used.add(h.label);
|
|
2750
|
+
anchors.push({ label: h.label, x: cx });
|
|
2751
|
+
}
|
|
2695
2752
|
}
|
|
2696
2753
|
}
|
|
2697
|
-
|
|
2698
|
-
return { anchors: subTierAnchors(rows, i, anchors.sort((a, b) => a.x - b.x), vocab), rowIndex: i };
|
|
2754
|
+
return { anchors: subTierAnchors(rows, idx, anchors.sort((a, b) => a.x - b.x), vocab), rowIndex: idx };
|
|
2699
2755
|
}
|
|
2700
2756
|
return null;
|
|
2701
2757
|
}
|
|
2702
2758
|
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
|
-
|
|
2759
|
+
function parentLabelOver(rows, hdrIdx, topIdx, gx0, gx1, vocab) {
|
|
2760
|
+
const width = Math.max(Math.min(gx1, gx0 + 4e3) - gx0, 1);
|
|
2761
|
+
const hs = rows[hdrIdx].map((t) => t.h || 8).sort((a, b) => a - b);
|
|
2762
|
+
const near = Math.max(24, (hs[hs.length >> 1] || 8) * 4);
|
|
2763
|
+
const hy = rowY(rows[hdrIdx]);
|
|
2764
|
+
const floorIdx = Math.max(0, Math.min(topIdx, hdrIdx - 8));
|
|
2765
|
+
for (let j = hdrIdx - 1; j >= floorIdx; j--) {
|
|
2766
|
+
if (hy - rowY(rows[j]) > near) break;
|
|
2706
2767
|
for (const t of rows[j]) {
|
|
2707
|
-
|
|
2768
|
+
const cx = t.x + (t.w || 0) / 2;
|
|
2769
|
+
const inInterval = cx >= gx0 && cx < gx1;
|
|
2770
|
+
const overlaps = Math.min(t.x + (t.w || 0), gx1) - Math.max(t.x, gx0) > width * 0.3;
|
|
2771
|
+
if (!inInterval && !overlaps) continue;
|
|
2708
2772
|
const lbl = headerLabel(t.str, vocab);
|
|
2709
2773
|
if (lbl) return lbl;
|
|
2710
2774
|
}
|
|
@@ -2733,7 +2797,7 @@ function subTierAnchors(rows, hdrIdx, anchors, vocab) {
|
|
|
2733
2797
|
for (const r of runs) {
|
|
2734
2798
|
if (r.length < 2) continue;
|
|
2735
2799
|
const last = r[r.length - 1];
|
|
2736
|
-
const parent = parentLabelOver(rows, hdrIdx, r[0].x, last.x + (last.w || 0), vocab);
|
|
2800
|
+
const parent = parentLabelOver(rows, hdrIdx, hdrIdx - 2, r[0].x, last.x + (last.w || 0), vocab);
|
|
2737
2801
|
if (!parent) continue;
|
|
2738
2802
|
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
2803
|
for (const t of r) {
|
|
@@ -2819,14 +2883,78 @@ function rowKeyOf(raw, kind, buildings) {
|
|
|
2819
2883
|
}
|
|
2820
2884
|
var numOf = (key) => key.match(QUALIFIED_KEY_RE)?.[2] ?? key;
|
|
2821
2885
|
var centerX = (t) => t.x + (t.w || 0) / 2;
|
|
2886
|
+
function columnMapFor(rows, anchors, cfg, x0, x1, coord) {
|
|
2887
|
+
const at = (t) => coord === "left" ? t.x : t.x + (t.w || 0) / 2;
|
|
2888
|
+
const xs = [];
|
|
2889
|
+
const hs = [];
|
|
2890
|
+
for (let i = Math.max(cfg.fromIdx, 0); i < rows.length; i++) {
|
|
2891
|
+
if (rowY(rows[i]) <= cfg.belowY) continue;
|
|
2892
|
+
for (const t of rows[i]) {
|
|
2893
|
+
if (t.x < x0 || t.x > x1 || revisionOf(t.str) != null) continue;
|
|
2894
|
+
xs.push(at(t));
|
|
2895
|
+
hs.push(t.h || 8);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
if (xs.length < anchors.length * 2) return null;
|
|
2899
|
+
hs.sort((a, b) => a - b);
|
|
2900
|
+
const tol = Math.max(4, hs[hs.length >> 1] * 0.5);
|
|
2901
|
+
xs.sort((a, b) => a - b);
|
|
2902
|
+
const clusters = [];
|
|
2903
|
+
for (const x of xs) {
|
|
2904
|
+
const last = clusters[clusters.length - 1];
|
|
2905
|
+
if (last && x - last.start <= tol) {
|
|
2906
|
+
last.n++;
|
|
2907
|
+
continue;
|
|
2908
|
+
}
|
|
2909
|
+
clusters.push({ start: x, n: 1 });
|
|
2910
|
+
}
|
|
2911
|
+
const maxN = Math.max(...clusters.map((c) => c.n));
|
|
2912
|
+
const kept = clusters.filter((c) => c.n >= Math.max(2, maxN * 0.25));
|
|
2913
|
+
if (kept.length < anchors.length) return null;
|
|
2914
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
2915
|
+
for (const c of kept) {
|
|
2916
|
+
const own = anchors.find((a) => a.x >= c.start);
|
|
2917
|
+
if (!own) continue;
|
|
2918
|
+
const cur = byLabel.get(own.label);
|
|
2919
|
+
if (cur == null || c.start < cur) byLabel.set(own.label, c.start);
|
|
2920
|
+
}
|
|
2921
|
+
if (byLabel.size !== anchors.length) return null;
|
|
2922
|
+
const cols = [...byLabel.entries()].map(([label, start]) => ({ label, start })).sort((a, b) => a.start - b.start);
|
|
2923
|
+
if (cols.map((c) => c.label).join("|") !== anchors.map((a) => a.label).join("|")) return null;
|
|
2924
|
+
const starts = kept.map((c) => c.start);
|
|
2925
|
+
let on = 0;
|
|
2926
|
+
for (const x of xs) if (starts.some((st) => Math.abs(x - st) <= tol)) on++;
|
|
2927
|
+
return { coord, cols, score: on / xs.length };
|
|
2928
|
+
}
|
|
2929
|
+
function columnStarts(rows, anchors, cfg, x0, x1) {
|
|
2930
|
+
const FIT_FLOOR = 0.7;
|
|
2931
|
+
const fits = (m) => m && m.score >= FIT_FLOOR ? m : null;
|
|
2932
|
+
const left = fits(columnMapFor(rows, anchors, cfg, x0, x1, "left"));
|
|
2933
|
+
const center = fits(columnMapFor(rows, anchors, cfg, x0, x1, "center"));
|
|
2934
|
+
if (!left) return center;
|
|
2935
|
+
if (!center) return left;
|
|
2936
|
+
return center.score > left.score + 0.05 ? center : left;
|
|
2937
|
+
}
|
|
2822
2938
|
function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
2823
2939
|
const { x0, x1, medGap } = bandLimits(anchors);
|
|
2940
|
+
const cols = columnStarts(rows, anchors, cfg, x0, x1);
|
|
2941
|
+
const keyTol = cols && cols.cols.length > 1 ? Math.max(8, (cols.cols[1].start - cols.cols[0].start) * 0.5) : 40;
|
|
2824
2942
|
const out = [];
|
|
2825
2943
|
const outY = [];
|
|
2826
2944
|
let region = null;
|
|
2945
|
+
const columnOf = (t) => {
|
|
2946
|
+
if (!cols) return nearestAnchor(centerX(t), anchors);
|
|
2947
|
+
const at = cols.coord === "left" ? t.x : centerX(t);
|
|
2948
|
+
let label = cols.cols[0].label;
|
|
2949
|
+
for (const c of cols.cols) {
|
|
2950
|
+
if (at + 1 >= c.start) label = c.label;
|
|
2951
|
+
else break;
|
|
2952
|
+
}
|
|
2953
|
+
return label;
|
|
2954
|
+
};
|
|
2827
2955
|
const add = (row, toks) => {
|
|
2828
2956
|
for (const t of toks) {
|
|
2829
|
-
const label =
|
|
2957
|
+
const label = columnOf(t);
|
|
2830
2958
|
const text = t.str.trim();
|
|
2831
2959
|
if (!row.cells[label]) row.cells[label] = { text, bbox: bboxOf(t) };
|
|
2832
2960
|
else row.cells[label] = { text: `${row.cells[label].text} ${text}`, bbox: merge(row.cells[label].bbox, bboxOf(t)) };
|
|
@@ -2853,6 +2981,7 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
2853
2981
|
orphans.push({ toks: banded, y: rowY(rows[i]) });
|
|
2854
2982
|
continue;
|
|
2855
2983
|
}
|
|
2984
|
+
if (cols && Math.abs((cols.coord === "left" ? banded[0].x : centerX(banded[0])) - cols.cols[0].start) > keyTol) continue;
|
|
2856
2985
|
if (cfg.keyAlign && Math.abs(centerX(banded[0]) - cfg.keyAlign.x) > cfg.keyAlign.tol) continue;
|
|
2857
2986
|
const row = { key: keyed.key, sheet: sheetKey, cells: {} };
|
|
2858
2987
|
if (keyed.building) row.building = keyed.building;
|
|
@@ -2860,6 +2989,21 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
2860
2989
|
out.push(row);
|
|
2861
2990
|
outY.push(rowY(rows[i]));
|
|
2862
2991
|
}
|
|
2992
|
+
if (out.length > 2) {
|
|
2993
|
+
const d = outY.slice(1).map((y, i) => y - outY[i]).filter((g) => g > 0).sort((a, b) => a - b);
|
|
2994
|
+
const pitch0 = d.length ? d[d.length >> 1] : 0;
|
|
2995
|
+
if (pitch0 > 0) {
|
|
2996
|
+
let end = out.length;
|
|
2997
|
+
for (let i = 1; i < outY.length; i++) if (outY[i] - outY[i - 1] > pitch0 * 8) {
|
|
2998
|
+
end = i;
|
|
2999
|
+
break;
|
|
3000
|
+
}
|
|
3001
|
+
if (end < out.length) {
|
|
3002
|
+
out.length = end;
|
|
3003
|
+
outY.length = end;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
2863
3007
|
const gaps = outY.slice(1).map((y, i) => y - outY[i]).filter((d) => d > 0).sort((a, b) => a - b);
|
|
2864
3008
|
const pitch = gaps.length ? gaps[gaps.length >> 1] : 0;
|
|
2865
3009
|
const nearest = (y) => {
|
|
@@ -2922,8 +3066,12 @@ function extractTable(sheet, kind, opts = {}) {
|
|
|
2922
3066
|
titleFrom = rows.findIndex((r) => rowY(r) >= rot.top) - 1;
|
|
2923
3067
|
if (titleFrom < -1) titleFrom = rows.length - 1;
|
|
2924
3068
|
}
|
|
3069
|
+
const hdrBand = bandLimits(anchors);
|
|
2925
3070
|
let region = null;
|
|
2926
|
-
for (const t of headerSpans)
|
|
3071
|
+
for (const t of headerSpans) {
|
|
3072
|
+
if (centerX(t) < hdrBand.x0 || centerX(t) > hdrBand.x1) continue;
|
|
3073
|
+
region = region ? merge(region, bboxOf(t)) : bboxOf(t);
|
|
3074
|
+
}
|
|
2927
3075
|
const banded = bandDataRows(rows, anchors, kind, sheet.key, opts.buildings, { fromIdx: dataFrom, belowY: dataBelowY, deltas: opts.deltas });
|
|
2928
3076
|
const out = banded.out;
|
|
2929
3077
|
if (banded.region) region = region ? merge(region, banded.region) : banded.region;
|
|
@@ -2983,6 +3131,7 @@ function adoptContinuationRows(sheet, titleSpan, base, buildings, deltas) {
|
|
|
2983
3131
|
};
|
|
2984
3132
|
}
|
|
2985
3133
|
var QUALIFIED_TAG_RE = /^([A-Z]{1,2})-(\d{2,3}[A-Z]?)$/;
|
|
3134
|
+
var NON_ROOM_NAME = /* @__PURE__ */ new Set(["NUMBER", "NO", "NAME", "MARK", "SYMBOL", "CODE", "TYPE", "QTY", "SIZE", "TOTAL", "SHEET", "DATE", "SCALE", "REV", "REVISION", "DESCRIPTION", "REMARKS", "COMMENTS", "DETAIL", "ROOM"]);
|
|
2986
3135
|
function roomTags(sheet, opts = {}) {
|
|
2987
3136
|
const out = [];
|
|
2988
3137
|
const spans = sheet.spans;
|
|
@@ -3007,7 +3156,10 @@ function roomTags(sheet, opts = {}) {
|
|
|
3007
3156
|
const dy = b[1] - cb[3];
|
|
3008
3157
|
if (dy < -hgt * 0.2 || dy > hgt * 2.2) continue;
|
|
3009
3158
|
if (cb[2] < b[0] - hgt || cb[0] > b[2] + hgt) continue;
|
|
3010
|
-
|
|
3159
|
+
const raw = cand.str.trim();
|
|
3160
|
+
if (/[a-z]/.test(raw)) continue;
|
|
3161
|
+
if (!/^[A-Z][A-Z .'’\/&-]{1,}$/.test(norm(raw))) continue;
|
|
3162
|
+
if (NON_ROOM_NAME.has(norm(raw))) continue;
|
|
3011
3163
|
if (dy < best) {
|
|
3012
3164
|
best = dy;
|
|
3013
3165
|
name = cand.str.trim();
|
|
@@ -3054,7 +3206,7 @@ function detailCallouts(sheet) {
|
|
|
3054
3206
|
}
|
|
3055
3207
|
function buildSheetGraph(sheets) {
|
|
3056
3208
|
const withText = sheets.filter((s) => s.spans.length > 0);
|
|
3057
|
-
if (!withText.length) return { available: false, sheets: [], rooms: [], tables: [], callouts: [], buildings: [], revisions: [], notes: [] };
|
|
3209
|
+
if (!withText.length) return { available: false, sheets: [], rooms: [], unmatched_tags: [], tables: [], callouts: [], buildings: [], revisions: [], notes: [] };
|
|
3058
3210
|
const notes = [];
|
|
3059
3211
|
const deltasBySheet = /* @__PURE__ */ new Map();
|
|
3060
3212
|
const revisions = [];
|
|
@@ -3132,19 +3284,46 @@ function buildSheetGraph(sheets) {
|
|
|
3132
3284
|
const n = norm(s.sheet_number || "").replace(/[^A-Z0-9]/g, "");
|
|
3133
3285
|
if (n) sheetNumbers.add(n);
|
|
3134
3286
|
}
|
|
3135
|
-
const
|
|
3287
|
+
const found = [];
|
|
3136
3288
|
const callouts = [];
|
|
3137
3289
|
for (const s of withText) {
|
|
3138
3290
|
const role = roles.get(s.key);
|
|
3139
|
-
|
|
3291
|
+
const suppresses = (role.role === "schedule" || role.role === "legend" || role.role === "elevation" || role.role === "detail") && role.confidence >= 0.6;
|
|
3292
|
+
if (!suppresses) {
|
|
3140
3293
|
const ctxB = ctxBySheet.get(s.key);
|
|
3141
3294
|
for (const r of roomTags(s, { buildings, exclude: sheetNumbers, deltas: deltasBySheet.get(s.key) })) {
|
|
3142
3295
|
if (r.building == null && ctxB) r.building = ctxB;
|
|
3143
|
-
|
|
3296
|
+
found.push(r);
|
|
3144
3297
|
}
|
|
3145
3298
|
}
|
|
3146
3299
|
callouts.push(...detailCallouts(s));
|
|
3147
3300
|
}
|
|
3301
|
+
const roomRows = tables.filter((t) => t.kind === "room-finish");
|
|
3302
|
+
const scheduleNums = /* @__PURE__ */ new Set();
|
|
3303
|
+
for (const t of roomRows) for (const r of t.rows) scheduleNums.add(numOf(norm(r.key)));
|
|
3304
|
+
const rooms = [];
|
|
3305
|
+
const unmatched = [];
|
|
3306
|
+
for (const r of found) {
|
|
3307
|
+
const num2 = numOf(norm(r.tag).replace(/\s+/g, ""));
|
|
3308
|
+
const byName = !!r.name.trim();
|
|
3309
|
+
const bySchedule = scheduleNums.has(num2);
|
|
3310
|
+
if (bySchedule || byName && !roomRows.length) {
|
|
3311
|
+
r.corroboration = bySchedule ? byName ? "name+schedule" : "schedule" : "name";
|
|
3312
|
+
rooms.push(r);
|
|
3313
|
+
} else {
|
|
3314
|
+
unmatched.push({
|
|
3315
|
+
tag: r.tag,
|
|
3316
|
+
sheet: r.sheet,
|
|
3317
|
+
bbox: r.bbox,
|
|
3318
|
+
...r.building ? { building: r.building } : {},
|
|
3319
|
+
...byName ? { name: r.name } : {},
|
|
3320
|
+
reason: !roomRows.length ? "no room name drawn with it, and the set carries no room-finish schedule to check it against" : byName ? `"${r.name}" is drawn with it but no room-finish row answers for it \u2014 either a room the schedule omits, or a keynote/legend row; LOOK before pricing it` : "no room name drawn with it and no room-finish row answers for it \u2014 reads as a keynote, detail marker or dimension fragment rather than a room"
|
|
3321
|
+
});
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
if (unmatched.length) {
|
|
3325
|
+
notes.push(`${unmatched.length} numbered tag(s) on plan sheets are NOT counted as rooms \u2014 no name drawn with them and no schedule row answers for them; see unmatched_tags (they are listed, never dropped)`);
|
|
3326
|
+
}
|
|
3148
3327
|
const outSheets = withText.map((s) => {
|
|
3149
3328
|
const role = roles.get(s.key);
|
|
3150
3329
|
const schedules = [];
|
|
@@ -3168,7 +3347,7 @@ function buildSheetGraph(sheets) {
|
|
|
3168
3347
|
if (b) entry.building = b;
|
|
3169
3348
|
return entry;
|
|
3170
3349
|
});
|
|
3171
|
-
return { available: true, sheets: outSheets, rooms, tables, callouts, buildings: [...buildings].sort(), revisions, notes };
|
|
3350
|
+
return { available: true, sheets: outSheets, rooms, unmatched_tags: unmatched, tables, callouts, buildings: [...buildings].sort(), revisions, notes };
|
|
3172
3351
|
}
|
|
3173
3352
|
var SURFACE_HEADERS = ["FLOOR", "BASE", "WALL", "WALLS", "NORTH", "SOUTH", "EAST", "WEST", "CEILING", "WAINSCOT"];
|
|
3174
3353
|
var surfaceRank = (label) => SURFACE_HEADERS.indexOf(label.split(" ")[0]);
|
|
@@ -3177,7 +3356,9 @@ function resolveTag(graph, tag) {
|
|
|
3177
3356
|
const q = t.match(QUALIFIED_KEY_RE);
|
|
3178
3357
|
const wantB = q ? q[1] : null;
|
|
3179
3358
|
const num2 = q ? q[2] : t;
|
|
3180
|
-
const
|
|
3359
|
+
const asRoom = (u) => ({ tag: u.tag, name: u.name ?? "", sheet: u.sheet, bbox: u.bbox, ...u.building ? { building: u.building } : {} });
|
|
3360
|
+
const candidates = [...graph.rooms, ...graph.unmatched_tags.map(asRoom)];
|
|
3361
|
+
const rooms = candidates.filter((r2) => {
|
|
3181
3362
|
const rt = norm(r2.tag).replace(/\s+/g, "");
|
|
3182
3363
|
return rt === t || numOf(rt) === num2;
|
|
3183
3364
|
});
|
|
@@ -7796,6 +7977,7 @@ var Session = class _Session {
|
|
|
7796
7977
|
sheet: r.sheet,
|
|
7797
7978
|
bbox: _Session.wireBox(r.bbox),
|
|
7798
7979
|
...r.building ? { building: r.building } : {},
|
|
7980
|
+
...r.corroboration ? { corroboration: r.corroboration } : {},
|
|
7799
7981
|
...r.revision ? { revision: { rev: r.revision.rev, source: _Session.wireEvidence(r.revision.source), ...r.revision.drawn ? { drawn: true } : {} } } : {}
|
|
7800
7982
|
};
|
|
7801
7983
|
}
|
|
@@ -7819,11 +8001,21 @@ var Session = class _Session {
|
|
|
7819
8001
|
}))
|
|
7820
8002
|
})),
|
|
7821
8003
|
rooms: g.rooms.map(_Session.wireRoom),
|
|
8004
|
+
...g.unmatched_tags.length ? {
|
|
8005
|
+
unmatched_tags: g.unmatched_tags.map((u) => ({
|
|
8006
|
+
tag: u.tag,
|
|
8007
|
+
sheet: u.sheet,
|
|
8008
|
+
bbox: _Session.wireBox(u.bbox),
|
|
8009
|
+
...u.building ? { building: u.building } : {},
|
|
8010
|
+
...u.name ? { name: u.name } : {},
|
|
8011
|
+
reason: u.reason
|
|
8012
|
+
}))
|
|
8013
|
+
} : {},
|
|
7822
8014
|
callouts: g.callouts.map((c) => ({ detail: c.detail, target_sheet: c.target_sheet, sheet: c.sheet, bbox: _Session.wireBox(c.bbox) })),
|
|
7823
8015
|
...g.buildings.length ? { buildings: g.buildings } : {},
|
|
7824
8016
|
...g.revisions.length ? { revisions: g.revisions.map((r) => ({ rev: r.rev, sheet: r.sheet, bbox: _Session.wireBox(r.bbox), ...r.drawn ? { drawn: true } : {} })) } : {},
|
|
7825
8017
|
...g.notes.length ? { notes: g.notes } : {},
|
|
7826
|
-
counts: { rooms: g.rooms.length, schedules: g.tables.length, callouts: g.callouts.length }
|
|
8018
|
+
counts: { rooms: g.rooms.length, unmatched_tags: g.unmatched_tags.length, schedules: g.tables.length, callouts: g.callouts.length }
|
|
7827
8019
|
};
|
|
7828
8020
|
}
|
|
7829
8021
|
/** The FLOOR finish a room's own schedule row states — assign-from-schedule's
|
|
@@ -8466,7 +8658,8 @@ var graphRoom = z.object({
|
|
|
8466
8658
|
sheet: z.string(),
|
|
8467
8659
|
bbox: wireBox,
|
|
8468
8660
|
building: z.string().optional().describe("The building the room belongs to, when the set names one \u2014 its plan sheet's BUILDING/BLDG context, or the tag's own qualifier ('A-134')"),
|
|
8469
|
-
revision: wireRevision.optional()
|
|
8661
|
+
revision: wireRevision.optional(),
|
|
8662
|
+
corroboration: z.string().optional().describe('Why this number is believed to be a room: "schedule" (a room-finish row answers for it), "name" (a name is drawn with it and the set has no room-finish schedule), or "name+schedule"')
|
|
8470
8663
|
});
|
|
8471
8664
|
var sheetGraphOutput = {
|
|
8472
8665
|
available: z.boolean().describe("false = the set has no text layer (a scan) \u2014 the graph degrades to unavailable, never half-populates"),
|
|
@@ -8485,12 +8678,20 @@ var sheetGraphOutput = {
|
|
|
8485
8678
|
rotated_headers: z.boolean().optional().describe("true when the column headers were read at a quarter-turn")
|
|
8486
8679
|
}))
|
|
8487
8680
|
})),
|
|
8488
|
-
rooms: z.array(graphRoom).describe("
|
|
8681
|
+
rooms: z.array(graphRoom).describe("Numbers CORROBORATED as rooms \u2014 a room-finish row answers for them, or (where the set carries no room-finish schedule) a room name is drawn with them. Each says which in `corroboration`. Schedule sheets contribute rows, never phantom rooms"),
|
|
8682
|
+
unmatched_tags: z.array(z.object({
|
|
8683
|
+
tag: z.string(),
|
|
8684
|
+
sheet: z.string(),
|
|
8685
|
+
bbox: wireBox,
|
|
8686
|
+
building: z.string().optional(),
|
|
8687
|
+
name: z.string().optional().describe("Text drawn with the number, when there is any \u2014 on a keynote legend this is the accessory description, not a room name"),
|
|
8688
|
+
reason: z.string().describe("WHY this number is not counted as a room. Read these: one of them may be a room the schedule left out, which is a hole in the bid")
|
|
8689
|
+
})).optional().describe('Numbered tags on plan sheets that are NOT counted as rooms \u2014 keynote hexagons, detail markers, dimension fragments, legend rows. Listed with a reason, never dropped. A real finish plan is covered in 2\u20133 digit numbers that are not rooms; counting them as rooms makes every one come back "no schedule row", which reads exactly like the lost-bid case and buries it'),
|
|
8489
8690
|
callouts: z.array(z.object({ detail: z.string(), target_sheet: z.string(), sheet: z.string(), bbox: wireBox })).describe("Detail callouts (3/A-601) \u2014 edges to their target sheets"),
|
|
8490
8691
|
buildings: z.array(z.string()).optional().describe("Every building designator the set names (sorted) \u2014 present only on multi-building-aware sets. Room numbers reused across these need qualified tags ('A-134')"),
|
|
8491
8692
|
revisions: z.array(z.object({ rev: z.string(), sheet: z.string(), bbox: wireBox, drawn: z.boolean().optional() })).optional().describe("Every delta-triangle / REV-tag marker the set carries \u2014 text markers ('\u03942', 'REV 2') and DRAWN deltas (a bare digit inside a triangle of linework, drawn: true) \u2014 where one sits, the ink changed under that revision. Markers on a schedule row or room bubble also attach there (and ride resolve_tag). A revision CLOUD is arc-chain linework these detectors do not read \u2014 absence here is not absence of revisions"),
|
|
8492
8693
|
notes: z.array(z.string()).optional().describe("Named gaps found while indexing (e.g. a continuation whose rows could not be aligned) \u2014 the graph refuses silently dropping anything"),
|
|
8493
|
-
counts: z.object({ rooms: z.number().int(), schedules: z.number().int().describe("LOGICAL tables \u2014 a schedule continued across sheets counts once"), callouts: z.number().int() })
|
|
8694
|
+
counts: z.object({ rooms: z.number().int(), unmatched_tags: z.number().int().optional(), schedules: z.number().int().describe("LOGICAL tables \u2014 a schedule continued across sheets counts once"), callouts: z.number().int() })
|
|
8494
8695
|
};
|
|
8495
8696
|
var resolveTagOutput = {
|
|
8496
8697
|
status: z.enum(["resolved", "unresolved"]),
|
|
@@ -10627,7 +10828,7 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
10627
10828
|
outputSchema: undoLastOutput
|
|
10628
10829
|
}, run("undo_last", ({ n }) => session.undoLast(n)));
|
|
10629
10830
|
server.registerTool("sheet_graph", {
|
|
10630
|
-
description: `The plan-set INDEX (#87): every sheet's role (plan / schedule / legend / \u2026, with confidence and the title evidence), the schedule tables found (kind, row count, region \u2014 a schedule CONTINUED across sheets ("\u2026 SCHEDULE \u2014 CONT'D") reads as ONE table, the continuation fragment naming its base in "continues"; rotated column headers are read at their quarter-turn and flagged), every
|
|
10831
|
+
description: `The plan-set INDEX (#87): every sheet's role (plan / schedule / legend / \u2026, with confidence and the title evidence), the schedule tables found (kind, row count, region \u2014 a schedule CONTINUED across sheets ("\u2026 SCHEDULE \u2014 CONT'D") reads as ONE table, the continuation fragment naming its base in "continues"; rotated column headers are read at their quarter-turn and flagged), every number CORROBORATED as a room (with the stacked room NAME when one exists, the room's BUILDING on multi-building sets, and "corroboration" saying why it counts as a room) plus "unmatched_tags" \u2014 the numbers that are NOT rooms (keynote hexagons, detail markers, dimension fragments, legend rows), each with a reason, listed and never dropped; READ those reasons, one of them may be a room the schedule left out, the detail callouts (3/A-601 \u2192 sheet edges), the set's building designators, every REVISION marker the set carries (text markers "\u03942"/"REV 2" AND drawn deltas \u2014 a bare digit inside a triangle of linework, proven from vector geometry and flagged drawn \u2014 in "revisions", and attached to the schedule row / room tag they sit on), and named indexing gaps in "notes". Built once per document from the text layer and cached. This is how an agent decides WHAT to measure without a human enumerating the rooms: list the rooms here, resolve each with resolve_tag, then measure with one_click/detect_rooms. A scanned set (no text layer) returns available: false \u2014 unavailable, never half-populated. ${COORDS}`,
|
|
10631
10832
|
inputSchema: {},
|
|
10632
10833
|
outputSchema: sheetGraphOutput
|
|
10633
10834
|
}, run("sheet_graph", () => session.sheetGraph()));
|
|
@@ -10918,7 +11119,7 @@ function applyStagedTools(server, registered) {
|
|
|
10918
11119
|
// package.json
|
|
10919
11120
|
var package_default = {
|
|
10920
11121
|
name: "opentakeoff-mcp",
|
|
10921
|
-
version: "0.9.
|
|
11122
|
+
version: "0.9.47",
|
|
10922
11123
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
10923
11124
|
type: "module",
|
|
10924
11125
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
package/package.json
CHANGED