---
skill: lfcs-loo-builder
version: 1
status: live
model: claude-haiku-4-5-20251001
inputs:
  - bid_record_id (Airtable Bid)
  - recipient_company
  - recipient_address (optional)
  - recipient_salutation (optional, default "Dear,")
  - submission_date (optional, default today)
  - validity_days (optional, default 30)
  - package_name (optional, default "FRP Package")
  - operator_of_record (default "Liam Fitzgerald")
outputs:
  - Letter-of-Offer-{job_no}-{date}.md (sanitised markdown)
  - LoOOutput dataclass (sections_present + mandatory_clauses_flagged + structural_parity dict)
implementation: HQ-Vault/11_OpsMan_LFCS/Codebase/lfcs-pricing-agent/lib/loo_builder.py
spec_source: Operations/lfcs-standards/templates/letter-of-offer-structure.md
fixture: HCB Letter of Offer (P424-HCB, $434,462 ex GST, May 2026)
---

# lfcs-loo-builder

Programmatic Letter of Offer builder reading from Airtable Bid + Bid Line Items
+ methodology Exclusion Library. Renders the 7-section structural template defined
in `letter-of-offer-structure.md`.

Distinct from existing `lfcs-letter-of-offer-draft` skill (v1, 2026-05-06):
- `lfcs-letter-of-offer-draft` — reads vault md inputs, produces docx (cowork-driven, manual signoff)
- `lfcs-loo-builder` (this skill) — reads Airtable directly, produces sanitised md (programmatic, M1 sanitisation seam applied)

Both honour the same structural spec.

## Behaviour

1. Read Bid + linked Job from Airtable. Pull `final_submitted_total_ex_gst` (or
   `Total_ex_GST` if final not yet set) + `job_category` + `Name`.
2. Resolve detailed sections by querying `_methodology_Exclusion_Library` for tier=
   DETAILED-EXCLUSIONS / DETAILED-ASSUMPTIONS / DETAILED-INCLUSIONS rows that match
   `applicable_job_categories` (or carry `ALL`).
3. Render the 7-section markdown with section-tag comments (`<!-- 1_HEADER -->`, etc.)
   for structural-parity verification.
4. Render lump-sum statement as **words AND figures** per spec.
5. Apply Standard Exclusions + Standard Preliminaries Assumptions verbatim from
   `standard-exclusions-boilerplate.md` (codified in module).
6. Detect mandatory-clause flags (discrepancy / scope-gap / cranage / hire-duration
   / optional-rate) by scanning detailed-section text for trigger phrases.
7. Run sanitisation Layer 1 (sanitise.sanitise_markdown) + Layer 2 (verify.verify_text)
   on output. Return both results in `LoOOutput`.
8. If `output_dir` provided, write the sanitised md to disk.

## Structural-parity output

`LoOOutput.structural_parity` returns:
- `all_7_sections_present` — bool
- `lump_sum_words_and_figures_both_present` — bool
- `validity_clause_present` — bool
- `signature_block_present` — bool
- `standard_exclusions_count` — int (always 10 for canonical fixture)
- `preliminaries_assumptions_count` — int (always 6)
- `detailed_exclusions_count` — int (variable per job)
- `layer2_passed` — bool (sanitisation independence verified)

## Number-to-words

In-module helper `_num_to_words(amount)` covers integers up to 999,999,999.
Output mirrors HCB canonical fixture wording: e.g. `$434,462.00` → `"Four
Hundred Thirty-Four Thousand Four Hundred Sixty-Two Dollars and Zero cents"`.
No external dependency on `num2words` package.

## Run

```python
from lib.loo_builder import LoOInputs, build_loo_for_bid

inputs = LoOInputs(
    bid_record_id="recXXXXXXXXXXXXXX",
    recipient_company="The Civil Experts Pty Ltd",
    recipient_address="Suite 2.01, Building G, 350 Parramatta Rd\nHomebush West, NSW 2140",
    package_name="FRP Package",
    operator_of_record="Liam Fitzgerald",
)
result = build_loo_for_bid(inputs, output_dir=Path("./output"))
print(result.structural_parity)
```
