VG Home Buyers LLC · Operations Reference

Assembly & Estimating System

How VG captures vendor price quotes, turns them into pre-priced scope bundles, and uses them to estimate rehab costs before — and during — a deal.


1 · What This System Does

Before this system, every rehab estimate started from memory. Quotes arrived via WhatsApp and were never stored. There was no "factory default" budget for a standard scope — no reliable way to say "a house like this should cost approximately X."

The Assembly & Estimating System fixes that. It captures vendor price quotes from WhatsApp (and manual entry), organizes them into named scope bundles called assemblies, and uses those bundles to automatically populate property budgets and produce estimate reports.

Problem it solves
  • Quotes arrive, disappear, and are forgotten
  • Every estimate starts from scratch
  • No benchmark pricing by scope type
  • No visibility into which optional scopes a property carries
What you get
  • A searchable scope + price library (Scope Library page)
  • Per-property estimate reports auto-built from assemblies
  • Budgets auto-populated with buyer_initial prices
  • WhatsApp quotes captured and reviewed before storing

2 · Key Concepts

Assembly

An assembly is a named, pre-priced scope bundle for one type of construction work. Each assembly maps to exactly one VG Segment and Work Order. Examples: Standard AV & Security Package (Seg 65 / P2W05), Rough Plumbing (Seg 61 / P2W02).

Assemblies accumulate vendor quotes over time — they're not single quotes, they're benchmarks. When a new quote comes in, it extends the price range if it falls outside it; the range never narrows.

Standard vs. Optional

Every assembly (and its Work Order) is classified as either Standard or Optional.

Standard

Auto-included in every property estimate. Covers scopes that appear in every VG rehab — plumbing, electrical, HVAC, drywall, framing, etc. You don't need to select these per property.

Optional

Must be explicitly selected for each property via selected_assemblies: in properties.yaml. Covers scopes that don't occur on every property — security cameras, exterior masonry, specialty trim, EV/solar, pool, etc.

Vendor Quotes

Each assembly stores a list of all vendor quotes that backed its price range. Quotes carry the vendor name, amount, date noted, document type (quote / PO / receipt / SOW), and source (PM verbal, WA ingest, etc.). The assembly's price range is VG's benchmark — not just the most recent quote.

Budget Source: buyer_initial

Assembly-derived budgets are written to budgets.yaml with source: buyer_initial — a mid-range benchmark intended for early deal underwriting. The source system has a maturity order: admin_placeholder → buyer_initial → buyer_topdown → detailed_estimate → committed. Assembly prices never overwrite anything at detailed_estimate or higher. They fill gaps only.

3 · How the Data Flows

Two paths feed assemblies.yaml: (A) WhatsApp ingest with --extract-quotes, and (B) manual entry. Once in assemblies.yaml, one generator populates budgets; two generators produce HTML outputs.

Input
WhatsApp ZIP
Stage 8
quote_extractor
Staging
quotes-pending
.yaml
You run
approve_quotes
.py
Source of truth
assemblies
.yaml
You run
generate_budgets
.py
Output
budgets.yaml
+ estimates
File Role Where
assemblies.yaml Canonical scope + price library. One entry per named scope variant. Never delete entries — update and extend. reference/cost-codes/
quotes-pending.yaml Ingest staging area. LLM writes here; you review with approve_quotes.py. Approved entries promote to assemblies.yaml. reference/cost-codes/
work-orders.yaml All 54 WOs with optional: flag added. Standard WOs auto-include in every estimate; optional WOs require per-property selection. reference/cost-codes/
properties.yaml Each active property has a selected_assemblies: [] list for optional scopes selected for that deal. workspaces/actualsboard/config/
budgets.yaml Per-property, per-segment budget amounts. Assembly prices enter here with source: buyer_initial. workspaces/actualsboard/config/

4 · The Tools

approve_quotes.py — Review pending quotes

Run this after any WhatsApp ingest with --extract-quotes. It walks through every pending quote in quotes-pending.yaml and asks you to approve, reject, edit, or skip each one.

$ python tools/approve_quotes.py Reviewing 3 pending quote(s). Commands: [a]pprove [r]eject [e]dit assembly_guess [s]kip [q]uit ──────────────────────────────────────────────────────────── Quote 1/3 (id: wa_vg_iha_20260603_1142) ──────────────────────────────────────────────────────────── Vendor: HD CCTV Security LLC Scope: cameras, speakers, low voltage IT Amount: $7,000 – $7,500 Segment: 65 (conf: 0.92) Assembly: security_av_standard Doc type: quote Extracted: 2026-06-03 > a ✓ Approved → security_av_standard
CommandWhat it does
a — ApproveAppends to assemblies.yaml vendor_quotes[]; extends price range if needed; marks status: approved
r — RejectMarks status: rejected; stays in file for audit; never touches assemblies.yaml
e security_av_standard — EditChanges the assembly_guess field before approving — use when the LLM guessed wrong
s — SkipLeaves as pending; will appear again next time you run
q — QuitSaves progress so far and exits cleanly

generate_budgets.py — Populate budgets from assemblies

Reads assemblies.yaml + work-orders.yaml + properties.yaml → writes buyer_initial midpoint prices to budgets.yaml. Standard WOs are included for every property; optional WOs are included only if selected in that property's selected_assemblies: list.

# Preview what would change (no writes) $ python tools/generate_budgets.py --dry-run [dry] 185 Fernwood / seg 65 → $7,250 (buyer_initial) [skip] 185 Fernwood / seg 63 — already at detailed_estimate [dry] 32 Clinton / seg 65 → $7,250 (buyer_initial) Dry run — 2 entries would be written # Run for all active properties $ python tools/generate_budgets.py # Run for one property only $ python tools/generate_budgets.py --property "185 Fernwood"
Safety rule — never downgrades

The script skips any segment that already has source: buyer_topdown, detailed_estimate, or committed. Assembly prices only fill gaps — they don't overwrite real numbers. Always run with --dry-run first to review what would change.

generate_scope_library.py — Regenerate the Scope Library page

Reads assemblies.yaml and produces the Scope Library portal page, showing all assemblies grouped by segment with Standard/Optional badges, price ranges, vendor quote counts, and scope notes.

$ python tools/generate_scope_library.py Written → published/estimating/scope-library.html

Run this any time you add or update an assembly. The portal sync daemon picks up the change automatically.

generate_estimate.py — Per-property estimate reports

Reads assemblies + properties + budgets and produces a deal-level HTML cost summary for each active property. Standard WOs appear with their budgeted amounts; optional selected scopes are marked with ★.

# Generate for all active properties $ python tools/generate_estimate.py [ok] 185 Fernwood → 185-fernwood-estimate.html (total: $187,400) [ok] 32 Clinton → 32-clinton-estimate.html (total: $164,200) [skip] 1309 Graymill — no assembly budgets found Generated 2 estimate report(s) → published/estimates/ # Generate for one property $ python tools/generate_estimate.py --property "185 Fernwood"

A [skip] message means no assembly-derived budget entries exist for that property yet. Run generate_budgets.py first, or manually add budget entries to budgets.yaml.

WhatsApp Ingest — Stage 8 (--extract-quotes)

Add --extract-quotes to any WhatsApp ingest run to activate Stage 8. The pipeline scans new messages for price signals (dollar amounts, scope words, or attached documents) and calls the local LLM to extract structured quote data. Results land in quotes-pending.yaml for your review.

$ python run.py \ --adapter vg_iha \ --zip "path/to/WhatsApp Chat.zip" \ --workspace "/path/to/vg" \ --extract-quotes Stage 8 Extracting vendor price quotes (--extract-quotes)... [Stage 8] 12 messages with price signals out of 847 [Stage 8] ✓ 3 new quote candidates written to quotes-pending.yaml ✓ Stage 8 complete: 3 new quote candidates Review with: python tools/approve_quotes.py
Document-first extraction

Attached PDFs, purchase orders, receipts, and SOWs are the authoritative source — the LLM reads document text first and treats the chat message as secondary context. If a quote comes with an attached document, the document content drives the extraction. Higher accuracy than text-only messages.

5 · Adding a Quote Manually

When you receive a quote outside WhatsApp (email, phone, on-site), add it directly to assemblies.yaml. Open the file and append to the vendor_quotes: list of the relevant assembly:

# In reference/cost-codes/assemblies.yaml assemblies: security_av_standard: ... vendor_quotes: - vendor: "HD CCTV Security LLC" amount_low: 7000 amount_high: 7500 noted: "2026-06-03" source: "PM verbal" doc_type: quote attachment_ref: null - vendor: "SecureHome Pro" # ← add new entry here amount_low: 6800 amount_high: 7200 noted: "2026-06-15" source: "PM verbal" doc_type: quote attachment_ref: null price_low: 6800 # update if new quote extends the range last_updated: "2026-06-15"

Then regenerate the Scope Library page so the updated quote count shows on the portal:

$ python tools/generate_scope_library.py

6 · Selecting Optional Scopes for a Property

When a property will carry an optional scope (e.g., security cameras), add the assembly key to that property's selected_assemblies: list in workspaces/actualsboard/config/properties.yaml:

# In workspaces/actualsboard/config/properties.yaml - short_name: 185 Fernwood qb_custom_field_value: 185 Fernwood status: active-construction include: true selected_assemblies: - security_av_standard # ← add the assembly key here sqft: ...

Then run generate_budgets.py to write the assembly price to budgets.yaml:

$ python tools/generate_budgets.py --property "185 Fernwood" --dry-run [dry] 185 Fernwood / seg 65 → $7,250 (buyer_initial) # Looks correct — run without --dry-run to write $ python tools/generate_budgets.py --property "185 Fernwood"

7 · Adding a New Assembly

When you encounter a new scope type that doesn't exist in assemblies.yaml yet, add it as a new entry. One assembly per named scope variant.

  1. Find the right Segment and WO. Check reference/cost-codes/work-orders.yaml for the WO that covers this scope. The assembly's segment: must match that WO's cost codes' leading digits (e.g., WO with cost_codes: [65.0]segment: "65").
  2. Add the entry to assemblies.yaml. Use a snake_case key (e.g., fireplace_gas_insert). Fill in all required fields: name, segment, segment_name, work_order, optional, scope, price_low, price_high, unit, vendor_quotes, last_updated.
  3. Set optional: correctly. If this scope doesn't occur on every property, set optional: true. If it's part of every VG rehab, set optional: false — it will auto-include in every estimate.
  4. Regenerate the Scope Library. Run python tools/generate_scope_library.py so the new assembly appears on the portal.
  5. Populate budgets. Run python tools/generate_budgets.py --dry-run to see what gets written, then run without --dry-run to apply.

8 · Typical Workflows

Quote arrives via WhatsApp (most common)

  1. Run WhatsApp ingest with --extract-quotes after receiving new chat exports.
  2. Run python tools/approve_quotes.py — review each extracted quote. Approve good ones; reject noise.
  3. Run python tools/generate_scope_library.py to update the Scope Library page with the new quote count.
  4. If the quote is for an optional scope on a specific property, add it to selected_assemblies: and run generate_budgets.py --property "...".

Setting up a new property for estimating

  1. Add the property to properties.yaml with status: active-construction and selected_assemblies: [].
  2. With the PM, decide which optional scopes this property will carry (security cameras? specialty trim? masonry?). Add those assembly keys to selected_assemblies:.
  3. Run python tools/generate_budgets.py --property "Property Name" --dry-run to preview. Then run without --dry-run.
  4. Run python tools/generate_estimate.py --property "Property Name" to generate the deal estimate HTML.

Quote arrives by phone/email (manual)

  1. Open reference/cost-codes/assemblies.yaml. Find the matching assembly or create a new one.
  2. Append to vendor_quotes:. Update price_low/price_high if the new quote extends the range.
  3. Run python tools/generate_scope_library.py to update the portal.

9 · Reading the Scope Library

The Scope Library page shows all assemblies grouped by Segment number. Each card shows:

What you see
  • Assembly name — the scope type
  • Standard or Optional badge
  • WO code · quote count · last updated
  • Price range — VG's benchmark low–high
  • Scope notes — what's included and excluded
What the price range means

The range reflects all vendor quotes on file, from the lowest to the highest. It's not a single quote — it's the observed market range for this scope at VG's typical property size. More quotes = tighter, more reliable range. The midpoint is used when writing buyer_initial budgets.

Regenerate the page whenever assemblies change: python tools/generate_scope_library.py. The sync daemon pushes the change to the portal within seconds.

10 · File Quick Reference

TaskCommand
Review pending quotes from WA ingest python tools/approve_quotes.py
Populate budgets from assemblies (preview) python tools/generate_budgets.py --dry-run
Populate budgets from assemblies (write) python tools/generate_budgets.py
Populate budgets for one property python tools/generate_budgets.py --property "185 Fernwood"
Regenerate Scope Library portal page python tools/generate_scope_library.py
Generate estimate reports (all active properties) python tools/generate_estimate.py
Generate estimate for one property python tools/generate_estimate.py --property "185 Fernwood"
Ingest WhatsApp + extract quotes python run.py --adapter vg_iha --zip <path> --workspace <vg> --extract-quotes
Validate budgets.yaml for errors python tools/validate_budgets.py