The BOQ Grammar
Version 1.11 — the canonical description of how this system reads a Bill of Quantities.
Select any passage — a sentence, a table row, a rule name — and an Add comment button appears. Everyone reviewing this copy sees the same thread and can reply. Every comment is recorded against v1.11, so it stays clear what wording you were reading.
This document is the single source of truth for three consumers:
The app —
boq-sheet-parser.tsimplements Parts II–III, withnaming.ts(full names, §2.8) andzones.ts(the zone band, §2.9);validation.tsimplements Part IV and the ladder (§3.7), withrulings.ts(§4.4) anddeclared.ts(§6.6b);verification.tsimplements Part V;summary-sheet.tsreads Part VI;report.tsandfindings.tsbuild Part VIII. Code cites this document; this document describes the code. When they disagree, one of them has a bug — the disagreement is surfaced, never silently resolved.The version in this document's header is also a constant in the code (
GRAMMAR_VERSION,src/lib/types.ts), stamped into every audit and every report. Bumping one without the other is itself a defect: a finding that records the wrong grammar version cannot be re-judged later.The audit agent — the Deep Audit agent (see
DEEP_AUDIT_AGENT.md) receives this document as its domain knowledge. Its probes test the claims made here against the raw workbook.The client — this is the methodology statement. When a reviewer asks "how does it know?", the answer is a section reference in this document.
Everything here was learned empirically from real tender workbooks (~4,000 items across four bills, reconciled to the printed grand totals to the cent) — none of it is assumed.
Part I The Mirror Principle
§1.1 The app digitizes the file 100% as it is
The mirror stage has one job: hold in structured form exactly what the Excel file says — no more, no less, no judgement. It does not decide what the bill should contain. A blank tender, a rate-only schedule, a fully priced bill, and a structure-only skeleton are all valid documents whose blanks are facts, not defects.
This foundation is what later makes comparison possible: blank BOQ vs PTE vs multiple bidders (description changes, high/low rate errors) — all consume the same faithful mirror. If the mirror is 100% honest, every later stage is easy; if the mirror flattens or invents anything, every later stage inherits the lie.
§1.2 The three honest states of a cell
Every value cell (Quantity, Rate, Amount) is in exactly one of three states:
| State | Meaning | Stored as |
|---|---|---|
| number | The cell holds a numeric value (including a genuine 0) | quantity / rate_aed / amount_aed |
| text | The cell prints words ("RATE ONLY", "Incl.", "-") | verbatim in quantity_printed / rate_printed / amount_printed; the numeric field holds what the totals count (0 for amounts, null for qty/rate) |
| empty | The cell is blank | null (qty/rate) or amount_state: "empty" — empty is NOT zero; it is "unpriced", a different fact |
The Amount cell carries an explicit amount_state: "number" | "text" | "empty" so the three states survive export and comparison. Numbers stay primary; printed text is collected and considered, never discarded and never allowed to silently become a number.
§1.3 How a number is read
A cell counts as a number when its raw value is numeric, or when its text — after stripping thousands-commas — matches ^\(?-?\d+(\.\d+)?\)?$ (parentheses mean negative: (1,234.50) = −1234.50). Text-formatted numbers ("2,854" stored as a string) are still numbers in the source. Anything else — "###" overflow, "-", "Incl.", "RATE ONLY", blanks — is text or empty. Because the parser reads cell values, not the printed page, it is immune to ### column-width overflow.
Cell text is read by the same mirror rule in reverse: a text cell contributes its raw string (superscripts survive — m² stays m², never flattened to m2), while a numeric cell contributes its formatted display — that is what the sheet prints, including - for a format-suppressed zero.
Formula error cells (#REF!, #VALUE!, #DIV/0!, …) carry a cached numeric value in the file format, but the sheet prints the error token — so they are text, never numbers: an item amount of #REF! is captured verbatim in amount_printed with state text, counting 0 in the totals. Both engines apply this rule identically.
§1.3b Classification follows the printed page, values follow the cell
Accounting formats print zero as - (or blank). A stray =E×C formula can therefore put a hidden numeric 0 under a heading row. The rule that resolves this class of trap: row classification asks what the cell prints — a row is "description-only" when its Qty/Rate/Amount cells display no number (the displayed text does not parse as a number) — while item values keep the stored number: an item whose Amount prints - over a stored 0 still counts 0, exactly as the source's own totals count it. One rule, no format-specific or dash-specific hardcoding, and it covers every zero-suppressing format at once.
§1.4 Bill profiles — detected, never assumed
The document's nature is detected from content (validate()), with these exact thresholds:
| Profile | Condition (in priority order) |
|---|---|
empty |
zero items |
priced |
≥ 30% of items have a non-zero numeric amount that is not printed text — a blank tender whose amount cells hold =qty*rate formulas evaluating to 0 is not priced |
rate-only |
otherwise, ≥ 50% of items carry a numeric rate |
unpriced |
otherwise, ≥ 50% of items carry a numeric quantity |
structure-only |
otherwise |
Profile gates judgement: e.g. text amounts on a rate-only schedule are its nature, not per-line surprises (§4, NON_NUMERIC_AMOUNT).
Part II The shape of a BOQ sheet
§2.1 The table
A BOQ worksheet contains one table, located by its header row: the first row (searched within the first 41 rows and 13 columns of the used range — both windows inclusive and relative to where the sheet's data starts, not to A1) that labels, case-insensitively, at least Item (exact word), Description… (prefix), and Amount… (prefix). Quantity/Qty, Unit, and Rate are recognized when present but optional. A sheet with no such header row has no BOQ structure and is rejected (the sheet picker disables it).
Rows above the header row are cover/title material. Every row below it is classified by the row taxonomy (§2.2). A repeated header row (Item in the item column and Description… in the description column) marks a printed-page break and is skipped.
§2.1b Split-quantity layouts — the quantity column identified arithmetically v1.8
Some books split the quantity across several columns per row — zone/tower quantities plus their total — under headers no word-list can anticipate ("TOTAL QTY" with an embedded line-break, "NET QTY", another language). When no Quantity/Qty header matches but Rate and Amount do, the quantity column is identified arithmetically, never by its header text: it is the unmapped header-labelled column whose value satisfies value × rate = amount across the sheet's rated rows (a numeric rate and a non-zero numeric amount).
Guards, all structural:
- Minimum evidence — at least 20 qualifying rows, covering at least half of the sheet's rated rows; small sheets keep
quantity: null(safe failure) rather than guess. - Match share ≥ 95 % — a handful of genuine qty × rate ≠ amount lines are findings (
LINE_ARITHMETIC), not disqualifiers. - A partial column cannot win — a zone column satisfies the identity only on rows where it is the sole contributor; the total column matches a strict superset, and more matches wins. An exact tie means the values are identical; the rightmost column is taken (a total follows its parts).
- Provenance on the record — the promotion is written to
parse_profileswithsource: "arithmetic"and a note stating the column header and the match count (e.g. "qty × rate = amount on 6144/6144 rated rows"). Arithmetic profiles are records, not instructions: the identity is re-derived fresh from the cells on every parse. - The mirror is untouched — the promoted column also stays verbatim in
extra_columnsunder the sheet's own header label; promotion only fills thequantityfield. The verification engine re-derives the same identity with independently written logic before trusting the parse's quantity sums.
§2.2 The row taxonomy
Every non-empty row below the header is classified by testing, in this exact order (earlier tests win — an ALL-CAPS row that is note-shaped is a note, never a section title):
| Order | Row kind | Recognition |
|---|---|---|
| 1 | Grand-total row (§3.3) | matches carried to summary / grand total / bill total with no item code |
| 2 | Collection row (§3.1) | matches to collection / to summary / carried to|forward / c/f (case-insensitive) with no item code — even printed in an off-column with no value: an unvalued marker closes its block with a null checksum, surfacing as MISSING_COLLECTION, never silently |
| 3 | Brought-forward row | matches brought forward / b/f with no code — skipped (its value was already counted where it originated) |
| 4 | Spacer | no text in code/description/unit and the Amount cell prints no number (§1.3b) — skipped |
| 5 | Repeated header | Item in the item column and Description… in the description column — a printed-page break, skipped |
| 6 | Item (§2.3) | has a reference code and a description — or prints an amount and a rate/qty without a code (a genuine source anomaly, transcribed and flagged) |
| 7 | Reference note (§2.6) | description-only row (prints no numbers, §1.3b) that is note-shaped |
| 8 | Continuation marker | matches cont'd / contd / (cont (apostrophe optional) — skipped |
| 9 | Section title (§2.5) | description-only row with no lowercase letters, < 60 chars, no ; |
| 10 | Governing heading (§2.5) | any other description-only row — it describes work |
| 11 | Anything else | subtotal-shaped rows without a collection marker — skipped; collection rows are the only totals treated as data |
Total/collection/brought markers (rows 1–3) are matched against the space-joined text of the first 11 columns of the used range — a marker printed beyond column 11 is not seen (narrower than the 13-column header search; a deliberate bound, stated here so no one is surprised by it). "Description-only" everywhere in this taxonomy means prints no numbers in Qty/Rate/Amount (§1.3b) — a hidden formula-zero under a dash format does not make a heading row an item row.
§2.3 What an item is
An item is a row with a printed reference and a description:
item := (code && description) || (no code && description && amount is a number && (rate or qty is a number))
The second arm transcribes priced-but-unreferenced rows faithfully; validation then raises MISSING_ITEM_REF (an error), because every priced line must carry its printed reference — this is a real anomaly seen in real bills (including a 14.99M unreferenced line).
An item whose Unit is ITEM and whose Rate holds no number (empty or text — "Incl.", "RATE ONLY") is a lump sum (is_lump_sum); when the rate cell held text, rate_printed still preserves it verbatim.
Header-labelled columns outside the standard six (e.g. a "VE Ref." column carrying "EM-16") are captured verbatim per item into extra_columns — the sheet's own header declared them item data, so the mirror carries them. No column name is special-cased; any labelled header cell not mapped to Item/Description/Qty/Unit/Rate/Amount qualifies — within the same 13-column header window as §2.1 (a labelled column beyond the 13th column of the used range is not seen; stated here so no one is surprised by it).
Each item gets a unique internal id SheetName/blockOrdinal-code; an item with no printed code uses r{ExcelRow} in place of the code (e.g. 2B/3-r57), and when the same printed code repeats within a block, ids get #2, #3… suffixes — printed codes are kept exactly as printed, ids stay unique.
§2.4 Full names — every item reads on its own
An item's full_description is [section, ...heading_path, own_description] joined with " — ". A QS seeing only this line must know exactly what is being priced. The heading stack rules:
- A section title (§2.5) resets the heading stack.
- Consecutive heading rows stack (deeper nesting).
- A heading row appearing after items under the previous heading replaces the stack (a sibling heading, not a child).
§2.5 Section titles vs governing headings
A description-only row (no code, no numbers) that has no lowercase letters, is shorter than 60 characters, and contains no ; is a section title — provided it is not note-shaped or a continuation marker, which are tested first (§2.2 order). "No lowercase letters" is the literal test: a cell holding only digits or symbols ("-", "2019") technically qualifies and resets the heading stack — a known sharp edge, stated so nobody is surprised. Any other description-only row is a governing heading — it governs the items beneath it and enters their names.
§2.6 Reference notes — collected, never in names
A description-only row is a note when it is note-shaped: starts with the word Note followed by : or whitespace ("Note: …" and "Note all dimensions…" both qualify), or is a bare numbered line (1) … / 2. …), or cites drawings/specifications (drawing, specification, refer to) without describing work. A sentence that describes work is a heading even when it cites a drawing — real headings do; "work-shaped" means it contains ; or the words including/includes. Note-shape wins over section-title shape (§2.2): "REFER TO STRUCTURAL DRAWINGS" is a note, not a title.
Notes are preserved verbatim in the block's section_notes[] and shown to the reviewer. They are never folded into item names. Known gap: notes that appear after a sheet's final collection row (trailing end-of-sheet notes with no items after them) currently have no block to attach to and are dropped — a candidate grammar/parser fix (§7).
§2.7 Printed page markers v1.9
Some books print their own page reference between items — a bare bill/section/page token (2/F/24) alone on its own row, the footer of the printed page (the same grammar §6.4 documents on summary sheets). These are the addresses that words like "Included in 2/F/24" point at, so the mirror captures them: each item carries printed_page, the first marker at or after its row (footer semantics — a page's items sit above its marker).
Detection is by shape, never position: a marker row carries no item code and no description, and exactly one cell prints the anchored token. " Page 2/B/1" in a summary line never matches (prefixed); a row printing two different tokens is ambiguous and skipped. A book that prints no markers yields no printed_page anywhere — the field is absent, never inferred.
Not every printed page carries items: the Zaabeel main bill prints 500 markers, of which 468 pages hold items — the other 32 are per-section collection-summary pages. In this family the marker row is usually also the block's "To Collection" row (footer and checksum share the line), which is why item-carrying pages coincide with blocks; the grammar still treats pages and blocks as separate facts, because nothing guarantees that coincidence in another book.
§2.8 The levels a bill nests a line under v1.11
A line is not a flat record. Every book nests it, and the app now holds each level as its own fact rather than collapsing them into one string. Five levels, from the outside in:
| Level | Field | Where it comes from |
|---|---|---|
| Bill | bill_ref |
the first token of the line's printed page marker (§2.7): 2/C/7 → "2" |
| Section | section_ref |
the first two tokens of the same marker: 2/C/7 → "2/C" |
| Group | section |
the ALL-CAPS section-title row above the line (§2.5) |
| Headings | heading_path[] |
the governing headings stacked above it (§2.4) |
| Line | description |
the line's own text |
The naming is a historical accident worth stating plainly, because it misleads on sight: the field called section is not the section. It holds the ALL-CAPS work-group heading the sheet prints — one book carries 75 distinct values of it against 25 real sections — so it is a level below section_ref, not the same thing. It is called the group everywhere a person can see it.
bill_ref and section_ref exist only where the book prints page markers. A book that prints none carries neither, and every check keyed to them reports itself inapplicable (§3.7) rather than guessing.
A section is named from two sources, and the workbook's own summary is senior. A section_ref is an address (2/C), not a name. The title comes from:
- the declared summary — the label on the summary row whose page range resolves to that ref (§6.6b). Senior everywhere, because the workbook states it about itself as a whole.
- the bill sheet's own printed title (
section_titles) — the zero-item ALL-CAPS heading that a section's first item follows. Used only where the summary names nothing, which is the common case for a bill declared to its summary as a single row.
Capture rule for the second source: a section-title row (§2.5) that is superseded before any item appears under it is a printed section heading rather than a work-group heading, and it belongs to the section of the first item that follows it in its sheet. COLLECTION and SUMMARY are excluded by name — they are the collection page's own caption, always zero-item, never a title. Where several such headings precede the same first item, the last binds (the nearer heading is the section's; the earlier ones are the previous section's tail). This is mirror data: read from the sheet, never inferred, and absent when the sheet prints nothing.
One assembler, used by both writers. full_description is built by a single function (naming.ts) in the bill's own nesting order:
Bill 2 — CONCRETE WORK (2/C) — SUBSTRUCTURE — Reinforced concrete; grade C40 — Beds, 300 thick
└ bill └ section title + ref └ group └ heading path └ the line
The parser builds names at import; the workspace rebuilds one when a QS edits a line. Those were two implementations, and the one in the workspace silently dropped the bill and section levels on every edited line — which raised FULL_NAME_MISMATCH on lines nobody had broken, and made "resolve" appear to do nothing (resolving the flag created a fresh one). Two assemblers cannot be kept in step by discipline; there is one.
§2.9 Zones — the second decomposition v1.11
Some books price every line against several zones (basements, podium, towers) and carry a per-zone amount band beside the Amount column, with the tender's Grand Summary declaring the same split per section. That is a whole extra rung: 25 sections × 6 zones is 150 independent checks on figures the workbook printed for itself.
The band is identified arithmetically, never by column letter or label list — the same discipline §2.1b applies to the quantity column. It is the run of header-labelled captured columns (extra_columns, §2.3) whose values add, row by row, to the line's own amount. That is the identity the bidders write into their own check columns (IF(SUM(M:R)=L,TRUE,FALSE)), so the app re-derives a rule the source states rather than inventing one.
- An accounting dash in a zone cell is zero (a zone that contributes nothing); a cell printing anything else disqualifies the row from the test, never the column.
- A book with no such run yields no band, and every zone check reports itself inapplicable — the
ZONErung is simply not drawn (§3.7). - The band is a reading of the mirror; the columns stay verbatim in
extra_columnsregardless.
Part III Blocks, checksums, and totals
§3.1 The collection block — the unit of accounting
A "To Collection"-style row closes a block, and its printed value is the block's checksum — recorded, never computed. Blocks are the only unit the source itself reconciles, so they are the only unit we reconcile.
- A block can span printed pages (pages without a collection row carry their lines forward into the next page that has one).
- A block can never span two worksheets — the carry resets at a sheet boundary; an unclosed block there is flagged, not merged onward.
- A collection row arriving when no items are open is a re-statement (an end-of-sheet summary re-listing): adopted as the sheet's carried-to-summary figure when it matches the running total (within 1.00), ignored otherwise.
- A trailing block with no collection row is transcribed with a
nullchecksum and flaggedMISSING_COLLECTION— it cannot be reconciled against the source.
§3.2 The parser never computes a checksum
source_collection_aed is what the source printed. Reconciliation happens later, in validation, by comparing the printed checksum against the independently summed lines. The parser transcribes; the validator judges; the two never blur (this is the app's own layer-separation law).
§3.3 The grand-total remainder rule
A grand-total row (Carried To Summary / Grand Total / Bill Total) is a sheet-level re-statement, not a block checksum — except that real bills print two shapes:
- a sheet whose only total row is "Carried To Summary" (the whole sheet is one block), and
- a trailing 0-value block after a final "To Collection".
One rule covers both: if a block is still open when a grand row arrives, the block's checksum is the remainder = grand value − running total (adopted only when ≥ 0, else null). Separately, when the grand value matches the running total within 1.00 it is adopted as the sheet's carried-to-summary figure.
The bill ends at its final grand row (v1.4). A valued grand row lying after the sheet's last collection row (grand rows themselves are not collections — the "Carried to Summary" wording overlaps the collection markers and must be excluded from that look-ahead) begins the annex: item-shaped rows after it (elemental cost summaries, analyses — real books append them) are counted as annex rows, never items. A valued grand row with collection rows still ahead is a running re-statement, not the end — one observed sheet carries 11 such re-statements across 468 blocks. Keyed to structure already in the grammar, never to annex wording.
§3.4 The totals (extended v1.7, v1.11)
Five figures, each answering a different question. None of them replaces another, and every one of them is disclosed beside the others — a single number labelled "total" is exactly the shape of the lie this section exists to prevent.
| Figure | The question it answers |
|---|---|
| billTotal | What does the source carry? The sum of printed amounts (text/empty amounts contribute 0, exactly as the source's own page totals do). |
| arithmeticTotal | What would it carry if line arithmetic ruled? qty × rate on every rated line (excluding lump sums, allowances, and text amounts), the printed amount elsewhere. |
| arithmeticAsSubmitted | What would it carry if line arithmetic ruled and nobody had touched it? The same view computed from the figures the source prints (§4.5), so a reviewer's own changes are never mistaken for the bidder's arithmetic. |
| excludedTotal | What is held out of the picture, and by whose decision? The sum of sheets a QS placed outside the measured works (§4.3). |
| adjustedLines | How much of this is ours rather than theirs? The count of lines carrying a QS figure change — always shown next to a total that reflects one. |
arithmeticTotal − billTotal quantifies exactly how much the source's internal line contradictions are worth. arithmeticTotal − arithmeticAsSubmitted quantifies exactly how much of that is the reviewer's own doing. Keeping only the first pair is what allows a reviewer's correction to be quietly attributed to the bidder, which is the dishonesty the pair exists to prevent.
§3.5 Reference conventions
Item references follow a convention detected per collection block (each block's codes are examined independently — there is no sheet-level convention) — single letters (A–Z), plain numbers (1–999), or other (dotted, mixed — no order assumed):
- Letters legitimately restart at each printed page and at each heading change (e.g. General Requirements: single-item groups each lettered "A" under their own sub-heading). Restarts are conventions, not errors.
- Within one ascending run, under one heading context, a repeat is suspicious (
DUPLICATE_ITEM_CODE). - A single restart to the sequence start with no heading change suggests a missed heading (
REF_SEQUENCE_RESET); multiple restarts in a block mean the block spans printed pages — a paging convention, not an error. - Convention other → order is unknowable; only exact repeats within a heading context are flagged.
- Marker tokens (v1.6): a non-numeric code (≥ 2 chars) repeating ≥ 3× within one block is a change/annotation marker living in the code column ("ADD" = addendum line), not a reference. Markers are excluded from convention detection and duplicate checks, kept verbatim on every item, and reported once per block as an info note (
CODE_MARKER) — never as hundreds of duplicates.
§3.6 Excluded sheets are the workbook's self-declared truth
Sheets the parser rejects (covers, flysheets, summaries) are not discarded — they are gold. The workbook's own summary sheet re-states, in its own hand, what each bill sheet should total. That self-declared truth is used two ways: the SUMMARY_ECHO proof (§5) checks our totals appear verbatim in the summary sheets; the Deep Audit's SUMMARY_MAP reconciles the complete summary against the complete import and raises the alarm on unaccounted money — a summary line whose amount matches no imported sheet.
§3.7 The reconciliation ladder v1.11
The checks of Parts IV and V are not a flat list. They are a ladder, and each rung closes the money at one level of the bill's own nesting. Every rung compares figures the source printed for itself — never a figure the app invented.
| Rung | Closes | Against | Rule |
|---|---|---|---|
| LINE | one line | quantity × rate vs the printed amount | LINE_ARITHMETIC |
| PAGE | a collection block (§3.1) | summed lines vs the printed collection checksum | PAGE_COLLECTION |
| SECTION | a printed section (§2.8) | summed lines vs the summary's declared section total | SECTION_COLLECTION |
| ZONE | a section × zone cell (§2.9) | summed zone column vs the summary's declared split | ZONE_COLLECTION |
| BILL | the sheet | collection checksums vs billTotal | BILL_ROLLUP |
| SUMMARY | the tender | billTotal vs the carried/declared figure | SUMMARY_TIE |
Each rung carries checked — whether the source printed the figures the rung compares — alongside applied, the number of comparisons that actually ran, and a detail sentence saying so in the source's own terms.
A rung nobody could check is never drawn as one.
This is the whole point of the structure. A book that prints no page markers has no SECTION rung and no ZONE rung; a book that prints no collection rows has no PAGE rung; a bill whose lines carry no rate has no LINE rung. Reporting those as passing would be a lie of exactly the kind that is hardest to catch, because it looks like success. They report as not checked, with the reason: "No line prints both a quantity and a rate, so the line identity cannot be tested on this bill." Six rungs of which four could be checked is a 6-rung ladder with 4 checked, never "4/4 pass".
Part IV The Flag Catalog
Every flag the validator can raise, with its exact trigger. Errors never leave the review queue; acknowledged warnings do. Field names use the historical _aed suffix; actual currency comes from document.currency.
| Rule | Sev | Trigger (exact) |
|---|---|---|
LINE_ARITHMETIC |
error | Numeric qty and rate, not lump-sum/allowance, no printed-text amount, and |qty × rate − amount| > 0.01 + |qty| × 0.005 (tolerance covers a printed rate being a rounded display of a fuller-precision rate). Suggests the implied quantity amount ÷ rate. |
PAGE_COLLECTION |
error | A closed block's summed lines differ from its printed collection checksum by more than max(0.02, 0.01 × items in block) (per-line display rounding accumulates ~1¢/item). |
BILL_ROLLUP |
error | Printed collection checksums exist and their sum differs from billTotal by more than max(0.02, 0.01 × total items). |
SUMMARY_TIE |
error | A non-zero carried-to-summary figure exists and differs from billTotal beyond the same tolerance. |
SECTION_COLLECTION |
error | The imported lines carrying a section reference (§2.8) sum to something other than the total the workbook's own summary declares for that section, by more than max(0.02, 0.01 × lines in the section). Raised only for sections that are both declared and imported — a section declared but absent from this import is a scope fact, reported by SECTION_TIE (Part V), never as an error. |
ZONE_COLLECTION |
error | The same comparison one cell deeper: a section's lines summed per zone column (§2.9) against the per-zone figure the summary declares for that section. Sections × zones, each cell an independent check, same tolerance. |
FULL_NAME_MISMATCH |
error | full_description does not contain the item's own description, every heading in its path, and its section — structure, not a fixed format. |
MISSING_ITEM_REF |
error | An item with an empty reference code (§2.3). |
UNRESOLVED_DITTO |
error | The full description still contains ditto / as above / -do- / d.o. / do. — the item does not read on its own. |
NON_NUMERIC_AMOUNT |
warn / info | Amount cell prints text. info when the printed text matches an accepted QS class ruling (§4.1) — recorded and visible everywhere, never queued. Otherwise warn, raised only when the profile is priced — on a rate-only schedule text amounts are the document's nature (recorded on the item, not flagged). Counted as 0.00 either way, exactly as the source's own page total does. |
UNPRICED_LINE |
warn | The line carries no money and no rate: no printed-text amount, rate holds no number, and the amount is 0 (a numeric 0 or an empty cell). Raised only when the profile is priced — on a rate-only or unpriced schedule blank amounts are the document's nature. A numeric 0 is not evidence of pricing: an =qty*rate formula over an empty rate writes a real 0 (§1.3b), which §1.4 already refuses to count as priced. An empty rate is not a rate of zero — a bidder who printed 0 has spoken, and that line never triggers this. Nothing else catches the shape: NON_NUMERIC_AMOUNT needs printed text, LINE_ARITHMETIC needs a rate to multiply, and a 0 sums correctly into every total above it — an unpriced hole is self-consistent, so every consistency check passes it. |
MISSING_COLLECTION |
warn | An unclosed trailing block (§3.1) — lines total is stated but cannot be reconciled. |
BARE_DESCRIPTION |
warn | Structural only: a leaf ≤ 25 chars with an empty heading path on a block where other items do have heading context — a governing heading may have been missed. Flat bills that never use headings are never flagged. |
REF_SEQUENCE_RESET |
warn | Ordered convention, exactly one restart in the block, and that restart has an unchanged section + heading path (§3.5). |
DUPLICATE_ITEM_CODE |
warn | A repeat within one ascending run under one heading context (§3.5). Kept as printed. |
RECONSTRUCTED |
warn | (AI path only) A line rebuilt from a misaligned table — verify against the source. |
LUMP_SUM_PRICED |
info | A lump-sum line that prices itself by repeating the amount in the Rate cell while leaving the quantity at 0. The qty × rate identity cannot apply — a zero cannot be the multiplier that produced this amount — so LINE_ARITHMETIC deliberately does not run on it. Recorded here rather than left silent. Nothing is being asked of the QS, only disclosed, so it never queues. |
CODE_MARKER |
info | A non-numeric code (≥ 2 chars) repeating ≥ 3× within one block is a change/annotation marker living in the code column, not a reference (§3.5). Reported once per block, never as hundreds of duplicates. |
§4.1 QS class rulings — the queue only ever contains open questions
A class ruling is a recorded, document-level QS decision: lines whose Amount cell prints this text (normalized: lowercase, collapsed whitespace) are the nature of this bill — accepted. Stored on the document (printed_text_rulings: class, label, decision, timestamp), exported with it, and honored by validation: ruled lines carry the info variant of NON_NUMERIC_AMOUNT — fully visible on every line, never queued. Properties:
- Explicit — rulings are offered at sheet selection (all unticked; every acceptance is an act) and in the review rail; never assumed, never preselected.
- Reversible — revoking a ruling returns its lines to the review queue immediately.
- Item-independent — a ruling never edits items; the mirror (verbatim text, counts 0) is untouched.
- Audited — the Deep Audit agent runs a class-homogeneity probe per ruling: every member's printed text must truly match the class; an outlier hiding inside a blessed class is reported as a new finding. A blanket ruling cannot bury an anomaly.
- Classes emerge from the data — whatever the source printed; no hardcoded list.
§4.2 QS error resolutions — closing an error on the record
Errors block acceptance — unless a QS closes one on their own responsibility with a recorded resolution: a mandatory reason and timestamp stored on the item. The flag remains fully visible (marked resolved); the data is never touched; the resolution is reversible ("Reopen") and travels with the document (exports carry a "QS Resolution" column). Document-level structural errors (block/rollup/summary reconciliation) are not item-resolvable — they indicate the parse or the source arithmetic itself is broken and must be fixed, not waived.
§4.3 QS scope exclusions — a sheet held out of the totals v1.7
QS practice sometimes carries a whole sheet outside the measured works — most commonly General Requirements / Preliminaries, which the team totals apart from the bills proper. A scope exclusion is a recorded, document-level QS decision: this sheet stays imported, mirrored and reviewable, but its sum is held out of the bill total. Stored on the document (sheet_exclusions: sheet, timestamp), toggled per sheet in the review rail, persisted and exported with the document.
What changes and what does not:
- Only TOTALS scope changes.
billTotaland the arithmetic (qty × rate) view both skip excluded pages, so they always describe the same scope. The excluded sum is reported separately (excludedTotal) and always disclosed next to the total ("excl. GR1 270,300,000.00") — a scoped total is never presented as the whole picture. - Every line-level check still runs. Excluded sheets keep their mirror, flags, rulings, resolutions and block reconciliation (
PAGE_COLLECTION) — the sheet is out of the sum, not out of review. - Document reconciliation respects the scope on both sides.
BILL_ROLLUPcompares the scoped total against only the included sheets' collection figures.SUMMARY_TIEcompares against the workbook's carried figure — which covers all sheets — so the tie is checked asbillTotal + excludedTotalvs carried. Exclusion can never manufacture a reconciliation: the full picture must still tie. - Explicit and reversible — no sheet is ever excluded by default or by name-matching; the QS clicks, the record shows when, and one click re-includes it. The classifier has no notion of "GR-like" sheets — scope is a human decision, never a parser inference.
§4.4 Scoped QS rulings — one determination at the reach its evidence supports v1.11
§4.1 rulings key on the words the Amount cell prints. That reaches "By Others" and "Rates Only"; it can never reach a line whose Rate cell is simply empty, because an empty cell prints no class to rule on. One real submission carries 1,801 of those — and nobody clears 1,801 identical questions one line at a time, so the queue was theatre.
A line ruling generalises §4.1 past printed text: a recorded QS determination answering one finding, at whatever scope the evidence supports.
- Keyed by finding, not by text.
RULING_OPTIONSmaps a rule to the determinations that rule's own evidence can support. A finding becomes rulable by adding options, never by adding machinery, and a finding is only ever offered choices that make sense for it — a priced line is never asked to justify not pricing. - Four scopes, offered narrowest first:
line·section(section_ref) ·bill(bill_ref) ·document. A QS should have to reach for reach. - The reach is counted before it is taken. The lines, amount and quantity a scope would cover are computed over the lines that actually carry the finding, and shown before the decision commits — so "all 1,801" is a number the reviewer saw, not a promise they took on trust. What it covered is frozen onto the record (
scope.covered). - The narrowest wins. A ruling made about this very line outranks one about its section, which outranks one about the whole submission. A bulk decision can therefore always be corrected line by line without being revoked — which is what makes a bulk decision safe to take at all.
- Reasons strong enough to need justifying, do. "Genuinely nil cost", "our reading is wrong" and "another reason" require a written note; "covered by another rate" does not.
- It is not an edit. No quantity, rate or total moves. The finding stays visible on every surface; it stops being an open question (
warn→info), which is precisely what an accepted class already does (§4.1). Reversible, timestamped, exported with the document, and carried across a re-import.
The queue only ever contains open questions. A ruling answers questions; it does not hide them.
§4.5 Figure overrides — a change that sits beside the source, never on top of it v1.11
A QS may change a quantity or a rate on a line. The change is stored as a FieldOverride carrying from — what the workbook holds — together with to, a mandatory reason, and a timestamp. One entry per field; editing again replaces it.
- The mirror is never overwritten. A submission whose figures can be edited without trace is not a mirror of anything, and a total that silently mixes the bidder's numbers with ours cannot be defended to the bidder.
fromis read once, at the moment of the change, and is authoritative thereafter. It is a realnumber | null: a source that prints nothing has afromofnull, and that is a fact, not a missing value. Code that recovers the source figure must therefore test whether an override exists — never fall back through??, which silently readsnullas "no override" and re-attributes the reviewer's own number to the bidder on every unpriced line.- Both arithmetic views are kept (§3.4), so the bidder's own arithmetic and the arithmetic after review are always separable, and
adjustedLinesis disclosed beside any total that reflects a change.
§4.6 Implied quantity — the bidder's own arithmetic, marked as inferred v1.11
Where a line's Amount cell prints a figure and its Quantity cell prints 0, a zero cannot be the multiplier that produced that amount. The quantity the bidder's own arithmetic implies is amount ÷ rate, and it is shown — marked ✻, with the footnote stating that the cell itself is unchanged.
This is a reading, not a repair. It is never written to quantity, never entered into a total, and never used by a check. It appears only where a reviewer would otherwise read a printed 0 as a measured zero. LINE_ARITHMETIC surfaces the same figure for the same reason (impliedQ), and neither one moves the mirror.
Part V The Proof Catalog
After every deterministic parse, the verification engine reverse-engineers the result against the raw workbook with independently written logic (it deliberately re-implements row classification rather than importing the parser — an independent pair of eyes, not the same code checking itself). Proofs are pass/fail with detail, shown in the review UI. Fifteen of them, in three families: the transcription against the raw cells (SOURCE_READ … MIRROR), the import against what the workbook declares about itself (SUMMARY_*, SECTION_TIE, ZONE_TIE, TENDER_TIE, AMOUNT_IN_WORDS), and the source confirming us in its own hand (SUMMARY_ECHO).
A proof that could not run passes with a detail sentence saying why, exactly as a ladder rung reports itself unchecked (§3.7) — "This workbook carries no summary sheet, so it declares nothing to check against" is an honest pass; a silent absence would not be.
| Proof | What it re-derives |
|---|---|
SOURCE_READ |
(failure only) the stored workbook could not be re-read. |
ITEM_COUNT |
Independent per-sheet re-count of item rows equals the parsed item count. |
QTY_INTEGRITY |
Per-sheet sum of raw quantity cells equals the parsed quantity sum (±0.01). |
AMOUNT_INTEGRITY |
Per-sheet sum of raw item amounts equals the parsed amount sum (±0.01). |
ROW_COVERAGE |
Every money-bearing row is accounted for: an item, a block total, a brought-forward row (its money was counted where it originated), a summary re-statement (a non-item row whose value equals a known block checksum — excluded, would double-count), or debris (#REF!/#VALUE!/#DIV/#N/A/#NAME — a broken leftover formula, no meaning). Anything else is the real alarm: unaccounted money on an unclassified row. |
MIRROR |
Cell-for-cell fidelity: re-walks every raw item row in order and compares description, code, and the number/text/empty state of qty, rate, and amount against the stored item — value for value, text for text, empty for empty. This is the proof the app holds the file "100% as it is". Known gap: for qty and rate, a raw numeric cell paired with a parsed null/text state escapes the check (the comparison resolves to NaN > tolerance = no mismatch) — one-directional blindness, candidate fix (§7). |
SUMMARY_ECHO |
Our non-zero sheet totals appear verbatim in the workbook's own unselected (summary) sheets — scanned across all rows but only the first 13 columns of each sheet's used range — the source confirms our totals in its own hand. |
SUMMARY_MAP |
The complete reconciliation of §3.6, per the algorithm in §6.6: every summary entry matched to imported totals (with chain resolution through summary levels), self-checks that entries sum to declared totals, unmatched non-zero entries raised as unaccounted money (collapsed by value across summary levels), text amounts honored as declared-unpriced, printed zeros as declarations, stray off-column numbers reported. Emitted even when no summary sheets exist (stated honestly). |
SUMMARY_DECLARED |
Whether the workbook declares anything at all, and what: the bills, sections, zone columns and totals read from its summary sheets (§6.6b). Emitted even when there is no summary — "this workbook carries no summary sheet, so it declares nothing to check against" — because an absent declaration is a fact about the submission, not a gap in the report. |
SUMMARY_SELF_CHECK |
The summary against itself, before anything is compared to the import: each declared subtotal against the entries beneath it, and the grand total against the bill subtotals. A workbook that contradicts its own summary is a finding about the workbook — and one that must be established before its figures are used to judge ours. |
SECTION_TIE |
Every declared section total against the imported lines carrying that section_ref (§2.8). Reports four populations separately and never merges them: tie to the cent, differing (both figures given, no winner picked), declared but not in this import (a scope fact), and imported but not declared. Lines carrying no section reference are counted and stated. |
ZONE_TIE |
The section × zone grid (§2.9): each section's per-zone sums against the summary's own declared split. Inapplicable — and says which — when the book carries no zone band or the summary declares no zone columns. |
TENDER_TIE |
The top of the ladder, in the workbook's own hand: the declared tender figure against the sum of imported sheets. An import is usually a subset of the tender, so the check is not "our total equals the tender" (which manufactures a failure) but "the gap is accounted for in full by declared scopes absent from this import" — naming them and their figures. The louder direction is the other one: holding more than the summary declares means imported money the summary never mentions. |
AMOUNT_IN_WORDS |
The tender figure the sheet spells out, read back into a number and compared with the figure it prints. Three outcomes, all honest: matches, disagrees (both figures quoted with the phrase), or could not be read back — in which case it is reported as not compared, never guessed at. |
SUMMARY_STRAYS |
Off-column numbers standing beside summary entries (§6.5) — reported verbatim and never counted, in either direction. |
Part VI The Summary Grammar (the excluded sheets)
Learned by inspecting the excluded sheets of three independent tender workbooks (JAVC External Works, Mall Refurbishment Bill 03, EMR Enabling & Piling). The structure is remarkably consistent — it is standard QS practice, not one firm's habit.
§6.1 Workbook composition
A tender workbook is built from repeating units:
Cover → Contents → Preambles (R&M clauses)
then per bill/section: Flysheet → BOQ sheet(s) → Bill Summary
finally: Main-Summary flysheet → Main Summary
- Cover / Contents — employer, project, package title ("TENDER DOCUMENTS FOR …"), pricing basis ("PRICE BREAKDOWN", "LUMPSUM"); Contents lists every bill with its title and page range. Pure metadata — the deterministic source for bill/project identity, and the parser uses it: the project is the first line under "TENDER DOCUMENTS FOR", and a bill identity ("BILL/SECTION NO. X" + title) is adopted from a near-empty flysheet only when exactly one distinct identity exists among the excluded sheets — a multi-bill import keeps the filename, never a guessed identity. Currency is read from the Rate/Amount header band (a standalone 3-letter uppercase token in or directly under those header cells); sheets printing no token abstain, and the currency is adopted when all declaring sheets agree — any disagreement between declaring sheets leaves it empty.
- Preambles ("R&M CLAUSES") — measurement clauses, symbol/abbreviation tables (m², E.O., "Item = composite item (all inclusive)"). Context, not money.
- Flysheets — a bill number + title around rows 21–24, nothing else. Printed dividers.
- Bill Summaries and the Main Summary — the money mirror (§6.2, §6.3).
§6.1b Summary sheets disguised as bill sheets v1.6
Some workbooks give their summary sheet a full Item | Description | Amount header, making it parseable — and importing it double-counts catastrophically (the whole bill re-listed as line items, tender totals included). Detection is data-driven, no label matching: a supported sheet whose item amounts contain the exact totals of ≥ 2 other supported sheets is summary_like. The picker warns and deselects it by default (the human can still override); excluded, it becomes SUMMARY_MAP's verifying gold instead of pollution.
§6.2 The Bill Summary sheet
Header row: Item | Description | Page Nr. | Amount (so it has a BOQ-like header but zero priced items — which is exactly why the picker excludes it). Then:
- Entry lines — one per BOQ sheet (or per extra-over section): a label that mirrors the sheet's section title ("SITE WORK", "CONCRETE WORK", "EXTRA-OVER FOR COMPLYING TO AUTHORITY COMMENTS"), a page range ("2/B/1 - 2/B/16"), and a tri-state amount — number, text ("Rates Only"), or a printed
0.00(a declared zero, which is not the same as empty — the mirror principle applies to summaries too). - Total line — "TOTAL FOR BILL NO. N …" / "TO MAIN SUMMARY" with the bill total. The entry lines sum exactly to the total line (verified to the cent on all three workbooks).
§6.3 The Main Summary sheet
One line per bill: bill title + page ref N/SUM + tri-state amount (each echoing that bill's summary total), closed by "TOTAL CARRIED TO FORM OF TENDER" — the tender figure, equal to the sum of the lines. This completes a two-level chain: BOQ blocks → bill summary → main summary → tender figure, every link printed by the workbook itself.
§6.4 The page-reference grammar
Printed page refs follow bill/section/page (e.g. 2/B/16, 1/GR/11, 3/PS/2), ranges as start - end, and N/SUM for a bill's summary page. The section token frequently ties to the sheet name (2/B ↔ sheet "2B", 5/PW ↔ "B5 - PW"). Useful as corroboration; never the primary match key.
§6.5 Debris exists in summaries too
Observed: a stray second number in an off-column next to a real entry (7,190,900 beside the real 6,831,036 in EMR's Main Summary). Summary parsing reads the amount column identified by the header, and reports stray off-column numbers rather than silently counting or ignoring them.
§6.6 The SUMMARY_MAP algorithm
Deterministic, mirror-faithful reconciliation of the workbook's self-declared truth against the import:
- Detect summary sheets among the excluded sheets: a
Description + Page Nr./Page No. + Amountheader, or a "SUMMARY"/"MAIN SUMMARY" title with label + amount lines. - Extract entry lines (label, page ref, tri-state amount) and total lines ("TOTAL FOR…", "TO MAIN SUMMARY", "TOTAL CARRIED TO…"), keeping text amounts verbatim.
- Self-check each summary: entries sum to its declared total (within the standard rounding tolerance).
- Match each entry against the imported per-sheet totals by value (to the cent first, then tolerance), with label similarity and page-ref prefix as corroboration. Match Main-Summary lines against Bill-Summary totals (the chain).
- Report, never judge:
- matched pairs (the workbook confirms the import),
- unaccounted summary money — a non-zero summary entry no imported sheet accounts for (e.g. a bill whose sheet failed to parse: JAVC's Gen. Req., declared at 1,579,936.00, excluded by its two-row header — found by this rule automatically),
- imported sheet totals absent from every summary,
- text-amount entries ("Rates Only") noted as declared-unpriced, matched to sheets whose parsed total is 0,
- stray off-column numbers (§6.5).
§6.6b The declared summary, read as a structure v1.11
§3.6 and §6.6 treat the summary sheets as a verification input — values to match against. They are now also read at import into a structure the app keeps (declared_summary), because the summary is where a bill states its own sections and their totals, and nothing else in the workbook does.
What is read, all verbatim and all mirror-faithful:
- bills — each summary sheet's bills, with the ref the summary itself names (
BILL NO. 2) ornullwhere it names none; a whole bill declared as a single figure is anentryrather than a list of sections. - entries — label, page range verbatim (
"Page 2/C/1 - 2/C/12"), the tri-state amount (§6.2), the per-zone split in the summary's own column order, and thesection_refthe page range resolves to (2/C) — the join key against items (§2.8). - subtotals, the grand total, the tender total, and the tender figure in words with whether it reads back to the same number.
- self-checks and strays, kept as facts about the summary itself.
declaredSections() collapses those entries to one row per section, and both of its rules are facts about how these books are printed, not choices:
- Add-on rows fold upward. An
(ADD items)row carries no page range of its own and belongs to the section above it, so a section's declared total is every row that resolves to its key, summed. The row that carried the page range is the one that names the section; an add-on does not. - Echoes collapse; disagreements do not. The same section is typically declared twice — once on its bill's own summary and again on the Grand Summary. Equal figures are one figure. Figures that disagree are kept as an explicit
conflictcarrying both, because a workbook contradicting itself is a finding, and picking a winner would bury it.
The cross-section guard. A summary row whose printed page range crosses section boundaries (Page 3/B/1 - 3/R/3) is declaring a bill, not a section. Folding it into the section its range happens to start at hands that section the whole bill's figure and the bill's name — and the "does not tie" mark that inevitably follows is a finding the app invented about a submission that was fine. Such rows are excluded from the section model. The test is structural: more than one distinct bill/section prefix inside the range.
§6.7 Known gaps this grammar exposes (candidate versioned fixes)
Stated openly — honesty is structural, and the audit agent probes these first:
Two-row split header (
Fixed / Time Related / Total AmountaboveItem / Description / Unit) defeats the single-row header detection (§2.1) and excludes a genuine BOQ sheet (JAVC "Gen. Req."). SUMMARY_MAP turns that silent exclusion into a loud, quantified finding; teaching §2.1 to read split headers is a candidate grammar change.Trailing end-of-sheet notes after a sheet's final collection row are dropped (§2.6) — no block remains to carry them.
MIRROR's one-directional blindness on qty/rate: raw number vs parsed null/text escapes the state comparison (Part V).
Wired collection cells (observed): a workbook may point a sheet's collection/summary cells at the whole bill's grand total (unpriced GR sheets showing hundreds of millions in a zero-sum block). This is a fact about the source — flagged loudly (Σ vs printed), resolved by QS judgement, never "fixed" by parsing.
A section declared only by a bill-level row has no printed title of its own in the summary; §2.8 fills it from the bill sheet where the sheet prints one, and leaves it as a bare reference where it does not. A section named by neither is shown by its address — never by a name the app composed.
Fixed in v1.11: a bill-level summary row folded into the section its page range started at, handing that section the whole bill's figure and a false SECTION_COLLECTION (→ §6.6b, the cross-section guard); two full-name assemblers drifting apart, so every QS edit dropped the bill and section levels and raised FULL_NAME_MISMATCH on a line nobody had broken (→ §2.8, one assembler); the collection page's own COLLECTION caption captured as a section title (→ §2.8).
Fixed in v1.1 (found by the first Deep Audit): hidden formula-zeros under dash formats swallowing heading rows (→ §1.3b), units flattened from m² to m2 (→ §1.3), header-labelled columns dropped (→ §2.3 extra_columns), unvalued off-column collection markers read as spacers (→ §2.2 order).
Part VII Assisted parse profiles (Stage 2 of the import ladder)
When the grammar parser rejects a sheet, the ladder escalates — free first, AI only on explicit request:
- Grammar parser (free, instant) — handles every shape this document describes.
- Structure scan (user-triggered AI, reads only headers/markers/tail — never the item rows): proposes a parse profile per rejected sheet: header row, explicit column letters, optional annex start. The profile parameterizes parsing; it never changes parser logic.
- The deterministic parser executes the profile; the proofs judge it — a wrong mapping cannot survive the checksum proofs, which are profile-independent truths.
- Profiles are stored on the document (
parse_profiles: sheet, mapping, source, note) — auditable, exportable, visible in the picker before confirm. Nothing imports without the human's confirm. - Recurring profiles are grammar candidates: when the same shape appears across workbooks (split headers, trailing annexes), it graduates into this document as a versioned rule — and those files return to costing zero.
The AI proposes; the engine executes; the proofs judge; the human confirms. A profile is a reviewable claim about structure — never a silent adaptation.
Part VIII The digitisation report v1.11
Everything above establishes what the app knows. This Part is how it hands that back — the receipt a reviewer signs off, and the artefact that goes to the bidder's file. It is generated from one registry (report.ts), so the figures on the report and the figures in the workspace cannot drift apart.
§8.1 Three states, and "assessable" excludes the third
Every part of the report resolves to exactly one state:
| State | Shown as | Meaning |
|---|---|---|
met |
settled | the part was assessable and nothing is outstanding |
open |
open | the part was assessable and something is outstanding |
unavailable |
not assessable | the source gave us nothing to check |
Progress is stated as met of assessable, where assessable excludes unavailable — the same law as §3.7, applied to the report: a part nobody could assess is never counted as one that passed. The three counts are printed together, so "7 of 8 settled · 1 the source gave us nothing to check" is the whole sentence, never just its first half.
§8.2 The nine parts
| Part | Answers | unavailable when |
|---|---|---|
| Coverage | Was anything in the file dropped? | the coverage proofs were not recorded |
| Fidelity to the source | Does the transcription match the file? | no proofs were recorded |
| Structure and naming | Does every line read on its own? | — |
| What the bidder did with each line | priced · unpriced · words instead of a figure · priced as a whole | — |
| Reconciliation | Do the totals tie, rung by rung (§3.7)? | no rung could be checked |
| Figures written in words | Are the words-lines accounted for? | the submission writes none |
| What is still open | What is being asked of the reviewer, and about how much money? | — |
| What you decided | Every ruling, confirmation, resolution and figure change | nothing has been decided yet |
| The workbook beyond this bill | What was read but deliberately not counted | the workbook carries no summary sheets |
Every figure that can be opened into the lines behind it is openable, and every such line carries its reference, printed page, section, group, full name, source cell, and the findings against it — the report is not a summary of the evidence, it is a door onto it.
§8.3 The plain-language layer
The validator names findings the way code names things (UNPRICED_LINE, REF_SEQUENCE_RESET). A quantity surveyor names them the way a bill is discussed. One lookup (findings.ts) maps a rule to the words a person reads, and every surface that shows a finding to a person reads from it — the work list, the line pane, the clarification list, the report — so the same finding is never described two different ways in the same building.
Each entry carries the predicate that completes "1,801 lines …", its singular form, and the ask — what the reviewer is being invited to do about it, in one clause. A rule with no entry falls back to its own name rather than to an invented phrase: a finding described in code terms is honest, a finding described in words the system made up is not.
§8.4 Provenance — what ties a report to one file and one moment
A report that leaves the building with no bidder, no date and no version is not evidence of anything. Every report carries: the source filename, a SHA-256 fingerprint of the stored workbook, its size, when it was received, when the report was generated, and the grammar version that judged it (§7). Where the source is not stored, the fingerprint reads "source not stored" — the field is never omitted and never filled with something else.
§7 Versioning
This grammar is versioned. Any change to a trigger, threshold, or row rule bumps the version, and the parser/validator/verifier and the audit agent's skill are updated in the same change. A finding always records which grammar version judged it.
- v1.112026-08-21
the levels, the ladder, and the reviewer's own hand. Sections and groups (§2.8):
bill_ref/section_refread from printed page markers, the field namedsectionstated plainly to be the group (a level below), section titles from the declared summary first and the bill sheet's own printed heading second, one shared full-name assembler. Zones (§2.9): the per-zone amount band identified arithmetically. The reconciliation ladder (§3.7): six rungs, each carrying whether it could be checked — a rung nobody could check is never drawn as one. The totals (§3.4): five figures, includingarithmeticAsSubmitted, so the bidder's arithmetic and the arithmetic after review are always separable. Four flags documented that the validator had been raising undocumented since 2026-08-17:SECTION_COLLECTION,ZONE_COLLECTION,LUMP_SUM_PRICED,CODE_MARKER. Seven proofs documented likewise:SUMMARY_DECLARED,SUMMARY_SELF_CHECK,SECTION_TIE,ZONE_TIE,TENDER_TIE,AMOUNT_IN_WORDS,SUMMARY_STRAYS— the catalog had 8 of 15. Scoped QS rulings (§4.4) generalising §4.1 past printed text, four scopes, narrowest-wins, reach counted before it is taken. Figure overrides (§4.5) with the source figure preserved, and the??trap that mis-attributes money on every line whose source prints nothing. Implied quantity (§4.6), marked✻, never written. The declared summary as a structure (§6.6b) with the add-on, echo/conflict and cross-section rules. The digitisation report (Part VIII), its three states, its nine parts, the plain-language layer and report provenance. Also: the header said v1.9 while the changelog said v1.10, andGRAMMAR_VERSIONin the code said1.9— three versions for one document, stamped onto every finding. Now one. - v1.102026-08-20
UNPRICED_LINE(§4): a line carrying no money and no rate is flagged, where before it passed silently — the Amount cell's numeric 0 (written by=qty*rateover an empty rate) read as "priced" everywhere, no per-line rule tested for a missing rate, and theunpricedclarification category only ever triggered on a printed-text amount. Observed on a real bill where 1,875 of 2,998 lines (63%) were silently unpriced while the queue showed 139. - v1.92026-08-13
printed page markers (§2.7): the bill's own
bill/section/pagefooters captured onto items asprinted_page, discovered by shape per sheet; groundwork for the Words Gate (WORDS_GATE.md), whose address references resolve against them. - v1.82026-08-12
split-quantity layouts (§2.1b): the quantity column identified arithmetically (value × rate = amount) when no qty header matches; provenance recorded as an
arithmeticparse profile; verifier re-derives independently. From the Zaabeel book's "TOTAL QTY" layout. - v1.72026-08-12 QS scope exclusions (§4.3): a sheet recorded as held out of the totals (GR practice) while staying imported and fully reviewable; scoped rollup, full-picture summary tie, mandatory disclosure next to the total.
- v1.62026-08-12
marker tokens in the code column (§3.5,
CODE_MARKER); data-driven summary-like sheet detection at the picker (§6.1b). - v1.52026-08-12 assisted parse profiles (Part VII): declarative per-sheet column maps proposed by AI or humans, executed deterministically, judged by proofs, stored on the document.
- v1.42026-08-12
the bill ends at its final grand row: post-summary annex rows (elemental summaries) recognized structurally (valued grand after the last collection row) and excluded from items; look-ahead excludes grand rows from the collection scan; corpus regression harness (
npm run test:corpus) guards every known workbook. - v1.32026-08-11 QS error resolutions (§4.2): recorded, reasoned, reversible closure of item-level errors; queue/accept honor resolutions; structural errors remain non-waivable.
- v1.22026-08-11 QS class rulings (§4.1): document-level accepted printed-text classes; NON_NUMERIC_AMOUNT info variant; born-acknowledged lines; class-homogeneity audit duty.
- v1.12026-08-11
from the first Deep Audit's findings: printed-text classification rule (§1.3b), text cells verbatim incl. superscripts (§1.3),
extra_columnscapture (§2.3), marker-before-spacer taxonomy order with unvalued-marker handling (§2.2), deterministic identity + currency from covers/flysheets/header band (§6.1). - v1.0 initial canonical grammar.