$npx -y skills add dleerdefi/claude-code-construction --skill bid-tabulatorExtract data from subcontractor bid PDFs and produce a comparison spreadsheet. Feeds into /bid-evaluator. Triggers: 'tabulate bids', 'bid comparison', 'compare bids', 'buyout analysis', 'bid tab'.
| 1 | # Bid Tabulator |
| 2 | |
| 3 | Processes multiple subcontractor bid PDFs for a scope of work and produces an Excel comparison workbook. Extracts each bid's data **as-submitted** — the engineer handles normalization and alignment after reviewing discrepancies and contacting subcontractors as needed. |
| 4 | |
| 5 | ## RIGID Rules (non-negotiable) |
| 6 | |
| 7 | 1. **Complete extraction — zero tolerance on omission.** If a bid contains data that fits ANY field in the schema, extract it. When in doubt, extract it. Extra data can be filtered; missing data cannot be recovered. |
| 8 | 2. **Every page of every bid.** Process ALL pages — do not stop at the first page or summary page. Bids over 10 pages contain detailed breakdowns, alternates, and qualifications that MUST be captured. |
| 9 | 3. **Numeric verification.** After extracting each bid, verify: (a) line item amounts sum to the stated subtotal, (b) subtotal + alternates/adjustments = base bid total. Flag any mismatch as `[MATH ERROR: line items sum to $X, bid states $Y]`. |
| 10 | 4. **Formulas in Excel, not hardcoded values.** Subtotals use SUM formulas. Reconciliation uses difference formulas. The Comparison Summary uses cross-sheet formula references to per-bidder tabs. Never paste calculated values — the engineer must be able to trace every number. |
| 11 | 5. **Preserve original language verbatim.** Line item descriptions, exclusions, qualifications, and notes are extracted exactly as written. No paraphrasing, normalization, or cleanup. |
| 12 | |
| 13 | ## Step 1: Gather Inputs |
| 14 | |
| 15 | Ask the user for: |
| 16 | 1. **Bid PDFs** — folder path or list of individual files |
| 17 | 2. **Scope description** — what trade/division is being bought out (e.g., "Division 09 - Finishes", "Structural Steel") |
| 18 | 3. **Any specific data points** the user wants extracted beyond the defaults |
| 19 | |
| 20 | If project context is available (`.construction/` directory), read `project.yaml` for project name/number to include in the workbook header. |
| 21 | |
| 22 | ## Pipeline Position |
| 23 | ``` |
| 24 | THIS SKILL → /bid-evaluator → user confirms → /subcontract-writer |
| 25 | ``` |
| 26 | This skill is the entry point of the bid pipeline. It produces the tabulated data that `/bid-evaluator` consumes for analysis. |
| 27 | |
| 28 | ## Workflow |
| 29 | |
| 30 | ``` |
| 31 | Bid Tabulation Progress: |
| 32 | - [ ] Step 1: Gather inputs (bid PDFs, scope) |
| 33 | - [ ] Step 2: Read first bid to discover structure |
| 34 | - [ ] Step 3: Process all bids |
| 35 | - [ ] Step 4: Generate comparison Excel |
| 36 | - [ ] Step 5: Present summary to engineer |
| 37 | - [ ] Step 6: Write graph entry |
| 38 | ``` |
| 39 | |
| 40 | ### Step 2: Read First Bid to Discover Structure |
| 41 | |
| 42 | Open the first bid PDF to understand what data is available: |
| 43 | |
| 44 | Try pdfplumber first. If text extraction returns meaningful content (>50 chars per page), use text mode. Otherwise fall back to vision. |
| 45 | |
| 46 | **Vision fallback** for scanned bids: |
| 47 | ```bash |
| 48 | ${CLAUDE_SKILL_DIR}/../../bin/construction-python ${CLAUDE_SKILL_DIR}/../../scripts/pdf/rasterize_page.py BID.pdf 1 --dpi 200 --output bid_page.png |
| 49 | ``` |
| 50 | |
| 51 | From the first bid, identify what data fields are present. Common bid data: |
| 52 | |
| 53 | ```yaml |
| 54 | # Bidder info |
| 55 | company_name: "" |
| 56 | contact_name: "" |
| 57 | contact_phone: "" |
| 58 | contact_email: "" |
| 59 | bid_date: "" |
| 60 | bid_validity_period: "" |
| 61 | |
| 62 | # Financial |
| 63 | base_bid_amount: "" |
| 64 | line_items: # Every line item as an object: |
| 65 | - spec_section: "" # CSI section if shown (e.g., "09 65 19") |
| 66 | description: "" # Original description verbatim |
| 67 | qty: null # Quantity (numeric or null if lump sum) |
| 68 | unit: "" # Unit as written (SF, LF, EA, LS, etc.) |
| 69 | unit_price: null # Per-unit cost (numeric — extract or calculate) |
| 70 | extended_price: null # Line total (qty × unit_price, or lump sum amount) |
| 71 | notes: "" # Flags, clarifications |
| 72 | alternates: [] # Alternate pricing: [{name, description, amount}] |
| 73 | allowances: [] # Allowances included |
| 74 | |
| 75 | # Terms |
| 76 | scope_inclusions: [] # What the bid explicitly includes |
| 77 | scope_exclusions: [] # What the bid explicitly excludes |
| 78 | qualifications: [] # Conditions, assumptions, caveats |
| 79 | schedule_duration: "" # Proposed duration if stated |
| 80 | payment_terms: "" # Net 30, etc. |
| 81 | bond_included: false # Whether bid/performance bond is included |
| 82 | insurance_confirmed: false |
| 83 | ``` |
| 84 | |
| 85 | **Line item extraction rules:** |
| 86 | - Every line item MUST be an object with the fields above — never a bare string. |
| 87 | - If the bid shows qty, unit, and unit_price explicitly → extract all three. |
| 88 | - If the bid shows only extended_price and qty → calculate: `unit_price = extended_price / qty`. |
| 89 | - If the bid shows only a lump sum amount → set `qty: null`, `unit: "LS"`, `unit_price: null`, `extended_price: <amount>`. |
| 90 | - `spec_section` may not always be present — extract it if the bid references CSI section numbers. |
| 91 | |
| 92 | **Present the discovered structure to the user** before processing remaining bids: " |