Commit Graph
18 Commits
Author SHA1 Message Date
Umut CANandClaude Opus 5.5 fb2953c324 fix(understand): a line number is not an equipment tag
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
2026-09-24 12:48:56 +03:00
Umut CANandClaude Opus 5.5 27d44e909e merge: track H group N
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
2026-09-24 12:48:12 +03:00
Umut CANandClaude Opus 5.5 cfcc8ce6bb merge: track H group D
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-24 12:32:37 +03:00
Umut CANandClaude Opus 5.5 d0e99bab7b merge: track H group U
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-24 12:30:30 +03:00
Umut CANandClaude Opus 5.5 459d3b9228 feat(understand): cable_takeoff - one row per load, rounded to the metre
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
2026-09-24 12:28:59 +03:00
Umut CANandClaude Opus 5.5 1795dc7f2c fix(understand): drawing_diff minors from the review — direction, repeated tags, bounded output
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
2026-09-24 12:26:56 +03:00
Umut CANandClaude Opus 5.5 cdff63fce0 fix(understand): a DWG the operator has open is exported as it stands, never closed
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
2026-09-24 12:24:02 +03:00
Umut CANandClaude Opus 5.5 efa1ea8182 fix(understand): a truncated DXF is refused by the snapshot, not left hanging
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
2026-09-24 12:22:23 +03:00
Umut CANandClaude Opus 5.5 653535bcc1 feat(understand): the takeoff workbook and pipe_takeoff (SECTION 27 opens)
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
2026-09-24 12:22:15 +03:00
Umut CANandClaude Opus 5.5 2b74bf20a6 feat(understand): drawing_topology_check and the three topo_* critique focuses
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
2026-09-24 12:20:24 +03:00
Umut CANandClaude Opus 5.5 e640def4ff feat(understand): orthogonal lengths and pipe takeoff rows
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
2026-09-24 12:14:17 +03:00
Umut CANandClaude Opus 5.5 30f886c240 fix(task-9): no phantom changes from the tol grid, no handle stage without evidence, a truncated DXF refused
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
2026-09-24 12:12:40 +03:00
Umut CANandClaude Opus 5.5 7bfba02ef0 fix(task-5): fittings bridge every port pair, reducers found however the pipe meets them, unread-layer lines keep their labels
- 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
2026-09-24 12:08:47 +03:00
Umut CANandClaude Opus 5.5 7aaead43d9 feat(understand): drawing_scale_check - is this drawing to scale against that one
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
2026-09-24 11:55:57 +03:00
Umut CANandClaude Opus 5.5 1426c53fd3 feat(understand): the line-network engine - runs, services, diameters, equipment
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
2026-09-24 11:53:03 +03:00
Umut CANandClaude Opus 5.5 54325e6de0 feat(understand): drawing_diff - two revisions matched by geometric signature
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
2026-09-24 11:51:19 +03:00
Umut CANandClaude Opus 5.5 2459694880 feat(understand): drawing_understand - units, extents, clusters, layers, tags, rooms
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
2026-09-24 11:50:41 +03:00
Umut CANandClaude Opus 5.5 9ab0ae1773 feat(understand): the snapshot, the vocabulary and the label parsers; MAX_DXF_BYTES 512 MB; the office extra
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
2026-09-24 11:38:59 +03:00