fix(task-6): the reader no longer hands a signal line the neighbouring pipe's label

pid_graph step 5 ran the TEXT label fallback for every edge without a
number. Once signal lines stopped carrying a fabricated number (3c0babf),
every pneumatic/electric run drawn by pid_line_draw entered it, and a
transmitter-to-controller run 12 mm above a labelled header came back as
line_number="100-P-1", number_source="label" — on both engines, with no
confidence field on edges to flag it.

The fallback now skips an edge that carries its own record (an
ACADMCP_PID payload: its `number: null` is the answer) and any
signal-class edge, payload or layer-classified (`signal_unknown`
included): LINE_NUMBER_RE is the pipe grammar, so a match near a signal
line always belongs to a neighbouring pipe. A foreign process line still
takes its label; a verbatim number on a signal line still reads back
from XDATA.

Tests: the finding's layout in test_pid_graph (engine-drawn signal beside
a labelled pipe, verbatim signal number, foreign electric and
signal_unknown lines beside a labelled foreign pipe) and the line_list
row in test_pid_deliverables — three of the four fail on the old reader.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-21 22:44:35 +03:00
co-authored by Claude Opus 5
parent 3c0babfef1
commit cfb5d5082c
3 changed files with 108 additions and 2 deletions
+19 -2
View File
@@ -12,7 +12,7 @@ import re
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from .lines import LAYER_TO_CLASS, PID_LINE_LAYERS
from .lines import LAYER_TO_CLASS, LINE_CLASSES, PID_LINE_LAYERS
from .symbols import Port, transform_port
from .tags import parse_tag
from .xdata import read_payload
@@ -55,6 +55,15 @@ CONFIDENCE = {"catalog": 1.0, "xdata": 0.95, "heuristic": 0.6, "inferred": 0.3}
_PAGE = 1000
def _is_signal_class(line_class: str | None) -> bool:
"""True for a class that is a signal in the ISA-5.1 sense (`signal_unknown`
is the layer-classified one that LINE_CLASSES does not list)."""
if line_class == "signal_unknown":
return True
cls = LINE_CLASSES.get(line_class) if line_class else None
return bool(cls and cls.kind == "signal")
@dataclass
class _Node:
id: str
@@ -684,7 +693,15 @@ async def build_graph(
if found:
node.tag, node.tag_source = found, "text"
for edge in edges.values():
if edge.line_number:
if edge.line_number or edge.payload or _is_signal_class(edge.line_class):
# The label search is a fallback for a line that carries no
# record of its own. An engine-authored line (payload) is that
# record — its `number: null` is the answer, not an invitation to
# adopt the nearest pipe's label. A signal line never has a pipe
# line number (ISA-5.1), and LINE_NUMBER_RE is the pipe grammar,
# so any match near it belongs to a neighbouring pipe: the
# ordinary transmitter-to-controller run 12 mm above a header
# would otherwise be listed under the header's number.
continue
a, b = max(zip(edge.vertices, edge.vertices[1:], strict=False), key=lambda s: math.dist(*s))
found = _text_near(