What the first Linux CI run of the 1.6 tree found.
- fastmcp 3's enable()/disable() append a Visibility transform per call,
and a tool lookup walks the transform stack as nested calls. The
lifespan re-applies the tool profile on every client connection, so an
in-process server grew two layers per session until every tool call,
system_status included, raised RecursionError (CPython 3.11 counts C
frames against the limit). The profile's transforms now replace the
previous ones, as _apply_discovery_mode already did; a test switches
the profile 600 times and then calls a tool. The local suite runs in
half the time it took with the stack growing.
- 21 fake-ActiveX tests build a real pywin32 VARIANT; they now skip
without pywin32, like the suite's other VARIANT-dependent COM tests,
and the Windows lane keeps running them.
- A test spelled a path through a directory it never created, which only
Windows normalises away; it creates the directory.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The v1.6 roadmap in five tracks: 154 -> 247 registered tools in 28 groups.
Evidence re-measured for the release:
- correctness A/B against v1.5.1: 26/48 -> 48/48, 22 fixed, 0 regressed
(ab-v1.5.1-vs-v1.6.0.json, replacing the -dev report);
- the reference adapter on matrix v7: 21/21 (autocad-mcp-pro.json, which
was the v3 run of 1.5); the published-report test follows it to v7;
- the 100-point rubric re-scored per category, every withheld point named:
93 (the competitor rows are their July snapshots, not re-reviewed);
- the perf lane as medians of five alternating runs, beside a v1.4.0
comparison on one interpreter (perf-v1.4.0-vs-v1.6.0.json).
Two roadmap criteria are not green, and the changelog, the README and the
roadmap say so: criterion 5 is withdrawn (track D, Plant 3D, dropped) and
criterion 7 is not met (creation 1.25x / 1.38x v1.4.0's time, still
1.17x / 1.20x with the timeout off). Carried to 1.7.
README rewritten for 1.6: what is new, the mechanical and architectural
sheets, the rubric and task-matrix charts, the measured limits (including
the four feature tables that ship empty), and the 1.7 roadmap. server.json
describes the 1.6 surface and declares TOOL_PACKS. CLAUDE.md corrects the
ISO 286 letter set and the empty feature tables.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
- The exterior dimension chains follow the plot scale. DIMSCALE is the
plan's scale while they are drawn and is put back afterwards; a 1:50
chain's 2.5 text had been 2.5 drawing units, 0.05 mm on the sheet.
- A headless TABLE merges its title row, as AutoCAD's TABLE does; a long
title no longer wraps inside the first column over the header row.
- A headless TABLE's cell margin scales with the lettering. A flat 1.0 was
0.02 mm on a 1:50 schedule; tables at the default 2.5 text are unchanged.
- drawing_new writes the Standard text style in arial.ttf, as AutoCAD
2026's acadiso.dwt ships it: txt has no superscript two, so 'm²' read
'm?' live and rendered as a box headlessly. The five bundled templates
are rebuilt and build_templates --check reproduces them.
Each fix has a test that fails on the old code.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The takeoffs were run again on a real P&ID pair (kept outside the repository)
and compared with the hand-made workbooks. The user chose how three gaps
should close:
- Rooms drawn as frames. A room label that no wall face holds takes the
smallest closed, straight-sided polyline around it, provided it holds no
other room number and is at least ten label heights a side (label+frame,
0.8). A P&ID draws its rooms this way, so P&ID lengths are now cut at the
frames rather than sent to the nearest label. An unnumbered mention inside
a frame ('hot room') does not deny it.
- The nearest panel, flagged. cable_takeoff(panel_rule="nearest") is the
default: a load that no callout places takes the nearest panel on the
layout, among names with a panel prefix (is_panel_name: CP, JB, DB,
MCC ...) or names a callout gives. The row is flagged panel_inferred, and
an inferred panel never makes a package. panel_rule="stated" keeps the old
behaviour. The takeoff_roundtrip gate and the cable correctness check read
stated panels only: a guess is no evidence.
- Possible packages and detail copies. pipe_takeoff reports detail_copies:
compact groups of tags drawn twice (repeated), and compact groups of one
equipment's own parts, such as M150B, M150C and M150D of M150 (family). A
vendor package or an equipment detail shows up this way. Nothing is
removed until exclude_regions names a region. The runs left out go to the
control rows as excluded_region and to excluded_m. The workbook's method
sheet names the regions.
Each change has a synthetic test that failed first. Suite: 4986 passed, 1
xfailed. The inventory and the document numbers match (README: 4987
collected).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
A Russian P&ID often closes a load's text with the panel it is wired to:
'... кВт' on one line, then 'к CP1' ('to CP1') or 'к щиту CP-2' ('to
switchboard CP-2') on the next. wiring_target knew 'подключение к ...' but
not the bare preposition. On the field P&ID most loads therefore had no
panel and no measurable cable.
The preposition к / ко now counts when it is a whole word and the panel is
the next word, optionally after щиту / шкафу / панели. The к of кВт is
never read as the preposition. The new test failed before the change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Run on a real P&ID pair (kept outside the repository), the pipe takeoff wrote
an empty table. Three causes, each fixed with a test that failed first:
- Measured on the P&ID, a run still needed equipment tags at both ends,
which only routing on the layout needs. Few pipes on a field P&ID end on
a tagged outline. On the P&ID each piece now goes to the room it lies in:
cut at the room outlines when the P&ID has room faces, else to the
nearest room label. Diameters are kept piece by piece. On the layout a
takeoff that could not route every run now says how many, why, the drawn
length left to the control rows, and that length_source='pid' with
force=True measures on the P&ID instead.
- A TEXT stores its line breaks in caret notation ('6^JROOM'), which
plain() left as it was, so a room label's number read as '6^J...'.
plain() now decodes ^J, ^M, ^I and '^ ', leaving MTEXT stacks
('\S1^2;') alone.
- Labels that only mention a room were read as rooms: a control cabinet
naming the room it serves, or a note pointing into a room. An unnumbered
label that names equipment is no room. Without room faces, when the
drawing numbers its rooms, only numbered labels take pieces.
On the field pair the takeoff now fills its table, and every room key is a
real room number. The comparison with the hand-made workbook stays with
the user's files. README collected tests: 4981. The suite passes (4980, 1
xfailed).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The field test ran the takeoffs on a real P&ID pair, kept outside the
repository. It showed that the default network layers took every layer the
vocabulary files under piping or electrical. That included the valve layers
(KLEPPEN, Valve_*), the pump layers (a "product pumps" layer reads as
product), and annotation layers such as pipe sizes, materials and valve
numbers. Symbol outlines and leader lines became pipe runs.
vocab.is_network_layer is now the one rule. A layer is a network layer when
it is piping or electrical at 0.9, its name names no equipment
(equipment_kind: valve, pump, tank, motor, panel, reducer), and it names no
annotation (sizes, materials, numbers, hatching, or an annotation word).
build_network's default layers, drawing_topology_check and the topo_*
focuses all use it, so they agree on which lines are the network.
Sixteen parametrized cases pin it, across symbol, annotation, pipe and cable
layers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The evidence for track H, in the shape tracks A, E, B + G and F used.
- benchmarks/tasks_v7.py: 21 tasks, DEFAULT_MATRIX = "v7"; v6 to v2 stay
addressable. Both new tasks read the synthetic plant pair and nothing
else. takeoff_roundtrip: the P&ID called schematic, every routable run's
layout length equal to the rectilinear MST of its tags, sizes and services
as drawn (a return drawn on the supply layer included), the cable metres
rounded up after the allowance, an unstated power left empty.
understand_foreign: inches over millimetres flagged, the stray entity found
by handle, two plan copies separated, five layers classified. A test breaks
each input and watches its gate fail by name.
- Five correctness checks, every number worked out by hand in its docstring;
the published A/B report is re-run over 48 checks: 26 / 48 -> 48 / 48,
zero regressed.
- scripts/smoke_understand_com.py reads the pair through the live engine -
snapshots, the DWG route, both takeoffs and a diff - and closes every
document it opens; the CHANGELOG records what it returned.
scripts/field_test_takeoff.py runs the takeoffs on a user's own files,
refuses to write inside the repository and prints no quantity by default.
- The README token table is remeasured; check_doc_numbers derives the task
count from the v7 matrix; CLAUDE.md, both READMEs, the CHANGELOG and the
spec status line say what shipped.
One reader defect found on the way: describe.tag_occurrences read a wiring
callout ("Wiring to CP-2") as a place its panel is written, so the scale
check called CP-2 ambiguous and read "Wiring to CP1" as a phantom tag CP1 -
10 tags and 45 pairs (13.3 % within 10 %) where the generator placed 11 and
55 (21.8 %). A callout is no longer a tag occurrence, the rule
network.tag_occurrences already kept; two tests failed before the fix.
The smoke works only in documents it opened from its own temporary folder,
known by full path; it attaches with GetActiveObject only (the backend's
Dispatch fallback would launch AutoCAD), checks in the same COM-thread call
that the active document is its own before each snapshot, the DWG save and
the move, closes its documents with Close(False) and re-activates the
document that was active when it started. No new ActiveX member: Documents,
Item, FullName, ActiveDocument, Activate and Close are the ones the backend
already calls. Measured on AutoCAD 2026 (25.1s): exit 0, model-space types
equal file / live / DWG, export 0.104 s, no document of the run left open.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The three track-H branches are merged; this is the integration that makes the
six tools one surface.
- TOOL_PACK_NAMES gains "plant" (pipe_takeoff, cable_takeoff); SECTION 26's
four readers stay core. Lean gains drawing_understand and reaches 65 (55
with TOOL_PACKS=core). The `understand` and `plant` tags rank after `arch`,
so the sections file as `understanding` (4) and `plant` (2): 247 tools in
28 groups.
- discovery/aliases.py folds spec section 12's vocabulary into the six records
the branches wrote - metraj, metre cetveli, boru / kablo listesi, fark,
karsilastir, revizyon farki, olcek, birim in both Turkish spellings, BOQ,
takeoff, scale check, COMPARE - without writing a phrase twice. The Russian
terms are data only: the BM25 tokenizer keeps [a-z0-9], and no golden case
pretends otherwise.
- Eight golden tuning cases, four of them Turkish; the shared-command
allowlist reconciled with what the merged records claim; the token-suite
formula excludes the new pack; the inventory regenerated, the registry
snapshot retaken and the README figures taken from check_doc_numbers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Groups U and N were built side by side, so the takeoffs carried narrow
stand-ins for what group U reads properly. On main they read through U:
- describe.drawing_units is the one call every reader makes (kept records,
labelled room faces as evidence); describe, scale_check and the takeoffs
all use it. Without the room faces, a layout that declares inches over
millimetres can tie three ways and fall to the header.
- layout_tags takes its plan copies from describe.prepare, room_regions its
rooms from describe.find_rooms, and pipe_rows its scale from scale_check,
per-room groups included. scale_stand_in and the takeoff's own unit bands
and cluster grid are gone.
- test_understand_one_reading pins the agreement: one unit, one plan copy,
one verdict and one set of rooms per drawing, across drawing_understand,
drawing_scale_check and the two takeoffs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
pipe_takeoff merged line ends closer than tol = 1.0 drawing units and
silently dropped any segment shorter than that. On a millimetre P&ID that is
1 mm. On a metre drawing it is a metre: ends a metre apart were merged and
every pipe under a metre disappeared from the totals, with no warning. The
review of the network engine found this.
- tol now defaults to 1 mm in the unit the P&ID's geometry implies: 1.0 on
a millimetre drawing, 0.001 on a metre one. A tol the caller gives is
used as given.
- build_network counts the segments that collapse to a point
(stats.segments_below_tol), and pipe_rows turns a non-zero count into a
warning naming the tolerance.
A new wire test builds a metre P&ID with a 0.6 m pipe and a 5 m pipe. Before
this change the takeoff said 5.0 m; now it says 5.6 m. The network test
checks that a sub-tolerance stub is counted. Registry, packs, aliases and
every track H suite: 472 passed. The inventory matches the live surface.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
parse_tag started a token at the first letter, so '100-P-001', a line
number that begins with its size, read as the pump tag P-001. The review of
the network engine found this. On a P&ID the phantom tag can capture a
valve beside the line as equipment, or collide with the real pump P-001.
A token that is the tail of a word beginning with a digit is no longer a
tag. The same text still yields T101 in 'LINE 100-P-001 TO T101', and
P-001 alone is still a tag. The new test failed before this change; the
label, network, describe, scale, cable and plant-pair suites all pass
(160 tests).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The merge agent stopped this branch on an add/add conflict it had not been
told to expect: groups U and N each created tests/test_understand_tools.py.
Resolved by hand:
- tests/test_understand_tools.py stays U's (the drawing_understand wire
tests), and N's takeoff wire tests move whole to
tests/test_understand_takeoff_tools.py. Each file keeps its own client
fixture: U's opens a fresh document, N's reads files only.
- server.py: SECTION 26 (4 tools, U then D) is kept as merged, and SECTION
27 (Plant Takeoffs, 2 tools) follows it whole, above the RESOURCES
banner.
- discovery/aliases.py: additive; the two takeoff records follow the four
readers.
- tests/test_tool_registry.py: all three branches' paragraphs are kept.
The snapshot is recomputed from the live registry: analysis 18, 247
tools. Task 11 ranks `understand` and `plant`.
- docs/tool-inventory.json regenerated (247 tools, 26 groups); README
counts are 247 registered / 242 advertised. The collected-tests figure
is left to Task 11.
ruff check passes. Suite: 4919 passed, 1 xfailed. The one deselected test
is the collected-tests figure.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
cable_rows reads each load's power, phases, voltage, neutral and VFD from the
P&ID's electrical texts near its tag, and its panel from the layout's wiring
callouts (else the P&ID's), names normalised CP 1 -> CP-1. The length is the
Manhattan distance on the layout from the tag to its panel, and the cable
ROUNDUP(length x (1 + allowance)) to the metre, on the length rounded to
1e-6 m so binary noise never buys a metre. Power is never invented: a load
with no stated kW keeps an empty cell and an open item. Without a layout the
lengths stay empty with an open item per row. Room and panel totals count a
panel package's power once, never its children's as well; the rule is on the
method sheet. Sections only from the caller's section_rules - no standard
table exists here.
The workbook gains the cable shape (Metraj, Ozet by room and panel, Kontrol
as the open-items list, Metodoloji) in the same three languages;
cable_takeoff joins SECTION 27. Inventory regenerated; README snapshot 243
tools (238 advertised); registry snapshot analysis: 14.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The review of Task 9 left four minor findings. Three change what a user reads:
- A LINE drawn back to front and moved was reported as a geometry change.
The signature already ignores a LINE's direction, so its movement now
does too: the reversed order is tried before the entity is called
reshaped.
- An INSERT carrying one attribute tag twice was read as a dict. An edit
of the first value could disappear, and since the pair then had nothing
to report, it counted as unchanged. A repeated tag is now compared as
the list of its values; a tag carried once keeps plain from / to.
- cap_report capped the three lists but not the clusters. A widespread
edit of a large drawing could return an unbounded cluster list. It is
capped like the others, with truncated["clusters"]; the markup still
clouds every cluster.
The fourth bounds work: when the whole drawing moves, every entity reaches
the position stage, and a generous move_tol gave each one hundreds of
candidates, which is n x m memory. Only the 32 nearest per entity are kept
now. A 200-line test shows each line still pairs with its own moved copy.
The first three tests failed against the previous module (the attribute
test with no changed row at all). The whole diff suite passes: 38 tests.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
On the live engine, take_snapshot read a .dwg path by calling
Documents.Open(path, read-only), exporting and then Close(False) on what
Open returned. If the operator already had that drawing open, that was
their document, and closing it unsaved could throw away unsaved work.
drawing_understand, drawing_scale_check, drawing_diff and the takeoffs all
read a path through this function.
The review of drawing_diff raised the risk and did not probe it live, to
avoid a modal dialog on the shared AutoCAD. The fix makes the question
moot: the path is looked up among the open documents first, case-blind as
Windows paths are, and an open drawing is exported as it stands (unsaved
edits included) and left open. Only a document opened here is closed here.
The new test, against the fake that behaves as measured, fails on the old
code with an open and a close in the log.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
ezdxf's tag reader raises StopIteration on a file that ends early, for example
after an interrupted save or a partial copy. read_snapshot caught only OSError
and DXFError, so the StopIteration escaped. asyncio.to_thread cannot hand
StopIteration back to an awaiting caller, so every track H tool reading such
a file would have hung instead of refusing it.
The review of drawing_diff found this in its own reader. The snapshot now
does the same: every parse or walk failure except MemoryError becomes the
existing "is not a readable DXF file" refusal. A new test checks this both
directly and through a worker thread with a 10 s timeout. It failed with
StopIteration before this change and passes after it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
write_workbook builds the user's takeoff workbook from a result: Metraj (rows
with direct, continuity and unassigned lengths split, the allowance as its
own column, routed / unroutable / grand totals), Ozet (by service, diameter
and room), Kontrol (unassigned diameters and unroutable runs with the
reason) and Metodoloji, generated from the result: sources, the length
source and why, the scale-check verdict with its statistics, the units,
every rule and tolerance, and every warning. Headers and sentences in
Turkish, English or Russian. CSV always, one file per sheet; XLSX with
openpyxl, otherwise the xlsx_write refusal naming the office extra while
the CSV files are still written.
pipe_takeoff reads the P&ID and the layout once each (take_snapshot),
computes in a worker thread and never modifies either drawing. Tagged plant
and analysis; the plant pack is the integration task's. Alias record in
English, Turkish and Russian. Inventory regenerated; README snapshot 242
tools (237 advertised); registry snapshot analysis: 13.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
engineering/understand/topology.py reads a line network (LINE, polylines
with true bulge arcs, ARC) on the chosen layers and reports dangling
ends, near misses (end-end and end-segment within gap, one row per pair)
and interior crossings of two straight segments of one layer. An end on
another network line is joined; an end on any curve of another layer or
inside a block reference's box is attached; a line drawn twice is not a
join, so a doubled pipe still dangles. gap defaults to 0.2 % of the
network diagonal, never under ten tol.
drawing_topology_check joins drawing_diff in SECTION 26: default layers
are those classify_layer calls piping or electrical at >= 0.9, reported
with their classification; no such layer is a refusal, not a pass.
topo_dangling_endpoint (warning), topo_near_miss (error) and
topo_interior_crossing (info) join the closed enum and run under
focus=None, excluding the P&ID and architectural layer sets, so a
pid_from_spec sheet and an arch_plan_from_spec plan get none of them;
a drawing with no candidate layer is never snapshotted.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
route.py: Manhattan distance, the rectilinear minimum spanning tree (Prim
over |dx| + |dy|, exact on the points given) and the x-then-y route.
pipe_rows turns the P&ID's runs into rows by room x service x diameter.
length_source auto measures on the P&ID only when the scale check calls it
to scale; otherwise each run is the RMST of its tags' layout positions plus
vertical_allowance per connection, with the drawn length kept beside it as
schematic_m. length_source=pid on a schematic P&ID is refused with the
statistics unless force=True. Status A / B / C; a C run goes to control
with its schematic length, counted and flagged. Routed lengths are shared
among the layout rooms their route crosses (track F's face finder); the
allowance is its own column. Lengths are metres from the unit the geometry
implies: a layout declaring inches over millimetres is read in mm and warned
about, never multiplied by 25.4.
The takeoff carries narrow stand-ins for the unit inference, the plan-copy
clusters and the scale check (group U's modules are on another branch); a
scale result passed in is used first. Tags written twice in the chosen
cluster are ambiguous, never picked.
Beyond the plan: the scale stand-in calls a non-positive median ratio
insufficient (as group U's scale_check does) instead of to_scale with a
factor of 0; an INSUNITS code the reader does not convert is named in the
unit warning instead of being called "no unit"; wall layers are classified
once per layer, not once per record.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Three blocking review findings on drawing_diff.
1. An unedited entity reported as changed with changes == {} and clouded.
The signature snaps values to the tol grid, so a value 1 ulp across a
half-grid point lands in another bucket. AutoCAD writes 16 significant
digits (0.14 + 0.005 = 0.14500000000000002 comes back as 0.145), so a
round trip split the pair, which was matched later and counted as
changed. A matched pair is now compared by difference (more than half a
tol, around the circle for angles) instead of by grid bucket, and a pair
with nothing to report counts as unchanged, with no row and no cluster.
Measured live on AutoCAD 2026 (Documents.Open read-only, select all,
Export DXF, Close(False)): before, summary changed=1, a row with
changes {} and one cluster; after, unchanged=21, changed=0, no cluster.
Over 12000 computed coordinates (a*0.01 + {0.005, 0.015, 0.105, 0.305})
795 signatures flip; before, 795 were changed, and after, 0.
2. With zero exact matches, handles were declared stable, so strangers from
two fresh files that share handles were paired and given wrong moves.
The handle stage now needs at least one exact match. The report adds
handle_evidence {exact_matches, kept_handle}.
3. A truncated DXF raised StopIteration from ezdxf's tag reader.
asyncio.to_thread cannot hand that exception to an awaiting caller, so
drawing_diff hung instead of refusing. read_revision now turns every
parse or walk failure (not MemoryError) into ValueError "not a readable
DXF (...)". tol is validated first, so its refusal keeps its own name.
Tests: grid splits by handle and by position, radius/height/rotation/bulge
split at tol=1, a real edit next to a split, no-evidence handles, a
truncated file directly and over the MCP wire with a 30 s timeout. The six
new tests fail against the previous module.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
- A three- or four-way fitting joined its ports as a star around the port
seen first, so the diameter carried into an unlabelled branch depended on
record order. Every port pair is now bridged; two sizes equally near stay
unassigned whatever the order (six-order parametrised test).
- A reducer was seen only through free ends in its box. It is now also found
where pieces meet at a vertex inside its box (a line broken at the
reducer) and where a straight segment is drawn through the middle of its
box (cut at the foot of the box centre). A direct label holds for its
entity only on its side of every reducer.
- A diameter or direction label nearer to an open line on a layer that is
not read belongs to that line: it is dropped and listed in
stats.labels_skipped instead of landing on the nearest read pipe.
Headless engine only: no new ActiveX call, so no live probe applies.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Equipment tags found exactly once in each drawing are matched (a tag
found twice - a plan copy, a legend - is reported ambiguous and takes no
part; two plan copies resolve to the cluster that matches the most
tags). For every pair of matched tags at least min_pair_mm apart on the
reference, the ratio of their distances is taken in millimetres by each
drawing's inferred unit; the median, the quartiles, the share within
+/-10 % of the median, the same per room of the reference, and the
verdict: to_scale (>= 80 %, with its factor), schematic (<= 50 %),
partly, or insufficient under three matched tags.
server.py grows SECTION 26 to two tools with drawing_scale_check (core,
read-only, tagged analysis/query); alias record, inventory, README
release snapshot 243, frozen group sizes (analysis 14).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
build_network reads the segments of a foreign P&ID's piping and electrical
layers into runs: ends merged within tol, a branch ending on a header split
into a tee, a crossing without an end left unconnected, and a line drawn
twice - or a line over a polyline - cut into identical pieces that are kept
once, the doubled length reported as overlap_length. An INSERT holding two to
four free ends is an inline fitting joined through; a reducer word in its
block name (reducer, perekhod, reduksiyon, verloop, reduzier) stops diameter
continuity.
Each diameter label goes to the nearest piece and holds for its drawn entity
(direct); unlabelled pieces take the nearest direct diameter along the run
(continuity) or stay unassigned - two sizes equally near are never guessed.
Labels stay as drawn. A supply or return word near a run overrides its
layer's service for the whole run. Run ends attach to the tag whose labelled
geometry holds them; a shape holding two tags (a room, a skid) is not
equipment. Tests on hand-built drawings and on the synthetic plant pair.
Beyond the plan: a CIP / glycol layer naming no direction (classify_layer
files it `unknown` on purpose) now takes its direction from the word instead
of staying unknown; is_reducer matches on vocab.fold and knows the German
word equipment_kind already had; the fitting, equipment and attachment
lookups go through grid indexes - measured 149 s -> 2.8 s on a 59,200-record
synthetic drawing, results identical to the plan engine on it and on the
plant pair. No ActiveX call is added (pure module).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
engineering/understand/diff.py reads each revision once into entity
records and matches them in three stages: exact signature (type, layer,
geometry rounded to tol, text, block, rotation, scale, attributes;
handles ignored), then handle while at least half of the exact matches
kept theirs, then type + layer + nearest anchor within move_tol (default
2 % of the 1st-99th percentile diagonal). The report counts changes by
layer and type, says what changed per entity (moved by, text, attribute,
radius ... from -> to), compares named block definitions by content and
groups the changes into clusters.
drawing_diff opens group D's copy of SECTION 26. It is read-only unless
markup=True, which files revision clouds around the model-space clusters
on the current document through add_revision - refused before any read
with new_path, without a valid rev, or on an engine without revcloud.
A revision path that is neither .dxf nor .dwg is refused naming the
parameter (old_path: / new_path:), like every other path refusal.
Tests: the synthetic plant P&ID edited five ways (a move, a text, an
attribute, an add, a delete) under renumbered handles reports exactly
those five edits; renumbering alone reports nothing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
A drawing somebody else made, read back as one report in which every
claim carries its evidence and a confidence; the drawing is never
modified.
- units.py: $INSUNITS against the unit the geometry implies (text
heights, wall thickness, door swings, labelled room areas, overall
size). The header is overruled only when the evidence outweighs it,
and then with a warning that quotes the numbers. Robust extents grow
from the quartile box of entity centres; what never joins is an
outlier, named by handle.
- clusters.py: separate bodies of model-space content by raster,
8-connectivity and box absorption - two copies of one plan are two
clusters.
- describe.py: layers with drawn length and the vocabulary's meaning
(name keyword, else the texts on the layer), equipment tags (one tag
lettered twice beside its symbol merged, repeats flagged ambiguous),
room labels in the wall-layer faces of engineering.arch.faces, text
languages by character class, blocks, layouts, the title block.
- server.py opens SECTION 26 (Understanding & QA) with
drawing_understand: core, read-only, tagged analysis/query. Alias
record, inventory, README release snapshot 242, frozen group sizes
(analysis 13).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Every track H reader is measured on this pair, never on a client drawing
(spec section 14). tests/fixtures/plant_pair.py writes pid.dxf and layout.dxf
with invented names and returns their truth, computed from the positions it
placed:
- the P&ID is room 1 of the layout stretched 1.3x and room 2 rearranged, so
21.8 % of the 55 tag pairs fall within 10 % of the median ratio (schematic)
while room 1 alone is 100 % at 1.3;
- service layers in English and Dutch, Russian supply / return words, one CIP
return drawn on the supply layer, sizes spelled as drafters do (an MTEXT
format run, %%c, \U+2205, the U+2300 sign, o-stroke, SMS, DN, 20x27), a
reducer, inline valves, an arc bend, a segment drawn twice;
- electrical texts in Russian with the power stated for all loads but one;
- a layout declaring inches over millimetre geometry, a stray line 5,000 km
out, two plan copies 100 m apart, wall outlines, room labels and wiring
callouts in four spellings.
Run lengths are the rectilinear MST of each run's tags, cable metres are
the Manhattan length rounded up after the 20 % allowance; the generator
refuses to write a pair whose own premises fail.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Track H's foundation, landed on main before its three groups branch.
- engineering/understand/snapshot.py: one bulk read of a drawing into frozen
WCS records (OCS translated through backends/ocs.py, text decoded, an
INSERT boxed by its block geometry without its attribute texts). A DXF is
read with ezdxf after the size check; the headless engine's document is
read under its own lock; the live engine exports the active document with
Document.Export(<tmp>/snap, "DXF", <set of all>) - never renamed, deleted
afterwards - and a DWG is opened read-only, exported and closed. Measured on
AutoCAD 2026: ".dxf" is refused with Invalid argument, the file lands as
snap.DXF, model space, paper space and unused block definitions all reach
it, and the selection set does not narrow a DXF export.
- engineering/understand/labels.py: %%c, \U+2205, the U+2300 sign, o-stroke
and MTEXT format runs all read as one diameter spelling; sizes are kept as
drawn and never equated; Cyrillic look-alike capitals fold into Latin tags;
panels, wiring callouts and electrical data (kW, phases, volts, N, VFD) in
EN / TR / RU / NL / DE. A power that is not written stays None.
- engineering/understand/vocab.py: layer classification with a confidence and
the deciding keyword, supply / return words, room labels and block kinds in
five languages. A CIP layer without a direction word is not guessed.
- MAX_DXF_BYTES defaults to 512 MB (a real layout was 188 MB); the refusal
names the size and the variable. New `office` extra (openpyxl), also in
`full`; uv.lock regenerated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The spec promised that the first live task would measure xrefs. It never
did: the plan measures model space, paper space and block definitions. 1.6
does not read an xref's own entities on either engine; the host drawing is
what gets read. The snapshot docstring and drawing_understand's docstring
now say so, and nothing is claimed about the live engine that was never
measured.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The reconcile pass left one real defect for the reviewer. Groups U and N are
built side by side, so the takeoffs carry their own unit bands, cluster grid,
scale statistic and room faces, and nothing ever retires them. Two readers of
one drawing can disagree. On Task 6's own inches-over-millimetres layout,
infer_units without room faces ties three ways and falls back to the header,
while the takeoff's bands say mm.
Task 11 gains Steps 15-20, committed on their own before the README figures
are retaken:
- describe.drawing_units becomes the one unit call, used by describe,
scale_check and the takeoffs;
- the takeoffs read plan copies, scale and rooms through group U's modules;
- the stand-ins are deleted;
- tests/test_understand_one_reading.py pins one unit, one plan copy, one
verdict and one set of rooms per drawing.
Tasks 3, 4 and 6 now point at those steps. Review Focus 1 names
parse_diameters, the function every task uses.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
A second pass compared every tag, number and literal in the plan with the
field files, locally. Two borrowed values remained: a motor rating and one
motor tag. Both are now invented (18,5 kW, M82), and the dependent sums and
section rules were checked against them. The rest of the tank series was
renumbered to keep its old sort order, since several assertions compare
sorted tags: T4101-T4103, T4500 and T4600.
Docstrings no longer tie example layer names to a particular drawing. The
fixture's claim now says exactly what is invented (every tag and number) and
what is ordinary drafting vocabulary (layer and block names). The spec's
grounding paragraph now says "process plant" and no longer names the sector.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The skeleton's examples (and the tests drafted from them) borrowed equipment
tag numbers seen in the field drawings. They identify nothing on their own,
but the plan's own rule is that every example and fixture is synthetic, so
they are replaced by invented tags of the same shape and sort order
(T41xx-T44xx, M41A, M47.1, M82, CP-M82), Cyrillic spellings included.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The skeleton's header, constraints, file map and pinned interfaces now carry
their twelve tasks, in order:
- main (Tasks 1-2): the snapshot, vocabulary and label parsers, MAX_DXF_BYTES
512 MB and the office extra; the synthetic plant pair with its truth.
- U (Tasks 3-4): drawing_understand and drawing_scale_check; SECTION 26 opens.
- N (Tasks 5-8): the line network, orthogonal lengths and pipe rows, the
takeoff workbook with pipe_takeoff, then cable_takeoff; SECTION 27, pack plant.
- D (Tasks 9-10): drawing_diff and drawing_topology_check with the three
topo_* focuses, on D's own copy of SECTION 26.
- final (Tasks 11-12): packs, lean 65, aliases, golden queries and counts;
benchmark v7, five correctness checks, the live smoke, the field-test
script and the docs.
Interface pass: the U and N texts now say what Task 6 really does with the
scale check (it takes one through pipe_rows(scale=...) and otherwise keeps its
own stand-ins), and Task 11 moves D's block above SECTION 27 after the merge.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Header, global constraints (confidentiality, read-only by default, nothing
invented, inferred units), the five real-world inputs no happy-path test
exercises (AutoCAD text codes, Cyrillic look-alike tags, repeated tags,
double-drawn pipe, a layout whose INSUNITS lies), file structure, the
interface block, execution with the worktree fast-forward rule, and the merge
notes. Task sections are drafted next.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Read a drawing somebody else made and turn it into a deliverable: a one-call
drawing report (units that lie, stray extents, plan copies, multilingual layer
meaning), a scale check between a P&ID and its layout, pipe and cable takeoffs
whose topology comes from the P&ID and whose lengths come from the layout, a
revision diff by geometric signature, and topology checks. Grounded in a real
client project read locally (never committed): its P&ID proved to be
unscaled against its layout, which no tool had flagged.
Six tools (+ pack plant), three topo_* focuses, the office extra, a live DXF
snapshot through Document.Export (measured: the document is not renamed), and
synthetic fixtures that reproduce the field findings.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The evidence for track F, in the shape tracks A, E and B + G used.
- benchmarks/tasks_v6.py: 19 tasks, DEFAULT_MATRIX = "v6"; v5 to v2 stay
addressable. arch_roundtrip draws the example plan with
arch_plan_from_spec and reads it back without the spec: rooms_detect must
find each labelled room within 0.1 % of its label and of the net floor
computed by hand (4825 x 5750 and 3825 x 5750), the schedules must list
D1, D2, W1, W2, and the plan is gated on a zero-issue critique and a
finalize score of at least 90. A test removes one room label and the gate
fails naming arch_room_unlabelled.
- Four correctness checks, every number worked out by hand in the docstring:
L / T / X junction corners and wall areas, a 4 x 5 m room labelled 20.00 m2
rather than its 21.84 m2 axis area, a door interrupting both faces, and a
plan of plain lines read as 20 and 15 m2 at confidence 0.6. The published
A/B report is re-run over 43 checks: 26 / 43 -> 43 / 43, zero regressed.
- scripts/smoke_arch_com.py runs the whole plan once on a live seat and
closes every document it opens; the CHANGELOG records what it returned.
- The token suite measures a TOOL_PACKS=core,arch row; check_doc_numbers
derives the task count from the v6 matrix; CLAUDE.md, both READMEs and
the spec status line say what shipped.
- fix(com): the first live smoke exited 1 - the room reader found 0 wall
segments. Measured on AutoCAD 2026 (2026-09-23): with a makepy cache,
ModelSpace.Item(i) returns the gen_py IAcadEntity wrapper
(type(item).__name__ == "IAcadEntity") and item.StartPoint on an
AcDbLine raises AttributeError, so every entity_list row lost its
type-specific properties (28 wall LINEs, 0 with start/end). _entity_info
now re-dispatches exactly that wrapper through _com_unnarrow; after the
fix 38 of 53 rows carry start/end and the smoke exits 0 with both rooms
at 27 743 750 / 21 993 750 mm2 and 0 critique issues. Re-dispatch
measured at ~1.7 ms per entity (dynamic vs gencache alike).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The three track-F branches are merged; this is the integration that makes the
twelve SECTION 25 tools one surface.
- engineering/arch/spec.py: a whole plan from one document - walls with their
openings, stairs, room labels measured off the walls just drawn, the
exterior chains and the schedules - inside one transaction. Everything the
model, the wall engine or the stair symbol can refuse is refused before the
transaction opens, by path; a room point in no closed face rolls it back,
and so does a cancelled request, under the anyio shield of pid/spec.py.
EXAMPLE_SPEC's rooms measure 4825 x 5750 and 3825 x 5750 - the net floor,
computed by hand in the tests.
- engineering/arch/critique.py: arch_room_unlabelled, arch_wall_gap (50 mm,
one issue per pair of walls, measured to the wall body so an L corner and a
T stem both read as joined) and arch_opening_clash. One index per critique
run; a drawing without an ACADMCP_ARCH record yields nothing. Each focus
has a positive and a negative fixture.
- TOOL_PACK_NAMES gains "arch" (the twelve SECTION 25 tools); lean gains
arch_wall, arch_opening, arch_room and arch_plan_from_spec and reaches 64;
the arch tag ranks after mech, so SECTION 25 files as one `architecture`
group - 241 tools in 26 groups.
- Six golden tuning cases, two of them Turkish, the arch_plan_from_spec alias
record, the shared-command allowlist reconciled with what the merged records
claim, the inventory regenerated, the registry snapshot retaken, and the
README figures taken from check_doc_numbers rather than typed.
- Two more Turkish golden cases close the reconciliation's alias gaps
("vaziyet planı çiz" -> arch_plan_from_spec, "oda alanlarını hesapla" ->
arch_rooms_detect); the arch records carry vaziyet / alan / oda / kat planı
in diacritic and ASCII-folded spellings, because the search tokenizer splits
on a non-ASCII letter. BOUNDARY and AREA stay with boundary_trace and
analysis_measure_entity on purpose, recorded in the arch alias comment.
Tuning top-1 73/84, top-3 83/84; holdout top-3 15/15.
- tests/test_token_suite.py: TOOL_PACKS=core,mech now drops the arch pack
too (measured 193 = 236 - 9 - 22 - 12).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
engineering/arch/draw.py is the only module of the track that touches a
backend. It draws through draw_prims(role_layer=ARCH_ROLE_LAYER) and the
generic contract members, so it runs on both engines as written, and it writes
the plan onto the drawing: each element's ACADMCP_ARCH record on one of its own
entities (a wall's first face line, a door's leaf, a window's frame, a stair's
first riser) and, beside it under ACADMCP_ARCH_H, the handles that element
owns. read_plan reads the records back and never modifies the drawing.
arch_wall computes the whole network before and after the new walls and
redraws only the drawn walls whose outline changes - erasing exactly the
entities they own - and says which under redrawn. arch_opening hosts a door or
a window in a drawn wall found by id and redraws that wall cut; its tag
defaults to the next D1/W1 (K1/P1 in Turkish). A new wall that would leave a
drawn opening uncuttable, and an opening that runs into a junction, are refused
before anything is erased. arch_stair reports the Blondel value; it does not
enforce it.
exterior_chains gives each side up to three rows, nearest first: every jamb in
that facade, both faces of every wall meeting it, and the overall - from the
wall engine's own faces. A row that would only restate a farther one is not
emitted (ISO 129-1: each measurement once). The 10 mm / 8 mm row spacing is a
declared drafting default times the plot scale, not a standard value.
The arch tag is ranked after mech so the four tools file under a new
architecture group; alias records carry WALLADD/MLINE, DOORADD/WINDOWADD and
STAIRADD with English and Turkish shop words. Inventory regenerated; README
snapshot 233 tools (228 advertised) in 26 groups; registry snapshot
architecture: 4.
No new ActiveX member is called. Measured on AutoCAD 2026 in a scratch
document through ComBackend: shell + teed partition, D1 + W1, a straight 17-riser stair
and the bottom chains gave the same 20 wall LINEs (identical endpoints),
9 poche HATCHes, 1+1 door, 4 window lines, 11 dimensions and a read_plan
round trip as the headless engine.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
engineering/arch/schedule.py turns the plan records into schedule rows: doors
by tag with width, height, swing, hand and wall; windows by tag with width,
height, sill and wall; rooms by number with name and the measured area the
label carries, plus a total. Tags compare as numbers (D2 before D10). An
opening without a tag gets the next free D1 / W1 - K1 / P1 in Turkish - in
wall order; an explicit tag is never renumbered and a generated one never
repeats it. A value the model does not carry is an empty cell, never a default,
and Turkish output writes decimal commas.
draw_schedule reads the ACADMCP_ARCH records off the drawing through the one
reader rooms.py already has and draws a real TABLE the way the ISO 7573 parts
list does - top-left insertion, the table growing down, the parts list's 7 mm
rows and 2.5 mm text at the plot scale, the engine's representation reported.
Nothing to schedule, an unknown kind or language and more rows than one TABLE
takes are refused before anything is drawn.
arch_schedule joins arch_room and arch_rooms_detect under SECTION 25's rooms
marker (banner 3 tools), shares the TABLE command with entity_create_table and
bom_table, and the counts follow: 232 tools in 25 groups.
Live fix in com_backend.entity_create_table. MEASURED on AutoCAD 2026: IAcadTable
has no TextHeight property. pywin32 raised AttributeError on
`table.TextHeight = h`, the old code swallowed it, and every live table kept
the Standard style's heights (title 6.0, header and data 4.5), which is 0.1 mm
on paper at 1:50. The member that works is SetCellTextHeight(row, col, h),
probed on a scratch document: every cell read back 125.0 via
GetCellTextHeight. The row heights stayed 350.0 and GetBoundingBox matched the
reported bbox (10000,3250)-(15750,5000). Then draw_schedule ran end to end
through ComBackend on a scratch document: a native TABLE on A-SYMB-T-N for
doors (5 rows) and rooms/tr (6 rows, total 45,00 m²), and the document was
closed afterwards. The new fake in tests/test_table_leader.py has only the
measured members.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
openings.py draws what stands in the gap the wall engine cuts, from the same
numbers - the wall axis, its face offsets and model.segment_of - so the symbol
cannot drift off its gap. A door is drawn open at 90 degrees: its leaf as long
as the opening is wide, square to the wall from the hinge on the swing-side
face, and a quarter arc from the closed position to the open one. swing=in is
the left of the axis (the inside of a CCW perimeter); hand names the hinge jamb
seen from the swing side facing the wall. All four swing/hand pairs are tested
against hand-computed hinges and arc angles. A window is a frame line on each
face and two glazing lines at the thirds of the thickness.
stairs.py draws every riser once as a line across its flight, the outline, a
walking line up the middle with an open arrow at the top, and the language's
'up' word on the first tread at 2.5 mm times the plot scale. An L or a U turns
on a square landing; its first flight carries (risers + 1) // 2 risers. The
riser count and every dimension are refused by name before any geometry.
blondel() reports 2R + G against 600-650 mm - the step-length rule attributed
to Francois Blondel (1675), reported as a design rule and never as a building
code: no national stair regulation is checked.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
read_segments trusted any row with `points`. On a live seat an old-style
AcDb2dPolyline's Coordinates are x, y, z triples (measured on AutoCAD 2026,
2026-09-23: [200,200,0, 4200,200,0, ...]) which the backend reads two at a
time, so three closed ms.AddPolyline outlines on WALLS reported points like
[[0,0],[0,8600],[0,0],[8600,5400],...] and rooms_detect answered five faces of
2.6-6.9 m2 for two 20 m2 rooms. Headless, the same POLYLINE arrives with no
points and was dropped without a word; so were walls inside a block reference.
The reader now reads LINE and the lightweight polyline only (LWPOLYLINE, and
the live AcDbPolyline reported POLYLINE), and lists every other entity that
could bound a room in `skipped` with its handle, type and reason: arcs and
bulged edges as before, old-style polylines (2DPOLYLINE/3DPOLYLINE live, a
vertex-less POLYLINE headless), block references (INSERT / BLOCKREFERENCE)
and anything else not a line. Text, dimensions, leaders, points and hatches
bound nothing and are passed over. Re-run live on AutoCAD 2026 through the
real com_backend._entity_info rows: count 0 with the three polylines in
skipped (was 5 wrong rooms); with a lightweight pair and a block added, count
1 at 20 000 000 mm2 plus the block reference in skipped. The backend's stride
is left alone (shared file, other readers); this reader no longer trusts it.
label_room refused a reused id but not a reused face: a second label in a
labelled room (a retry after a transport timeout) wrote a second record with
the same area, which a schedule total counts twice. It is now refused before
anything is drawn, naming the record and label already there. rooms_detect
reports `labels` (every record in the face; `label` stays the first) and
`label_conflicts` for a face holding more than one.
No new ActiveX member is called.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
A T recorded its cut on the crossing segment as a distance from that
segment's start; when the start was carried onto a third wall later in the
same pass the cut went stale, so [c,b,a] drew a free-end jamb inside wall a,
an uncut face and a double-hatched overlap where [c,a,b] was right. Cuts are
now stored as points and measured against the final axis.
A near-straight join whose faces are offset (a thickness or justification
change, or a rounding kink between such walls) mitred at offset/sin(kink):
faces 1e8 mm long and a clockwise region for a 0.01 mm rounding. A
straight-through pair whose mitre lies more than _MITRE_REACH (2) thicknesses
from the node now steps on the line bisecting the join - both sides of a
two-end node together, so the pieces tile without a sliver - and every piece
and hub ring is refused unless counter-clockwise, not only when it crosses
itself.
Pure geometry: no ActiveX member is touched, so no live probe applies.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd