opentakeoff-mcp 0.9.30 → 0.9.32
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 +378 -15
- package/package.json +2 -2
package/dist/server-core.js
CHANGED
|
@@ -3043,6 +3043,136 @@ var fail = (err) => ({
|
|
|
3043
3043
|
var round2 = (n) => +n.toFixed(2);
|
|
3044
3044
|
var round1 = (n) => +n.toFixed(1);
|
|
3045
3045
|
|
|
3046
|
+
// ../web/src/lib/variants.ts
|
|
3047
|
+
var SEP = " \u2013 ";
|
|
3048
|
+
var ROW_LINK_FIELDS = ["id", "origin_id", "inherited"];
|
|
3049
|
+
function baseTagOf(tag) {
|
|
3050
|
+
const s = String(tag ?? "");
|
|
3051
|
+
const i = s.indexOf(SEP);
|
|
3052
|
+
return (i === -1 ? s : s.slice(0, i)).trim();
|
|
3053
|
+
}
|
|
3054
|
+
function variantTag(baseOrTag, label) {
|
|
3055
|
+
const base = baseTagOf(baseOrTag);
|
|
3056
|
+
const l = String(label ?? "").trim();
|
|
3057
|
+
return l ? `${base}${SEP}${l}` : base;
|
|
3058
|
+
}
|
|
3059
|
+
function copyRow(row, id, originId) {
|
|
3060
|
+
return { ...row, id, origin_id: originId, inherited: true };
|
|
3061
|
+
}
|
|
3062
|
+
function childrenOf(conds, parentId) {
|
|
3063
|
+
return conds.filter((c) => c.variant_of === parentId);
|
|
3064
|
+
}
|
|
3065
|
+
function stripLinks(patch) {
|
|
3066
|
+
const out = { ...patch };
|
|
3067
|
+
for (const f of ROW_LINK_FIELDS) delete out[f];
|
|
3068
|
+
return out;
|
|
3069
|
+
}
|
|
3070
|
+
function mintTwin(parent, opts) {
|
|
3071
|
+
const family_id = parent.family_id || opts.mintId("fam");
|
|
3072
|
+
const materials = (parent.materials || []).map((r) => copyRow(r, opts.mintId("mat"), r.id));
|
|
3073
|
+
const twin = {
|
|
3074
|
+
...parent,
|
|
3075
|
+
id: opts.mintId("cnd"),
|
|
3076
|
+
created_at: opts.nowIso(),
|
|
3077
|
+
finish_tag: opts.tag || variantTag(parent.finish_tag, opts.label),
|
|
3078
|
+
family_id,
|
|
3079
|
+
variant_of: parent.id,
|
|
3080
|
+
variant_label: String(opts.label ?? "").trim() || void 0,
|
|
3081
|
+
materials,
|
|
3082
|
+
...opts.nextHatch ? { hatch: opts.nextHatch } : {}
|
|
3083
|
+
};
|
|
3084
|
+
delete twin.materials_dropped;
|
|
3085
|
+
delete twin.updated_at;
|
|
3086
|
+
if (twin.attrs && typeof twin.attrs === "object") twin.attrs = { ...twin.attrs };
|
|
3087
|
+
return { twin, parentPatch: parent.family_id ? null : { family_id } };
|
|
3088
|
+
}
|
|
3089
|
+
function propagateRowPatch(conds, parentId, originId, patch, seen = /* @__PURE__ */ new Set()) {
|
|
3090
|
+
if (seen.has(parentId)) return conds;
|
|
3091
|
+
seen.add(parentId);
|
|
3092
|
+
const clean = stripLinks(patch);
|
|
3093
|
+
let out = conds;
|
|
3094
|
+
for (const child of childrenOf(conds, parentId)) {
|
|
3095
|
+
if ((child.materials_dropped || []).includes(originId)) continue;
|
|
3096
|
+
const row = (child.materials || []).find((r) => r.origin_id === originId && r.inherited);
|
|
3097
|
+
if (!row) continue;
|
|
3098
|
+
out = out.map((c) => c.id !== child.id ? c : {
|
|
3099
|
+
...c,
|
|
3100
|
+
materials: (c.materials || []).map((r) => r.id !== row.id ? r : { ...r, ...clean, id: r.id, origin_id: r.origin_id, inherited: true })
|
|
3101
|
+
});
|
|
3102
|
+
out = propagateRowPatch(out, child.id, row.id, clean, seen);
|
|
3103
|
+
}
|
|
3104
|
+
return out;
|
|
3105
|
+
}
|
|
3106
|
+
function propagateRowAdd(conds, parentId, parentRow, mintId, seen = /* @__PURE__ */ new Set()) {
|
|
3107
|
+
if (seen.has(parentId)) return conds;
|
|
3108
|
+
seen.add(parentId);
|
|
3109
|
+
let out = conds;
|
|
3110
|
+
for (const child of childrenOf(conds, parentId)) {
|
|
3111
|
+
if ((child.materials_dropped || []).includes(parentRow.id)) continue;
|
|
3112
|
+
if ((child.materials || []).some((r) => r.origin_id === parentRow.id)) continue;
|
|
3113
|
+
const row = copyRow(parentRow, mintId("mat"), parentRow.id);
|
|
3114
|
+
out = out.map((c) => c.id !== child.id ? c : { ...c, materials: [...c.materials || [], row] });
|
|
3115
|
+
out = propagateRowAdd(out, child.id, row, mintId, seen);
|
|
3116
|
+
}
|
|
3117
|
+
return out;
|
|
3118
|
+
}
|
|
3119
|
+
function propagateRowRemove(conds, parentId, originId, seen = /* @__PURE__ */ new Set()) {
|
|
3120
|
+
if (seen.has(parentId)) return conds;
|
|
3121
|
+
seen.add(parentId);
|
|
3122
|
+
let out = conds;
|
|
3123
|
+
for (const child of childrenOf(conds, parentId)) {
|
|
3124
|
+
const row = (child.materials || []).find((r) => r.origin_id === originId);
|
|
3125
|
+
if (!row) continue;
|
|
3126
|
+
if (row.inherited) {
|
|
3127
|
+
out = out.map((c) => c.id !== child.id ? c : { ...c, materials: (c.materials || []).filter((r) => r.id !== row.id) });
|
|
3128
|
+
out = propagateRowRemove(out, child.id, row.id, seen);
|
|
3129
|
+
} else {
|
|
3130
|
+
out = out.map((c) => c.id !== child.id ? c : {
|
|
3131
|
+
...c,
|
|
3132
|
+
materials: (c.materials || []).map((r) => {
|
|
3133
|
+
if (r.id !== row.id) return r;
|
|
3134
|
+
const rest = { ...r };
|
|
3135
|
+
delete rest.origin_id;
|
|
3136
|
+
delete rest.inherited;
|
|
3137
|
+
return rest;
|
|
3138
|
+
})
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
return out;
|
|
3143
|
+
}
|
|
3144
|
+
function markRowLocal(cond, id) {
|
|
3145
|
+
return {
|
|
3146
|
+
...cond,
|
|
3147
|
+
materials: (cond.materials || []).map((r) => r.id === id ? { ...r, inherited: false } : r)
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
function dropRowLocal(cond, id) {
|
|
3151
|
+
const row = (cond.materials || []).find((r) => r.id === id);
|
|
3152
|
+
const materials = (cond.materials || []).filter((r) => r.id !== id);
|
|
3153
|
+
if (!row?.inherited || !row.origin_id) return { ...cond, materials };
|
|
3154
|
+
const dropped = [...cond.materials_dropped || []];
|
|
3155
|
+
if (!dropped.includes(row.origin_id)) dropped.push(row.origin_id);
|
|
3156
|
+
return { ...cond, materials, materials_dropped: dropped };
|
|
3157
|
+
}
|
|
3158
|
+
function splitFromFamily(conds, condId) {
|
|
3159
|
+
return conds.map((c) => {
|
|
3160
|
+
if (c.id !== condId) return c;
|
|
3161
|
+
const next = {
|
|
3162
|
+
...c,
|
|
3163
|
+
materials: (c.materials || []).map((r) => {
|
|
3164
|
+
const rest = { ...r };
|
|
3165
|
+
delete rest.origin_id;
|
|
3166
|
+
delete rest.inherited;
|
|
3167
|
+
return rest;
|
|
3168
|
+
})
|
|
3169
|
+
};
|
|
3170
|
+
delete next.variant_of;
|
|
3171
|
+
delete next.materials_dropped;
|
|
3172
|
+
return next;
|
|
3173
|
+
});
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3046
3176
|
// ../web/src/lib/confidence.ts
|
|
3047
3177
|
var CONF_RASTER = 0.9;
|
|
3048
3178
|
var CONF_HATCH = 0.95;
|
|
@@ -4500,6 +4630,7 @@ var PALETTE = ["#c96442", "#2f7d54", "#2563eb", "#9333ea", "#b8860b", "#0d9488",
|
|
|
4500
4630
|
var HATCH_IDS = ["solid", "diag", "diag2", "cross", "diagdense", "horiz", "vert", "grid", "brick", "plank", "herring", "basket", "checker", "wave", "dots", "speckle", "iso", "honeycomb", "scan", "plus", "circuit", "topo"];
|
|
4501
4631
|
var mintUuid2 = () => globalThis.crypto && typeof globalThis.crypto.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
4502
4632
|
var uid = (p) => `${p}-${mintUuid2()}`;
|
|
4633
|
+
var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
4503
4634
|
var ANN_SCHEMA = "opentakeoff.takeoff_canvas.v1";
|
|
4504
4635
|
var sanitizeApprovals2 = sanitizeApprovals;
|
|
4505
4636
|
var applyApprovalCommand2 = applyApprovalCommand;
|
|
@@ -6249,7 +6380,19 @@ var Session = class _Session {
|
|
|
6249
6380
|
if (!Object.keys(patch[i].fields).length) throw new UserError(`patch[${i}]: fields must be non-empty.`);
|
|
6250
6381
|
}
|
|
6251
6382
|
const c = this.conditionFor(tag);
|
|
6252
|
-
const
|
|
6383
|
+
const cid = c.id;
|
|
6384
|
+
const snap = [cid, ...this.descendantConditionIds(cid)].map((id) => {
|
|
6385
|
+
const x = this.conditions.find((q) => q.id === id);
|
|
6386
|
+
return {
|
|
6387
|
+
condition_id: id,
|
|
6388
|
+
before: structuredClone(x.materials),
|
|
6389
|
+
...x.materials_dropped ? { dropped_before: [...x.materials_dropped] } : {}
|
|
6390
|
+
};
|
|
6391
|
+
});
|
|
6392
|
+
let conds = this.conditions;
|
|
6393
|
+
const isParent = () => conds.some((x) => x.variant_of === cid);
|
|
6394
|
+
const target = () => conds.find((x) => x.id === cid);
|
|
6395
|
+
const mint = (p) => uid(p);
|
|
6253
6396
|
const added = [];
|
|
6254
6397
|
for (const a of add) {
|
|
6255
6398
|
const row = {
|
|
@@ -6261,23 +6404,57 @@ var Session = class _Session {
|
|
|
6261
6404
|
round: a.round ?? true,
|
|
6262
6405
|
...a.note ? { note: a.note } : {}
|
|
6263
6406
|
};
|
|
6264
|
-
|
|
6407
|
+
conds = conds.map((x) => x.id !== cid ? x : { ...x, materials: [...x.materials || [], row] });
|
|
6265
6408
|
added.push(row.id);
|
|
6409
|
+
if (isParent()) conds = propagateRowAdd(conds, cid, row, mint);
|
|
6266
6410
|
}
|
|
6267
6411
|
const removed = new Set(remove);
|
|
6268
|
-
|
|
6412
|
+
for (const id of remove) {
|
|
6413
|
+
if (target().variant_of) {
|
|
6414
|
+
conds = conds.map((x) => x.id !== cid ? x : dropRowLocal(x, id));
|
|
6415
|
+
} else {
|
|
6416
|
+
conds = conds.map((x) => x.id !== cid ? x : { ...x, materials: (x.materials || []).filter((r) => r.id !== id) });
|
|
6417
|
+
conds = propagateRowRemove(conds, cid, id);
|
|
6418
|
+
}
|
|
6419
|
+
}
|
|
6269
6420
|
const patched = [];
|
|
6270
6421
|
for (const p of patch) {
|
|
6271
|
-
|
|
6272
|
-
|
|
6422
|
+
conds = conds.map((x) => x.id !== cid ? x : {
|
|
6423
|
+
...x,
|
|
6424
|
+
materials: (x.materials || []).map((r) => {
|
|
6425
|
+
if (r.id !== p.id) return r;
|
|
6426
|
+
const merged = { ...r, ...p.fields, id: r.id };
|
|
6427
|
+
if (r.origin_id === void 0) delete merged.origin_id;
|
|
6428
|
+
else merged.origin_id = r.origin_id;
|
|
6429
|
+
if (r.inherited === void 0) delete merged.inherited;
|
|
6430
|
+
else merged.inherited = r.inherited;
|
|
6431
|
+
return merged;
|
|
6432
|
+
})
|
|
6433
|
+
});
|
|
6273
6434
|
patched.push(p.id);
|
|
6274
|
-
|
|
6275
|
-
|
|
6435
|
+
const cur = target();
|
|
6436
|
+
if (cur.variant_of) {
|
|
6437
|
+
conds = conds.map((x) => x.id !== cid ? x : markRowLocal(x, p.id));
|
|
6438
|
+
} else if (isParent()) {
|
|
6439
|
+
const row = (cur.materials || []).find((r) => r.id === p.id);
|
|
6440
|
+
if (row) conds = propagateRowPatch(conds, cid, p.id, row);
|
|
6441
|
+
}
|
|
6442
|
+
}
|
|
6443
|
+
this.conditions = conds;
|
|
6444
|
+
const [primary, ...familySnap] = snap;
|
|
6445
|
+
this.record({
|
|
6446
|
+
op: "materials",
|
|
6447
|
+
tool: "edit_materials",
|
|
6448
|
+
condition_id: cid,
|
|
6449
|
+
before: primary.before,
|
|
6450
|
+
...primary.dropped_before ? { dropped_before: primary.dropped_before } : {},
|
|
6451
|
+
...familySnap.length ? { family: familySnap } : {}
|
|
6452
|
+
});
|
|
6276
6453
|
return {
|
|
6277
6454
|
condition: tag,
|
|
6278
|
-
condition_id:
|
|
6455
|
+
condition_id: cid,
|
|
6279
6456
|
changed: { added, removed: [...removed], patched },
|
|
6280
|
-
materials:
|
|
6457
|
+
materials: target().materials
|
|
6281
6458
|
};
|
|
6282
6459
|
}
|
|
6283
6460
|
/** Set a condition's quantity knobs — waste % and multiplier. Both are
|
|
@@ -6347,6 +6524,117 @@ var Session = class _Session {
|
|
|
6347
6524
|
...roll ? { roll } : {}
|
|
6348
6525
|
};
|
|
6349
6526
|
}
|
|
6527
|
+
/**
|
|
6528
|
+
* Twin a condition — the same finish measured somewhere else, with its own materials.
|
|
6529
|
+
*
|
|
6530
|
+
* The same sheet goods over a slab and over a raised deck take the same field material and
|
|
6531
|
+
* different preparation underneath. The twin carries the original's whole materials list and
|
|
6532
|
+
* keeps FOLLOWING it: change a coverage rate on the original and every twin that hasn't
|
|
6533
|
+
* touched that row gets it; edit a row on the twin and only that row goes local. The rule
|
|
6534
|
+
* lives in web/src/lib/variants.ts — one copy, shared with the canvas, so a headless session
|
|
6535
|
+
* and the app can never disagree about what a twin holds.
|
|
6536
|
+
*
|
|
6537
|
+
* A twin needs its OWN tag, and that is not cosmetic here: every tool in this server resolves
|
|
6538
|
+
* a condition by finish tag and takes the FIRST match, so two conditions sharing one would
|
|
6539
|
+
* make the second permanently unreachable — and a takeoff re-import collapses them last-wins.
|
|
6540
|
+
* So the label is required and a collision is refused rather than de-collided.
|
|
6541
|
+
*/
|
|
6542
|
+
/** Every condition below `rootId` in the family tree, children first. The
|
|
6543
|
+
* propagate functions walk this same tree; undo snapshots ride it so a
|
|
6544
|
+
* family edit's inverse restores exactly the set the write could touch.
|
|
6545
|
+
* The `seen` guard mirrors variants.ts — a hand-edited payload with a
|
|
6546
|
+
* cycle must not hang the session. */
|
|
6547
|
+
descendantConditionIds(rootId) {
|
|
6548
|
+
const out = [];
|
|
6549
|
+
const seen = /* @__PURE__ */ new Set([rootId]);
|
|
6550
|
+
const walk = (pid) => {
|
|
6551
|
+
for (const child of this.conditions) {
|
|
6552
|
+
if (child.variant_of === pid && !seen.has(child.id)) {
|
|
6553
|
+
seen.add(child.id);
|
|
6554
|
+
out.push(child.id);
|
|
6555
|
+
walk(child.id);
|
|
6556
|
+
}
|
|
6557
|
+
}
|
|
6558
|
+
};
|
|
6559
|
+
walk(rootId);
|
|
6560
|
+
return out;
|
|
6561
|
+
}
|
|
6562
|
+
duplicateCondition(tag, label) {
|
|
6563
|
+
const src = this.conditions.find((x) => x.finish_tag === tag);
|
|
6564
|
+
if (!src) {
|
|
6565
|
+
const known = this.conditions.map((x) => x.finish_tag);
|
|
6566
|
+
throw new UserError(`No condition ${JSON.stringify(tag)} to duplicate.${known.length ? ` Known tags: ${known.join(", ")}.` : ""}`);
|
|
6567
|
+
}
|
|
6568
|
+
const lab = String(label || "").trim();
|
|
6569
|
+
if (!lab) throw new UserError("label is required \u2014 it is what gives the twin its own finish tag, and a tag is how every tool here resolves a condition.");
|
|
6570
|
+
const newTag = variantTag(src.finish_tag, lab);
|
|
6571
|
+
if (this.conditions.some((x) => x.finish_tag.trim().toUpperCase() === newTag.trim().toUpperCase())) {
|
|
6572
|
+
throw new UserError(`A condition is already called ${JSON.stringify(newTag)} \u2014 pick a different label. Two conditions sharing a tag would make one of them unreachable to every tool.`);
|
|
6573
|
+
}
|
|
6574
|
+
const { twin, parentPatch } = mintTwin(src, {
|
|
6575
|
+
label: lab,
|
|
6576
|
+
tag: newTag,
|
|
6577
|
+
mintId: (p) => uid(p),
|
|
6578
|
+
nowIso: nowIso2,
|
|
6579
|
+
nextHatch: HATCH_IDS[1 + (this.conditions.length + 1) % (HATCH_IDS.length - 1)]
|
|
6580
|
+
});
|
|
6581
|
+
if (parentPatch) Object.assign(src, parentPatch);
|
|
6582
|
+
this.conditions.push(twin);
|
|
6583
|
+
this.record({
|
|
6584
|
+
op: "duplicate_condition",
|
|
6585
|
+
tool: "duplicate_condition",
|
|
6586
|
+
condition_id: twin.id,
|
|
6587
|
+
parent_id: src.id,
|
|
6588
|
+
parent_had_family: !parentPatch
|
|
6589
|
+
});
|
|
6590
|
+
return {
|
|
6591
|
+
condition: newTag,
|
|
6592
|
+
condition_id: twin.id,
|
|
6593
|
+
variant_of: src.id,
|
|
6594
|
+
variant_label: lab,
|
|
6595
|
+
family_id: twin.family_id,
|
|
6596
|
+
inherited_rows: (twin.materials || []).length,
|
|
6597
|
+
note: `Materials follow ${src.finish_tag} until you edit them on this condition. No takeoffs came along \u2014 measure into ${newTag}.`
|
|
6598
|
+
};
|
|
6599
|
+
}
|
|
6600
|
+
/** Cut a twin loose: every following row freezes where it stands. It KEEPS its family_id, so
|
|
6601
|
+
* it still groups with its siblings — only the inheritance ends. */
|
|
6602
|
+
splitCondition(tag) {
|
|
6603
|
+
const c = this.conditions.find((x) => x.finish_tag === tag);
|
|
6604
|
+
if (!c) {
|
|
6605
|
+
const known = this.conditions.map((x) => x.finish_tag);
|
|
6606
|
+
throw new UserError(`No condition ${JSON.stringify(tag)}.${known.length ? ` Known tags: ${known.join(", ")}.` : ""}`);
|
|
6607
|
+
}
|
|
6608
|
+
const cond = c;
|
|
6609
|
+
if (!cond.variant_of) {
|
|
6610
|
+
return {
|
|
6611
|
+
condition: tag,
|
|
6612
|
+
condition_id: c.id,
|
|
6613
|
+
split: false,
|
|
6614
|
+
frozen_rows: 0,
|
|
6615
|
+
note: "Already owns its materials \u2014 nothing was following."
|
|
6616
|
+
};
|
|
6617
|
+
}
|
|
6618
|
+
const frozen = (cond.materials || []).filter((r) => r.inherited).length;
|
|
6619
|
+
const before = structuredClone({
|
|
6620
|
+
variant_of: cond.variant_of,
|
|
6621
|
+
materials: cond.materials,
|
|
6622
|
+
materials_dropped: cond.materials_dropped
|
|
6623
|
+
});
|
|
6624
|
+
const [next] = splitFromFamily([cond], cond.id);
|
|
6625
|
+
Object.assign(c, next);
|
|
6626
|
+
delete c.variant_of;
|
|
6627
|
+
delete c.materials_dropped;
|
|
6628
|
+
this.record({ op: "split_condition", tool: "split_condition", condition_id: c.id, before });
|
|
6629
|
+
return {
|
|
6630
|
+
condition: tag,
|
|
6631
|
+
condition_id: c.id,
|
|
6632
|
+
split: true,
|
|
6633
|
+
frozen_rows: frozen,
|
|
6634
|
+
family_id: cond.family_id,
|
|
6635
|
+
note: "Frozen at its current values; edits to the original no longer reach it. It still groups with its family."
|
|
6636
|
+
};
|
|
6637
|
+
}
|
|
6350
6638
|
/** Step back over this session's own last n mutations, newest first. Each
|
|
6351
6639
|
* entry's inverse is exact (see JournalEntry), so this restores state rather
|
|
6352
6640
|
* than approximating it. Reads are not journaled, so undo never has to step
|
|
@@ -6365,8 +6653,15 @@ var Session = class _Session {
|
|
|
6365
6653
|
if (i >= 0) this.shapes[i] = e.before;
|
|
6366
6654
|
undone.push({ seq: e.seq, op: e.op, tool: e.tool, shapes: i >= 0 ? 1 : 0 });
|
|
6367
6655
|
} else if (e.op === "materials") {
|
|
6368
|
-
const
|
|
6369
|
-
|
|
6656
|
+
const put = (condition_id, before, dropped) => {
|
|
6657
|
+
const cc = this.conditions.find((x) => x.id === condition_id);
|
|
6658
|
+
if (!cc) return;
|
|
6659
|
+
cc.materials = before;
|
|
6660
|
+
if (dropped === void 0) delete cc.materials_dropped;
|
|
6661
|
+
else cc.materials_dropped = dropped;
|
|
6662
|
+
};
|
|
6663
|
+
put(e.condition_id, e.before, e.dropped_before);
|
|
6664
|
+
for (const f of e.family ?? []) put(f.condition_id, f.before, f.dropped_before);
|
|
6370
6665
|
undone.push({ seq: e.seq, op: e.op, tool: e.tool, shapes: 0 });
|
|
6371
6666
|
} else if (e.op === "condition") {
|
|
6372
6667
|
const c = this.conditions.find((x) => x.id === e.condition_id);
|
|
@@ -6379,6 +6674,24 @@ var Session = class _Session {
|
|
|
6379
6674
|
else c.roll_setup = e.before.roll_setup;
|
|
6380
6675
|
}
|
|
6381
6676
|
undone.push({ seq: e.seq, op: e.op, tool: e.tool, shapes: 0 });
|
|
6677
|
+
} else if (e.op === "duplicate_condition") {
|
|
6678
|
+
const at = this.conditions.findIndex((x) => x.id === e.condition_id);
|
|
6679
|
+
if (at >= 0) this.conditions.splice(at, 1);
|
|
6680
|
+
if (!e.parent_had_family) {
|
|
6681
|
+
const parent = this.conditions.find((x) => x.id === e.parent_id);
|
|
6682
|
+
if (parent) delete parent.family_id;
|
|
6683
|
+
}
|
|
6684
|
+
undone.push({ seq: e.seq, op: e.op, tool: e.tool, shapes: 0 });
|
|
6685
|
+
} else if (e.op === "split_condition") {
|
|
6686
|
+
const c = this.conditions.find((x) => x.id === e.condition_id);
|
|
6687
|
+
if (c) {
|
|
6688
|
+
if (e.before.variant_of === void 0) delete c.variant_of;
|
|
6689
|
+
else c.variant_of = e.before.variant_of;
|
|
6690
|
+
c.materials = structuredClone(e.before.materials);
|
|
6691
|
+
if (e.before.materials_dropped === void 0) delete c.materials_dropped;
|
|
6692
|
+
else c.materials_dropped = structuredClone(e.before.materials_dropped);
|
|
6693
|
+
}
|
|
6694
|
+
undone.push({ seq: e.seq, op: e.op, tool: e.tool, shapes: 0 });
|
|
6382
6695
|
} else if (e.op === "approval") {
|
|
6383
6696
|
this.approvals = applyApprovalCommand2(this.approvals, e.inverse).approvals;
|
|
6384
6697
|
undone.push({ seq: e.seq, op: e.op, tool: e.tool, shapes: 0 });
|
|
@@ -7201,7 +7514,7 @@ var undoLastOutput = {
|
|
|
7201
7514
|
undone: z.number().int().describe("Steps actually reversed"),
|
|
7202
7515
|
steps: z.array(z.object({
|
|
7203
7516
|
seq: z.number().int(),
|
|
7204
|
-
op: z.enum(["commit", "edit", "delete", "materials", "condition", "approval"]),
|
|
7517
|
+
op: z.enum(["commit", "edit", "delete", "materials", "condition", "approval", "duplicate_condition", "split_condition"]),
|
|
7205
7518
|
tool: z.string().describe("The tool call this step came from"),
|
|
7206
7519
|
shapes: z.number().int().describe("Shapes affected by reversing this step \u2014 0 for a materials step (it restores a condition's supporting-materials rows, not shapes), for a condition step (it restores the waste/multiplier pair), and for an approval step (it re-seats or removes a verdict mark)")
|
|
7207
7520
|
})).describe("Newest first"),
|
|
@@ -7227,7 +7540,9 @@ var materialRow = z.object({
|
|
|
7227
7540
|
basis: z.enum(["area", "linear", "count"]).describe("Which of the condition's totals this row's quantity is computed against"),
|
|
7228
7541
|
unit: z.string(),
|
|
7229
7542
|
round: z.boolean().describe("true = round up to whole purchase units (the default \u2014 you buy whole bags/buckets)"),
|
|
7230
|
-
note: z.string().optional()
|
|
7543
|
+
note: z.string().optional(),
|
|
7544
|
+
origin_id: z.string().optional().describe("On a twin: the parent row this one follows (the variants.ts family link)"),
|
|
7545
|
+
inherited: z.boolean().optional().describe("On a twin: true while the row still follows the family \u2014 a patch on it takes it local, split_condition freezes them all")
|
|
7231
7546
|
});
|
|
7232
7547
|
var editMaterialsOutput = {
|
|
7233
7548
|
condition: z.string().describe("The finish tag passed in"),
|
|
@@ -7282,6 +7597,23 @@ var exportMarkedPdfOutput = {
|
|
|
7282
7597
|
approvals_drawn: z.number().int().describe("Approval-family glyphs burned in (#176) \u2014 estimator APPROVED rings + agent AGENT diamonds; the cover tallies the split when any exist"),
|
|
7283
7598
|
note: z.string()
|
|
7284
7599
|
};
|
|
7600
|
+
var duplicateConditionOutput = {
|
|
7601
|
+
condition: z.string().describe("The twin's finish tag \u2014 base tag + the label, e.g. 'CPT-1 \u2013 Level 2'"),
|
|
7602
|
+
condition_id: z.string().describe("The TWIN \u2014 measure the new area against this"),
|
|
7603
|
+
variant_of: z.string().describe("The condition whose material rows this one follows"),
|
|
7604
|
+
variant_label: z.string(),
|
|
7605
|
+
family_id: z.string().describe("Shared by every variant of this finish \u2014 survives a split"),
|
|
7606
|
+
inherited_rows: z.number().int().describe("Material rows copied, all still following the original"),
|
|
7607
|
+
note: z.string()
|
|
7608
|
+
};
|
|
7609
|
+
var splitConditionOutput = {
|
|
7610
|
+
condition: z.string(),
|
|
7611
|
+
condition_id: z.string(),
|
|
7612
|
+
split: z.boolean().describe("false = it already owned its materials; nothing was following"),
|
|
7613
|
+
frozen_rows: z.number().int().describe("Following rows frozen at their current values"),
|
|
7614
|
+
family_id: z.string().optional().describe("Kept \u2014 it still groups with its siblings"),
|
|
7615
|
+
note: z.string()
|
|
7616
|
+
};
|
|
7285
7617
|
var editConditionOutput = {
|
|
7286
7618
|
condition: z.string().describe("The finish tag passed in"),
|
|
7287
7619
|
condition_id: z.string(),
|
|
@@ -8789,6 +9121,7 @@ function mergeTakeoffImport(current, imported, knownFiles = null) {
|
|
|
8789
9121
|
const byTag = new Map(conditions.map((c) => [tagKey(c.finish_tag), c.id]));
|
|
8790
9122
|
const condIds = new Set(conditions.map((c) => c.id));
|
|
8791
9123
|
const condMap = /* @__PURE__ */ new Map();
|
|
9124
|
+
const addedCondIds = /* @__PURE__ */ new Set();
|
|
8792
9125
|
let condMerged = 0, condAdded = 0;
|
|
8793
9126
|
for (const c of impConds) {
|
|
8794
9127
|
const hit = byTag.get(tagKey(c.finish_tag));
|
|
@@ -8803,8 +9136,23 @@ function mergeTakeoffImport(current, imported, knownFiles = null) {
|
|
|
8803
9136
|
condIds.add(id);
|
|
8804
9137
|
byTag.set(tagKey(c.finish_tag), id);
|
|
8805
9138
|
condMap.set(c.id, id);
|
|
9139
|
+
addedCondIds.add(id);
|
|
8806
9140
|
condAdded++;
|
|
8807
9141
|
}
|
|
9142
|
+
for (let i = 0; i < conditions.length; i++) {
|
|
9143
|
+
const c = conditions[i];
|
|
9144
|
+
if (!c?.variant_of || !addedCondIds.has(c.id)) continue;
|
|
9145
|
+
const mapped = condMap.get(c.variant_of);
|
|
9146
|
+
const parentArrived = !!mapped && addedCondIds.has(mapped);
|
|
9147
|
+
if (parentArrived && mapped !== c.variant_of) {
|
|
9148
|
+
conditions[i] = { ...c, variant_of: mapped };
|
|
9149
|
+
} else if (!parentArrived) {
|
|
9150
|
+
const next = { ...c, materials: (c.materials || []).map(({ origin_id: _o, inherited: _i, ...m }) => m) };
|
|
9151
|
+
delete next.variant_of;
|
|
9152
|
+
delete next.materials_dropped;
|
|
9153
|
+
conditions[i] = next;
|
|
9154
|
+
}
|
|
9155
|
+
}
|
|
8808
9156
|
const shapeIds = new Set(arr(cur.shapes).map((s) => s.id));
|
|
8809
9157
|
const addedShapes = impShapes.filter((s) => !shapeIds.has(s.id)).map((s) => condMap.has(s.condition_id) && condMap.get(s.condition_id) !== s.condition_id ? { ...s, condition_id: condMap.get(s.condition_id) } : s);
|
|
8810
9158
|
const markupIds = new Set(arr(cur.markups).map((m) => m?.id).filter(Boolean));
|
|
@@ -9195,6 +9543,21 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
9195
9543
|
},
|
|
9196
9544
|
outputSchema: editConditionOutput
|
|
9197
9545
|
}, run("edit_condition", (a) => session.editCondition(a.condition, { waste_pct: a.waste_pct, multiplier: a.multiplier, height_ft: a.height_ft, roll_setup: a.roll_setup })));
|
|
9546
|
+
server.registerTool("duplicate_condition", {
|
|
9547
|
+
description: `Twin a condition \u2014 the same finish measured somewhere else, with its own supporting materials. One finish in two areas is not two conditions and it is not one either: the same sheet goods over a slab and over a raised deck take the same field material and different preparation underneath (one wants a moisture barrier, the other a primer and a different adhesive). The twin arrives carrying the original's whole materials list and keeps FOLLOWING it \u2014 change a coverage rate on the original and every twin that has not touched that row gets it; edit a row on the twin and only THAT row stops following. \`label\` is REQUIRED and becomes the tag suffix ('CPT-1' + 'Level 2' \u2192 'CPT-1 \u2013 Level 2'), because every tool in this server resolves a condition by finish tag and takes the FIRST match: two conditions sharing a tag would make one permanently unreachable, and a takeoff re-import collapses them last-wins. A label already in use is refused rather than de-collided. No takeoffs come along \u2014 measure the new area against the returned condition_id. Reversible with undo_last; use split_condition to end the inheritance permanently.`,
|
|
9548
|
+
inputSchema: {
|
|
9549
|
+
condition: z2.string().describe("Finish tag of the condition to twin, e.g. 'CPT-1'"),
|
|
9550
|
+
label: z2.string().describe("What makes this one different, usually the area: 'Level 2', 'Building B', 'Phase 2'")
|
|
9551
|
+
},
|
|
9552
|
+
outputSchema: duplicateConditionOutput
|
|
9553
|
+
}, run("duplicate_condition", (a) => session.duplicateCondition(a.condition, a.label)));
|
|
9554
|
+
server.registerTool("split_condition", {
|
|
9555
|
+
description: `Cut a twin loose from its family: every following material row freezes at its current values and edits to the original stop reaching it. It keeps its finish tag and still groups with its siblings \u2014 only the inheritance ends. Use when two variants have diverged far enough that following one another is wrong. A condition that already owns its materials returns split:false rather than erroring. Reversible with undo_last.`,
|
|
9556
|
+
inputSchema: {
|
|
9557
|
+
condition: z2.string().describe("Finish tag of the twin to split, e.g. 'CPT-1 \u2013 Level 2'")
|
|
9558
|
+
},
|
|
9559
|
+
outputSchema: splitConditionOutput
|
|
9560
|
+
}, run("split_condition", (a) => session.splitCondition(a.condition)));
|
|
9198
9561
|
server.registerTool("undo_last", {
|
|
9199
9562
|
description: `Step back over your OWN last n mutations, newest first \u2014 a committed one_click, a whole detect_rooms sweep, an edit_shape, a delete_shape, an edit_materials call, or an edit_condition call. Each step is reversed exactly (a commit is removed, an edit is restored verbatim, a delete is re-inserted where it was, a materials edit's whole array is restored, a condition edit's waste/multiplier pair is restored), so this restores state rather than approximating it. Reads are never journaled, so n counts gestures that changed something, not tool calls you made. Use it when a sweep committed against the wrong condition or a batch went in on the wrong sheet \u2014 one call instead of N deletes. Scope: this session's own history only. It is not the browser canvas's undo stack, and load_plan clears it along with the shapes it refers to.`,
|
|
9200
9563
|
inputSchema: {
|
|
@@ -9396,7 +9759,7 @@ function registerResources(server, session) {
|
|
|
9396
9759
|
// package.json
|
|
9397
9760
|
var package_default = {
|
|
9398
9761
|
name: "opentakeoff-mcp",
|
|
9399
|
-
version: "0.9.
|
|
9762
|
+
version: "0.9.32",
|
|
9400
9763
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
9401
9764
|
type: "module",
|
|
9402
9765
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -9412,7 +9775,7 @@ var package_default = {
|
|
|
9412
9775
|
mcpb: "npm run build && node scripts/build-mcpb.mjs",
|
|
9413
9776
|
prepublishOnly: "npm run typecheck && npm test && npm run build",
|
|
9414
9777
|
typecheck: "tsc --noEmit",
|
|
9415
|
-
test: "node --import tsx --test test/conformance.test.ts test/context.test.ts test/e2e.test.ts test/parity.test.ts test/raster.test.ts test/resources.test.ts test/scalewarn.test.ts test/session.test.ts test/tools.test.ts test/transitions.test.ts test/view.test.ts"
|
|
9778
|
+
test: "node --import tsx --test test/conformance.test.ts test/context.test.ts test/e2e.test.ts test/parity.test.ts test/raster.test.ts test/resources.test.ts test/scalewarn.test.ts test/session.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts"
|
|
9416
9779
|
},
|
|
9417
9780
|
dependencies: {
|
|
9418
9781
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentakeoff-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.32",
|
|
4
4
|
"mcpName": "io.github.Kentucky-ai/opentakeoff",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenTakeoff MCP server — drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"mcpb": "npm run build && node scripts/build-mcpb.mjs",
|
|
17
17
|
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
18
18
|
"typecheck": "tsc --noEmit",
|
|
19
|
-
"test": "node --import tsx --test test/conformance.test.ts test/context.test.ts test/e2e.test.ts test/parity.test.ts test/raster.test.ts test/resources.test.ts test/scalewarn.test.ts test/session.test.ts test/tools.test.ts test/transitions.test.ts test/view.test.ts"
|
|
19
|
+
"test": "node --import tsx --test test/conformance.test.ts test/context.test.ts test/e2e.test.ts test/parity.test.ts test/raster.test.ts test/resources.test.ts test/scalewarn.test.ts test/session.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.12.0",
|