diff --git a/README.md b/README.md index c476ea9..31b8c38 100644 --- a/README.md +++ b/README.md @@ -419,3 +419,21 @@ wondering what the tool assumed. Those are preserved but will not resolve without the linked file. - Description joins can drop a space where Candy's print engine split a word (`Fixed-ChargeItems`). Cosmetic. + + +### Where the placement check stands + +The check is sound — silent on Blueberry, fires on Central Towers — but it has +exposed that **the alignment drift is not fixed.** Fifteen lines on Central +Towers are still placed on the wrong row, fourteen of them carrying a value. + +A gap-penalty sweep from -0.20 to -1.50 moves it not at all, so this is not the +aligner chasing scores. Both documents contain **four separate near-identical +depth-band groups** in `1200DB` alone, and a single global alignment +distributes them wrongly. Closing that needs a different strategy — anchor on +the confidently matched lines and interpolate within each block, rather than +solving the whole document as one problem. + +Until then **Central Towers is blocked and will not produce a file.** That is +the correct behaviour: the app now knows it is wrong and refuses, rather than +sending it. diff --git a/boq_engine/template.py b/boq_engine/template.py index c690a97..c6f4316 100644 --- a/boq_engine/template.py +++ b/boq_engine/template.py @@ -371,6 +371,12 @@ class Slot: # a description opening with a lettered sub-item label (a), b), c)) starts a # NEW line rather than continuing the one above LETTER_ONLY = re.compile(r"^\(?[a-z]\)\s", re.I) +# a description that OPENS with its own reference - ".01 ", "1.2 ", "c) " - +# starts a NEW line. Only bare continuation text may be folded into the row +# above. Excluding only "a)" left ".01 0.5 - 1.0m Deep" being swallowed by the +# sub-group heading above it, which is how a whole depth band went missing from +# the pool and its rate drifted onto the next group's rows. +STARTS_LINE = re.compile(r"^\(?[a-z]\)\s|^\.?\d+(?:\.\d+)*[\s.]", re.I) # ".01", "1.2", "a)" - a description that OPENS with a reference starts a new # line; only bare continuation text may be folded into the one above STARTS_LINE = re.compile(r"^\(?[a-z]\)\s|^\.?\d+(?:\.\d+)*[\s.]", re.I) @@ -426,7 +432,7 @@ def read_slots(ws, lay: Layout) -> list[Slot]: # A description opening "a)" / "b)" / "c)" is a NEW line and must never # be absorbed, or sub-items get folded into the heading above them. if (out and priced and not raw and desc and out[-1]._bare - and not LETTER_ONLY.match(desc)): + and not STARTS_LINE.match(desc)): prev = out[-1] prev.desc = f"{prev.desc} {desc}".strip() if prev.desc else desc prev.row = r diff --git a/boq_engine/verify.py b/boq_engine/verify.py index 0e7adaf..cd31320 100644 --- a/boq_engine/verify.py +++ b/boq_engine/verify.py @@ -180,6 +180,15 @@ def _placement_report(result, out) -> list: row_desc = _norm_txt(_row_desc(out[sheet], row, lay)) want = _norm_txt(want) + # A cell holding a formula is not the row's identity - these templates + # pull a description straight out of a Measurement sheet + # ("=Measurement!A18"). The row may be perfectly correct; we simply + # cannot read it, so say nothing rather than cry mismatch. + # any formula inside the text means this row pulls its description from + # elsewhere (these templates use "=Measurement!A18"); it may be the + # right row and simply not readable here + if "=" in row_desc: + continue if not row_desc: bad.append((sheet, row, want[:44], "that row carries no text at all")) continue