opentakeoff-mcp 0.9.93 → 0.9.96
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 +659 -154
- package/package.json +1 -1
package/dist/server-core.js
CHANGED
|
@@ -3466,6 +3466,32 @@ function finishSectionOf(text) {
|
|
|
3466
3466
|
return WORDS.has(clean[0]) ? clean[0] : null;
|
|
3467
3467
|
}
|
|
3468
3468
|
|
|
3469
|
+
// ../web/src/lib/notUsed.ts
|
|
3470
|
+
function normalizeTail(text) {
|
|
3471
|
+
let t = text.toUpperCase().trim().replace(/^[-–—:,\s]+/, "");
|
|
3472
|
+
t = t.replace(/\.$/, "");
|
|
3473
|
+
if (t.startsWith("(") && closingParen(t) === t.length - 1) t = t.slice(1, -1);
|
|
3474
|
+
t = t.replace(/\.$/, "");
|
|
3475
|
+
t = t.replace(/\bN\.?\s*I\.?\s*C\b\.?/g, "NIC");
|
|
3476
|
+
return t.replace(/\s+/g, " ").trim();
|
|
3477
|
+
}
|
|
3478
|
+
function closingParen(t) {
|
|
3479
|
+
let depth = 0;
|
|
3480
|
+
for (let i = 0; i < t.length; i++) {
|
|
3481
|
+
if (t[i] === "(") depth++;
|
|
3482
|
+
else if (t[i] === ")" && --depth === 0) return i;
|
|
3483
|
+
}
|
|
3484
|
+
return -1;
|
|
3485
|
+
}
|
|
3486
|
+
function normalizeNotUsed(text) {
|
|
3487
|
+
const t = normalizeTail(text);
|
|
3488
|
+
return t === "NOT USED" || t === "NOT IN CONTRACT" || t === "NIC" ? t : null;
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
// ../web/src/lib/finishCode.ts
|
|
3492
|
+
var CODE_RE = /^[A-Z]{1,4}(-?[A-Z0-9]{1,4})?$/;
|
|
3493
|
+
var finishCodeOk = (p) => !/^[A-Z]{4,}$/.test(p) && CODE_RE.test(p);
|
|
3494
|
+
|
|
3469
3495
|
// ../web/src/lib/sheetgraph.ts
|
|
3470
3496
|
var bboxOf = (s) => [s.x, s.y, s.x + (s.w || 0), s.y + (s.h || 0)];
|
|
3471
3497
|
var merge = (a, b) => [Math.min(a[0], b[0]), Math.min(a[1], b[1]), Math.max(a[2], b[2]), Math.max(a[3], b[3])];
|
|
@@ -3624,6 +3650,7 @@ var rowY = (r) => r.reduce((s, t) => s + t.y, 0) / r.length;
|
|
|
3624
3650
|
var SURFACE_WORDS = /* @__PURE__ */ new Set(["FLOOR", "BASE", "WALL", "WALLS", "CEILING", "NORTH", "SOUTH", "EAST", "WEST", "WAINSCOT"]);
|
|
3625
3651
|
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"];
|
|
3626
3652
|
var FINISH_HEADERS = ["CODE", "MARK", "SYMBOL", "TAG", "MATERIAL", "MANUFACTURER", "PRODUCT", "STYLE", "COLOR", "SIZE", "REMARKS", "DESCRIPTION", "PATTERN", "COMMENTS"];
|
|
3653
|
+
var FOREIGN_HDR = /* @__PURE__ */ new Set(["QTY", "QUANTITY", "MESSAGE", "WIDTH", "HEIGHT", "HARDWARE", "CFM", "VOLTS", "VOLTAGE", "WATTS", "LAMP", "LAMPS", "CATALOG", "FIXTURE", "FRAME", "GLAZING", "THICKNESS", "RATING", "LOUVER"]);
|
|
3627
3654
|
var EQUIPMENT_HEADERS = [
|
|
3628
3655
|
// identity + the shared columns
|
|
3629
3656
|
"ID",
|
|
@@ -3995,8 +4022,6 @@ function bandLimits(anchors) {
|
|
|
3995
4022
|
const rightMargin = WIDE_LAST.has(last.label) ? Math.max(300, medGap * 3) : Math.max(120, medGap);
|
|
3996
4023
|
return { x0: anchors[0].x - Math.max(80, medGap / 2), x1: last.x + rightMargin, medGap };
|
|
3997
4024
|
}
|
|
3998
|
-
var CODE_RE = /^[A-Z]{1,4}(-?[A-Z0-9]{1,4})?$/;
|
|
3999
|
-
var finishCodeOk = (p) => !/^[A-Z]{4,}$/.test(p) && CODE_RE.test(p);
|
|
4000
4025
|
var ROW_KEY_RE = /^\d{1,3}[A-Z]{0,2}$/;
|
|
4001
4026
|
var QUALIFIED_KEY_RE = /^([A-Z]{1,2})-(\d{1,3}[A-Z]{0,2})$/;
|
|
4002
4027
|
var CORRIDOR_KEY_RE = /^[A-Z]{1,3}(?:\d{1,3}-\d{1,3}|\d{3})[A-Z]?$/;
|
|
@@ -4025,6 +4050,24 @@ function rowKeyOf(raw, kind, buildings, typeKeyed = false) {
|
|
|
4025
4050
|
if (q && buildings?.has(q[1])) return { key, building: q[1] };
|
|
4026
4051
|
return null;
|
|
4027
4052
|
}
|
|
4053
|
+
var SPLIT_DENY = /* @__PURE__ */ new Set(["SEE", "NOTE", "NOTES", "NOT", "USED", "BY", "OWNER", "NIC", "TBD", "ALL", "THRU", "TO", "AND", "OR"]);
|
|
4054
|
+
var ALT_WORDS = /* @__PURE__ */ new Set(["ALT", "OPT", "OPTION", "ADD", "DEDUCT"]);
|
|
4055
|
+
function splitKeyCell(raw) {
|
|
4056
|
+
const r = raw.trim();
|
|
4057
|
+
const sp = r.search(/\s/);
|
|
4058
|
+
const w1 = (sp < 0 ? r : r.slice(0, sp)).toUpperCase().replace(/[:,;]+$/, "");
|
|
4059
|
+
if (!finishCodeOk(w1) || !/\d/.test(w1)) return null;
|
|
4060
|
+
const after = sp < 0 ? "" : r.slice(sp).replace(/^[-–—:,\s]+/, "");
|
|
4061
|
+
const tail = normalizeTail(after);
|
|
4062
|
+
if (tail === "NOT USED" || tail === "NOT IN CONTRACT" || tail === "NIC") return { key: w1, notUsed: true, notUsedText: after };
|
|
4063
|
+
const words = tail ? tail.split(" ") : [];
|
|
4064
|
+
if (words.length < 1 || words.length > 2) return null;
|
|
4065
|
+
if (!words.every((w) => /^[A-Z]{2,8}$/.test(w) || /^\([A-Z]{1,8}\)$/.test(w))) return null;
|
|
4066
|
+
const bare = words.map((w) => w.replace(/[()]/g, ""));
|
|
4067
|
+
if (bare.some((w) => SPLIT_DENY.has(w)) || bare.join("").length < 3) return null;
|
|
4068
|
+
return { key: w1, qualifier: after };
|
|
4069
|
+
}
|
|
4070
|
+
var isAltQualifier = (q) => ALT_WORDS.has(q.toUpperCase().replace(/[()]/g, "").trim());
|
|
4028
4071
|
var rowKeyAnswersFor = (key, want) => {
|
|
4029
4072
|
const c = norm(key).replace(/\s+/g, "");
|
|
4030
4073
|
const w = norm(want).replace(/\s+/g, "");
|
|
@@ -4195,6 +4238,58 @@ function columnStarts(rows, anchors, cfg, x0, x1, kind) {
|
|
|
4195
4238
|
if (!center) return left;
|
|
4196
4239
|
return center.score > left.score + 0.05 ? center : left;
|
|
4197
4240
|
}
|
|
4241
|
+
var KEY_FIRST = /* @__PURE__ */ new Set(["CODE", "MARK", "SYMBOL", "TAG"]);
|
|
4242
|
+
var LETTERS_RE = /^[A-Z]{4,5}$/;
|
|
4243
|
+
var OTHER_FAMILY_WORDS = ["DOOR", "WINDOW", "PARTITION", "EQUIPMENT", "HARDWARE", "LOUVER", "SIGNAGE", "LIGHTING", "LUMINAIRE", "PLUMBING", "MECHANICAL", "ELECTRICAL", "STOREFRONT", "GLAZING", "CASEWORK", "MILLWORK", "APPLIANCE"];
|
|
4244
|
+
var LETTERS_STOP = /* @__PURE__ */ new Set([
|
|
4245
|
+
...FINISH_HEADERS,
|
|
4246
|
+
...EQUIPMENT_HEADERS,
|
|
4247
|
+
...ROOM_HEADERS,
|
|
4248
|
+
...FINISH_SECTION_HEADINGS.filter((h) => !h.includes(" ")),
|
|
4249
|
+
...OTHER_FAMILY_WORDS.flatMap((w) => [w, w + "S"]),
|
|
4250
|
+
"LEGEND",
|
|
4251
|
+
"NOTE",
|
|
4252
|
+
"NOTES",
|
|
4253
|
+
"TYPE",
|
|
4254
|
+
"ITEM",
|
|
4255
|
+
"ROOM",
|
|
4256
|
+
"LEVEL",
|
|
4257
|
+
"AREA",
|
|
4258
|
+
"FIELD",
|
|
4259
|
+
"WHERE",
|
|
4260
|
+
"PATCH",
|
|
4261
|
+
"MATCH",
|
|
4262
|
+
"REFER",
|
|
4263
|
+
"STAIR",
|
|
4264
|
+
"ABOVE",
|
|
4265
|
+
"APPLY",
|
|
4266
|
+
"CAULK",
|
|
4267
|
+
"COVE",
|
|
4268
|
+
"COVED",
|
|
4269
|
+
"MFG",
|
|
4270
|
+
"MFR",
|
|
4271
|
+
"SPEC",
|
|
4272
|
+
"PAINT",
|
|
4273
|
+
"TILE",
|
|
4274
|
+
"STONE",
|
|
4275
|
+
"VINYL",
|
|
4276
|
+
"WOOD",
|
|
4277
|
+
"GLASS",
|
|
4278
|
+
"METAL",
|
|
4279
|
+
"EPOXY",
|
|
4280
|
+
"STAIN",
|
|
4281
|
+
"GROUT",
|
|
4282
|
+
// sheens and formats: a key-column wrap under a code (PT-1 / SATIN)
|
|
4283
|
+
"SATIN",
|
|
4284
|
+
"GLOSS",
|
|
4285
|
+
"MATTE",
|
|
4286
|
+
"FLAT",
|
|
4287
|
+
"HONED",
|
|
4288
|
+
"PLANK"
|
|
4289
|
+
]);
|
|
4290
|
+
var HEADER_WORDS_STRONG = /* @__PURE__ */ new Set([...FINISH_HEADERS, ...EQUIPMENT_HEADERS, ...FOREIGN_HDR, "MFG", "MFR", "SPEC", "SPECIFICATION", "NOTES"]);
|
|
4291
|
+
var HEADER_WORDS = /* @__PURE__ */ new Set([...HEADER_WORDS_STRONG, ...ROOM_HEADERS]);
|
|
4292
|
+
var SPEC_SECTION_RE = /^\d{2} ?\d{2}[ .]?\d{2}\b/;
|
|
4198
4293
|
function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
4199
4294
|
const { x0, x1, medGap } = bandLimits(anchors);
|
|
4200
4295
|
const typeKeyed = kind === "equipment" && (anchors[0]?.label === "TYPE" || anchors[0]?.label === "FIXTURE");
|
|
@@ -4212,13 +4307,12 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
4212
4307
|
const a = atOf(t);
|
|
4213
4308
|
return a >= Math.min(x0, keyLo - colTol) && a < keyHi;
|
|
4214
4309
|
};
|
|
4215
|
-
let tLeft = 0, tRight = 0, medPitch = 0;
|
|
4216
|
-
let prevBandY = null;
|
|
4310
|
+
let tLeft = 0, tRight = 0, medPitch = 0, medPitchLo = 0;
|
|
4217
4311
|
if (finish) {
|
|
4218
4312
|
let ys = rows.slice(Math.max(cfg.fromIdx, 0)).filter((r) => r.some(inBand)).map(rowY);
|
|
4219
4313
|
const med = (v) => {
|
|
4220
|
-
const
|
|
4221
|
-
return
|
|
4314
|
+
const d2 = v.slice(1).map((y, k) => y - v[k]).sort((p, q) => p - q);
|
|
4315
|
+
return d2.length ? d2[d2.length >> 1] : 0;
|
|
4222
4316
|
};
|
|
4223
4317
|
const p0 = med(ys);
|
|
4224
4318
|
const cut = p0 > 0 ? ys.findIndex((y, k) => k > 0 && y - ys[k - 1] > 8 * p0) : -1;
|
|
@@ -4226,9 +4320,9 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
4226
4320
|
const lastTableY = ys.length ? ys[ys.length - 1] : Infinity;
|
|
4227
4321
|
if (cols) {
|
|
4228
4322
|
const cs = cols.cols;
|
|
4229
|
-
const
|
|
4323
|
+
const pitch = cs.length > 1 ? cs.slice(1).map((c, k) => c.start - cs[k].start).sort((p, q) => p - q)[cs.length - 1 >> 1] : medGap;
|
|
4230
4324
|
tLeft = cs[0].start;
|
|
4231
|
-
tRight = cs[cs.length - 1].start +
|
|
4325
|
+
tRight = cs[cs.length - 1].start + pitch;
|
|
4232
4326
|
} else {
|
|
4233
4327
|
tLeft = Infinity;
|
|
4234
4328
|
tRight = -Infinity;
|
|
@@ -4239,19 +4333,10 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
4239
4333
|
}
|
|
4240
4334
|
}
|
|
4241
4335
|
medPitch = med(ys);
|
|
4336
|
+
const d = ys.slice(1).map((y, k) => y - ys[k]).filter((g) => g > 0).sort((p, q) => p - q);
|
|
4337
|
+
medPitchLo = d.length ? d[d.length - 1 >> 1] : 0;
|
|
4242
4338
|
}
|
|
4243
|
-
const headings = [];
|
|
4244
|
-
const headLines = [];
|
|
4245
|
-
const consumeHeading = (banded, y) => {
|
|
4246
|
-
let hb = null;
|
|
4247
|
-
for (const t of banded) hb = hb ? merge(hb, bboxOf(t)) : bboxOf(t);
|
|
4248
|
-
headings.push({ bbox: hb, y });
|
|
4249
|
-
};
|
|
4250
4339
|
const keyTol = cols && cols.cols.length > 1 ? Math.max(8, (cols.cols[1].start - cols.cols[0].start) * 0.5) : 40;
|
|
4251
|
-
const out = [];
|
|
4252
|
-
const outY = [];
|
|
4253
|
-
const everRows = [];
|
|
4254
|
-
let region = null;
|
|
4255
4340
|
const columnOf = (t) => {
|
|
4256
4341
|
if (!cols) return nearestAnchor(centerX(t), anchors);
|
|
4257
4342
|
const at = cols.coord === "left" ? t.x : centerX(t);
|
|
@@ -4281,147 +4366,549 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
4281
4366
|
const text = t.str.trim();
|
|
4282
4367
|
if (!row.cells[label]) row.cells[label] = { text, bbox: bboxOf(t) };
|
|
4283
4368
|
else row.cells[label] = { text: `${row.cells[label].text} ${text}`, bbox: merge(row.cells[label].bbox, bboxOf(t)) };
|
|
4284
|
-
region = region ? merge(region, bboxOf(t)) : bboxOf(t);
|
|
4285
4369
|
}
|
|
4286
4370
|
};
|
|
4287
|
-
const
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
const
|
|
4292
|
-
|
|
4293
|
-
const
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4371
|
+
const scan = (over2, gone, breaks) => {
|
|
4372
|
+
const S = { out: [], outY: [], outI: [], outToks: [], everRows: [], everI: [], orphans: [], markers: [], headings: [], headLines: [], lines: [], outEpoch: [], outLine: [] };
|
|
4373
|
+
let curSection2;
|
|
4374
|
+
let prevBandY = null;
|
|
4375
|
+
const consumeHeading = (banded, y, section) => {
|
|
4376
|
+
let hb = null;
|
|
4377
|
+
for (const t of banded) hb = hb ? merge(hb, bboxOf(t)) : bboxOf(t);
|
|
4378
|
+
S.headings.push({ bbox: hb, y, section });
|
|
4379
|
+
};
|
|
4380
|
+
const keyOf = (raw, i) => over2?.get(i) ?? rowKeyOf(raw, kind, buildings, typeKeyed);
|
|
4381
|
+
let epoch = 0, line = -1;
|
|
4382
|
+
for (let i = Math.max(cfg.fromIdx, 0); i < rows.length; i++) {
|
|
4383
|
+
if (rowY(rows[i]) <= cfg.belowY) continue;
|
|
4384
|
+
const banded = [];
|
|
4385
|
+
for (const t of rows[i]) {
|
|
4386
|
+
const tri = cfg.deltas?.get(t);
|
|
4387
|
+
const rev = tri ? norm(t.str) : revisionOf(t.str);
|
|
4388
|
+
if (rev != null) {
|
|
4389
|
+
if (centerX(t) >= x0 - 2.5 * medGap && centerX(t) <= x1 + medGap) S.markers.push({ rev, span: t, i, ...tri ? { drawn: true, tri } : {} });
|
|
4390
|
+
continue;
|
|
4391
|
+
}
|
|
4392
|
+
if (inBand(t)) banded.push(t);
|
|
4393
|
+
}
|
|
4394
|
+
if (!banded.length) continue;
|
|
4395
|
+
line++;
|
|
4396
|
+
const bandYBefore = prevBandY;
|
|
4397
|
+
prevBandY = rowY(rows[i]);
|
|
4398
|
+
S.lines.push({ i, y: rowY(rows[i]), toks: banded });
|
|
4399
|
+
if (finish) {
|
|
4400
|
+
const h = finishSectionOf(banded[0].str);
|
|
4401
|
+
const joinedLen = banded.map((t) => t.str.trim()).join(" ").length;
|
|
4402
|
+
const inKeyColumn = inKey(banded[0]) && banded.every((t) => t.x + (t.w || 0) <= keyHi);
|
|
4403
|
+
const one = banded.length === 1 ? banded[0] : null;
|
|
4404
|
+
const straddles = !!one && !!cols && cols.cols.slice(1).some((c) => one.x < c.start - colTol && one.x + (one.w || 0) > c.start + colTol);
|
|
4405
|
+
const hugs = bandYBefore != null && rowY(rows[i]) - bandYBefore < 0.6 * medPitch;
|
|
4406
|
+
const tMid = (tLeft + tRight) / 2;
|
|
4407
|
+
const centered = !!one && !hugs && (Math.abs(centerX(one) - tMid) <= Math.max(0.1 * (tRight - tLeft), 3 * (one.h || 8)) || straddles);
|
|
4408
|
+
if (h && (inKeyColumn || centered) && joinedLen < 24) {
|
|
4409
|
+
curSection2 = h;
|
|
4410
|
+
epoch++;
|
|
4411
|
+
consumeHeading(banded, rowY(rows[i]), h);
|
|
4412
|
+
continue;
|
|
4413
|
+
}
|
|
4414
|
+
if (h && !hugs) S.headLines.push(rowY(rows[i]));
|
|
4415
|
+
if (banded.length <= 2 && inKey(banded[0]) && /^\d{2} ?\d{2} ?\d{2}(\.\d+)?\b/.test(norm(banded[0].str)) && !rowKeyOf(banded[0].str, kind, buildings, typeKeyed)) {
|
|
4416
|
+
curSection2 = void 0;
|
|
4417
|
+
epoch++;
|
|
4418
|
+
consumeHeading(banded, rowY(rows[i]), null);
|
|
4419
|
+
continue;
|
|
4420
|
+
}
|
|
4421
|
+
if (one && inKey(one) && (breaks?.has(i) || !keyOf(one.str, i))) {
|
|
4422
|
+
curSection2 = void 0;
|
|
4423
|
+
epoch++;
|
|
4424
|
+
if (!hugs && joinedLen < 24 && !/\d/.test(one.str)) continue;
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
if (kind === "equipment" && S.out.length) {
|
|
4428
|
+
if (rows[i].some((t) => /SCHEDULE/.test(norm(t.str)) && !EQUIP_KEY_RE.test(norm(t.str).replace(/[^A-Z0-9-]/g, "")))) break;
|
|
4429
|
+
const hh = headerHits(rows[i], EQUIPMENT_HEADERS);
|
|
4430
|
+
if (hh.length >= 3 && hh.some((h) => EQUIPMENT_KEY_HEADERS.includes(h.label))) break;
|
|
4298
4431
|
}
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
const bandYBefore = prevBandY;
|
|
4303
|
-
prevBandY = rowY(rows[i]);
|
|
4304
|
-
if (finish) {
|
|
4305
|
-
const h = finishSectionOf(banded[0].str);
|
|
4306
|
-
const joinedLen = banded.map((t) => t.str.trim()).join(" ").length;
|
|
4307
|
-
const inKeyColumn = inKey(banded[0]) && banded.every((t) => t.x + (t.w || 0) <= keyHi);
|
|
4308
|
-
const one = banded.length === 1 ? banded[0] : null;
|
|
4309
|
-
const straddles = !!one && !!cols && cols.cols.slice(1).some((c) => one.x < c.start - colTol && one.x + (one.w || 0) > c.start + colTol);
|
|
4310
|
-
const hugs = bandYBefore != null && rowY(rows[i]) - bandYBefore < 0.6 * medPitch;
|
|
4311
|
-
const tMid = (tLeft + tRight) / 2;
|
|
4312
|
-
const centered = !!one && !hugs && (Math.abs(centerX(one) - tMid) <= Math.max(0.1 * (tRight - tLeft), 3 * (one.h || 8)) || straddles);
|
|
4313
|
-
if (h && (inKeyColumn || centered) && joinedLen < 24) {
|
|
4314
|
-
curSection = h;
|
|
4315
|
-
consumeHeading(banded, rowY(rows[i]));
|
|
4432
|
+
const keyed = keyOf(banded[0].str, i);
|
|
4433
|
+
if (!keyed) {
|
|
4434
|
+
if (!gone?.has(i)) S.orphans.push({ toks: banded, y: rowY(rows[i]), i });
|
|
4316
4435
|
continue;
|
|
4317
4436
|
}
|
|
4318
|
-
if (
|
|
4319
|
-
|
|
4320
|
-
curSection = void 0;
|
|
4321
|
-
consumeHeading(banded, rowY(rows[i]));
|
|
4437
|
+
if (/\d{3}/.test(keyed.key) && cfg.sheetNumbers?.has(keyed.key.replace(/[^A-Z0-9]/g, ""))) {
|
|
4438
|
+
S.orphans.push({ toks: banded, y: rowY(rows[i]), i });
|
|
4322
4439
|
continue;
|
|
4323
4440
|
}
|
|
4324
|
-
if (
|
|
4325
|
-
|
|
4326
|
-
|
|
4441
|
+
if (cols && Math.abs((cols.coord === "left" ? banded[0].x : centerX(banded[0])) - cols.cols[0].start) > keyTol) continue;
|
|
4442
|
+
if (cfg.keyAlign && Math.abs(centerX(banded[0]) - cfg.keyAlign.x) > cfg.keyAlign.tol) continue;
|
|
4443
|
+
const row = { key: keyed.key, sheet: sheetKey, cells: {} };
|
|
4444
|
+
if (keyed.building) row.building = keyed.building;
|
|
4445
|
+
if (curSection2) row.section = curSection2;
|
|
4446
|
+
if (keyed.qualifier) row.qualifier = keyed.qualifier;
|
|
4447
|
+
if (keyed.notUsed) {
|
|
4448
|
+
row.notUsed = true;
|
|
4449
|
+
row.notUsedText = keyed.notUsedText;
|
|
4327
4450
|
}
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
if (
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
if (
|
|
4363
|
-
|
|
4364
|
-
|
|
4451
|
+
add(row, banded);
|
|
4452
|
+
S.out.push(row);
|
|
4453
|
+
S.outY.push(rowY(rows[i]));
|
|
4454
|
+
S.outI.push(i);
|
|
4455
|
+
S.outToks.push(banded);
|
|
4456
|
+
S.outEpoch.push(epoch);
|
|
4457
|
+
S.outLine.push(line);
|
|
4458
|
+
S.everRows.push(row);
|
|
4459
|
+
S.everI.push(i);
|
|
4460
|
+
}
|
|
4461
|
+
return S;
|
|
4462
|
+
};
|
|
4463
|
+
const dropRowAt = (S, k) => {
|
|
4464
|
+
S.out.splice(k, 1);
|
|
4465
|
+
S.outY.splice(k, 1);
|
|
4466
|
+
S.outI.splice(k, 1);
|
|
4467
|
+
S.outToks.splice(k, 1);
|
|
4468
|
+
};
|
|
4469
|
+
const blankBandResets = (S) => {
|
|
4470
|
+
if (!finish || !cfg.resetAtBlankBand) return [];
|
|
4471
|
+
const { out, outY, outEpoch, outLine } = S;
|
|
4472
|
+
const clear = /* @__PURE__ */ new Set();
|
|
4473
|
+
const prefixOf = (k) => /^[A-Z]*/.exec(k)[0];
|
|
4474
|
+
const plain = [];
|
|
4475
|
+
for (let i = 1; i < out.length; i++) if (outEpoch[i] === outEpoch[i - 1] && outLine[i] === outLine[i - 1] + 1) plain.push(i);
|
|
4476
|
+
const byEpoch = /* @__PURE__ */ new Map();
|
|
4477
|
+
for (const i of plain) {
|
|
4478
|
+
const g = outY[i] - outY[i - 1];
|
|
4479
|
+
if (g > 0) byEpoch.set(outEpoch[i], [...byEpoch.get(outEpoch[i]) ?? [], g]);
|
|
4480
|
+
}
|
|
4481
|
+
for (const i of plain) {
|
|
4482
|
+
const gaps = (byEpoch.get(outEpoch[i]) ?? []).slice().sort((a, b) => a - b);
|
|
4483
|
+
if (gaps.length < 4) continue;
|
|
4484
|
+
const pitch = gaps[gaps.length - 1 >> 1];
|
|
4485
|
+
if (outY[i] - outY[i - 1] <= 1.6 * pitch || prefixOf(out[i].key) === prefixOf(out[i - 1].key)) continue;
|
|
4486
|
+
for (let j = i; j < out.length && outEpoch[j] === outEpoch[i]; j++) clear.add(out[j]);
|
|
4487
|
+
}
|
|
4488
|
+
return [...clear];
|
|
4489
|
+
};
|
|
4490
|
+
const resetSections = (rs) => {
|
|
4491
|
+
for (const r of rs) delete r.section;
|
|
4492
|
+
};
|
|
4493
|
+
const endCut = (S) => {
|
|
4494
|
+
if (S.out.length > 2 && !(finish && cfg.marquee)) {
|
|
4495
|
+
const d = S.outY.slice(1).map((y, i) => y - S.outY[i]).filter((g) => g > 0).sort((a, b) => a - b);
|
|
4496
|
+
const pitch0 = d.length ? d[d.length >> 1] : 0;
|
|
4497
|
+
if (pitch0 > 0) {
|
|
4498
|
+
let end = S.out.length;
|
|
4499
|
+
for (let i = 1; i < S.outY.length; i++) if (S.outY[i] - S.outY[i - 1] > pitch0 * 8) {
|
|
4500
|
+
end = i;
|
|
4501
|
+
break;
|
|
4502
|
+
}
|
|
4503
|
+
if (end < S.out.length) {
|
|
4504
|
+
S.out.length = end;
|
|
4505
|
+
S.outY.length = end;
|
|
4506
|
+
S.outI.length = end;
|
|
4507
|
+
S.outToks.length = end;
|
|
4508
|
+
}
|
|
4365
4509
|
}
|
|
4366
4510
|
}
|
|
4367
|
-
}
|
|
4368
|
-
const
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4511
|
+
};
|
|
4512
|
+
const attachPlan = (S) => {
|
|
4513
|
+
const outY = S.outY;
|
|
4514
|
+
const headY = [...S.headings.map((h) => h.y), ...S.headLines];
|
|
4515
|
+
const ly2 = outY.length > 1 ? [...outY, ...headY].filter((y) => y <= outY[outY.length - 1]).sort((a, b) => a - b) : [];
|
|
4516
|
+
const gaps = ly2.slice(1).map((y, i) => y - ly2[i]).filter((d) => d > 0).sort((a, b) => a - b);
|
|
4517
|
+
const pitch = gaps.length ? gaps[gaps.length >> 1] : 0;
|
|
4518
|
+
const nearestIn = (ys, y) => {
|
|
4519
|
+
let bi = -1, bd = Infinity;
|
|
4520
|
+
ys.forEach((ry, i) => {
|
|
4521
|
+
const d = Math.abs(y - ry);
|
|
4522
|
+
if (d < bd) {
|
|
4523
|
+
bd = d;
|
|
4524
|
+
bi = i;
|
|
4525
|
+
}
|
|
4526
|
+
});
|
|
4527
|
+
return { i: bi, d: bd };
|
|
4528
|
+
};
|
|
4529
|
+
const radius = (h) => pitch ? pitch * 0.6 : Math.max(h, 8) * 1.6;
|
|
4530
|
+
const headYs = [...headY].sort((p, q) => p - q);
|
|
4531
|
+
const byHeading = (y, d, rowAt) => headYs.some((hy) => hy !== y && (Math.abs(y - hy) < d || Math.abs(y - hy) === d && hy < rowAt));
|
|
4532
|
+
const orphanT = S.orphans.map((o) => {
|
|
4533
|
+
const { i, d } = nearestIn(outY, o.y);
|
|
4534
|
+
if (i < 0 || d > radius(Math.max(...o.toks.map((t) => t.h || 8)))) return -1;
|
|
4535
|
+
return byHeading(o.y, d, outY[i]) ? -1 : i;
|
|
4536
|
+
});
|
|
4537
|
+
const taken = /* @__PURE__ */ new Set();
|
|
4538
|
+
const markerT = S.markers.map((m) => {
|
|
4539
|
+
const { i, d } = nearestIn(outY, m.span.y);
|
|
4540
|
+
if (i < 0 || d > radius(m.span.h || 8) || taken.has(i)) return -1;
|
|
4541
|
+
taken.add(i);
|
|
4542
|
+
return i;
|
|
4380
4543
|
});
|
|
4381
|
-
return {
|
|
4544
|
+
return { radius, nearestIn, byHeading, orphanT, markerT };
|
|
4382
4545
|
};
|
|
4383
|
-
const
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4546
|
+
const applyAttach = (S, orphanT, markerT) => {
|
|
4547
|
+
S.orphans.forEach((o, k) => {
|
|
4548
|
+
if (orphanT[k] >= 0) add(S.out[orphanT[k]], o.toks);
|
|
4549
|
+
});
|
|
4550
|
+
S.markers.forEach((m, k) => {
|
|
4551
|
+
const i = markerT[k];
|
|
4552
|
+
if (i < 0 || S.out[i].revision) return;
|
|
4553
|
+
const ebox = m.tri ? merge(bboxOf(m.span), m.tri) : bboxOf(m.span);
|
|
4554
|
+
S.out[i].revision = { rev: m.rev, source: { sheet: sheetKey, text: m.span.str.trim(), bbox: ebox }, ...m.drawn ? { drawn: true } : {} };
|
|
4555
|
+
});
|
|
4556
|
+
};
|
|
4557
|
+
const keyCol = cols ? cols.cols[0].label : anchors[0].label;
|
|
4558
|
+
const printed = (r) => {
|
|
4559
|
+
const keyRest = norm(r.cells[keyCol]?.text ?? r.key).replace(/[^A-Z0-9 ]/g, "").trim().replace(new RegExp("^" + r.key + "\\b"), "").trim();
|
|
4560
|
+
return [...keyRest ? [keyRest] : [], ...Object.entries(r.cells).filter(([k]) => k !== keyCol).map(([, c]) => norm(c.text))];
|
|
4561
|
+
};
|
|
4562
|
+
const prefixes = (label, n) => n.key.startsWith(label + "-") || new RegExp("^" + label + "\\d").test(n.key);
|
|
4563
|
+
const dropGroupLabels = (S) => {
|
|
4399
4564
|
const dropped = /* @__PURE__ */ new Set();
|
|
4400
|
-
for (let i = out.length - 1; i >= 0; i--) {
|
|
4401
|
-
const r = out[i];
|
|
4565
|
+
for (let i = S.out.length - 1; i >= 0; i--) {
|
|
4566
|
+
const r = S.out[i];
|
|
4402
4567
|
if (!/^[A-Z]{1,3}$/.test(r.key)) continue;
|
|
4403
|
-
const
|
|
4404
|
-
const texts = [...keyRest ? [keyRest] : [], ...Object.entries(r.cells).filter(([k]) => k !== keyCol).map(([, c]) => norm(c.text))];
|
|
4568
|
+
const texts = printed(r);
|
|
4405
4569
|
if (texts.length && !texts.some((x) => /^\d{2} ?\d{2}[ .]?\d{2}\b/.test(x))) continue;
|
|
4406
|
-
if (out.slice(i + 1, i + 4).some((n) =>
|
|
4570
|
+
if (S.out.slice(i + 1, i + 4).some((n) => prefixes(r.key, n))) {
|
|
4407
4571
|
dropped.add(r);
|
|
4408
|
-
|
|
4409
|
-
outY.splice(i, 1);
|
|
4572
|
+
dropRowAt(S, i);
|
|
4410
4573
|
}
|
|
4411
4574
|
}
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4575
|
+
return dropped;
|
|
4576
|
+
};
|
|
4577
|
+
const setBuildings = (out) => {
|
|
4578
|
+
for (const row of out) {
|
|
4579
|
+
if (row.building) continue;
|
|
4580
|
+
const cellB = norm(row.cells.BLDG?.text || row.cells.BUILDING?.text || "");
|
|
4581
|
+
if (DESIGNATOR_RE.test(cellB)) row.building = cellB;
|
|
4582
|
+
}
|
|
4583
|
+
};
|
|
4584
|
+
const regionOf = (S, dropped) => {
|
|
4585
|
+
let region = null;
|
|
4586
|
+
for (const r of S.everRows) if (!dropped.has(r)) for (const c of Object.values(r.cells)) region = region ? merge(region, c.bbox) : c.bbox;
|
|
4587
|
+
const lastY = S.outY.length ? S.outY[S.outY.length - 1] : -Infinity;
|
|
4588
|
+
for (const hd of S.headings) if (hd.y <= lastY) region = region ? merge(region, hd.bbox) : hd.bbox;
|
|
4589
|
+
return region;
|
|
4590
|
+
};
|
|
4591
|
+
const markWordSplitNotUsed = (S, keyFirst2) => {
|
|
4592
|
+
if (!keyFirst2) return;
|
|
4593
|
+
S.out.forEach((r, k) => {
|
|
4594
|
+
if (r.notUsed) return;
|
|
4595
|
+
const rest = S.outToks[k].slice(1).filter((t) => columnOf(t) === keyCol).map((t) => t.str.trim()).join(" ");
|
|
4596
|
+
if (!rest || !normalizeNotUsed(rest)) return;
|
|
4597
|
+
r.notUsed = true;
|
|
4598
|
+
r.notUsedText = rest.replace(/^[-–—:,\s]+/, "");
|
|
4599
|
+
});
|
|
4600
|
+
};
|
|
4601
|
+
const P1 = scan();
|
|
4602
|
+
const reset1 = blankBandResets(P1);
|
|
4603
|
+
endCut(P1);
|
|
4604
|
+
const plan1 = attachPlan(P1);
|
|
4605
|
+
const orphanLine1 = new Map(P1.orphans.map((o, k) => [o.i, plan1.orphanT[k] >= 0 ? P1.outI[plan1.orphanT[k]] : -1]));
|
|
4606
|
+
const markerLine1 = plan1.markerT.map((t) => t >= 0 ? P1.outI[t] : -1);
|
|
4607
|
+
const keyedY1 = new Map(P1.outI.map((i, k) => [i, P1.outY[k]]));
|
|
4608
|
+
const everLine1 = new Map(P1.everRows.map((r, k) => [r, P1.everI[k]]));
|
|
4609
|
+
const keyByLine1 = new Map(P1.everRows.map((r, k) => [P1.everI[k], r.key]));
|
|
4610
|
+
applyAttach(P1, plan1.orphanT, plan1.markerT);
|
|
4611
|
+
const dropped1 = finish ? dropGroupLabels(P1) : /* @__PURE__ */ new Set();
|
|
4612
|
+
setBuildings(P1.out);
|
|
4613
|
+
const region1 = regionOf(P1, dropped1);
|
|
4614
|
+
const mq = finish && !!cfg.marqueeRules && cfg.headerY != null;
|
|
4615
|
+
if (!mq) {
|
|
4616
|
+
resetSections(reset1);
|
|
4617
|
+
return { out: P1.out, region: region1 };
|
|
4618
|
+
}
|
|
4619
|
+
const headerY = cfg.headerY;
|
|
4620
|
+
const keyFirst = KEY_FIRST.has(anchors[0]?.label);
|
|
4621
|
+
const p1Lines = new Set(P1.outI);
|
|
4622
|
+
const lowerMedianGap = (ys) => {
|
|
4623
|
+
const d = ys.slice(1).map((y, k) => y - ys[k]).filter((g) => g > 0).sort((p, q) => p - q);
|
|
4624
|
+
return d.length ? d[d.length - 1 >> 1] : 0;
|
|
4625
|
+
};
|
|
4626
|
+
const lastP1Y = P1.outY.length ? P1.outY[P1.outY.length - 1] : -Infinity;
|
|
4627
|
+
const lineYs = [headerY, ...P1.lines.map((l) => l.y)].sort((p, q) => p - q);
|
|
4628
|
+
const linePitch = lowerMedianGap(lineYs.filter((y) => y <= lastP1Y));
|
|
4629
|
+
const keyXs = P1.outToks.map((t) => t[0].x).sort((p, q) => p - q);
|
|
4630
|
+
const keyHdr = cfg.hdrSpans?.find((t) => t.x - 0.5 <= anchors[0].x && anchors[0].x <= t.x + (t.w || 0) + 0.5);
|
|
4631
|
+
const keyRefX = keyXs.length ? keyXs[keyXs.length - 1 >> 1] : keyHdr ? keyHdr.x : anchors[0].x;
|
|
4632
|
+
const textH = (toks) => {
|
|
4633
|
+
const hs = toks.map((t) => t.h || 8).sort((p, q) => p - q);
|
|
4634
|
+
return hs[hs.length >> 1];
|
|
4635
|
+
};
|
|
4636
|
+
const aligned = (t, h) => inKey(t) && (cols ? Math.abs(atOf(t) - cols.cols[0].start) <= keyTol : Math.abs(t.x - keyRefX) <= Math.max(8, 0.5 * h));
|
|
4637
|
+
const gapAbove = (y) => {
|
|
4638
|
+
let above = -Infinity;
|
|
4639
|
+
for (const ly of lineYs) if (ly < y && ly > above) above = ly;
|
|
4640
|
+
return y - above;
|
|
4641
|
+
};
|
|
4642
|
+
const eligible = [];
|
|
4643
|
+
const ocr = !!cfg.ocr;
|
|
4644
|
+
if (keyFirst) {
|
|
4645
|
+
for (const o of P1.orphans) {
|
|
4646
|
+
const h = textH(o.toks);
|
|
4647
|
+
if (!aligned(o.toks[0], h)) continue;
|
|
4648
|
+
const att = orphanLine1.get(o.i) ?? -1, gap = gapAbove(o.y);
|
|
4649
|
+
if (att < 0 || !ocr && gap >= Math.max(1.6 * h, 0.75 * linePitch)) eligible.push({ i: o.i, y: o.y, toks: o.toks, att, gap });
|
|
4415
4650
|
}
|
|
4416
4651
|
}
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4652
|
+
const newRule = [];
|
|
4653
|
+
const cands = [];
|
|
4654
|
+
const hdrLines = [];
|
|
4655
|
+
const cellsOfLine = (toks) => {
|
|
4656
|
+
const by = /* @__PURE__ */ new Map();
|
|
4657
|
+
for (const t of toks.slice(1)) {
|
|
4658
|
+
const l = columnOf(t);
|
|
4659
|
+
if (l !== keyCol) by.set(l, by.has(l) ? `${by.get(l)} ${t.str.trim()}` : t.str.trim());
|
|
4660
|
+
}
|
|
4661
|
+
return by;
|
|
4662
|
+
};
|
|
4663
|
+
const wordsOfTok = (t) => norm(t.str).split(/[\s/]+/).map((w) => w.replace(/^[^A-Z0-9]+|[^A-Z0-9]+$/g, "")).filter(Boolean);
|
|
4664
|
+
const allHeaderWords = (t) => {
|
|
4665
|
+
const w = wordsOfTok(t);
|
|
4666
|
+
return w.length > 0 && /[A-Z]/.test(norm(t.str)) && w.every((x) => !/\d/.test(x) && HEADER_WORDS.has(x));
|
|
4667
|
+
};
|
|
4668
|
+
const headerLine = (toks) => toks.length > 2 && toks.slice(1).every(allHeaderWords) && toks.slice(1).some((t) => wordsOfTok(t).some((x) => HEADER_WORDS_STRONG.has(x)));
|
|
4669
|
+
const codeShaped = (x) => {
|
|
4670
|
+
const c = norm(x);
|
|
4671
|
+
return CODE_RE.test(c) && /\d/.test(c);
|
|
4672
|
+
};
|
|
4673
|
+
for (const e of eligible) {
|
|
4674
|
+
const s = splitKeyCell(e.toks[0].str);
|
|
4675
|
+
if (s) {
|
|
4676
|
+
newRule.push({ e, s });
|
|
4677
|
+
continue;
|
|
4678
|
+
}
|
|
4679
|
+
const word = norm(e.toks[0].str).replace(/^[^A-Z0-9]+|[^A-Z0-9]+$/g, "");
|
|
4680
|
+
if (LETTERS_RE.test(word) && !e.toks.slice(1).some(inKey)) {
|
|
4681
|
+
if (ocr) continue;
|
|
4682
|
+
if (LETTERS_STOP.has(word)) continue;
|
|
4683
|
+
if (headerLine(e.toks)) {
|
|
4684
|
+
hdrLines.push(e);
|
|
4685
|
+
continue;
|
|
4686
|
+
}
|
|
4687
|
+
const cells = cellsOfLine(e.toks);
|
|
4688
|
+
if ([...cells.values()].filter(codeShaped).length >= 2) continue;
|
|
4689
|
+
cands.push({ e, word, cells });
|
|
4690
|
+
continue;
|
|
4691
|
+
}
|
|
4692
|
+
if (allHeaderWords(e.toks[0]) && headerLine(e.toks)) hdrLines.push(e);
|
|
4693
|
+
}
|
|
4694
|
+
if (cands.length) {
|
|
4695
|
+
const decidedLines = /* @__PURE__ */ new Set([...newRule.map((n) => n.e.i), ...cands.map((c) => c.e.i), ...hdrLines.map((e) => e.i)]);
|
|
4696
|
+
const wraps = [];
|
|
4697
|
+
for (const o of P1.orphans) {
|
|
4698
|
+
const t1 = orphanLine1.get(o.i) ?? -1;
|
|
4699
|
+
const rowY1 = keyedY1.get(t1);
|
|
4700
|
+
if (t1 < 0 || rowY1 == null || decidedLines.has(o.i) || o.y <= rowY1) continue;
|
|
4701
|
+
wraps.push({ row: t1, off: o.y - rowY1 });
|
|
4702
|
+
}
|
|
4703
|
+
const rowAbove = (y) => {
|
|
4704
|
+
let best = null;
|
|
4705
|
+
P1.outY.forEach((ry, k) => {
|
|
4706
|
+
if (ry < y && (!best || ry > best.y)) best = { row: P1.outI[k], y: ry, cells: P1.outToks[k].slice(1).some((t) => columnOf(t) !== keyCol) };
|
|
4707
|
+
});
|
|
4708
|
+
return best;
|
|
4709
|
+
};
|
|
4710
|
+
for (let k = cands.length - 1; k >= 0; k--) {
|
|
4711
|
+
const c = cands[k], up = rowAbove(c.e.y);
|
|
4712
|
+
if (!up || !up.cells) continue;
|
|
4713
|
+
const off = c.e.y - up.y, tol = Math.max(1, 0.1 * textH(c.e.toks));
|
|
4714
|
+
if (wraps.some((w) => w.row !== up.row && Math.abs(w.off - off) <= tol)) cands.splice(k, 1);
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
const rowGap = lowerMedianGap(P1.outY);
|
|
4718
|
+
const runPitch = P1.outY.length > 1 ? rowGap : medPitchLo;
|
|
4719
|
+
const walk = [
|
|
4720
|
+
...P1.outY.map((y, k) => ({ y, kind: "row", at: k })),
|
|
4721
|
+
...P1.headings.map((h, k) => ({ y: h.y, kind: "head", at: k })),
|
|
4722
|
+
...newRule.map((n, k) => ({ y: n.e.y, kind: "new", at: k })),
|
|
4723
|
+
...cands.map((c, k) => ({ y: c.e.y, kind: "cand", at: k }))
|
|
4724
|
+
].sort((p, q) => p.y - q.y);
|
|
4725
|
+
const reachedDown = /* @__PURE__ */ new Set(), reached = /* @__PURE__ */ new Set();
|
|
4726
|
+
let prev = headerY;
|
|
4727
|
+
for (const w of walk) {
|
|
4728
|
+
if (w.y - prev <= (w.kind === "row" ? 8 : 2.5) * runPitch) {
|
|
4729
|
+
reachedDown.add(w);
|
|
4730
|
+
reached.add(w);
|
|
4731
|
+
prev = w.y;
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
let next = Infinity;
|
|
4735
|
+
for (let k = walk.length - 1; k >= 0; k--) {
|
|
4736
|
+
const w = walk[k];
|
|
4737
|
+
if (w.kind === "row") {
|
|
4738
|
+
next = reachedDown.has(w) ? w.y : Infinity;
|
|
4739
|
+
continue;
|
|
4740
|
+
}
|
|
4741
|
+
if (w.kind === "head") continue;
|
|
4742
|
+
if (next - w.y <= 2.5 * runPitch) {
|
|
4743
|
+
reached.add(w);
|
|
4744
|
+
next = w.y;
|
|
4745
|
+
} else next = Infinity;
|
|
4746
|
+
}
|
|
4747
|
+
const runHasSection = walk.some((w) => w.kind === "head" && reachedDown.has(w) && P1.headings[w.at].section != null);
|
|
4748
|
+
const newReached = new Set(walk.filter((w) => w.kind === "new" && reached.has(w)).map((w) => w.at));
|
|
4749
|
+
const surviving = newRule.filter((_n, k) => newReached.has(k));
|
|
4750
|
+
const rekey = /* @__PURE__ */ new Map();
|
|
4751
|
+
if (keyFirst) {
|
|
4752
|
+
const p1Split = P1.out.map((r, k) => ({ i: P1.outI[k], key: r.key, s: splitKeyCell(P1.outToks[k][0].str) }));
|
|
4753
|
+
for (const h of p1Split) if (h.s?.notUsed) rekey.set(h.i, { key: h.s.key, notUsed: true, notUsedText: h.s.notUsedText });
|
|
4754
|
+
const finalKeys = [...p1Split.map((h) => ({ i: h.i, key: h.s?.notUsed ? h.s.key : h.key })), ...surviving.map((n) => ({ i: n.e.i, key: n.s.key }))];
|
|
4755
|
+
const splitters = p1Split.filter((h) => h.s?.qualifier != null && !isAltQualifier(h.s.qualifier));
|
|
4756
|
+
for (const h of splitters) {
|
|
4757
|
+
const clash = finalKeys.some((f) => f.i !== h.i && f.key === h.s.key);
|
|
4758
|
+
const twin = splitters.some((o) => o !== h && o.s.key === h.s.key);
|
|
4759
|
+
if (!clash && !twin) rekey.set(h.i, { key: h.s.key, qualifier: h.s.qualifier });
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
const fate = /* @__PURE__ */ new Map();
|
|
4763
|
+
const keyedRows = [...P1.out.map((r, k) => ({ key: r.key, y: P1.outY[k] })), ...surviving.map((n) => ({ key: n.s.key, y: n.e.y }))].sort((p, q) => p.y - q.y);
|
|
4764
|
+
const numberedYs = keyedRows.filter((r) => /\d/.test(r.key)).map((r) => r.y);
|
|
4765
|
+
for (let k = 0; k < walk.length; k++) {
|
|
4766
|
+
const w = walk[k];
|
|
4767
|
+
if (w.kind !== "cand") continue;
|
|
4768
|
+
const c = cands[w.at];
|
|
4769
|
+
if (!reached.has(w)) {
|
|
4770
|
+
fate.set(w.at, "out");
|
|
4771
|
+
continue;
|
|
4772
|
+
}
|
|
4773
|
+
const prefixed = keyedRows.filter((r) => r.y > c.e.y).slice(0, 3).some((r) => r.key.startsWith(c.word + "-") || new RegExp("^" + c.word + "\\d").test(r.key));
|
|
4774
|
+
if (prefixed && c.cells.size <= 2 && [...c.cells.values()].some((x) => SPEC_SECTION_RE.test(norm(x)))) {
|
|
4775
|
+
fate.set(w.at, "s");
|
|
4776
|
+
continue;
|
|
4777
|
+
}
|
|
4778
|
+
if (prefixed && c.cells.size === 1) {
|
|
4779
|
+
fate.set(w.at, "g");
|
|
4780
|
+
continue;
|
|
4781
|
+
}
|
|
4782
|
+
let above = null;
|
|
4783
|
+
for (let j = k - 1; j >= 0 && !above; j--) {
|
|
4784
|
+
const l = walk[j];
|
|
4785
|
+
if (l.kind === "head") continue;
|
|
4786
|
+
if (l.kind === "cand" && fate.get(l.at) !== "row") continue;
|
|
4787
|
+
if (l.kind === "new" && !newReached.has(l.at)) continue;
|
|
4788
|
+
above = l;
|
|
4789
|
+
}
|
|
4790
|
+
const aboveOk = !above || above.kind === "row" || above.kind === "new" || above.kind === "cand" && fate.get(above.at) === "row";
|
|
4791
|
+
fate.set(w.at, aboveOk && numberedYs.some((y) => y > c.e.y) && c.cells.size >= 2 ? "row" : "skipped");
|
|
4792
|
+
}
|
|
4793
|
+
const letterRows = cands.filter((_c, k) => fate.get(k) === "row");
|
|
4794
|
+
const drops = [
|
|
4795
|
+
...hdrLines.map((e) => ({ e, reason: "h" })),
|
|
4796
|
+
...cands.flatMap((c, k) => {
|
|
4797
|
+
const f = fate.get(k);
|
|
4798
|
+
return f === "s" || f === "g" || f === "skipped" ? [{ e: c.e, reason: f, word: c.word }] : [];
|
|
4799
|
+
})
|
|
4800
|
+
].sort((p, q) => p.e.y - q.e.y);
|
|
4801
|
+
const skipped = drops.filter((d) => d.reason === "skipped").map((d) => d.word);
|
|
4802
|
+
const diagOf = (e, kind2) => ({ y: e.y, kind: kind2, gapAbove: e.gap, linePitch, runPitch });
|
|
4803
|
+
const diag = [
|
|
4804
|
+
...surviving.map((n) => diagOf(n.e, "new")),
|
|
4805
|
+
...letterRows.map((c) => diagOf(c.e, "new")),
|
|
4806
|
+
...drops.map((d) => diagOf(d.e, d.reason === "skipped" ? "skipped" : "consumed"))
|
|
4807
|
+
].sort((p, q) => p.y - q.y);
|
|
4808
|
+
const ownerOf = (line) => line >= 0 ? keyByLine1.get(line) ?? null : null;
|
|
4809
|
+
const textOf = (toks) => toks.map((t) => t.str.trim()).join(" ");
|
|
4810
|
+
const consumed = drops.map((d) => ({ text: textOf(d.e.toks), y: d.e.y, reason: d.reason, owner: ownerOf(d.e.att) }));
|
|
4811
|
+
const guard = { pass1Region: region1, pass1Rows: P1.out.length, pass1HasSection: P1.out.some((r) => !!r.section), runHasSection };
|
|
4812
|
+
const meta = /* @__PURE__ */ new Map();
|
|
4813
|
+
if (!surviving.length && !rekey.size && !letterRows.length && !drops.length) {
|
|
4814
|
+
for (let k = 0; k < P1.out.length; k++) meta.set(P1.out[k], { y: P1.outY[k], pass1: true });
|
|
4815
|
+
markWordSplitNotUsed(P1, keyFirst);
|
|
4816
|
+
resetSections(reset1);
|
|
4817
|
+
return { out: P1.out, region: region1, marquee: { ...guard, meta, consumed, diag, skipped } };
|
|
4818
|
+
}
|
|
4819
|
+
const over = new Map(rekey);
|
|
4820
|
+
for (const n of surviving) over.set(n.e.i, { key: n.s.key, qualifier: n.s.qualifier, notUsed: n.s.notUsed, notUsedText: n.s.notUsedText });
|
|
4821
|
+
for (const c of letterRows) over.set(c.e.i, { key: c.word });
|
|
4822
|
+
const dropLines = new Set(drops.map((d) => d.e.i));
|
|
4823
|
+
const P2 = scan(over, dropLines, ocr ? new Set(surviving.map((n) => n.e.i)) : void 0);
|
|
4824
|
+
const reset2 = blankBandResets(P2);
|
|
4825
|
+
endCut(P2);
|
|
4826
|
+
const lettersLines = new Set(letterRows.map((c) => c.e.i));
|
|
4827
|
+
const newLines = /* @__PURE__ */ new Set([...surviving.map((n) => n.e.i), ...lettersLines]);
|
|
4828
|
+
const at2 = new Map(P2.outI.map((i, k) => [i, k]));
|
|
4829
|
+
const p1Ys = [...keyedY1.entries()].sort((p, q) => p[1] - q[1]);
|
|
4830
|
+
const moved = /* @__PURE__ */ new Map();
|
|
4831
|
+
const dropCont = /* @__PURE__ */ new Set();
|
|
4832
|
+
const ungluedFrom = /* @__PURE__ */ new Map();
|
|
4833
|
+
const decided = [...surviving.map((n) => ({ e: n.e, drop: false })), ...letterRows.map((c) => ({ e: c.e, drop: false })), ...drops.map((d) => ({ e: d.e, drop: true }))].sort((p, q) => p.e.y - q.e.y);
|
|
4834
|
+
for (const d of decided) {
|
|
4835
|
+
const tookFrom = d.e.toks.map((t) => ({ from: ownerOf(d.e.att), text: t.str.trim() }));
|
|
4836
|
+
const nextKeyed = Math.min(...P2.outY.filter((y) => y > d.e.y), ...decided.map((x) => x.e.y).filter((y) => y > d.e.y), Infinity);
|
|
4837
|
+
const nextP1 = p1Ys.find(([, y]) => y > d.e.y);
|
|
4838
|
+
let lastY = d.e.y;
|
|
4839
|
+
for (const o of P2.orphans.filter((q) => q.y > d.e.y && q.y < nextKeyed).sort((p, q) => p.y - q.y)) {
|
|
4840
|
+
const t1 = orphanLine1.get(o.i) ?? -1;
|
|
4841
|
+
if (o.y - lastY > plan1.radius(Math.max(...o.toks.map((t) => t.h || 8)))) break;
|
|
4842
|
+
const take = t1 < 0 || t1 === d.e.att || !!nextP1 && t1 === nextP1[0] && o.y - lastY < nextP1[1] - o.y;
|
|
4843
|
+
if (!take) break;
|
|
4844
|
+
if (d.drop) {
|
|
4845
|
+
dropCont.add(o.i);
|
|
4846
|
+
consumed.push({ text: textOf(o.toks), y: o.y, reason: "continuation", owner: ownerOf(t1) });
|
|
4847
|
+
} else {
|
|
4848
|
+
moved.set(o.i, d.e.i);
|
|
4849
|
+
for (const t of o.toks) tookFrom.push({ from: ownerOf(t1), text: t.str.trim() });
|
|
4850
|
+
}
|
|
4851
|
+
lastY = o.y;
|
|
4852
|
+
}
|
|
4853
|
+
if (!d.drop) ungluedFrom.set(d.e.i, tookFrom);
|
|
4421
4854
|
}
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4855
|
+
consumed.sort((p, q) => p.y - q.y);
|
|
4856
|
+
const toNewRow = (y, h) => {
|
|
4857
|
+
const { i, d } = plan1.nearestIn(P2.outY, y);
|
|
4858
|
+
if (i < 0 || d > plan1.radius(h) || plan1.byHeading(y, d, P2.outY[i])) return -1;
|
|
4859
|
+
return newLines.has(P2.outI[i]) ? i : -1;
|
|
4860
|
+
};
|
|
4861
|
+
const orphanT2 = P2.orphans.map((o) => {
|
|
4862
|
+
if (dropCont.has(o.i)) return -1;
|
|
4863
|
+
const to = moved.get(o.i);
|
|
4864
|
+
if (to != null) return at2.get(to) ?? -1;
|
|
4865
|
+
const t1 = orphanLine1.get(o.i) ?? -1;
|
|
4866
|
+
if (t1 >= 0) return at2.get(t1) ?? -1;
|
|
4867
|
+
return toNewRow(o.y, Math.max(...o.toks.map((t) => t.h || 8)));
|
|
4868
|
+
});
|
|
4869
|
+
const markerT2 = P2.markers.map((m, k) => {
|
|
4870
|
+
if (dropLines.has(m.i)) return -1;
|
|
4871
|
+
if (newLines.has(m.i)) return at2.get(m.i) ?? -1;
|
|
4872
|
+
const t1 = markerLine1[k];
|
|
4873
|
+
if (t1 >= 0) return at2.get(t1) ?? -1;
|
|
4874
|
+
return toNewRow(m.span.y, m.span.h || 8);
|
|
4875
|
+
});
|
|
4876
|
+
applyAttach(P2, orphanT2, markerT2);
|
|
4877
|
+
const droppedLines1 = new Set([...dropped1].map((r) => everLine1.get(r)));
|
|
4878
|
+
const emptied = /* @__PURE__ */ new Set();
|
|
4879
|
+
for (const o of P1.orphans) {
|
|
4880
|
+
const t1 = orphanLine1.get(o.i) ?? -1;
|
|
4881
|
+
if (t1 >= 0 && (newLines.has(o.i) || moved.has(o.i) || dropLines.has(o.i) || dropCont.has(o.i))) emptied.add(t1);
|
|
4882
|
+
}
|
|
4883
|
+
const dropped2 = /* @__PURE__ */ new Set();
|
|
4884
|
+
for (let k = P2.out.length - 1; k >= 0; k--) if (droppedLines1.has(P2.outI[k])) {
|
|
4885
|
+
dropped2.add(P2.out[k]);
|
|
4886
|
+
dropRowAt(P2, k);
|
|
4887
|
+
}
|
|
4888
|
+
for (let k = P2.out.length - 1; k >= 0; k--) {
|
|
4889
|
+
const r = P2.out[k];
|
|
4890
|
+
if (!emptied.has(P2.outI[k]) || !/^[A-Z]{1,3}$/.test(r.key) || printed(r).length) continue;
|
|
4891
|
+
if (P2.out.slice(k + 1, k + 4).some((n) => prefixes(r.key, n))) {
|
|
4892
|
+
dropped2.add(r);
|
|
4893
|
+
dropRowAt(P2, k);
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
setBuildings(P2.out);
|
|
4897
|
+
const region2 = regionOf(P2, dropped2);
|
|
4898
|
+
for (let k = 0; k < P2.out.length; k++) {
|
|
4899
|
+
const i = P2.outI[k], r = P2.out[k];
|
|
4900
|
+
const m = { y: P2.outY[k], pass1: p1Lines.has(i) };
|
|
4901
|
+
if (newLines.has(i)) {
|
|
4902
|
+
m.newRule = true;
|
|
4903
|
+
m.ungluedFrom = ungluedFrom.get(i);
|
|
4904
|
+
}
|
|
4905
|
+
if (lettersLines.has(i)) m.lettersKey = true;
|
|
4906
|
+
if (rekey.has(i)) m.pass1Key = keyByLine1.get(i);
|
|
4907
|
+
meta.set(r, m);
|
|
4908
|
+
}
|
|
4909
|
+
markWordSplitNotUsed(P2, keyFirst);
|
|
4910
|
+
resetSections(reset2);
|
|
4911
|
+
return { out: P2.out, region: region2, marquee: { ...guard, meta, consumed, diag, skipped } };
|
|
4425
4912
|
}
|
|
4426
4913
|
function extractTable(sheet, kind, opts = {}) {
|
|
4427
4914
|
const r = extractTableCore(sheet, kind, opts);
|
|
@@ -4453,7 +4940,7 @@ function extractTables(sheet, kind, opts = {}) {
|
|
|
4453
4940
|
}
|
|
4454
4941
|
return out;
|
|
4455
4942
|
}
|
|
4456
|
-
function extractTableCore(sheet, kind, opts = {}) {
|
|
4943
|
+
function extractTableCore(sheet, kind, opts = {}, core = {}) {
|
|
4457
4944
|
const horiz = sheet.spans.filter((s) => !isVertical(s));
|
|
4458
4945
|
const vert = sheet.spans.filter(isVertical);
|
|
4459
4946
|
const rows = clusterRows(horiz);
|
|
@@ -4467,6 +4954,7 @@ function extractTableCore(sheet, kind, opts = {}) {
|
|
|
4467
4954
|
let dataBelowY = -Infinity;
|
|
4468
4955
|
let titleFrom;
|
|
4469
4956
|
let rotated = false;
|
|
4957
|
+
let headerY;
|
|
4470
4958
|
const flat = findHeaderRow(rows, vocab, required, minHits);
|
|
4471
4959
|
if (flat) {
|
|
4472
4960
|
anchors = flat.anchors;
|
|
@@ -4481,6 +4969,7 @@ function extractTableCore(sheet, kind, opts = {}) {
|
|
|
4481
4969
|
hdrBlock = [...band].flatMap((k) => rows[k]);
|
|
4482
4970
|
dataFrom = flat.rowIndex + 1;
|
|
4483
4971
|
titleFrom = flat.rowIndex - 1;
|
|
4972
|
+
headerY = hy;
|
|
4484
4973
|
} else {
|
|
4485
4974
|
const rot = findRotatedHeader(vert, vocab, required, minHits);
|
|
4486
4975
|
if (!rot) return null;
|
|
@@ -4488,6 +4977,7 @@ function extractTableCore(sheet, kind, opts = {}) {
|
|
|
4488
4977
|
anchors = rot.anchors;
|
|
4489
4978
|
headerSpans = rot.spans;
|
|
4490
4979
|
dataBelowY = rot.bottom - 2;
|
|
4980
|
+
headerY = rot.bottom;
|
|
4491
4981
|
dataFrom = 0;
|
|
4492
4982
|
titleFrom = rows.findIndex((r) => rowY(r) >= rot.top) - 1;
|
|
4493
4983
|
if (titleFrom < -1) titleFrom = rows.length - 1;
|
|
@@ -4503,23 +4993,38 @@ function extractTableCore(sheet, kind, opts = {}) {
|
|
|
4503
4993
|
if (centerX(t) < hdrBand.x0 || centerX(t) > hdrBand.x1) continue;
|
|
4504
4994
|
region = region ? merge(region, bboxOf(t)) : bboxOf(t);
|
|
4505
4995
|
}
|
|
4506
|
-
const
|
|
4996
|
+
const marqueeRules = kind === "finish" && !!core.marqueeRules;
|
|
4997
|
+
const headerRegion = region;
|
|
4998
|
+
const banded = bandDataRows(rows, anchors, kind, sheet.key, opts.buildings, { fromIdx: dataFrom, belowY: dataBelowY, deltas: opts.deltas, sheetNumbers: opts.sheetNumbers, hdrSpans: headerSpans, hdrBand: hdrBlock, marquee: opts.marquee, resetAtBlankBand: opts.resetAtBlankBand, ...marqueeRules ? { marqueeRules, headerY, ...core.ocr ? { ocr: true } : {} } : {} });
|
|
4507
4999
|
const out = banded.out;
|
|
4508
5000
|
if (banded.region) region = region ? merge(region, banded.region) : banded.region;
|
|
5001
|
+
const { x0, x1 } = bandLimits(anchors);
|
|
5002
|
+
const findTitle = () => {
|
|
5003
|
+
let title2 = null;
|
|
5004
|
+
for (let i = titleFrom; i >= 0 && i >= titleFrom - 5 && !title2; i--) {
|
|
5005
|
+
const hit = rows[i].find((t) => /SCHEDULE/.test(norm(t.str)) && t.x >= x0 && t.x <= x1);
|
|
5006
|
+
if (hit) title2 = { sheet: sheet.key, text: hit.str.trim(), bbox: bboxOf(hit) };
|
|
5007
|
+
}
|
|
5008
|
+
return title2;
|
|
5009
|
+
};
|
|
5010
|
+
const wordsOf = () => headerSpans.flatMap((t) => t.str.toUpperCase().split(/[^A-Z]+/)).filter(Boolean);
|
|
4509
5011
|
if (!out.length) {
|
|
4510
5012
|
if (kind === "equipment" && region) return { skip: region };
|
|
5013
|
+
if (marqueeRules && headerRegion) {
|
|
5014
|
+
const mb2 = banded.marquee;
|
|
5015
|
+
return { headerOnly: { title: findTitle(), headers: anchors.map((a) => a.label), headerWords: wordsOf(), hasSection: mb2.runHasSection, region: headerRegion, skipped: mb2.skipped, trace: { meta: mb2.meta, consumed: mb2.consumed, diag: mb2.diag } } };
|
|
5016
|
+
}
|
|
4511
5017
|
return null;
|
|
4512
5018
|
}
|
|
4513
|
-
const
|
|
4514
|
-
let title = null;
|
|
4515
|
-
for (let i = titleFrom; i >= 0 && i >= titleFrom - 5 && !title; i--) {
|
|
4516
|
-
const hit = rows[i].find((t) => /SCHEDULE/.test(norm(t.str)) && t.x >= x0 && t.x <= x1);
|
|
4517
|
-
if (hit) title = { sheet: sheet.key, text: hit.str.trim(), bbox: bboxOf(hit) };
|
|
4518
|
-
}
|
|
5019
|
+
const title = findTitle();
|
|
4519
5020
|
const table = { kind, sheet: sheet.key, title, headers: anchors.map((a) => a.label), rows: out, region, anchors };
|
|
4520
5021
|
if (rotated) table.rotated_headers = true;
|
|
4521
|
-
const headerWords =
|
|
4522
|
-
return { table, headerWords };
|
|
5022
|
+
const headerWords = wordsOf();
|
|
5023
|
+
if (!marqueeRules) return { table, headerWords };
|
|
5024
|
+
const mb = banded.marquee;
|
|
5025
|
+
const guardRegion = mb.pass1Rows ? headerRegion && mb.pass1Region ? merge(headerRegion, mb.pass1Region) : headerRegion ?? mb.pass1Region : headerRegion ?? region;
|
|
5026
|
+
const hasSection = mb.pass1Rows ? mb.pass1HasSection : mb.runHasSection;
|
|
5027
|
+
return { table, headerWords, guard: { guardRegion, hasSection, skipped: mb.skipped, trace: { meta: mb.meta, consumed: mb.consumed, diag: mb.diag } } };
|
|
4523
5028
|
}
|
|
4524
5029
|
var CONT_TAIL_RE = /[\s\-–—:.,(]*(?:CONTINUATION|CONTINUED|CONT['’]?D?)[\s.)]*$/;
|
|
4525
5030
|
var isContinuationTitle = (text) => {
|
|
@@ -9192,7 +9697,7 @@ var Session = class _Session {
|
|
|
9192
9697
|
if (assign) {
|
|
9193
9698
|
if (s.upp == null) throw new UserError(this.scaleGate(s));
|
|
9194
9699
|
graph = await this.ensureGraph();
|
|
9195
|
-
if (!graph.available) throw new UserError("This set has no text layer (a
|
|
9700
|
+
if (!graph.available) throw new UserError("This set has no text layer (a raster image) \u2014 the sheet graph is unavailable, not empty.");
|
|
9196
9701
|
if (!graph.tables.some((t) => t.kind === "room-finish")) {
|
|
9197
9702
|
throw new UserError("No room-finish schedule in the working set \u2014 load_plan the schedule sheet with merge: true, or pass condition to commit every room under one tag.");
|
|
9198
9703
|
}
|
|
@@ -16062,7 +16567,7 @@ No review gate: the pencil-not-ink rule exists to stop an agent inventing geomet
|
|
|
16062
16567
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16063
16568
|
|
|
16064
16569
|
// src/wiki.generated.ts
|
|
16065
|
-
var WIKI_VERSION = "0.9.
|
|
16570
|
+
var WIKI_VERSION = "0.9.96";
|
|
16066
16571
|
var WIKI_PAGES = [
|
|
16067
16572
|
{
|
|
16068
16573
|
"key": "index",
|
|
@@ -16077,8 +16582,8 @@ var WIKI_PAGES = [
|
|
|
16077
16582
|
"uri": "takeoff://wiki/status",
|
|
16078
16583
|
"title": "Capability status",
|
|
16079
16584
|
"source": "docs/wiki/status.md",
|
|
16080
|
-
"source_sha256": "
|
|
16081
|
-
"text": "# Capability status\n\nThis page describes implementation support, not a promise that every plan can\nbe measured automatically. Follow its source links before changing behavior.\n\n| Capability | Current boundary | Evidence |\n|---|---|---|\n| Human and agent measurements | Shared geometry/quantity modules; source interpretation still requires inspection | [Geometry workflow](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md) |\n| Original machine geometry after human correction | Outer vertices preserved, including manual agent traces; missing historical originals cannot be recovered | [Compatibility](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/COMPATIBILITY.md) |\n| Human stitching | Browser gallery and alignment; editable browser archive preserves composites | [Workflow](takeoff://wiki/workflows) |\n| MCP stitched documents | No stitch tool; current import/export omits stitch records | [Field inventory](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/INVENTORY.md) |\n| One-Click | Temporarily gated in the default browser/MCP builds | [Gate](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/design/ONE_CLICK_GATE.md) |\n| Browser/MCP room boundaries | Shared quantity modules do not prove identical detector boundaries; the open [#385](https://github.com/Kentucky-ai/opentakeoff/issues/385) report remains unresolved | [Architecture](takeoff://wiki/architecture), [geometry evidence](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md) |\n| Toolbar command and voice | Gated in the default build; use normal controls and keyboard shortcuts | [User guide](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md#17-voice-and-the-command-box) |\n| TakeoffDocument v1 | Draft schemas, preflight and opt-in repository adapters for a bounded profile; legacy writer format remains | [Protocol](takeoff://wiki/protocol) |\n| Annotation cleanup | Text-only MCP edit with undo; RFI-linked notes refuse | [MCP routing](takeoff://wiki/mcp) |\n| Agent knowledge | Packaged wiki resources and source-generated tool/schema references | [Index](takeoff://wiki) |\n| Academy interoperability | Incompatibilities inventoried; no adapter or certification bridge claimed | [Academy report](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/ACADEMY_COMPATIBILITY.md) |\n| Shared work review | Personal layouts/shared review proposal is not merged; do not describe [PR #388](https://github.com/Kentucky-ai/opentakeoff/pull/388) as shipped | [Roadmap](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/ROADMAP.md) |\n| Microsoft 365 annotation sync | Unproven across machines; the live validation remains open in [#315](https://github.com/Kentucky-ai/opentakeoff/issues/315) | [Roadmap](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/ROADMAP.md) |\n| Learned floor-model bridge | Not on `main`; no model-generated geometry integration is advertised | [Roadmap](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/ROADMAP.md) |\n| Independent geometry benchmark | Reference-assisted and synthetic checks are not independently reviewed ground truth | [Geometry evidence](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md#reusable-evidence-from-a-project-test) |\n\nNext protocol work connects the tested contracts to focused MCP workflows and\nmeasures tool-selection/completion results. Broader adapter profiles remain\nbounded by preservation tests. Package extraction, verified identity/signatures, Academy credentials\nand optional anchoring come later. Rendering optimization is separate from\nthis protocol and knowledge work.\n"
|
|
16585
|
+
"source_sha256": "af701c08166b85f012db03053e7adecf0634ac6a8145f24e8d4c9dca02d314a2",
|
|
16586
|
+
"text": "# Capability status\n\nThis page describes implementation support, not a promise that every plan can\nbe measured automatically. Follow its source links before changing behavior.\n\n| Capability | Current boundary | Evidence |\n|---|---|---|\n| Human and agent measurements | Shared geometry/quantity modules; source interpretation still requires inspection | [Geometry workflow](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md) |\n| Original machine geometry after human correction | Outer vertices preserved, including manual agent traces; missing historical originals cannot be recovered | [Compatibility](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/COMPATIBILITY.md) |\n| Human stitching | Browser gallery and alignment; editable browser archive preserves composites | [Workflow](takeoff://wiki/workflows) |\n| MCP stitched documents | No stitch tool; current import/export omits stitch records | [Field inventory](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/INVENTORY.md) |\n| One-Click | Temporarily gated in the default browser/MCP builds | [Gate](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/design/ONE_CLICK_GATE.md) |\n| Browser/MCP room boundaries | Shared quantity modules do not prove identical detector boundaries; the open [#385](https://github.com/Kentucky-ai/opentakeoff/issues/385) report remains unresolved | [Architecture](takeoff://wiki/architecture), [geometry evidence](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md) |\n| Plan-set search, Copy text, on-device page reads | Browser gallery and canvas; MCP text tools read the text layer only, no OCR. OCR reads scanned pages after a download notice and is labelled OCR; Copy text works on single sheets, not stitched surfaces. Import from schedule reads a raster schedule box on-device after the same notice; the in-canvas agent's `read_schedule` reads the text layer only. Kept OCR reads stay in this browser after sign-out, kept by the PDF's content hash | [User guide](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md#copy-text) |\n| Import from schedule\u2014extra forms | Drawn box only: codes followed by a word (FTB-01 CUT) read as the code; NOT USED / N.I.C. rows start unticked; on a text-layer box, a four- or five-letter code with no number is read when the header or a read row is above it, it fills two or more other columns, and a numbered code comes after it, else listed as not read (the on-device raster read neither reads nor lists them, and there a NOT USED or word-after code line in a short table can stay part of the row above); the sheet graph's whole-sheet index reads them as before | [User guide](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md#import-from-schedule) |\n| Toolbar command and voice | Gated in the default build; use normal controls and keyboard shortcuts | [User guide](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md#17-voice-and-the-command-box) |\n| TakeoffDocument v1 | Draft schemas, preflight and opt-in repository adapters for a bounded profile; legacy writer format remains | [Protocol](takeoff://wiki/protocol) |\n| Annotation cleanup | Text-only MCP edit with undo; RFI-linked notes refuse | [MCP routing](takeoff://wiki/mcp) |\n| Agent knowledge | Packaged wiki resources and source-generated tool/schema references | [Index](takeoff://wiki) |\n| Academy interoperability | Incompatibilities inventoried; no adapter or certification bridge claimed | [Academy report](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/ACADEMY_COMPATIBILITY.md) |\n| Shared work review | Personal layouts/shared review proposal is not merged; do not describe [PR #388](https://github.com/Kentucky-ai/opentakeoff/pull/388) as shipped | [Roadmap](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/ROADMAP.md) |\n| Microsoft 365 annotation sync | Unproven across machines; the live validation remains open in [#315](https://github.com/Kentucky-ai/opentakeoff/issues/315) | [Roadmap](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/ROADMAP.md) |\n| Learned floor-model bridge | Not on `main`; no model-generated geometry integration is advertised | [Roadmap](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/ROADMAP.md) |\n| Independent geometry benchmark | Reference-assisted and synthetic checks are not independently reviewed ground truth | [Geometry evidence](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md#reusable-evidence-from-a-project-test) |\n\nNext protocol work connects the tested contracts to focused MCP workflows and\nmeasures tool-selection/completion results. Broader adapter profiles remain\nbounded by preservation tests. Package extraction, verified identity/signatures, Academy credentials\nand optional anchoring come later. Rendering optimization is separate from\nthis protocol and knowledge work.\n"
|
|
16082
16587
|
},
|
|
16083
16588
|
{
|
|
16084
16589
|
"key": "architecture",
|
|
@@ -16673,7 +17178,7 @@ function nameTheStageInRefusals(server) {
|
|
|
16673
17178
|
// package.json
|
|
16674
17179
|
var package_default = {
|
|
16675
17180
|
name: "opentakeoff-mcp",
|
|
16676
|
-
version: "0.9.
|
|
17181
|
+
version: "0.9.96",
|
|
16677
17182
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
16678
17183
|
type: "module",
|
|
16679
17184
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
package/package.json
CHANGED