opentakeoff-mcp 0.9.83 → 0.9.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/server-core.js +77 -43
  2. package/package.json +2 -2
@@ -118,8 +118,65 @@ function memberEmbed(vpTransform, member, pageH, renderScale) {
118
118
  };
119
119
  }
120
120
 
121
- // ../web/src/lib/sheets.ts
121
+ // ../web/src/lib/takeoffConstants.ts
122
+ var TAKEOFF_SCHEMA = "opentakeoff.takeoff_canvas.v1";
123
+ var REPORT_SCHEMA = "opentakeoff.report.v1";
122
124
  var RENDER_SCALE = 2;
125
+ var PALETTE = Object.freeze([
126
+ "#c96442",
127
+ "#2f7d54",
128
+ "#2563eb",
129
+ "#9333ea",
130
+ "#b8860b",
131
+ "#0d9488",
132
+ "#be185d",
133
+ "#1f2937",
134
+ "#dc2626",
135
+ "#0891b2"
136
+ ]);
137
+ var HATCH_IDS = Object.freeze([
138
+ "solid",
139
+ "diag",
140
+ "diag2",
141
+ "cross",
142
+ "diagdense",
143
+ "horiz",
144
+ "vert",
145
+ "grid",
146
+ "brick",
147
+ "plank",
148
+ "herring",
149
+ "basket",
150
+ "checker",
151
+ "wave",
152
+ "dots",
153
+ "speckle",
154
+ "iso",
155
+ "honeycomb",
156
+ "scan",
157
+ "plus",
158
+ "circuit",
159
+ "topo",
160
+ "woodgrain",
161
+ "chevron",
162
+ "pinwheel",
163
+ "harlequin",
164
+ "hexagon",
165
+ "penny",
166
+ "octagondot",
167
+ "fleur",
168
+ "concrete"
169
+ ]);
170
+ var SNAP_CELL = 24;
171
+ var SNAP_TOL = 7;
172
+ function nextHatchId(conditionCount) {
173
+ return HATCH_IDS[1 + conditionCount % (HATCH_IDS.length - 1)];
174
+ }
175
+ function nextPaletteColor(conditionCount) {
176
+ return PALETTE[conditionCount % PALETTE.length];
177
+ }
178
+
179
+ // ../web/src/lib/sheets.ts
123
180
  function sheetBaseLabelFromKey(key) {
124
181
  if (typeof key !== "string" || !key || isStitchKey(key)) return "";
125
182
  const t = parseSheetKey(key);
@@ -6937,7 +6994,7 @@ function reportJson({ projectName = "", rows = [], bySheet = [], scaleInfo = [],
6937
6994
  const colDefs = (Array.isArray(conditionColumns) ? conditionColumns : []).filter((cc) => cc && typeof cc === "object" && typeof cc.id === "string");
6938
6995
  const attrs = attrsByCond instanceof Map ? attrsByCond : /* @__PURE__ */ new Map();
6939
6996
  return {
6940
- schema: "opentakeoff.report.v1",
6997
+ schema: REPORT_SCHEMA,
6941
6998
  project_name: projectName || null,
6942
6999
  generated_with: "OpenTakeoff",
6943
7000
  // scale_confirmed (scale gate): false = an agent set this sheet's scale and
@@ -7656,14 +7713,10 @@ function drawMarks(ctx, toCanvas, marks, longEdge) {
7656
7713
  }
7657
7714
 
7658
7715
  // src/session.ts
7659
- var SNAP_CELL = 24;
7660
- var SNAP_TOL = 7;
7661
- var PALETTE = ["#c96442", "#2f7d54", "#2563eb", "#9333ea", "#b8860b", "#0d9488", "#be185d", "#1f2937", "#dc2626", "#0891b2"];
7662
- var HATCH_IDS = ["solid", "diag", "diag2", "cross", "diagdense", "horiz", "vert", "grid", "brick", "plank", "herring", "basket", "checker", "wave", "dots", "speckle", "iso", "honeycomb", "scan", "plus", "circuit", "topo", "woodgrain", "chevron", "pinwheel", "harlequin", "hexagon", "penny", "octagondot", "fleur", "concrete"];
7663
7716
  var mintUuid2 = () => globalThis.crypto && typeof globalThis.crypto.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
7664
7717
  var uid = (p) => `${p}-${mintUuid2()}`;
7665
7718
  var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString();
7666
- var ANN_SCHEMA = "opentakeoff.takeoff_canvas.v1";
7719
+ var ANN_SCHEMA = TAKEOFF_SCHEMA;
7667
7720
  var sanitizeApprovals2 = sanitizeApprovals;
7668
7721
  var applyApprovalCommand2 = applyApprovalCommand;
7669
7722
  var CONTEXT_MIN_LEN_PX = 2;
@@ -8374,13 +8427,13 @@ var Session = class _Session {
8374
8427
  conditionFor(tag) {
8375
8428
  let c = this.conditions.find((x) => x.finish_tag === tag);
8376
8429
  if (!c) {
8377
- const lc = PALETTE[this.conditions.length % PALETTE.length];
8430
+ const lc = nextPaletteColor(this.conditions.length);
8378
8431
  c = {
8379
8432
  id: uid("cnd"),
8380
8433
  finish_tag: tag,
8381
8434
  color: lc,
8382
8435
  fill: lc,
8383
- hatch: HATCH_IDS[1 + this.conditions.length % (HATCH_IDS.length - 1)],
8436
+ hatch: nextHatchId(this.conditions.length),
8384
8437
  multiplier: 1,
8385
8438
  waste_pct: 0,
8386
8439
  materials: []
@@ -10793,7 +10846,7 @@ var Session = class _Session {
10793
10846
  tag: newTag,
10794
10847
  mintId: (p) => uid(p),
10795
10848
  nowIso: nowIso2,
10796
- nextHatch: HATCH_IDS[1 + (this.conditions.length + 1) % (HATCH_IDS.length - 1)]
10849
+ nextHatch: nextHatchId(this.conditions.length + 1)
10797
10850
  });
10798
10851
  if (parentPatch) Object.assign(src, parentPatch);
10799
10852
  this.conditions.push(twin);
@@ -12358,7 +12411,7 @@ var reportMaterialLine = z.object({
12358
12411
  qty: z.number().describe("Computed order quantity")
12359
12412
  }).passthrough();
12360
12413
  var exportReportOutput = {
12361
- schema: z.literal("opentakeoff.report.v1"),
12414
+ schema: z.literal(REPORT_SCHEMA),
12362
12415
  project_name: z.string().nullable(),
12363
12416
  generated_with: z.string(),
12364
12417
  sheets: z.array(z.object({ sheet_id: z.string(), sheet: z.string(), scale_source: z.string() }).passthrough()).describe("Scale provenance per sheet \u2014 how each scale was set"),
@@ -14182,25 +14235,6 @@ async function exportMarkedPdf(session, opts) {
14182
14235
  import path4 from "node:path";
14183
14236
  import { readFile as readFile3 } from "node:fs/promises";
14184
14237
 
14185
- // ../web/src/lib/stamps.js
14186
- var DEFAULT_STAMPS = [
14187
- { id: "stmp-direction", name: "Plank / tile direction", elements: [
14188
- { type: "arrow", from: [-0.05, 0], to: [0.05, 0], color: "#1f3fc7", weight: 1.5 }
14189
- ] },
14190
- { id: "stmp-seam", name: "Seam direction", elements: [
14191
- { type: "arrow", from: [-0.05, 0], to: [0.05, 0], color: "#b03a26", line_style: "dashed" }
14192
- ] },
14193
- { id: "stmp-origin", name: "Pattern origin", elements: [
14194
- { type: "bubble", at: [0, 0], r: 0.018, text: "PO", color: "#0d9488" }
14195
- ] }
14196
- ];
14197
- var DEFAULT_STAMP_SETS = [
14198
- { id: "set-flooring", name: "Flooring shop drawings", stampIds: DEFAULT_STAMPS.map((s) => s.id) }
14199
- ];
14200
-
14201
- // ../web/src/lib/store.js
14202
- var ANN_SCHEMA2 = "opentakeoff.takeoff_canvas.v1";
14203
-
14204
14238
  // ../web/src/lib/reviewState.js
14205
14239
  function normalizeAgentReview(shape) {
14206
14240
  return shape?.origin?.actor === "agent" && shape.origin.reviewed == null ? { ...shape, origin: { ...shape.origin, reviewed: false } } : shape;
@@ -14214,8 +14248,8 @@ function parseTakeoffImport(text) {
14214
14248
  } catch {
14215
14249
  throw new Error("Couldn't import takeoff: that file is not valid JSON.");
14216
14250
  }
14217
- if (!doc || typeof doc !== "object" || Array.isArray(doc) || doc.schema !== ANN_SCHEMA2) {
14218
- throw new Error(`Couldn't import takeoff: not a takeoff export (expected schema "${ANN_SCHEMA2}" \u2014 the file export_takeoff or the app writes).`);
14251
+ if (!doc || typeof doc !== "object" || Array.isArray(doc) || doc.schema !== TAKEOFF_SCHEMA) {
14252
+ throw new Error(`Couldn't import takeoff: not a takeoff export (expected schema "${TAKEOFF_SCHEMA}" \u2014 the file export_takeoff or the app writes).`);
14219
14253
  }
14220
14254
  return doc;
14221
14255
  }
@@ -14508,7 +14542,7 @@ function registerTools(realServer, session, opts = {}) {
14508
14542
  outputSchema: proposeTakeoffOutput
14509
14543
  }, run("propose_takeoff", (a) => session.proposeTakeoff(a.label, a.rationale)));
14510
14544
  server.registerTool("measure_polygon", {
14511
- description: `Measure a closed polygon you supply (min 3 vertices, image px): area_sf and perimeter_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it; role "deduct" subtracts. ${COORDS}`,
14545
+ description: `Measure a closed polygon you supply (min 3 vertices, image px): area_sf and perimeter_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it; role "deduct" subtracts. A room ring belongs on the innermost wall-face strokes from get_sheet_vectors, crossing each door opening on the wall centerline and wrapping columns and stubs; never on a hatch edge, casework or a door leaf. Check it with view_sheet overlay:true on a tight crop and fix it with edit_shape. ${COORDS}`,
14512
14546
  inputSchema: {
14513
14547
  sheet: z2.string(),
14514
14548
  verts: z2.array(pointSchema).min(3),
@@ -15055,7 +15089,7 @@ No review gate: the pencil-not-ink rule exists to stop an agent inventing geomet
15055
15089
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
15056
15090
 
15057
15091
  // src/wiki.generated.ts
15058
- var WIKI_VERSION = "0.9.83";
15092
+ var WIKI_VERSION = "0.9.85";
15059
15093
  var WIKI_PAGES = [
15060
15094
  {
15061
15095
  "key": "index",
@@ -15094,8 +15128,8 @@ var WIKI_PAGES = [
15094
15128
  "uri": "takeoff://wiki/workflows",
15095
15129
  "title": "Workflows",
15096
15130
  "source": "docs/wiki/workflows.md",
15097
- "source_sha256": "23d921b95108731f41f1a68d132bd64693c081e7ee30627a12d147cced6a15f0",
15098
- "text": "# Workflows\n\n## Human: open, stitch and measure\n\nOpen the plans and check their revision. Press **G** (or choose **Sheets \u2192 Open\ngallery\u2026**) to open the sheet gallery, then select the\n2\u20134 split sheets in left-to-right order and click **Stitch N into one surface**.\nOn the composite, choose **Align**, click a recognizable point near the joint,\nthen click the same drawn point on the other sheet. Check a second recognizable\npoint and the scale before tracing. Align translates a member; it does not\nrotate or resize it. Once shapes exist on a stitch, alignment is locked.\n\nIf the controls seem missing, **Stitch N into one surface** is in the gallery\nfooter and requires 2\u20134 selected sheets; **Align** appears only on an open\nstitched surface. Create a fresh stitch if Align refuses because takeoffs\nalready exist; deleting a stitch also refuses while takeoffs or markups exist.\n\nUse the composite as one measuring surface. Its marked-set page is labeled as\na composite, not an architect-issued sheet. Save the editable project archive\nwhen preserving the composite is required. MCP has no stitch/alignment tool and\nits current import/export path omits stitches: it cannot be used as a lossless\nhandoff for that document. Have an MCP agent measure source sheets individually\nwith explicit scope boundaries instead.\n\nSource: [Human stitching instructions](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md#stitching-a-floor-split-at-a-match-line),\n[stitch records](https://github.com/Kentucky-ai/opentakeoff/blob/main/web/src/lib/stitches.ts),\n[compatibility evidence](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/COMPATIBILITY.md).\n\n## Agent: source to reviewed handoff\n\n1. `load_plan` \u2192 inspect sheets/revisions \u2192 `set_scale` on each measured sheet.\n Confirm the relevant detail's scale, which may differ from the overall plan.\n2. Read source text and vectors; inspect the matching region with `view_sheet`.\n Use each room's schedule evidence for its finish. Missing/ambiguous evidence\n is a qualification or RFI, not an inferred assignment.\n3. `propose_takeoff` names a batch; it creates no geometry. Measure small batches\n with the appropriate area, run, surface or count tool.\n4. Inspect overlays and actual boundaries, openings, jambs and deductions.\n `edit_shape` corrects pending shapes. Physical base gaps use explicit runs;\n stepped wall faces use separate height bands. See the [geometry workflow](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md).\n5. `takeoff_summary` and `export_report` check quantities and material coverage.\n Shorten notes through `list_annotations` \u2192 `edit_annotation` where permitted.\n6. Export editable takeoff JSON and a marked-set PDF, reopen the JSON against\n the same source, and check the handoff. Leave agent work pending for the\n human's review; exported files and agent verdicts do not create approval.\n\nThe [MCP route map](takeoff://wiki/mcp) gives the next tool at each step. The\n[agent guide](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/AGENT_GUIDE.md) covers staging and refusal recovery.\n\n## Evidence that another person can review\n\nKeep source/revision identifiers, calibration choices, measured geometry,\nexpected-versus-observed checks and an overlay or marked-set view. Compare\nspatial agreement as well as quantities. Disclose reference-assisted work;\nit is not a blind accuracy benchmark. Public PRs include screenshots, video or\nreproducible stats, using publishable fixtures. Private drawings and pricing stay\noutside the public repository. See the [repository guide](takeoff://wiki/repo-guide).\n"
15131
+ "source_sha256": "75afa0b82f7163038164851e49f9c63c8ed6c7a69eb2dc1e084423ad8424cc4a",
15132
+ "text": "# Workflows\n\n## Human: open, stitch and measure\n\nOpen the plans and check their revision. Press **G** (or choose **Sheets \u2192 Open\ngallery\u2026**) to open the sheet gallery, then select the\n2\u20134 split sheets in left-to-right order and click **Stitch N into one surface**.\nOn the composite, choose **Align**, click a recognizable point near the joint,\nthen click the same drawn point on the other sheet. Check a second recognizable\npoint and the scale before tracing. Align translates a member; it does not\nrotate or resize it. Once shapes exist on a stitch, alignment is locked.\n\nIf the controls seem missing, **Stitch N into one surface** is in the gallery\nfooter and requires 2\u20134 selected sheets; **Align** appears only on an open\nstitched surface. Create a fresh stitch if Align refuses because takeoffs\nalready exist; deleting a stitch also refuses while takeoffs or markups exist.\n\nUse the composite as one measuring surface. Its marked-set page is labeled as\na composite, not an architect-issued sheet. Save the editable project archive\nwhen preserving the composite is required. MCP has no stitch/alignment tool and\nits current import/export path omits stitches: it cannot be used as a lossless\nhandoff for that document. Have an MCP agent measure source sheets individually\nwith explicit scope boundaries instead.\n\nSource: [Human stitching instructions](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md#stitching-a-floor-split-at-a-match-line),\n[stitch records](https://github.com/Kentucky-ai/opentakeoff/blob/main/web/src/lib/stitches.ts),\n[compatibility evidence](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/COMPATIBILITY.md).\n\n## Agent: source to reviewed handoff\n\n1. `load_plan` \u2192 inspect sheets/revisions \u2192 `set_scale` on each measured sheet.\n Confirm the relevant detail's scale, which may differ from the overall plan.\n2. Read source text and vectors; inspect the matching region with `view_sheet`.\n Use each room's schedule evidence for its finish. Missing/ambiguous evidence\n is a qualification or RFI, not an inferred assignment.\n3. `propose_takeoff` names a batch; it creates no geometry. Measure small batches\n with the appropriate area, run, surface or count tool.\n4. Inspect overlays and actual boundaries, openings, jambs and deductions.\n `edit_shape` corrects pending shapes. Physical base gaps use explicit runs;\n stepped wall faces use separate height bands. See the [geometry workflow](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md).\n\n### Trace a room the way an estimator does\n\nBlind agent runs against reviewed references showed that the ring, not the\ntotal, is what fails. These rules are what the references are drawn to:\n\n1. The boundary is the **innermost interior wall face**. Never the wall\n centerline, never the far face, never under a wall. Casework, counters,\n fixtures, equipment, hatch patterns, dimension strings, text and leaders\n never define the boundary; the finish runs under casework and fixtures.\n2. Corners sit where two adjacent wall-face strokes meet. Read the strokes with\n `get_sheet_vectors` over a tight region and put each vertex on a stroke; do\n not trace a hatch edge or a raster guess.\n3. At every **door or cased opening** the ring follows the face to the jamb,\n turns into the opening, runs across it on the **wall centerline** (midway\n between that wall's two faces, the full wall on a composite wall), and\n returns along the far jamb. Both rooms sharing the door share that segment.\n A door leaf and its swing arc are never part of the boundary; a leaf drawn\n standing open looks like a wall line and is not one.\n4. **Windows** and other non-passable openings do not break the ring.\n5. Columns, chases, pilasters and wall stubs that project into the room are\n traced around; an enclosed cell drawn with wall-weight lines is not floor.\n6. A **finish split** inside one room is two rings sharing the drawn\n transition line exactly, no overlap and no gap. Where two rooms meet with no\n wall, split on the drawn transition line or the partition's centerline.\n7. Take the finish from the schedule row; a plan tag alone is a cross-check.\n8. After each ring, `view_sheet` a tight crop with `overlay: true` at a high\n `px` and look at every corner and notch before the next room; `edit_shape`\n fixes what the crop shows. A full-sheet render cannot audit a ring.\n\nWhen something is genuinely ambiguous, follow the rule most literally, carry it,\nand say so in the shape's label or an annotation. Do not stop.\n5. `takeoff_summary` and `export_report` check quantities and material coverage.\n Shorten notes through `list_annotations` \u2192 `edit_annotation` where permitted.\n6. Export editable takeoff JSON and a marked-set PDF, reopen the JSON against\n the same source, and check the handoff. Leave agent work pending for the\n human's review; exported files and agent verdicts do not create approval.\n\nThe [MCP route map](takeoff://wiki/mcp) gives the next tool at each step. The\n[agent guide](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/AGENT_GUIDE.md) covers staging and refusal recovery.\n\n## Evidence that another person can review\n\nKeep source/revision identifiers, calibration choices, measured geometry,\nexpected-versus-observed checks and an overlay or marked-set view. Compare\nspatial agreement as well as quantities. Disclose reference-assisted work;\nit is not a blind accuracy benchmark. Public PRs include screenshots, video or\nreproducible stats, using publishable fixtures. Private drawings and pricing stay\noutside the public repository. See the [repository guide](takeoff://wiki/repo-guide).\n"
15099
15133
  },
15100
15134
  {
15101
15135
  "key": "mcp",
@@ -15110,16 +15144,16 @@ var WIKI_PAGES = [
15110
15144
  "uri": "takeoff://wiki/domain",
15111
15145
  "title": "Takeoff domain knowledge",
15112
15146
  "source": "docs/wiki/domain.md",
15113
- "source_sha256": "6a055b911e85cf9416c9036ab53a63a13ea260c91b2184b5386643f0db7a7b98",
15114
- "text": "# Takeoff domain knowledge\n\n| Quantity or record | Meaning | Common mistake |\n|---|---|---|\n| Floor SF | Area assigned to a floor finish, net of supported deductions | Adding wall SF to call the result building floor area |\n| Wall SF | Measured run LF \xD7 that shape's height | Applying one elevation width to every wall or treating floor deducts as wall openings |\n| Base/transition LF | Installed run length or a disclosed derived allowance | Treating a numeric opening allowance as a located gap |\n| Count | Located instances with count semantics | Counting a note's bare mention as a drawn device |\n| Order quantity | Net quantity with the condition's stated multiplier/waste rules | Increasing the traced geometry to carry waste |\n| Material coverage | A material row derived from measured finish quantities | Drawing duplicate finish polygons for membrane/protection coverage |\n| Confidence | A signal for prioritizing inspection | Treating it as human approval or an accuracy guarantee |\n| Agent verdict | The agent's own recorded check | Creating or claiming an estimator's approval seal |\n\nUse `measure_surface` bands for stepped faces. `cut_out` removes the full height\nof the particular wall run it clips; a partial-height opening needs bands so\nonly affected heights are clipped. Existing records have no elevation-plane\ncoordinate or vertical band offset. Preserve that limitation rather than\ninventing a new meaning for `deduct`.\n\n`derive_base` retains a whole perimeter and may subtract stated LF numerically.\nExplicit `measure_line` runs and located cuts show installation gaps. Inspect\nopen finish splits, jamb returns and columns; apparent openings need source\nevidence. `derive_transitions` can withhold a wall-separated boundary: a returned\ncandidate is still something to inspect.\n\nMCP work stays pending; correction does not approve it. A human's correction\nfreezes original machine outer vertices, including manual agent traces, but it\ncannot restore previously discarded history. Schema validation, confidence,\nreview, approval, identity and Academy certification remain separate.\n\nSources: [quantity math](https://github.com/Kentucky-ai/opentakeoff/blob/main/web/src/lib/totals.js),\n[Session measurement and derivation](https://github.com/Kentucky-ai/opentakeoff/blob/main/mcp/src/session.ts),\n[provenance](https://github.com/Kentucky-ai/opentakeoff/blob/main/web/src/lib/provenance.js),\n[review semantics](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/COMPATIBILITY.md),\n[human glossary](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md#18-glossary--what-the-words-mean-here).\n"
15147
+ "source_sha256": "4d6d3d1d4368e6e8e9e3ba5eba8f01ee61d0d093913392508789217e8efa14e0",
15148
+ "text": "# Takeoff domain knowledge\n\n| Quantity or record | Meaning | Common mistake |\n|---|---|---|\n| Floor SF | Area assigned to a floor finish, net of supported deductions | Adding wall SF to call the result building floor area |\n| Room boundary | The innermost interior wall face, with door openings crossed on the wall centerline | Tracing a hatch edge, a door leaf drawn open, or stopping at casework; see [workflows](takeoff://wiki/workflows) |\n| Wall SF | Measured run LF \xD7 that shape's height | Applying one elevation width to every wall or treating floor deducts as wall openings |\n| Base/transition LF | Installed run length or a disclosed derived allowance | Treating a numeric opening allowance as a located gap |\n| Count | Located instances with count semantics | Counting a note's bare mention as a drawn device |\n| Order quantity | Net quantity with the condition's stated multiplier/waste rules | Increasing the traced geometry to carry waste |\n| Material coverage | A material row derived from measured finish quantities | Drawing duplicate finish polygons for membrane/protection coverage |\n| Confidence | A signal for prioritizing inspection | Treating it as human approval or an accuracy guarantee |\n| Agent verdict | The agent's own recorded check | Creating or claiming an estimator's approval seal |\n\nUse `measure_surface` bands for stepped faces. `cut_out` removes the full height\nof the particular wall run it clips; a partial-height opening needs bands so\nonly affected heights are clipped. Existing records have no elevation-plane\ncoordinate or vertical band offset. Preserve that limitation rather than\ninventing a new meaning for `deduct`.\n\n`derive_base` retains a whole perimeter and may subtract stated LF numerically.\nExplicit `measure_line` runs and located cuts show installation gaps. Inspect\nopen finish splits, jamb returns and columns; apparent openings need source\nevidence. `derive_transitions` can withhold a wall-separated boundary: a returned\ncandidate is still something to inspect.\n\nMCP work stays pending; correction does not approve it. A human's correction\nfreezes original machine outer vertices, including manual agent traces, but it\ncannot restore previously discarded history. Schema validation, confidence,\nreview, approval, identity and Academy certification remain separate.\n\nSources: [quantity math](https://github.com/Kentucky-ai/opentakeoff/blob/main/web/src/lib/totals.js),\n[Session measurement and derivation](https://github.com/Kentucky-ai/opentakeoff/blob/main/mcp/src/session.ts),\n[provenance](https://github.com/Kentucky-ai/opentakeoff/blob/main/web/src/lib/provenance.js),\n[review semantics](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/COMPATIBILITY.md),\n[human glossary](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md#18-glossary--what-the-words-mean-here).\n"
15115
15149
  },
15116
15150
  {
15117
15151
  "key": "repo-guide",
15118
15152
  "uri": "takeoff://wiki/repo-guide",
15119
15153
  "title": "Working on the repository",
15120
15154
  "source": "docs/wiki/repo-guide.md",
15121
- "source_sha256": "f7ed92fc2e49cbb1e00b7e90335120dccd226bd837d1d7064c38bc9e74500cf7",
15122
- "text": "# Working on the repository\n\nOpenTakeoff is a **client-only React app**: a PDF construction-takeoff canvas for flooring (useful for any trade). No backend, no database, no auth\u2014everything runs and persists in the browser. Apache-2.0. (For the one-page project pitch and vision, see [`AGENT_BRIEF.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/AGENT_BRIEF.md); for capability \u2192 code mapping, see [`FEATURES.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/FEATURES.md).)\n\n## Run / build / check\n\n```bash\ncd web\nnvm use # web/.nvmrc matches the root .nvmrc used by CI\nnpm install\nnpm run dev # http://localhost:5173 \u2014 hot reload\nnpm test # node:test over the pure geometry + totals math (test/*.test.ts)\nnpm run build # \u2192 web/dist/ (static output; this is what Netlify deploys)\nnpm run check # web typecheck + lint + test + benchmark + build\n```\n\n## Shipping \u2014 the required steps, every change\n\n`main` is protected on GitHub by a ruleset (PR-only, one approving review,\ngreen `web` check\u2014the repo owner has a standing bypass\nas the solo maintainer). **Merging to `main` deploys to production**\n(<https://opentakeoff.kentucky-ai.com>)\u2014Netlify's own git integration builds\nthe merge commit and publishes it. `netlify.toml` holds the whole recipe\n(`base = \"web\"`, `command = npm run build`, `publish = \"dist\"`), so the deploy\nruns a fresh build from the merged source; nothing is uploaded from CI.\n\n`.github/workflows/deploy.yml` used to do this by publishing `web/dist` with\n`--no-build`, and it was **deleted on 2026-07-13 in `e701f1a`** (\"deploys here\nare manual CLI; it fails on every push without the fork's secrets\"). Current checks and release automation live in\n[the workflow directory](https://github.com/Kentucky-ai/opentakeoff/blob/main/.github/workflows); `publish-mcp.yml` fires on\nan `mcp-v*` tag. The old line here said \"Netlify never builds anything\nitself\", which is now exactly backwards\u2014Netlify is the only thing that\nbuilds production. **Merge = deploy either way: that part has never changed.**\n\n> **This is the canonical `Kentucky-ai/opentakeoff` repo\u2014production is\n> <https://opentakeoff.kentucky-ai.com>, nothing else.** A downstream fork\n> (`knmurphy/opentakeoff`) tracks this repo as its own upstream and deploys\n> separately to `takeoff.345flooring.com`\u2014that URL belongs to *that* fork,\n> not this repo. If you see `takeoff.345flooring.com` referenced elsewhere in\n> this repo's docs, it's either an example value in the optional cloud-mode\n> guides or leftover content from that fork's docs that rode along in a\n> wholesale history merge (2026-07-13)\u2014treat it as describing the\n> *downstream* fork's deployment, not this one's.\n\nSo:\n\n1. **Branch first**\u2014never commit on `main`: `git checkout -b <topic>`.\n2. **`npm run check` before pushing** (in `web/`). It covers the independent web\n job on Node 24; a green web check does not replace the MCP, protocol, docs,\n capture or optional-server jobs.\n3. **Include review evidence in every PR**: screenshots or a short video for\n visible changes; measured expected-versus-observed stats and reproducible\n commands for engine/tool changes. Link the actual checks or evidence. This\n repository is also the maintainer's public portfolio: keep claims verifiable\n and private project data out of public artifacts.\n4. **Open a PR** and wait for the `web` check to pass. Don't merge red or\n pending.\n5. **Squash-merge with branch delete**\n (`gh pr merge <n> --squash --delete-branch`), then\n `git checkout main && git pull --ff-only` and delete the local branch\n (`git branch -D <topic>`\u2014squash merges need `-D`).\n6. **Remember a merge is a deploy.** Don't merge work you haven't verified in\n the running app.\n\nThe tests cover the pure math (`web/test/geometry.test.ts`, `web/test/totals.test.ts`); the canvas itself is verified by hand\u2014**Vite does not flag undefined identifiers in JSX**, so grep for your new identifiers after editing and load the app once before you call it done. The bundled sample plan (`web/public/demo/`, wired to the \"Load sample plan\" button) is the fastest end-to-end check: load it, press `A`, trace a room, open Report.\n\n## Where things live\n\n| Concern | Path |\n|---|---|\n| **The canvas\u201490% of the app** | `web/src/pages/TakeoffCanvas.jsx` (one large, deliberately monolithic component) |\n| Geometry: vector extraction, One-Click flood fill, vertex snap | `web/src/lib/oneclick.ts` |\n| Sheet/page helpers, scale detection | `web/src/lib/sheets.ts` |\n| Totals and materials math (waste, SY, coverage \u2192 order qty) | `web/src/lib/totals.js` |\n| Persistence (IndexedDB + localStorage) | `web/src/lib/store.js` |\n| PDF/image/zip ingest | `web/src/lib/ingest.js` |\n| Icon set | `web/src/brand/icons.jsx` |\n| Design tokens (colors, spacing\u2014the source of truth) | `web/src/styles/tokens.css` |\n| Sheet gallery / report UI | `web/src/components/` |\n| Pure-math tests (node:test) | `web/test/` |\n| **Optional AI backend** (pluggable adapter: scale/room/finish suggestions) | `server/`\u2014`app.py` + `adapters/base.py` (interface) + `adapters/heuristic.py` (default, no model) |\n\n## How the canvas works (the mental model)\n\n- Each open sheet renders into a `<canvas>` bitmap; **all takeoff geometry is an SVG overlay** on top; pan/zoom is a single CSS transform on the stage div, written imperatively (`tfRef` \u2192 `style.transform`) to avoid React re-renders per frame.\n- Coordinates: pointer events (client px) \u2192 `toImage()` \u2192 **stage px**; committed shapes store **normalized [0..1] vertices per sheet** (`verts_norm`), so quantities survive re-renders and zoom.\n- Cursor-following UI (crosshair hairlines, readout chip, rubber band) updates through **direct DOM writes in `moveCrosshair`**\u2014never React state per mousemove. Keep it that way.\n- Angle snapping: `angleSnap()` locks in-progress segments to the 45\xB0 family; endpoint snap (`nearestSnap` over a spatial hash of PDF vector endpoints) takes priority. The committed click reuses the same locked point (`angleRef`).\n- Past ~1.15\xD7 zoom, a **detail-view canvas** re-renders the visible region from PDF vectors at the current zoom (crispness); the base bitmap stays as first paint.\n- pdf.js rendering schedules work on `requestAnimationFrame`\u2014a fully hidden/occluded window will pause mid-render by design; it resumes when visible.\n\n## Conventions\n\n- **SVG presentation attributes take literal colors** (CSS vars don't resolve there): cobalt `#1f3fc7`, danger `#b03a26`, positive `#1f6b4a`\u2014centralized in `web/src/lib/ui.js` (`SVG`, with HUD-dark counterparts through `svgAccent(isDark)`). DOM/HTML chrome may use `var(--\u2026)` from `tokens.css`.\n- Condition palettes (`PALETTE` in `web/src/components/hatches.jsx`, the seeded condition colors in `FLOORING_DEFAULTS` in `web/src/lib/canvasConstants.js`, and the mirrored copies in `mcp/src/session.ts`) are **user data**\u2014don't re-theme them.\n- Waste applies only in the report (order quantities), never to live measured numbers.\n- Keyboard shortcuts are single letters registered on `window` (see `docs/USER_GUIDE.md` \xA715); toolbar menus pause them through `menuDepthRef`.\n- Brand voice: **precision instrument** (2026-08 overhaul). Light theme = \"ice\": bright white surfaces on a cool field, cool-slate neutrals, cobalt the one saturated thing. Dark theme = \"HUD\": true-black cockpit, electric blue `#3f8cff`, phosphor `--glow` on exactly five elements (active tool face, status verb, hero quantity, primary CTA, calibration dot). Square corners; the single sanctioned radius is `--r-1` on floating chrome. Mono tabular numerals on every readout. Drafting-table language stays. No vendor mimicry.\n- Layout/spacing/type come from the token scales in `tokens.css` (`--sp-*`, `--fs-*`, `--ctl-*`); zIndex comes from the `Z` ladder in `web/src/lib/ui.js`. No new magic numbers.\n\n## Docs to keep in sync when you change behavior\n\nThe draft Takeoff Protocol lives in [`protocol/`](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/README.md). When\nchanging persisted fields, consult its [inventory](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/INVENTORY.md) and\n[compatibility report](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/ACADEMY_COMPATIBILITY.md). Run\n`npm run check --prefix protocol` after installing web, MCP, and protocol\ndependencies. Refresh schema references with\n`node protocol/scripts/check-docs.mjs --write`. The draft does not authorize\nchanging writer formats or adopting a migration.\n\n1. `README.md` (Features + \"What's in the box\")\n2. `docs/USER_GUIDE.md` (shortcuts + the relevant section)\n3. `CHANGELOG.md`\n\nTouching the **MCP server** also requires checking its runtime guidance and\ngenerated references:\n\n4. `mcp/src/tools.ts`\u2014the tool's own `description` is part of its integration.\n An MCP client reads it at runtime. Initialize instructions and packaged wiki\n resources are also agent-facing surfaces, so keep them consistent with the\n runtime behavior.\n5. `mcp/server.ts`\u2014the `instructions` block sent at `initialize`. This is the\n decision tree every client receives before its first call. A new *verb* does\n not belong here; a new *step in the standard finish* does. Packaged wiki\n resources are another deliberate agent-facing surface; update their source\n pages and run the wiki/resource checks when their guidance changes.\n6. `mcp/README.md` (the tool table) and `docs/MCP.md` (the reach-for-it ordering,\n the example session, and the tool count in its opening line). A new tool also\n needs a row in `mcp/src/staging.ts`'s `TOOL_STAGES`\u2014the four lists must\n partition the tool set exactly, and a test fails CI if one doesn't.\n `npm run check:tool-count --prefix mcp` checks default, gated and setup counts across the current entry points,\n reference-table coverage, and [`docs/MCP_TOOL_INDEX.md`](takeoff://wiki/tool-index)\n against runtime schemas. Add `-- --write` to regenerate counts and the index. If the\n change alters *doctrine* rather than adding a verb\u2014what withholds, what\n refuses, what has no agent verb\u2014it belongs in `docs/AGENT_GUIDE.md` too,\n and the tool count appears there and in `docs/USER_GUIDE.md` \xA714.\n7. **Version fields must agree.** The MCP source version is\n in `mcp/package.json`; its lockfile has two matching fields, while\n `mcp/server.json` and `web/public/.well-known/mcp.json` each have two. The\n version checker checks all seven MCP fields. Separately, it checks the web\n package version against the two root-version fields in its lockfile.\n Check current `origin/main` and existing tags before\n reserving a number. The web package has its own version; it is not the MCP\n release number.\n8. `npm run check:tool-count --prefix mcp` runs version agreement before checking\n counts and the schema-backed inventory. With `-- --write`, it refuses version\n mismatches before changing generated documents; update release metadata\n explicitly, then regenerate. Run `npm run check:versions --prefix mcp` for the\n version check alone. Wiki content has its own generator:\n `npm run check:wiki --prefix mcp -- --write`. Review generated diffs and run\n both checks without `--write` afterward.\n\nArchitecture rather than behavior\u2014what MCP is versus what the `/ai` sandbox\nis, and why the server imports the web engine in-process\u2014lives at the end of\n[`docs/MCP.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/MCP.md) (\"Where this sits\") and in\n[`server/README.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/server/README.md).\n\n## The doc set, and who each one is for\n\nFour documents carry the product, and they're deliberately split by audience\u2014don't\nanswer an estimator's question in the agent manual or vice versa:\n\n| Document | Audience | What belongs in it |\n|---|---|---|\n| [`README.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/README.md) | everyone, ~60 seconds | what this is, the three doors, what's in the box |\n| [`docs/USER_GUIDE.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md) | the estimator at the canvas | every shipped UI behavior, the working order on a real bid, the glossary |\n| [`docs/AGENT_GUIDE.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/AGENT_GUIDE.md) | an agent driving the engine | the operating model, the standard finish, withheld doctrine, staging, refusal\u2192next-move |\n| [`mcp/README.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/mcp/README.md) | an agent's integrator | tool-by-tool reference, resources, coordinate contract, limits |\n\nAll of them follow one house style\u2014the Apple Style Guide, with the rules that\nactually come up written out in [`CONTRIBUTING.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/CONTRIBUTING.md#docs-house-style).\nInterface text quoted in a doc is copied from the code verbatim, so changing a\nmessage means changing it in both places.\n\n[`AGENT_BRIEF.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/AGENT_BRIEF.md) is the one-page orientation that routes to\nall four. A behavior change usually touches two of them; a new MCP tool touches\nthree plus this file's sync list above.\n"
15155
+ "source_sha256": "9ef949a3dca1f992a005764a825ca86b5f4da1fd631e9eed1011b8d6fe967ab3",
15156
+ "text": "# Working on the repository\n\nOpenTakeoff is a **client-only React app**: a PDF construction-takeoff canvas for flooring (useful for any trade). No backend, no database, no auth\u2014everything runs and persists in the browser. Apache-2.0. (For the one-page project pitch and vision, see [`AGENT_BRIEF.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/AGENT_BRIEF.md); for capability \u2192 code mapping, see [`FEATURES.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/FEATURES.md).)\n\n## Run / build / check\n\n```bash\ncd web\nnvm use # web/.nvmrc matches the root .nvmrc used by CI\nnpm install\nnpm run dev # http://localhost:5173 \u2014 hot reload\nnpm test # node:test over the pure geometry + totals math (test/*.test.ts)\nnpm run build # \u2192 web/dist/ (static output; this is what Netlify deploys)\nnpm run check # web typecheck + lint + test + benchmark + build\n```\n\n## Shipping \u2014 the required steps, every change\n\n`main` is protected on GitHub by a ruleset (PR-only, one approving review,\ngreen `web` check\u2014the repo owner has a standing bypass\nas the solo maintainer). **Merging to `main` deploys to production**\n(<https://opentakeoff.kentucky-ai.com>)\u2014Netlify's own git integration builds\nthe merge commit and publishes it. `netlify.toml` holds the whole recipe\n(`base = \"web\"`, `command = npm run build`, `publish = \"dist\"`), so the deploy\nruns a fresh build from the merged source; nothing is uploaded from CI.\n\n`.github/workflows/deploy.yml` used to do this by publishing `web/dist` with\n`--no-build`, and it was **deleted on 2026-07-13 in `e701f1a`** (\"deploys here\nare manual CLI; it fails on every push without the fork's secrets\"). Current checks and release automation live in\n[the workflow directory](https://github.com/Kentucky-ai/opentakeoff/blob/main/.github/workflows); `publish-mcp.yml` fires on\nan `mcp-v*` tag. The old line here said \"Netlify never builds anything\nitself\", which is now exactly backwards\u2014Netlify is the only thing that\nbuilds production. **Merge = deploy either way: that part has never changed.**\n\n> **This is the canonical `Kentucky-ai/opentakeoff` repo\u2014production is\n> <https://opentakeoff.kentucky-ai.com>, nothing else.** A downstream fork\n> (`knmurphy/opentakeoff`) tracks this repo as its own upstream and deploys\n> separately to `takeoff.345flooring.com`\u2014that URL belongs to *that* fork,\n> not this repo. If you see `takeoff.345flooring.com` referenced elsewhere in\n> this repo's docs, it's either an example value in the optional cloud-mode\n> guides or leftover content from that fork's docs that rode along in a\n> wholesale history merge (2026-07-13)\u2014treat it as describing the\n> *downstream* fork's deployment, not this one's.\n\nSo:\n\n1. **Branch first**\u2014never commit on `main`: `git checkout -b <topic>`.\n2. **`npm run check` before pushing** (in `web/`). It covers the independent web\n job on Node 24; a green web check does not replace the MCP, protocol, docs,\n capture or optional-server jobs.\n3. **Include review evidence in every PR**: screenshots or a short video for\n visible changes; measured expected-versus-observed stats and reproducible\n commands for engine/tool changes. Link the actual checks or evidence. This\n repository is also the maintainer's public portfolio: keep claims verifiable\n and private project data out of public artifacts.\n4. **Open a PR** and wait for the `web` check to pass. Don't merge red or\n pending.\n5. **Squash-merge with branch delete**\n (`gh pr merge <n> --squash --delete-branch`), then\n `git checkout main && git pull --ff-only` and delete the local branch\n (`git branch -D <topic>`\u2014squash merges need `-D`).\n6. **Remember a merge is a deploy.** Don't merge work you haven't verified in\n the running app.\n\nThe tests cover the pure math (`web/test/geometry.test.ts`, `web/test/totals.test.ts`); the canvas itself is verified by hand\u2014**Vite does not flag undefined identifiers in JSX**, so grep for your new identifiers after editing and load the app once before you call it done. The bundled sample plan (`web/public/demo/`, wired to the \"Load sample plan\" button) is the fastest end-to-end check: load it, press `A`, trace a room, open Report.\n\n## Where things live\n\n| Concern | Path |\n|---|---|\n| **The canvas\u201490% of the app** | `web/src/pages/TakeoffCanvas.jsx` (one large, deliberately monolithic component) |\n| Geometry: vector extraction, One-Click flood fill, vertex snap | `web/src/lib/oneclick.ts` |\n| Sheet/page helpers, scale detection | `web/src/lib/sheets.ts` |\n| Totals and materials math (waste, SY, coverage \u2192 order qty) | `web/src/lib/totals.js` |\n| Persistence (IndexedDB + localStorage) | `web/src/lib/store.js` |\n| PDF/image/zip ingest | `web/src/lib/ingest.js` |\n| Icon set | `web/src/brand/icons.jsx` |\n| Design tokens (colors, spacing\u2014the source of truth) | `web/src/styles/tokens.css` |\n| Sheet gallery / report UI | `web/src/components/` |\n| Pure-math tests (node:test) | `web/test/` |\n| **Optional AI backend** (pluggable adapter: scale/room/finish suggestions) | `server/`\u2014`app.py` + `adapters/base.py` (interface) + `adapters/heuristic.py` (default, no model) |\n\n## How the canvas works (the mental model)\n\n- Each open sheet renders into a `<canvas>` bitmap; **all takeoff geometry is an SVG overlay** on top; pan/zoom is a single CSS transform on the stage div, written imperatively (`tfRef` \u2192 `style.transform`) to avoid React re-renders per frame.\n- Coordinates: pointer events (client px) \u2192 `toImage()` \u2192 **stage px**; committed shapes store **normalized [0..1] vertices per sheet** (`verts_norm`), so quantities survive re-renders and zoom.\n- Cursor-following UI (crosshair hairlines, readout chip, rubber band) updates through **direct DOM writes in `moveCrosshair`**\u2014never React state per mousemove. Keep it that way.\n- Angle snapping: `angleSnap()` locks in-progress segments to the 45\xB0 family; endpoint snap (`nearestSnap` over a spatial hash of PDF vector endpoints) takes priority. The committed click reuses the same locked point (`angleRef`).\n- Past ~1.15\xD7 zoom, a **detail-view canvas** re-renders the visible region from PDF vectors at the current zoom (crispness); the base bitmap stays as first paint.\n- pdf.js rendering schedules work on `requestAnimationFrame`\u2014a fully hidden/occluded window will pause mid-render by design; it resumes when visible.\n\n## Conventions\n\n- **SVG presentation attributes take literal colors** (CSS vars don't resolve there): cobalt `#1f3fc7`, danger `#b03a26`, positive `#1f6b4a`\u2014centralized in `web/src/lib/ui.js` (`SVG`, with HUD-dark counterparts through `svgAccent(isDark)`). DOM/HTML chrome may use `var(--\u2026)` from `tokens.css`.\n- Condition palettes (`PALETTE` and `HATCH_IDS` in `web/src/lib/takeoffConstants.ts`, which both the canvas and `mcp/src/session.ts` import, and the seeded condition colors in `FLOORING_DEFAULTS` in `web/src/lib/canvasConstants.js`) are **user data**\u2014don't re-theme them. The same module owns the takeoff and report schema ids, `RENDER_SCALE` and the snap constants; parity tests in `web/test`, `mcp/test` and `protocol/test` fail if a copy reappears.\n- Waste applies only in the report (order quantities), never to live measured numbers.\n- Keyboard shortcuts are single letters registered on `window` (see `docs/USER_GUIDE.md` \xA715); toolbar menus pause them through `menuDepthRef`.\n- Brand voice: **precision instrument** (2026-08 overhaul). Light theme = \"ice\": bright white surfaces on a cool field, cool-slate neutrals, cobalt the one saturated thing. Dark theme = \"HUD\": true-black cockpit, electric blue `#3f8cff`, phosphor `--glow` on exactly five elements (active tool face, status verb, hero quantity, primary CTA, calibration dot). Square corners; the single sanctioned radius is `--r-1` on floating chrome. Mono tabular numerals on every readout. Drafting-table language stays. No vendor mimicry.\n- Layout/spacing/type come from the token scales in `tokens.css` (`--sp-*`, `--fs-*`, `--ctl-*`); zIndex comes from the `Z` ladder in `web/src/lib/ui.js`. No new magic numbers.\n\n## Docs to keep in sync when you change behavior\n\nThe draft Takeoff Protocol lives in [`protocol/`](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/README.md). When\nchanging persisted fields, consult its [inventory](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/INVENTORY.md) and\n[compatibility report](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/ACADEMY_COMPATIBILITY.md). Run\n`npm run check --prefix protocol` after installing web, MCP, and protocol\ndependencies. Refresh schema references with\n`node protocol/scripts/check-docs.mjs --write`. The draft does not authorize\nchanging writer formats or adopting a migration.\n\n1. `README.md` (Features + \"What's in the box\")\n2. `docs/USER_GUIDE.md` (shortcuts + the relevant section)\n3. `CHANGELOG.md`\n\nTouching the **MCP server** also requires checking its runtime guidance and\ngenerated references:\n\n4. `mcp/src/tools.ts`\u2014the tool's own `description` is part of its integration.\n An MCP client reads it at runtime. Initialize instructions and packaged wiki\n resources are also agent-facing surfaces, so keep them consistent with the\n runtime behavior.\n5. `mcp/server.ts`\u2014the `instructions` block sent at `initialize`. This is the\n decision tree every client receives before its first call. A new *verb* does\n not belong here; a new *step in the standard finish* does. Packaged wiki\n resources are another deliberate agent-facing surface; update their source\n pages and run the wiki/resource checks when their guidance changes.\n6. `mcp/README.md` (the tool table) and `docs/MCP.md` (the reach-for-it ordering,\n the example session, and the tool count in its opening line). A new tool also\n needs a row in `mcp/src/staging.ts`'s `TOOL_STAGES`\u2014the four lists must\n partition the tool set exactly, and a test fails CI if one doesn't.\n `npm run check:tool-count --prefix mcp` checks default, gated and setup counts across the current entry points,\n reference-table coverage, and [`docs/MCP_TOOL_INDEX.md`](takeoff://wiki/tool-index)\n against runtime schemas. Add `-- --write` to regenerate counts and the index. If the\n change alters *doctrine* rather than adding a verb\u2014what withholds, what\n refuses, what has no agent verb\u2014it belongs in `docs/AGENT_GUIDE.md` too,\n and the tool count appears there and in `docs/USER_GUIDE.md` \xA714.\n7. **Version fields must agree.** The MCP source version is\n in `mcp/package.json`; its lockfile has two matching fields, while\n `mcp/server.json` and `web/public/.well-known/mcp.json` each have two. The\n version checker checks all seven MCP fields. Separately, it checks the web\n package version against the two root-version fields in its lockfile.\n Check current `origin/main` and existing tags before\n reserving a number. The web package has its own version; it is not the MCP\n release number.\n8. `npm run check:tool-count --prefix mcp` runs version agreement before checking\n counts and the schema-backed inventory. With `-- --write`, it refuses version\n mismatches before changing generated documents; update release metadata\n explicitly, then regenerate. Run `npm run check:versions --prefix mcp` for the\n version check alone. Wiki content has its own generator:\n `npm run check:wiki --prefix mcp -- --write`. Review generated diffs and run\n both checks without `--write` afterward.\n\nArchitecture rather than behavior\u2014what MCP is versus what the `/ai` sandbox\nis, and why the server imports the web engine in-process\u2014lives at the end of\n[`docs/MCP.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/MCP.md) (\"Where this sits\") and in\n[`server/README.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/server/README.md).\n\n## The doc set, and who each one is for\n\nFour documents carry the product, and they're deliberately split by audience\u2014don't\nanswer an estimator's question in the agent manual or vice versa:\n\n| Document | Audience | What belongs in it |\n|---|---|---|\n| [`README.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/README.md) | everyone, ~60 seconds | what this is, the three doors, what's in the box |\n| [`docs/USER_GUIDE.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md) | the estimator at the canvas | every shipped UI behavior, the working order on a real bid, the glossary |\n| [`docs/AGENT_GUIDE.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/AGENT_GUIDE.md) | an agent driving the engine | the operating model, the standard finish, withheld doctrine, staging, refusal\u2192next-move |\n| [`mcp/README.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/mcp/README.md) | an agent's integrator | tool-by-tool reference, resources, coordinate contract, limits |\n\nAll of them follow one house style\u2014the Apple Style Guide, with the rules that\nactually come up written out in [`CONTRIBUTING.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/CONTRIBUTING.md#docs-house-style).\nInterface text quoted in a doc is copied from the code verbatim, so changing a\nmessage means changing it in both places.\n\n[`AGENT_BRIEF.md`](https://github.com/Kentucky-ai/opentakeoff/blob/main/AGENT_BRIEF.md) is the one-page orientation that routes to\nall four. A behavior change usually touches two of them; a new MCP tool touches\nthree plus this file's sync list above.\n"
15123
15157
  },
15124
15158
  {
15125
15159
  "key": "tool-index",
@@ -15493,7 +15527,7 @@ function nameTheStageInRefusals(server) {
15493
15527
  // package.json
15494
15528
  var package_default = {
15495
15529
  name: "opentakeoff-mcp",
15496
- version: "0.9.83",
15530
+ version: "0.9.85",
15497
15531
  mcpName: "io.github.Kentucky-ai/opentakeoff",
15498
15532
  type: "module",
15499
15533
  description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
@@ -15513,7 +15547,7 @@ var package_default = {
15513
15547
  "check:wiki": "node scripts/check-wiki.mjs",
15514
15548
  "check:versions": "node ../scripts/check-version-consistency.mjs",
15515
15549
  "check:tool-count": "node --import tsx scripts/check-tool-count.mjs",
15516
- test: "node --import tsx --test test/checkers.test.ts test/conformance.test.ts test/context.test.ts test/dxf.test.ts test/e2e.test.ts test/gate.test.ts test/graphEvalRowsym.test.ts test/labels.test.ts test/mep.test.ts test/overlap.test.ts test/parity.test.ts test/proposals.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/scope.test.ts test/session.test.ts test/staging.test.ts test/sweepguard.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts test/wiki.test.ts"
15550
+ test: "node --import tsx --test test/checkers.test.ts test/conformance.test.ts test/constants.test.ts test/context.test.ts test/dxf.test.ts test/e2e.test.ts test/gate.test.ts test/graphEvalRowsym.test.ts test/labels.test.ts test/mep.test.ts test/overlap.test.ts test/parity.test.ts test/proposals.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/scope.test.ts test/session.test.ts test/staging.test.ts test/sweepguard.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts test/wiki.test.ts"
15517
15551
  },
15518
15552
  dependencies: {
15519
15553
  "@modelcontextprotocol/sdk": "^1.12.0",
@@ -15582,7 +15616,7 @@ function buildServer(session = new Session(), opts = {}) {
15582
15616
  "Knowledge: read takeoff://wiki for the task router, then only the relevant takeoff://wiki/{page}. Pages cover capability limits, architecture, protocol, workflows, MCP routing and domain knowledge; takeoff://wiki/tool-index lists tool stages and required inputs. These resources work before loading a plan and do not change the session.",
15583
15617
  "A takeoff's deliverable is the marked-up planset, not a numbers report. Standard finish for ANY takeoff:",
15584
15618
  "1. load_plan, then set_scale on each sheet you measure (quantities are px-only until the scale is set).",
15585
- oneClick ? "2. Commit shapes under finish-tag conditions (one_click / detect_rooms / measure_polygon / measure_line with `condition`; when the set carries a room-finish schedule, prefer detect_rooms assign_from_schedule so each room commits under its OWN row). A COUNT takeoff of value-annotated device marks (GRDs, fixtures, equipment \u2014 the tag-over-value pattern) starts with count_marks {commit: true}: the whole census in one deterministic call, then audit its withheld entries \u2014 reach for the agent-driven per-mark tools only where it refuses or withholds." : "2. Commit shapes under finish-tag conditions (measure_polygon / measure_line with `condition`; a room's polygon is its wall faces \u2014 read them with get_sheet_vectors, confirm on view_sheet, and take the finish tag from the room's own schedule row via resolve_tag). A COUNT takeoff of value-annotated device marks (GRDs, fixtures, equipment \u2014 the tag-over-value pattern) starts with count_marks {commit: true}: the whole census in one deterministic call, then audit its withheld entries \u2014 reach for the agent-driven per-mark tools only where it refuses or withholds.",
15619
+ oneClick ? "2. Commit shapes under finish-tag conditions (one_click / detect_rooms / measure_polygon / measure_line with `condition`; when the set carries a room-finish schedule, prefer detect_rooms assign_from_schedule so each room commits under its OWN row; a traced ring sits on the INNERMOST wall faces from get_sheet_vectors, crosses doors on the wall centerline, wraps columns and stubs, never follows hatch or a door leaf, and is checked on a tight view_sheet overlay crop before the next room \u2014 takeoff://wiki/workflows has the rule set). A COUNT takeoff of value-annotated device marks (GRDs, fixtures, equipment \u2014 the tag-over-value pattern) starts with count_marks {commit: true}: the whole census in one deterministic call, then audit its withheld entries \u2014 reach for the agent-driven per-mark tools only where it refuses or withholds." : "2. Commit shapes under finish-tag conditions (measure_polygon / measure_line with `condition`; a room's polygon is its INNERMOST wall faces \u2014 read the strokes with get_sheet_vectors over a tight region, put every vertex on a face stroke, cross each door or cased opening on the wall's centerline so both rooms share that segment, run straight past windows, wrap columns, chases and wall stubs, never trace hatch, casework or a door leaf, then view_sheet a tight crop with overlay:true and fix the ring with edit_shape before the next room; take the finish tag from the room's own schedule row via resolve_tag; takeoff://wiki/workflows has the full rule set). A COUNT takeoff of value-annotated device marks (GRDs, fixtures, equipment \u2014 the tag-over-value pattern) starts with count_marks {commit: true}: the whole census in one deterministic call, then audit its withheld entries \u2014 reach for the agent-driven per-mark tools only where it refuses or withholds.",
15586
15620
  "3. DERIVE what follows from the rooms instead of re-measuring it: derive_base for base LF (perimeter \u2212 the door openings YOU state), derive_transitions for the line where two finishes meet. Both read committed floor shapes, so they come after step 2. derive_base draws the whole perimeter even where LF is deducted: when the handoff needs actual installed runs, use measure_line on the physical base segments and cut_out for located gaps. Do not clip a derived base that already carries numeric openings. Inspect finish splits, alcoves, jambs and withheld transitions in step 4.",
15587
15621
  "4. LOOK at what landed with view_sheet overlay:true and fix misses with edit_shape before trusting totals \u2014 crop the work region tight (full-sheet renders downsample too far to audit a ring).",
15588
15622
  "5. Finish by writing the marked-up planset with export_marked_pdf and give the user its file path, alongside export_report for the numbers. Never end a takeoff with numbers alone.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opentakeoff-mcp",
3
- "version": "0.9.83",
3
+ "version": "0.9.85",
4
4
  "mcpName": "io.github.Kentucky-ai/opentakeoff",
5
5
  "type": "module",
6
6
  "description": "OpenTakeoff MCP server — drive the takeoff engine from your MCP client over stdio.",
@@ -20,7 +20,7 @@
20
20
  "check:wiki": "node scripts/check-wiki.mjs",
21
21
  "check:versions": "node ../scripts/check-version-consistency.mjs",
22
22
  "check:tool-count": "node --import tsx scripts/check-tool-count.mjs",
23
- "test": "node --import tsx --test test/checkers.test.ts test/conformance.test.ts test/context.test.ts test/dxf.test.ts test/e2e.test.ts test/gate.test.ts test/graphEvalRowsym.test.ts test/labels.test.ts test/mep.test.ts test/overlap.test.ts test/parity.test.ts test/proposals.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/scope.test.ts test/session.test.ts test/staging.test.ts test/sweepguard.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts test/wiki.test.ts"
23
+ "test": "node --import tsx --test test/checkers.test.ts test/conformance.test.ts test/constants.test.ts test/context.test.ts test/dxf.test.ts test/e2e.test.ts test/gate.test.ts test/graphEvalRowsym.test.ts test/labels.test.ts test/mep.test.ts test/overlap.test.ts test/parity.test.ts test/proposals.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/scope.test.ts test/session.test.ts test/staging.test.ts test/sweepguard.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts test/wiki.test.ts"
24
24
  },
25
25
  "dependencies": {
26
26
  "@modelcontextprotocol/sdk": "^1.12.0",