fd93b7d3595c23da6e845115f3cfbcc2837abf5fsource_git · kentucky-ai/opentakeoff · current release
Observed 2026-08-25T08:35:13.657Z using mcpSecurity-inventory. Status: succeeded. Negotiated protocol: 2025-06-18.
{
"tools": {
"listChanged": true
},
"resources": {
"listChanged": true
}
}| Tool | Category | Annotations | Risk |
|---|---|---|---|
annotatePlace 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.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string",
"description": "Sheet name or number, as sheet_info reports it"
},
"type": {
"type": "string",
"enum": [
"cloud",
"text",
"callout",
"highlight",
"arrow",
"bubble",
"dimension"
],
"description": "cloud/highlight need rect; text/callout/bubble need at; callout also needs target; arrow and dimension need from + to"
},
"text": {
"type": "string",
"default": "",
"description": "The 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"
},
"condition": {
"type": "string",
"description": "Finish tag to attach this note to, e.g. 'CPT-1' (minted on first use). Omit for an unattached sheet note"
},
"at": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "Anchor point (image px) — text, callout, and bubble (the circle's center)"
},
"target": {
"$ref": "#/properties/at",
"description": "What a callout's leader line points at (image px)"
},
"rect": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"$ref": "#/properties/at"
},
{
"$ref": "#/properties/at"
}
],
"description": "Corners (image px) — cloud and highlight"
},
"from": {
"$ref": "#/properties/at",
"description": "Arrow tail / dimension start (image px)"
},
"to": {
"$ref": "#/properties/at",
"description": "Arrow head / dimension end (image px)"
},
"r": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Bubble radius (image px); omitted → the canvas default (2% of sheet width)"
}
},
"required": [
"sheet",
"type"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"sheet": {
"type": "string"
},
"type": {
"type": "string"
},
"text": {
"type": "string"
},
"condition": {
"type": "string"
},
"condition_id": {
"type": "string"
},
"length_lf": {
"type": "number",
"description": "Dimension only: the measured length (real feet) the annotation will label itself with"
},
"note": {
"type": "string"
}
},
"required": [
"id",
"sheet",
"type",
"text",
"condition",
"condition_id",
"note"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
apply_rulesRe-run the correction rules the takeoff arrived with (#207) — the lessons an estimator TAUGHT the canvas (#88): "every room like this loses the mechanical chase." A rule is a deterministic predicate (enclosed linework islands under a size cap, inside the rule's condition's rooms), never a re-prompt. Evaluation is the same pure rules.ts engine the canvas Preview runs; the commit is the one batch the canvas's Apply makes — ONE journal entry, undo_last takes the whole batch back. Everything lands reviewed: false (this server has no review gate), and the reply's per-rule disclosure — what each rule produced, what was skipped, with ids — IS your preview: read it, then view_sheet overlay:true. Idempotent by construction: any candidate an existing deduct already covers is dropped by the engine, so re-running after new rooms commit is the intended workflow and never double-deducts. Rules arrive ONLY via import_takeoff (minting a new rule is an estimator's correction and stays behind the canvas's human Preview→Apply gate); with none imported this refuses. Pass sheet to scan one sheet; omit it to scan every sheet holding the rules' rooms. Uncalibrated and scanned-raster sheets come back in skipped_sheets, named.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string",
"description": "Scan only this sheet (default: every sheet holding the rules' conditions' rooms)"
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"rules": {
"type": "array",
"items": {
"type": "object",
"properties": {
"rule_id": {
"type": "string"
},
"label": {
"type": "string",
"description": "The rule's own plain-language statement, minted at creation"
},
"condition": {
"type": "string",
"description": "The finish tag whose rooms were scanned"
},
"produced": {
"type": "integer"
},
"shape_ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "The committed deducts — reviewed: false, one batch"
},
"deduct_sf": {
"type": "number"
}
},
"required": [
"rule_id",
"label",
"condition",
"produced",
"shape_ids",
"deduct_sf"
],
"additionalProperties": false
}
},
"committed": {
"type": "integer",
"description": "Deducts committed across all rules — 0 is a result (idempotence)"
},
"total_deduct_sf": {
"type": "number"
},
"skipped_rules": {
"type": "array",
"items": {
"type": "object",
"properties": {
"rule_id": {
"type": "string"
},
"label": {
"type": "string"
},
"reason": {
"type": "string",
"enum": [
"inactive",
"condition_not_in_session"
]
}
},
"required": [
"rule_id",
"label",
"reason"
],
"additionalProperties": false
},
"description": "Rules not evaluated, named — never silently dropped"
},
"skipped_sheets": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet_id": {
"type": "string"
},
"reason": {
"type": "string",
"enum": [
"no_scale",
"no_vector_mask"
]
}
},
"required": [
"sheet_id",
"reason"
],
"additionalProperties": false
},
"description": "Sheets that could not be scanned (uncalibrated, or scanned raster with no linework mask)"
},
"note": {
"type": "string"
}
},
"required": [
"rules",
"committed",
"total_deduct_sf",
"skipped_rules",
"skipped_sheets",
"note"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
count_marksThe COUNT TAKEOFF in one deterministic call — no seeds, no model, seconds: census every VALUE-ANNOTATED mark tag on the plan-role sheets, counted per schedule mark, committed as EA markers when asked. The identity rule is the annotated-device drafting pattern: a device is drawn as its mark tag with a value under it ("S1" over "200" — CFM on air devices, GPM on fixtures, a rating on equipment), so a tag WITH a paired value counts, a tag inside a schedule table's own region is a row label (excluded, tallied), and every other occurrence is WITHHELD with a reason and coordinates — a tag amid linework but unvalued may be a real device (view_sheet it), a bare tag is probably a note mention. Marks default to the set's schedule row keys (a compound row "R1 / E1" answers for R1 AND E1; each mark cites its row), or state them: {marks: ["S1","R1"]}. The complement to sweep_schedule_row: THAT tool is for marks drawn ON their marker with no value (finish tags in bubbles) and matches geometry; this one is for annotated devices and needs no fingerprint at all. Refusal-honest: scans refuse (no text layer), a set with no mark-shaped rows refuses unless marks are stated, non-plan sheets are skipped with the role that excused them. commit: true commits every counted occurrence under its mark's own tag — ONE undo step for the whole census, schedule citation on origin. Counts are scale-free (EA) — no set_scale needed. Then AUDIT: view_sheet {overlay: true} where the markers landed, and read every withheld entry — a withheld item you ignore is a hole in the bid. 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{
"type": "object",
"properties": {
"marks": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"description": "The marks to census, e.g. [\"S1\", \"R1\"] — omit to take them from the schedule tables' row keys"
},
"commit": {
"type": "boolean",
"default": false,
"description": "Commit every counted occurrence as one EA count marker under its mark (withheld/excluded never commit)"
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"marks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"mark": {
"type": "string"
},
"count": {
"type": "integer",
"description": "Value-paired instances counted on plan-role sheets"
},
"row": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"key": {
"type": "string"
},
"table": {
"type": "string"
}
},
"required": [
"sheet",
"key",
"table"
],
"additionalProperties": false,
"description": "The schedule row that answers for this mark (a compound key answers for each part)"
},
"unscheduled": {
"type": "boolean",
"description": "true when the mark was stated by the caller but no schedule row answers for it"
},
"occurrences": {
"type": "array",
"items": {
"type": "object",
"properties": {
"at": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "The tag's center (image px)"
},
"value": {
"type": "string",
"description": "The paired value drawn under the tag (CFM, GPM, a count — the annotation that makes it an instance)"
},
"sheet": {
"type": "string"
}
},
"required": [
"at",
"value",
"sheet"
],
"additionalProperties": false
}
},
"occurrences_elided": {
"type": "integer"
},
"withheld": {
"type": "array",
"items": {
"type": "object",
"properties": {
"at": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "The tag's center (image px) — view_sheet here"
},
"sheet": {
"type": "string"
},
"reason": {
"type": "string"
}
},
"required": [
"at",
"sheet",
"reason"
],
"additionalProperties": false
},
"description": "Tag occurrences that did NOT count, each with the reason — read them, look, resolve or report"
},
"withheld_elided": {
"type": "integer"
},
"committed": {
"type": "object",
"properties": {
"committed": {
"type": "integer"
},
"ea_total": {
"type": "number"
}
},
"required": [
"committed",
"ea_total"
],
"additionalProperties": false
}
},
"required": [
"mark",
"count",
"occurrences",
"withheld"
],
"additionalProperties": false
}
},
"total": {
"type": "integer",
"description": "All counted instances across every mark"
},
"per_sheet": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"counts": {
"type": "object",
"additionalProperties": {
"type": "integer"
}
}
},
"required": [
"sheet",
"counts"
],
"additionalProperties": false
}
},
"excluded_in_tables": {
"type": "integer",
"description": "Tag occurrences inside a schedule table's own region — row labels, never instances"
},
"skipped": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"role": {
"type": "string"
},
"reason": {
"type": "string"
}
},
"required": [
"sheet",
"role",
"reason"
],
"additionalProperties": false
}
},
"complete": {
"type": "boolean"
}
},
"required": [
"marks",
"total",
"per_sheet",
"skipped",
"complete"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
cut_outCut a REAL hole in a committed floor_area shape (#206) — the way the canvas cuts one (#137): the same lib/cutout.js boolean subtract, so the two surfaces can never disagree about what a hole holds. The parent keeps its outer ring plus the reconciled hole(s) (verts_norm_holes), its computed nets for real — N cuts compose, overlap between cuts never double-deducts (set subtraction), a hole ADDS perimeter — and the deduct commits carrying cuts_shape_id so the report and legend read the reconciled number, never a second arithmetic pass. This is the verb for a column, a floor drain, an island of casework INSIDE a room; an independent measure_polygon role:"deduct" stays the tool for a deduction that isn't a hole in one parent. Refusal over guessing: the ring must sit FULLY inside the parent's outer ring (an edge-crossing cut is a boundary correction — edit_shape the parent instead), and a cut that would erase the parent or split it in two refuses whole (trace the pieces as rooms). One journal entry — undo_last restores parent and hole together; delete_shape on the deduct later reverts the cut too (a multi-cut parent rebuilds from the chain's pristine snapshot minus the survivors). AN OPEN RUN IS CLIPPED, NOT SUBTRACTED: wall tile (surface_area) and base/transitions (linear) are polylines traced in plan, so the ring removes the stretch it covers, the run keeps its id and takes what survives, and a cut through the MIDDLE leaves the far side as its own shape (same condition, same height) — quantities ride the surviving length, which is exact, since wall SF is LF × height and a border's SF is LF × thickness. No deduct is minted for a run: there is no area for one to sit on, and a deduct's SF counts against the FLOOR total a run never fills. A ring that misses the run, one that swallows it whole (delete_shape it), and a curved run (its verts are control points) all refuse. 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{
"type": "object",
"properties": {
"parent_shape_id": {
"type": "string",
"description": "A committed floor_area shape id, or an open run (surface_area / linear) to clip (list_shapes)"
},
"verts": {
"type": "array",
"items": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"minItems": 3,
"description": "The ring, image px — fully inside the parent for an area; over the stretch to remove for a run"
}
},
"required": [
"parent_shape_id",
"verts"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"deduct_shape_id": {
"type": "string",
"description": "Area parent: the reconciled deduct — carries cuts_shape_id; totals skip it (the parent nets the hole)"
},
"parent_shape_id": {
"type": "string"
},
"hole_sf": {
"type": "number",
"description": "Area parent: what this cut actually removed from the parent's net — 0 when the ring fell entirely inside an existing hole"
},
"parent_net": {
"type": "object",
"properties": {
"area_sf": {
"type": "number",
"description": "The parent's recomputed net after the subtract"
},
"perimeter_lf": {
"type": "number",
"description": "Outer ring + hole boundaries — a hole ADDS perimeter"
}
},
"required": [
"area_sf",
"perimeter_lf"
],
"additionalProperties": false
},
"holes": {
"type": "integer",
"description": "Area parent: holes the parent now carries"
},
"shape_id": {
"type": "string",
"description": "Run parent: the run that was clipped — it keeps its id and takes the first surviving stretch"
},
"measure_role": {
"type": "string",
"description": "Run parent: surface_area or linear"
},
"pieces": {
"type": "array",
"items": {
"type": "object",
"properties": {
"shape_id": {
"type": "string"
},
"lf": {
"type": "number",
"description": "This piece's own length"
},
"sf": {
"type": "number",
"description": "LF × the height (wall) or the thickness (border) it was measured at"
}
},
"required": [
"shape_id",
"lf",
"sf"
],
"additionalProperties": false
},
"description": "Run parent: every stretch that survived the cut — more than one when the ring fell in the middle"
},
"removed_lf": {
"type": "number",
"description": "Run parent: length the cut took out"
},
"removed_sf": {
"type": "number",
"description": "Run parent: the SF that rode on that length"
},
"note": {
"type": "string"
}
},
"required": [
"note"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
delete_shapeRemove 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.Input schema{
"type": "object",
"properties": {
"shape_id": {
"type": "string"
}
},
"required": [
"shape_id"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"deleted": {
"type": "string",
"description": "The removed shape's id"
},
"shape_count": {
"type": "integer",
"description": "Committed shapes remaining"
},
"note": {
"type": "string",
"description": "Cutout interplay (#206), when it applies: the parent's cut was reverted, could not be rebuilt, or reconciled deducts were orphaned by a parent delete"
}
},
"required": [
"deleted",
"shape_count"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
delete_verdictLift 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.Input schema{
"type": "object",
"properties": {
"verdict_id": {
"type": "string",
"description": "Record id from mark_verdict or list_annotations verdicts[]"
}
},
"required": [
"verdict_id"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"deleted": {
"type": "string",
"description": "The lifted record's id"
},
"verdicts_remaining": {
"type": "integer",
"description": "Approval-family records still on the takeoff (both actors)"
}
},
"required": [
"deleted",
"verdicts_remaining"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
derive_baseMint 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.Input schema{
"type": "object",
"properties": {
"source_condition": {
"type": "string",
"description": "Finish tag whose floor_area rooms the base derives from, e.g. 'CPT-1'"
},
"condition": {
"type": "string",
"description": "Finish tag the base commits under (minted on first use), e.g. 'RB-1'"
},
"openings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"shape_id": {
"type": "string",
"description": "A floor_area shape id of source_condition (list_shapes)"
},
"lf": {
"type": "number",
"minimum": 0,
"description": "Door/opening width to deduct from that room's perimeter, in feet"
}
},
"required": [
"shape_id",
"lf"
],
"additionalProperties": false
},
"description": "Stated openings per room — omit for gross perimeters"
}
},
"required": [
"source_condition",
"condition"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"condition": {
"type": "string",
"description": "The tag the base committed under"
},
"source_condition": {
"type": "string"
},
"rooms": {
"type": "array",
"items": {
"type": "object",
"properties": {
"source_shape_id": {
"type": "string"
},
"base_shape_id": {
"type": "string",
"description": "The committed linear base shape"
},
"sheet": {
"type": "string"
},
"gross_lf": {
"type": "number",
"description": "The room's full perimeter"
},
"openings_lf": {
"type": "number",
"description": "The openings you stated for this room"
},
"net_lf": {
"type": "number",
"description": "gross − openings — the committed quantity"
}
},
"required": [
"source_shape_id",
"base_shape_id",
"sheet",
"gross_lf",
"openings_lf",
"net_lf"
],
"additionalProperties": false
}
},
"committed": {
"type": "integer"
},
"total_lf": {
"type": "number",
"description": "Sum of net_lf across rooms"
},
"note": {
"type": "string"
}
},
"required": [
"condition",
"source_condition",
"rooms",
"committed",
"total_lf",
"note"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
derive_transitionsMint 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_run_in (default 12) drops corner artifacts. The butt-joint threshold is fixed at one inch and is not a knob: "these two finishes touch" is not a judgement call.
All-or-nothing, like derive_base: an unknown tag, a transition landing on either source tag, the same tag twice, or a sheet without a scale refuses the whole call before anything commits. The whole sweep is ONE undo step. After it, LOOK — view_sheet {overlay: true} over each run — before trusting total_lf. 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{
"type": "object",
"properties": {
"condition_a": {
"type": "string",
"description": "First finish tag, e.g. 'CPT-1' — its committed rooms are walked, and runs are traced along their boundaries"
},
"condition_b": {
"type": "string",
"description": "Second finish tag, e.g. 'PT-1'"
},
"condition": {
"type": "string",
"description": "Finish tag the transitions commit under (minted on first use), e.g. 'T-1'. Must differ from both sources"
},
"max_gap_in": {
"type": "number",
"exclusiveMinimum": 0,
"description": "How 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_in": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Shortest run worth reporting, in inches (default 12) — below this is a corner where two rooms clip, not a transition"
}
},
"required": [
"condition_a",
"condition_b",
"condition"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"condition": {
"type": "string",
"description": "The tag the transitions committed under"
},
"between": {
"type": "array",
"items": {
"type": "string"
},
"description": "The two finish tags"
},
"committed": {
"type": "integer"
},
"total_lf": {
"type": "number",
"description": "Sum of committed run lengths — butt joints only"
},
"runs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"between_shape_ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "The two floor_area shapes this run separates"
},
"length_lf": {
"type": "number",
"description": "Run length along the first shape's boundary"
},
"gap_in": {
"type": "number",
"description": "Median distance between the two rings across the run, in inches — 0-ish is one open space, 4-8 is a partition"
},
"at": {
"type": "array",
"items": {
"type": "number"
},
"description": "Run midpoint (image px) — pass to view_sheet to look at it"
},
"shape_id": {
"type": "string"
}
},
"required": [
"sheet",
"between_shape_ids",
"length_lf",
"gap_in",
"at",
"shape_id"
],
"additionalProperties": false
}
},
"withheld": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet": {
"$ref": "#/properties/runs/items/properties/sheet"
},
"between_shape_ids": {
"$ref": "#/properties/runs/items/properties/between_shape_ids"
},
"length_lf": {
"$ref": "#/properties/runs/items/properties/length_lf"
},
"gap_in": {
"$ref": "#/properties/runs/items/properties/gap_in"
},
"at": {
"$ref": "#/properties/runs/items/properties/at"
},
"reason": {
"type": "string",
"const": "wall_separated"
},
"detail": {
"type": "string"
}
},
"required": [
"sheet",
"between_shape_ids",
"length_lf",
"gap_in",
"at",
"reason",
"detail"
],
"additionalProperties": false
},
"description": "Adjacency across a wall: real, measured, and NOT committed — the transition there is a threshold at a doorway this cannot locate"
},
"withheld_lf": {
"type": "number",
"description": "Shared-wall length held back — never part of total_lf"
},
"note": {
"type": "string"
}
},
"required": [
"condition",
"between",
"committed",
"total_lf",
"runs",
"withheld",
"withheld_lf",
"note"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
detect_roomsBatch 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. Every committed room carries the room number it was traced from as the shape's `label`, so a sweep arrives already sliced by room — that field is what the Report's per-room grouping and the workbook's floor × room tab read, and it is the one thing about a batch that cannot be recovered downstream if it is dropped. 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 wrong bid; condition commits every room under that one stated tag (only right when the rooms genuinely share it; role "deduct" makes them subtract). Without a scale, returns px-only quantities per room and commits nothing — the plausibility floor needs real units, so it only applies once a scale is set. A batch commit is NOT finished until you have LOOKED at it: view_sheet {overlay: true}, audit every ring against the walls, fix misses with edit_shape / delete_shape — before the totals mean anything. 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{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"condition": {
"type": "string",
"description": "Finish tag to commit every detected room under (minted on first use). Mutually exclusive with assign_from_schedule"
},
"assign_from_schedule": {
"type": "boolean",
"default": false,
"description": "Commit 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 row, no FLOOR cell, a compound cell like \"CPT-1/VCT-1\" — are returned in unresolved[] with reasons and seeds instead of committed under a guess. Needs the sheet's scale and a room-finish schedule in the working set (merge the schedule sheet in with load_plan first). Mutually exclusive with condition"
},
"role": {
"type": "string",
"enum": [
"floor_area",
"deduct"
],
"default": "floor_area"
},
"return_verts": {
"type": "boolean",
"default": false,
"description": "Include each traced polygon's vertices (image px)"
},
"min_area_sf": {
"type": "number",
"exclusiveMinimum": 0,
"default": 5,
"description": "Plausibility 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 what was skipped."
},
"sensitivity": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Fill 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 stops short at hatching INSIDE the room; verify the grown ring with view_sheet overlay before committing"
},
"layers": {
"type": "object",
"properties": {
"include": {
"type": "array",
"items": {
"type": "string"
},
"description": "Layer names or ids whose ink must plot as HARD boundary"
},
"exclude": {
"type": "array",
"items": {
"type": "string"
},
"description": "Layer names or ids whose ink must not block the flood at all"
}
},
"additionalProperties": false,
"description": "Override the sheet's classified layer roles for THIS call (see sheet_info.layers)"
}
},
"required": [
"sheet"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"detected": {
"type": "integer",
"description": "Count of cleanly-detected rooms — may be fewer than the labels found on the sheet"
},
"rooms": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "The room-number text the seed was read from (e.g. \"104\", \"139A\")"
},
"nverts": {
"type": "integer",
"description": "Vertex count of the traced polygon"
},
"merged_labels": {
"type": "array",
"items": {
"type": "string"
},
"description": "Other labels that flooded to this same region — the area is counted once, under `label`"
},
"confidence": {
"type": "number",
"description": "0..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 verification: 1.0 means every signal came back clean, never that the trace is right. A low score is a view_sheet {overlay:true} audit prompt, not a fact to bid from"
},
"confidence_factors": {
"type": "array",
"items": {
"type": "string"
},
"description": "The 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"
},
"gap_sealed_px": {
"type": "number",
"description": "Present 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 is synthetic, and confidence deducts by that share; rides origin.gap_sealed_px on the committed shape"
},
"min_pass_px": {
"type": "number",
"description": "The 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"
},
"min_pass_delta": {
"type": "number",
"description": "Fraction 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"
},
"door_wedges": {
"type": "integer",
"description": "Door-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"
},
"ring_interiors": {
"type": "integer",
"description": "Of 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"
},
"hatch_filtered": {
"type": "boolean",
"const": true,
"description": "Present when hatch/pattern linework was classified out of the boundary"
},
"gap_bridged_px": {
"type": "number",
"description": "Present when the seal ladder bridged a drafting pinhole this many px wide to close the region"
},
"raster_traced": {
"type": "boolean",
"const": true,
"description": "Present when the room was bounded by rendered pixels (scanned-sheet raster fallback, #154) rather than vector linework — sheet-wide per sweep, and it rides origin.raster_traced on the committed shape"
},
"verts": {
"type": "array",
"items": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"description": "Traced polygon vertices (image px), when return_verts was set"
},
"area_sf": {
"type": "number",
"description": "Scaled mode: traced area in SF"
},
"perimeter_lf": {
"type": "number",
"description": "Scaled mode: traced perimeter in LF"
},
"shape_id": {
"type": "string",
"description": "Scaled mode: id of the committed shape, when condition was passed"
},
"condition": {
"type": "string",
"description": "The finish tag this room committed under — the passed condition, or in assign mode the FLOOR finish its own schedule row states. Present exactly when shape_id is"
},
"area_px2": {
"type": "number",
"description": "Preview mode (no scale): raw area in px²"
},
"perimeter_px": {
"type": "number",
"description": "Preview mode (no scale): raw perimeter in px"
}
},
"required": [
"label",
"nverts"
],
"additionalProperties": false
}
},
"withheld": {
"type": "object",
"properties": {
"total": {
"type": "integer",
"description": "Seeds found on the sheet but not reported as rooms"
},
"degenerate": {
"type": "integer",
"description": "Traced to fewer than 3 vertices"
},
"duplicate": {
"type": "integer",
"description": "Flooded to a region another label already claimed — counted once, never twice"
},
"bubble": {
"type": "integer",
"description": "Labels whose every clean flood was their own label BUBBLE (ring bbox ≈ label bbox — plans box their room numbers). Scale-free, so it guards unscaled previews too"
},
"implausible": {
"type": "integer",
"description": "Enclosed, clean, non-bubble, but smaller than min_area_sf — a door swing or wall cavity rather than a room"
},
"unresolved": {
"type": "integer",
"description": "Assign mode: rooms the schedule could not answer for (no row, no FLOOR cell, or a compound cell) — withheld into unresolved[], never committed under a guess. Always present; 0 outside assign mode"
},
"min_area_sf": {
"type": "number",
"description": "The plausibility floor applied (scaled mode only)"
}
},
"required": [
"total",
"degenerate",
"duplicate",
"bubble",
"implausible",
"unresolved"
],
"additionalProperties": false,
"description": "What detection skipped and why — a withheld room is a question the caller can ask; a silently dropped one is a hole in a bid"
},
"unresolved": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "The room tag as drawn"
},
"reason": {
"type": "string",
"description": "WHY the schedule could not answer — resolveTag's own reason, \"states no FLOOR finish\", or \"ambiguous: …\" for a compound cell"
},
"area_sf": {
"type": "number",
"description": "The room's real traced area — withheld from committing, not from reporting"
},
"perimeter_lf": {
"type": "number"
},
"seed": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "The flood seed (image px) — once the estimator answers, one_click here with the stated condition commits it"
}
},
"required": [
"label",
"reason",
"area_sf",
"perimeter_lf",
"seed"
],
"additionalProperties": false
},
"description": "Assign mode only, empty array included: [] is the positive claim that every detected room resolved against its own schedule row"
},
"note": {
"type": "string",
"description": "Human-readable summary of what was withheld, when anything was"
},
"multiple_scales": {
"type": "boolean",
"const": true,
"description": "Several DISTINCT scale notes on this sheet (#153) — rooms inside an enlarged viewport may be figured at the wrong scale"
},
"warning": {
"type": "string",
"description": "Preview mode (no scale): why quantities are unavailable and what to do"
}
},
"required": [
"detected",
"rooms",
"withheld"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
duplicate_conditionTwin a condition — 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 — 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' → 'CPT-1 – 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 — measure the new area against the returned condition_id. Reversible with undo_last; use split_condition to end the inheritance permanently.Input schema{
"type": "object",
"properties": {
"condition": {
"type": "string",
"description": "Finish tag of the condition to twin, e.g. 'CPT-1'"
},
"label": {
"type": "string",
"description": "What makes this one different, usually the area: 'Level 2', 'Building B', 'Phase 2'"
}
},
"required": [
"condition",
"label"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"condition": {
"type": "string",
"description": "The twin's finish tag — base tag + the label, e.g. 'CPT-1 – Level 2'"
},
"condition_id": {
"type": "string",
"description": "The TWIN — measure the new area against this"
},
"variant_of": {
"type": "string",
"description": "The condition whose material rows this one follows"
},
"variant_label": {
"type": "string"
},
"family_id": {
"type": "string",
"description": "Shared by every variant of this finish — survives a split"
},
"inherited_rows": {
"type": "integer",
"description": "Material rows copied, all still following the original"
},
"note": {
"type": "string"
}
},
"required": [
"condition",
"condition_id",
"variant_of",
"variant_label",
"family_id",
"inherited_rows",
"note"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
edit_conditionSet 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).Input schema{
"type": "object",
"properties": {
"condition": {
"type": "string",
"description": "Finish tag of an existing condition, e.g. 'CPT-1'"
},
"waste_pct": {
"type": "number",
"minimum": 0,
"description": "Waste percentage applied to net order quantities, e.g. 10 for 10%"
},
"multiplier": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Quantity multiplier (×N identical areas). Note: the canvas treats 0 as 1, so 0 is rejected here rather than silently meaning 'off'"
},
"height_ft": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Wall height in feet — the canvas's H knob; measure_surface quantifies traced LF × this"
},
"roll_setup": {
"anyOf": [
{
"type": "null",
"description": "Opt the condition OUT of roll goods"
},
{
"type": "object",
"properties": {
"material": {
"type": "string",
"enum": [
"carpet",
"sheet_vinyl",
"rubber"
],
"description": "Material class — fresh opt-ins and material changes start from this class's engine defaults (carpet sells sy, others sf)"
},
"roll_width_ft": {
"type": "number",
"exclusiveMinimum": 0
},
"roll_length_ft": {
"type": "number",
"minimum": 0,
"description": "Physical roll length; 0 = unlimited"
},
"seam_allowance_in": {
"type": "number",
"minimum": 0
},
"wall_overage_in": {
"type": "number",
"minimum": 0
},
"doorway_overage_in": {
"type": "number",
"minimum": 0
},
"direction": {
"type": "string",
"enum": [
"auto",
"ns",
"ew"
],
"description": "Run direction; auto lets the engine pick per room"
},
"price_unit": {
"type": "string",
"enum": [
"sy",
"sf",
"lf"
],
"description": "Sell unit the order quantity is figured in"
}
},
"additionalProperties": false
}
],
"description": "Roll-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 existing setup; null opts out. The reply echoes the figured order (cuts, order_lf, rolls, order_qty) whenever floor shapes exist on scaled sheets, and export_report's roll_goods block carries the same rows"
}
},
"required": [
"condition"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"condition": {
"type": "string",
"description": "The finish tag passed in"
},
"condition_id": {
"type": "string"
},
"waste_pct": {
"type": "number",
"description": "The condition's waste % after this write"
},
"multiplier": {
"type": "number",
"description": "The condition's quantity multiplier after this write"
},
"height_ft": {
"type": "number",
"description": "The condition's wall height after this write — present once set (measure_surface multiplies traced LF by it)"
},
"roll_setup": {
"type": "object",
"properties": {},
"additionalProperties": true,
"description": "The condition's roll-goods setup after this write — present while opted in"
},
"roll": {
"type": "object",
"properties": {
"condition_id": {
"type": "string"
},
"finish_tag": {
"type": "string"
},
"material": {
"type": "string"
},
"roll_width_ft": {
"type": "number"
},
"roll_length_ft": {
"type": "number"
},
"direction": {
"type": "string"
},
"cuts": {
"type": "integer"
},
"order_lf": {
"type": "number",
"description": "Full-width roll footage to order, ×N applied, rounded up to the inch"
},
"rolls": {
"type": "number"
},
"order_qty": {
"type": "number"
},
"order_unit": {
"type": "string"
},
"oversize": {
"type": "boolean",
"description": "true when a cut exceeds the physical roll length (roll_length_ft binds)"
}
},
"required": [
"condition_id",
"finish_tag",
"material",
"roll_width_ft",
"roll_length_ft",
"direction",
"cuts",
"order_lf",
"rolls",
"order_qty",
"order_unit",
"oversize"
],
"additionalProperties": true,
"description": "The figured order (same row export_report's roll_goods carries) — present when the roll-goods condition has floor shapes on scaled sheets"
}
},
"required": [
"condition",
"condition_id",
"waste_pct",
"multiplier"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
edit_materialsAdd, 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/seam_lf) ÷ per, rounded up to whole purchase units unless round:false. basis "seam_lf" is the one basis that is FIGURED rather than measured: it is the length where two cuts meet on the floor, read off the condition's roll layout (set roll_setup with edit_condition), which is what a heat-weld rod or a carpet seam tape is bought by. A 20-ft-wide room off a 12-ft roll seams once down its length; the same square footage as two 10-ft rooms seams not at all, and no percentage of the area or the perimeter can tell those two jobs apart. Without a roll_setup — or with no committed floor shapes to lay out — a seam_lf row reads 0, which is the honest state rather than a guess. 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).Input schema{
"type": "object",
"properties": {
"condition": {
"type": "string",
"description": "Finish tag, e.g. 'CPT-1'"
},
"add": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"per": {
"type": "number",
"minimum": 0,
"description": "Coverage rate — basis units per purchase unit, e.g. 250 for 1 gal / 250 sf. Default 0 (quantity 0 until set)"
},
"basis": {
"type": "string",
"enum": [
"area",
"linear",
"count",
"seam_lf"
],
"description": "Which of the condition's totals this row divides against — default 'area' (total SF). 'seam_lf' is the figured roll-layout seam length (weld rod, seam tape), 0 until the condition carries a roll_setup"
},
"unit": {
"type": "string",
"description": "Purchase unit, e.g. 'gal', 'bag', 'roll'"
},
"round": {
"type": "boolean",
"description": "Round up to whole purchase units — default true"
},
"note": {
"type": "string"
}
},
"required": [
"name"
],
"additionalProperties": false
},
"description": "New rows to add"
},
"remove": {
"type": "array",
"items": {
"type": "string"
},
"description": "Existing row ids to remove"
},
"patch": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": [
"string",
"number",
"boolean"
]
},
"description": "Field:value pairs — name/per/basis/unit/round/note only"
}
},
"required": [
"id",
"fields"
],
"additionalProperties": false
},
"description": "Field changes on existing rows"
}
},
"required": [
"condition"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"condition": {
"type": "string",
"description": "The finish tag passed in"
},
"condition_id": {
"type": "string"
},
"changed": {
"type": "object",
"properties": {
"added": {
"type": "array",
"items": {
"type": "string"
},
"description": "Ids of newly added rows"
},
"removed": {
"type": "array",
"items": {
"type": "string"
},
"description": "Ids removed"
},
"patched": {
"type": "array",
"items": {
"type": "string"
},
"description": "Ids whose fields changed"
}
},
"required": [
"added",
"removed",
"patched"
],
"additionalProperties": false
},
"materials": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"per": {
"type": "number",
"description": "Coverage rate: basis ÷ per = order quantity"
},
"basis": {
"type": "string",
"enum": [
"area",
"linear",
"count",
"seam_lf"
],
"description": "Which of the condition's totals this row's quantity is computed against — 'seam_lf' is the FIGURED roll-layout seam length (weld rod, seam tape), 0 until the condition carries a roll_setup"
},
"unit": {
"type": "string"
},
"round": {
"type": "boolean",
"description": "true = round up to whole purchase units (the default — you buy whole bags/buckets)"
},
"note": {
"type": "string"
},
"origin_id": {
"type": "string",
"description": "On a twin: the parent row this one follows (the variants.ts family link)"
},
"inherited": {
"type": "boolean",
"description": "On a twin: true while the row still follows the family — a patch on it takes it local, split_condition freezes them all"
}
},
"required": [
"id",
"name",
"per",
"basis",
"unit",
"round"
],
"additionalProperties": false
},
"description": "The condition's full materials array after this write"
}
},
"required": [
"condition",
"condition_id",
"changed",
"materials"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
edit_shapeREVISE 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, label to name the room it belongs to, 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. label is the per-room reporting seam: detect_rooms already stamps the room number it traced from, so this is how a shape traced by hand — or one whose room number the sweep read wrong — joins the same per-room breakdown the Report and the workbook's floor × room tab group by. 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.Input schema{
"type": "object",
"properties": {
"shape_id": {
"type": "string",
"description": "Id returned when the shape was committed"
},
"verts": {
"type": "array",
"items": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"description": "Replacement geometry (image px): ≥3 vertices for an area shape, ≥2 points for a linear/surface run, ≥1 for a count marker"
},
"condition": {
"type": "string",
"description": "Reassign to this finish tag (minted on first use)"
},
"role": {
"type": "string",
"enum": [
"floor_area",
"deduct",
"linear",
"surface_area",
"count"
],
"description": "Switch what the shape measures — flipping INTO surface_area needs a height on the shape or its condition"
},
"label": {
"type": "string",
"description": "The room (or phase/area) this shape belongs to, e.g. \"134\" or \"OFFICE 101\" — what per-room reporting groups by. Pass \"\" to clear it"
}
},
"required": [
"shape_id"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"shape_id": {
"type": "string"
},
"changed": {
"type": "array",
"items": {
"type": "string",
"enum": [
"verts",
"condition",
"role",
"label"
]
},
"description": "Which fields this call actually changed"
},
"measure_role": {
"type": "string",
"enum": [
"floor_area",
"deduct",
"linear",
"surface_area",
"count"
]
},
"nverts": {
"type": "integer"
},
"area_sf": {
"type": "number",
"description": "0 for linear shapes; LF × height for surface_area; absent for count"
},
"perimeter_lf": {
"type": "number",
"description": "Length for linear/surface runs, perimeter for closed ones; absent for count"
},
"count": {
"type": "number",
"description": "count shapes only — the marker's EA (preserved across the edit)"
},
"label": {
"type": "string",
"description": "The shape's room/phase label after this call — absent when it carries none (a cleared label reports as absent, not as an empty string)"
},
"agent_edits": {
"type": "integer",
"description": "How many times the agent has revised this shape — separate from the human-correction tally"
}
},
"required": [
"shape_id",
"changed",
"measure_role",
"nverts",
"agent_edits"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
export_dxfThe takeoff as a CAD drawing — a DXF (R2000) AutoCAD, BricsCAD, LibreCAD and Revit import as native geometry, not a picture. ONE sheet per file, like a DWG: every committed shape on that sheet becomes an LWPOLYLINE (floor rings CLOSED, walls and linear runs open, count marks a 1-ft circle), on a layer named for its finish — OT-<TAG>, with -DEDUCT / -HOLE / -WALL / -LINEAR / -COUNT suffix layers so a CAD user isolates any bucket with one layer filter, and room labels as TEXT on OT-LABELS. Coordinates are real units in the sheet's own frame: origin at the sheet's BOTTOM-left, Y up (CAD convention), feet by default ($INSUNITS 2) or metres with units:"m"; a ring's area in CAD equals its area in export_report to rounding, so the drawing IS the audit. Requires the sheet's scale (refuses otherwise — pixels in a DXF are worse than nothing); with several sheets carrying shapes, pass sheet to choose the drawing (the refusal lists them). The reply names every shape left out and why — a reconciled deduct ships as its parent's -HOLE ring, never twice. Writes to path (required — a DXF lives on disk, next to the DWG it aligns to); pair with export_marked_pdf for the reviewed planset.Input schema{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "File path to write the .dxf to"
},
"sheet": {
"type": "string",
"description": "Sheet key (\"plan.pdf\", \"plan.pdf#2\") or title-block number (\"A-101\"). Optional only when exactly one calibrated sheet carries shapes"
},
"units": {
"type": "string",
"enum": [
"ft",
"m"
],
"description": "Output units — \"ft\" (default) or \"m\""
},
"overwrite": {
"type": "boolean",
"description": "Replace the file at path even when it is not an OpenTakeoff export. Off by default: re-exporting over a previous export of your own already overwrites without this, so you only need it to deliberately destroy an unrelated file."
}
},
"required": [
"path"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "The DXF written"
},
"sheet": {
"type": "string",
"description": "Sheet key the drawing was cut from"
},
"sheet_number": {
"type": [
"string",
"null"
]
},
"units": {
"type": "string",
"enum": [
"ft",
"m"
]
},
"layers": {
"type": "array",
"items": {
"type": "string"
},
"description": "Layer names in table order — OT-<TAG>, plus -DEDUCT/-HOLE/-WALL/-LINEAR/-COUNT suffix layers and OT-LABELS"
},
"entities": {
"type": "integer",
"description": "LWPOLYLINE + CIRCLE + TEXT entities in model space"
},
"shapes": {
"type": "integer",
"description": "Committed shapes that produced geometry"
},
"skipped": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"reason": {
"type": "string"
}
},
"required": [
"id",
"reason"
],
"additionalProperties": false
},
"description": "Shapes on this sheet left out, each with why — never silent"
},
"extents": {
"anyOf": [
{
"type": "object",
"properties": {
"min": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"max": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
}
},
"required": [
"min",
"max"
],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "Model-space bounding box in output units; origin = sheet's bottom-left, Y up"
},
"bytes": {
"type": "integer"
}
},
"required": [
"path",
"sheet",
"sheet_number",
"units",
"layers",
"entities",
"shapes",
"skipped",
"extents",
"bytes"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
export_marked_pdfThe 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.Input schema{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Where to write the PDF (default: \"<plan dir>/<plan> - marked set.pdf\")"
},
"project_name": {
"type": "string",
"description": "Cover-page project name (default: the plan file's name)"
},
"overwrite": {
"type": "boolean",
"description": "Replace the file at path even when it is not an OpenTakeoff export. Off by default: re-exporting over a previous export of your own already overwrites without this, so you only need it to deliberately destroy an unrelated file."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute path of the written marked-set PDF — hand this to the user"
},
"pages": {
"type": "integer",
"description": "Legend cover + one page per marked sheet"
},
"sheets_marked": {
"type": "integer",
"description": "Sheets carrying shapes, annotations, or approval marks — unmarked sheets are omitted"
},
"shapes_drawn": {
"type": "integer"
},
"annotations_drawn": {
"type": "integer"
},
"approvals_drawn": {
"type": "integer",
"description": "Approval-family glyphs burned in (#176) — estimator APPROVED rings + agent AGENT diamonds; the cover tallies the split when any exist"
},
"note": {
"type": "string"
}
},
"required": [
"path",
"pages",
"sheets_marked",
"shapes_drawn",
"annotations_drawn",
"approvals_drawn",
"note"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
export_reportThe 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.Input schema{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "File path to write the document to"
},
"project_name": {
"type": "string",
"description": "Label for the document's project_name field (a headless session has no project of its own; omitted → null)"
},
"overwrite": {
"type": "boolean",
"description": "Replace the file at path even when it is not an OpenTakeoff export. Off by default: re-exporting over a previous export of your own already overwrites without this, so you only need it to deliberately destroy an unrelated file."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"schema": {
"type": "string",
"const": "opentakeoff.report.v1"
},
"project_name": {
"type": [
"string",
"null"
]
},
"generated_with": {
"type": "string"
},
"sheets": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet_id": {
"type": "string"
},
"sheet": {
"type": "string"
},
"scale_source": {
"type": "string"
}
},
"required": [
"sheet_id",
"sheet",
"scale_source"
],
"additionalProperties": true
},
"description": "Scale provenance per sheet — how each scale was set"
},
"conditions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"finish_tag": {
"type": "string"
},
"multiplier": {
"type": "number"
},
"waste_pct": {
"type": "number"
},
"shape_count": {
"type": "integer"
},
"floor_sf": {
"type": "number"
},
"wall_sf": {
"type": "number"
},
"border_sf": {
"type": "number"
},
"lf": {
"type": "number"
},
"ea": {
"type": "number"
},
"total_sf": {
"type": "number"
},
"floor_sf_net": {
"type": "number"
},
"wall_sf_net": {
"type": "number"
},
"border_sf_net": {
"type": "number"
},
"lf_net": {
"type": "number"
},
"total_sf_net": {
"type": "number"
},
"sy_net": {
"type": "number"
},
"materials": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"unit": {
"type": "string",
"description": "Purchase unit, e.g. 'gal', 'bag'"
},
"per": {
"type": "number",
"description": "Coverage rate — basis units per purchase unit"
},
"basis": {
"type": "string",
"enum": [
"area",
"linear",
"count",
"seam_lf"
]
},
"round": {
"type": "boolean"
},
"basis_qty": {
"type": "number",
"description": "The condition total this row divides (SF, LF, EA, or figured seam LF — multiplier applied, waste not)"
},
"qty": {
"type": "number",
"description": "Computed order quantity"
}
},
"required": [
"name",
"unit",
"per",
"basis",
"round",
"basis_qty",
"qty"
],
"additionalProperties": true
}
}
},
"required": [
"id",
"finish_tag",
"multiplier",
"waste_pct",
"shape_count",
"floor_sf",
"wall_sf",
"border_sf",
"lf",
"ea",
"total_sf",
"floor_sf_net",
"wall_sf_net",
"border_sf_net",
"lf_net",
"total_sf_net",
"sy_net",
"materials"
],
"additionalProperties": true
},
"description": "conditionTotals rows: gross + *_net quantities AND the computed materials buy list"
},
"by_sheet": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet_id": {
"type": "string"
},
"sheet": {
"type": "string"
},
"rows": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {}
}
}
},
"required": [
"sheet_id",
"sheet",
"rows"
],
"additionalProperties": true
},
"description": "BASE per-sheet subtotals — multiplier NOT applied, no waste, no materials"
},
"totals": {
"type": "object",
"properties": {
"total_sf": {
"type": "number"
},
"total_sf_net": {
"type": "number"
},
"lf": {
"type": "number"
},
"lf_net": {
"type": "number"
},
"ea": {
"type": "number"
},
"sy_net": {
"type": "number"
}
},
"required": [
"total_sf",
"total_sf_net",
"lf",
"lf_net",
"ea",
"sy_net"
],
"additionalProperties": true
},
"materials": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"unit": {
"type": "string"
},
"qty": {
"type": "number"
}
},
"required": [
"name",
"unit",
"qty"
],
"additionalProperties": true
},
"description": "Project-wide buy list — condition rows summed by (name, unit)"
},
"markups": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {}
}
},
"rfis": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {}
}
},
"condition_columns": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {}
}
},
"shape_labels": {
"type": "array",
"items": {
"type": "string"
}
},
"by_label": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {}
}
},
"units": {
"type": "string"
},
"display_units": {
"type": "string"
},
"roll_goods": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {}
},
"description": "Roll-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)"
}
},
"required": [
"schema",
"project_name",
"generated_with",
"sheets",
"conditions",
"by_sheet",
"totals",
"materials",
"markups",
"rfis",
"condition_columns",
"shape_labels",
"by_label",
"units",
"display_units",
"roll_goods"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
export_takeoffThe 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.Input schema{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "File path to write the payload to"
},
"overwrite": {
"type": "boolean",
"description": "Replace the file at path even when it is not an OpenTakeoff export. Off by default: re-exporting over a previous export of your own already overwrites without this, so you only need it to deliberately destroy an unrelated file."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"schema": {
"type": "string"
},
"project_name": {
"type": "string"
},
"units": {
"type": "string"
},
"sheets": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet_id": {
"type": "string"
},
"units_per_px": {
"type": "number"
}
},
"required": [
"sheet_id",
"units_per_px"
],
"additionalProperties": false
}
},
"conditions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"finish_tag": {
"type": "string"
},
"color": {
"type": "string"
},
"fill": {
"type": "string"
},
"hatch": {
"type": "string"
},
"multiplier": {
"type": "number"
},
"waste_pct": {
"type": "number"
},
"materials": {
"type": "array",
"items": {}
}
},
"required": [
"id",
"finish_tag",
"color",
"fill",
"hatch",
"multiplier",
"waste_pct",
"materials"
],
"additionalProperties": true
}
},
"shapes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"sheet_id": {
"type": "string"
},
"condition_id": {
"type": "string"
},
"measure_role": {
"type": "string",
"enum": [
"floor_area",
"deduct",
"linear",
"surface_area",
"count"
]
},
"verts_norm": {
"type": "array",
"items": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"description": "Vertices normalized to sheet dims (0–1)"
},
"computed": {
"type": "object",
"properties": {
"area_sf": {
"type": "number"
},
"perimeter_lf": {
"type": "number"
},
"count": {
"type": "number"
}
},
"additionalProperties": true,
"description": "count shapes carry {count} alone; every other role carries area_sf + perimeter_lf"
},
"origin": {
"type": "object",
"properties": {},
"additionalProperties": true,
"description": "Provenance: method (manual|one_click_v1), actor (omitted=human, 'agent'=MCP/automation), reviewed (human affirmed at an explicit gate), assignment (where the finish tag came from — {source: 'schedule', room_tag, surface, schedule_sheet} when the room's own schedule row decided it, {source: 'asserted'} when the agent chose; stamped on every agent commit), and correction fields (edited, edited_before_create, copied, proposed_verts_norm, edits)"
}
},
"required": [
"id",
"sheet_id",
"condition_id",
"measure_role",
"verts_norm",
"computed"
],
"additionalProperties": true
}
},
"markups": {
"type": "array",
"items": {}
},
"approvals": {
"type": "array",
"items": {},
"description": "Approval-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's own convention), so a verdict-free export stays byte-identical"
},
"sheet_group": {
"type": "array",
"items": {}
},
"last_group": {
"type": "array",
"items": {}
},
"sheet_tabs": {
"type": "array",
"items": {}
},
"sheet_levels": {
"type": "object",
"properties": {},
"additionalProperties": true
}
},
"required": [
"schema",
"project_name",
"units",
"sheets",
"conditions",
"shapes",
"markups",
"sheet_group",
"last_group",
"sheet_tabs",
"sheet_levels"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
find_scheduleLocate 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"; a table with delta/REV-marked rows says how many in "revised_rows". 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.Input schema{
"type": "object",
"properties": {
"kind": {
"type": "string",
"description": "\"room finish\" (rooms → surface finishes) or \"finish\"/\"material\" (codes → products)"
}
},
"required": [
"kind"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"matches": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"kind": {
"type": "string"
},
"title": {
"type": "string"
},
"rows": {
"type": "integer",
"description": "Total data rows — a continued schedule counts every fragment's rows"
},
"headers": {
"type": "array",
"items": {
"type": "string"
}
},
"region": {
"type": "object",
"properties": {
"x0": {
"type": "number"
},
"y0": {
"type": "number"
},
"x1": {
"type": "number"
},
"y1": {
"type": "number"
}
},
"required": [
"x0",
"y0",
"x1",
"y1"
],
"additionalProperties": false,
"description": "Pass to view_sheet to look at the table (the BASE fragment's region when the table continues)"
},
"building": {
"type": "string",
"description": "The building this table answers for, when its title or sheet names one"
},
"rotated_headers": {
"type": "boolean",
"description": "true when the column headers were read at a quarter-turn"
},
"revised_rows": {
"type": "integer",
"description": "Rows carrying a delta/REV marker — the ink changed there; resolve those tags to see which"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"title": {
"type": "string"
},
"rows": {
"type": "integer"
},
"region": {
"type": "object",
"properties": {
"x0": {
"$ref": "#/properties/matches/items/properties/region/properties/x0"
},
"y0": {
"$ref": "#/properties/matches/items/properties/region/properties/y0"
},
"x1": {
"$ref": "#/properties/matches/items/properties/region/properties/x1"
},
"y1": {
"$ref": "#/properties/matches/items/properties/region/properties/y1"
}
},
"required": [
"x0",
"y0",
"x1",
"y1"
],
"additionalProperties": false
}
},
"required": [
"sheet",
"title",
"rows",
"region"
],
"additionalProperties": false
},
"description": "Present when the table CONTINUES across sheets ('… SCHEDULE — CONT'D'): every fragment, base first, each with its own viewable region"
}
},
"required": [
"sheet",
"kind",
"title",
"rows",
"headers",
"region"
],
"additionalProperties": false
}
}
},
"required": [
"matches"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
find_textLOCATE 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.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"q": {
"type": "string",
"minLength": 1,
"description": "Text to find — a room number ('134'), a label fragment ('RECEPTION'), a schedule tag ('CPT-1')"
},
"region": {
"type": "object",
"properties": {
"x0": {
"type": "number"
},
"y0": {
"type": "number"
},
"x1": {
"type": "number"
},
"y1": {
"type": "number"
}
},
"required": [
"x0",
"y0",
"x1",
"y1"
],
"additionalProperties": false,
"description": "Rect in image px (origin top-left, y down); omit for the full sheet"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 2000,
"default": 200,
"description": "Max hits returned"
}
},
"required": [
"sheet",
"q"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"q": {
"type": "string"
},
"count": {
"type": "integer",
"description": "Total matches before the limit cap"
},
"truncated": {
"type": "boolean",
"description": "true = count exceeds hits.length; narrow the region or raise limit"
},
"hits": {
"type": "array",
"items": {
"type": "object",
"properties": {
"str": {
"type": "string",
"description": "The matched pdf.js text run, verbatim (may be shorter than the full label — runs aren't merged into lines)"
},
"bbox": {
"type": "array",
"minItems": 4,
"maxItems": 4,
"items": [
{
"type": "number"
},
{
"type": "number"
},
{
"type": "number"
},
{
"type": "number"
}
],
"description": "[x0, y0, x1, y1] image px"
},
"center": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "Bbox center, image px — feed straight into one_click's seed"
}
},
"required": [
"str",
"bbox",
"center"
],
"additionalProperties": false
}
}
},
"required": [
"sheet",
"q",
"count",
"truncated",
"hits"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
import_takeoffThe 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.Input schema{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to a takeoff_canvas.v1 JSON file on disk"
}
},
"required": [
"path"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"file": {
"type": "string",
"description": "Basename of the imported file"
},
"replaced": {
"type": "boolean",
"description": "true = the session was empty and adopted the file wholesale"
},
"shapes_added": {
"type": "integer"
},
"shapes_pending": {
"type": "integer",
"description": "Of the added shapes, how many are unreviewed machine pencil"
},
"conditions_merged": {
"type": "integer",
"description": "Imported conditions that joined an existing finish tag (its knobs won)"
},
"conditions_added": {
"type": "integer"
},
"scales_adopted": {
"type": "integer",
"description": "Sheets whose calibration came from the file (this session's own always wins)"
},
"unknown_files": {
"type": "array",
"items": {
"type": "string"
},
"description": "Files referenced by imported shapes that this document doesn't have — they count in totals but can't be viewed here"
},
"rules_imported": {
"type": "integer",
"description": "Correction rules (#88) that arrived with the file — apply_rules re-runs them"
},
"shapes_total": {
"type": "integer"
},
"note": {
"type": "string"
}
},
"required": [
"file",
"replaced",
"shapes_added",
"shapes_pending",
"conditions_merged",
"conditions_added",
"scales_adopted",
"unknown_files",
"rules_imported",
"shapes_total",
"note"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
link_annotationAttach 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.Input schema{
"type": "object",
"properties": {
"annotation_id": {
"type": "string",
"description": "Id from annotate or list_annotations"
},
"condition": {
"type": "string",
"description": "Finish tag to attach to; empty string detaches"
}
},
"required": [
"annotation_id",
"condition"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"condition": {
"type": "string"
},
"condition_id": {
"type": "string"
},
"note": {
"type": "string"
}
},
"required": [
"id",
"condition",
"note"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
list_annotationsEvery 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.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string",
"description": "Only annotations on this sheet"
},
"condition": {
"type": "string",
"description": "Only annotations attached to this finish tag"
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"annotations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"sheet": {
"type": "string"
},
"type": {
"type": "string"
},
"text": {
"type": "string"
},
"condition": {
"type": "string",
"description": "Resolved finish tag, or '' when unattached — saves joining against conditions[]"
},
"condition_id": {
"type": "string"
},
"at": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"target": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"rect": {
"type": "array",
"items": {
"anyOf": [
{
"not": {}
},
{
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
}
]
}
},
"from": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "Arrow tail / dimension start (image px)"
},
"to": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "Arrow head / dimension end (image px)"
},
"r": {
"type": "number",
"description": "Bubble radius (image px)"
},
"length_lf": {
"type": "number",
"description": "Dimension only: the measured length in real feet, snapshotted at annotate time from the sheet scale"
}
},
"required": [
"id",
"sheet",
"type",
"text",
"condition",
"condition_id"
],
"additionalProperties": false
}
},
"count": {
"type": "integer"
},
"unattached": {
"type": "integer",
"description": "How many carry no condition — candidates for link_annotation"
},
"verdicts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"actor": {
"type": "string",
"enum": [
"estimator",
"agent"
],
"description": "\"estimator\" = the human APPROVED ring (ink — import-borne here, never minted over MCP), \"agent\" = the AGENT diamond"
},
"sheet": {
"type": "string"
},
"at": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "Render anchor (image px) — absent only when the record rides a sheet from a file this session hasn't loaded (#152)"
},
"ts": {
"type": "string",
"description": "ISO-8601 mint time"
},
"shape_id": {
"type": "string",
"description": "Present when the verdict targets a committed shape — WHAT was marked, not where it draws"
},
"condition": {
"type": "string",
"description": "The targeted shape's finish tag, resolved — '' for sheet-point marks"
},
"text": {
"type": "string",
"description": "The optional short note riding the record"
}
},
"required": [
"id",
"actor",
"sheet",
"condition"
],
"additionalProperties": false
},
"description": "Approval-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)"
},
"verdict_count": {
"type": "integer"
}
},
"required": [
"annotations",
"count",
"unattached",
"verdicts",
"verdict_count"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
list_shapesThe mid-session shape inventory (#149): every committed shape's id, sheet, condition tag, role, quantities, room label, 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.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string",
"description": "Only shapes on this sheet"
},
"condition": {
"type": "string",
"description": "Only shapes under this finish tag (must exist)"
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"shapes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"sheet": {
"type": "string"
},
"condition": {
"type": "string"
},
"measure_role": {
"type": "string",
"enum": [
"floor_area",
"deduct",
"linear",
"surface_area",
"count"
]
},
"area_sf": {
"type": "number"
},
"perimeter_lf": {
"type": "number"
},
"count": {
"type": "number"
},
"height_ft": {
"type": "number",
"description": "surface_area shapes — the height they were quantified at"
},
"label": {
"type": "string",
"description": "The room (or phase/area) this shape belongs to — detect_rooms stamps the room number it traced from; edit_shape sets or clears it. Absent when unlabeled"
},
"nverts": {
"type": "integer"
},
"reviewed": {
"type": "boolean",
"description": "true = human-affirmed ink, refused by every agent mutation"
},
"assignment": {
"type": "string",
"enum": [
"schedule",
"asserted"
],
"description": "Where the finish tag came from: \"schedule\" = resolved from the room's own schedule row, \"asserted\" = the agent chose it. origin.assignment in export_takeoff carries the citation. Absent on human canvas shapes"
},
"agent_edits": {
"type": "integer",
"description": "Present when the agent has revised this shape"
}
},
"required": [
"id",
"sheet",
"condition",
"measure_role",
"nverts",
"reviewed"
],
"additionalProperties": false
}
},
"count": {
"type": "integer"
}
},
"required": [
"shapes",
"count"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
load_planOpen 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.Input schema{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to a plan PDF on disk"
},
"merge": {
"type": "boolean",
"description": "true = ADD this document to the working set, keeping all existing work (merge into an empty session is just a load)"
}
},
"required": [
"path"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"file": {
"type": "string",
"description": "The document just loaded (basename)"
},
"files": {
"type": "array",
"items": {
"type": "string"
},
"description": "Every document in the working set, load order (#152 — one entry unless merge was used)"
},
"page_count": {
"type": "integer",
"description": "Total sheets across the working set"
},
"sheets": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet": {
"type": "string",
"description": "Sheet key: page 1 is the bare file name (\"plan.pdf\"), pages 2+ are \"plan.pdf#2\""
},
"page": {
"type": "integer",
"description": "1-based page number"
},
"width_pt": {
"type": "number"
},
"height_pt": {
"type": "number"
},
"width_px": {
"type": "number",
"description": "Image px at render scale 2.0 — the coordinate space every tool speaks"
},
"height_px": {
"type": "number"
},
"sheet_number": {
"type": "string",
"description": "Title-block sheet number (\"A-101\") where detected"
},
"detected_scale": {
"type": "string",
"description": "Drawn scale note read off the sheet — a suggestion, never auto-applied"
}
},
"required": [
"sheet",
"page",
"width_pt",
"height_pt",
"width_px",
"height_px"
],
"additionalProperties": false
},
"description": "EVERY sheet in the working set, not just the file loaded by this call"
},
"note": {
"type": "string"
}
},
"required": [
"file",
"files",
"page_count",
"sheets",
"note"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
mark_verdictMark 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.Input schema{
"type": "object",
"properties": {
"shape_id": {
"type": "string",
"description": "Mark a committed shape (list_shapes has the ids) — anchored on the shape, recorded as provenance. Exactly one target: this OR sheet + at"
},
"sheet": {
"type": "string",
"description": "Sheet-point mode: the sheet, together with at"
},
"at": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "Sheet-point mode: where the AGENT diamond renders (image px)"
},
"text": {
"type": "string",
"description": "Optional short note riding the record and every export — the glyph always reads AGENT"
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The minted record id (\"apr-…\")"
},
"actor": {
"type": "string",
"const": "agent",
"description": "Always agent — this tool is structurally incapable of minting the estimator's seal"
},
"sheet": {
"type": "string"
},
"at": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "Where the AGENT diamond renders (image px) — absent only when the marked shape rides a sheet from a file this session hasn't loaded (#152)"
},
"ts": {
"type": "string",
"description": "ISO-8601 mint time"
},
"shape_id": {
"type": "string",
"description": "Shape mode: the committed shape this verdict is about"
},
"condition": {
"type": "string",
"description": "Shape mode: the marked shape's finish tag, resolved"
},
"text": {
"type": "string"
},
"note": {
"type": "string"
}
},
"required": [
"id",
"actor",
"sheet",
"ts",
"note"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
measure_lineMeasure 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.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"pts": {
"type": "array",
"items": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"minItems": 2
},
"condition": {
"type": "string"
}
},
"required": [
"sheet",
"pts"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"length_lf": {
"type": "number"
},
"npts": {
"type": "integer"
},
"shape_id": {
"type": "string",
"description": "Present when condition was passed and the shape committed"
}
},
"required": [
"length_lf",
"npts"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
measure_polygonMeasure 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.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"verts": {
"type": "array",
"items": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"minItems": 3
},
"condition": {
"type": "string"
},
"role": {
"type": "string",
"enum": [
"floor_area",
"deduct"
],
"default": "floor_area"
}
},
"required": [
"sheet",
"verts"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"area_sf": {
"type": "number"
},
"perimeter_lf": {
"type": "number"
},
"nverts": {
"type": "integer"
},
"shape_id": {
"type": "string",
"description": "Present when condition was passed and the shape committed"
},
"warning": {
"type": "string",
"description": "Mixed-scale warning (#153): a scale note disagreeing with the sheet's sits in the measured region — verify before trusting these numbers"
}
},
"required": [
"area_sf",
"perimeter_lf",
"nverts"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
measure_surfaceSurface 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.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"pts": {
"type": "array",
"items": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"minItems": 2,
"description": "The wall run, an open polyline (image px)"
},
"condition": {
"type": "string",
"description": "Finish tag to commit under (minted on first use), e.g. 'CT-W1'"
},
"height_ft": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Wall height in feet — written to the condition's H knob first, then used"
}
},
"required": [
"sheet",
"pts",
"condition"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"condition": {
"type": "string"
},
"height_ft": {
"type": "number",
"description": "The height this shape was quantified at (snapshotted on the shape)"
},
"length_lf": {
"type": "number",
"description": "The traced run's open length"
},
"area_sf": {
"type": "number",
"description": "length_lf × height_ft — the wall SF committed"
},
"npts": {
"type": "integer"
},
"shape_id": {
"type": "string"
}
},
"required": [
"condition",
"height_ft",
"length_lf",
"area_sf",
"npts",
"shape_id"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
one_clickOne-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 committing, LOOK at what landed — view_sheet {overlay: true} — and fix an overshot ring with edit_shape before trusting any total. 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{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"x": {
"type": "number"
},
"y": {
"type": "number"
},
"condition": {
"type": "string",
"description": "Finish tag to commit under (minted on first use)"
},
"role": {
"type": "string",
"enum": [
"floor_area",
"deduct"
],
"default": "floor_area"
},
"return_verts": {
"type": "boolean",
"default": false,
"description": "Include the traced polygon's vertices (image px)"
},
"sensitivity": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Fill 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 stops short at hatching INSIDE the room; verify the grown ring with view_sheet overlay before committing"
},
"layers": {
"type": "object",
"properties": {
"include": {
"type": "array",
"items": {
"type": "string"
},
"description": "Layer names or ids whose ink must plot as HARD boundary"
},
"exclude": {
"type": "array",
"items": {
"type": "string"
},
"description": "Layer names or ids whose ink must not block the flood at all"
}
},
"additionalProperties": false,
"description": "Override the sheet's classified layer roles for THIS call (see sheet_info.layers)"
}
},
"required": [
"sheet",
"x",
"y"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"status": {
"type": "string",
"const": "ok"
},
"nverts": {
"type": "integer",
"description": "Vertex count of the traced polygon"
},
"confidence": {
"type": "number",
"description": "0..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 verification: 1.0 means every signal came back clean, never that the trace is right. A low score is a view_sheet {overlay:true} audit prompt, not a fact to bid from"
},
"confidence_factors": {
"type": "array",
"items": {
"type": "string"
},
"description": "The 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"
},
"gap_sealed_px": {
"type": "number",
"description": "Present 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 is synthetic, and confidence deducts by that share; rides origin.gap_sealed_px on the committed shape"
},
"min_pass_px": {
"type": "number",
"description": "The 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"
},
"min_pass_delta": {
"type": "number",
"description": "Fraction 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"
},
"door_wedges": {
"type": "integer",
"description": "Door-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"
},
"ring_interiors": {
"type": "integer",
"description": "Of 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"
},
"hatch_filtered": {
"type": "boolean",
"const": true,
"description": "Present when hatch/pattern linework was classified out of the boundary"
},
"gap_bridged_px": {
"type": "number",
"description": "Present 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"
},
"raster_traced": {
"type": "boolean",
"const": true,
"description": "Present 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 on the committed shape; a raster ring's corners are unsnapped (a scan has no true endpoints), so audit it with view_sheet overlay before trusting the total"
},
"verts": {
"type": "array",
"items": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"description": "Traced polygon vertices (image px), when return_verts was set"
},
"area_sf": {
"type": "number",
"description": "Scaled mode: traced area in SF"
},
"perimeter_lf": {
"type": "number",
"description": "Scaled mode: traced perimeter in LF"
},
"shape_id": {
"type": "string",
"description": "Scaled mode: id of the committed shape, when condition was passed"
},
"area_px2": {
"type": "number",
"description": "Preview mode (no scale): raw area in px²"
},
"perimeter_px": {
"type": "number",
"description": "Preview mode (no scale): raw perimeter in px"
},
"warning": {
"type": "string",
"description": "Preview 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 viewport likely)"
}
},
"required": [
"status",
"nverts"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
place_countCount 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.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"points": {
"type": "array",
"items": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"minItems": 1,
"description": "Marker positions (image px), one committed count shape each"
},
"condition": {
"type": "string",
"description": "Finish tag to commit under (minted on first use), e.g. 'TR-1'"
}
},
"required": [
"sheet",
"points",
"condition"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"committed": {
"type": "integer",
"description": "Count shapes committed by this call — one per point"
},
"shape_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"condition": {
"type": "string"
},
"ea_total": {
"type": "number",
"description": "The condition's total EA after this call"
}
},
"required": [
"committed",
"shape_ids",
"condition",
"ea_total"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
read_sheet_textThe 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.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"region": {
"type": "object",
"properties": {
"x0": {
"type": "number"
},
"y0": {
"type": "number"
},
"x1": {
"type": "number"
},
"y1": {
"type": "number"
}
},
"required": [
"x0",
"y0",
"x1",
"y1"
],
"additionalProperties": false
}
},
"required": [
"sheet"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"str": {
"type": "string"
},
"x": {
"type": "number"
},
"y": {
"type": "number"
}
},
"required": [
"str",
"x",
"y"
],
"additionalProperties": false
},
"description": "Positioned text items (image px)"
},
"text": {
"type": "string",
"description": "The items joined with spaces"
}
},
"required": [
"sheet",
"items",
"text"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
resolve_tagResolve 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. A delta triangle or REV tag on the answering row (or the plan bubble) rides the result as "revisions": the codes returned are the POST-revision answer, but the ink changed under that delta — view_sheet the marker's bbox and check the addendum before pricing. 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{
"type": "object",
"properties": {
"tag": {
"type": "string",
"description": "The 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)"
}
},
"required": [
"tag"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"resolved",
"unresolved"
]
},
"tag": {
"type": "string"
},
"room": {
"anyOf": [
{
"type": "object",
"properties": {
"tag": {
"type": "string"
},
"name": {
"type": "string",
"description": "The name span stacked over the tag ('' when none)"
},
"sheet": {
"type": "string"
},
"bbox": {
"type": "object",
"properties": {
"x0": {
"type": "number"
},
"y0": {
"type": "number"
},
"x1": {
"type": "number"
},
"y1": {
"type": "number"
}
},
"required": [
"x0",
"y0",
"x1",
"y1"
],
"additionalProperties": false
},
"building": {
"type": "string",
"description": "The building the room belongs to, when the set names one — its plan sheet's BUILDING/BLDG context, or the tag's own qualifier ('A-134')"
},
"revision": {
"type": "object",
"properties": {
"rev": {
"type": "string"
},
"source": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"text": {
"type": "string"
},
"bbox": {
"$ref": "#/properties/room/anyOf/0/properties/bbox"
}
},
"required": [
"sheet",
"text",
"bbox"
],
"additionalProperties": false,
"description": "An evidence pointer — the sheet, the literal text, and where it sits (image px). Every edge in the graph carries one; pass the bbox to view_sheet to LOOK at the source."
},
"drawn": {
"type": "boolean",
"description": "true = a DRAWN delta: a bare digit inside a triangle of linework (the common CAD convention — the text layer carries only the digit; the geometry proved the triangle). The evidence bbox spans digit and triangle"
}
},
"required": [
"rev",
"source"
],
"additionalProperties": false,
"description": "A revision marker (delta triangle / 'REV 2' tag) attached to this item: the ink CHANGED under that revision. The value read is the post-revision answer — view_sheet the marker's bbox and check the addendum before pricing"
},
"corroboration": {
"type": "string",
"description": "Why this number is believed to be a room: \"schedule\" (a room-finish row answers for it), \"name\" (a name is drawn with it and the set has no room-finish schedule), or \"name+schedule\""
}
},
"required": [
"tag",
"name",
"sheet",
"bbox"
],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "The 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"
},
"building": {
"type": "string",
"description": "resolved only — the building whose schedule row answered, when the set names buildings"
},
"finishes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"surface": {
"type": "string",
"description": "The schedule column: FLOOR / BASE / WALL / …"
},
"code": {
"type": "string"
},
"source": {
"$ref": "#/properties/room/anyOf/0/properties/revision/properties/source"
},
"definition": {
"type": "object",
"properties": {
"cells": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"source": {
"$ref": "#/properties/room/anyOf/0/properties/revision/properties/source"
}
},
"required": [
"cells",
"source"
],
"additionalProperties": false,
"description": "The finish/material-schedule row this code chains to, when one exists"
}
},
"required": [
"surface",
"code",
"source"
],
"additionalProperties": false
}
},
"sources": {
"type": "array",
"items": {
"$ref": "#/properties/room/anyOf/0/properties/revision/properties/source"
},
"description": "The chain: plan tag → schedule row (the row cites the sheet that CARRIES it — under a continuation that is the CONT'D sheet)"
},
"revisions": {
"type": "array",
"items": {
"$ref": "#/properties/room/anyOf/0/properties/revision"
},
"description": "resolved only — delta/REV markers on the answering schedule row or the plan bubble. The finishes above are the POST-revision answer, but the ink changed: check the marker (view_sheet its bbox) and the addendum before pricing"
},
"reason": {
"type": "string",
"description": "unresolved 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"
},
"candidates": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"building": {
"type": "string"
},
"sheet": {
"type": "string"
},
"table": {
"type": "string"
}
},
"required": [
"key",
"sheet",
"table"
],
"additionalProperties": false
},
"description": "unresolved 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)"
}
},
"required": [
"status",
"tag",
"room"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
set_scaleSet 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.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"label": {
"type": "string",
"description": "A standard scale label, exactly as listed in the error on a miss"
},
"upp": {
"type": "number",
"description": "Real feet per image px at render scale 2.0"
},
"calibrate": {
"type": "object",
"properties": {
"p1": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"p2": {
"$ref": "#/properties/calibrate/properties/p1"
},
"feet": {
"type": "number"
}
},
"required": [
"p1",
"p2",
"feet"
],
"additionalProperties": false,
"description": "Two points (image px) a known real distance apart, and that distance in feet"
},
"use_detected": {
"type": "boolean",
"const": true,
"description": "true = adopt the sheet's detected scale"
}
},
"required": [
"sheet"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"upp": {
"type": "number",
"description": "Real feet per image px at render scale 2.0"
},
"label": {
"type": "string",
"description": "The standard scale label, when set by label or detected note"
},
"source": {
"type": "string",
"enum": [
"label",
"upp",
"calibrate",
"detected"
]
},
"confirmed": {
"type": "boolean",
"description": "Always false here: set_scale is the agent surface, and an agent-set scale stays UNCONFIRMED until a human confirms it in the canvas — quantities still flow, wearing the caveat"
},
"warning": {
"type": "string",
"description": "Present when the sheet carries MULTIPLE distinct scale notes (#153) — enlarged plans/details likely; region measurements under a disagreeing note will warn"
}
},
"required": [
"sheet",
"upp",
"source",
"confirmed"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
sheet_contextThe 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.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"region": {
"type": "object",
"properties": {
"x0": {
"type": "number"
},
"y0": {
"type": "number"
},
"x1": {
"type": "number"
},
"y1": {
"type": "number"
}
},
"required": [
"x0",
"y0",
"x1",
"y1"
],
"additionalProperties": false,
"description": "Rect in image px (origin top-left, y down); omit for the full sheet"
},
"min_len_px": {
"type": "number",
"minimum": 0,
"default": 2,
"description": "Drop segments shorter than this (default 2 — one PDF point at render scale 2.0, below any pen width). 0 keeps everything"
},
"max_segments": {
"type": "integer",
"minimum": 1,
"maximum": 20000,
"default": 4000,
"description": "Segment cap, applied longest-first (default 4000). The reply's dropped.cap says exactly what a smaller region would recover"
}
},
"required": [
"sheet"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"page": {
"type": "integer"
},
"sheet_px": {
"type": "array",
"items": {
"type": "number"
},
"minItems": 2,
"maxItems": 2
},
"region": {
"type": "array",
"items": {
"type": "number"
},
"minItems": 4,
"maxItems": 4,
"description": "The region actually resolved, post-clamp — pass this same rect to view_sheet and the render is in the same frame by construction"
},
"has_vector_linework": {
"type": "boolean",
"description": "false = a scan: vectors and hatch are empty because there are none, not because the region is blank"
},
"vectors": {
"type": "object",
"properties": {
"segments": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "number"
},
"minItems": 4,
"maxItems": 4
},
"description": "[x0, y0, x1, y1] per segment, image px, endpoints exactly as drawn — clipped by KEEPING whole intersecting segments, never by rewriting them"
},
"meta": {
"type": "array",
"items": {
"type": "integer"
},
"description": "One byte per segment, aligned with segments: bit 1 = curve chord, bit 2 = clip-only, bit 4 = filled-not-stroked; high nibble = device pen width"
},
"family": {
"type": "array",
"items": {
"type": [
"string",
"null"
]
},
"description": "Aligned with segments: the hatch-family id this segment belongs to, or null for structural linework"
},
"kept": {
"type": "integer"
},
"total_in_region": {
"type": "integer",
"description": "Segments intersecting the region before any decimation — kept + dropped always reconciles to this"
},
"truncated": {
"type": "boolean"
},
"dropped": {
"type": "object",
"properties": {
"short": {
"type": "integer",
"description": "Below min_len_px (invisible ink)"
},
"cap": {
"type": "integer",
"description": "Over max_segments — the SHORTEST went first, so walls survive"
}
},
"required": [
"short",
"cap"
],
"additionalProperties": false
},
"note": {
"type": "string"
}
},
"required": [
"segments",
"meta",
"family",
"kept",
"total_in_region",
"truncated",
"dropped"
],
"additionalProperties": false
},
"text": {
"type": "object",
"properties": {
"spans": {
"type": "array",
"items": {
"type": "object",
"properties": {
"str": {
"type": "string"
},
"x0": {
"type": "number"
},
"y0": {
"type": "number"
},
"x1": {
"type": "number"
},
"y1": {
"type": "number"
},
"rot": {
"type": "number",
"description": "Run direction in degrees, clockwise, y down — present only when rotated (90/270 = a quarter-turn, e.g. rotated schedule headers)"
}
},
"required": [
"str",
"x0",
"y0",
"x1",
"y1"
],
"additionalProperties": false
},
"description": "Text with bboxes, image px, same frame as the vectors"
},
"count": {
"type": "integer"
}
},
"required": [
"spans",
"count"
],
"additionalProperties": false
},
"hatch": {
"type": "object",
"properties": {
"families": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Content hash of the quantized (angle, pitch, pen-width) signature — the SAME id for the same pattern spec anywhere on the sheet, so legend↔plan matching is id === id. Identifies a pattern, not a material; the legend maps pattern → material."
},
"angle_deg": {
"type": "number",
"description": "Raw mean angle [0, 180) — rides beside the id for tolerance matching at bucket boundaries"
},
"pitch_px": {
"type": "number",
"description": "Raw median row pitch, image px"
},
"pen_w_px": {
"type": "integer",
"description": "Modal device pen width of the members"
},
"rows": {
"type": "integer"
},
"segments": {
"type": "integer",
"description": "Member segments in the whole instance"
},
"segments_in_region": {
"type": "integer",
"description": "…of which this many were returned in vectors (post-decimation)"
},
"bbox": {
"type": "array",
"items": {
"type": "number"
},
"minItems": 4,
"maxItems": 4,
"description": "The instance's tight bbox [x0, y0, x1, y1], image px"
}
},
"required": [
"id",
"angle_deg",
"pitch_px",
"pen_w_px",
"rows",
"segments",
"segments_in_region",
"bbox"
],
"additionalProperties": false
}
},
"count": {
"type": "integer"
}
},
"required": [
"families",
"count"
],
"additionalProperties": false
}
},
"required": [
"sheet",
"page",
"sheet_px",
"region",
"has_vector_linework",
"vectors",
"text",
"hatch"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
sheet_graphThe 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 number CORROBORATED as a room (with the stacked room NAME when one exists, the room's BUILDING on multi-building sets, and "corroboration" saying why it counts as a room) plus "unmatched_tags" — the numbers that are NOT rooms (keynote hexagons, detail markers, dimension fragments, legend rows), each with a reason, listed and never dropped; READ those reasons, one of them may be a room the schedule left out, the detail callouts (3/A-601 → sheet edges), the set's building designators, every REVISION marker the set carries (text markers "Δ2"/"REV 2" AND drawn deltas — a bare digit inside a triangle of linework, proven from vector geometry and flagged drawn — in "revisions", and attached to the schedule row / room tag they sit on), 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{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {}
}Output schema{
"type": "object",
"properties": {
"available": {
"type": "boolean",
"description": "false = the set has no text layer (a scan) — the graph degrades to unavailable, never half-populates"
},
"sheets": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"role": {
"type": "string",
"enum": [
"plan",
"schedule",
"legend",
"detail",
"elevation",
"demolition",
"unknown"
]
},
"confidence": {
"type": "number",
"description": "0..1; mixed title signals halve it, a bare sheet-number convention stays under 0.5"
},
"evidence": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"text": {
"type": "string"
},
"bbox": {
"type": "object",
"properties": {
"x0": {
"type": "number"
},
"y0": {
"type": "number"
},
"x1": {
"type": "number"
},
"y1": {
"type": "number"
}
},
"required": [
"x0",
"y0",
"x1",
"y1"
],
"additionalProperties": false
}
},
"required": [
"sheet",
"text",
"bbox"
],
"additionalProperties": false,
"description": "An evidence pointer — the sheet, the literal text, and where it sits (image px). Every edge in the graph carries one; pass the bbox to view_sheet to LOOK at the source."
},
"building": {
"type": "string",
"description": "The sheet's building context, when it names exactly one (BUILDING A / BLDG 2)"
},
"schedules": {
"type": "array",
"items": {
"type": "object",
"properties": {
"kind": {
"type": "string"
},
"title": {
"type": "string"
},
"rows": {
"type": "integer"
},
"region": {
"$ref": "#/properties/sheets/items/properties/evidence/properties/bbox"
},
"continues": {
"type": "string",
"description": "Present on a continuation fragment ('… SCHEDULE — CONT'D'): the sheet carrying the table's base fragment. The fragments read as ONE table — resolve_tag and find_schedule already see the union"
},
"rotated_headers": {
"type": "boolean",
"description": "true when the column headers were read at a quarter-turn"
}
},
"required": [
"kind",
"title",
"rows",
"region"
],
"additionalProperties": false
}
}
},
"required": [
"sheet",
"role",
"confidence",
"schedules"
],
"additionalProperties": false
}
},
"rooms": {
"type": "array",
"items": {
"type": "object",
"properties": {
"tag": {
"type": "string"
},
"name": {
"type": "string",
"description": "The name span stacked over the tag ('' when none)"
},
"sheet": {
"type": "string"
},
"bbox": {
"$ref": "#/properties/sheets/items/properties/evidence/properties/bbox"
},
"building": {
"type": "string",
"description": "The building the room belongs to, when the set names one — its plan sheet's BUILDING/BLDG context, or the tag's own qualifier ('A-134')"
},
"revision": {
"type": "object",
"properties": {
"rev": {
"type": "string"
},
"source": {
"$ref": "#/properties/sheets/items/properties/evidence"
},
"drawn": {
"type": "boolean",
"description": "true = a DRAWN delta: a bare digit inside a triangle of linework (the common CAD convention — the text layer carries only the digit; the geometry proved the triangle). The evidence bbox spans digit and triangle"
}
},
"required": [
"rev",
"source"
],
"additionalProperties": false,
"description": "A revision marker (delta triangle / 'REV 2' tag) attached to this item: the ink CHANGED under that revision. The value read is the post-revision answer — view_sheet the marker's bbox and check the addendum before pricing"
},
"corroboration": {
"type": "string",
"description": "Why this number is believed to be a room: \"schedule\" (a room-finish row answers for it), \"name\" (a name is drawn with it and the set has no room-finish schedule), or \"name+schedule\""
}
},
"required": [
"tag",
"name",
"sheet",
"bbox"
],
"additionalProperties": false
},
"description": "Numbers CORROBORATED as rooms — a room-finish row answers for them, or (where the set carries no room-finish schedule) a room name is drawn with them. Each says which in `corroboration`. Schedule sheets contribute rows, never phantom rooms"
},
"unmatched_tags": {
"type": "array",
"items": {
"type": "object",
"properties": {
"tag": {
"type": "string"
},
"sheet": {
"type": "string"
},
"bbox": {
"$ref": "#/properties/sheets/items/properties/evidence/properties/bbox"
},
"building": {
"type": "string"
},
"name": {
"type": "string",
"description": "Text drawn with the number, when there is any — on a keynote legend this is the accessory description, not a room name"
},
"reason": {
"type": "string",
"description": "WHY this number is not counted as a room. Read these: one of them may be a room the schedule left out, which is a hole in the bid"
}
},
"required": [
"tag",
"sheet",
"bbox",
"reason"
],
"additionalProperties": false
},
"description": "Numbered tags on plan sheets that are NOT counted as rooms — keynote hexagons, detail markers, dimension fragments, legend rows. Listed with a reason, never dropped. A real finish plan is covered in 2–3 digit numbers that are not rooms; counting them as rooms makes every one come back \"no schedule row\", which reads exactly like the lost-bid case and buries it"
},
"callouts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"detail": {
"type": "string"
},
"target_sheet": {
"type": "string"
},
"sheet": {
"type": "string"
},
"bbox": {
"$ref": "#/properties/sheets/items/properties/evidence/properties/bbox"
}
},
"required": [
"detail",
"target_sheet",
"sheet",
"bbox"
],
"additionalProperties": false
},
"description": "Detail callouts (3/A-601) — edges to their target sheets"
},
"buildings": {
"type": "array",
"items": {
"type": "string"
},
"description": "Every building designator the set names (sorted) — present only on multi-building-aware sets. Room numbers reused across these need qualified tags ('A-134')"
},
"revisions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"rev": {
"type": "string"
},
"sheet": {
"type": "string"
},
"bbox": {
"$ref": "#/properties/sheets/items/properties/evidence/properties/bbox"
},
"drawn": {
"type": "boolean"
}
},
"required": [
"rev",
"sheet",
"bbox"
],
"additionalProperties": false
},
"description": "Every delta-triangle / REV-tag marker the set carries — text markers ('Δ2', 'REV 2') and DRAWN deltas (a bare digit inside a triangle of linework, drawn: true) — where one sits, the ink changed under that revision. Markers on a schedule row or room bubble also attach there (and ride resolve_tag). A revision CLOUD is arc-chain linework these detectors do not read — absence here is not absence of revisions"
},
"notes": {
"type": "array",
"items": {
"type": "string"
},
"description": "Named gaps found while indexing (e.g. a continuation whose rows could not be aligned) — the graph refuses silently dropping anything"
},
"counts": {
"type": "object",
"properties": {
"rooms": {
"type": "integer"
},
"unmatched_tags": {
"type": "integer"
},
"schedules": {
"type": "integer",
"description": "LOGICAL tables — a schedule continued across sheets counts once"
},
"callouts": {
"type": "integer"
}
},
"required": [
"rooms",
"schedules",
"callouts"
],
"additionalProperties": false
}
},
"required": [
"available",
"sheets",
"rooms",
"callouts",
"counts"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
sheet_infoSheet 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.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string",
"description": "Sheet key (\"plan.pdf\", \"plan.pdf#2\") or title-block number (\"A-101\")"
}
},
"required": [
"sheet"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"sheet": {
"type": "string",
"description": "Sheet key: page 1 is the bare file name (\"plan.pdf\"), pages 2+ are \"plan.pdf#2\""
},
"page": {
"type": "integer",
"description": "1-based page number"
},
"width_pt": {
"type": "number"
},
"height_pt": {
"type": "number"
},
"width_px": {
"type": "number",
"description": "Image px at render scale 2.0 — the coordinate space every tool speaks"
},
"height_px": {
"type": "number"
},
"sheet_number": {
"type": "string",
"description": "Title-block sheet number (\"A-101\") where detected"
},
"detected_scale": {
"type": "string",
"description": "Drawn scale note read off the sheet — a suggestion, never auto-applied"
},
"seg_count": {
"type": "integer",
"description": "Vector segment count"
},
"has_vector_linework": {
"type": "boolean",
"description": "one_click needs vector linework"
},
"scale_set": {
"type": "boolean"
},
"upp": {
"type": "number",
"description": "Real feet per image px at render scale 2.0 — present once the scale is set"
},
"shape_count": {
"type": "integer",
"description": "Committed shapes on this sheet"
},
"multiple_scales": {
"type": "boolean",
"const": true,
"description": "Several DISTINCT scale notes on this sheet (#153) — enlarged plans/details likely"
},
"layers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Optional Content Group id — pass to one_click/detect_rooms layers.include/exclude"
},
"name": {
"type": "string",
"description": "The CAD layer name as exported (e.g. A-WALL-FULL)"
},
"role": {
"type": "string",
"enum": [
"boundary",
"finish-pattern",
"annotation",
"structure",
"demolition",
"unknown"
],
"description": "What this layer's linework IS to a takeoff (lib/layers.ts) — boundary/structure plot hard, pattern/annotation/demolition are excluded, unknown falls back to the hatch heuristics"
},
"confidence": {
"type": "number",
"description": "0..1 — how sure the name classifier is"
},
"visible": {
"type": "boolean",
"description": "Default-config visibility — a hidden layer's ink is excluded outright (or you trace demolition)"
},
"seg_count": {
"type": "integer",
"description": "Segments this layer owns on this sheet"
}
},
"required": [
"id",
"name",
"role",
"confidence",
"visible",
"seg_count"
],
"additionalProperties": false
},
"description": "The sheet's PDF layer table (#85) — [] when no Optional Content survived export (every engine path then runs the heuristics unchanged)"
}
},
"required": [
"sheet",
"page",
"width_pt",
"height_pt",
"width_px",
"height_px",
"seg_count",
"has_vector_linework",
"scale_set",
"shape_count",
"layers"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
split_conditionCut 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 — 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.Input schema{
"type": "object",
"properties": {
"condition": {
"type": "string",
"description": "Finish tag of the twin to split, e.g. 'CPT-1 – Level 2'"
}
},
"required": [
"condition"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"condition": {
"type": "string"
},
"condition_id": {
"type": "string"
},
"split": {
"type": "boolean",
"description": "false = it already owned its materials; nothing was following"
},
"frozen_rows": {
"type": "integer",
"description": "Following rows frozen at their current values"
},
"family_id": {
"type": "string",
"description": "Kept — it still groups with its siblings"
},
"note": {
"type": "string"
}
},
"required": [
"condition",
"condition_id",
"split",
"frozen_rows",
"note"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
sweep_schedule_rowTake 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 a confident zero. After committing, LOOK: view_sheet {overlay: true} over each swept sheet. 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{
"type": "object",
"properties": {
"tag": {
"type": "string",
"minLength": 1,
"description": "The schedule row's key exactly as drawn, e.g. 'T1', 'TR-2' — it becomes the condition tag on commit"
},
"commit": {
"type": "boolean",
"default": false,
"description": "Commit every counted match as one EA count marker (excluded/withheld/text_only never commit)"
},
"rotations": {
"type": "boolean",
"default": true,
"description": "Also match 90/180/270-rotated markers"
},
"mirror": {
"type": "boolean",
"default": true,
"description": "Also match mirrored markers"
},
"tolerance_px": {
"type": "number",
"exclusiveMinimum": 0,
"maximum": 20,
"default": 2,
"description": "Endpoint match tolerance in image px (default 2 — CAD jitter, not drift)"
}
},
"required": [
"tag"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"tag": {
"type": "string",
"description": "The row key as normalized (the tag as drawn)"
},
"row": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"table": {
"type": "string",
"description": "The table's title (or kind, when untitled)"
},
"key": {
"type": "string"
},
"cells": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "The row's cells, header → text — what the schedule SAYS this mark is"
},
"citation": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"text": {
"type": "string"
},
"bbox": {
"type": "object",
"properties": {
"x0": {
"type": "number"
},
"y0": {
"type": "number"
},
"x1": {
"type": "number"
},
"y1": {
"type": "number"
}
},
"required": [
"x0",
"y0",
"x1",
"y1"
],
"additionalProperties": false
}
},
"required": [
"sheet",
"text",
"bbox"
],
"additionalProperties": false,
"description": "An evidence pointer — the sheet, the literal text, and where it sits (image px). Every edge in the graph carries one; pass the bbox to view_sheet to LOOK at the source."
}
},
"required": [
"sheet",
"table",
"key",
"cells",
"citation"
],
"additionalProperties": false,
"description": "The schedule row the sweep was seeded from — the condition's source"
},
"anchor": {
"type": "object",
"properties": {
"sheet": {
"type": "string",
"description": "The plan sheet the fingerprint was anchored on"
},
"at": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "The anchoring tag occurrence's center (image px)"
},
"rect": {
"type": "array",
"items": {
"type": "number"
},
"minItems": 4,
"maxItems": 4,
"description": "The fingerprint rect actually used [x0, y0, x1, y1] — the pad ladder's winning step"
},
"segments": {
"type": "integer",
"description": "Vector segments in the marker fingerprint"
},
"length_px": {
"type": "number"
},
"corroborated": {
"type": "boolean",
"description": "true = the fingerprint recurred at a second tag occurrence before being trusted; false = the tag is drawn too sparsely to cross-check (see note)"
},
"occurrences": {
"type": "integer",
"description": "Drawn occurrences of the tag across all plan sheets"
}
},
"required": [
"sheet",
"at",
"rect",
"segments",
"length_px",
"corroborated",
"occurrences"
],
"additionalProperties": false
},
"found": {
"type": "integer",
"description": "Matches carrying the row's own tag — the honest count, across every plan sheet"
},
"sheets": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"found": {
"type": "integer"
},
"matches": {
"type": "array",
"items": {
"type": "object",
"properties": {
"at": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "The matched marker's centroid (image px)"
},
"score": {
"type": "number",
"description": "Length-weighted fraction of the anchor's segments matched within tolerance, 0..1"
},
"rotation": {
"type": "number",
"description": "Detected rotation in degrees (0 | 90 | 180 | 270)"
},
"mirrored": {
"type": "boolean"
},
"tag_at": {
"type": "object",
"properties": {
"x0": {
"$ref": "#/properties/row/properties/citation/properties/bbox/properties/x0"
},
"y0": {
"$ref": "#/properties/row/properties/citation/properties/bbox/properties/y0"
},
"x1": {
"$ref": "#/properties/row/properties/citation/properties/bbox/properties/x1"
},
"y1": {
"$ref": "#/properties/row/properties/citation/properties/bbox/properties/y1"
}
},
"required": [
"x0",
"y0",
"x1",
"y1"
],
"additionalProperties": false,
"description": "The corroborating tag text's bbox — the evidence that this marker is THIS row's"
}
},
"required": [
"at",
"score",
"rotation",
"mirrored",
"tag_at"
],
"additionalProperties": false
}
},
"withheld": {
"type": "array",
"items": {
"type": "object",
"properties": {
"at": {
"$ref": "#/properties/sheets/items/properties/matches/items/properties/at"
},
"score": {
"$ref": "#/properties/sheets/items/properties/matches/items/properties/score"
},
"rotation": {
"$ref": "#/properties/sheets/items/properties/matches/items/properties/rotation"
},
"mirrored": {
"$ref": "#/properties/sheets/items/properties/matches/items/properties/mirrored"
},
"reason": {
"type": "string"
}
},
"required": [
"at",
"score",
"rotation",
"mirrored",
"reason"
],
"additionalProperties": false
},
"description": "Questions, never counts: markers matching the geometry but carrying no tag (an unlabeled instance or a shared bubble shape), and near-miss scores in the [0.75, 0.92) band"
},
"excluded": {
"type": "array",
"items": {
"type": "object",
"properties": {
"at": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"tag": {
"type": "string"
}
},
"required": [
"at",
"tag"
],
"additionalProperties": false
},
"description": "Markers matching the geometry but labeled with a SIBLING row's tag — the bubble shape is shared across marks, so these belong to that row, not this one"
},
"text_only": {
"type": "array",
"items": {
"type": "object",
"properties": {
"at": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
}
},
"required": [
"at"
],
"additionalProperties": false
},
"description": "The tag drawn with NO matching marker geometry nearby — a note reference or a variant marker; a question, never a count"
},
"candidates": {
"type": "object",
"properties": {
"considered": {
"type": "integer"
},
"dropped": {
"type": "integer"
}
},
"required": [
"considered",
"dropped"
],
"additionalProperties": false
},
"complete": {
"type": "boolean",
"description": "True when every proposed placement on this sheet was scored — false means this sheet's count is a FLOOR, not a total (#261)"
},
"elapsed_ms": {
"type": "number",
"description": "Wall-clock for this sheet's sweep"
},
"scaled": {
"type": "object",
"properties": {
"ratio": {
"type": "number",
"description": "Seed-sheet px per target-sheet px, computed from the two sheets' own committed scales (upp_seed / upp_target) — stated, never scale-searched"
},
"segments": {
"type": "integer",
"description": "Fingerprint segments that survived the resize and were actually searched for"
},
"sub_pixel_dropped": {
"type": "integer",
"description": "Seed segments that fell below matchable length when scaled down — excluded from the score rather than depressing it, so a score here is a fraction of what survived, not of the whole seed"
},
"footprint_px": {
"type": "number",
"description": "The symbol's size on THIS sheet after the resize"
},
"tol_px": {
"type": "number",
"description": "The endpoint tolerance actually applied — it rides the ratio up when the seed is magnified (its drawn jitter magnifies too) and never down"
}
},
"required": [
"ratio",
"segments",
"sub_pixel_dropped",
"footprint_px",
"tol_px"
],
"additionalProperties": false,
"description": "#186: present only when the seed was resized for this sheet"
},
"scale_assumed": {
"type": "string",
"description": "#186: present when the true ratio is UNKNOWN (a scale is missing on the seed sheet or this one) and the sweep ran at 1:1 — an unstated ratio plus a zero count is not evidence of absence"
}
},
"required": [
"sheet",
"found",
"matches",
"withheld",
"excluded",
"text_only",
"candidates",
"complete",
"elapsed_ms"
],
"additionalProperties": false
},
"description": "One entry per swept PLAN-role sheet, load order"
},
"complete": {
"type": "boolean",
"description": "True when every proposed placement was scored on every swept sheet — false means at least one sheet's count is a FLOOR, not a total (#261)"
},
"skipped": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"role": {
"type": "string"
},
"reason": {
"type": "string"
}
},
"required": [
"sheet",
"role",
"reason"
],
"additionalProperties": false
},
"description": "Sheets excluded from counting (schedule/detail/legend/unknown), each with its reason"
},
"committed": {
"type": "integer",
"description": "commit mode: count shapes committed — one per counted match, the whole sweep ONE undo step"
},
"shape_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"condition": {
"type": "string",
"description": "commit mode: the condition minted FROM the row — its key is the tag"
},
"ea_total": {
"type": "number"
},
"note": {
"type": "string"
},
"warning": {
"type": "string",
"description": "Present when the per-sheet work cap dropped candidates"
}
},
"required": [
"tag",
"row",
"anchor",
"found",
"sheets",
"complete",
"skipped"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
symbol_sweepFind 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. 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 — some placements were never scored — so tighten the seed rect around more distinctive geometry rather than trusting it as a total. 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 `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 — 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 — 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 SEED instance is not in that count (#296) — in sheet scope it is almost always installed work, so pass commit_seed: true to mint it into the same batch (the reply reminds you whenever a sheet-scope commit leaves it out; ea_total one short of the hand tally is exactly this). The COUNT is scale-free (EA), but matching across sheets of different scales is not — set_scale on the sheets involved is what turns the ratio from an assumption into arithmetic. Counter-examples (#259): drafting reuses one generic shape for different devices — a wall-mounted data outlet drawn as a plain triangle, the flush-floor variant the SAME triangle inside a square, keynote callouts a triangle with a letter in it — so the seed legitimately matches things you do not mean, and seeding more geometry only works where the drawing offers more to capture. `exclude` takes rects around instances you do NOT mean, marqueed exactly like the seed. You never choose a mechanism; the rect's contents decide, because both are the same gesture: a rect holding EXTRA linework beyond the seed rejects placements where that extra linework is present too (the box, the letter), and a rect holding no extra linework of its own is read as the line running THROUGH it — a bare ceiling-grid tile whose grid line a real fixture, drawn over it, would BREAK. That second mechanic is not expressible as a seed: only segments fully INSIDE a rect define a symbol, and background structure is long by nature. Every rejection is disclosed in rejected[] — which negative, what fraction of its evidence was found, and the placement — and NEVER counted in found: an exclusion is a judgement, so look at it and reinstate any you disagree with using place_count at its `at`. A counter-example that holds no instance of the seed, or holds the seed with nothing extra, is REFUSED rather than silently doing nothing. Stroke luminance (#260): a flattened export strips the layer tree and flattens every pen, but the file still STATES stroke color — a black fixture outline over a grey ceiling grid is unambiguous there even when the geometry is identical (two empty 2 ft grid tiles reproduce a 2×4 fixture's outline exactly). luminance_tolerance (0–254) gates on it: a sheet segment only answers for a seed segment when their stroke luminances are within the stated tolerance (Rec. 709, 0 = black, 255 = white; 32–64 separates black from grey without touching anti-aliasing wobble). OPT-IN and disclosed, in the spirit of tolerance_px — omitted, sweeps score exactly as before; stated, the reply's lum_gate says the seed's own luminance band and names every placement the geometry would have committed and the pen did not, so you can LOOK at what a stated gate cost. Prefer geometry (a counter-example, a tighter seed) where the drawing offers it — color is the fallback for exports where nothing else survived. Labels (#308): for a LABELED family — fixtures, tagged equipment, keyed devices — the drawing already names every instance, and the sweep reads those names: a fixture token written beside a placement, or connected to it by a drawn leader line (leader-following arms only on multi-pen sheets, where the annotation pen separates from the work), comes back as `label` + `label_via` on the row, and the seed's own tag rides `seed.label`. Disclosure in both directions, never a recount: a committed match with NO label while the family is labeled was counted on shape alone (measured case: two 0.97 matches that were valve internals, not drains — LOOK at those first), a withheld row carrying the seed's own tag is the drawing vouching for a near-miss (look, then place_count), and a withheld row named a DIFFERENT tag is a sibling fixture answered, not a missed count. After any batch commit, LOOK at what landed — view_sheet {overlay: true} over the swept area — and audit the markers against the drawing before trusting the EA total. 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{
"type": "object",
"properties": {
"sheet": {
"type": "string",
"description": "The 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)"
},
"seed_rect": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
{
"$ref": "#/properties/seed_rect/items/0"
}
],
"description": "Marquee around ONE example instance, [[x0,y0],[x1,y1]] in image px — tight: segments fully inside define the symbol"
},
"condition": {
"type": "string",
"description": "Finish tag to commit match markers under (minted on first use), e.g. 'FD-1'. Required when commit is true"
},
"commit": {
"type": "boolean",
"default": false,
"description": "Commit every MATCH center as one EA count marker (withheld placements never commit)"
},
"commit_seed": {
"type": "boolean",
"default": false,
"description": "Sheet scope + commit only (#296): also commit the SEED instance — in sheet scope the seed is almost always installed work, and a count that excludes it bids one short. Joins the same one-undo-step batch, origin score 1. Refused in set scope, where a detail/legend seed is a reference drawing"
},
"scope": {
"type": "string",
"enum": [
"sheet",
"set"
],
"default": "sheet",
"description": "\"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)"
},
"rotations": {
"type": "boolean",
"default": true,
"description": "Also match 90/180/270-rotated placements"
},
"mirror": {
"type": "boolean",
"default": true,
"description": "Also match mirrored placements"
},
"tolerance_px": {
"type": "number",
"exclusiveMinimum": 0,
"maximum": 20,
"default": 2,
"description": "Endpoint match tolerance in image px (default 2 — CAD jitter, not drift)"
},
"exclude": {
"type": "array",
"items": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"$ref": "#/properties/seed_rect/items/0"
},
{
"$ref": "#/properties/seed_rect/items/0"
}
]
},
"description": "Counter-examples: rects around instances you do NOT mean, same gesture as seed_rect — 'count the triangles, not the keynote ones'. Marquee the LOOKALIKE ITSELF (the flush-floor variant with its box, the keynote triangle with its letter) or an EMPTY position whose background line a real instance would break (a bare ceiling grid tile). You never say which kind it is: the rect's own contents decide. Every rejection comes back in rejected[] with which negative did it and what it saw"
},
"luminance_tolerance": {
"type": "integer",
"minimum": 0,
"maximum": 254,
"description": "Stroke-luminance gate, 0–254 (#260): a sheet segment only answers for a seed segment when their stroke luminances (Rec. 709, 0 black – 255 white) are within this. For flattened exports where a black device and its grey background twin are geometrically identical — 32–64 separates black from grey. Omit to score on geometry alone; stated, the reply's lum_gate discloses the seed's luminance band and every placement the gate pulled under the commit bar"
}
},
"required": [
"sheet",
"seed_rect"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"scope": {
"type": "string",
"enum": [
"sheet",
"set"
],
"description": "\"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[])"
},
"found": {
"type": "integer",
"description": "Placements that cleared the commit bar — across every swept sheet in set scope"
},
"matches": {
"type": "array",
"items": {
"type": "object",
"properties": {
"at": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "The placed symbol's centroid (image px) — the point a commit places its count marker at"
},
"score": {
"type": "number",
"description": "Length-weighted fraction of the seed's segments matched within tolerance, 0..1"
},
"rotation": {
"type": "number",
"description": "Detected rotation in degrees (0 | 90 | 180 | 270)"
},
"mirrored": {
"type": "boolean"
},
"label": {
"type": "string",
"description": "The drawing's own tag for this placement (#308) — a fixture token written beside it or connected by a drawn leader (e.g. \"P-7\", \"FD1\"). Disclosure, never a recount: a match with NO label in a labeled family was counted on shape alone (look before trusting), and a withheld row carrying the seed's own tag is the drawing vouching for it"
},
"label_via": {
"type": "string",
"enum": [
"adjacent",
"leader"
],
"description": "How the tag reached this placement: written beside it, or followed along a drawn leader line (leader-following arms only on multi-pen sheets, where the annotation pen separates from the work)"
}
},
"required": [
"at",
"score",
"rotation",
"mirrored"
],
"additionalProperties": false
},
"description": "Sheet scope only. Deterministic reading order (y, then x). The seed's own location is never listed here"
},
"withheld": {
"type": "array",
"items": {
"type": "object",
"properties": {
"at": {
"$ref": "#/properties/matches/items/properties/at"
},
"score": {
"$ref": "#/properties/matches/items/properties/score"
},
"rotation": {
"$ref": "#/properties/matches/items/properties/rotation"
},
"mirrored": {
"$ref": "#/properties/matches/items/properties/mirrored"
},
"label": {
"$ref": "#/properties/matches/items/properties/label"
},
"label_via": {
"$ref": "#/properties/matches/items/properties/label_via"
},
"reason": {
"type": "string"
}
},
"required": [
"at",
"score",
"rotation",
"mirrored",
"reason"
],
"additionalProperties": false
},
"description": "Sheet 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"
},
"seed": {
"type": "object",
"properties": {
"sheet": {
"type": "string",
"description": "The sheet the seed rect was marqueed on"
},
"role": {
"type": "string",
"description": "Set scope: the seed sheet's graph role — a non-plan seed sheet is the fingerprint SOURCE and is excluded from counting"
},
"segments": {
"type": "integer",
"description": "Vector segments fully inside the seed rect — the fingerprint"
},
"center": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "The seed instance's own centroid (image px) — reported here, never double-committed as a match"
},
"rect": {
"type": "array",
"items": {
"type": "number"
},
"minItems": 4,
"maxItems": 4,
"description": "The seed rect actually used, post-clamp [x0, y0, x1, y1]"
},
"length_px": {
"type": "number",
"description": "Total seed linework length, image px"
},
"label": {
"type": "string",
"description": "The drawing's own tag for the seed instance (#308) — the family's identity, e.g. seeding a drain the sheet labels \"P-7\""
},
"label_via": {
"type": "string",
"enum": [
"adjacent",
"leader"
]
}
},
"required": [
"sheet",
"segments",
"center",
"rect",
"length_px"
],
"additionalProperties": false
},
"rejected": {
"type": "array",
"items": {
"type": "object",
"properties": {
"at": {
"$ref": "#/properties/matches/items/properties/at"
},
"score": {
"$ref": "#/properties/matches/items/properties/score"
},
"rotation": {
"$ref": "#/properties/matches/items/properties/rotation"
},
"mirrored": {
"$ref": "#/properties/matches/items/properties/mirrored"
},
"label": {
"$ref": "#/properties/matches/items/properties/label"
},
"label_via": {
"$ref": "#/properties/matches/items/properties/label_via"
},
"by": {
"type": "integer",
"description": "Which counter-example rejected it — 1-based index into the `exclude` rects you passed"
},
"mode": {
"type": "string",
"enum": [
"shape",
"crossing"
],
"description": "What that counter-example was read as. \"shape\": it carries extra linework the seed does not, and that linework is present here too. \"crossing\": it carries no extra linework of its own — what marks it is a line running THROUGH it, and that line runs unbroken through this placement"
},
"evidence": {
"type": "number",
"description": "Fraction of that counter-example's discriminating linework found at this placement, 0..1 (rejection bar 0.5)"
},
"reason": {
"type": "string"
}
},
"required": [
"at",
"score",
"rotation",
"mirrored",
"by",
"mode",
"evidence",
"reason"
],
"additionalProperties": false
},
"description": "Sheet scope only. Placements the geometry accepted and a counter-example refused (#259) — NEVER counted in found, and never silent: each says which negative did it and what it saw. Reinstate one by hand with place_count at its `at` if you disagree"
},
"negatives": {
"type": "array",
"items": {
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": [
"shape",
"crossing"
]
},
"segments": {
"type": "integer",
"description": "Discriminating segments this counter-example contributes — the linework that is NOT the seed"
},
"center": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"description": "Where the seed's own geometry was located inside that rect, image px — what the negative aligned to"
}
},
"required": [
"mode",
"segments",
"center"
],
"additionalProperties": false
},
"description": "What each `exclude` rect was read as, in the order you passed them (#259)"
},
"rejected_total": {
"type": "integer",
"description": "Set scope: placements counter-examples rejected across every swept sheet"
},
"seed_committed": {
"type": "boolean",
"description": "Present when commit_seed: true minted the seed instance into the batch (#296) — ea_total then includes it"
},
"lum_gate": {
"type": "object",
"properties": {
"tol": {
"type": "number",
"description": "The luminance tolerance that was applied, 0–254"
},
"seed_lum": {
"type": "array",
"items": {
"type": "number"
},
"description": "The seed's own stroke luminances, deduplicated — the band candidates were held to"
},
"rejected": {
"type": "integer",
"description": "Placements the geometry alone would have COMMITTED and the gate did not — one entry per physical spot"
},
"at": {
"type": "array",
"items": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"description": "Where each of them is, image px — view_sheet and look before trusting the gate; place_count reinstates one you disagree with"
}
},
"required": [
"tol",
"seed_lum",
"rejected",
"at"
],
"additionalProperties": false,
"description": "Sheet scope only. The stated stroke-luminance gate's accounting (#260): the tolerance, the seed's own luminance band, and every placement the geometry would have committed that the pen pulled under the bar — NEVER counted in found, never silent. Set scope accounts per sheet in sheets[]"
},
"candidates": {
"type": "object",
"properties": {
"considered": {
"type": "integer"
},
"dropped": {
"type": "integer",
"description": "Placements never scored because the work cap bit — always disclosed, never silent"
}
},
"required": [
"considered",
"dropped"
],
"additionalProperties": false,
"description": "Sheet scope only — set scope accounts per sheet in sheets[]"
},
"complete": {
"type": "boolean",
"description": "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 — acknowledge it before trusting found (#261)"
},
"sheets": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"found": {
"type": "integer"
},
"matches": {
"type": "array",
"items": {
"type": "object",
"properties": {
"at": {
"$ref": "#/properties/matches/items/properties/at"
},
"score": {
"$ref": "#/properties/matches/items/properties/score"
},
"rotation": {
"$ref": "#/properties/matches/items/properties/rotation"
},
"mirrored": {
"$ref": "#/properties/matches/items/properties/mirrored"
},
"label": {
"$ref": "#/properties/matches/items/properties/label"
},
"label_via": {
"$ref": "#/properties/matches/items/properties/label_via"
}
},
"required": [
"at",
"score",
"rotation",
"mirrored"
],
"additionalProperties": false
}
},
"withheld": {
"type": "array",
"items": {
"type": "object",
"properties": {
"at": {
"$ref": "#/properties/matches/items/properties/at"
},
"score": {
"$ref": "#/properties/matches/items/properties/score"
},
"rotation": {
"$ref": "#/properties/matches/items/properties/rotation"
},
"mirrored": {
"$ref": "#/properties/matches/items/properties/mirrored"
},
"label": {
"$ref": "#/properties/matches/items/properties/label"
},
"label_via": {
"$ref": "#/properties/matches/items/properties/label_via"
},
"reason": {
"type": "string"
}
},
"required": [
"at",
"score",
"rotation",
"mirrored",
"reason"
],
"additionalProperties": false
}
},
"rejected": {
"type": "array",
"items": {
"$ref": "#/properties/rejected/items"
},
"description": "Placements a counter-example rejected on this sheet (#259) — never counted, always named"
},
"lum_gate": {
"$ref": "#/properties/lum_gate",
"description": "This sheet's stated-luminance-gate accounting (#260) — present only when luminance_tolerance was stated"
},
"candidates": {
"type": "object",
"properties": {
"considered": {
"$ref": "#/properties/candidates/properties/considered"
},
"dropped": {
"$ref": "#/properties/candidates/properties/dropped"
}
},
"required": [
"considered",
"dropped"
],
"additionalProperties": false,
"description": "The work ceiling applies PER SHEET; dropped > 0 here names exactly where the count is incomplete"
},
"complete": {
"type": "boolean",
"description": "True when every proposed placement on this sheet was scored — false means this sheet's count is a FLOOR, not a total (#261)"
},
"elapsed_ms": {
"type": "number",
"description": "Wall-clock for this sheet's sweep"
},
"scaled": {
"type": "object",
"properties": {
"ratio": {
"type": "number",
"description": "Seed-sheet px per target-sheet px, computed from the two sheets' own committed scales (upp_seed / upp_target) — stated, never scale-searched"
},
"segments": {
"type": "integer",
"description": "Fingerprint segments that survived the resize and were actually searched for"
},
"sub_pixel_dropped": {
"type": "integer",
"description": "Seed segments that fell below matchable length when scaled down — excluded from the score rather than depressing it, so a score here is a fraction of what survived, not of the whole seed"
},
"footprint_px": {
"type": "number",
"description": "The symbol's size on THIS sheet after the resize"
},
"tol_px": {
"type": "number",
"description": "The endpoint tolerance actually applied — it rides the ratio up when the seed is magnified (its drawn jitter magnifies too) and never down"
}
},
"required": [
"ratio",
"segments",
"sub_pixel_dropped",
"footprint_px",
"tol_px"
],
"additionalProperties": false,
"description": "#186: present only when the seed was resized for this sheet"
},
"scale_assumed": {
"type": "string",
"description": "#186: present when the true ratio is UNKNOWN (a scale is missing on the seed sheet or this one) and the sweep ran at 1:1 — an unstated ratio plus a zero count is not evidence of absence"
}
},
"required": [
"sheet",
"found",
"matches",
"withheld",
"candidates",
"complete",
"elapsed_ms"
],
"additionalProperties": false
},
"description": "Set scope only: one entry per swept PLAN-role sheet, load order"
},
"skipped": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"role": {
"type": "string",
"description": "The sheet's graph role (plan / schedule / legend / detail / …)"
},
"reason": {
"type": "string"
}
},
"required": [
"sheet",
"role",
"reason"
],
"additionalProperties": false
},
"description": "Set scope only: every sheet excluded from counting, with role and reason — including the seed's own sheet when it is not a plan"
},
"committed": {
"type": "integer",
"description": "commit mode: count shapes committed — one per match"
},
"shape_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"condition": {
"type": "string",
"description": "commit mode: the finish tag the markers counted under"
},
"ea_total": {
"type": "number",
"description": "commit mode: the condition's total EA after this call"
},
"note": {
"type": "string"
},
"warning": {
"type": "string",
"description": "Present when the work cap dropped candidates — what a tighter seed rect would recover"
}
},
"required": [
"scope",
"found",
"seed",
"complete"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
takeoff_summaryPer-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{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {}
}Output schema{
"type": "object",
"properties": {
"conditions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"finish_tag": {
"type": "string"
},
"multiplier": {
"type": "number"
},
"waste_pct": {
"type": "number"
},
"shape_count": {
"type": "integer"
},
"floor_sf": {
"type": "number"
},
"wall_sf": {
"type": "number"
},
"border_sf": {
"type": "number"
},
"lf": {
"type": "number"
},
"ea": {
"type": "number"
},
"total_sf": {
"type": "number"
},
"floor_sf_net": {
"type": "number"
},
"wall_sf_net": {
"type": "number"
},
"border_sf_net": {
"type": "number"
},
"lf_net": {
"type": "number"
},
"total_sf_net": {
"type": "number"
},
"sy_net": {
"type": "number"
}
},
"required": [
"id",
"finish_tag",
"multiplier",
"waste_pct",
"shape_count",
"floor_sf",
"wall_sf",
"border_sf",
"lf",
"ea",
"total_sf",
"floor_sf_net",
"wall_sf_net",
"border_sf_net",
"lf_net",
"total_sf_net",
"sy_net"
],
"additionalProperties": true
}
},
"totals": {
"type": "object",
"properties": {
"total_sf": {
"type": "number"
},
"total_sf_net": {
"type": "number"
},
"lf": {
"type": "number"
},
"lf_net": {
"type": "number"
},
"ea": {
"type": "number"
},
"sy_net": {
"type": "number"
}
},
"required": [
"total_sf",
"total_sf_net",
"lf",
"lf_net",
"ea",
"sy_net"
],
"additionalProperties": true
},
"scale_unconfirmed": {
"type": "array",
"items": {
"type": "string"
},
"description": "Sheets whose scale is agent-set and no human has confirmed — these totals stand on an unverified scale; verify against a stated dimension or confirm in the canvas"
}
},
"required": [
"conditions",
"totals"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
undo_lastStep 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.Input schema{
"type": "object",
"properties": {
"n": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 1,
"description": "How many steps to reverse (1–100)"
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}Output schema{
"type": "object",
"properties": {
"undone": {
"type": "integer",
"description": "Steps actually reversed"
},
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"seq": {
"type": "integer"
},
"op": {
"type": "string",
"enum": [
"commit",
"edit",
"delete",
"materials",
"condition",
"approval",
"duplicate_condition",
"split_condition",
"cutout",
"cutout_restore",
"runcut"
]
},
"tool": {
"type": "string",
"description": "The tool call this step came from"
},
"shapes": {
"type": "integer",
"description": "Shapes affected by reversing this step — 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)"
}
},
"required": [
"seq",
"op",
"tool",
"shapes"
],
"additionalProperties": false
},
"description": "Newest first"
},
"shape_count": {
"type": "integer",
"description": "Committed shapes after the undo"
},
"remaining": {
"type": "integer",
"description": "Steps still available to undo"
},
"note": {
"type": "string"
}
},
"required": [
"undone",
"steps",
"shape_count",
"remaining"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
view_sheetSEE 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". marks (#297) burns DISCLOSURE layers into the render, so what a reply names, the picture shows: pass the coordinate lists a tool disclosed — question: withheld placements (orange ?-circles), struck: rejections a counter-example or luminance gate refused (magenta struck ×), ring: reference points like the sweep's own seed (violet double ring). The colors sit deliberately off the common CAD pens so they cannot vanish into color-plotted work. An overlay audit without marks shows only committed ink — the validation trap where 37 disclosed near-misses read as "it missed them". 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.Input schema{
"type": "object",
"properties": {
"sheet": {
"type": "string"
},
"region": {
"type": "object",
"properties": {
"x0": {
"type": "number"
},
"y0": {
"type": "number"
},
"x1": {
"type": "number"
},
"y1": {
"type": "number"
}
},
"required": [
"x0",
"y0",
"x1",
"y1"
],
"additionalProperties": false,
"description": "Crop rect in image px (origin top-left, y down); omit for the full sheet"
},
"px": {
"type": "integer",
"minimum": 200,
"maximum": 2000,
"description": "Long-side pixel budget of the returned image (default 1400) — small region + high px = readable dimension strings"
},
"overlay": {
"type": "boolean",
"description": "Burn committed shapes into the render (solid = human-affirmed, dashed = unreviewed)"
},
"grid": {
"type": "string",
"description": "Burn 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\""
},
"marks": {
"type": "object",
"properties": {
"question": {
"type": "array",
"items": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number"
},
{
"type": "number"
}
]
},
"description": "Open questions — withheld placements, spots to look at. Orange ?-in-circle"
},
"struck": {
"type": "array",
"items": {
"$ref": "#/properties/marks/properties/question/items"
},
"description": "Refusals — rejected[] placements, lum_gate.at. Magenta struck ×"
},
"ring": {
"type": "array",
"items": {
"$ref": "#/properties/marks/properties/question/items"
},
"description": "Reference points — the sweep's seed.center, an anchor. Violet double ring"
}
},
"additionalProperties": false,
"description": "Disclosure marks to burn into the render (#297): what the reply names, the picture shows. Coordinates in image px"
}
},
"required": [
"sheet"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
} | — | — · — | — |
The loaded plan set at a glance: file, page count, and every sheet's dims, title-block number, detected scale, scale state, and shape count. Read this first.
{
"resource_key": "takeoff://sheets",
"uri": "takeoff://sheets",
"name": "sheet-index",
"description": "The loaded plan set at a glance: file, page count, and every sheet's dims, title-block number, detected scale, scale state, and shape count. Read this first.",
"mime_type": "application/json",
"annotations": null,
"metadata_hash": "9b73c6bbab03e85e0342c9e814141e991bab24008998a98f7c5f51d3d6398595"
}One sheet: dims (px and pt), title-block sheet number, detected scale, scale state, committed shape count. JSON.
{
"template_key": "takeoff://sheet/{page}",
"uri_template": "takeoff://sheet/{page}",
"name": "sheet",
"description": "One sheet: dims (px and pt), title-block sheet number, detected scale, scale state, committed shape count. JSON.",
"mime_type": "application/json",
"annotations": null,
"metadata_hash": "6471ff17a28ea4ce0a54bf5c7fdf25c65937aa582bf8ec7bbc97e1e771c24762"
}The page rendered to PNG, long edge capped at 1568 px — sized for vision-model eyes. Coordinates in the image scale linearly to the tool coordinate space (image px at render scale 2.0).
{
"template_key": "takeoff://sheet/{page}/image",
"uri_template": "takeoff://sheet/{page}/image",
"name": "sheet-image",
"description": "The page rendered to PNG, long edge capped at 1568 px — sized for vision-model eyes. Coordinates in the image scale linearly to the tool coordinate space (image px at render scale 2.0).",
"mime_type": "image/png",
"annotations": null,
"metadata_hash": "7e4aec8ba6f18b05aac6be5061858cb55eee992368abd0c3aafc0cdb0a8c0ee5"
}The sheet's text content, reading order, joined — title block, room labels, schedules, scale notes. For positions use the read_sheet_text tool.
{
"template_key": "takeoff://sheet/{page}/text",
"uri_template": "takeoff://sheet/{page}/text",
"name": "sheet-text",
"description": "The sheet's text content, reading order, joined — title block, room labels, schedules, scale notes. For positions use the read_sheet_text tool.",
"mime_type": "text/plain",
"annotations": null,
"metadata_hash": "8b08194b2c4efc15e3c7bef556e30955c1cdb8ce7df3f8802b8fcb59481eb2c2"
}