# SKILL — lfcs-boq-parse

## Purpose

Parse a Bill of Quantities xlsx into a canonical normalised JSON schema and write a quantities-at-a-glance markdown table. Feeds `lfcs-bid-prep-doc`, `lfcs-incl-excl-draft`, `lfcs-pricing-estimator` (v2), and `lfcs-bid-vs-qs-compare` (v2).

## Trigger

User says any of:
- "Parse the BOQ for [job]"
- "I dropped the BOQ"
- "Build quantities table"

Or invoked programmatically by `lfcs-bid-prep-doc` if `boq-normalised.json` is absent.

## Inputs

Required:
- `job_folder` — absolute path

Optional:
- `boq_filename` — explicit filename (default: scan `02 - Scope and Pricing/02b - BOQ and Rate Schedules/` for `*.xlsx`)
- `output_format` — `json+markdown` (default), `json-only`, `markdown-only`

## Behaviour

1. **Discover xlsx.** Read `02 - Scope and Pricing/02b - BOQ and Rate Schedules/`. If multiple xlsx, prefer the most recent by `_date` in filename or mtime; ask user to confirm.
2. **Open xlsx** using openpyxl (CC CLI) or Cowork's xlsx Read.
3. **Detect header row** — first row containing both an "item" / "code" / "ref" column and a "qty" / "quantity" column.
4. **Parse each line** to canonical schema:

```json
{
  "boq_section": "6.4.1",
  "description": "Place + finish concrete — abutments",
  "section_heading": "Substructure",
  "qty": 53.064,
  "unit": "m3",
  "rate": null,
  "subtotal": null,
  "notes_inline": "",
  "source_row": 47
}
```

5. **Normalise units:** `m3`, `m2`, `m`, `T` (tonnes), `kg`, `Item`, `Sum`, `LS`, `lf`. Map common aliases (`cu.m` → `m3`, `tonnes` → `T`, `each` → `Item`).
6. **Group by section_heading** for the quantities-at-a-glance table.
7. **Write `{job_folder}/02 - Scope and Pricing/02b - BOQ and Rate Schedules/boq-normalised.json`** — one object per line, `lines: [...]` plus `meta: {filename, parsed_at, header_row, total_lines, sections}`.
8. **Write `{job_folder}/02 - Scope and Pricing/02b - BOQ and Rate Schedules/boq-quantities-at-a-glance.md`** — markdown grouped by section, totals per element type (concrete m³, reo T, formwork m², strip m²) where extractable.
9. **Update `00-brief.md`** quantities-at-a-glance table (if section exists) — append-only with timestamp note ("Updated by lfcs-boq-parse 2026-MM-DD HH:MM"); never overwrite Rocky-edited content.
10. **Append project log + daily note.**
11. **After vault writes commit, invoke `lfcs-airtable-sync mode=audit-only`** so the catalog log records the run for audit (no direct writes Phase 1 — `Bid Line Items` rows are seeded by downstream pricing skill).

## Files to read (in order)

```
HQ-Vault/_System/CLAUDE.md
HQ-Vault/11_OpsMan_LFCS/Operations/Jobs/Upcoming/2602 - Dec Projects - Munmorah Bridge/00-Bid-Prep.md  ← regression fixture for quantities table format
HQ-Vault/11_OpsMan_LFCS/Operations/lfcs-standards/folder-spec.md
{job_folder}/02 - Scope and Pricing/02b - BOQ and Rate Schedules/*.xlsx  ← input
{job_folder}/00-brief.md  ← may update quantities-at-a-glance section
```

## Airtable behaviour

- **Reads:** `Jobs` (resolve Job FK for downstream BOQ-derived rows).
- **Writes:** none in Phase 1. The `boq-normalised.json` is consumed by downstream skills (`lfcs-bid-prep-doc`, future `lfcs-pricing-estimator`). `Bid Line Items` rows are created later by the pricing skill or by `lfcs-airtable-sync` when explicitly called with `target_table=Bid Line Items`.
- **Idempotency:** N/A (no direct writes). Calls `lfcs-airtable-sync mode=audit-only` so the catalog log records the run for audit.

## Output format

To user:

```
✓ BOQ parsed for {job_no}
  Source: {filename} (rev {rev}, dated {date})
  Lines parsed: {n}
  Sections: {list}
  Totals (extracted):
    - Concrete: {x} m³
    - Reo: {x} T
    - Formwork erection: {x} m²
    - Formwork strip: {x} m²
  Lines with no rate (LFCS to fill): {n}/{total}

  Outputs:
    - boq-normalised.json
    - boq-quantities-at-a-glance.md
    - 00-brief.md quantities table updated (append-only)

  Next skills:
    - lfcs-boq-cross-check (Phase 2) — flag BOQ↔drawing misalignment
    - lfcs-bid-prep-doc — generate 00-Bid-Prep.md
    - lfcs-incl-excl-draft — draft inclusions/exclusions
```

## Hard rules

- **Never invent quantities.** If a cell is blank or unparseable, output `null` + flag in `parse_warnings: []` array on the line object.
- **Preserve `boq_section`** verbatim from the xlsx. Don't renumber, don't pad zeros, don't normalise format.
- **Preserve `description`** verbatim. Don't paraphrase, don't shorten.
- **Append-only on `00-brief.md`** — if quantities section exists with Rocky's edits, append a new section dated; do not overwrite.
- **Read-before-edit** every invocation.
- **Regression fixture:** running on 2602 BOQ (`P424-HCB-BOQ-FRP-Package.xlsx`) must reproduce the quantities-at-a-glance table in 2602 `00-brief.md` (concrete ~240.6, reo ~69.2, formwork ~407.6, strip ~142.5).
- **Vault markdown is source of truth.** If `lfcs-airtable-sync` write fails (audit-only mode), this skill's run is still considered successful — log to `_System/Conflicts.md` and continue. Never block on Airtable.

## Dependencies

- openpyxl (CC CLI dispatched), or Cowork xlsx Read (cowork-lfcs)
- 2602 BOQ + 2602 `00-brief.md` quantities table (regression fixture)

## Failure modes

| Failure | Detection | Recovery |
|---|---|---|
| No xlsx in 02b | Pre-scan | Abort, tell user where to drop BOQ |
| Multiple xlsx | Pre-scan | Ask user which (or default to most recent + confirm) |
| Header row not detectable | Per-sheet validation | Abort + ask user to confirm header row index |
| Unit unmappable | Per-line validation | Output `unit: "{raw}"` with `parse_warnings: ["unmapped unit"]` |
| Quantity unparseable | Per-line | `qty: null`, `parse_warnings: ["qty unparseable: '{raw}'"]` |
