opentakeoff-mcp 0.9.68 → 0.9.71
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 +129 -24
- package/package.json +2 -2
package/dist/server-core.js
CHANGED
|
@@ -233,18 +233,22 @@ function _findScales(canon) {
|
|
|
233
233
|
}
|
|
234
234
|
return out;
|
|
235
235
|
}
|
|
236
|
+
function _scaleHits(parts) {
|
|
237
|
+
const joined = _findScales(parts.map(_canonScaleText).filter(Boolean).join("|"));
|
|
238
|
+
return joined.length ? joined : _findScales(_canonScaleText(parts.join(" ")));
|
|
239
|
+
}
|
|
236
240
|
function detectScale(textContent, viewport) {
|
|
237
241
|
const W = viewport.width, H = viewport.height;
|
|
238
|
-
|
|
242
|
+
const all = [], tb = [];
|
|
239
243
|
for (const it of textContent.items || []) {
|
|
240
244
|
const str = it.str || "";
|
|
241
245
|
if (!str.trim()) continue;
|
|
242
|
-
all
|
|
246
|
+
all.push(str);
|
|
243
247
|
const t = pdfjsLib.Util.transform(viewport.transform, it.transform);
|
|
244
|
-
if (t[4] > W * 0.55 && t[5] > H * 0.5) tb
|
|
248
|
+
if (t[4] > W * 0.55 && t[5] > H * 0.5) tb.push(str);
|
|
245
249
|
}
|
|
246
|
-
const tbHits =
|
|
247
|
-
const allHits =
|
|
250
|
+
const tbHits = _scaleHits(tb);
|
|
251
|
+
const allHits = _scaleHits(all);
|
|
248
252
|
if (tbHits.length) return { upp: tbHits[0].upp, label: tbHits[0].label, multi: allHits.length > 1 };
|
|
249
253
|
if (allHits.length === 1) return { upp: allHits[0].upp, label: allHits[0].label, multi: false };
|
|
250
254
|
return null;
|
|
@@ -3209,6 +3213,7 @@ function floodAtSeed(maskObj, ix, iy, sensitivity = SENS_BALANCED, maskPxPerFt =
|
|
|
3209
3213
|
return floodRegionSealed(maskObj, ix, iy, sensitivity, a.radii, a.wedgeCapPx, a.minPassPx);
|
|
3210
3214
|
}
|
|
3211
3215
|
var BUBBLE_RATIO = 2.5;
|
|
3216
|
+
var BUBBLE_WIDE_RATIO = 6;
|
|
3212
3217
|
function seedLadderPx(b, first = "anchor") {
|
|
3213
3218
|
const cx = (b.x0 + b.x1) / 2, cy = (b.y0 + b.y1) / 2;
|
|
3214
3219
|
const h = Math.max(b.y1 - b.y0, 1);
|
|
@@ -3216,6 +3221,25 @@ function seedLadderPx(b, first = "anchor") {
|
|
|
3216
3221
|
const rest = [[cx, cy - 2 * h], [cx, cy + 3.5 * h]];
|
|
3217
3222
|
return first === "below-box" ? [below, center, ...rest] : [center, below, ...rest];
|
|
3218
3223
|
}
|
|
3224
|
+
var SURROUND_MIN_SIDES = 3;
|
|
3225
|
+
function floodSurroundsLabelPx(f, b) {
|
|
3226
|
+
const inRegion = (x, y) => {
|
|
3227
|
+
const mx = Math.round(x * f.ws), my = Math.round(y * f.ws);
|
|
3228
|
+
return mx >= 0 && my >= 0 && mx < f.mw && my < f.mh && !!f.region[my * f.mw + mx];
|
|
3229
|
+
};
|
|
3230
|
+
const h = Math.max(b.y1 - b.y0, 1);
|
|
3231
|
+
const cx = (b.x0 + b.x1) / 2, cy = (b.y0 + b.y1) / 2, w = b.x1 - b.x0;
|
|
3232
|
+
const alongX = [cx - 0.35 * w, cx, cx + 0.35 * w];
|
|
3233
|
+
const alongY = [cy - 0.3 * h, cy, cy + 0.3 * h];
|
|
3234
|
+
const vd = [0.6, 1.1, 1.6].map((k) => k * h);
|
|
3235
|
+
const hd = [1.3, 1.8, 2.3].map((k) => k * h);
|
|
3236
|
+
let sides = 0;
|
|
3237
|
+
if (alongX.some((x) => vd.some((d) => inRegion(x, b.y0 - d)))) sides++;
|
|
3238
|
+
if (alongX.some((x) => vd.some((d) => inRegion(x, b.y1 + d)))) sides++;
|
|
3239
|
+
if (alongY.some((y) => hd.some((d) => inRegion(b.x0 - d, y)))) sides++;
|
|
3240
|
+
if (alongY.some((y) => hd.some((d) => inRegion(b.x1 + d, y)))) sides++;
|
|
3241
|
+
return sides >= SURROUND_MIN_SIDES;
|
|
3242
|
+
}
|
|
3219
3243
|
function isLabelBubblePx(ring, b) {
|
|
3220
3244
|
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
3221
3245
|
for (const [x, y] of ring) {
|
|
@@ -3225,7 +3249,9 @@ function isLabelBubblePx(ring, b) {
|
|
|
3225
3249
|
if (y > y1) y1 = y;
|
|
3226
3250
|
}
|
|
3227
3251
|
const lw = Math.max(b.x1 - b.x0, 1e-6), lh = Math.max(b.y1 - b.y0, 1e-6);
|
|
3228
|
-
|
|
3252
|
+
const rw = x1 - x0, rh = y1 - y0;
|
|
3253
|
+
if (rw <= BUBBLE_RATIO * lw && rh <= BUBBLE_RATIO * lh) return true;
|
|
3254
|
+
return rh <= BUBBLE_RATIO * lh && rw <= BUBBLE_WIDE_RATIO * lw;
|
|
3229
3255
|
}
|
|
3230
3256
|
|
|
3231
3257
|
// ../web/src/lib/sheetgraph.ts
|
|
@@ -3378,7 +3404,7 @@ function clusterRows(spans) {
|
|
|
3378
3404
|
}
|
|
3379
3405
|
var rowY = (r) => r.reduce((s, t) => s + t.y, 0) / r.length;
|
|
3380
3406
|
var SURFACE_WORDS = /* @__PURE__ */ new Set(["FLOOR", "BASE", "WALL", "WALLS", "CEILING", "NORTH", "SOUTH", "EAST", "WEST", "WAINSCOT"]);
|
|
3381
|
-
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"];
|
|
3407
|
+
var ROOM_HEADERS = ["ROOM", "NO", "NUMBER", "NAME", "MARK", "LOCATION", "FLOOR", "BASE", "WALL", "WALLS", "NORTH", "SOUTH", "EAST", "WEST", "CEILING", "WAINSCOT", "REMARKS", "CLG", "HT", "HEIGHT", "FINISH", "MAT", "MATERIAL", "COMMENTS", "CASEWORK", "CABINET", "COUNTER", "COUNTERTOP", "BLDG", "BUILDING"];
|
|
3382
3408
|
var FINISH_HEADERS = ["CODE", "MARK", "SYMBOL", "TAG", "MATERIAL", "MANUFACTURER", "PRODUCT", "STYLE", "COLOR", "SIZE", "REMARKS", "DESCRIPTION", "PATTERN", "COMMENTS"];
|
|
3383
3409
|
var headerLabel = (s, vocab) => headerLabels(s, vocab)[0] ?? null;
|
|
3384
3410
|
var headerLabels = (s, vocab) => {
|
|
@@ -3392,6 +3418,15 @@ function headerHits(row, vocab) {
|
|
|
3392
3418
|
for (const t of row) {
|
|
3393
3419
|
const words = headerLabels(t.str, vocab);
|
|
3394
3420
|
if (!words.length) continue;
|
|
3421
|
+
const all = norm(t.str).split(/[^A-Z]+/).filter(Boolean);
|
|
3422
|
+
if (all.length >= 2 && all.every((w2) => SURFACE_WORDS.has(w2))) {
|
|
3423
|
+
const n = all.length, w2 = (t.w || 0) / n;
|
|
3424
|
+
all.forEach((word, k) => {
|
|
3425
|
+
used.add(word);
|
|
3426
|
+
out.push({ label: word, span: { ...t, x: t.x + w2 * k, w: w2 } });
|
|
3427
|
+
});
|
|
3428
|
+
continue;
|
|
3429
|
+
}
|
|
3395
3430
|
const w = words.find((word) => !used.has(word)) ?? words[0];
|
|
3396
3431
|
used.add(w);
|
|
3397
3432
|
out.push({ label: w, span: t });
|
|
@@ -3400,7 +3435,7 @@ function headerHits(row, vocab) {
|
|
|
3400
3435
|
}
|
|
3401
3436
|
var qualifies = (hits, required, minHits) => {
|
|
3402
3437
|
const seen = new Set(hits.map((h) => h.label));
|
|
3403
|
-
return seen.size >= minHits && required.some((r) => seen.has(r));
|
|
3438
|
+
return seen.size >= minHits && (!required.length || required.some((r) => seen.has(r)));
|
|
3404
3439
|
};
|
|
3405
3440
|
function findHeaderRow(rows, vocab, required, minHits) {
|
|
3406
3441
|
for (let i = 0; i < rows.length; i++) {
|
|
@@ -3412,7 +3447,7 @@ function findHeaderRow(rows, vocab, required, minHits) {
|
|
|
3412
3447
|
for (let j = idx + 1; j < Math.min(idx + 4, rows.length); j++) {
|
|
3413
3448
|
const h = headerHits(rows[j], vocab);
|
|
3414
3449
|
const ratio = h.length / Math.max(1, rows[j].length);
|
|
3415
|
-
if (qualifies(h,
|
|
3450
|
+
if (qualifies(h, [], minHits) && h.length > hits.length && ratio >= 0.6) {
|
|
3416
3451
|
next = j;
|
|
3417
3452
|
break;
|
|
3418
3453
|
}
|
|
@@ -3426,13 +3461,21 @@ function findHeaderRow(rows, vocab, required, minHits) {
|
|
|
3426
3461
|
for (const h of hits) (once.has(h.label) ? dup : once).add(h.label);
|
|
3427
3462
|
const anchors = [];
|
|
3428
3463
|
const used = /* @__PURE__ */ new Set();
|
|
3464
|
+
const parents = [];
|
|
3429
3465
|
for (let j = 0; j < hits.length; j++) {
|
|
3430
3466
|
const h = hits[j];
|
|
3467
|
+
parents[j] = null;
|
|
3431
3468
|
let label = h.label;
|
|
3432
3469
|
if (dup.has(h.label) && !SURFACE_WORDS.has(h.label)) {
|
|
3433
3470
|
const hi = j + 1 < hits.length ? hits[j + 1].span.x : Infinity;
|
|
3434
|
-
const
|
|
3435
|
-
|
|
3471
|
+
const lo2 = j > 0 ? (hits[j - 1].span.x + (hits[j - 1].span.w || 0) + h.span.x) / 2 : h.span.x;
|
|
3472
|
+
const hi2 = j + 1 < hits.length ? (h.span.x + (h.span.w || 0) + hits[j + 1].span.x) / 2 : hi;
|
|
3473
|
+
let parent = parentLabelOver(rows, idx, i, h.span.x, hi, vocab) ?? parentLabelOver(rows, idx, i, lo2, hi2, vocab);
|
|
3474
|
+
if (!parent && j > 0 && parents[j - 1] && h.span.x - (hits[j - 1].span.x + (hits[j - 1].span.w || 0)) < bandLimits(hits.map((x) => ({ label: x.label, x: x.span.x }))).medGap * 1.5) parent = parents[j - 1];
|
|
3475
|
+
if (parent && parent !== h.label) {
|
|
3476
|
+
label = `${parent} ${h.label}`;
|
|
3477
|
+
parents[j] = parent;
|
|
3478
|
+
}
|
|
3436
3479
|
}
|
|
3437
3480
|
if (used.has(label)) {
|
|
3438
3481
|
const alt = headerLabels(h.span.str, vocab).find((w) => !used.has(w));
|
|
@@ -3445,10 +3488,12 @@ function findHeaderRow(rows, vocab, required, minHits) {
|
|
|
3445
3488
|
if (anchors.length < minHits) continue;
|
|
3446
3489
|
if (idx > i) {
|
|
3447
3490
|
const lo = Math.min(...anchors.map((a) => a.x)), hi = Math.max(...anchors.map((a) => a.x));
|
|
3491
|
+
const reach = bandLimits(anchors).medGap * 2;
|
|
3448
3492
|
for (let j = i; j < idx; j++) {
|
|
3449
3493
|
for (const h of headerHits(rows[j], vocab)) {
|
|
3450
3494
|
const cx = h.span.x + (h.span.w || 0) / 2;
|
|
3451
3495
|
if (cx >= lo && cx <= hi) continue;
|
|
3496
|
+
if (cx < lo - reach || cx > hi + reach) continue;
|
|
3452
3497
|
if (used.has(h.label)) continue;
|
|
3453
3498
|
used.add(h.label);
|
|
3454
3499
|
anchors.push({ label: h.label, x: cx });
|
|
@@ -3561,7 +3606,7 @@ var nearestAnchor = (x, anchors) => {
|
|
|
3561
3606
|
}
|
|
3562
3607
|
return (best ?? anchors[0]).label;
|
|
3563
3608
|
};
|
|
3564
|
-
var WIDE_LAST = /* @__PURE__ */ new Set(["REMARKS", "DESCRIPTION", "NOTES"]);
|
|
3609
|
+
var WIDE_LAST = /* @__PURE__ */ new Set(["REMARKS", "DESCRIPTION", "NOTES", "COMMENTS"]);
|
|
3565
3610
|
function bandLimits(anchors) {
|
|
3566
3611
|
const gaps = anchors.slice(1).map((a, i) => a.x - anchors[i].x).sort((a, b) => a - b);
|
|
3567
3612
|
const medGap = gaps.length ? gaps[gaps.length >> 1] : 150;
|
|
@@ -3572,6 +3617,7 @@ function bandLimits(anchors) {
|
|
|
3572
3617
|
var CODE_RE = /^[A-Z]{1,4}(-?[A-Z0-9]{1,4})?$/;
|
|
3573
3618
|
var ROW_KEY_RE = /^\d{1,3}[A-Z]{0,2}$/;
|
|
3574
3619
|
var QUALIFIED_KEY_RE = /^([A-Z]{1,2})-(\d{1,3}[A-Z]{0,2})$/;
|
|
3620
|
+
var CORRIDOR_KEY_RE = /^[A-Z]{1,3}(?:\d{1,3}-\d{1,3}|\d{3})[A-Z]?$/;
|
|
3575
3621
|
var OTHER_FAMILY_RE = /\b(DOOR|WINDOW|PARTITION|EQUIPMENT|HARDWARE|LOUVER|SIGNAGE|LIGHTING|LUMINAIRE|PLUMBING|MECHANICAL|ELECTRICAL|STOREFRONT|GLAZING|CASEWORK|MILLWORK|APPLIANCE)S?\b/;
|
|
3576
3622
|
var isNonFinishSchedule = (title) => {
|
|
3577
3623
|
const u = norm(title);
|
|
@@ -3586,6 +3632,7 @@ function rowKeyOf(raw, kind, buildings) {
|
|
|
3586
3632
|
return CODE_RE.test(key) ? { key } : null;
|
|
3587
3633
|
}
|
|
3588
3634
|
if (ROW_KEY_RE.test(key)) return { key };
|
|
3635
|
+
if (CORRIDOR_KEY_RE.test(key)) return { key };
|
|
3589
3636
|
const q = key.match(QUALIFIED_KEY_RE);
|
|
3590
3637
|
if (q && buildings?.has(q[1])) return { key, building: q[1] };
|
|
3591
3638
|
return null;
|
|
@@ -3597,14 +3644,20 @@ var rowKeyAnswersFor = (key, want) => {
|
|
|
3597
3644
|
};
|
|
3598
3645
|
var numOf = (key) => key.match(QUALIFIED_KEY_RE)?.[2] ?? key;
|
|
3599
3646
|
var centerX = (t) => t.x + (t.w || 0) / 2;
|
|
3647
|
+
var PLACEHOLDER_RE = /^[-–—]{1,3}$/;
|
|
3600
3648
|
function columnMapFor(rows, anchors, cfg, x0, x1, coord) {
|
|
3601
3649
|
const at = (t) => coord === "left" ? t.x : t.x + (t.w || 0) / 2;
|
|
3602
3650
|
const xs = [];
|
|
3603
3651
|
const hs = [];
|
|
3652
|
+
const dashes = [];
|
|
3604
3653
|
for (let i = Math.max(cfg.fromIdx, 0); i < rows.length; i++) {
|
|
3605
3654
|
if (rowY(rows[i]) <= cfg.belowY) continue;
|
|
3606
3655
|
for (const t of rows[i]) {
|
|
3607
3656
|
if (t.x < x0 || t.x > x1 || revisionOf(t.str) != null) continue;
|
|
3657
|
+
if (PLACEHOLDER_RE.test(t.str.trim())) {
|
|
3658
|
+
dashes.push(at(t));
|
|
3659
|
+
continue;
|
|
3660
|
+
}
|
|
3608
3661
|
xs.push(at(t));
|
|
3609
3662
|
hs.push(t.h || 8);
|
|
3610
3663
|
}
|
|
@@ -3632,6 +3685,24 @@ function columnMapFor(rows, anchors, cfg, x0, x1, coord) {
|
|
|
3632
3685
|
const cur = byLabel.get(own.label);
|
|
3633
3686
|
if (cur == null || c.start < cur) byLabel.set(own.label, c.start);
|
|
3634
3687
|
}
|
|
3688
|
+
if (byLabel.size < anchors.length && dashes.length) {
|
|
3689
|
+
dashes.sort((a, b) => a - b);
|
|
3690
|
+
const dc = [];
|
|
3691
|
+
for (const x of dashes) {
|
|
3692
|
+
const last = dc[dc.length - 1];
|
|
3693
|
+
if (last && x - last.start <= tol) {
|
|
3694
|
+
last.n++;
|
|
3695
|
+
continue;
|
|
3696
|
+
}
|
|
3697
|
+
dc.push({ start: x, n: 1 });
|
|
3698
|
+
}
|
|
3699
|
+
for (const c of dc.filter((d) => d.n >= 2)) {
|
|
3700
|
+
const own = anchors.find((a) => a.x >= c.start);
|
|
3701
|
+
if (!own || byLabel.has(own.label)) continue;
|
|
3702
|
+
const real = clusters.filter((k) => k.n >= 1 && anchors.find((a) => a.x >= k.start)?.label === own.label).sort((a, b) => a.start - b.start)[0];
|
|
3703
|
+
byLabel.set(own.label, real ? real.start : c.start);
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3635
3706
|
if (byLabel.size !== anchors.length) return null;
|
|
3636
3707
|
const cols = [...byLabel.entries()].map(([label, start]) => ({ label, start })).sort((a, b) => a.start - b.start);
|
|
3637
3708
|
if (cols.map((c) => c.label).join("|") !== anchors.map((a) => a.label).join("|")) return null;
|
|
@@ -3664,6 +3735,19 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
3664
3735
|
if (at + 1 >= c.start) label = c.label;
|
|
3665
3736
|
else break;
|
|
3666
3737
|
}
|
|
3738
|
+
if (cols.coord === "left" && (t.w || 0) > 0) {
|
|
3739
|
+
const x12 = t.x + (t.w || 0);
|
|
3740
|
+
let best = label, bestOv = -1;
|
|
3741
|
+
for (let ci = 0; ci < cols.cols.length; ci++) {
|
|
3742
|
+
const lo = cols.cols[ci].start, hi = ci + 1 < cols.cols.length ? cols.cols[ci + 1].start : Infinity;
|
|
3743
|
+
const ov = Math.min(hi, x12) - Math.max(lo, t.x);
|
|
3744
|
+
if (ov > bestOv) {
|
|
3745
|
+
bestOv = ov;
|
|
3746
|
+
best = cols.cols[ci].label;
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
if (bestOv > 0) label = best;
|
|
3750
|
+
}
|
|
3667
3751
|
return label;
|
|
3668
3752
|
};
|
|
3669
3753
|
const add = (row, toks) => {
|
|
@@ -4969,6 +5053,16 @@ function buildRasterMask(rgba, mw, mh, ws = 1, opts = {}) {
|
|
|
4969
5053
|
};
|
|
4970
5054
|
}
|
|
4971
5055
|
|
|
5056
|
+
// src/sweepGuard.ts
|
|
5057
|
+
var SEED_SEGMENTS_FLOOR = 40;
|
|
5058
|
+
var COMMON_MATCH_CEILING = 50;
|
|
5059
|
+
function sweepCommitRefusal(i) {
|
|
5060
|
+
if (i.variantGuard || i.negatives > 0) return null;
|
|
5061
|
+
if (i.seedSegments >= SEED_SEGMENTS_FLOOR) return null;
|
|
5062
|
+
if (i.found <= COMMON_MATCH_CEILING) return null;
|
|
5063
|
+
return `commit refused (#376): the seed is ${i.seedSegments} segment(s) of linework and ${i.found} placement(s) cleared the bar \u2014 geometry this small and this common on the sheet is tree canopy, text, and line ticks as often as it is the symbol. Nothing was committed; the ${i.found} placements are listed in matches so you can look. To commit, say what you mean: variant_guard: true (the seed IS the whole symbol \u2014 richer placements become questions), exclude rects around what you do NOT mean, or a seed rect that captures more of the symbol's own linework (${SEED_SEGMENTS_FLOOR}+ segments stands the guard down).`;
|
|
5064
|
+
}
|
|
5065
|
+
|
|
4972
5066
|
// ../web/src/lib/symbolsweep.ts
|
|
4973
5067
|
var SWEEP_TOL_PX = 2;
|
|
4974
5068
|
var SWEEP_SCORE_HIGH = 0.92;
|
|
@@ -8026,14 +8120,14 @@ var Session = class _Session {
|
|
|
8026
8120
|
const num2 = (sp.str || "").trim().split(/\s+/).find((tok) => ROOM_LABEL_RE.test(tok));
|
|
8027
8121
|
if (num2) labels.push({ str: num2, bbox: sp });
|
|
8028
8122
|
}
|
|
8029
|
-
const withheld = { degenerate: 0, duplicate: 0, bubble: 0, implausible: 0, unresolved: 0 };
|
|
8123
|
+
const withheld = { degenerate: 0, duplicate: 0, bubble: 0, unowned: 0, implausible: 0, unresolved: 0 };
|
|
8030
8124
|
const unresolved = [];
|
|
8031
8125
|
const byRing = /* @__PURE__ */ new Map();
|
|
8032
8126
|
const order = [];
|
|
8033
8127
|
const sweepMppf = raster ? s.upp ? mask.ws / s.upp : 0 : mask.mppf || 0;
|
|
8034
8128
|
for (const lb of labels) {
|
|
8035
8129
|
let ring = null, ev = null, seed = null;
|
|
8036
|
-
let sawBubble = false, sawDegenerate = false;
|
|
8130
|
+
let sawBubble = false, sawDegenerate = false, sawUnowned = false;
|
|
8037
8131
|
for (const probe of seedLadderPx(lb.bbox)) {
|
|
8038
8132
|
const f = floodAtSeed(mask, probe[0], probe[1], opts.sensitivity ?? SENS_BALANCED, sweepMppf);
|
|
8039
8133
|
if (f.status !== "ok") continue;
|
|
@@ -8046,13 +8140,18 @@ var Session = class _Session {
|
|
|
8046
8140
|
sawBubble = true;
|
|
8047
8141
|
continue;
|
|
8048
8142
|
}
|
|
8143
|
+
if (!floodSurroundsLabelPx(f, lb.bbox)) {
|
|
8144
|
+
sawUnowned = true;
|
|
8145
|
+
continue;
|
|
8146
|
+
}
|
|
8049
8147
|
ring = r;
|
|
8050
8148
|
ev = _Session.floodEvidence(f, raster, sweepMppf);
|
|
8051
8149
|
seed = probe;
|
|
8052
8150
|
break;
|
|
8053
8151
|
}
|
|
8054
8152
|
if (!ring || !ev || !seed) {
|
|
8055
|
-
if (sawBubble) withheld.bubble++;
|
|
8153
|
+
if (sawBubble && !sawUnowned) withheld.bubble++;
|
|
8154
|
+
else if (sawUnowned) withheld.unowned++;
|
|
8056
8155
|
else if (sawDegenerate) withheld.degenerate++;
|
|
8057
8156
|
continue;
|
|
8058
8157
|
}
|
|
@@ -8136,7 +8235,7 @@ var Session = class _Session {
|
|
|
8136
8235
|
seed_norm: [u.seed[0] / s.widthPx, u.seed[1] / s.heightPx]
|
|
8137
8236
|
}));
|
|
8138
8237
|
}
|
|
8139
|
-
const withheldTotal = withheld.degenerate + withheld.duplicate + withheld.bubble + withheld.implausible + withheld.unresolved;
|
|
8238
|
+
const withheldTotal = withheld.degenerate + withheld.duplicate + withheld.bubble + withheld.unowned + withheld.implausible + withheld.unresolved;
|
|
8140
8239
|
return {
|
|
8141
8240
|
detected: rooms.length,
|
|
8142
8241
|
rooms,
|
|
@@ -8149,7 +8248,7 @@ var Session = class _Session {
|
|
|
8149
8248
|
// positive claim "every detected room resolved against its own row"
|
|
8150
8249
|
...assign ? { unresolved } : {},
|
|
8151
8250
|
...s.detected?.multi ? { multiple_scales: true } : {},
|
|
8152
|
-
...withheldTotal ? { note: `${withheldTotal} seed(s) withheld \u2014 ${withheld.duplicate} duplicate region(s), ${withheld.bubble} label-bubble(s), ${withheld.implausible} under ${minAreaSf} SF, ${withheld.degenerate} untraceable${assign ? `, ${withheld.unresolved} unresolved against the schedule (see unresolved[])` : ""}.` } : {},
|
|
8251
|
+
...withheldTotal ? { note: `${withheldTotal} seed(s) withheld \u2014 ${withheld.duplicate} duplicate region(s), ${withheld.bubble} label-bubble(s), ${withheld.unowned} unowned (every clean flood was a neighbouring space or door pocket \u2014 one_click inside the room), ${withheld.implausible} under ${minAreaSf} SF, ${withheld.degenerate} untraceable${assign ? `, ${withheld.unresolved} unresolved against the schedule (see unresolved[])` : ""}.` } : {},
|
|
8153
8252
|
...s.upp == null ? { warning: `No scale set for ${s.key} \u2014 quantities unavailable. Call set_scale${s.detected ? ` (detected: ${s.detected.label})` : ""}.` } : {}
|
|
8154
8253
|
};
|
|
8155
8254
|
}
|
|
@@ -8937,7 +9036,8 @@ var Session = class _Session {
|
|
|
8937
9036
|
if (!s.spans) s.spans = textSpans(s.page);
|
|
8938
9037
|
const lbl = this.sweepLabels(s.spans, geo, fp.center, res.matches, res.withheld);
|
|
8939
9038
|
let committed2;
|
|
8940
|
-
|
|
9039
|
+
const refusal2 = opts.commit ? sweepCommitRefusal({ seedSegments: fp.segments, found: res.matches.length, variantGuard: !!opts.variantGuard, negatives: negatives.length }) : null;
|
|
9040
|
+
if (opts.commit && !refusal2 && (res.matches.length || opts.commitSeed)) {
|
|
8941
9041
|
const points = [...opts.commitSeed ? [fp.center] : [], ...res.matches.map((m) => m.at)];
|
|
8942
9042
|
const seedOrigin = {
|
|
8943
9043
|
method: "symbol_sweep",
|
|
@@ -8975,9 +9075,10 @@ var Session = class _Session {
|
|
|
8975
9075
|
ea_total: committed2.ea_total,
|
|
8976
9076
|
...opts.commitSeed ? { seed_committed: true } : {}
|
|
8977
9077
|
} : {},
|
|
9078
|
+
...refusal2 ? { committed: 0, commit_refused: refusal2 } : {},
|
|
8978
9079
|
...(() => {
|
|
8979
9080
|
const parts = [];
|
|
8980
|
-
if (opts.commit && !res.matches.length && !opts.commitSeed) parts.push("commit requested but nothing cleared the bar \u2014 no shapes were committed.");
|
|
9081
|
+
if (opts.commit && !refusal2 && !res.matches.length && !opts.commitSeed) parts.push("commit requested but nothing cleared the bar \u2014 no shapes were committed.");
|
|
8981
9082
|
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.`);
|
|
8982
9083
|
if (labelNote2) parts.push(labelNote2);
|
|
8983
9084
|
return parts.length ? { note: parts.join(" ") } : {};
|
|
@@ -9032,7 +9133,8 @@ var Session = class _Session {
|
|
|
9032
9133
|
}
|
|
9033
9134
|
const found = perSheet.reduce((n, p) => n + p.matches.length, 0);
|
|
9034
9135
|
let committed;
|
|
9035
|
-
|
|
9136
|
+
const refusal = opts.commit ? sweepCommitRefusal({ seedSegments: fp.segments, found, variantGuard: !!opts.variantGuard, negatives: negatives.length }) : null;
|
|
9137
|
+
if (opts.commit && !refusal && found) {
|
|
9036
9138
|
const ids = [];
|
|
9037
9139
|
for (const ps of perSheet) {
|
|
9038
9140
|
for (const m of ps.matches) {
|
|
@@ -9052,7 +9154,7 @@ var Session = class _Session {
|
|
|
9052
9154
|
const capped = perSheet.filter((p) => p.candidates.dropped > 0);
|
|
9053
9155
|
const notes = [];
|
|
9054
9156
|
if (!perSheet.length) notes.push("No plan-role sheet in the set was sweepable \u2014 nothing was counted; skipped[] says why, sheet by sheet.");
|
|
9055
|
-
if (opts.commit && !found) notes.push("commit requested but nothing cleared the bar on any plan sheet \u2014 no shapes were committed.");
|
|
9157
|
+
if (opts.commit && !refusal && !found) notes.push("commit requested but nothing cleared the bar on any plan sheet \u2014 no shapes were committed.");
|
|
9056
9158
|
const rescaled = perSheet.filter((p) => p.scaled);
|
|
9057
9159
|
const assumed = perSheet.filter((p) => !p.scale.known);
|
|
9058
9160
|
if (rescaled.length) {
|
|
@@ -9106,6 +9208,7 @@ var Session = class _Session {
|
|
|
9106
9208
|
complete: perSheet.every((p) => p.complete),
|
|
9107
9209
|
skipped,
|
|
9108
9210
|
...committed ?? {},
|
|
9211
|
+
...refusal ? { committed: 0, commit_refused: refusal } : {},
|
|
9109
9212
|
...notes.length ? { note: notes.join(" ") } : {},
|
|
9110
9213
|
...capped.length ? { warning: `Work ceiling: candidate placements were dropped un-scored on ${capped.map((p) => p.state.key).join(", ")} \u2014 counts there are FLOORS, not totals. The seed's linework is too common there for an exhaustive sweep; tighten the seed rect around more distinctive geometry, or sweep those sheets singly and reconcile the counts.` } : {}
|
|
9111
9214
|
};
|
|
@@ -10545,7 +10648,7 @@ var Session = class _Session {
|
|
|
10545
10648
|
floorTagFor(g, tag) {
|
|
10546
10649
|
const res = resolveTag(g, tag);
|
|
10547
10650
|
if (res.status !== "resolved") return { reason: res.reason };
|
|
10548
|
-
const floor = res.finishes.find((f) => f.surface === "FLOOR");
|
|
10651
|
+
const floor = res.finishes.find((f) => f.surface === "FLOOR" || f.surface.startsWith("FLOOR "));
|
|
10549
10652
|
const code = floor?.code.trim();
|
|
10550
10653
|
if (!floor || !code) return { reason: `schedule row ${res.tag} states no FLOOR finish` };
|
|
10551
10654
|
if (/[/,]|\bOR\b/i.test(code)) return { reason: `ambiguous: floor cell "${code}" names more than one finish with no stated split` };
|
|
@@ -10752,6 +10855,7 @@ var detectRoomsOutput = {
|
|
|
10752
10855
|
degenerate: z.number().int().describe("Traced to fewer than 3 vertices"),
|
|
10753
10856
|
duplicate: z.number().int().describe("Flooded to a region another label already claimed \u2014 counted once, never twice"),
|
|
10754
10857
|
bubble: z.number().int().describe("Labels whose every clean flood was their own label BUBBLE (ring bbox \u2248 label bbox \u2014 plans box their room numbers). Scale-free, so it guards unscaled previews too"),
|
|
10858
|
+
unowned: z.number().int().describe("Labels whose every clean, non-bubble flood did not SURROUND the label's box \u2014 a ladder rung stepped past the wall into a neighbouring space or a door-swing pocket. Withheld rather than committed under the tag (#373); one_click inside the room answers it"),
|
|
10755
10859
|
implausible: z.number().int().describe("Enclosed, clean, non-bubble, but smaller than min_area_sf \u2014 a door swing or wall cavity rather than a room"),
|
|
10756
10860
|
unresolved: z.number().int().describe("Assign mode: rooms the schedule could not answer for (no row, no FLOOR cell, or a compound cell) \u2014 withheld into unresolved[], never committed under a guess. Always present; 0 outside assign mode"),
|
|
10757
10861
|
min_area_sf: z.number().optional().describe("The plausibility floor applied (scaled mode only)")
|
|
@@ -10869,7 +10973,8 @@ var symbolSweepOutput = {
|
|
|
10869
10973
|
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)"),
|
|
10870
10974
|
sheets: z.array(sweepSheetBlock).optional().describe("Set scope only: one entry per swept PLAN-role sheet, load order"),
|
|
10871
10975
|
skipped: sweepSkipped.optional().describe("Set scope only: every sheet excluded from counting, with role and reason \u2014 including the seed's own sheet when it is not a plan"),
|
|
10872
|
-
committed: z.number().int().optional().describe("commit mode: count shapes committed \u2014 one per match"),
|
|
10976
|
+
committed: z.number().int().optional().describe("commit mode: count shapes committed \u2014 one per match (0 when commit_refused is present)"),
|
|
10977
|
+
commit_refused: z.string().optional().describe("commit mode (#376): present when the seed was too small and too common to commit on shape alone \u2014 fewer than 40 segments of seed linework and more than 50 placements cleared the bar. NOTHING was committed; the placements are still listed in matches (or per sheet in sheets[]) so you can look, and the text says what stands the guard down: variant_guard: true, exclude counter-examples, or a seed rect that captures more of the symbol"),
|
|
10873
10978
|
shape_ids: z.array(z.string()).optional(),
|
|
10874
10979
|
condition: z.string().optional().describe("commit mode: the finish tag the markers counted under"),
|
|
10875
10980
|
ea_total: z.number().optional().describe("commit mode: the condition's total EA after this call"),
|
|
@@ -13923,7 +14028,7 @@ function nameTheStageInRefusals(server) {
|
|
|
13923
14028
|
// package.json
|
|
13924
14029
|
var package_default = {
|
|
13925
14030
|
name: "opentakeoff-mcp",
|
|
13926
|
-
version: "0.9.
|
|
14031
|
+
version: "0.9.71",
|
|
13927
14032
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
13928
14033
|
type: "module",
|
|
13929
14034
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentakeoff-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.71",
|
|
4
4
|
"mcpName": "io.github.Kentucky-ai/opentakeoff",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "OpenTakeoff MCP server
|
|
6
|
+
"description": "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=20"
|