Skip to content
verify mcp Beta VerifyMCP is currently in beta. If you notice any issues, email [email protected] and we’ll put it right.

OpenTakeoff

NPM · OPENTAKEOFF-MCP · SCANNED AUG 3

Construction takeoff for AI agents: load plans, set scale, measure, count, export with provenance.

Available components

−35 this week 38 Trust /100
Trust breakdown (6 categories)

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. How we score →

Supply Chain Security63
  • No malware found by supply-chain analysis.Pass
  • CVE data not yet available for this package.Unverified
  • No install/post-install scripts declared.Pass
  • Dependency-health data not yet available.Unverified
Provenance & Transparency97
  • Source repository is publicly reachable at the declared URL. View diagnostics → Pass
  • Cryptographically verified build provenance (signed, bound to Kentucky-ai/opentakeoff). View diagnostics → Pass
  • Clear OSI-approved license (Apache-2.0).Pass
  • Actively maintained (last published 0 days ago).Pass
  • Disclosure check failed: no security disclosure policy was found in the source repository. See how to fix → Fail
Schema Quality & AI Usability0
  • Schema quality not yet verified: we do not have a sandbox capture of the MCP schema this version of the package serves yet.Unverified
Stability & Change Management0
  • Stability not yet verified: we do not have a sandbox capture of the MCP schema this version of the package serves yet.Unverified
Tool Coverage0
  • Tool coverage not yet verified: we do not have a sandbox capture of the tool definitions this version of the package serves yet.Unverified
Capabilities0
  • Protocol version not yet verified: we do not have a sandbox capture of the MCP handshake this version of the package performs yet.Unverified

Unverified: 4 categories

Categories scored 0 because our sandbox run of this package has not given us the schema these checks need to read. That is a gap on our side rather than a finding about the package, and we only credit what we can confirm, so the score stands at 0 until the capture succeeds. We are working through the fleet, so this normally clears without any action from you. How we score packages →

Install

Add this component to your MCP client. Where a client-specific snippet is available, pick your client below and copy it straight into your config; otherwise use the connection detail shown.

npm · opentakeoff-mcp

# add to Claude Code
claude mcp add kentucky-ai-opentakeoff -- npx -y opentakeoff-mcp
# add to Codex CLI
codex mcp add kentucky-ai-opentakeoff -- npx -y opentakeoff-mcp
// opencode.json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "kentucky-ai-opentakeoff": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "opentakeoff-mcp"
      ],
      "enabled": true
    }
  }
}
# add to OpenClaw
openclaw mcp add kentucky-ai-opentakeoff --command npx --arg -y --arg opentakeoff-mcp
# ~/.hermes/config.yaml
mcp_servers:
  kentucky-ai-opentakeoff:
    command: "npx"
    args: ["-y", "opentakeoff-mcp"]
// mcp.json
{
  "mcpServers": {
    "kentucky-ai-opentakeoff": {
      "command": "npx",
      "args": [
        "-y",
        "opentakeoff-mcp"
      ]
    }
  }
}
Changelog

Every change we have recorded for this component, newest first. Security-relevant changes are always shown. ▲ marks a change for the better, ▼ a change for the worse; unmarked changes are neutral.

  • 3 Aug 26 −40
    • Known CVEs: partial → unverified security
    • Stability: 0.23 → unverified security
    • Dependency health: partial → unverified functional
    • Schema quality: 100 → unverified functional
    • Tool coverage: 100 → unverified functional
    • Capabilities: pass → unverified functional
    • Package version: 0.9.27 → 0.9.31 functional
    • Package version: 0.9.27 → 0.9.29 functional
  • 2 Aug 26 +55
    • Known CVEs: unverified → partial security
    • Provenance: unverified → pass security
    • Install scripts: unverified → pass security
    • Malware scan: unverified → pass security
    • The attested source repository moved: Kentucky-ai/opentakeoff security
    • Schema quality: 255 → 312 functional
    • Tool coverage: 100 → unverified functional
    • Schema quality: 100 → unverified functional
    • Stability: unverified → 0.23 functional
    • MCP protocol: unverified → pass functional
    • Maintenance: unverified → pass functional
    • Dependency health: unverified → partial functional
    • License: unverified → pass functional
    • Schema quality: unverified → excellent functional
    • Licence: Apache-2.0 functional
    • Package version: 0.9.19 → 0.9.27 functional
    • Package version: 0.9.19 → 0.9.21 functional
  • 1 Aug 26 −8
    • We updated how we score, so this day's move reflects our rubric, not a change to the server See what changed → functional
  • 31 Jul 26 +25
    • Schema quality: 4730 → 7793 functional
    • Schema quality: excellent → unverified functional
    • Tool coverage: 69% → 77% functional
    • Schema quality: unverified → 100 functional
    • Tool coverage: unverified → 100 functional
    • Package version: 0.9.0 → 0.9.17 functional
  • 30 Jul 26 0
    • Security disclosure: unverified → fail functional
    • Package version: 0.9.3 → 0.9.7 functional
  • 29 Jul 26 0
    • Security disclosure: fail → unverified functional
    • Package version: 0.9.2 → 0.9.3 functional
  • 28 Jul 26 −67
    • Known CVEs: partial → unverified security
    • Install scripts: pass → unverified security
    • Provenance: pass → unverified security
    • The attested source repository moved: Kentucky-ai/opentakeoff security
    • Maintenance: pass → unverified functional
    • Dependency health: partial → unverified functional
    • Schema quality: 100 → unverified functional
    • Tool coverage: 100 → unverified functional
    • License: pass → unverified functional
    • Licence: Apache-2.0 functional
    • Package version: 0.9.0 → 0.9.2 functional
  • 27 Jul 26 +61
    • We updated how we score, so this day's move reflects our rubric, not a change to the server See what changed → functional
Diagnostics

Diagnostic detail from the automated scan of this channel: what the scanner observed at each step, so you can see exactly where a check passed or failed. It is informational only and never changes the trust score.

Captured 3 Aug 2026 · Analysed npm/[email protected]

Provenance verified

Ecosystem: npm · Outcome: verified

Reason: verified

Source repo:
Kentucky-ai/opentakeoff
Certificate issuer:
https://token.actions.githubusercontent.com
Certificate SAN:
https://github.com/Kentucky-ai/opentakeoff/.github/workflows/publish-mcp.yml@refs/tags/mcp-v0.9.31
Rekor log index:
2332732956
Predicate type:
https://slsa.dev/provenance/v1
Subject digest:
sha512:4ec1065889d93603794e1254665cad271c153889d3db231b5e05f216ad69f1c08b9e2aff038692335a6f0ff1f668efe07739ae4b0d660c1189c60e555
Discovery method:
attestation_endpoint
MCP tools — 36 exposed · ~11,781 tokens

The tools this component advertises to a client, with an estimated token cost for each. Expand a tool to see its parameters and schema. The per-tool counts are indicative and are not scored directly; the schema's total context footprint is one signal in Schema Quality & AI Usability.

Tool Tokens
annotate ~666

Place an annotation on a sheet — a note ABOUT the work, never a measurement of it. Types: cloud and highlight take rect:[[x0,y0],[x1,y1]] (a revision cloud around an area, a highlight box over it), text takes at:[x,y], callout takes at:[x,y] plus target:[x,y] (the point its leader aims at), arrow takes from:[x,y] and to:[x,y] (tail and head — plank/seam direction, the markup flooring drawings use most; #150), bubble takes at:[x,y] plus optional r (a keynote/detail circle carrying centered text), dimension takes from:[x,y] and to:[x,y] (its two measured endpoints) and labels itself with the length between them at the sheet's scale — drawn as a dimension line with end ticks and the measurement centered. A dimension states a REAL length, so it is the one annotation the scale gate applies to: on an unscaled sheet it refuses exactly like the measure tools (set_scale first) rather than dressing a px figure up as feet. It still touches no quantity — a dimension is a note about a distance, not a takeoff line item. Pass condition to attach the note to a finish tag, which is what makes it part of that SCOPE rather than a floating remark: it then wears the condition's colour on the canvas and in the marked-set PDF, and travels with it into the report. The tag is minted on first touch like one_click/measure_polygon, so you can annotate CPT-1 before anything is traced for it. Omit condition for a note about the sheet itself. No review gate: the pencil-not-ink rule exists to stop an agent inventing geometry, and a cloud reading "verify substrate" is not geometry. It touches no quantity. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
atarrayAnchor point (image px) — text, callout, and bubble (the circle's center)
conditionstringFinish tag to attach this note to, e.g. 'CPT-1' (minted on first use). Omit for an unattached sheet note
fromArrow tail / dimension start (image px)
rnumberBubble radius (image px); omitted → the canvas default (2% of sheet width)
rectarrayCorners (image px) — cloud and highlight
sheetstringyesSheet name or number, as sheet_info reports it
targetWhat a callout's leader line points at (image px)
textstringThe note. A cloud with no text still reads as 'look here'; a bubble's text draws centered in the circle; a dimension appends it after the measured length
toArrow head / dimension end (image px)
typestringyescloud/highlight need rect; text/callout/bubble need at; callout also needs target; arrow and dimension need from + to
NameTypeReqDescription
conditionstringyes
condition_idstringyes
idstringyes
length_lfnumberDimension only: the measured length (real feet) the annotation will label itself with
notestringyes
sheetstringyes
textstringyes
typestringyes

No examples provided.

delete_shape ~72

Remove a committed shape by the id returned when it was committed. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
shape_idstringyes
NameTypeReqDescription
deletedstringyesThe removed shape's id
shape_countintegeryesCommitted shapes remaining

No examples provided.

delete_verdict ~96

Lift an agent verdict mark by id (mark_verdict's reply, or list_annotations verdicts[]). Agent marks only: the estimator's APPROVED seal is human ink and is refused — the same line edit_shape holds on reviewed shapes. Journaled like every mutation, so undo_last re-seats a lifted mark exactly where it was.

NameTypeReqDescription
verdict_idstringyesRecord id from mark_verdict or list_annotations verdicts[]
NameTypeReqDescription
deletedstringyesThe lifted record's id
verdicts_remainingintegeryesApproval-family records still on the takeoff (both actors)

No examples provided.

derive_base ~264

Mint the wall base from committed rooms (#148) — the estimator's most mechanical derivation: base LF = room perimeter − stated door openings. For every floor_area shape of source_condition, commits ONE linear shape under condition (e.g. 'RB-1') tracing that room's boundary, quantified NET of the openings you state per room. The openings are YOUR claim to make — look at the doors with view_sheet, state {shape_id, lf} per room (repeat a shape_id to stack openings); the tool never guesses, and your claim is recorded on origin.derived (from_shape_id, gross_lf, openings_lf). All-or-nothing: an unknown shape_id, a negative lf, or openings meeting a room's whole perimeter refuses the call before anything commits. The whole derivation is ONE undo step. Deriving onto the source condition is refused — base lands on its own tag.

NameTypeReqDescription
conditionstringyesFinish tag the base commits under (minted on first use), e.g. 'RB-1'
openingsarrayStated openings per room — omit for gross perimeters
source_conditionstringyesFinish tag whose floor_area rooms the base derives from, e.g. 'CPT-1'
NameTypeReqDescription
committedintegeryes
conditionstringyesThe tag the base committed under
notestringyes
roomsarrayyes
source_conditionstringyes
total_lfnumberyesSum of net_lf across rooms

No examples provided.

derive_transitions ~775

Mint the transition where two finishes MEET (#202) — the derivation that follows derive_base, and the line an estimator draws by hand on every job. Pass the two finish tags and the tag the transition commits under (e.g. condition_a 'CPT-1', condition_b 'PT-1', condition 'T-1'), and every committed room of each is compared against every committed room of the other. WHAT THE GEOMETRY ACTUALLY IS, because it decides what you get back: flood-traced rooms DO NOT SHARE EDGES. A trace fills to the wall linework, so two rooms across a partition are separated by four to eight inches of nothing — testing for a shared edge finds zero transitions on a real planset. What is there is proximity, in two flavours that mean completely different things: • BUTT JOINT — the two rings run together inside ONE open space (a lobby that changes from carpet to tile with no wall between). The transition IS that run, and it commits as a linear shape under your tag, origin.derived naming both parent shapes and the measured gap. • WALL-SEPARATED — the rings run parallel across a partition. The rooms are adjacent, but the transition is NOT the shared wall: it is a threshold, in the doorway, and NOTHING in the trace record says where the doorway is (the flood engine seals openings and reports how MUCH boundary it synthesised, never where). Committing 34 LF of threshold because two rooms share 34 LF of wall would be a wrong bid with a machine's confidence behind it. These come back in `withheld` — measured, with their length, their gap in inches, and an `at` point — as questions you answer by LOOKING (view_sheet at `at`, then measure_line or place_count the threshold yourself). The symbol_sweep doctrine: a near-match is never a silent commit and never a silent drop. Tuning: max_gap_in (default 12) is how far apart two rings can be and still count as adjacent at all — raise it for thick walls, and every extra inch turns more of the plan into wall_separated questions, never into committed LF. min…

NameTypeReqDescription
conditionstringyesFinish tag the transitions commit under (minted on first use), e.g. 'T-1'. Must differ from both sources
condition_astringyesFirst finish tag, e.g. 'CPT-1' — its committed rooms are walked, and runs are traced along their boundaries
condition_bstringyesSecond finish tag, e.g. 'PT-1'
max_gap_innumberHow far apart two rings can be and still count as adjacent, in inches (default 12 — a thick partition). Wider only produces more wall_separated QUESTIONS, never more committed LF
min_run_innumberShortest run worth reporting, in inches (default 12) — below this is a corner where two rooms clip, not a transition
NameTypeReqDescription
betweenarrayyesThe two finish tags
committedintegeryes
conditionstringyesThe tag the transitions committed under
notestringyes
runsarrayyes
total_lfnumberyesSum of committed run lengths — butt joints only
withheldarrayyesAdjacency across a wall: real, measured, and NOT committed — the transition there is a threshold at a doorway this cannot locate
withheld_lfnumberyesShared-wall length held back — never part of total_lf

No examples provided.

detect_rooms ~932

Batch room detection: reads every room-number label off the sheet's text layer (e.g. "134", "OFFICE 101") and runs One-Click at each — one call instead of read_sheet_text + reasoning + N one_click calls. An OCR'd scan (text layer, no vector linework) floods the rendered pixels instead (#154), disclosed per room and on origin as raster_traced. A seed is only reported as a room once it survives three gates, and everything skipped is counted and reasoned in `withheld` — never dropped silently, because a room the tool tells you it skipped is a question you can ask, while one it hides is a hole in a bid. The gates: a flood that leaked or landed in dense linework never becomes a region; two labels flooding the SAME region commit once (the extra labels ride on `merged_labels` — double-counting an area is the worst failure an estimating tool has); and a flood that is enclosed and clean but smaller than min_area_sf is a room-number bubble, a door swing, or a wall cavity rather than a room. Every room floods through the SAME sealed engine a single one_click runs (RFC #60 — feet-true gap sealing, door-swing wedges, the minimum-passage rule), so a batch detection and a click at the same seed measure the same square footage; each room carries the engine's account of its own trace (confidence + confidence_factors, gap_sealed_px, door_wedges, min_pass_px/min_pass_delta), and the same account rides origin on everything committed. Confidence is a review prioritizer, never a verification — a low-confidence room is a view_sheet {overlay: true} audit prompt, not a fact to bid from. With the sheet's scale set, returns area_sf/perimeter_lf per room. TO COMMIT, choose the honest source of the finish tag: assign_from_schedule: true routes every room through its OWN room-finish schedule row and commits each under the FLOOR finish that row states — when a schedule exists in the set, THIS is the default move, because one agent-chosen tag across N rooms flattens real finish variety into a wro…

NameTypeReqDescription
assign_from_schedulebooleanCommit each room under the FLOOR finish its OWN room-finish schedule row states (resolve_tag's chain, per room): the citation rides origin.assignment, and rooms the schedule cannot answer for — no ro…
conditionstringFinish tag to commit every detected room under (minted on first use). Mutually exclusive with assign_from_schedule
layersobjectOverride the sheet's classified layer roles for THIS call (see sheet_info.layers)
min_area_sfnumberPlausibility floor: enclosed non-bubble regions smaller than this are withheld as cavities, not rooms. Default 5 SF — below any real finished space (a broom closet is ~10 SF). Lower it to inspect wha…
return_vertsbooleanInclude each traced polygon's vertices (image px)
rolestring
sensitivitynumberFill sensitivity, the same knob the canvas has: 0 strict (hatch/light linework always blocks), 0.5 balanced (default), 1 aggressive (crosses more hatch, tolerates more growth). Raise it when a flood…
sheetstringyes
NameTypeReqDescription
detectedintegeryesCount of cleanly-detected rooms — may be fewer than the labels found on the sheet
multiple_scalesbooleanSeveral DISTINCT scale notes on this sheet (#153) — rooms inside an enlarged viewport may be figured at the wrong scale
notestringHuman-readable summary of what was withheld, when anything was
roomsarrayyes
unresolvedarrayAssign mode only, empty array included: [] is the positive claim that every detected room resolved against its own schedule row
warningstringPreview mode (no scale): why quantities are unavailable and what to do
withheldobjectyesWhat detection skipped and why — a withheld room is a question the caller can ask; a silently dropped one is a hole in a bid

No examples provided.

edit_condition ~433

Set a condition's quantity knobs — waste %, multiplier, height_ft (the H knob measure_surface quantifies against), and/or roll_setup (the roll-goods opt-in: seams and order footage figured from the committed rooms, #147). takeoff_summary emits waste-adjusted *_net order quantities and a per-condition multiplier, and every export carries both, but conditions minted through the measure tools start at waste 0 / multiplier 1 — without this tool an agent's takeoff always ships net === gross (#131). waste_pct is the estimator's cut-waste percentage (carpet commonly 5–10); multiplier scales every quantity on the condition (×N identical floors — takeoff_summary applies it before waste). condition must resolve to an EXISTING finish tag — a typo'd tag errors rather than minting an empty condition (the edit_materials remove/patch rule, not its add rule: these knobs mean nothing on a condition that doesn't exist yet). No review gate — quantity config, not traced geometry; undo_last reverses a call in one step (both knobs snapshotted together, restored verbatim).

NameTypeReqDescription
conditionstringyesFinish tag of an existing condition, e.g. 'CPT-1'
height_ftnumberWall height in feet — the canvas's H knob; measure_surface quantifies traced LF × this
multipliernumberQuantity multiplier (×N identical areas). Note: the canvas treats 0 as 1, so 0 is rejected here rather than silently meaning 'off'
roll_setupRoll-goods opt-in (#147): presence of a setup is what makes the condition roll goods — seams figured, cuts packed, order footage beside the measured quantities. Same-material partial edits patch the…
waste_pctnumberWaste percentage applied to net order quantities, e.g. 10 for 10%
NameTypeReqDescription
conditionstringyesThe finish tag passed in
condition_idstringyes
height_ftnumberThe condition's wall height after this write — present once set (measure_surface multiplies traced LF by it)
multipliernumberyesThe condition's quantity multiplier after this write
rollobjectThe figured order (same row export_report's roll_goods carries) — present when the roll-goods condition has floor shapes on scaled sheets
roll_setupobjectThe condition's roll-goods setup after this write — present while opted in
waste_pctnumberyesThe condition's waste % after this write

No examples provided.

edit_materials ~314

Add, remove, or patch supporting-materials rows on a condition — the coverage-rate lines that turn a measured area/length/count into an order quantity (adhesive at N sf/gal, grout at N lf/bag, …), matching the canvas's per-condition Supporting Materials panel. Each row is {name, per, basis, unit, round, note}: quantity = the condition's basis total (area/linear/count) ÷ per, rounded up to whole purchase units unless round:false. condition names an existing OR NEW finish tag (minted on first touch, same as one_click/measure_polygon) — add alone is enough to seed materials on a condition before you've traced anything. remove/patch target existing row ids from this reply or export_takeoff (takeoff_summary strips materials for a compact quantities-only reply); a bad id 404s the WHOLE call before anything is written, and referencing an id on a tag with no condition yet errors rather than silently minting an empty one. No review gate here — materials rows are quantity config, not traced geometry, so this edits directly; undo_last reverses a call in one step (the condition's whole materials array, snapshotted before the write, restored verbatim).

NameTypeReqDescription
addarrayNew rows to add
conditionstringyesFinish tag, e.g. 'CPT-1'
patcharrayField changes on existing rows
removearrayExisting row ids to remove
NameTypeReqDescription
changedobjectyes
conditionstringyesThe finish tag passed in
condition_idstringyes
materialsarrayyesThe condition's full materials array after this write

No examples provided.

edit_shape ~311

REVISE a shape you already committed, instead of deleting it and starting over: pass new verts to move the geometry, condition to reassign it to a different finish tag, role to switch between floor_area / deduct / linear, or any combination. Quantities are recomputed from the result — a role flip alone re-measures (closed area vs open length). The loop this is for: one_click or measure_polygon to commit, view_sheet with overlay:true to LOOK at what landed, then edit_shape to fix the two vertices that overshot into the corridor. Shapes a human affirmed (origin.reviewed) are ink and are refused — an agent revises its own pencil and nothing else. Agent self-revision is tallied on origin.agent_edits, kept deliberately separate from the human-correction fields. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
conditionstringReassign to this finish tag (minted on first use)
rolestringSwitch what the shape measures — flipping INTO surface_area needs a height on the shape or its condition
shape_idstringyesId returned when the shape was committed
vertsarrayReplacement geometry (image px): ≥3 vertices for an area shape, ≥2 points for a linear/surface run, ≥1 for a count marker
NameTypeReqDescription
agent_editsintegeryesHow many times the agent has revised this shape — separate from the human-correction tally
area_sfnumber0 for linear shapes; LF × height for surface_area; absent for count
changedarrayyesWhich fields this call actually changed
countnumbercount shapes only — the marker's EA (preserved across the edit)
measure_rolestringyes
nvertsintegeryes
perimeter_lfnumberLength for linear/surface runs, perimeter for closed ones; absent for count
shape_idstringyes

No examples provided.

export_marked_pdf ~309

The MARKED-UP PLANSET — the deliverable of every takeoff. Writes a distribution-ready PDF to disk: a legend cover (per-condition totals, swatches, a by-sheet breakdown) followed by every sheet that carries takeoff shapes or annotations, vector-copied from the source plan with the work burned in as drawn — condition colors and hatches, a quantity chip on every shape, annotation clouds/callouts/highlights, and approval marks (the estimator's APPROVED rings, the agent's AGENT diamonds — the cover tallies the split). Built by the same module as the canvas's MARKED SET button, so agent output and app output are one implementation. A construction takeoff is no good without markup: finish EVERY takeoff by writing this file and giving the user its path (export_report carries the numbers for pricing; this carries the evidence). When the shapes were machine-traced and unreviewed, the document says so on its last page — the review path is importing the export_takeoff payload into the app, where agent shapes arrive as pencil proposals. Default path: next to the loaded plan as "<plan> - marked set.pdf". Needs no native canvas — pure vector copy, so it works even where view_sheet cannot render.

NameTypeReqDescription
pathstringWhere to write the PDF (default: "<plan dir>/<plan> - marked set.pdf")
project_namestringCover-page project name (default: the plan file's name)
NameTypeReqDescription
annotations_drawnintegeryes
approvals_drawnintegeryesApproval-family glyphs burned in (#176) — estimator APPROVED rings + agent AGENT diamonds; the cover tallies the split when any exist
notestringyes
pagesintegeryesLegend cover + one page per marked sheet
pathstringyesAbsolute path of the written marked-set PDF — hand this to the user
shapes_drawnintegeryes
sheets_markedintegeryesSheets carrying shapes, annotations, or approval marks — unmarked sheets are omitted

No examples provided.

export_report ~246

The computed Report document — "opentakeoff.report.v1", the same schema the canvas Report's JSON export writes. Everything a pricing consumer needs without re-implementing the app's math: per-condition quantities with waste and multiplier applied (gross and *_net), the computed materials BUY LIST per condition (order quantity = basis ÷ coverage rate, rounded up to whole purchase units) plus the project-wide roll-up summed by (name, unit), per-sheet BASE subtotals, scale provenance per sheet, and annotations. Contrast: export_takeoff is the raw canvas payload (materials as CONFIG rows, no computed quantities) and takeoff_summary strips materials for a compact reply — when the numbers are leaving for pricing, consume this. A report alone is HALF the deliverable: pair it with export_marked_pdf, because a takeoff is reviewed on marked drawings, not on numbers. Returned inline; pass path to also write it to disk as JSON.

NameTypeReqDescription
pathstringFile path to write the document to
project_namestringLabel for the document's project_name field (a headless session has no project of its own; omitted → null)
NameTypeReqDescription
by_labelarrayyes
by_sheetarrayyesBASE per-sheet subtotals — multiplier NOT applied, no waste, no materials
condition_columnsarrayyes
conditionsarrayyesconditionTotals rows: gross + *_net quantities AND the computed materials buy list
display_unitsstringyes
generated_withstringyes
markupsarrayyes
materialsarrayyesProject-wide buy list — condition rows summed by (name, unit)
project_namestring|nullyes
rfisarrayyes
roll_goodsarrayyesRoll-goods order rows (#136) — order_lf / rolls / order_qty per roll-goods condition, ×N applied; empty when no condition carries a roll_setup (always the case for a headless session today)
schemastringyes
shape_labelsarrayyes
sheetsarrayyesScale provenance per sheet — how each scale was set
totalsobjectyes
unitsstringyes

No examples provided.

export_takeoff ~108

The full "opentakeoff.takeoff_canvas.v1" annotations payload — exactly what the app autosaves, importable by it. Returned inline; pass path to also write it to disk as JSON. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
pathstringFile path to write the payload to
NameTypeReqDescription
approvalsarrayApproval-family records (#176) — the estimator's APPROVED seals and the agent's verdict marks {id, actor, ts, sheet_id, at:[nx,ny], shape_id?, text?}. Present only when any exist (the canvas payload'…
conditionsarrayyes
last_grouparrayyes
markupsarrayyes
project_namestringyes
schemastringyes
shapesarrayyes
sheet_grouparrayyes
sheet_levelsobjectyes
sheet_tabsarrayyes
sheetsarrayyes
unitsstringyes

No examples provided.

find_schedule ~197

Locate a schedule table in the set (#87): pass a kind ("room finish", "material"/"finish") and get every matching table's sheet, title, headers, TOTAL row count, and REGION — sized for a view_sheet look or a read_sheet_text pull of exactly the table. A schedule continued across sheets is ONE match whose "parts" list every fragment (base first) with its own viewable region; tables read through rotated headers say so; a table answering for one building carries "building". Errors with what WAS found when the asked-for kind isn't in the set. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
kindstringyes"room finish" (rooms → surface finishes) or "finish"/"material" (codes → products)
NameTypeReqDescription
matchesarrayyes

No examples provided.

find_text ~295

LOCATE a known string on a sheet — the complement to read_sheet_text (which returns what a region SAYS; this finds WHERE a string you already know sits). Case-insensitive substring match against each pdf.js text run, so a room label split across runs ("OFFICE" then "134" as separate items) needs a find_text call per fragment, or read_sheet_text over a region to see the whole thing joined. Every hit's center feeds straight into one_click as the seed — the locate-then-trace workflow: find_text the room number, one_click at (or just past) its center. Optionally restrict to a region {x0, y0, x1, y1}; results cap at limit (default 200), with count/truncated telling you exactly how much a tighter region or higher limit would recover. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
limitintegerMax hits returned
qstringyesText to find — a room number ('134'), a label fragment ('RECEPTION'), a schedule tag ('CPT-1')
regionobjectRect in image px (origin top-left, y down); omit for the full sheet
sheetstringyes
NameTypeReqDescription
countintegeryesTotal matches before the limit cap
hitsarrayyes
qstringyes
sheetstringyes
truncatedbooleanyestrue = count exceeds hits.length; narrow the region or raise limit

No examples provided.

import_takeoff ~257

The way BACK IN (#151): load an "opentakeoff.takeoff_canvas.v1" file — a prior export_takeoff, or the app's own save — into this session, through the SAME tested merge rules as the app's Sheet-menu import: finish-tag identity joins imported conditions onto this session's own (their knobs win), new ids append, duplicate ids skip (re-import is idempotent), and THIS session's calibration wins per sheet. An empty session adopts the file wholesale. Resume yesterday's work, extend a takeoff a human already reviewed (their ink stays ink — reviewed shapes arrive untouchable by agent verbs), or audit someone else's export with list_shapes/takeoff_summary. Requires a loaded plan; shapes referencing OTHER files ride along and count in totals but can't be viewed against this document — the reply's unknown_files names them. Approval marks ride the file too — transport, not minting: an estimator seal arriving by import stays estimator ink, listable but untouchable here. undo_last removes the imported SHAPES as one step; adopted conditions, scales, annotations, and approval marks stay.

NameTypeReqDescription
pathstringyesPath to a takeoff_canvas.v1 JSON file on disk
NameTypeReqDescription
conditions_addedintegeryes
conditions_mergedintegeryesImported conditions that joined an existing finish tag (its knobs won)
filestringyesBasename of the imported file
notestringyes
replacedbooleanyestrue = the session was empty and adopted the file wholesale
scales_adoptedintegeryesSheets whose calibration came from the file (this session's own always wins)
shapes_addedintegeryes
shapes_pendingintegeryesOf the added shapes, how many are unreviewed machine pencil
shapes_totalintegeryes
unknown_filesarrayyesFiles referenced by imported shapes that this document doesn't have — they count in totals but can't be viewed here

No examples provided.

link_annotation ~106

Attach an existing annotation to a condition, or detach it by passing an empty condition — the canvas's Attach/Detach control, reachable by an agent. Use it to tie up notes left unattached (list_annotations reports how many), or to move one to the finish it actually concerns. Attaching mints the tag on first use.

NameTypeReqDescription
annotation_idstringyesId from annotate or list_annotations
conditionstringyesFinish tag to attach to; empty string detaches
NameTypeReqDescription
conditionstringyes
condition_idstring
idstringyes
notestringyes

No examples provided.

list_annotations ~210

Every annotation on the takeoff, with condition_id RESOLVED to its finish tag so you can act on the reply without joining against conditions[]. Filter by sheet, by condition, or both. Coordinates come back in image px (the same frame you passed in), not the normalized form they're stored as. `unattached` counts the notes carrying no condition — the candidates for link_annotation. `verdicts` is the approval family's inventory (mark_verdict/delete_verdict): every mark with its actor stated — the estimator's APPROVED ring or the agent's AGENT diamond — under the same filters, a condition filter reaching a verdict through its target shape. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
conditionstringOnly annotations attached to this finish tag
sheetstringOnly annotations on this sheet
NameTypeReqDescription
annotationsarrayyes
countintegeryes
unattachedintegeryesHow many carry no condition — candidates for link_annotation
verdict_countintegeryes
verdictsarrayyesApproval-family records (#176) under the same filters: sheet applies directly; a condition filter reaches a verdict THROUGH its target shape (a sheet-point mark carries no scope and drops out)

No examples provided.

list_shapes ~118

The mid-session shape inventory (#149): every committed shape's id, sheet, condition tag, role, quantities, vertex count, and review state in one compact read — the ids edit_shape and delete_shape assume you have, without pulling the whole export_takeoff payload to find one shape. Filter by sheet, by condition, or both; filters narrow, an empty list is a result, not an error.

NameTypeReqDescription
conditionstringOnly shapes under this finish tag (must exist)
sheetstringOnly shapes on this sheet
NameTypeReqDescription
countintegeryes
shapesarrayyes

No examples provided.

load_plan ~251

Open a plan PDF from disk. Default: replace the whole session (previous documents, scales, conditions, and shapes are cleared). merge: true ADDS the document to the working set instead (#152) — a bid set is plans + schedule + addenda, not one PDF — keeping every scale, condition, and shape; sheet keys carry file names so documents never collide, the sheet graph spans the whole set (resolve_tag can chain a plan tag on one file to a schedule row in another), and the marked set covers every worked sheet. Re-loading an already-merged file is refused — reload = replace, deliberately. Returns file, files, page_count, and one entry per sheet. The loaded sheets also become browsable resources (takeoff://sheets). Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
mergebooleantrue = ADD this document to the working set, keeping all existing work (merge into an empty session is just a load)
pathstringyesPath to a plan PDF on disk
NameTypeReqDescription
filestringyesThe document just loaded (basename)
filesarrayyesEvery document in the working set, load order (#152 — one entry unless merge was used)
notestringyes
page_countintegeryesTotal sheets across the working set
sheetsarrayyesEVERY sheet in the working set, not just the file loaded by this call

No examples provided.

mark_verdict ~467

Mark the agent's VERDICT on work — the pencil half of the approval family, and the only half an agent can mint. Two actors exist on the record: the estimator's APPROVED ring is ink, minted solely by a human's click at the canvas's Approve tool; this tool mints the AGENT diamond and structurally nothing else — it takes no actor input to misuse. Target the work either way: shape_id anchors the mark ON a committed shape (a room at its area centroid, a run at its on-path midpoint, a count marker at its point) and records WHAT was marked — the shape_id stays on the record as provenance, and the glyph keeps its own anchor even if the shape is later deleted; or sheet + at drops the mark at a sheet point (image px). Exactly one target. Optional text rides the record through every export; the glyph itself always reads AGENT. A verdict touches no quantity and gates nothing: it is the agent's signed claim that it checked this work — pencil beside the estimator's ink, never in its place. The mark renders as the graphite AGENT diamond on the canvas and in the marked set, the marked-set cover tallies the split ("Approval stamps: N estimator-approved · M agent-marked"), and the record rides the annotations payload through export_takeoff / import_takeoff and the app's own saves. One mark per shape (re-mark = delete_verdict, then mark again); list_annotations returns the inventory in verdicts[]; undo_last steps over a mark exactly like any other mutation. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
atarraySheet-point mode: where the AGENT diamond renders (image px)
shape_idstringMark a committed shape (list_shapes has the ids) — anchored on the shape, recorded as provenance. Exactly one target: this OR sheet + at
sheetstringSheet-point mode: the sheet, together with at
textstringOptional short note riding the record and every export — the glyph always reads AGENT
NameTypeReqDescription
actorstringyesAlways agent — this tool is structurally incapable of minting the estimator's seal
atarrayWhere the AGENT diamond renders (image px) — absent only when the marked shape rides a sheet from a file this session hasn't loaded (#152)
conditionstringShape mode: the marked shape's finish tag, resolved
idstringyesThe minted record id ("apr-…")
notestringyes
shape_idstringShape mode: the committed shape this verdict is about
sheetstringyes
textstring
tsstringyesISO-8601 mint time

No examples provided.

measure_line ~119

Measure an open polyline (min 2 points, image px): length_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it as a linear shape (base, transitions, feature strips). Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
conditionstring
ptsarrayyes
sheetstringyes
NameTypeReqDescription
length_lfnumberyes
nptsintegeryes
shape_idstringPresent when condition was passed and the shape committed

No examples provided.

measure_polygon ~127

Measure a closed polygon you supply (min 3 vertices, image px): area_sf and perimeter_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it; role "deduct" subtracts. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
conditionstring
rolestring
sheetstringyes
vertsarrayyes
NameTypeReqDescription
area_sfnumberyes
nvertsintegeryes
perimeter_lfnumberyes
shape_idstringPresent when condition was passed and the shape committed
warningstringMixed-scale warning (#153): a scale note disagreeing with the sheet's sits in the measured region — verify before trusting these numbers

No examples provided.

measure_surface ~260

Surface Area — wall SF (#146): trace an OPEN run along the wall in plan view (min 2 points, image px) and the quantity is traced LF × height. This is how wall tile, wainscot, and wall systems are taken off — the quantity family one_click and measure_polygon cannot produce. Height lives on the CONDITION (the canvas's H knob): pass height_ft to set it on this call (journals as its own undo step, like typing H before tracing), or set it once with edit_condition; with neither, this refuses and mints nothing. The shape snapshots the height it was quantified at. Requires the sheet's scale. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
conditionstringyesFinish tag to commit under (minted on first use), e.g. 'CT-W1'
height_ftnumberWall height in feet — written to the condition's H knob first, then used
ptsarrayyesThe wall run, an open polyline (image px)
sheetstringyes
NameTypeReqDescription
area_sfnumberyeslength_lf × height_ft — the wall SF committed
conditionstringyes
height_ftnumberyesThe height this shape was quantified at (snapshotted on the shape)
length_lfnumberyesThe traced run's open length
nptsintegeryes
shape_idstringyes

No examples provided.

one_click ~689

One-Click Area: click inside a room (image px) and the plan's vector linework bounds it — the sealed flood engine (RFC #60), contour trace, vertices snapped to true PDF endpoints. The engine's arguments are FEET-TRUE through the sheet's scale, exactly the canvas's: gap sealing bridges up to a door-width opening (disclosed as gap_sealed_px — that much boundary is synthetic), door-swing wedges annex the swing a doorway sweeps (door_wedges), and the minimum-passage rule keeps sub-half-foot slits from conjoining two rooms (min_pass_px/min_pass_delta). Every trace carries the engine's own account of itself: confidence (0..1, with confidence_factors naming what deducted) — a review PRIORITIZER, never a verification. 1.0 means every signal ran clean, not that the trace is right; a LOW confidence is a view_sheet {overlay: true} audit prompt, not a fact to bid from — put eyes on the flagged edge before the total means anything. SCANNED sheets work too (#154): where vectors can't bound the room (an image-only scan, or a scan wrapper whose only linework is the title block), the flood falls back automatically to the sheet's rendered pixels — same engine the canvas uses — and the reply plus the committed shape's origin carry raster_traced: true so a pixel-bounded ring is never mistaken for a vector-snapped one. Vector always wins where it works; a raster ring's corners are unsnapped, so audit it with view_sheet {overlay: true} before trusting the total. With the sheet's scale set, returns area_sf / perimeter_lf; pass condition (a finish tag, e.g. "CPT-1") to commit the traced shape to the takeoff — the full engine account rides the committed shape's origin, so the export tells the truth about how each shape was made. Without a scale it returns px-only quantities with a warning and commits nothing (the engine also degrades to its scale-blind fallbacks — a weaker measurement, one more reason set_scale comes first). role "deduct" makes the committed shape subtract. After committin…

NameTypeReqDescription
conditionstringFinish tag to commit under (minted on first use)
layersobjectOverride the sheet's classified layer roles for THIS call (see sheet_info.layers)
return_vertsbooleanInclude the traced polygon's vertices (image px)
rolestring
sensitivitynumberFill sensitivity, the same knob the canvas has: 0 strict (hatch/light linework always blocks), 0.5 balanced (default), 1 aggressive (crosses more hatch, tolerates more growth). Raise it when a flood…
sheetstringyes
xnumberyes
ynumberyes
NameTypeReqDescription
area_px2numberPreview mode (no scale): raw area in px²
area_sfnumberScaled mode: traced area in SF
confidencenumber0..1 — the trace scored from the engine's own signals (sealed openings, door wedges, min-passage rule, hatch tier, raster boundary, mask coarseness, implausible size). A review PRIORITIZER, not a ver…
confidence_factorsarrayThe named factors behind a sub-1.0 confidence (e.g. "sealed-opening(10% synthetic boundary)") — each names the edge worth putting eyes on; absent when every signal ran clean
door_wedgesintegerDoor-swing wedges annexed into the region under grow-but-verify — how many doorways' swings were included, the canvas's own door handling; rides origin.door_wedges
gap_bridged_pxnumberPresent when the seal ladder bridged a drafting pinhole this many px wide to close the region — the rescue rides provenance (origin.gap_bridged_px) rather than passing as a clean fill
gap_sealed_pxnumberPresent when the seal ladder closed a genuine OPENING this many mask px wide (doorway-scale — scaled by the sheet's feet, distinct from gap_bridged_px's drafting-pinhole rescue). Part of the boundary…
hatch_filteredbooleanPresent when hatch/pattern linework was classified out of the boundary
min_pass_deltanumberFraction of the verbatim flood the minimum-passage rule removed; 1 means the drawn linework bounds nothing here and the rule is the only reason there is a measurement — audit before trusting
min_pass_pxnumberThe feet-true minimum-passage rule (openings under ~0.5 ft never connect two spaces) ran at this dilation radius AND changed the answer — present only with min_pass_delta
nvertsintegeryesVertex count of the traced polygon
perimeter_lfnumberScaled mode: traced perimeter in LF
perimeter_pxnumberPreview mode (no scale): raw perimeter in px
raster_tracedbooleanPresent when the region was bounded by the sheet's RENDERED PIXELS (the scanned-sheet raster fallback, #154) rather than vector linework — absent means the vector path ran. Rides origin.raster_traced…
ring_interiorsintegerOf those wedges, how many were a CLOSED ring's interior (round column, callout bubble) rather than a door swing — annexed floor you may want as a deduct instead
shape_idstringScaled mode: id of the committed shape, when condition was passed
statusstringyes
vertsarrayTraced polygon vertices (image px), when return_verts was set
warningstringPreview mode (no scale): why quantities are unavailable — OR, in scaled mode, a mixed-scale warning (#153): a scale note disagreeing with the sheet's sits in the measured region (enlarged plan/detail…

No examples provided.

place_count ~189

Count markers — EA (#146): one point, one each. Thresholds, stair nosings, floor boxes, entrance mats — the scale-free quantity family. Commits one count shape per point (computed {count: 1}, exactly the canvas's Count tool), NO scale required, and the whole call is ONE undo step like a detect_rooms sweep. takeoff_summary reports them as ea; the marked set draws each marker. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
conditionstringyesFinish tag to commit under (minted on first use), e.g. 'TR-1'
pointsarrayyesMarker positions (image px), one committed count shape each
sheetstringyes
NameTypeReqDescription
committedintegeryesCount shapes committed by this call — one per point
conditionstringyes
ea_totalnumberyesThe condition's total EA after this call
shape_idsarrayyes

No examples provided.

read_sheet_text ~125

The sheet's text with positions — items [{str, x, y}] in image px plus the joined text. Optionally restrict to a region {x0, y0, x1, y1}. Use it to read title blocks, room labels, finish schedules, and scale notes. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
regionobject
sheetstringyes
NameTypeReqDescription
itemsarrayyesPositioned text items (image px)
sheetstringyes
textstringyesThe items joined with spaces

No examples provided.

resolve_tag ~267

Resolve ONE room tag across the set (#87): the plan tag → its room-finish schedule row → each finish code's definition in the finish/material schedule, EVERY edge carrying an evidence pointer (sheet + literal text + bbox — pass a bbox to view_sheet to look at the source). Rows carried by a continuation sheet ("… SCHEDULE — CONT'D") resolve exactly like base-sheet rows, citing the sheet the ink is on. The doctrine is refusal over guessing: a room that appears on the plan with no schedule row returns status "unresolved" with the reason (and still cites the plan tag); reused room numbers return "ambiguous" rather than picking one — on a multi-building set the refusal LISTS the candidate rows per building, and a building-qualified tag ("A-134") picks the building the set names. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
tagstringyesThe room tag as drawn, e.g. "134" or "139A" — or building-qualified on a multi-building set, e.g. "A-134" (building A, room 134)
NameTypeReqDescription
buildingstringresolved only — the building whose schedule row answered, when the set names buildings
candidatesarrayunresolved only — every schedule row that COULD have answered (an ambiguous multi-building tag lists one per building; qualify the tag, e.g. "A-134", to pick)
finishesarray
reasonstringunresolved only — WHY (no schedule row / ambiguous / no schedule found). A room that appears on the plan with no row comes back here, never as a silent omission
roomyesThe plan tag, when the room appears on a plan sheet — cited even when resolution fails. null on a multi-building ambiguity: citing one building's tag would be quietly wrong
sourcesarrayThe chain: plan tag → schedule row (the row cites the sheet that CARRIES it — under a continuation that is the CONT'D sheet)
statusstringyes
tagstringyes

No examples provided.

set_scale ~225

Set a sheet's scale — exactly ONE of: label (a standard scale, e.g. '1/4" = 1'-0"'), upp (real feet per image px), calibrate (two points along a known dimension plus its real feet), or use_detected (adopt the drawn scale note read off the sheet). The detected scale is never applied automatically — setting it is always this explicit call. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
calibrateobjectTwo points (image px) a known real distance apart, and that distance in feet
labelstringA standard scale label, exactly as listed in the error on a miss
sheetstringyes
uppnumberReal feet per image px at render scale 2.0
use_detectedbooleantrue = adopt the sheet's detected scale
NameTypeReqDescription
labelstringThe standard scale label, when set by label or detected note
sheetstringyes
sourcestringyes
uppnumberyesReal feet per image px at render scale 2.0
warningstringPresent when the sheet carries MULTIPLE distinct scale notes (#153) — enlarged plans/details likely; region measurements under a disagreeing note will warn

No examples provided.

sheet_context ~455

The sheet's STRUCTURE in one call and one frame: the classified vector segments, the positioned text spans, and the hatch-family instances of a region — everything the engine itself floods against, exposed as data instead of pixels. Use it when you need to REASON about a region rather than look at it: which lines bound this space and at what pen weight, what the region says, and which periodic fill pattern covers it. The join is the point — all three arrive in image px with no reconciliation left to do, and the reply echoes the post-clamp region so passing that same rect to view_sheet gives you the matching render by construction. Hatch families carry a content-derived id (same pattern spec ⇒ same id, anywhere on the sheet), so matching a plan region to a legend swatch is comparing two ids, not guessing from a render — read the legend region, read the room region, match ids, and cite both bboxes as evidence. Decimation is declared, ordered, and counted on every reply: segments shorter than min_len_px drop first (invisible ink), then a max_segments cap applies LONGEST-FIRST so walls survive and hatch strokes go; kept + dropped always reconciles to total_in_region, and whole segments drop with their meta intact — nothing is ever simplified or merged, because these are classified segments and a merge would rewrite the classification. A scan returns has_vector_linework: false with empty vectors — absence of linework, never a claim the region is blank. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
max_segmentsintegerSegment cap, applied longest-first (default 4000). The reply's dropped.cap says exactly what a smaller region would recover
min_len_pxnumberDrop segments shorter than this (default 2 — one PDF point at render scale 2.0, below any pen width). 0 keeps everything
regionobjectRect in image px (origin top-left, y down); omit for the full sheet
sheetstringyes
NameTypeReqDescription
has_vector_lineworkbooleanyesfalse = a scan: vectors and hatch are empty because there are none, not because the region is blank
hatchobjectyes
pageintegeryes
regionarrayyesThe region actually resolved, post-clamp — pass this same rect to view_sheet and the render is in the same frame by construction
sheetstringyes
sheet_pxarrayyes
textobjectyes
vectorsobjectyes

No examples provided.

sheet_graph ~257

The plan-set INDEX (#87): every sheet's role (plan / schedule / legend / …, with confidence and the title evidence), the schedule tables found (kind, row count, region — a schedule CONTINUED across sheets ("… SCHEDULE — CONT'D") reads as ONE table, the continuation fragment naming its base in "continues"; rotated column headers are read at their quarter-turn and flagged), every room tag on the plan sheets (with the stacked room NAME when one exists, and the room's BUILDING on multi-building sets), the detail callouts (3/A-601 → sheet edges), the set's building designators, and named indexing gaps in "notes". Built once per document from the text layer and cached. This is how an agent decides WHAT to measure without a human enumerating the rooms: list the rooms here, resolve each with resolve_tag, then measure with one_click/detect_rooms. A scanned set (no text layer) returns available: false — unavailable, never half-populated. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

Input schema present but exposes no named parameters.

NameTypeReqDescription
availablebooleanyesfalse = the set has no text layer (a scan) — the graph degrades to unavailable, never half-populates
buildingsarrayEvery building designator the set names (sorted) — present only on multi-building-aware sets. Room numbers reused across these need qualified tags ('A-134')
calloutsarrayyesDetail callouts (3/A-601) — edges to their target sheets
countsobjectyes
notesarrayNamed gaps found while indexing (e.g. a continuation whose rows could not be aligned) — the graph refuses silently dropping anything
roomsarrayyesRoom tags read off plan-role sheets — schedule sheets contribute rows, never phantom rooms
sheetsarrayyes

No examples provided.

sheet_info ~137

Sheet detail: dims (px and pt), vector segment count, whether the sheet has vector linework (one_click floods it when present; a scanned sheet falls back to rendered pixels, disclosed as raster_traced), scale status, the detected scale suggestion, and this sheet's committed shape count. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
sheetstringyesSheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101")
NameTypeReqDescription
detected_scalestringDrawn scale note read off the sheet — a suggestion, never auto-applied
has_vector_lineworkbooleanyesone_click needs vector linework
height_ptnumberyes
height_pxnumberyes
layersarrayyesThe sheet's PDF layer table (#85) — [] when no Optional Content survived export (every engine path then runs the heuristics unchanged)
multiple_scalesbooleanSeveral DISTINCT scale notes on this sheet (#153) — enlarged plans/details likely
pageintegeryes1-based page number
scale_setbooleanyes
seg_countintegeryesVector segment count
shape_countintegeryesCommitted shapes on this sheet
sheetstringyesSheet key: page 1 is the bare file name ("plan.pdf"), pages 2+ are "plan.pdf#2"
sheet_numberstringTitle-block sheet number ("A-101") where detected
uppnumberReal feet per image px at render scale 2.0 — present once the scale is set
width_ptnumberyes
width_pxnumberyesImage px at render scale 2.0 — the coordinate space every tool speaks

No examples provided.

sweep_schedule_row ~631

Take off a schedule row's mark from the row itself — the estimator's own gesture: a transition type sometimes exists only as a schedule row plus tag markers scattered across the plan sheets, and this tool mints the condition FROM the row and finds every occurrence. Pass the row's key (e.g. 'T1') and the tool (1) reads the row from the set's schedule tables (the sheet_graph/find_schedule machinery — the row is the condition's cited source), (2) anchors a geometric fingerprint on the marker the tag is DRAWN as on a plan sheet (a deterministic pad ladder around the tag text; where the tag occurs more than once the fingerprint must recur at a second occurrence before it is trusted — `anchor.corroborated`), and (3) sweeps every PLAN-role sheet for it. The count is geometry AND text agreeing: drafting reuses one bubble shape across many marks, so a match counts ONLY when the row's own tag sits within the marker footprint (its bbox rides the match as `tag_at` evidence); a match labeled with a SIBLING row's tag is excluded and says whose it is, an unlabeled match is withheld as a question, and a tag drawn with no matching marker is disclosed as text_only. REFUSAL over guessing, with the reason and the fix: no such row; the same key in two tables (ambiguous); a tag drawn on no plan sheet; no repeatable marker linework around the tag — a fingerprint is never guessed from text alone (the fallback is always: marquee one instance with symbol_sweep). commit: true commits the counted matches as EA markers under the row's own key — one undo step for the whole set-wide sweep, every marker carrying origin.assignment {source: "schedule"} plus the anchor and row citation on origin.symbol.seed. The COUNT is scale-free (EA), but matching is not: where the anchor sheet and a target sheet both carry a scale, the marker is resized by their exact ratio before matching (`scaled` per sheet), and where one does not, the sweep runs at 1:1 and discloses it (`scale_assumed`) rather than reporting…

NameTypeReqDescription
commitbooleanCommit every counted match as one EA count marker (excluded/withheld/text_only never commit)
mirrorbooleanAlso match mirrored markers
rotationsbooleanAlso match 90/180/270-rotated markers
tagstringyesThe schedule row's key exactly as drawn, e.g. 'T1', 'TR-2' — it becomes the condition tag on commit
tolerance_pxnumberEndpoint match tolerance in image px (default 2 — CAD jitter, not drift)
NameTypeReqDescription
anchorobjectyes
committedintegercommit mode: count shapes committed — one per counted match, the whole sweep ONE undo step
conditionstringcommit mode: the condition minted FROM the row — its key is the tag
ea_totalnumber
foundintegeryesMatches carrying the row's own tag — the honest count, across every plan sheet
notestring
rowobjectyesThe schedule row the sweep was seeded from — the condition's source
shape_idsarray
sheetsarrayyesOne entry per swept PLAN-role sheet, load order
skippedarrayyesSheets excluded from counting (schedule/detail/legend/unknown), each with its reason
tagstringyesThe row key as normalized (the tag as drawn)
warningstringPresent when the per-sheet work cap dropped candidates

No examples provided.

symbol_sweep ~936

Find EVERY instance of a repeated plan symbol from ONE example — 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 — both ON by default; turn them off to pin orientation). Score ≥ 0.92 is a match; the 0.75–0.92 band comes back in `withheld` with a reason — 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. Work is capped and the cap is disclosed: a reply with candidates.dropped > 0 says exactly that some placements were never scored — tighten the seed rect around more distinctive geometry rather than trusting a truncated count. Marquee discipline: the rect must hug ONE instance — 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 — 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× the size of the same mark on a 1/8" plan — 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 `sc…

NameTypeReqDescription
commitbooleanCommit every MATCH center as one EA count marker (withheld placements never commit)
conditionstringFinish tag to commit match markers under (minted on first use), e.g. 'FD-1'. Required when commit is true
mirrorbooleanAlso match mirrored placements
rotationsbooleanAlso match 90/180/270-rotated placements
scopestring"sheet" = this sheet only; "set" = every PLAN-role sheet in the working set (needs a text layer for the sheet graph; non-plan sheets are excluded and disclosed)
seed_rectarrayyesMarquee around ONE example instance, [[x0,y0],[x1,y1]] in image px — tight: segments fully inside define the symbol
sheetstringyesThe sheet the seed rect sits on — in scope 'set' it may be ANY sheet (a detail/legend seed sheet is fingerprint source only, never counted)
tolerance_pxnumberEndpoint match tolerance in image px (default 2 — CAD jitter, not drift)
NameTypeReqDescription
candidatesobjectSheet scope only — set scope accounts per sheet in sheets[]
committedintegercommit mode: count shapes committed — one per match
conditionstringcommit mode: the finish tag the markers counted under
ea_totalnumbercommit mode: the condition's total EA after this call
foundintegeryesPlacements that cleared the commit bar — across every swept sheet in set scope
matchesarraySheet scope only. Deterministic reading order (y, then x). The seed's own location is never listed here
notestring
scopestringyes"sheet" = the swept sheet alone (matches/withheld/candidates at top level); "set" = every PLAN-role sheet in the working set (per-sheet results in sheets[], exclusions in skipped[])
seedobjectyes
shape_idsarray
sheetsarraySet scope only: one entry per swept PLAN-role sheet, load order
skippedarraySet scope only: every sheet excluded from counting, with role and reason — including the seed's own sheet when it is not a plan
warningstringPresent when the work cap dropped candidates — what a tighter seed rect would recover
withheldarraySheet scope only. Near-matches in the [0.75, 0.92) band — reported with a reason, NEVER committed. A withheld placement is a question you can answer with view_sheet; a hidden one is a miscount

No examples provided.

takeoff_summary ~109

Per-condition totals (floor/wall/border SF, LF, EA, SY, with and without waste) plus grand totals — the Report's numbers, computed by the same rules. Numbers only: the deliverable that SHOWS the work on the drawings is export_marked_pdf. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

Input schema present but exposes no named parameters.

NameTypeReqDescription
conditionsarrayyes
totalsobjectyes

No examples provided.

undo_last ~206

Step back over your OWN last n mutations, newest first — 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 — 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.

NameTypeReqDescription
nintegerHow many steps to reverse (1–100)
NameTypeReqDescription
notestring
remainingintegeryesSteps still available to undo
shape_countintegeryesCommitted shapes after the undo
stepsarrayyesNewest first
undoneintegeryesSteps actually reversed

No examples provided.

view_sheet ~622

SEE the sheet — render the page (or a crop of it) to a PNG image. This is your eyes on the plan, so CROP, DON'T SQUINT: the render downsamples to the px budget (≤2000 long side), which on an E-size sheet is ~4 sheet pixels per returned pixel — a full-sheet render finds WHERE things are, and only a tight region crop can tell you what the linework and labels actually say. Never audit a trace or read a dimension off a full-sheet render. region is in image px — the same space as every other tool — so a feature at pixel (ix, iy) of the returned image sits at x = region_x0 + ix × (region_x1 − region_x0) / img_w (same for y), and those coordinates go straight into one_click, measure_polygon, or read_sheet_text. overlay:true burns the session's committed shapes into the render (human-affirmed ink solid red, unreviewed machine shapes dashed blue) — render again after committing to verify your geometry landed where you intended, and sanity-check what you see: a fixture-sized ring where a room should be means the seed landed inside a stall or casework; an outsized ring means the flood escaped through an opening. To MEASURE rather than guess, pass grid: a calibrated measuring grid is burned in — thin lines every 1 ft, heavy blue every 5 ft, foot labels along the crop edges, feet counted from the crop's top-left corner. Count grid cells between walls exactly like an estimator scaling a plan; never derive a dimension by eye when the grid can give it to you. grid "auto" uses the sheet's set scale; before set_scale, pass the drawing scale read off the title block as inches-per-foot — "1/4" for a 1/4" = 1'-0" plan, "3/16", "0.25". Rendering needs the optional native canvas (@napi-rs/canvas); where it isn't installed this tool errors cleanly and every other tool still works. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
gridstringBurn in a calibrated 1-ft/5-ft measuring grid: "auto" = the sheet's set scale; otherwise the drawing scale as inches-per-foot, e.g. "1/4", "3/16", "0.25"
overlaybooleanBurn committed shapes into the render (solid = human-affirmed, dashed = unreviewed)
pxintegerLong-side pixel budget of the returned image (default 1400) — small region + high px = readable dimension strings
regionobjectCrop rect in image px (origin top-left, y down); omit for the full sheet
sheetstringyes

No output schema declared.

No examples provided.