opentakeoff-mcp 0.9.41 → 0.9.43
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 +19 -8
- package/package.json +1 -1
package/dist/server-core.js
CHANGED
|
@@ -3370,7 +3370,7 @@ function buildRasterMask(rgba, mw, mh, ws = 1, opts = {}) {
|
|
|
3370
3370
|
var SWEEP_TOL_PX = 2;
|
|
3371
3371
|
var SWEEP_SCORE_HIGH = 0.92;
|
|
3372
3372
|
var SWEEP_SCORE_LOW = 0.75;
|
|
3373
|
-
var
|
|
3373
|
+
var SWEEP_CANDIDATE_CEILING = 25e4;
|
|
3374
3374
|
var ANCHOR_COUNT = 3;
|
|
3375
3375
|
var MIN_SEG_LEN = 0.5;
|
|
3376
3376
|
var MAX_SEED_SEGS = 2e3;
|
|
@@ -3510,7 +3510,7 @@ function matchSymbol(fp, segs, opts = {}) {
|
|
|
3510
3510
|
const tol = (opts.tolPx ?? SWEEP_TOL_PX) * Math.max(1, scale);
|
|
3511
3511
|
const scoreHigh = opts.scoreHigh ?? SWEEP_SCORE_HIGH;
|
|
3512
3512
|
const scoreLow = opts.scoreLow ?? SWEEP_SCORE_LOW;
|
|
3513
|
-
const maxCandidates = opts.maxCandidates ??
|
|
3513
|
+
const maxCandidates = opts.maxCandidates ?? SWEEP_CANDIDATE_CEILING;
|
|
3514
3514
|
const xforms = transformsFor(opts.rotations ?? true, opts.mirror ?? true);
|
|
3515
3515
|
const n = segs.length >> 2;
|
|
3516
3516
|
if (scale !== 1 && opts.excludeCenter) {
|
|
@@ -3642,6 +3642,7 @@ function matchSymbol(fp, segs, opts = {}) {
|
|
|
3642
3642
|
matches,
|
|
3643
3643
|
withheld,
|
|
3644
3644
|
candidates: { considered, dropped },
|
|
3645
|
+
complete: dropped === 0,
|
|
3645
3646
|
...scale === 1 ? {} : {
|
|
3646
3647
|
scaled: {
|
|
3647
3648
|
ratio: Math.round(scale * 1e6) / 1e6,
|
|
@@ -6282,6 +6283,7 @@ var Session = class _Session {
|
|
|
6282
6283
|
withheld: res.withheld.map((w) => ({ at: [round1(w.at[0]), round1(w.at[1])], score: w.score, rotation: w.rotation, mirrored: w.mirrored, reason: w.reason })),
|
|
6283
6284
|
seed: seedOut,
|
|
6284
6285
|
candidates: res.candidates,
|
|
6286
|
+
complete: res.complete,
|
|
6285
6287
|
...committed2 ? {
|
|
6286
6288
|
committed: committed2.committed,
|
|
6287
6289
|
shape_ids: committed2.shape_ids,
|
|
@@ -6289,7 +6291,7 @@ var Session = class _Session {
|
|
|
6289
6291
|
ea_total: committed2.ea_total
|
|
6290
6292
|
} : {},
|
|
6291
6293
|
...opts.commit && !res.matches.length ? { note: "commit requested but nothing cleared the bar \u2014 no shapes were committed." } : {},
|
|
6292
|
-
...res.candidates.dropped > 0 ? { warning: `Work
|
|
6294
|
+
...res.candidates.dropped > 0 ? { warning: `Work ceiling: ${res.candidates.dropped} candidate placement(s) were never scored \u2014 this count is a FLOOR, not a total. The seed's linework is too common on this sheet for an exhaustive sweep; tighten the seed rect around more distinctive geometry, or sweep a region at a time and reconcile the counts.` } : {}
|
|
6293
6295
|
};
|
|
6294
6296
|
}
|
|
6295
6297
|
const graph = await this.ensureGraph();
|
|
@@ -6381,14 +6383,16 @@ var Session = class _Session {
|
|
|
6381
6383
|
matches: p.matches.map((m) => ({ at: [round1(m.at[0]), round1(m.at[1])], score: m.score, rotation: m.rotation, mirrored: m.mirrored })),
|
|
6382
6384
|
withheld: p.withheld.map((w) => ({ at: [round1(w.at[0]), round1(w.at[1])], score: w.score, rotation: w.rotation, mirrored: w.mirrored, reason: w.reason })),
|
|
6383
6385
|
candidates: p.candidates,
|
|
6386
|
+
complete: p.complete,
|
|
6384
6387
|
elapsed_ms: p.elapsed_ms,
|
|
6385
6388
|
...p.scaled ? { scaled: p.scaled } : {},
|
|
6386
6389
|
...p.scale.known ? {} : { scale_assumed: "no scale set on the seed sheet or this one \u2014 swept at 1:1" }
|
|
6387
6390
|
})),
|
|
6391
|
+
complete: perSheet.every((p) => p.complete),
|
|
6388
6392
|
skipped,
|
|
6389
6393
|
...committed ?? {},
|
|
6390
6394
|
...notes.length ? { note: notes.join(" ") } : {},
|
|
6391
|
-
...capped.length ? { warning: `Work
|
|
6395
|
+
...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.` } : {}
|
|
6392
6396
|
};
|
|
6393
6397
|
}
|
|
6394
6398
|
/** sweep_schedule_row (phase 2) — the estimator's story, honored: a
|
|
@@ -6567,7 +6571,7 @@ var Session = class _Session {
|
|
|
6567
6571
|
excluded.sort(byPos);
|
|
6568
6572
|
withheld.sort(byPos);
|
|
6569
6573
|
const text_only = occ.filter((o, k) => !matchedOcc.has(k) && !res.withheld.some((w) => Math.hypot(w.at[0] - o.cx, w.at[1] - o.cy) <= R)).map((o) => ({ at: [round1(o.cx), round1(o.cy)] }));
|
|
6570
|
-
perSheet.push({ state: sh, matches, withheld, excluded, text_only, candidates: res.candidates, elapsed_ms, scale: ratio, ...res.scaled ? { scaled: res.scaled } : {} });
|
|
6574
|
+
perSheet.push({ state: sh, matches, withheld, excluded, text_only, candidates: res.candidates, complete: res.complete, elapsed_ms, scale: ratio, ...res.scaled ? { scaled: res.scaled } : {} });
|
|
6571
6575
|
}
|
|
6572
6576
|
const found = perSheet.reduce((n, p) => n + p.matches.length, 0);
|
|
6573
6577
|
let committed;
|
|
@@ -6636,10 +6640,12 @@ var Session = class _Session {
|
|
|
6636
6640
|
excluded: p.excluded.map((e) => ({ at: [round1(e.at[0]), round1(e.at[1])], tag: e.tag })),
|
|
6637
6641
|
text_only: p.text_only,
|
|
6638
6642
|
candidates: p.candidates,
|
|
6643
|
+
complete: p.complete,
|
|
6639
6644
|
elapsed_ms: p.elapsed_ms,
|
|
6640
6645
|
...p.scaled ? { scaled: p.scaled } : {},
|
|
6641
6646
|
...p.scale.known ? {} : { scale_assumed: `no scale set on ${anchorSheet.key} or this sheet \u2014 swept at 1:1` }
|
|
6642
6647
|
})),
|
|
6648
|
+
complete: perSheet.every((p) => p.complete),
|
|
6643
6649
|
skipped,
|
|
6644
6650
|
...committed ?? {},
|
|
6645
6651
|
...notes.length ? { note: notes.join(" ") } : {},
|
|
@@ -7838,7 +7844,8 @@ var sweepSheetBlock = z.object({
|
|
|
7838
7844
|
found: z.number().int(),
|
|
7839
7845
|
matches: z.array(z.object(sweepPlacement)),
|
|
7840
7846
|
withheld: z.array(z.object({ ...sweepPlacement, reason: z.string() })),
|
|
7841
|
-
candidates: sweepCandidates.describe("The work
|
|
7847
|
+
candidates: sweepCandidates.describe("The work ceiling applies PER SHEET; dropped > 0 here names exactly where the count is incomplete"),
|
|
7848
|
+
complete: z.boolean().describe("True when every proposed placement on this sheet was scored \u2014 false means this sheet's count is a FLOOR, not a total (#261)"),
|
|
7842
7849
|
elapsed_ms: z.number().describe("Wall-clock for this sheet's sweep"),
|
|
7843
7850
|
scaled: sweepScaled.optional(),
|
|
7844
7851
|
scale_assumed: sweepScaleAssumed.optional()
|
|
@@ -7862,6 +7869,7 @@ var symbolSweepOutput = {
|
|
|
7862
7869
|
length_px: z.number().describe("Total seed linework length, image px")
|
|
7863
7870
|
}),
|
|
7864
7871
|
candidates: sweepCandidates.optional().describe("Sheet scope only \u2014 set scope accounts per sheet in sheets[]"),
|
|
7872
|
+
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)"),
|
|
7865
7873
|
sheets: z.array(sweepSheetBlock).optional().describe("Set scope only: one entry per swept PLAN-role sheet, load order"),
|
|
7866
7874
|
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"),
|
|
7867
7875
|
committed: z.number().int().optional().describe("commit mode: count shapes committed \u2014 one per match"),
|
|
@@ -8283,10 +8291,12 @@ var sweepScheduleRowOutput = {
|
|
|
8283
8291
|
excluded: z.array(z.object({ at: z.tuple([z.number(), z.number()]), tag: z.string() })).describe("Markers matching the geometry but labeled with a SIBLING row's tag \u2014 the bubble shape is shared across marks, so these belong to that row, not this one"),
|
|
8284
8292
|
text_only: z.array(z.object({ at: z.tuple([z.number(), z.number()]) })).describe("The tag drawn with NO matching marker geometry nearby \u2014 a note reference or a variant marker; a question, never a count"),
|
|
8285
8293
|
candidates: z.object({ considered: z.number().int(), dropped: z.number().int() }),
|
|
8294
|
+
complete: z.boolean().describe("True when every proposed placement on this sheet was scored \u2014 false means this sheet's count is a FLOOR, not a total (#261)"),
|
|
8286
8295
|
elapsed_ms: z.number().describe("Wall-clock for this sheet's sweep"),
|
|
8287
8296
|
scaled: sweepScaled.optional(),
|
|
8288
8297
|
scale_assumed: sweepScaleAssumed.optional()
|
|
8289
8298
|
})).describe("One entry per swept PLAN-role sheet, load order"),
|
|
8299
|
+
complete: z.boolean().describe("True when every proposed placement was scored on every swept sheet \u2014 false means at least one sheet's count is a FLOOR, not a total (#261)"),
|
|
8290
8300
|
skipped: z.array(z.object({ sheet: z.string(), role: z.string(), reason: z.string() })).describe("Sheets excluded from counting (schedule/detail/legend/unknown), each with its reason"),
|
|
8291
8301
|
committed: z.number().int().optional().describe("commit mode: count shapes committed \u2014 one per counted match, the whole sweep ONE undo step"),
|
|
8292
8302
|
shape_ids: z.array(z.string()).optional(),
|
|
@@ -10117,7 +10127,7 @@ function registerTools(realServer, session) {
|
|
|
10117
10127
|
outputSchema: placeCountOutput
|
|
10118
10128
|
}, run("place_count", (a) => session.placeCount(a.sheet, a.points, { condition: a.condition })));
|
|
10119
10129
|
server.registerTool("symbol_sweep", {
|
|
10120
|
-
description: `Find EVERY instance of a repeated plan symbol from ONE example \u2014 drains, thresholds, fixtures, transition markers: marquee a tight seed_rect around a single instance and the vector linework is searched for every other placement of that same segment cluster. Deterministic geometry, not vision: each placement scores as the length-weighted fraction of the seed's segments reproduced within tolerance_px, under translation plus 0/90/180/270 rotation and mirroring (symbols rotate on plans \u2014 both ON by default; turn them off to pin orientation). Score \u2265 0.92 is a match; the 0.75\u20130.92 band comes back in \`withheld\` with a reason \u2014 a near-match is a question you answer by LOOKING (view_sheet at its \`at\`), never a silent commit and never a silent drop. The seed's own location is reported in \`seed\` and never double-committed.
|
|
10130
|
+
description: `Find EVERY instance of a repeated plan symbol from ONE example \u2014 drains, thresholds, fixtures, transition markers: marquee a tight seed_rect around a single instance and the vector linework is searched for every other placement of that same segment cluster. Deterministic geometry, not vision: each placement scores as the length-weighted fraction of the seed's segments reproduced within tolerance_px, under translation plus 0/90/180/270 rotation and mirroring (symbols rotate on plans \u2014 both ON by default; turn them off to pin orientation). Score \u2265 0.92 is a match; the 0.75\u20130.92 band comes back in \`withheld\` with a reason \u2014 a near-match is a question you answer by LOOKING (view_sheet at its \`at\`), never a silent commit and never a silent drop. The seed's own location is reported in \`seed\` and never double-committed. Every proposed placement is scored up to a hard work ceiling sized for pathological sheets, and the reply says which it was: complete true means the count is a total; complete false (with candidates.dropped > 0) means the count is a FLOOR \u2014 some placements were never scored \u2014 so tighten the seed rect around more distinctive geometry rather than trusting it as a total. Marquee discipline: the rect must hug ONE instance \u2014 only segments FULLY inside it define the symbol, so a loose rect that swallows wall linework fingerprints the wall, not the symbol. scope "set" sweeps the WHOLE working set, counting on PLAN-role sheets only (the sheet graph decides): a symbol drawn in a detail, legend, or schedule is a reference drawing and never counts itself \u2014 which is also how you seed from one: marquee the assembly on the detail sheet and its plan-sheet occurrences are counted while the detail stays excluded (the exclusion disclosed in \`skipped\`, per-sheet results with per-sheet caps and wall-clock in \`sheets\`). Scale across sheets: the fingerprint is size-true and is never scale-SEARCHED, so a detail drawn at 1-1/2" = 1'-0" is 12\xD7 the size of the same mark on a 1/8" plan \u2014 when BOTH sheets have a scale set, the exact ratio is computed from them and the seed is resized before matching (reported per sheet as \`scaled\`); when a scale is missing, the sweep runs at 1:1 and SAYS so (\`scale_assumed\`), because an unknown ratio plus a zero count is not evidence of absence. Seeding from a detail/legend/schedule sheet REFUSES outright until both scales are set \u2014 that is the case where an unstated ratio silently finds nothing. commit: true (requires condition) commits every match center as an EA count marker through the same path as place_count \u2014 the whole sweep (set-wide included) is ONE undo step, each marker carries origin.method "symbol_sweep" with its score, transform, and seed source, and withheld placements are NEVER committed. The COUNT is scale-free (EA), but matching across sheets of different scales is not \u2014 set_scale on the sheets involved is what turns the ratio from an assumption into arithmetic. After any batch commit, LOOK at what landed \u2014 view_sheet {overlay: true} over the swept area \u2014 and audit the markers against the drawing before trusting the EA total. ${COORDS}`,
|
|
10121
10131
|
inputSchema: {
|
|
10122
10132
|
sheet: z2.string().describe("The sheet the seed rect sits on \u2014 in scope 'set' it may be ANY sheet (a detail/legend seed sheet is fingerprint source only, never counted)"),
|
|
10123
10133
|
seed_rect: z2.tuple([pointSchema, pointSchema]).describe("Marquee around ONE example instance, [[x0,y0],[x1,y1]] in image px \u2014 tight: segments fully inside define the symbol"),
|
|
@@ -10639,7 +10649,7 @@ function applyStagedTools(server, registered) {
|
|
|
10639
10649
|
// package.json
|
|
10640
10650
|
var package_default = {
|
|
10641
10651
|
name: "opentakeoff-mcp",
|
|
10642
|
-
version: "0.9.
|
|
10652
|
+
version: "0.9.43",
|
|
10643
10653
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
10644
10654
|
type: "module",
|
|
10645
10655
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -10725,6 +10735,7 @@ function buildServer(session = new Session(), opts = {}) {
|
|
|
10725
10735
|
"3. DERIVE what follows from the rooms instead of re-measuring it: derive_base for base LF (perimeter \u2212 the door openings YOU state), derive_transitions for the line where two finishes meet. Both read committed floor shapes, so they come after step 2 and their output is audited in step 4 like anything else.",
|
|
10726
10736
|
"4. LOOK at what landed with view_sheet overlay:true and fix misses with edit_shape before trusting totals \u2014 crop the work region tight (full-sheet renders downsample too far to audit a ring).",
|
|
10727
10737
|
"5. Finish by writing the marked-up planset with export_marked_pdf and give the user its file path, alongside export_report for the numbers. Never end a takeoff with numbers alone.",
|
|
10738
|
+
"A floor split across sheets at a MATCH LINE: there is no stitch verb, deliberately \u2014 joining and aligning a match line is human judgment in the canvas (a sloppy join silently skews every seam-crossing quantity). Measure each member sheet as its own surface and tell the user a seam-crossing room needs their stitch in the app; never approximate one by combining sheets yourself.",
|
|
10728
10739
|
"WITHHELD IS NOT A FAILURE \u2014 IT IS THE ANSWER. detect_rooms, symbol_sweep, sweep_schedule_row and derive_transitions all measure things they then decline to commit, and say why: a near-match in the score band, a room the schedule cannot answer for, adjacency across a WALL rather than a butt joint. Read those arrays, view_sheet the coordinates they hand you, and resolve them or report them. A withheld item you ignore is a hole in the bid; one you never mention is worse.",
|
|
10729
10740
|
...staged ? [STAGED_INSTRUCTIONS] : []
|
|
10730
10741
|
].join("\n")
|
package/package.json
CHANGED