---
name: epd-parser
description: Extract GWP, life-cycle stages, certifications, and impact metrics from an EPD PDF. Use when given a declaration to parse; not to find or compare EPDs.
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash
  - Glob
  - Grep
  - AskUserQuestion
---

# /as:epd-parser — EPD PDF Parser

Extract structured environmental impact data from EPD (Environmental Product Declaration) PDF files. Uses PyMuPDF for text extraction and Claude's reasoning to parse varying EPD formats into a standardized 42-column schema.

EPDs follow ISO 14025 / ISO 21930 / EN 15804 and report life cycle environmental impacts of building products. This skill reads those PDFs and structures the data for comparison, specification, and LEED documentation.

## Input

The user provides EPD PDFs in one of these ways:

1. **File paths** — one or more PDF file paths
2. **Folder path** — a directory containing PDFs (will process all `.pdf` files)
3. **Just invoked** — ask the user for file paths or a folder

## Output Schema

Use the canonical 42-column contract in [`schema/epd-schema.md`](../../schema/epd-schema.md). It is separate from the 33-column FF&E product schema. Never substitute or persist an FF&E-shaped record as EPD data. Leave unavailable values empty and use a plain URL for `EPD Link`.

### Material Category Vocabulary

Use ONE normalized term: Concrete, Steel, Aluminum, Wood/Timber, Insulation, Gypsum, Glass, Ceramic/Tile, Carpet, Resilient Flooring, Roofing Membrane, Sealant, Paint/Coating, Masonry, Stone, Composite Panel, Acoustic, Cladding, Rebar, Cement, Aggregate, Furniture, Other.

### EPD-specific data in Notes (col AO)

EPDs contain fields that don't have dedicated columns. Append these to Notes:

- **EPD Type**: `Type: Product-specific` or `Type: Industry-average` or `Type: Sector`
- **Verification**: `Verified by: [verifier name]`
- **EN version**: `EN 15804+A1` or `EN 15804+A2` (important for comparability)
- **Source File**: `Source: holcim-readymix-epd.pdf`
- **LCA Software**: `LCA: GaBi` or `LCA: SimaPro` or `LCA: openLCA`
- **Biogenic carbon note**: If the EPD reports biogenic carbon storage separately

Example Notes cell: `Type: Product-specific | Verified by: Underwriters Laboratories | EN 15804+A2 | LCA: GaBi | Source: holcim-readymix-epd.pdf`

### LEED Eligibility Logic (col AI)

- **Yes**: Product-specific, third-party verified EPD conforming to ISO 14025
- **Partial**: Industry-average or sector EPD (counts for LEED Option 1 but not Option 2)
- **No**: Self-declared, unverified, or non-conforming

## Workflow

### Step 1: Get input

Parse the user's input to identify PDF file(s) and output preferences.

- If given a folder, list all `.pdf` files and report count
- If no PDFs found or path is invalid, ask the user
- Report: "Found N EPD PDF(s) to process."

### Step 2: Extract text from PDF

Use PyMuPDF (fitz) to extract text from each PDF. Run this Python script via Bash:

```python
import fitz
import sys
import json

pdf_path = sys.argv[1]
doc = fitz.open(pdf_path)
pages = []
for i, page in enumerate(doc):
    text = page.get_text()
    pages.append({"page": i + 1, "text": text})
doc.close()

print(json.dumps({"filename": pdf_path.split("/")[-1], "total_pages": len(pages), "pages": pages}))
```

For each PDF, extract all pages and keep the JSON output transiently for parsing; do not create a persistent JSON artifact.

### Step 3: Parse EPD data

Read the extracted text and identify all environmental impact data. This is the core intelligence step.

**For small EPDs (<=30 pages):** Process all pages at once.

**For large EPDs (>30 pages):** Process in chunks of 15 pages. Carry forward context between chunks.

**Parsing instructions:**

1. **Identify the EPD structure** — EPDs typically have these sections:
   - General information (pages 1-2): manufacturer, product, declared unit, program operator, validity
   - Product description (pages 2-3): materials, manufacturing process, application
   - LCA information: system boundary, data sources, allocation rules
   - Impact indicator tables: the core data — usually 1-3 pages of tables
   - Resource use and waste tables
   - Additional information: scenarios, biogenic carbon, recycled content

2. **Extract product identity first** — manufacturer, product name, declared unit, functional unit. These are always on page 1-2.

3. **Extract EPD metadata** — registration number, program operator, PCR, standard, dates, system boundary. Usually on page 1 or in a header/sidebar.

4. **Find and parse impact indicator tables** — This is the most critical step:
   - Look for tables with life cycle stage columns (A1, A2, A3 or A1-A3 combined, A4, A5, B1-B7, C1-C4, D)
   - Impact categories are rows: GWP, ODP, AP, EP, POCP, ADP (or ADPE/ADPF)
   - **EN 15804+A2 EPDs** split GWP into: GWP-total, GWP-fossil, GWP-biogenic, GWP-luluc. Capture all that exist.
   - **EN 15804+A1 EPDs** report a single GWP row. Put this value in GWP-total (A1-A3).
   - If A1, A2, A3 are reported separately (not combined), sum them for the A1-A3 total.
   - Units are standardized: GWP = kg CO2e, ODP = kg CFC-11e, AP = kg SO2e (or mol H+ eq for +A2), EP = kg PO4e (or mol N eq / kg P eq for +A2), POCP = kg C2H4e (or kg NMVOC eq for +A2).

5. **Extract resource use** — PERE, PENRE, fresh water, waste. Usually in a separate table following the impact indicators.

6. **Extract additional data** — recycled content %, manufacturing plant, country of origin, LCA software, verifier name.

7. **Determine LEED eligibility** — based on EPD type and verification status.

8. **Leave fields blank rather than guessing** — if a field isn't in the EPD, leave it empty.

### Multi-product EPDs

Some EPDs declare impacts for multiple products, product groups, or concrete mix designs. Create **one row per product/variant**:

- If the EPD covers "Product A" and "Product B" with separate impact tables, create two rows
- If the EPD covers multiple concrete mixes (e.g., 3000 PSI, 4000 PSI, 5000 PSI), create one row per mix
- The product name should distinguish variants: "ReadyMix Concrete — 4000 PSI"

### Step 4: Present results

Show a summary table for each parsed EPD:

```
## EPD Parse Results

### holcim-readymix-epd.pdf
| Field | Value |
|-------|-------|
| Product | ReadyMix Concrete — 4000 PSI |
| Manufacturer | Holcim |
| Declared Unit | 1 m3 |
| GWP (A1-A3) | 312 kg CO2e |
| System Boundary | Cradle-to-gate |
| Program Operator | NSF |
| Valid | 2024-01-15 to 2029-01-15 |
| LEED Eligible | Yes |

Products extracted: 3 (3000 PSI, 4000 PSI, 5000 PSI)
```

The preview is the default result. It can pass directly to `/as:epd-compare` or `/as:epd-to-spec` without creating a file.

### Step 5: Optional persistence

Do not create a file by default. Only when the user explicitly asks to save reusable records:

1. Resolve the nearest ancestor containing `PROJECT.md`; the target is `<project-root>/epd-library.csv`.
2. Build complete records using [`schema/epd-schema.md`](../../schema/epd-schema.md), with `Parsed At` as an ISO 8601 timestamp and `Source` as `epd-parser`.
3. Preview the records, target path, and whether this initializes or appends.
4. Use one confirmation gate; do not ask the same confirmation first in prose.
5. Serialize all approved records as one JSON array, preserving canonical field names. After approval, use `python3 "${CLAUDE_PLUGIN_ROOT}/skills/master-schedule/scripts/csv-library.py" init epd` if needed, then invoke `python3 "${CLAUDE_PLUGIN_ROOT}/skills/master-schedule/scripts/csv-library.py" append epd --row-json <batch.json>` exactly once. Never append in a per-record loop. The helper validates the entire file, rejects FF&E or malformed headers, and writes the whole batch atomically.

If no project root exists, keep the result in conversation and offer `/as:project init`; do not invent another CSV destination.

## Edge Cases

- **Scanned PDFs (image-only)**: PyMuPDF will return empty or garbage text. Detect this (very short text relative to page count) and tell the user: "This PDF appears to be scanned/image-based. Text extraction won't work — consider using an OCR tool first."
- **Non-English EPDs**: Common for European manufacturers (German, French, Spanish, Swedish). Impact indicator abbreviations (GWP, ODP, AP, EP, POCP) are the same internationally. Extract numeric data regardless; note the language in Notes.
- **EN 15804+A1 vs +A2**: Older EPDs use +A1 (single GWP row, different EP/AP units). Newer use +A2 (split GWP, different units for AP/EP/POCP). Always note which version in Notes. Map to schema as closely as possible.
- **Multi-product EPDs**: One row per product/variant. See Multi-product EPDs section above.
- **Expired EPDs**: Flag in Notes: `EXPIRED — valid to YYYY-MM-DD`. Still parse the data.
- **Password-protected PDFs**: PyMuPDF will fail to open. Catch the error and tell the user.
- **Very large PDFs (50+ pages)**: Process in 15-page chunks. Give progress updates.
- **EPDs with impact tables as images**: Detect missing numeric data in what should be table sections. Flag: "Impact tables may be embedded as images — manual extraction needed."

## GWP Baseline Policy

This policy is shared by all four EPD skills (`epd-parser`, `epd-research`, `epd-compare`, `epd-to-spec`) and must read identically in each. Industry-average GWP baselines are allowed only when cited with a named source and publication year (e.g., "NRMCA Industry-Wide Member EPD v3.2, 2022" or "AISC Fabricated Hot-Rolled Structural Sections EPD, 2021"). Uncited baseline numbers recalled from memory or training data are banned. If no source-and-year citation is available, ask the user to provide a baseline EPD or find one with `/as:epd-research` — never guess a baseline.

For this skill: report only values extracted from the parsed EPD itself. When contextualizing a parsed GWP against an industry average (e.g., in the parse-results summary), the baseline must carry a source-and-year citation or be omitted.

## Error Reporting

After processing, always report:

```
Parsed: X products from Y EPD PDF(s)
- filename.pdf: N products extracted
- filename2.pdf: M products extracted
Issues: [list any problems — expired, scanned, missing tables, etc.]
```
