Add willowbrook-submittal-review skill
Bundles the submittal pre-review skill (SKILL.md, weston_guide, project playbook, CSI division map, Python helpers) into the willow-pmtools plugin so it auto-distributes alongside the ProjectTodos MCP. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
BIN
Binary file not shown.
@@ -0,0 +1,188 @@
|
||||
# Willowbrook Submittal Review Skill — v1.1
|
||||
|
||||
A Claude Code skill that pre-reviews a construction submittal before it goes to the architect.
|
||||
|
||||
Built for Willowbrook Construction Services from 500 real submittals on project 0309A (Stillwater High School). Platform-agnostic: works against any local PDF or submittal folder today, designed for the in-house Willow AI to call via Claude Agent SDK tomorrow.
|
||||
|
||||
**v1.1 — spec-book aware.** If the PM supplies `project_spec_path`, the skill loads the matching CSI section from the project spec book and grounds every conformance check in real spec paragraphs (section, paragraph, page, excerpt). Without the spec, the skill falls back to checklist-only review.
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
When a PM says *"review this submittal"* or runs `/review-submittal <path>` and hands over a PDF or folder, the skill:
|
||||
|
||||
1. Detects whether the input is a single PDF or a Procore/Fieldwire-style folder
|
||||
2. Extracts text and metadata from the cover sheet and attachments
|
||||
3. Identifies the CSI division and loads the matching conformance checklist from Weston's submittal review guide
|
||||
4. Applies the universal first-pass checks to every submittal
|
||||
5. Flags historical patterns from 500 prior submittals on project 0309A
|
||||
6. Produces:
|
||||
- A short on-screen memo for the PM
|
||||
- A Willowbrook-branded Word checklist (`review_<num>.docx`)
|
||||
- A structured JSON file (`review_<num>.json`) aligned with Willow's planned database schema
|
||||
|
||||
Every output is stamped **DRAFT**. The skill never auto-sends anything — the PM is the publish button.
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
### One-time setup
|
||||
|
||||
```
|
||||
pip install pymupdf python-docx pypdf
|
||||
```
|
||||
|
||||
### Install the skill (for a single PM)
|
||||
|
||||
Copy or clone this folder into your Claude Code user skills directory:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
C:\Users\<you>\.claude\skills\willowbrook-submittal-review\
|
||||
```
|
||||
|
||||
**macOS / Linux:**
|
||||
```
|
||||
~/.claude/skills/willowbrook-submittal-review/
|
||||
```
|
||||
|
||||
Restart Claude Code. The skill will auto-trigger when you ask for a submittal review.
|
||||
|
||||
### Install the skill (for the Willowbrook PM team)
|
||||
|
||||
Option A — central SharePoint copy, each PM runs a sync script:
|
||||
```powershell
|
||||
robocopy "\\willowbrook\shared\Claudes Folder\skills\willowbrook-submittal-review" "$env:USERPROFILE\.claude\skills\willowbrook-submittal-review" /MIR
|
||||
```
|
||||
|
||||
Option B — publish as an internal Claude plugin (requires a plugin manifest; defer to v1.1).
|
||||
|
||||
The `willowbrook-brand-262` skill should be installed in the same session so the skill can apply brand standards to the generated .docx.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Natural language
|
||||
```
|
||||
You: Review this submittal: "C:\path\to\157_1_Operable Partitions - SD"
|
||||
Spec book: "C:\path\to\0309A\Spec Book"
|
||||
```
|
||||
|
||||
### Slash command
|
||||
```
|
||||
/review-submittal "C:\path\to\157_1_Operable Partitions - SD" --spec "C:\path\to\0309A\Spec Book"
|
||||
```
|
||||
|
||||
### Spec book handling
|
||||
- If the spec path points to a folder with already-extracted sections (the folder contains `_manifest.json`), the skill reads directly from it — instant.
|
||||
- If the spec path points to a folder containing only a raw spec-book PDF, the skill extracts it into `<path>/Extracted Sections/` on first use and caches for next time.
|
||||
- If the spec path points directly to a PDF, same behavior — extracts and caches alongside.
|
||||
- If no spec path is supplied, the skill runs checklist-only review and cannot recommend `Approved`.
|
||||
|
||||
### Programmatic (for Willow, via Claude Agent SDK later)
|
||||
Call the skill with a payload conforming to `schemas/input.schema.json`. Receive `schemas/output.schema.json`.
|
||||
|
||||
---
|
||||
|
||||
## Output
|
||||
|
||||
Every run produces three things:
|
||||
|
||||
1. **On-screen memo** — ~15 lines the PM can scan in 60 seconds
|
||||
2. **Word checklist** — `review/submittal_<num>_review.docx`, branded
|
||||
3. **Structured JSON** — `review/submittal_<num>_review.json`, ingestable by Willow
|
||||
|
||||
All three are stamped DRAFT.
|
||||
|
||||
---
|
||||
|
||||
## What v1.1 does NOT do
|
||||
|
||||
- Does not hit Procore, Fieldwire, SharePoint, or any API. Input is local files only.
|
||||
- Does not auto-send anything.
|
||||
- Does not review RFIs, change orders, bid leveling, sub qualification, or cost impacts.
|
||||
- Does not modify the input submittal.
|
||||
- Does not validate calculations (hydraulic calcs, photometrics, wind loads). Reads and flags mismatches; does not recompute.
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
| Version | Status | Adds |
|
||||
|---|---|---|
|
||||
| v1.0 | ✅ shipped | Static conformance checklist + historical pattern flags + branded .docx + structured JSON |
|
||||
| **v1.1** | ✅ current | Spec-book-aware: every pass/fail cites real spec paragraphs; `Approved` achievable with full citations |
|
||||
| v1.2 | planned | SharePoint run log per Weston's request; offline spec cache verification |
|
||||
| v2 | planned | Smart spec parsing (structured Part 2 Products extraction: manufacturer lists, performance tables, warranty durations) for exact-match validation vs. reading narrative text |
|
||||
| v3 | planned | Callable from Willow via Claude Agent SDK; output lands in Willow's database |
|
||||
| v4 | planned | Coordination-aware — flags conflicts against already-approved submittals in the same project |
|
||||
| v5 | planned | Willow fetches submittals from Fieldwire via API, runs the skill, posts review back (through Willow, not direct from the skill) |
|
||||
|
||||
---
|
||||
|
||||
## Folder structure
|
||||
|
||||
```
|
||||
willowbrook-submittal-review/
|
||||
├── SKILL.md ← Claude-facing trigger, workflow, rules
|
||||
├── README.md ← this file (human-facing)
|
||||
├── references/
|
||||
│ ├── weston_guide.md ← per-CSI-division conformance checklists
|
||||
│ ├── project_playbook.md ← historical patterns from 500 0309A submittals
|
||||
│ └── csi_division_map.json ← CSI # → checklist + pattern triggers
|
||||
├── schemas/
|
||||
│ ├── input.schema.json ← I/O contract for Willow
|
||||
│ └── output.schema.json ← matches Willowbrook's planned submittal schema
|
||||
├── scripts/
|
||||
│ ├── detect_input.py ← PDF vs. folder auto-detect
|
||||
│ ├── parse_submittal.py ← PyMuPDF extract + metadata regex
|
||||
│ ├── extract_spec_book.py ← spec-book PDF → one .txt per CSI section + manifest
|
||||
│ ├── spec_lookup.py ← CSI section → spec text for grounding checks
|
||||
│ └── build_checklist_docx.py ← renders review.json → baseline .docx
|
||||
└── test_fixtures/ ← sample outputs from a real 0309A submittal
|
||||
├── parsed_157_1.json ← v1.0 — checklist-only review
|
||||
├── review_157_1.json
|
||||
├── review_157_1.docx
|
||||
├── review_157_1_spec_aware.json ← v1.1 — same submittal, spec book loaded, real citations
|
||||
└── review_157_1_spec_aware.docx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
A real 0309A submittal (157.1 — Operable Partitions) is included in `test_fixtures/` as a regression sample. Two outputs are provided:
|
||||
|
||||
**`review_157_1.*`** — v1.0 behavior, no spec book. Demonstrates:
|
||||
- Steel-fabricator coordination flag fires (playbook pattern)
|
||||
- Status is `CONDITIONAL` (cannot be `PASS` without citations)
|
||||
- Several items marked "Requires PM judgment"
|
||||
|
||||
**`review_157_1_spec_aware.*`** — v1.1 behavior, spec book loaded. Demonstrates:
|
||||
- Every `pass` and `warn` result cites a real spec paragraph (§1.2.B.1, §1.4.B.1, §1.4.C.1, etc.)
|
||||
- Sample requirement failure caught against §1.4.C.1 and §1.4.C.2 (textile facing + panel edge samples) — which paint chips don't satisfy
|
||||
- Steel-fab coordination warn now cites §1.2.B.1 directly
|
||||
- Basis-of-Design check references Modernfold Acousti-Seal Legacy per §2.2.A.1
|
||||
- STC requirement references §2.2.G and §2.1.A.1
|
||||
- Pushback draft paragraph cites 4 specific spec sections for the sub to address
|
||||
|
||||
Open the two `.docx` files side by side to see the difference in specificity.
|
||||
|
||||
---
|
||||
|
||||
## Non-negotiables (from Weston)
|
||||
|
||||
1. Every `pass` result in v2+ must cite the spec paragraph. No citation, no `Approved`.
|
||||
2. Skill never auto-sends anything. Every output is DRAFT.
|
||||
3. Word output must use the `willowbrook-brand-262` skill — no off-brand templates.
|
||||
|
||||
---
|
||||
|
||||
## Support / Feedback
|
||||
|
||||
Skill built by the Claude assistant in collaboration with Weston and the Willowbrook team. To report issues, request additions, or suggest pattern refinements, drop a note in the Claudes Folder on SharePoint or tag Weston directly.
|
||||
|
||||
Version: v1.0 — April 2026
|
||||
@@ -0,0 +1,231 @@
|
||||
---
|
||||
name: willowbrook-submittal-review
|
||||
description: Reviews a construction submittal for Willowbrook Construction Services before it is forwarded to the architect. Checks conformance against Weston's division-by-division guide, flags historical patterns from 500 prior 0309A submittals, and produces an on-screen memo plus a Word checklist. Use when a PM says "review this submittal", "check this submittal", "submittal review", or invokes the `/review-submittal` command. Also use when a PDF or folder of submittal files is handed over with no other explicit task. Do NOT use for RFIs, change orders, bid leveling, sub qualification, or cost analysis — those are separate concerns.
|
||||
argument-hint: "[pdf or folder path] [--spec <spec book path>]"
|
||||
allowed-tools: Bash(python *), Read, Write, Glob, Grep
|
||||
---
|
||||
|
||||
# Willowbrook Submittal Review Skill — v1
|
||||
|
||||
## What this skill is
|
||||
|
||||
A pre-architect review pass on a construction submittal. The PM hands over a PDF or a submittal folder; the skill reports:
|
||||
|
||||
1. Conformance against the division-appropriate checklist from Weston's guide
|
||||
2. Historical pattern risks drawn from 500 prior submittals on project 0309A
|
||||
3. A recommended PM action (directive, not advisory)
|
||||
4. A draft pushback statement the PM can copy to the sub if needed
|
||||
5. A structured JSON file suitable for ingestion by the in-house Willow AI
|
||||
|
||||
Every output is stamped **DRAFT**. The skill never auto-sends anything. The PM is the publish button.
|
||||
|
||||
---
|
||||
|
||||
## Hard rules — do not violate
|
||||
|
||||
1. **Every "pass" result must cite the spec paragraph it passes against.** If the PM supplies `project_spec_path`, load the relevant CSI section text via `scripts/spec_lookup.py` and use it to ground each conformance check with a real citation (section, paragraph, page, short excerpt). Without a loaded spec section the skill cannot emit `Approved` — max is `Approved as Noted`.
|
||||
|
||||
2. **Confidence threshold is 0.95.** Any conformance check the skill is less than 95% certain about is emitted with result `insufficient_data` and confidence < 0.95, and rendered in the Word checklist as "Requires PM judgment." Do not guess.
|
||||
|
||||
3. **Tone is directive.** "Forward to architect." / "Request sample before forwarding." / "Return to sub — hardware schedule is missing PR#05 items." No softeners ("consider", "you may want to", "appears to").
|
||||
|
||||
4. **The skill never edits, sends, uploads, posts, or transmits anything.** Every output is a DRAFT for the PM. If the user asks the skill to send an email, post a comment, or forward the submittal, refuse and explain this is a DRAFT-only tool.
|
||||
|
||||
5. **Word output should follow Willowbrook brand standards.** The bundled `scripts/build_checklist_docx.py` produces a Word doc that already uses Willowbrook colors and typography. If the user also has the `willowbrook-brand-262` skill installed in the session (check `~/.claude/skills/willowbrook-brand-262/SKILL.md` exists), surface a one-line note to the user that they can run that skill on the generated .docx to apply the full brand template — but do NOT attempt to invoke it as a sub-skill. Claude Code does not support skill-to-skill invocation; brand polishing is a follow-up the user runs explicitly.
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
When a PM invokes this skill (natural language like *"review this submittal"* or the `/review-submittal <path>` command), execute these steps **in order**:
|
||||
|
||||
### Step 1 — Identify the input
|
||||
|
||||
Run `scripts/detect_input.py <path>` to determine:
|
||||
- `kind` = `pdf` or `folder`
|
||||
- For folders: which file is the cover sheet vs. attachments
|
||||
|
||||
If `kind` is `invalid`, stop and ask the PM for a valid path.
|
||||
|
||||
### Step 2 — Extract text and metadata
|
||||
|
||||
Run `scripts/parse_submittal.py <path>` to extract:
|
||||
- Full text of the cover sheet
|
||||
- Full text of each attachment (if folder input)
|
||||
- Best-effort structured metadata (submittal_num, revision, spec_section, type, contractor, package, dates)
|
||||
|
||||
If structured metadata is missing critical fields (spec_section or type), read the cover-sheet text and try to fill them. If still missing, emit `overall_status: "INSUFFICIENT_DATA"` and stop.
|
||||
|
||||
### Step 3 — Identify the CSI division and load the checklist
|
||||
|
||||
Look up the extracted `spec_section` in `references/csi_division_map.json`:
|
||||
- Try exact match on the 8-character CSI number (e.g., `"08 71 00"`)
|
||||
- If no exact match, try the 6-character prefix (`"08 71"`)
|
||||
- If still no match, try the 2-character division (`"08"`)
|
||||
- If still nothing, emit a `pattern_flag` noting the CSI section is not mapped, and proceed with the universal first-pass checks only
|
||||
|
||||
Load the matching checklist from `references/weston_guide.md`. Always also apply the **Universal First-Pass Checks** at the top of that file to every submittal.
|
||||
|
||||
### Step 3.5 — Load the spec section text (if project_spec_path was supplied)
|
||||
|
||||
If the PM supplied `project_spec_path`:
|
||||
|
||||
1. Run `scripts/spec_lookup.py <project_spec_path> "<spec_section>"` to resolve the CSI section
|
||||
2. If the folder only has a raw spec PDF (no `_manifest.json` yet), `spec_lookup.py` will auto-run `extract_spec_book.py` and cache into `<project_spec_path>/Extracted Sections/`. First run is slow (minutes); subsequent runs are instant.
|
||||
3. Store the returned `text`, `file`, and `page_breaks` for use in Step 4.
|
||||
4. If the section is not in the spec book, emit a `pattern_flag` noting the gap and proceed without spec text (downgrade max response to `Approved as Noted`).
|
||||
|
||||
When the spec text IS loaded, every conformance check in Step 4 must try to ground its result in a specific paragraph reference. Read the spec text, find the relevant PART 1 / PART 2 / PART 3 paragraph (e.g., "1.4 ACTION SUBMITTALS", "2.1 MANUFACTURERS", "3.3 INSTALLATION"), and capture:
|
||||
- `spec_section` (e.g., "10 22 39")
|
||||
- `spec_paragraph` (e.g., "1.4.B.3" or "2.1.A")
|
||||
- `page` (from the "10 2239-N" marker in the text or from the page-break index)
|
||||
- `excerpt` (short verbatim quote — 1-2 sentences max)
|
||||
|
||||
### Step 4 — Run conformance checks
|
||||
|
||||
For each checklist item:
|
||||
- Read the submittal text (cover sheet + attachments) and decide `pass` | `fail` | `warn` | `not_applicable` | `insufficient_data`
|
||||
- Assign a confidence score (0.0–1.0)
|
||||
- If spec section text is loaded (Step 3.5 succeeded), include a `citation` object for every `pass` and `fail` result. The citation must point to a specific spec paragraph — not just the section.
|
||||
- If the spec is loaded but this particular check can't be grounded in a spec paragraph (it's a procedural check, e.g., "sub has stamped the submittal"), omit the citation and note it as a procedural check rather than a spec-conformance check.
|
||||
- If the confidence is below 0.95, force the result to `insufficient_data`
|
||||
|
||||
### Step 5 — Apply historical pattern flags
|
||||
|
||||
For each pattern in `references/project_playbook.md`:
|
||||
- Check if the trigger conditions match this submittal
|
||||
- If so, add an entry to `pattern_flags[]` with `severity: "warn"` or `"info"`
|
||||
|
||||
Also consult `references/csi_division_map.json → pattern_triggers` for quick lookups:
|
||||
- `needs_sample_separate` — trades where product data approval does not imply color/finish approval
|
||||
- `coordination_required` — trades that must route to other subs
|
||||
- `multi_round_typical` — trades that historically need R2+
|
||||
- `icc_record_submittal` — trades requiring an ICC record copy
|
||||
|
||||
### Step 6 — Determine overall status
|
||||
|
||||
- `PASS` — every check is `pass` with confidence ≥ 0.95 AND every pass has a citation (v2+ only; v1 rarely emits this)
|
||||
- `CONDITIONAL` — all checks are `pass` or `warn` or `not_applicable`; at least one non-critical item is flagged
|
||||
- `FAIL` — any check is `fail`
|
||||
- `INSUFFICIENT_DATA` — too many checks are below the confidence threshold to draw a conclusion
|
||||
|
||||
Map to `submittal_status_recommended`:
|
||||
- `PASS` → `Approved`
|
||||
- `CONDITIONAL` → `Approved as Noted`
|
||||
- `FAIL` → `Revise and Resubmit` (or `Rejected` for severe / repeat failures)
|
||||
|
||||
### Step 7 — Draft the PM action
|
||||
|
||||
One directive sentence. Examples:
|
||||
- "Forward to architect."
|
||||
- "Request PR#05 hardware sets from sub before forwarding."
|
||||
- "Return to sub — submittal is against superseded floor plan."
|
||||
- "Forward to architect; flag that steel fab coordination copy has not been routed."
|
||||
|
||||
### Step 8 — Draft pushback (only if FAIL or CONDITIONAL)
|
||||
|
||||
Write a short paragraph the PM can paste into an email or Procore/Fieldwire comment. Address the sub directly, cite the specific missing/incorrect items, reference the spec section, and state what is needed for resubmittal.
|
||||
|
||||
Example:
|
||||
> "Resubmit with hardware sets for the PR#05 openings (Doors 205A, 205B, 211A). The current schedule is missing preps on these, per spec 08 71 00 §2.3. Once added, we'll forward to 505."
|
||||
|
||||
### Step 9 — Suggest redlines (only if FAIL or CONDITIONAL)
|
||||
|
||||
Where applicable, note page and item for markup on the returned PDF. Example:
|
||||
```json
|
||||
[
|
||||
{ "page": 3, "item": "Hardware Set HW-12", "note": "Missing closer spec" },
|
||||
{ "page": 7, "item": "Fire rating callout", "note": "Shows 20min; door schedule requires 45min" }
|
||||
]
|
||||
```
|
||||
|
||||
### Step 10 — Emit structured JSON
|
||||
|
||||
Write a JSON object conforming to `schemas/output.schema.json` to:
|
||||
`<output_dir>/submittal_<num>_review.json`
|
||||
|
||||
Default `output_dir` is a `review/` subfolder next to the submittal input. Every run also prints a confirmation line to stdout.
|
||||
|
||||
### Step 11 — Build the Word checklist
|
||||
|
||||
1. Run `scripts/build_checklist_docx.py <review.json>` to produce the .docx
|
||||
2. Save as `<output_dir>/submittal_<num>_review.docx`
|
||||
3. If `~/.claude/skills/willowbrook-brand-262/SKILL.md` exists, append a one-line note to the on-screen memo: *"For full brand polish, run the willowbrook-brand-262 skill on the .docx as a follow-up."* Do not attempt to invoke that skill yourself — Claude Code does not support skill-to-skill invocation in v1.
|
||||
|
||||
### Step 12 — Print the on-screen memo
|
||||
|
||||
Print a short, scannable summary to the user. Format:
|
||||
|
||||
```
|
||||
SUBMITTAL REVIEW — #157.1 Operable Partitions - SD
|
||||
────────────────────────────────────────────────────
|
||||
Overall: CONDITIONAL
|
||||
Spec: 10 22 39 - Folding Panel Partitions
|
||||
Type: Shop Drawing
|
||||
Contractor: The Best Company
|
||||
|
||||
Conformance — 4 pass, 1 warn, 0 fail, 2 need PM judgment
|
||||
⚠ Steel beam coordination not documented (p. 2)
|
||||
◻ Requires PM judgment — Wind load rating (confidence 72%)
|
||||
◻ Requires PM judgment — Seismic restraint detail (confidence 84%)
|
||||
|
||||
Pattern Flags
|
||||
⚠ Coordination — operable partitions historically must route to steel fab
|
||||
|
||||
Suggested Action: Forward to architect; confirm steel fab coordination copy has been routed.
|
||||
|
||||
Files written:
|
||||
review/submittal_157_1_review.json
|
||||
review/submittal_157_1_review.docx (branded)
|
||||
```
|
||||
|
||||
Do not print the full conformance list on-screen — it's in the .docx. Keep the on-screen memo to ~15 lines.
|
||||
|
||||
---
|
||||
|
||||
## Skill files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `SKILL.md` | This file — trigger, workflow, rules |
|
||||
| `references/weston_guide.md` | Per-division conformance checklists |
|
||||
| `references/project_playbook.md` | Historical patterns from 500 submittals on 0309A |
|
||||
| `references/csi_division_map.json` | CSI # → checklist lookup + quick pattern triggers |
|
||||
| `schemas/input.schema.json` | I/O contract for Willow integration |
|
||||
| `schemas/output.schema.json` | Structured review output contract |
|
||||
| `scripts/detect_input.py` | Auto-detects PDF vs. folder input |
|
||||
| `scripts/parse_submittal.py` | Extracts text + metadata using PyMuPDF |
|
||||
| `scripts/extract_spec_book.py` | Splits a spec book PDF into one .txt per CSI section + manifest |
|
||||
| `scripts/spec_lookup.py` | Resolves a CSI section → spec text for grounding conformance checks |
|
||||
| `scripts/build_checklist_docx.py` | Renders review.json → baseline .docx |
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
The skill shells out to Python. On first use:
|
||||
|
||||
```
|
||||
pip install pymupdf python-docx pypdf
|
||||
```
|
||||
|
||||
The `willowbrook-brand-262` skill should also be installed for branded .docx output.
|
||||
|
||||
---
|
||||
|
||||
## What v1+ does not do (by design)
|
||||
|
||||
- Does **not** hit Procore, Fieldwire, SharePoint, or any external API. Input is local files only.
|
||||
- Does **not** auto-send anything. Every output is DRAFT.
|
||||
- Does **not** review RFIs, change orders, bid packages, or cost impacts. Separate concerns.
|
||||
- Does **not** modify the input submittal PDF. Output goes in a sibling `review/` folder.
|
||||
- Does **not** validate performance calculations (hydraulic calcs, photometric values, wind loads). It reads them and flags mismatches against spec; it does not recompute.
|
||||
|
||||
---
|
||||
|
||||
## Version
|
||||
|
||||
v1.1 — April 2026 — spec-book-aware
|
||||
- v1.0: static conformance checklist + historical pattern flags
|
||||
- v1.1: folds in the spec-book extractor; conformance checks now cite real spec paragraphs when `project_spec_path` is supplied; `Approved` status is achievable with full citations
|
||||
Built for Willowbrook Construction Services. Target caller for v3+: the Willow AI over the Claude Agent SDK.
|
||||
@@ -0,0 +1,413 @@
|
||||
"""
|
||||
build_corbin_guide.py
|
||||
---------------------
|
||||
Generates a user-friendly Word doc guide for Corbin (PM) explaining how to
|
||||
use the willowbrook-submittal-review skill.
|
||||
"""
|
||||
|
||||
from docx import Document
|
||||
from docx.shared import Pt, RGBColor, Inches
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
import os
|
||||
|
||||
OUTPUT = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"How_to_Use_the_Submittal_Skill.docx",
|
||||
)
|
||||
|
||||
NAVY = RGBColor(0x1F, 0x3A, 0x5F)
|
||||
GOLD = RGBColor(0xC9, 0xA2, 0x27)
|
||||
GRAY = RGBColor(0x55, 0x55, 0x55)
|
||||
GREEN = RGBColor(0x1E, 0x7A, 0x1E)
|
||||
RED = RGBColor(0xB4, 0x1E, 0x1E)
|
||||
LIGHT_BG = "F5F3EB"
|
||||
|
||||
|
||||
def H(doc, text, level=1, color=NAVY):
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(text)
|
||||
r.bold = True
|
||||
r.font.color.rgb = color
|
||||
if level == 0:
|
||||
r.font.size = Pt(26)
|
||||
elif level == 1:
|
||||
r.font.size = Pt(17)
|
||||
p.paragraph_format.space_before = Pt(16)
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
elif level == 2:
|
||||
r.font.size = Pt(13)
|
||||
p.paragraph_format.space_before = Pt(10)
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
return p
|
||||
|
||||
|
||||
def P(doc, text, bold=False, italic=False, color=None, size=11):
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(text)
|
||||
r.bold = bold
|
||||
r.italic = italic
|
||||
r.font.size = Pt(size)
|
||||
if color:
|
||||
r.font.color.rgb = color
|
||||
return p
|
||||
|
||||
|
||||
def bullet(doc, text, indent=0):
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.left_indent = Inches(0.25 + 0.25 * indent)
|
||||
if p.runs:
|
||||
p.runs[0].text = text
|
||||
p.runs[0].font.size = Pt(11)
|
||||
else:
|
||||
p.add_run(text).font.size = Pt(11)
|
||||
return p
|
||||
|
||||
|
||||
def step(doc, n, text):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
r = p.add_run(f"{n}. ")
|
||||
r.bold = True
|
||||
r.font.color.rgb = GOLD
|
||||
r.font.size = Pt(12)
|
||||
r2 = p.add_run(text)
|
||||
r2.font.size = Pt(11)
|
||||
return p
|
||||
|
||||
|
||||
def code_block(doc, text):
|
||||
"""Indented grey mono block that looks like a command."""
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.left_indent = Inches(0.35)
|
||||
p.paragraph_format.space_before = Pt(3)
|
||||
p.paragraph_format.space_after = Pt(8)
|
||||
|
||||
# Shading via XML
|
||||
pPr = p._p.get_or_add_pPr()
|
||||
shd = OxmlElement("w:shd")
|
||||
shd.set(qn("w:val"), "clear")
|
||||
shd.set(qn("w:color"), "auto")
|
||||
shd.set(qn("w:fill"), LIGHT_BG)
|
||||
pPr.append(shd)
|
||||
|
||||
r = p.add_run(text)
|
||||
r.font.name = "Consolas"
|
||||
r.font.size = Pt(10)
|
||||
return p
|
||||
|
||||
|
||||
def callout(doc, label, text, color=GOLD):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.left_indent = Inches(0.2)
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
r = p.add_run(f"{label} ")
|
||||
r.bold = True
|
||||
r.font.color.rgb = color
|
||||
r.font.size = Pt(11)
|
||||
r2 = p.add_run(text)
|
||||
r2.font.size = Pt(11)
|
||||
return p
|
||||
|
||||
|
||||
def hr(doc):
|
||||
p = doc.add_paragraph()
|
||||
pPr = p._p.get_or_add_pPr()
|
||||
pBdr = OxmlElement("w:pBdr")
|
||||
bottom = OxmlElement("w:bottom")
|
||||
bottom.set(qn("w:val"), "single")
|
||||
bottom.set(qn("w:sz"), "6")
|
||||
bottom.set(qn("w:color"), "C9A227")
|
||||
pBdr.append(bottom)
|
||||
pPr.append(pBdr)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
doc = Document()
|
||||
style = doc.styles["Normal"]
|
||||
style.font.name = "Calibri"
|
||||
style.font.size = Pt(11)
|
||||
|
||||
# ── Cover ───────────────────────────────────────────────────────────────────
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run("How to Use the")
|
||||
r.bold = True; r.font.size = Pt(14); r.font.color.rgb = GRAY
|
||||
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run("Willowbrook Submittal Review Skill")
|
||||
r.bold = True; r.font.size = Pt(28); r.font.color.rgb = NAVY
|
||||
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run("A plain-English guide for Willowbrook project managers")
|
||||
r.italic = True; r.font.size = Pt(13); r.font.color.rgb = GRAY
|
||||
|
||||
hr(doc)
|
||||
|
||||
# ── What it is ───────────────────────────────────────────────────────────────
|
||||
H(doc, "What this tool actually does", level=1)
|
||||
|
||||
P(doc,
|
||||
"You drag a submittal at it — either a single PDF or the whole Procore "
|
||||
"export folder — and it does three things before the architect ever sees "
|
||||
"it:")
|
||||
|
||||
bullet(doc, "Checks the submittal against a detailed per-trade checklist "
|
||||
"(Weston's guide) so nothing obvious is missing.")
|
||||
bullet(doc, "Compares it to the actual project spec book and cites specific "
|
||||
"paragraphs the submittal passes or fails against.")
|
||||
bullet(doc, "Flags known patterns from 500 real submittals on 0309A — things "
|
||||
"like \"operable partitions historically need to be routed to the "
|
||||
"steel fabricator.\"")
|
||||
|
||||
P(doc, " ")
|
||||
P(doc, "You get two outputs every time:", bold=True)
|
||||
bullet(doc, "A short summary printed on your screen (scan in 60 seconds).")
|
||||
bullet(doc, "A full Word checklist saved next to the submittal — branded, "
|
||||
"marked DRAFT, ready to hand to the architect or send back to the sub.")
|
||||
|
||||
P(doc, " ")
|
||||
callout(doc, "Bottom line:",
|
||||
"Before every submittal leaves your desk, you run it through this. "
|
||||
"It takes 30 seconds and catches the stuff you'd otherwise miss at 4:55 "
|
||||
"on a Friday.", color=GOLD)
|
||||
|
||||
hr(doc)
|
||||
|
||||
# ── First-time setup ─────────────────────────────────────────────────────────
|
||||
H(doc, "First-time setup (do this once, takes 10 minutes)", level=1)
|
||||
|
||||
P(doc, "You'll do four small things. Ask Weston or IT for help on any of them "
|
||||
"— none of this is stuff you need to know how to do.")
|
||||
|
||||
H(doc, "1. Install Claude Code", level=2)
|
||||
P(doc, "If you don't already have it, download from claude.ai/download and "
|
||||
"install. Sign in with your Willowbrook account.")
|
||||
|
||||
H(doc, "2. Install Python (if it's not already there)", level=2)
|
||||
P(doc, "Open the Start menu, type \"cmd\", open the Command Prompt. Type:")
|
||||
code_block(doc, "python --version")
|
||||
P(doc, "If you see a version number (like \"Python 3.12.x\"), you're good. "
|
||||
"If you get \"'python' is not recognized\", install from "
|
||||
"python.org/downloads — pick the box that says \"Add Python to PATH\" "
|
||||
"during install.")
|
||||
|
||||
H(doc, "3. Install the three small Python helpers the skill uses", level=2)
|
||||
P(doc, "In Command Prompt, paste this and hit Enter:")
|
||||
code_block(doc, "pip install pymupdf python-docx pypdf")
|
||||
P(doc, "You'll see a bunch of green text scroll by. When the prompt comes back "
|
||||
"(a blinking cursor on a new line), you're done. It doesn't matter what "
|
||||
"folder the command prompt is in — pip installs to Python, not to a folder.")
|
||||
|
||||
H(doc, "4. Drop the skill into your Claude folder", level=2)
|
||||
P(doc, "The skill lives in a folder called "
|
||||
"willowbrook-submittal-review. Copy that whole folder into:")
|
||||
code_block(doc, "C:\\Users\\<your name>\\.claude\\skills\\")
|
||||
P(doc, "That .claude folder already exists if you've opened Claude Code before. "
|
||||
"If not, it'll create itself the first time you run Claude. "
|
||||
"Restart Claude Code after dropping the folder in.",
|
||||
italic=True, color=GRAY, size=10)
|
||||
|
||||
callout(doc, "Weston will share the skill folder on SharePoint:",
|
||||
"\\\\willowbrook\\shared\\Claudes Folder\\skills\\willowbrook-submittal-review\\ "
|
||||
"Just copy-paste that whole folder into your .claude/skills/ folder.",
|
||||
color=GOLD)
|
||||
|
||||
hr(doc)
|
||||
|
||||
# ── How to use it ────────────────────────────────────────────────────────────
|
||||
H(doc, "How to use it — the 30-second version", level=1)
|
||||
|
||||
P(doc, "Open Claude Code. Type one of these two things:")
|
||||
|
||||
H(doc, "Option A — Natural language (easiest)", level=2)
|
||||
code_block(doc,
|
||||
"Review this submittal: \"C:\\Users\\<you>\\Downloads\\157_1_Operable Partitions\"\n"
|
||||
"Spec book: \"C:\\Users\\<you>\\SharePoint\\0309A\\Spec Book\"")
|
||||
P(doc, "You can drag the submittal folder from File Explorer straight into "
|
||||
"the Claude window — it'll paste the path for you. Same with the spec "
|
||||
"book folder.", italic=True, color=GRAY, size=10)
|
||||
|
||||
H(doc, "Option B — Slash command (faster once you get used to it)", level=2)
|
||||
code_block(doc,
|
||||
'/review-submittal "C:\\...\\157_1_Operable Partitions" '
|
||||
'--spec "C:\\...\\0309A\\Spec Book"')
|
||||
|
||||
P(doc, " ")
|
||||
P(doc, "That's it. Claude takes 30–60 seconds and gives you back the review.",
|
||||
bold=True)
|
||||
|
||||
hr(doc)
|
||||
|
||||
# ── What to hand it ──────────────────────────────────────────────────────────
|
||||
H(doc, "What files to point it at", level=1)
|
||||
|
||||
H(doc, "The submittal", level=2)
|
||||
bullet(doc, "Option 1 — a single PDF of the submittal (works fine).")
|
||||
bullet(doc, "Option 2 — the whole Procore / Fieldwire folder for that "
|
||||
"submittal (better — it has the cover sheet + all attachments).")
|
||||
P(doc, "The skill figures out which kind you gave it automatically.",
|
||||
italic=True, color=GRAY, size=10)
|
||||
|
||||
H(doc, "The spec book (optional but highly recommended)", level=2)
|
||||
bullet(doc, "Point it at the project's Spec Book folder on SharePoint — "
|
||||
"something like C:\\Users\\<you>\\SharePoint\\0309A - "
|
||||
"Stillwater HS\\Specifications\\")
|
||||
bullet(doc, "Or point it at the raw spec-book PDF directly — it'll split it "
|
||||
"up by section the first time (takes a minute or two), then cache "
|
||||
"it so every run after that is instant.")
|
||||
|
||||
P(doc, " ")
|
||||
callout(doc, "Without the spec book loaded,",
|
||||
"the skill can only run the generic checklist. With the spec book "
|
||||
"loaded, it cites the exact paragraph that a missing item violates "
|
||||
"— which is what gets it through architect review cleanly. "
|
||||
"Spend the extra 2 seconds pointing at the spec folder.",
|
||||
color=GOLD)
|
||||
|
||||
hr(doc)
|
||||
|
||||
# ── Reading the output ───────────────────────────────────────────────────────
|
||||
H(doc, "Understanding what you get back", level=1)
|
||||
|
||||
H(doc, "The on-screen summary", level=2)
|
||||
P(doc, "About 15 lines. It tells you:")
|
||||
bullet(doc, "Overall status — PASS, CONDITIONAL, FAIL, or INSUFFICIENT_DATA.")
|
||||
bullet(doc, "What the skill recommends the architect response should be.")
|
||||
bullet(doc, "The specific items that failed or need your judgment.")
|
||||
bullet(doc, "Any historical pattern flags (things that burned us before).")
|
||||
bullet(doc, "One directive sentence telling you what to do next.")
|
||||
|
||||
H(doc, "The Word checklist", level=2)
|
||||
P(doc, "Saved in a review/ folder right next to the submittal. Opens in Word, "
|
||||
"looks like a branded Willowbrook doc, stamped DRAFT at the top.")
|
||||
P(doc, "It contains:")
|
||||
bullet(doc, "The full per-trade conformance checklist with pass/fail/warn marks.")
|
||||
bullet(doc, "Spec paragraph citations for every finding (when the spec is loaded).")
|
||||
bullet(doc, "Historical pattern flags with the language to copy into your notes.")
|
||||
bullet(doc, "A draft pushback paragraph if you need to send it back to the sub "
|
||||
"— just copy, edit, paste into email / Fieldwire / Procore.")
|
||||
bullet(doc, "Suggested redline callouts for marking up the returned PDF.")
|
||||
|
||||
H(doc, "The JSON file", level=2)
|
||||
P(doc, "Same folder as the Word doc. Not for you — it's for Willow to ingest "
|
||||
"later. Ignore it, but don't delete it if you're doing a run log for IT.",
|
||||
italic=True, color=GRAY)
|
||||
|
||||
hr(doc)
|
||||
|
||||
# ── Statuses ─────────────────────────────────────────────────────────────────
|
||||
H(doc, "What the statuses mean", level=1)
|
||||
|
||||
H(doc, "PASS (green)", level=2, color=GREEN)
|
||||
P(doc, "Every required item was found AND cited against the spec. "
|
||||
"Recommended response to architect: Approved. Forward it.")
|
||||
|
||||
H(doc, "CONDITIONAL (gold)", level=2, color=GOLD)
|
||||
P(doc, "Most items check out, but a few are flagged as warnings or need your "
|
||||
"judgment. Recommended response: Approved as Noted. Read the warnings, "
|
||||
"confirm they're acceptable, then forward.")
|
||||
|
||||
H(doc, "FAIL (red)", level=2, color=RED)
|
||||
P(doc, "Something substantive is missing or wrong. Recommended response: "
|
||||
"Revise and Resubmit. Use the draft pushback language to send it back "
|
||||
"to the sub before the architect sees it.")
|
||||
|
||||
H(doc, "INSUFFICIENT_DATA (gray)", level=2, color=GRAY)
|
||||
P(doc, "The skill couldn't evaluate enough of the checklist with high "
|
||||
"confidence — usually because the submittal came in as a scanned image "
|
||||
"instead of a text PDF. Review it manually. Tell Weston which submittal "
|
||||
"did this so we can improve the parser.")
|
||||
|
||||
hr(doc)
|
||||
|
||||
# ── Three things the skill won't do ──────────────────────────────────────────
|
||||
H(doc, "Three things the skill will NOT do (by design)", level=1)
|
||||
|
||||
bullet(doc, "Send anything, anywhere. Every output is DRAFT. You are the "
|
||||
"publish button — it's always you that forwards to the architect, "
|
||||
"always you that sends pushback to the sub.")
|
||||
bullet(doc, "Make the final decision on Approved vs. Approved as Noted. It "
|
||||
"recommends; you decide.")
|
||||
bullet(doc, "Review RFIs, change orders, bid leveling, sub qualification, or "
|
||||
"cost analysis. Different tools for different jobs.")
|
||||
|
||||
hr(doc)
|
||||
|
||||
# ── Troubleshooting ──────────────────────────────────────────────────────────
|
||||
H(doc, "If something doesn't work", level=1)
|
||||
|
||||
H(doc, "Claude doesn't respond to \"review this submittal\"", level=2)
|
||||
bullet(doc, "Did you restart Claude Code after dropping the skill folder in?")
|
||||
bullet(doc, "Is the folder in ~/.claude/skills/ and named exactly "
|
||||
"willowbrook-submittal-review (no extra characters)?")
|
||||
|
||||
H(doc, "\"Missing dependency: pymupdf\" (or python-docx or pypdf)", level=2)
|
||||
bullet(doc, "Run pip install pymupdf python-docx pypdf in Command Prompt again. "
|
||||
"Watch for errors in the output — if pip can't install, your Python "
|
||||
"install is probably the issue, not the skill.")
|
||||
|
||||
H(doc, "The skill says INSUFFICIENT_DATA", level=2)
|
||||
bullet(doc, "The submittal is probably a scanned image PDF. You'll need to "
|
||||
"review it manually, or re-request a text-based PDF from the sub.")
|
||||
bullet(doc, "Tell Weston — if it happens a lot, the next version can add OCR.")
|
||||
|
||||
H(doc, "The spec book extraction is taking forever", level=2)
|
||||
bullet(doc, "First time only, for a big spec book it can take 5+ minutes. "
|
||||
"Every run after that reads from the cache and is instant.")
|
||||
bullet(doc, "If it's been more than 15 minutes, kill it and let Weston know.")
|
||||
|
||||
H(doc, "The Word doc doesn't match the Willowbrook brand", level=2)
|
||||
bullet(doc, "The skill calls a separate brand skill to apply formatting. If "
|
||||
"that's not installed, you'll get a plainer version — still usable, "
|
||||
"just not branded. Ask Weston to share the willowbrook-brand-262 "
|
||||
"skill too.")
|
||||
|
||||
hr(doc)
|
||||
|
||||
# ── Quick reference ──────────────────────────────────────────────────────────
|
||||
H(doc, "Quick reference card", level=1)
|
||||
|
||||
P(doc, "Tape this to your monitor:", italic=True, color=GRAY, size=10)
|
||||
|
||||
code_block(doc,
|
||||
"One-time setup:\n"
|
||||
" pip install pymupdf python-docx pypdf\n"
|
||||
" Drop willowbrook-submittal-review/ into ~/.claude/skills/\n"
|
||||
" Restart Claude Code\n"
|
||||
"\n"
|
||||
"Every submittal:\n"
|
||||
" \"Review this submittal: <folder> Spec book: <spec folder>\"\n"
|
||||
" (Or drag the folders in — Claude reads them either way)\n"
|
||||
"\n"
|
||||
"Reading the result:\n"
|
||||
" PASS -> forward to architect\n"
|
||||
" CONDITIONAL -> address warnings, then forward\n"
|
||||
" FAIL -> copy pushback draft, send to sub first\n"
|
||||
" INSUFFICIENT -> review manually, tell Weston\n"
|
||||
"\n"
|
||||
"All outputs are DRAFT. You are the publish button.")
|
||||
|
||||
P(doc, " ")
|
||||
|
||||
# ── Footer ───────────────────────────────────────────────────────────────────
|
||||
doc.add_paragraph()
|
||||
f = doc.add_paragraph()
|
||||
fr = f.add_run(
|
||||
"Willowbrook Construction Services | Submittal Review Skill v1.1 | "
|
||||
"PM Quick-Start Guide"
|
||||
)
|
||||
fr.italic = True
|
||||
fr.font.size = Pt(9)
|
||||
fr.font.color.rgb = GRAY
|
||||
|
||||
f2 = doc.add_paragraph()
|
||||
fr2 = f2.add_run(
|
||||
"Questions, issues, or suggestions: talk to Weston. Feedback makes v1.2 better."
|
||||
)
|
||||
fr2.italic = True
|
||||
fr2.font.size = Pt(9)
|
||||
fr2.font.color.rgb = GRAY
|
||||
|
||||
doc.save(OUTPUT)
|
||||
print(f"Saved: {OUTPUT}")
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"description": "Maps CSI section numbers to the checklist section ID in weston_guide.md. When a submittal's spec_section is identified, look up the matching key (try exact match first, then fall back to prefix match on the 4-digit section, then the 2-digit division).",
|
||||
"sections": {
|
||||
"02 41 00": { "title": "Demolition", "checklist": "02 41 00 - Demolition" },
|
||||
"02 82 00": { "title": "Asbestos Abatement", "checklist": "02 82 00 - Asbestos Abatement" },
|
||||
"02 83 00": { "title": "Lead Abatement", "checklist": "02 82 00 - Asbestos Abatement" },
|
||||
"03 30 00": { "title": "Cast-in-Place Concrete", "checklist": "03 30 00 - Cast-in-Place Concrete" },
|
||||
"03 35 00": { "title": "Concrete Finishing / Polished Concrete", "checklist": "03 35 00 - Concrete Finishing / Polished Concrete" },
|
||||
"03 45 00": { "title": "Precast Architectural Concrete", "checklist": "03 45 00 - Precast Architectural Concrete" },
|
||||
"04 20 00": { "title": "Unit Masonry", "checklist": "04 20 00 - Unit Masonry (CMU / Brick)" },
|
||||
"04 22 00": { "title": "Concrete Unit Masonry", "checklist": "04 20 00 - Unit Masonry (CMU / Brick)" },
|
||||
"04 26 13": { "title": "Masonry Veneer", "checklist": "04 20 00 - Unit Masonry (CMU / Brick)" },
|
||||
"04 72 00": { "title": "Cast Stone", "checklist": "04 72 00 - Cast Stone / Architectural Stone" },
|
||||
"05 12 00": { "title": "Structural Steel Framing", "checklist": "05 12 00 - Structural Steel Framing" },
|
||||
"05 31 00": { "title": "Steel Deck", "checklist": "05 31 00 - Steel Deck" },
|
||||
"05 40 00": { "title": "Cold-Formed Metal Framing", "checklist": "05 40 00 - Cold-Formed Metal Framing" },
|
||||
"05 50 00": { "title": "Metal Fabrications", "checklist": "05 50 00 - Miscellaneous Metal Fabrications" },
|
||||
"06 10 00": { "title": "Rough Carpentry", "checklist": "06 10 00 - Rough Carpentry" },
|
||||
"06 16 00": { "title": "Sheathing", "checklist": "06 10 00 - Rough Carpentry" },
|
||||
"06 20 00": { "title": "Finish Carpentry", "checklist": "06 20 00 - Finish Carpentry / Trim" },
|
||||
"06 40 00": { "title": "Architectural Woodwork", "checklist": "06 40 00 - Architectural Woodwork / Casework" },
|
||||
"06 64 00": { "title": "FRP", "checklist": "06 40 00 - Architectural Woodwork / Casework" },
|
||||
"07 13 00": { "title": "Sheet Waterproofing", "checklist": "07 13 00 / 07 14 00 - Sheet / Fluid-Applied Waterproofing" },
|
||||
"07 14 00": { "title": "Fluid-Applied Waterproofing", "checklist": "07 13 00 / 07 14 00 - Sheet / Fluid-Applied Waterproofing" },
|
||||
"07 21 00": { "title": "Thermal Insulation", "checklist": "07 21 00 - Thermal Insulation" },
|
||||
"07 27 00": { "title": "Air Barriers", "checklist": "07 27 00 - Air Barriers" },
|
||||
"07 40 00": { "title": "Roofing", "checklist": "07 40 00 - Roofing (TPO, EPDM, Modified Bitumen, Metal)" },
|
||||
"07 54 23": { "title": "TPO Roofing", "checklist": "07 40 00 - Roofing (TPO, EPDM, Modified Bitumen, Metal)" },
|
||||
"07 62 00": { "title": "Metal Flashing", "checklist": "07 62 00 - Metal Flashing and Trim" },
|
||||
"07 84 00": { "title": "Firestopping", "checklist": "07 84 00 - Firestopping" },
|
||||
"07 92 00": { "title": "Joint Sealants", "checklist": "07 92 00 - Joint Sealants" },
|
||||
"08 11 13": { "title": "Hollow Metal Doors and Frames", "checklist": "08 11 13 - Hollow Metal Doors and Frames" },
|
||||
"08 14 16": { "title": "Flush Wood Doors", "checklist": "08 14 16 - Flush Wood Doors" },
|
||||
"08 33 00": { "title": "Overhead Coiling Doors", "checklist": "08 33 00 - Overhead Coiling / Sectional Doors" },
|
||||
"08 33 26": { "title": "Overhead Coiling Grilles", "checklist": "08 33 00 - Overhead Coiling / Sectional Doors" },
|
||||
"08 41 13": { "title": "Aluminum-Framed Entrances and Storefronts", "checklist": "08 41 13 - Aluminum-Framed Entrances and Storefronts" },
|
||||
"08 71 00": { "title": "Door Hardware", "checklist": "08 71 00 - Door Hardware" },
|
||||
"08 80 00": { "title": "Glazing", "checklist": "08 80 00 - Glazing" },
|
||||
"09 21 16": { "title": "Shaft Wall Assemblies", "checklist": "09 22 00 / 09 29 00 - Non-Structural Metal Framing / Gypsum Board" },
|
||||
"09 22 00": { "title": "Non-Structural Metal Framing", "checklist": "09 22 00 / 09 29 00 - Non-Structural Metal Framing / Gypsum Board" },
|
||||
"09 22 16": { "title": "Non-Structural Metal Framing", "checklist": "09 22 00 / 09 29 00 - Non-Structural Metal Framing / Gypsum Board" },
|
||||
"09 29 00": { "title": "Gypsum Board", "checklist": "09 22 00 / 09 29 00 - Non-Structural Metal Framing / Gypsum Board" },
|
||||
"09 30 00": { "title": "Tiling", "checklist": "09 30 00 - Tile" },
|
||||
"09 30 13": { "title": "Ceramic Tiling", "checklist": "09 30 00 - Tile" },
|
||||
"09 51 00": { "title": "Acoustical Ceilings", "checklist": "09 51 00 - Acoustical Ceilings" },
|
||||
"09 65 00": { "title": "Resilient Flooring", "checklist": "09 65 00 - Resilient Flooring / LVT / Rubber Base" },
|
||||
"09 68 00": { "title": "Carpet", "checklist": "09 68 00 - Carpet Tile / Broadloom" },
|
||||
"09 90 00": { "title": "Painting and Coatings", "checklist": "09 90 00 - Painting and Coatings" },
|
||||
"10 11 00": { "title": "Visual Display Boards", "checklist": null },
|
||||
"10 14 00": { "title": "Signage", "checklist": "10 14 00 - Signage" },
|
||||
"10 21 13": { "title": "Toilet Partitions", "checklist": "10 21 13 - Toilet Partitions" },
|
||||
"10 22 39": { "title": "Operable Partitions", "checklist": null },
|
||||
"10 28 00": { "title": "Toilet and Bath Accessories", "checklist": "10 28 00 - Toilet and Bath Accessories" },
|
||||
"10 44 00": { "title": "Fire Extinguishers", "checklist": "10 44 00 - Fire Extinguishers and Cabinets" },
|
||||
"10 51 00": { "title": "Lockers", "checklist": "10 51 00 - Lockers" },
|
||||
"11 30 00": { "title": "Residential Appliances", "checklist": "11 30 00 - Residential Appliances" },
|
||||
"11 40 00": { "title": "Food Service Equipment", "checklist": "11 40 00 - Food Service Equipment (FSE)" },
|
||||
"11 66 00": { "title": "Athletic Equipment", "checklist": "11 66 00 - Athletic Equipment" },
|
||||
"12 24 13": { "title": "Window Treatments", "checklist": "12 24 13 - Window Treatments (Roller Shades, Blinds)" },
|
||||
"12 36 00": { "title": "Countertops", "checklist": "12 36 00 - Countertops (Solid Surface, Quartz, Stone)" },
|
||||
"13 31 00": { "title": "Fabric Structures", "checklist": "13 31 00 - Fabric Structures / Pre-Engineered Metal Building" },
|
||||
"13 34 19": { "title": "Metal Building Systems", "checklist": "13 34 19 - Metal Building Systems" },
|
||||
"14 24 00": { "title": "Elevators", "checklist": "14 24 00 - Hydraulic / Traction Elevators" },
|
||||
"21 13 00": { "title": "Wet-Pipe Fire Sprinklers", "checklist": "21 13 00 - Wet-Pipe Fire Sprinklers" },
|
||||
"22 10 00": { "title": "Plumbing Piping", "checklist": "22 10 00 - Plumbing Piping" },
|
||||
"22 30 00": { "title": "Water Heaters", "checklist": "22 30 00 - Water Heaters / Tanks" },
|
||||
"22 40 00": { "title": "Plumbing Fixtures", "checklist": "22 40 00 - Plumbing Fixtures" },
|
||||
"23 05 00": { "title": "HVAC Common Work Results", "checklist": "23 05 00 - Common Work Results for HVAC" },
|
||||
"23 09 00": { "title": "HVAC Controls / BMS", "checklist": "23 09 00 - Instrumentation and Controls / BMS" },
|
||||
"23 30 00": { "title": "Air Distribution / Ductwork", "checklist": "23 30 00 - Air Distribution (Ductwork)" },
|
||||
"23 70 00": { "title": "Air Handling / RTU", "checklist": "23 70 00 - Air Handling Units / Rooftop Units" },
|
||||
"23 80 00": { "title": "Decentralized HVAC (VRF, Split, FCU)", "checklist": "23 80 00 - Decentralized HVAC Equipment (VRF, Split, FCU)" },
|
||||
"26 05 00": { "title": "Electrical Common Work Results", "checklist": "26 05 00 - Common Work Results for Electrical" },
|
||||
"26 24 00": { "title": "Switchboards / Panelboards", "checklist": "26 24 00 - Switchboards / Panelboards / MCC" },
|
||||
"26 27 00": { "title": "Wiring Devices", "checklist": "26 27 00 - Wiring Devices" },
|
||||
"26 51 00": { "title": "Interior Lighting", "checklist": "26 51 00 - Interior Lighting" },
|
||||
"26 56 00": { "title": "Exterior Lighting", "checklist": "26 56 00 - Exterior Lighting" },
|
||||
"27 10 00": { "title": "Structured Cabling", "checklist": "27 10 00 - Structured Cabling" },
|
||||
"27 40 00": { "title": "Audio-Video Systems", "checklist": "27 40 00 - Audio-Video Systems" },
|
||||
"28 13 00": { "title": "Access Control", "checklist": "28 13 00 / 28 23 00 - Access Control / Video Surveillance" },
|
||||
"28 23 00": { "title": "Video Surveillance", "checklist": "28 13 00 / 28 23 00 - Access Control / Video Surveillance" },
|
||||
"28 31 00": { "title": "Fire Detection and Alarm", "checklist": "28 31 00 - Fire Detection and Alarm" },
|
||||
"31 10 00": { "title": "Site Clearing", "checklist": "31 10 00 / 31 20 00 - Site Clearing / Earth Moving" },
|
||||
"31 20 00": { "title": "Earth Moving", "checklist": "31 10 00 / 31 20 00 - Site Clearing / Earth Moving" },
|
||||
"31 60 00": { "title": "Special Foundations", "checklist": "31 60 00 - Special Foundations / Piers / Piles" },
|
||||
"32 12 00": { "title": "Asphalt Paving", "checklist": "32 12 00 - Flexible Paving (Asphalt)" },
|
||||
"32 13 00": { "title": "Concrete Paving", "checklist": "32 13 00 - Rigid Paving (Concrete)" },
|
||||
"32 16 00": { "title": "Curbs, Gutters, Walks", "checklist": "32 16 00 - Curbs, Gutters, Walks" },
|
||||
"32 31 13": { "title": "Chain Link Fencing", "checklist": null },
|
||||
"32 90 00": { "title": "Landscaping", "checklist": "32 90 00 - Landscaping" },
|
||||
"33 10 00": { "title": "Water Utilities", "checklist": "33 10 00 - Water Utilities" },
|
||||
"33 30 00": { "title": "Sanitary Sewer", "checklist": "33 30 00 - Sanitary Sewer" },
|
||||
"33 40 00": { "title": "Storm Drainage", "checklist": "33 40 00 - Storm Drainage" }
|
||||
},
|
||||
"pattern_triggers": {
|
||||
"needs_sample_separate": [
|
||||
"07 62 00", "07 40 00", "04 20 00", "04 72 00", "06 40 00",
|
||||
"10 21 13", "09 90 00", "12 36 00", "09 30 00"
|
||||
],
|
||||
"coordination_required": {
|
||||
"10 22 39": ["Steel fabricator"],
|
||||
"08 33 00": ["Steel fabricator", "Precast fabricator"],
|
||||
"08 33 26": ["Steel fabricator"],
|
||||
"08 41 13": ["Metal panel fabricator"],
|
||||
"22 40 00": ["Civil engineer"],
|
||||
"08 11 13": ["Masonry contractor"],
|
||||
"14 24 00": ["Structural engineer", "Electrical", "MEP"]
|
||||
},
|
||||
"multi_round_typical": [
|
||||
"26 51 00", "26 56 00", "10 14 00", "06 40 00"
|
||||
],
|
||||
"icc_record_submittal": [
|
||||
"07 40 00", "07 54 23", "21 13 00", "07 84 00", "28 31 00", "14 24 00"
|
||||
]
|
||||
}
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
# Submittal Pattern Playbook (from 0309A Stillwater HS — 500 submittals)
|
||||
|
||||
Behavioral patterns from 500 real submittals. Use these to flag risks beyond the per-trade conformance checklists. When a submittal matches a pattern below, raise the flag in the review memo's "Historical Pattern Flags" section.
|
||||
|
||||
---
|
||||
|
||||
## Response distribution (for context)
|
||||
|
||||
- 73% Reviewed (clean approval)
|
||||
- 6% Reviewed As Noted (conditional approval)
|
||||
- 2% Revise and Resubmit (hard rejection)
|
||||
- 23% of submittals went to at least one revision
|
||||
- 17% had written reviewer comments
|
||||
|
||||
---
|
||||
|
||||
## Pattern 1 — Incomplete Packages
|
||||
|
||||
**Historical examples:**
|
||||
- Hollow Metal Doors — "Submit Missing Doors"
|
||||
- Door Hardware — hardware sets for PR#05 were missing
|
||||
- Interior Storefronts — "Need submittal on Doors from Addendum 1"
|
||||
- Floor Boxes — "Please resubmit floor box information per the redlines"
|
||||
|
||||
**Flag trigger:** Submittal references a door schedule, fixture schedule, hardware schedule, or any enumerated list that may have been updated by addendum.
|
||||
|
||||
**Flag language:** *"This trade historically has incomplete-package rejections. Verify every item in the spec section is included, and cross-check addenda for added items."*
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2 — Wrong or Outdated Drawings
|
||||
|
||||
**Historical example:** Duct Work (Floor 1 Area 5) — *"Not the current floor plans"*
|
||||
|
||||
**Flag trigger:** Any shop drawing submittal. Particularly flag HVAC/MEP, framing, and anything referencing floor plan dimensions.
|
||||
|
||||
**Flag language:** *"Confirm the drawings referenced are the current issued-for-construction set. Check for recent ASIs or PRs that may have changed dimensions or locations."*
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3 — Samples Required Separate from Product Data
|
||||
|
||||
**Historical examples:**
|
||||
- Sheet Metal Flashing — "Need to see a physical sample of the coping color that is to match PT-4 (Stillwater Gold)"
|
||||
- Louvers — "Provide physical samples of the clouded colors"
|
||||
- Roof Skylights — "Submit samples for final approval" (after product data review)
|
||||
- Millwork — "Please resubmit SS1 photo with color and manufacturer info"
|
||||
- Toilet Partitions — "Please confirm the material texture and color"
|
||||
- Direct Applied Soffit/Ceiling Finish — "Revise and resubmit gold sample"
|
||||
|
||||
**Flag trigger:** Submittal type is Product Information AND the product has a specified custom color, finish, or texture.
|
||||
|
||||
**Flag language:** *"Product data approval does not include color/finish approval. A separate physical sample submittal is historically required for this trade. Do not release material order until sample is approved."*
|
||||
|
||||
**Trades most affected:** Sheet metal, louvers, roofing, millwork, toilet partitions, soffit finishes, metal panels, masonry veneer.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 4 — Missing Multi-Trade Coordination
|
||||
|
||||
**Historical examples:**
|
||||
- Operable Partitions — "Please make sure this gets sent to steel fabricator for steel beam coordination"
|
||||
- Overhead Fire Rated Coiling Doors — "Please make sure this also gets sent to the Steel Fabricator for steel coordination"
|
||||
- Tornado Resistant Coiling Doors — "This should also go to the Precast Fabricator for coordination"
|
||||
- Grease Interceptor — civil engineer confirmation needed
|
||||
- Curtain wall — metal panel connection detail coordination
|
||||
|
||||
**Flag trigger:** The submittal involves an opening, door, curtain wall, operable partition, or any item that bears on / attaches to / penetrates another trade's work.
|
||||
|
||||
**Flag language:** *"This item historically requires coordination with [affected trade]. Confirm the submittal has been routed to that sub before architect review."*
|
||||
|
||||
**Coordination matrix:**
|
||||
|
||||
| Submittal trade | Also routes to |
|
||||
|---|---|
|
||||
| Operable partitions | Steel fabricator |
|
||||
| Coiling doors (fire-rated, tornado) | Steel fabricator, Precast fabricator |
|
||||
| Curtain wall | Metal panel fabricator |
|
||||
| Grease interceptor | Civil engineer |
|
||||
| Ductwork | MEP engineer for current floor plan version |
|
||||
| Hollow metal frames (at masonry) | Masonry contractor |
|
||||
| Elevator | Structural, electrical, MEP |
|
||||
| Overhead doors (at precast jamb) | Precast fabricator |
|
||||
|
||||
---
|
||||
|
||||
## Pattern 5 — Over-Resubmittal (Resubmitting Whole Package)
|
||||
|
||||
**Historical examples:**
|
||||
- Metal Panels — "Revise and Resubmit only the elevations and panels that have not been marked as approved"
|
||||
- Millwork — "Resubmit only request sheets"
|
||||
- Interior Lighting — "Revise and Resubmit only the fixtures noted as such"
|
||||
|
||||
**Flag trigger:** Submittal is a revision (R1, R2, R3) AND the prior submittal was Reviewed As Noted with partial approval.
|
||||
|
||||
**Flag language:** *"Prior revision was partially approved. Only the flagged items should be resubmitted. Confirm this revision does not include items that were already approved — doing so resets the review clock."*
|
||||
|
||||
---
|
||||
|
||||
## Pattern 6 — Missing ICC / Record Submittals
|
||||
|
||||
**Historical example:** Roof Hoods — *"Being is this an ICC submittal. We do need a records submittal with the redlines picked up"*
|
||||
|
||||
**Flag trigger:** Item is subject to AHJ inspection (roofing, fire suppression, life safety, firestopping, elevator, fire alarm).
|
||||
|
||||
**Flag language:** *"Confirm whether an ICC / record submittal with redlines incorporated is required for this item. If so, prepare a separate record copy."*
|
||||
|
||||
---
|
||||
|
||||
## Pattern 7 — Multi-Round Trades (Plan for R2+)
|
||||
|
||||
**Trades that historically required multiple rounds:**
|
||||
|
||||
| Trade | Rounds observed | Pattern |
|
||||
|---|---|---|
|
||||
| Interior Lighting | R0 → R2 | Partial approvals each round, resubmit only rejected fixtures |
|
||||
| Exterior Lighting | R0 → R1+ | Similar to interior |
|
||||
| Metal Panels | R0 → R3 | Elevations approved piecemeal |
|
||||
| Millwork | R0 → R3 | R3 triggered reviewer request for meeting |
|
||||
| Signage | R0 → R2 | Dimensional approval before graphic approval |
|
||||
| Fire Suppression | Split tracks | Shop drawings + calcs reviewed separately |
|
||||
|
||||
**Flag trigger:** Submittal is for one of the trades above.
|
||||
|
||||
**Flag language:** *"This trade historically required [N] rounds of review. Plan schedule accordingly. If this submittal is R2 or higher, proactively schedule a coordination meeting before R3."*
|
||||
|
||||
---
|
||||
|
||||
## Pattern 8 — Design Still in Flux (PR Pending)
|
||||
|
||||
**Historical examples:**
|
||||
- Area 4 Stairs guardrails — "We are working on a PR for the guardrails on 2nd floor. Resubmit guardrails"
|
||||
- Media Equipment — "We will issue a PR to pull the remaining scope out of the project"
|
||||
- Cast Stone engraving — "Once we have verbiage on the engraved cast stone we will provide"
|
||||
|
||||
**Flag trigger:** PM indication that a PR is pending, OR the submittal item is known to be in an area of recent/pending design change.
|
||||
|
||||
**Flag language:** *"Confirm no active PR or design change is pending on this item. Submitting against superseded scope creates rework."*
|
||||
|
||||
---
|
||||
|
||||
## Pattern 9 — Duplicate Submittals
|
||||
|
||||
**Historical example:** Structural Steel Area 5 R1 — *"Appears to be a duplicate submittal"*
|
||||
|
||||
**Flag trigger:** Submittal number / title is very similar to an already-logged submittal.
|
||||
|
||||
**Flag language:** *"This submittal number/title closely resembles prior submittal [X]. Confirm this is a new scope, not a duplicate."*
|
||||
|
||||
---
|
||||
|
||||
## Pattern 10 — Color/Finish Confirmation Questions
|
||||
|
||||
**Historical examples:**
|
||||
- Toilet Partitions — "Please confirm the material texture and color"
|
||||
- Exhaust Fan Manual Switch — "Please confirm operation of button"
|
||||
- Grease Interceptor — "Civil engineer is okay with length of interceptor"
|
||||
|
||||
**Flag trigger:** Submittal has a question-style check needed (confirm X, verify Y).
|
||||
|
||||
**Flag language:** *"This item historically required written confirmation of [finish / operation / dimension] rather than a full resubmittal. If architect raises a question, respond in writing — do not prepare a full resubmittal."*
|
||||
|
||||
---
|
||||
|
||||
## Turnaround expectations
|
||||
|
||||
| Submittal type | Typical turnaround | Examples |
|
||||
|---|---|---|
|
||||
| Simple product data | 1–3 days | Anchor bolts, masonry accessories |
|
||||
| Standard shop drawings | 10–15 business days | Most trades |
|
||||
| Complex shop drawings | 3–4 weeks | MEP systems, structural with engineer review |
|
||||
|
||||
**Reviewer routing observed on 0309A:**
|
||||
- Jeff Thomas (505 Architects) — primary reviewer for most trades
|
||||
- Annie Hecksher (505 Architects) — masonry and select others
|
||||
- Some submittals required both (extends timeline)
|
||||
|
||||
Use this to set realistic Final Due Dates and manage sub expectations.
|
||||
|
||||
---
|
||||
|
||||
## Reviewer phrase glossary
|
||||
|
||||
| Phrase | What it actually means |
|
||||
|---|---|
|
||||
| "Resubmit requested information" | Something is missing — read the attached redlines |
|
||||
| "See attached reviewed submittal" | Redlines are on the returned PDF — download and read |
|
||||
| "Approved as Noted" | Conditionally approved — work can proceed only if noted items are addressed |
|
||||
| "Reviewed and Approved" | Clean approval, no action |
|
||||
| "Submit samples for final approval" | Product data ok, physical sample still needed |
|
||||
| "Resubmit only [specific items]" | Do not resubmit the whole package |
|
||||
| "Please confirm…" | A question — respond in writing, not a full resubmittal |
|
||||
| "We are working on a PR…" | Hold the submittal, do not resubmit until PR is issued |
|
||||
|
||||
---
|
||||
|
||||
## What the skill should output when a pattern matches
|
||||
|
||||
For each pattern that matches, add a line to the **Historical Pattern Flags** section of the review memo:
|
||||
|
||||
```
|
||||
Historical Pattern Flags:
|
||||
⚠ [Pattern name] — [Flag language] (severity: warn | info)
|
||||
```
|
||||
|
||||
Severity:
|
||||
- **warn** — historically led to rejection or resubmittal
|
||||
- **info** — historically led to a comment or clarification request but not rejection
|
||||
+1101
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "willowbrook/submittal-review/input",
|
||||
"title": "Submittal Review Input",
|
||||
"description": "Payload the submittal-review skill accepts. Either submittal_pdf_path OR submittal_folder_path must be provided. metadata is optional — if absent, skill extracts what it can from the PDF content.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"submittal_pdf_path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to a single submittal PDF. Use for email-dropped or single-file submittals."
|
||||
},
|
||||
"submittal_folder_path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to a submittal folder (Procore/Fieldwire export style). Skill detects the cover sheet (filename matches folder name) and treats other files as attachments."
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Optional metadata supplied by the caller (Willow, PM, etc.). Overrides anything extracted from the PDF.",
|
||||
"properties": {
|
||||
"project_id": { "type": "string", "description": "Willowbrook project number (e.g. '0309A')" },
|
||||
"submittal_num": { "type": "string", "description": "Submittal number with revision (e.g. '157.1')" },
|
||||
"spec_section": { "type": "string", "description": "CSI section (e.g. '08 71 00 - Door Hardware')" },
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["Shop Drawing", "Product Information", "Sample", "Calculation", "Test Report", "Warranty", "Certificate", "Other"]
|
||||
},
|
||||
"contractor": { "type": "string" },
|
||||
"package": { "type": "string", "description": "Bid package this submittal belongs to" },
|
||||
"lead_time_days": { "type": "integer", "minimum": 0 }
|
||||
}
|
||||
},
|
||||
"project_spec_path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the project spec — either a folder of already-extracted CSI section .txt files (containing _manifest.json), a folder with the raw spec-book PDF (skill will extract and cache into 'Extracted Sections/' on first run), or a direct path to the spec-book PDF. When present, conformance checks cite actual spec paragraphs and 'Approved' status becomes achievable. When absent, the skill falls back to checklist-only review and can only recommend 'Approved as Noted' or lower."
|
||||
},
|
||||
"output_dir": {
|
||||
"type": "string",
|
||||
"description": "Where to write review.json and the Word checklist. Defaults to a 'review' subfolder next to the submittal."
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": ["submittal_pdf_path"] },
|
||||
{ "required": ["submittal_folder_path"] }
|
||||
]
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "willowbrook/submittal-review/output",
|
||||
"title": "Submittal Review Output",
|
||||
"description": "Structured review result. Field names align with Willowbrook's planned submittal database schema so Willow can ingest directly. Every run produces both this JSON and a matching Word checklist.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"submittal_id",
|
||||
"project_id",
|
||||
"overall_status",
|
||||
"conformance_checks",
|
||||
"pattern_flags",
|
||||
"draft_status"
|
||||
],
|
||||
"properties": {
|
||||
"submittal_id": { "type": "string", "description": "e.g. '167.0' — supplied or extracted" },
|
||||
"project_id": { "type": "string", "description": "Willowbrook project number" },
|
||||
"spec_section": { "type": "string" },
|
||||
"csi_division": { "type": "string", "description": "2-digit CSI division, e.g. '08'" },
|
||||
"package_number": { "type": "string", "description": "Bid package" },
|
||||
"product_name": { "type": "string" },
|
||||
"manufacturer": { "type": "string" },
|
||||
"model_number": { "type": "string" },
|
||||
"type": { "type": "string" },
|
||||
"contractor": { "type": "string", "description": "Submitted_by in Willow's schema" },
|
||||
"review_assignee": { "type": "string", "description": "Architect reviewer when known" },
|
||||
"review_due_date": { "type": "string", "format": "date" },
|
||||
"review_completed_date": { "type": "string", "format": "date" },
|
||||
|
||||
"overall_status": {
|
||||
"type": "string",
|
||||
"enum": ["PASS", "CONDITIONAL", "FAIL", "INSUFFICIENT_DATA"],
|
||||
"description": "PASS = ready for architect; CONDITIONAL = can forward with notes; FAIL = return to sub; INSUFFICIENT_DATA = skill could not evaluate with ≥95% confidence on enough items"
|
||||
},
|
||||
"submittal_status_recommended": {
|
||||
"type": "string",
|
||||
"enum": ["Approved", "Approved as Noted", "Revise and Resubmit", "Rejected"],
|
||||
"description": "What the skill recommends the architect response should be. Only 'Approved' if every check has a spec citation."
|
||||
},
|
||||
"draft_status": {
|
||||
"type": "string",
|
||||
"const": "DRAFT",
|
||||
"description": "Every output is always labeled DRAFT. Skill never produces final output."
|
||||
},
|
||||
|
||||
"conformance_checks": {
|
||||
"type": "array",
|
||||
"description": "One entry per checklist item from weston_guide.md applied to this submittal.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["item", "result", "confidence"],
|
||||
"properties": {
|
||||
"item": { "type": "string", "description": "Checklist item text" },
|
||||
"result": {
|
||||
"type": "string",
|
||||
"enum": ["pass", "fail", "warn", "not_applicable", "insufficient_data"]
|
||||
},
|
||||
"detail": { "type": "string", "description": "What was found (or missing)" },
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Skill's confidence in this result. Results below 0.95 are reported as 'Requires PM judgment' in the Word output."
|
||||
},
|
||||
"citation": {
|
||||
"type": "object",
|
||||
"description": "Required for any 'pass' result at PASS overall_status. Without citation, status cannot be Approved.",
|
||||
"properties": {
|
||||
"spec_section": { "type": "string" },
|
||||
"spec_paragraph": { "type": "string", "description": "e.g. '2.1.A.3'" },
|
||||
"page": { "type": "integer" },
|
||||
"excerpt": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"pattern_flags": {
|
||||
"type": "array",
|
||||
"description": "Historical patterns from project_playbook.md that apply to this submittal.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["source", "pattern", "flag", "severity"],
|
||||
"properties": {
|
||||
"source": { "type": "string", "const": "playbook" },
|
||||
"pattern": { "type": "string", "description": "Pattern name, e.g. 'Samples Required Separate from Product Data'" },
|
||||
"flag": { "type": "string", "description": "Language shown to the PM" },
|
||||
"severity": { "type": "string", "enum": ["warn", "info"] }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"deviations": {
|
||||
"type": "array",
|
||||
"description": "Explicit deviations from spec found in the submittal.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": { "type": "string" },
|
||||
"spec_requirement": { "type": "string" },
|
||||
"submitted_value": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"suggested_action": {
|
||||
"type": "string",
|
||||
"description": "Directive PM next step. Short, imperative."
|
||||
},
|
||||
|
||||
"pushback_draft": {
|
||||
"type": "string",
|
||||
"description": "Draft language the PM can copy-paste to the sub if the submittal fails. Empty if overall_status is PASS."
|
||||
},
|
||||
|
||||
"redline_suggestions": {
|
||||
"type": "array",
|
||||
"description": "Specific locations in the submittal the PM should consider marking up.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"page": { "type": "integer" },
|
||||
"item": { "type": "string" },
|
||||
"note": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"attachments": {
|
||||
"type": "array",
|
||||
"description": "Files that were part of the input package.",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
|
||||
"citations": {
|
||||
"type": "array",
|
||||
"description": "All spec paragraphs referenced, deduped across checks.",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
|
||||
"confidence_tier": {
|
||||
"type": "string",
|
||||
"enum": ["green", "yellow", "red"],
|
||||
"description": "green = all checks ≥95% confidence; yellow = some below; red = too many below for meaningful output"
|
||||
},
|
||||
|
||||
"skill_version": { "type": "string", "description": "Version of the submittal-review skill that produced this output" },
|
||||
"run_timestamp": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
}
|
||||
+334
@@ -0,0 +1,334 @@
|
||||
"""
|
||||
build_checklist_docx.py
|
||||
-----------------------
|
||||
Renders the skill's review.json into a Willowbrook-branded Word checklist.
|
||||
|
||||
USAGE
|
||||
python build_checklist_docx.py <review.json> [--out <output.docx>]
|
||||
|
||||
NOTE
|
||||
The skill's SKILL.md instructs Claude to invoke the willowbrook-brand-262
|
||||
skill directly when preparing the final .docx (per Weston's non-negotiable).
|
||||
This script is the mechanical baseline — it produces a correctly structured
|
||||
Word document with all fields populated. If willowbrook-brand-262 is
|
||||
available in the session, Claude should apply its brand standards on top
|
||||
of this output before handing to the PM.
|
||||
|
||||
The script is intentionally self-contained so it works offline too.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import datetime
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, RGBColor, Inches
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
except ImportError:
|
||||
sys.exit("Missing dependency. Run: pip install python-docx")
|
||||
|
||||
|
||||
# ── Willowbrook brand palette (override in brand skill if available) ─────────
|
||||
NAVY = RGBColor(0x1F, 0x3A, 0x5F)
|
||||
GOLD = RGBColor(0xC9, 0xA2, 0x27)
|
||||
GRAY = RGBColor(0x55, 0x55, 0x55)
|
||||
RED = RGBColor(0xB4, 0x1E, 0x1E)
|
||||
GREEN = RGBColor(0x1E, 0x7A, 0x1E)
|
||||
|
||||
CONFIDENCE_THRESHOLD = 0.95
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def add_heading(doc, text, level=1, color=NAVY):
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(text)
|
||||
r.bold = True
|
||||
r.font.color.rgb = color
|
||||
r.font.size = {0: Pt(24), 1: Pt(15), 2: Pt(12)}.get(level, Pt(12))
|
||||
if level == 1:
|
||||
p.paragraph_format.space_before = Pt(14)
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
elif level == 2:
|
||||
p.paragraph_format.space_before = Pt(8)
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
return p
|
||||
|
||||
|
||||
def add_kv_line(doc, key, value):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
rk = p.add_run(f"{key}: ")
|
||||
rk.bold = True
|
||||
rk.font.size = Pt(11)
|
||||
rk.font.color.rgb = GRAY
|
||||
rv = p.add_run(str(value) if value else "—")
|
||||
rv.font.size = Pt(11)
|
||||
return p
|
||||
|
||||
|
||||
def add_check_line(doc, mark, text, color=None):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
rm = p.add_run(mark + " ")
|
||||
rm.font.size = Pt(12)
|
||||
if color:
|
||||
rm.font.color.rgb = color
|
||||
rt = p.add_run(text)
|
||||
rt.font.size = Pt(11)
|
||||
return p
|
||||
|
||||
|
||||
def add_para(doc, text, italic=False, color=None, size=11):
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(text)
|
||||
r.italic = italic
|
||||
r.font.size = Pt(size)
|
||||
if color:
|
||||
r.font.color.rgb = color
|
||||
return p
|
||||
|
||||
|
||||
def add_hrule(doc):
|
||||
p = doc.add_paragraph()
|
||||
pPr = p._p.get_or_add_pPr()
|
||||
pBdr = OxmlElement("w:pBdr")
|
||||
bottom = OxmlElement("w:bottom")
|
||||
bottom.set(qn("w:val"), "single")
|
||||
bottom.set(qn("w:sz"), "6")
|
||||
bottom.set(qn("w:color"), "C9A227")
|
||||
pBdr.append(bottom)
|
||||
pPr.append(pBdr)
|
||||
|
||||
|
||||
# ── Main builder ─────────────────────────────────────────────────────────────
|
||||
|
||||
def build(review: dict, out_path: str) -> None:
|
||||
doc = Document()
|
||||
|
||||
# Default font
|
||||
style = doc.styles["Normal"]
|
||||
style.font.name = "Calibri"
|
||||
style.font.size = Pt(11)
|
||||
|
||||
# ── DRAFT watermark header ────────────────────────────────────────────────
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run("DRAFT — PM REVIEW")
|
||||
r.bold = True
|
||||
r.font.size = Pt(10)
|
||||
r.font.color.rgb = RED
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
|
||||
# Title
|
||||
title = review.get("product_name") or review.get("spec_section") or "Submittal Review"
|
||||
sub_id = review.get("submittal_id", "")
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(f"Submittal Review — {sub_id}")
|
||||
r.bold = True
|
||||
r.font.size = Pt(22)
|
||||
r.font.color.rgb = NAVY
|
||||
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(title)
|
||||
r.italic = True
|
||||
r.font.size = Pt(12)
|
||||
r.font.color.rgb = GRAY
|
||||
|
||||
add_hrule(doc)
|
||||
|
||||
# ── Summary block ─────────────────────────────────────────────────────────
|
||||
status = review.get("overall_status", "INSUFFICIENT_DATA")
|
||||
status_colors = {
|
||||
"PASS": GREEN, "CONDITIONAL": GOLD, "FAIL": RED,
|
||||
"INSUFFICIENT_DATA": GRAY,
|
||||
}
|
||||
p = doc.add_paragraph()
|
||||
rk = p.add_run("Overall Status: ")
|
||||
rk.bold = True
|
||||
rk.font.size = Pt(13)
|
||||
rv = p.add_run(status)
|
||||
rv.bold = True
|
||||
rv.font.size = Pt(13)
|
||||
rv.font.color.rgb = status_colors.get(status, GRAY)
|
||||
|
||||
add_kv_line(doc, "Project", review.get("project_id", ""))
|
||||
add_kv_line(doc, "Spec Section", review.get("spec_section", ""))
|
||||
add_kv_line(doc, "CSI Division", review.get("csi_division", ""))
|
||||
add_kv_line(doc, "Type", review.get("type", ""))
|
||||
add_kv_line(doc, "Contractor", review.get("contractor", ""))
|
||||
add_kv_line(doc, "Submittal Package", review.get("package_number", ""))
|
||||
add_kv_line(doc, "Manufacturer", review.get("manufacturer", ""))
|
||||
add_kv_line(doc, "Model Number", review.get("model_number", ""))
|
||||
add_kv_line(doc, "Confidence Tier", review.get("confidence_tier", "unknown"))
|
||||
add_kv_line(doc, "Recommended Response",
|
||||
review.get("submittal_status_recommended", "—"))
|
||||
|
||||
add_hrule(doc)
|
||||
|
||||
# ── Conformance checks ────────────────────────────────────────────────────
|
||||
add_heading(doc, "Conformance Checks", level=1)
|
||||
add_para(doc,
|
||||
f"Items below confidence threshold ({int(CONFIDENCE_THRESHOLD*100)}%) "
|
||||
f"are marked 'Requires PM judgment'.",
|
||||
italic=True, color=GRAY, size=10)
|
||||
|
||||
checks = review.get("conformance_checks", [])
|
||||
if not checks:
|
||||
add_para(doc, "No conformance checks were run (insufficient data).",
|
||||
italic=True, color=GRAY)
|
||||
else:
|
||||
for c in checks:
|
||||
conf = c.get("confidence", 0)
|
||||
result = c.get("result", "insufficient_data")
|
||||
item = c.get("item", "")
|
||||
detail = c.get("detail", "")
|
||||
citation = c.get("citation") or {}
|
||||
|
||||
if conf < CONFIDENCE_THRESHOLD:
|
||||
mark, color, prefix = "◻", GRAY, "Requires PM judgment — "
|
||||
elif result == "pass":
|
||||
mark, color, prefix = "✓", GREEN, ""
|
||||
elif result == "fail":
|
||||
mark, color, prefix = "✗", RED, ""
|
||||
elif result == "warn":
|
||||
mark, color, prefix = "⚠", GOLD, ""
|
||||
elif result == "not_applicable":
|
||||
mark, color, prefix = "–", GRAY, "N/A — "
|
||||
else:
|
||||
mark, color, prefix = "◻", GRAY, ""
|
||||
|
||||
line = prefix + item
|
||||
if detail:
|
||||
line += f" ({detail})"
|
||||
add_check_line(doc, mark, line, color=color)
|
||||
|
||||
# Citation under the check, if present
|
||||
if citation.get("spec_paragraph") or citation.get("excerpt"):
|
||||
sp = doc.add_paragraph()
|
||||
sp.paragraph_format.left_indent = Inches(0.6)
|
||||
sp.paragraph_format.space_after = Pt(4)
|
||||
sr = sp.add_run("Citation: ")
|
||||
sr.italic = True
|
||||
sr.font.size = Pt(9)
|
||||
sr.font.color.rgb = GRAY
|
||||
cite_text = []
|
||||
if citation.get("spec_section"):
|
||||
cite_text.append(citation["spec_section"])
|
||||
if citation.get("spec_paragraph"):
|
||||
cite_text.append(f"¶ {citation['spec_paragraph']}")
|
||||
if citation.get("page"):
|
||||
cite_text.append(f"p. {citation['page']}")
|
||||
if citation.get("excerpt"):
|
||||
cite_text.append(f"\"{citation['excerpt']}\"")
|
||||
sr2 = sp.add_run(" ".join(cite_text))
|
||||
sr2.italic = True
|
||||
sr2.font.size = Pt(9)
|
||||
sr2.font.color.rgb = GRAY
|
||||
|
||||
add_hrule(doc)
|
||||
|
||||
# ── Pattern flags ─────────────────────────────────────────────────────────
|
||||
add_heading(doc, "Historical Pattern Flags", level=1)
|
||||
flags = review.get("pattern_flags", [])
|
||||
if not flags:
|
||||
add_para(doc, "No historical patterns flagged for this submittal.",
|
||||
italic=True, color=GRAY)
|
||||
else:
|
||||
for f in flags:
|
||||
sev = f.get("severity", "info")
|
||||
mark = "⚠" if sev == "warn" else "ℹ"
|
||||
color = GOLD if sev == "warn" else NAVY
|
||||
text = f"{f.get('pattern', '')} — {f.get('flag', '')}"
|
||||
add_check_line(doc, mark, text, color=color)
|
||||
|
||||
add_hrule(doc)
|
||||
|
||||
# ── Deviations ────────────────────────────────────────────────────────────
|
||||
deviations = review.get("deviations", [])
|
||||
if deviations:
|
||||
add_heading(doc, "Declared Deviations from Spec", level=1)
|
||||
for d in deviations:
|
||||
add_check_line(
|
||||
doc, "→",
|
||||
f"{d.get('description', '')} "
|
||||
f"(spec: {d.get('spec_requirement', '?')}; "
|
||||
f"submitted: {d.get('submitted_value', '?')})",
|
||||
color=NAVY,
|
||||
)
|
||||
add_hrule(doc)
|
||||
|
||||
# ── Suggested action (directive) ──────────────────────────────────────────
|
||||
add_heading(doc, "Suggested PM Action", level=1)
|
||||
add_para(doc, review.get("suggested_action") or
|
||||
"Review items above, confirm completeness, then forward to architect.",
|
||||
size=12)
|
||||
|
||||
# ── Pushback draft ────────────────────────────────────────────────────────
|
||||
pushback = review.get("pushback_draft")
|
||||
if pushback:
|
||||
add_heading(doc, "Draft Pushback Language (to Sub)", level=1)
|
||||
add_para(doc,
|
||||
"DRAFT — review and edit before sending. Skill does not auto-send.",
|
||||
italic=True, color=RED, size=10)
|
||||
add_para(doc, pushback, size=11)
|
||||
|
||||
# ── Redline suggestions ───────────────────────────────────────────────────
|
||||
redlines = review.get("redline_suggestions", [])
|
||||
if redlines:
|
||||
add_heading(doc, "Suggested Redlines", level=1)
|
||||
for r in redlines:
|
||||
page_txt = f"p. {r.get('page')}" if r.get("page") else ""
|
||||
add_check_line(
|
||||
doc, "✎",
|
||||
f"{page_txt} {r.get('item', '')} — {r.get('note', '')}".strip(),
|
||||
color=GOLD,
|
||||
)
|
||||
|
||||
add_hrule(doc)
|
||||
|
||||
# ── Footer ────────────────────────────────────────────────────────────────
|
||||
doc.add_paragraph()
|
||||
ts = review.get("run_timestamp") or datetime.datetime.now().isoformat(timespec="seconds")
|
||||
ver = review.get("skill_version", "v1")
|
||||
ft = doc.add_paragraph()
|
||||
fr = ft.add_run(
|
||||
f"Willowbrook Submittal Review Skill {ver} | Generated {ts} | "
|
||||
f"DRAFT — PM is the publish button."
|
||||
)
|
||||
fr.italic = True
|
||||
fr.font.size = Pt(8)
|
||||
fr.font.color.rgb = GRAY
|
||||
|
||||
os.makedirs(os.path.dirname(os.path.abspath(out_path)), exist_ok=True)
|
||||
doc.save(out_path)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("review_json")
|
||||
ap.add_argument("--out", default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
with open(args.review_json, "r", encoding="utf-8") as f:
|
||||
review = json.load(f)
|
||||
|
||||
out_path = args.out
|
||||
if not out_path:
|
||||
stem = os.path.splitext(os.path.basename(args.review_json))[0]
|
||||
out_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(args.review_json)),
|
||||
f"{stem}.docx",
|
||||
)
|
||||
|
||||
build(review, out_path)
|
||||
print(f"Wrote {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
detect_input.py
|
||||
---------------
|
||||
Inspects a path supplied by the PM and decides whether it's a single PDF
|
||||
or a Procore/Fieldwire-style submittal folder.
|
||||
|
||||
Returns a JSON object Claude can read to drive the rest of the workflow.
|
||||
|
||||
USAGE
|
||||
python detect_input.py "<path>"
|
||||
|
||||
OUTPUT (stdout, single line of JSON)
|
||||
{
|
||||
"kind": "pdf" | "folder" | "invalid",
|
||||
"input_path": "<absolute path>",
|
||||
"cover_sheet": "<absolute path to cover sheet PDF, folder kind only>",
|
||||
"attachments": ["<paths>"],
|
||||
"detected_submittal_num": "157.1" (best-effort from the folder name)
|
||||
}
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
|
||||
|
||||
def detect(path: str) -> dict:
|
||||
path = os.path.abspath(path.strip().strip('"'))
|
||||
result = {"kind": "invalid", "input_path": path}
|
||||
|
||||
if not os.path.exists(path):
|
||||
result["error"] = "Path does not exist"
|
||||
return result
|
||||
|
||||
if os.path.isfile(path) and path.lower().endswith(".pdf"):
|
||||
result["kind"] = "pdf"
|
||||
result["attachments"] = []
|
||||
m = re.match(r"(\d+)[._](\d+)", os.path.basename(path))
|
||||
if m:
|
||||
result["detected_submittal_num"] = f"{m.group(1)}.{m.group(2)}"
|
||||
return result
|
||||
|
||||
if os.path.isdir(path):
|
||||
result["kind"] = "folder"
|
||||
folder_name = os.path.basename(path)
|
||||
|
||||
# Cover sheet: filename (stem) matches folder name
|
||||
cover_sheet = None
|
||||
attachments = []
|
||||
for f in sorted(os.listdir(path)):
|
||||
full = os.path.join(path, f)
|
||||
if not os.path.isfile(full):
|
||||
continue
|
||||
if not f.lower().endswith(".pdf"):
|
||||
continue
|
||||
stem = os.path.splitext(f)[0]
|
||||
if stem == folder_name:
|
||||
cover_sheet = full
|
||||
else:
|
||||
attachments.append(full)
|
||||
|
||||
if cover_sheet is None and attachments:
|
||||
# Fall back to the first PDF if no match found
|
||||
cover_sheet = attachments[0]
|
||||
attachments = attachments[1:]
|
||||
|
||||
result["cover_sheet"] = cover_sheet
|
||||
result["attachments"] = attachments
|
||||
|
||||
m = re.match(r"(\d+)_(\d+)_(.+)", folder_name)
|
||||
if m:
|
||||
result["detected_submittal_num"] = f"{m.group(1)}.{m.group(2)}"
|
||||
|
||||
return result
|
||||
|
||||
result["error"] = "Path is neither a PDF nor a folder"
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit("Usage: python detect_input.py <path>")
|
||||
print(json.dumps(detect(sys.argv[1]), indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
extract_spec_book.py
|
||||
--------------------
|
||||
Splits a construction spec book PDF into one .txt file per CSI section,
|
||||
plus a `_manifest.json` mapping section numbers to files and metadata.
|
||||
|
||||
Non-interactive. Safe to call programmatically from the skill OR run
|
||||
directly from the command line.
|
||||
|
||||
USAGE
|
||||
# As a CLI
|
||||
python extract_spec_book.py <spec_book.pdf> [--out <folder>] [--csv <specifications_log.csv>]
|
||||
|
||||
# From Python
|
||||
from extract_spec_book import extract
|
||||
manifest = extract(pdf_path, output_dir, csv_path=None)
|
||||
|
||||
OUTPUT
|
||||
<output_dir>/
|
||||
00_0001_project-directory.txt
|
||||
01_3300_submittal-procedures.txt
|
||||
03_3000_cast-in-place-concrete.txt
|
||||
...
|
||||
_manifest.json <- { "03 3000": {file, description, pages}, ... }
|
||||
|
||||
Descended from the working Stillwater extractor; interactive prompts removed
|
||||
so Claude and other callers can drive it without stdin.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import csv
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import pypdf
|
||||
except ImportError:
|
||||
sys.exit("Missing dependency. Run: pip install pypdf")
|
||||
|
||||
|
||||
# ── Section number patterns ──────────────────────────────────────────────────
|
||||
|
||||
# "03 3300- 19" / "03 3300 - 19" / "00 10 00-1" at end of a line
|
||||
HEADER_SECTION_RE = re.compile(
|
||||
r"(\d{2}[\s]\d{4}|\d{2}[\s]\d{2}[\s]\d{2})\s*-\s*\d+\s*$"
|
||||
)
|
||||
|
||||
# Fallback: copyright-line section number
|
||||
COPYRIGHT_SECTION_RE = re.compile(
|
||||
r"(\d{2}[\s]?\d{2}[\s]?\d{2})\s*-\s*\d+\s*$"
|
||||
)
|
||||
|
||||
|
||||
def normalize_section_num(raw: str) -> str:
|
||||
return " ".join(raw.split())
|
||||
|
||||
|
||||
def detect_section(lines: list[str]) -> str | None:
|
||||
if lines:
|
||||
m = HEADER_SECTION_RE.search(lines[0])
|
||||
if m:
|
||||
return normalize_section_num(m.group(1))
|
||||
for line in lines[:5]:
|
||||
m = COPYRIGHT_SECTION_RE.search(line)
|
||||
if m:
|
||||
return normalize_section_num(m.group(1))
|
||||
return None
|
||||
|
||||
|
||||
def load_descriptions(csv_path: Path | None) -> dict[str, str]:
|
||||
"""Build a section_number → description map from Specifications Log.csv."""
|
||||
descriptions: dict[str, str] = {}
|
||||
if not csv_path or not csv_path.exists():
|
||||
return descriptions
|
||||
with open(csv_path, newline="", encoding="utf-8-sig") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
num = normalize_section_num(row.get("Number", ""))
|
||||
desc = (row.get("Description", "") or "").strip()
|
||||
if num and desc:
|
||||
descriptions[num] = desc
|
||||
descriptions[num.replace(" ", "")] = desc
|
||||
return descriptions
|
||||
|
||||
|
||||
def safe_filename(section_num: str, description: str) -> str:
|
||||
num_part = section_num.replace(" ", "_")
|
||||
desc_part = re.sub(r"[^\w\s-]", "", description).strip()
|
||||
desc_part = re.sub(r"[\s]+", "-", desc_part).lower()[:60]
|
||||
if desc_part:
|
||||
return f"{num_part}_{desc_part}.txt"
|
||||
return f"{num_part}.txt"
|
||||
|
||||
|
||||
# ── Main extraction ──────────────────────────────────────────────────────────
|
||||
|
||||
def extract(
|
||||
pdf_path: str | Path,
|
||||
output_dir: str | Path,
|
||||
csv_path: str | Path | None = None,
|
||||
verbose: bool = False,
|
||||
) -> dict:
|
||||
"""
|
||||
Extract spec sections from a PDF into one .txt per section.
|
||||
|
||||
Returns the manifest dict: { section_num: {file, description, pages}, ... }
|
||||
Also writes `_manifest.json` into output_dir.
|
||||
"""
|
||||
pdf_path = Path(pdf_path)
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if csv_path is None:
|
||||
# Default: look for Specifications Log.csv next to the PDF
|
||||
candidate = pdf_path.parent / "Specifications Log.csv"
|
||||
if candidate.exists():
|
||||
csv_path = candidate
|
||||
descriptions = load_descriptions(Path(csv_path) if csv_path else None)
|
||||
if verbose:
|
||||
if descriptions:
|
||||
print(f"Loaded {len(descriptions)} section descriptions from CSV.")
|
||||
else:
|
||||
print("No Specifications Log.csv found — files named by section number only.")
|
||||
|
||||
if verbose:
|
||||
print(f"Reading {pdf_path.name}...")
|
||||
reader = pypdf.PdfReader(str(pdf_path))
|
||||
|
||||
sections: dict[str, list[str]] = {}
|
||||
section_order: list[str] = []
|
||||
unknown = 0
|
||||
|
||||
for i, page in enumerate(reader.pages):
|
||||
text = page.extract_text() or ""
|
||||
lines = [l.strip() for l in text.split("\n") if l.strip()]
|
||||
sec = detect_section(lines)
|
||||
if sec is None:
|
||||
unknown += 1
|
||||
sec = "_unknown"
|
||||
if sec not in sections:
|
||||
sections[sec] = []
|
||||
section_order.append(sec)
|
||||
sections[sec].append(text)
|
||||
|
||||
if verbose and unknown:
|
||||
print(f" {unknown} unmatched pages (blank/image) — skipped.")
|
||||
|
||||
manifest: dict[str, dict] = {}
|
||||
for sec in section_order:
|
||||
if sec == "_unknown":
|
||||
continue
|
||||
desc = descriptions.get(sec) or descriptions.get(sec.replace(" ", ""), "")
|
||||
filename = safe_filename(sec, desc)
|
||||
full_text = "\n\n--- PAGE BREAK ---\n\n".join(sections[sec])
|
||||
(output_dir / filename).write_text(full_text, encoding="utf-8")
|
||||
manifest[sec] = {
|
||||
"file": filename,
|
||||
"description": desc,
|
||||
"pages": len(sections[sec]),
|
||||
}
|
||||
|
||||
(output_dir / "_manifest.json").write_text(
|
||||
json.dumps(manifest, indent=2), encoding="utf-8"
|
||||
)
|
||||
if verbose:
|
||||
print(f"Wrote {len(manifest)} sections + manifest to {output_dir}")
|
||||
return manifest
|
||||
|
||||
|
||||
# ── CLI ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Extract CSI sections from a spec book PDF."
|
||||
)
|
||||
ap.add_argument("pdf", help="Path to the spec book PDF")
|
||||
ap.add_argument("--out", default=None,
|
||||
help="Output folder (default: 'Extracted Sections' next to the PDF)")
|
||||
ap.add_argument("--csv", default=None,
|
||||
help="Path to Specifications Log.csv (default: look next to PDF)")
|
||||
ap.add_argument("--quiet", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
pdf_path = Path(args.pdf).expanduser()
|
||||
if not pdf_path.exists():
|
||||
sys.exit(f"Not found: {pdf_path}")
|
||||
|
||||
output_dir = Path(args.out) if args.out else pdf_path.parent / "Extracted Sections"
|
||||
csv_path = Path(args.csv) if args.csv else None
|
||||
|
||||
extract(pdf_path, output_dir, csv_path=csv_path, verbose=not args.quiet)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
parse_submittal.py
|
||||
------------------
|
||||
Extracts text and best-effort structured metadata from a submittal PDF or folder.
|
||||
|
||||
USAGE
|
||||
python parse_submittal.py <path_to_pdf_or_folder> [--out <output_json>]
|
||||
|
||||
OUTPUT
|
||||
JSON written to stdout (or --out file) with:
|
||||
- kind: "pdf" | "folder"
|
||||
- cover_sheet_text: full extracted text of the cover sheet
|
||||
- attachment_texts: dict of {filename: full_extracted_text}
|
||||
- extracted_metadata: {submittal_num, revision, spec_section, type,
|
||||
response, reviewer, comments[], contractor,
|
||||
package, date_created, date_returned}
|
||||
|
||||
Depends on PyMuPDF (`pip install pymupdf`).
|
||||
Uses the same regex heuristics as extract_cover_sheets.py so it handles
|
||||
both Procore layout variants.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
|
||||
try:
|
||||
import fitz # PyMuPDF
|
||||
except ImportError:
|
||||
sys.exit("Missing dependency. Run: pip install pymupdf")
|
||||
|
||||
# Allow importing detect_input.py from the same folder
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from detect_input import detect # noqa: E402
|
||||
|
||||
|
||||
# ── PDF text extraction ──────────────────────────────────────────────────────
|
||||
|
||||
def extract_pdf_text(pdf_path: str) -> str:
|
||||
doc = fitz.open(pdf_path)
|
||||
pages = []
|
||||
for i, page in enumerate(doc, start=1):
|
||||
text = page.get_text() or ""
|
||||
pages.append(f"--- Page {i} ---\n{text}")
|
||||
doc.close()
|
||||
return "\n\n".join(pages)
|
||||
|
||||
|
||||
# ── Cover-sheet parsing (handles both Procore layouts) ───────────────────────
|
||||
|
||||
def _extract_response(text: str) -> str:
|
||||
for p in ["Revise and Resubmit", "Reviewed As Noted", "Reviewed"]:
|
||||
if p in text:
|
||||
return p
|
||||
return ""
|
||||
|
||||
|
||||
def _extract_reviewer(text: str) -> str:
|
||||
m = re.search(
|
||||
r"([\w][\w\s]+?)\s*\n\s*\(505 Architects\)\s*\n\s*"
|
||||
r"(Reviewed As Noted|Revise and Resubmit|Reviewed)", text)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
m = re.search(
|
||||
r"([\w][\w ]+?)\s*\(505 Architects\)\s+"
|
||||
r"(Reviewed As Noted|Revise and Resubmit|Reviewed)", text)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
m = re.search(r"Approvers[^\n]*\n?\s*(.+?)\s*\(505 Architects\)", text)
|
||||
if m:
|
||||
return m.group(1).strip().split(",")[-1].strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _extract_comments(text: str) -> list:
|
||||
comments = re.findall(r"^\s*Comment\s*\n\s*(.+?)[ \t]*\n", text, re.MULTILINE)
|
||||
comments += re.findall(r"^\s*Comment\s*\n([\s\S]+?)(?:\n\s*\n|\nPage \d|$)",
|
||||
text, re.MULTILINE)
|
||||
seen, out = set(), []
|
||||
for c in comments:
|
||||
c = c.strip()
|
||||
if c and c not in seen:
|
||||
seen.add(c)
|
||||
out.append(c)
|
||||
return out
|
||||
|
||||
|
||||
def _extract_spec_section(text: str) -> str:
|
||||
# Layout B
|
||||
m = re.search(r"^\s*Spec Section\s*\n\s*(.+?)[ \t]*\n", text, re.MULTILINE)
|
||||
if m and re.search(r"\d{2}\s*\d{4}", m.group(1)):
|
||||
return m.group(1).strip()
|
||||
# Layout A
|
||||
m = re.search(r"Spec Section\s+(.+?)(?:\s{2,}|\t|\n|$)", text)
|
||||
if m and re.search(r"\d{2}\s*\d{4}", m.group(1)):
|
||||
return m.group(1).strip()
|
||||
# Subtitle fallback
|
||||
m = re.search(r"Submittal #[\d.]+ - .+?\n\s*(.+?)\s*\n", text)
|
||||
if m and re.search(r"\d{2}\s*\d{4}", m.group(1)):
|
||||
return m.group(1).strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _extract_type(text: str) -> str:
|
||||
m = re.search(r"\nType\s*\n\s*(.+?)[ \t]*\n", text)
|
||||
if m and m.group(1).strip() in (
|
||||
"Shop Drawing", "Product Information", "Sample", "Calculation",
|
||||
"Test Report", "Warranty", "Certificate", "Other",
|
||||
):
|
||||
return m.group(1).strip()
|
||||
m = re.search(r"Location\s+Type\s+(.+?)(?:\s{2,}|$)", text, re.MULTILINE)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
m = re.search(r"\bType\s+(Shop Drawing|Product Information|Sample|"
|
||||
r"Calculation|Test Report|Warranty|Certificate|Other)\b", text)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
def _extract_contractor(text: str) -> str:
|
||||
m = re.search(r"Responsible\s*\nContractor\s*\n\s*(.+?)[ \t]*\n", text)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
m = re.search(r"Responsible\s+(.+?)(?:\s{2,}|Received From|\n)", text)
|
||||
if m:
|
||||
val = m.group(1).strip()
|
||||
if val and "Contractor" not in val:
|
||||
return val
|
||||
return ""
|
||||
|
||||
|
||||
def _extract_package(text: str) -> str:
|
||||
m = re.search(r"Submittal Package\s*\n\s*(.+?)[ \t]*\n", text)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
m = re.search(r"Submittal Package\s+(.+?)(?:\s{2,}|\n|$)", text, re.MULTILINE)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _extract_date_created(text: str) -> str:
|
||||
m = re.search(r"Date Created\s*\n\s*(.+?)[ \t]*\n", text)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
m = re.search(r"Date Created\s+(\w+ \d+, \d{4})", text)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _extract_date_returned(text: str) -> str:
|
||||
m = re.search(
|
||||
r"(\w+ \d+, \d{4})\s*\n\s*(Reviewed As Noted|Revise and Resubmit|Reviewed)\b",
|
||||
text)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
m = re.search(
|
||||
r"(\w+ \d+, \d{4})\s+(Reviewed As Noted|Revise and Resubmit|Reviewed)\b",
|
||||
text)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _extract_submittal_num(text: str, fallback: str = "") -> tuple:
|
||||
m = re.search(r"Submittal #(\d+)\.(\d+)", text)
|
||||
if m:
|
||||
return f"{m.group(1)}.{m.group(2)}", int(m.group(2))
|
||||
if fallback:
|
||||
m = re.match(r"(\d+)\.(\d+)", fallback)
|
||||
if m:
|
||||
return fallback, int(m.group(2))
|
||||
return "", 0
|
||||
|
||||
|
||||
def parse_cover_sheet_text(text: str, submittal_num_hint: str = "") -> dict:
|
||||
"""Return structured metadata extracted from cover-sheet text."""
|
||||
sub_num, rev = _extract_submittal_num(text, submittal_num_hint)
|
||||
return {
|
||||
"submittal_num": sub_num,
|
||||
"revision": rev,
|
||||
"spec_section": _extract_spec_section(text),
|
||||
"type": _extract_type(text),
|
||||
"response": _extract_response(text),
|
||||
"reviewer": _extract_reviewer(text),
|
||||
"comments": _extract_comments(text),
|
||||
"contractor": _extract_contractor(text),
|
||||
"package": _extract_package(text),
|
||||
"date_created": _extract_date_created(text),
|
||||
"date_returned": _extract_date_returned(text),
|
||||
}
|
||||
|
||||
|
||||
# ── Main entry ───────────────────────────────────────────────────────────────
|
||||
|
||||
def parse(path: str) -> dict:
|
||||
info = detect(path)
|
||||
if info["kind"] == "invalid":
|
||||
return {"error": info.get("error", "Invalid input"), "detect": info}
|
||||
|
||||
result = {
|
||||
"kind": info["kind"],
|
||||
"input_path": info["input_path"],
|
||||
"cover_sheet_text": "",
|
||||
"attachment_texts": {},
|
||||
"extracted_metadata": {},
|
||||
}
|
||||
|
||||
if info["kind"] == "pdf":
|
||||
text = extract_pdf_text(info["input_path"])
|
||||
result["cover_sheet_text"] = text
|
||||
result["extracted_metadata"] = parse_cover_sheet_text(
|
||||
text, info.get("detected_submittal_num", ""))
|
||||
return result
|
||||
|
||||
# Folder
|
||||
cover = info.get("cover_sheet")
|
||||
if cover:
|
||||
text = extract_pdf_text(cover)
|
||||
result["cover_sheet_text"] = text
|
||||
result["extracted_metadata"] = parse_cover_sheet_text(
|
||||
text, info.get("detected_submittal_num", ""))
|
||||
|
||||
for att_path in info.get("attachments", []):
|
||||
try:
|
||||
result["attachment_texts"][os.path.basename(att_path)] = \
|
||||
extract_pdf_text(att_path)
|
||||
except Exception as e:
|
||||
result["attachment_texts"][os.path.basename(att_path)] = \
|
||||
f"[extraction failed: {e}]"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("path")
|
||||
ap.add_argument("--out", default=None, help="Write JSON to this file instead of stdout")
|
||||
args = ap.parse_args()
|
||||
|
||||
data = parse(args.path)
|
||||
text = json.dumps(data, indent=2, ensure_ascii=False)
|
||||
if args.out:
|
||||
with open(args.out, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
print(f"Wrote {args.out}")
|
||||
else:
|
||||
print(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
spec_lookup.py
|
||||
--------------
|
||||
Given a project_spec_path (folder of extracted sections OR a raw spec-book PDF),
|
||||
looks up the text of a specific CSI section for use by the submittal-review
|
||||
skill.
|
||||
|
||||
If given a PDF, transparently extracts it to '<PDF dir>/Extracted Sections/'
|
||||
the first time, then reuses the cached output on subsequent runs.
|
||||
|
||||
USAGE
|
||||
# CLI
|
||||
python spec_lookup.py <project_spec_path> <csi_section>
|
||||
# e.g. python spec_lookup.py ".../Spec Book" "10 22 39"
|
||||
|
||||
# Python
|
||||
from spec_lookup import resolve_spec_section
|
||||
section = resolve_spec_section(project_spec_path, "10 22 39")
|
||||
section["text"] # full spec text
|
||||
section["file"] # absolute path to the .txt
|
||||
section["description"] # from the CSV / manifest
|
||||
section["page_breaks"] # list of indices where "--- PAGE BREAK ---" occurs
|
||||
|
||||
Normalizes section numbers so "10 22 39", "10 2239", "102239", and
|
||||
"10 22 39 - Folding Panel Partitions" all resolve to the same section.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# Allow relative import when run from the skill's scripts folder
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from extract_spec_book import extract # noqa: E402
|
||||
|
||||
|
||||
# ── Normalization ────────────────────────────────────────────────────────────
|
||||
|
||||
def canonical_section(raw: str) -> str:
|
||||
"""
|
||||
Normalize a user-supplied section ref to a canonical key.
|
||||
'Spec Section: 10 22 39 - Folding Panel Partitions' -> '10 22 39'
|
||||
'10 2239' -> '10 22 39'
|
||||
'102239' -> '10 22 39'
|
||||
'08 7100' -> '08 71 00'
|
||||
"""
|
||||
# Pull out the first digit-and-space pattern
|
||||
m = re.search(r"(\d{2})[\s.]?(\d{2})[\s.]?(\d{2})", raw)
|
||||
if m:
|
||||
return f"{m.group(1)} {m.group(2)} {m.group(3)}"
|
||||
# Fallback: 6-digit run
|
||||
m = re.search(r"(\d{2})\s?(\d{4})", raw)
|
||||
if m:
|
||||
four = m.group(2)
|
||||
return f"{m.group(1)} {four[:2]} {four[2:]}"
|
||||
return raw.strip()
|
||||
|
||||
|
||||
def canonical_keys(raw: str) -> list[str]:
|
||||
"""Possible canonical forms a manifest could key by."""
|
||||
c = canonical_section(raw)
|
||||
parts = c.split()
|
||||
out = {c}
|
||||
if len(parts) == 3:
|
||||
out.add(f"{parts[0]} {parts[1]}{parts[2]}") # '10 2239'
|
||||
out.add(f"{parts[0]}{parts[1]}{parts[2]}") # '102239'
|
||||
return list(out)
|
||||
|
||||
|
||||
# ── Path resolution ──────────────────────────────────────────────────────────
|
||||
|
||||
def _resolve_extracted_folder(project_spec_path: Path) -> Path:
|
||||
"""
|
||||
Given either a folder of extracted sections or a raw spec PDF,
|
||||
return the folder that has (or will have) the extracted .txt files
|
||||
and _manifest.json.
|
||||
"""
|
||||
if project_spec_path.is_dir():
|
||||
# Could be either the "Spec Book" folder (with PDF + Extracted Sections/)
|
||||
# or the "Extracted Sections" folder itself.
|
||||
manifest_here = project_spec_path / "_manifest.json"
|
||||
if manifest_here.exists():
|
||||
return project_spec_path
|
||||
nested = project_spec_path / "Extracted Sections"
|
||||
if (nested / "_manifest.json").exists():
|
||||
return nested
|
||||
# Look for any PDF to extract from
|
||||
pdfs = list(project_spec_path.glob("*.pdf"))
|
||||
if pdfs:
|
||||
extracted = project_spec_path / "Extracted Sections"
|
||||
extract(pdfs[0], extracted, verbose=False)
|
||||
return extracted
|
||||
# Empty directory — force caller to re-point
|
||||
raise FileNotFoundError(
|
||||
f"No _manifest.json and no PDF found in {project_spec_path}"
|
||||
)
|
||||
|
||||
if project_spec_path.is_file() and project_spec_path.suffix.lower() == ".pdf":
|
||||
extracted = project_spec_path.parent / "Extracted Sections"
|
||||
if not (extracted / "_manifest.json").exists():
|
||||
extract(project_spec_path, extracted, verbose=False)
|
||||
return extracted
|
||||
|
||||
raise FileNotFoundError(f"Not a spec folder or PDF: {project_spec_path}")
|
||||
|
||||
|
||||
# ── Main API ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def resolve_spec_section(project_spec_path: str | Path, section: str) -> dict | None:
|
||||
"""
|
||||
Return dict with {section, description, file, text, page_breaks}
|
||||
or None if the section is not in the spec book.
|
||||
"""
|
||||
project_spec_path = Path(project_spec_path).expanduser()
|
||||
extracted = _resolve_extracted_folder(project_spec_path)
|
||||
manifest_path = extracted / "_manifest.json"
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
|
||||
keys = canonical_keys(section)
|
||||
entry = None
|
||||
matched_key = None
|
||||
for k in keys:
|
||||
if k in manifest:
|
||||
entry = manifest[k]
|
||||
matched_key = k
|
||||
break
|
||||
|
||||
if entry is None:
|
||||
# Try loose match: any manifest key whose canonical form equals ours
|
||||
our_canon = canonical_section(section)
|
||||
for mk, me in manifest.items():
|
||||
if canonical_section(mk) == our_canon:
|
||||
entry = me
|
||||
matched_key = mk
|
||||
break
|
||||
|
||||
if entry is None:
|
||||
return None
|
||||
|
||||
text_path = extracted / entry["file"]
|
||||
text = text_path.read_text(encoding="utf-8") if text_path.exists() else ""
|
||||
# Indices of page-break markers — handy for page citations later
|
||||
pb_indices = [i for i, line in enumerate(text.splitlines())
|
||||
if line.strip() == "--- PAGE BREAK ---"]
|
||||
|
||||
return {
|
||||
"section": matched_key,
|
||||
"description": entry.get("description", ""),
|
||||
"file": str(text_path),
|
||||
"pages": entry.get("pages", 0),
|
||||
"text": text,
|
||||
"page_breaks": pb_indices,
|
||||
}
|
||||
|
||||
|
||||
def list_sections(project_spec_path: str | Path) -> dict:
|
||||
"""Return the full manifest — caller can browse available sections."""
|
||||
project_spec_path = Path(project_spec_path).expanduser()
|
||||
extracted = _resolve_extracted_folder(project_spec_path)
|
||||
return json.loads((extracted / "_manifest.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
# ── CLI ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("project_spec_path",
|
||||
help="Folder with extracted sections, or a spec-book PDF")
|
||||
ap.add_argument("section",
|
||||
help="CSI section — e.g. '10 22 39' or '10 2239'")
|
||||
ap.add_argument("--text-only", action="store_true",
|
||||
help="Print only the spec text")
|
||||
args = ap.parse_args()
|
||||
|
||||
result = resolve_spec_section(args.project_spec_path, args.section)
|
||||
if result is None:
|
||||
sys.exit(f"Section '{args.section}' not found in spec book.")
|
||||
|
||||
if args.text_only:
|
||||
print(result["text"])
|
||||
return
|
||||
|
||||
print(json.dumps({
|
||||
"section": result["section"],
|
||||
"description": result["description"],
|
||||
"file": result["file"],
|
||||
"pages": result["pages"],
|
||||
"page_break_count": len(result["page_breaks"]),
|
||||
"text_length": len(result["text"]),
|
||||
}, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+539
@@ -0,0 +1,539 @@
|
||||
=== 10 2239 Operable Partitions R1 - SD.pdf ===
|
||||
--- Page 1 ---
|
||||
Submittal Cover Sheet
|
||||
* Submials not submied as outlined below will not be reviewed but marked as revise and resubmit. Submials must be submied with this
|
||||
cover sheet, per spec, and with types/models outlined in red. Submials are to be divided as separate files or as a single file with each submial
|
||||
type and spec secon having its own corresponding cover sheet. Submials for each spec secon to be separated and idenfied as Product
|
||||
Data/Other Info, Licenses, Shop Drawings, and Samples.
|
||||
Project:
|
||||
__________________________________
|
||||
Company:
|
||||
__________________________________
|
||||
Date:
|
||||
__________________________________
|
||||
Submial/Spec Title:
|
||||
__________________________________________________________________
|
||||
Submial Revision #:
|
||||
__________________________________________________________________
|
||||
Spec Secon # (00 0000.00): __________________________________________________________________
|
||||
Subcontractor Comments: __________________________________________________________________
|
||||
__________________________________________________________________________________________
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Contractor
|
||||
Comments
|
||||
Architect
|
||||
Comments
|
||||
Engineer
|
||||
Comments
|
||||
Contractor
|
||||
Stamp
|
||||
Architect
|
||||
Stamp
|
||||
Engineer
|
||||
Stamp
|
||||
Submittal Review
|
||||
This submittal has been reviewed for general compliance with the
|
||||
construction documents. The submittal review process does not
|
||||
relieve the subcontractor of the responsibility to verify and provide
|
||||
the correct: quantities, dimensions, details, layouts, and incidentals
|
||||
per the construction documents and onsite project conditions.
|
||||
Reviewed By: Zac Hansen Date: 11/6/2024
|
||||
REVIEWED
|
||||
REVIEWED AS NOTED
|
||||
REVISE & RESUBMIT
|
||||
REJECTED
|
||||
11/07/2024
|
||||
Jeff Thomas
|
||||
505 Architects LLC
|
||||
X
|
||||
SHS
|
||||
The Best Companies
|
||||
11/6/2024
|
||||
Operable Partitions R1 - SD
|
||||
1
|
||||
10 2239
|
||||
|
||||
|
||||
--- Page 2 ---
|
||||
ELA Classrooms
|
||||
2183/2195
|
||||
REVISED
|
||||
REVISED
|
||||
9:10 AM, November 6, 2024
|
||||
9:10 AM, November 6, 2024
|
||||
4'-0" PKT BEING
|
||||
PROVIDED. RE:
|
||||
A145 PR#05
|
||||
5'-7 1/4"
|
||||
PROVIDE SMOKE GREY
|
||||
FOR TRIM AND HINGE
|
||||
|
||||
|
||||
--- Page 3 ---
|
||||
|
||||
|
||||
--- Page 4 ---
|
||||
|
||||
|
||||
--- Page 5 ---
|
||||
10'-0" FINISHED
|
||||
CEILING HEIGHT
|
||||
|
||||
|
||||
--- Page 6 ---
|
||||
|
||||
|
||||
--- Page 7 ---
|
||||
10'-0" FINISHED
|
||||
CEILING HEIGHT
|
||||
5'-7 1/4"
|
||||
|
||||
|
||||
--- Page 8 ---
|
||||
ELA Classrooms
|
||||
2197/2199
|
||||
REVISED
|
||||
REVISED
|
||||
9:10 AM, November 6, 2024
|
||||
9:10 AM, November 6, 2024
|
||||
4'-0" PKT BEING
|
||||
PROVIDED. RE:
|
||||
A145 PR#05
|
||||
5'-7 1/4"
|
||||
PROVIDE SMOKE GREY
|
||||
FOR TRIM AND HINGE
|
||||
|
||||
|
||||
--- Page 9 ---
|
||||
|
||||
|
||||
--- Page 10 ---
|
||||
|
||||
|
||||
--- Page 11 ---
|
||||
10'-0" FINISHED
|
||||
CEILING HEIGHT
|
||||
|
||||
|
||||
--- Page 12 ---
|
||||
|
||||
|
||||
--- Page 13 ---
|
||||
10'-0" FINISHED
|
||||
CEILING HEIGHT
|
||||
5'-7 1/4"
|
||||
|
||||
|
||||
--- Page 14 ---
|
||||
S
|
||||
h
|
||||
e
|
||||
r
|
||||
w
|
||||
i
|
||||
n
|
||||
|
||||
W
|
||||
il
|
||||
li
|
||||
a
|
||||
m
|
||||
s
|
||||
|
||||
W
|
||||
h
|
||||
it
|
||||
e
|
||||
S
|
||||
h
|
||||
e
|
||||
r
|
||||
w
|
||||
i
|
||||
n
|
||||
|
||||
W
|
||||
il
|
||||
l
|
||||
i
|
||||
a
|
||||
m
|
||||
s
|
||||
|
||||
S
|
||||
m
|
||||
o
|
||||
k
|
||||
e
|
||||
|
||||
G
|
||||
r
|
||||
a
|
||||
y
|
||||
S
|
||||
h
|
||||
e
|
||||
r
|
||||
w
|
||||
i
|
||||
n
|
||||
|
||||
W
|
||||
il
|
||||
l
|
||||
i
|
||||
a
|
||||
m
|
||||
s
|
||||
|
||||
N
|
||||
a
|
||||
t
|
||||
u
|
||||
r
|
||||
a
|
||||
l
|
||||
|
||||
C
|
||||
h
|
||||
o
|
||||
i
|
||||
c
|
||||
e
|
||||
S
|
||||
h
|
||||
e
|
||||
r
|
||||
w
|
||||
i
|
||||
n
|
||||
|
||||
W
|
||||
i
|
||||
l
|
||||
li
|
||||
a
|
||||
m
|
||||
s
|
||||
|
||||
D
|
||||
a
|
||||
r
|
||||
k
|
||||
|
||||
B
|
||||
r
|
||||
o
|
||||
n
|
||||
z
|
||||
e
|
||||
S
|
||||
h
|
||||
e
|
||||
r
|
||||
w
|
||||
i
|
||||
n
|
||||
|
||||
W
|
||||
i
|
||||
l
|
||||
li
|
||||
a
|
||||
m
|
||||
s
|
||||
|
||||
B
|
||||
l
|
||||
a
|
||||
c
|
||||
k
|
||||
TRIM COLOR OPTIONS
|
||||
Smoke Gray, Natural Choice, Dark Bronze, or Black
|
||||
Selection impacts sweeps, seals, pull handles,
|
||||
eraser trays, hinges, and worksurface trim.
|
||||
Selection will be uniform throughout the wall.
|
||||
HINGE COLOR OPTIONS
|
||||
White, Smoke Gray, Natural Choice, Dark Bronze, or Black
|
||||
Selection will be uniform throughout the wall.
|
||||
Form No. 2240 5/23
|
||||
800-869-9685
|
||||
TRIM & HINGE
|
||||
COLOR SELECTOR
|
||||
info@modernfold.com
|
||||
modernfold.com
|
||||
PROVIDE SMOKE GREY
|
||||
FOR TRIM AND HINGE
|
||||
|
||||
|
||||
--- Page 15 ---
|
||||
Adrift
|
||||
512 (S) 548 (H)
|
||||
Oats
|
||||
518 (S) 554 (H)
|
||||
Bobbin Weave
|
||||
524 (S) 560 (H)
|
||||
Reed
|
||||
513 (S) 549 (H)
|
||||
Prairie
|
||||
519 (S) 555 (H)
|
||||
Threads
|
||||
525 (S) 561 (H)
|
||||
Sandalwood
|
||||
514 (S) 550 (H)
|
||||
Reed
|
||||
520 (S) 556 (H)
|
||||
Common Thread
|
||||
526 (S) 562 (H)
|
||||
Slate
|
||||
516 (S) 552 (H)
|
||||
Feather Grass
|
||||
522 (S) 558 (H)
|
||||
Lustre
|
||||
528 (S) 564 (H)
|
||||
Lotus
|
||||
511 (S) 547 (H)
|
||||
Pumila Grass
|
||||
517 (S) 553 (H)
|
||||
Canvas
|
||||
523 (S) 559 (H)
|
||||
Veranda
|
||||
515 (S) 551 (H)
|
||||
Red Oat
|
||||
521 (S) 557 (H)
|
||||
Quill
|
||||
527 (S) 563 (H)
|
||||
White
|
||||
529 (S) 565 (H)
|
||||
Arctic
|
||||
535 (S) 571 (H)
|
||||
B. White
|
||||
541 (S) 577 (H)
|
||||
Silver Dust
|
||||
530 (S) 566 (H)
|
||||
Serenity
|
||||
536 (S) 572 (H)
|
||||
Eggshell
|
||||
542 (S) 578 (H)
|
||||
Cirrus
|
||||
531 (S) 567 (H)
|
||||
Grey Pearl
|
||||
537 (S) 573 (H)
|
||||
Frost Taupe
|
||||
543 (S) 579 (H)
|
||||
Camel Down
|
||||
532 (S) 568 (H)
|
||||
Willet
|
||||
538 (S) 574 (H)
|
||||
Aspen
|
||||
544 (S) 580 (H)
|
||||
Cimmerin
|
||||
533 (S) 569 (H)
|
||||
Windrift
|
||||
539 (S) 575 (H)
|
||||
Denver
|
||||
545 (S) 581 (H)
|
||||
Carbon
|
||||
534 (S) 570 (H)
|
||||
Hemp
|
||||
540 (S) 576 (H)
|
||||
Black
|
||||
546 (S) 582 (H)
|
||||
Standard Len-Tex® Vinyl Offering
|
||||
Standard Len-Tex® Vinyl Offering
|
||||
Displayed samples feature a standard weight 20-oz. (S). Also available in a heavy-duty 30-oz. construction (H).
|
||||
Note: Each digital swatch is a digital color presentation of a standard color and is subject to minor shade variations.
|
||||
Lennon Grass
|
||||
Arani Silk
|
||||
Loominous
|
||||
Soraya
|
||||
Coronado
|
||||
Emboss LT Suede
|
||||
|
||||
|
||||
--- Page 16 ---
|
||||
VINYL SELECTOR
|
||||
Len-Tex® Vinyl Wallcovering Finish Specifi cations
|
||||
Form: 2243 1/22
|
||||
800-869-9685 | info@modernfold.com | www.modernfold.com
|
||||
215 West New Rd. | Greenfi eld, IN 46140
|
||||
Federal Specifi cations:
|
||||
Len-Tex Clean Vinyl Wallcovering meets or exceeds all requirements of W-101 (20 oz), W-101, Type III (30 oz.) Quality
|
||||
Standard for Polymer Coated Fabric Wallcovering and EN 15102, European Standard for Decorative Wallcoverings. This
|
||||
product has been accepted for use by the City of New York Dept. of Buildings under MEA-381-93M.
|
||||
Fire Hazard Classifi cation Results:
|
||||
ASTM E-84 Tunnel Test: Class A, Flame Spread: 15; Smoke Developed: 10 (20 oz.)
|
||||
ASTM E-84 Tunnel Test: Class A, Flame Spread: 20; Smoke Developed: 65 (30 oz.)
|
||||
PASS rating as tested under NFPA 286 (Corner burn test)
|
||||
CAN/ ULC S102-10 Fire Test: Class A, Flame Spread: 15; Smoke Developed: 75
|
||||
EN 13501-1 Fire Classifi cation: B-s2, d0
|
||||
Performance Features:
|
||||
Len-Tex Wallcoverings use Clean Vinyl Technology™
|
||||
Phthalate-free
|
||||
Alumina trihydrate fi re retardant (antimony-free)
|
||||
Ultra-Fresh® antimicrobial (non-arsenate)
|
||||
No heavy metals or formaldehyde
|
||||
Printed exclusively with water-based, low VOC AQUA-CLEAR™ inks
|
||||
Roller applied AQUA-CLEAR 3.0™ top fi nish for improved stain resistance
|
||||
Microventing for permeability is available on a custom order basis
|
||||
Advanced Warning Eff ect (ionization-type smoke detector)
|
||||
Five year warranty against materials defects – Consult your distributor for details
|
||||
Environmental Attributes:
|
||||
Ultra-low emitting – Pass rating under CA 01350
|
||||
Listed by ZeroDocs and Sustainable Minds® for California CHPS (Collaborative for High Performance Schools) SCS Indoor
|
||||
Advantage™ Gold certifi ed
|
||||
CA Prop 65 compliant
|
||||
Published Health Product Declaration (HPD) – LEED eligible
|
||||
Published Len-Tex Environmental Product Declaration (EPD) – LEED eligible
|
||||
GreenCircle Certifi ed™
|
||||
Find Len-Tex products listed on:
|
||||
Ecomedes (ecomedes.com), SCS Global Services website (scsglobalservices.com), GreenCircle Certifi ed website
|
||||
(greencirclecertifi ed.com), Mortarr (mortarr.com)
|
||||
Permeability, Moisture and Mold:
|
||||
Vinyl wallcovering can act as a vapor barrier and consequently should NOT be installed on walls that are susceptible
|
||||
to excessive condensation or moisture infi ltration. To reduce the risk of fungal growth, Len-Tex recommends that
|
||||
all wallcovering(s) be microvented when installed in Humid and Fringe Zone 1 climates.(ref: ASHRAE Handbook of
|
||||
Fundamentals, Chapter 21, Figure 16). Please see full details on microventing at our website, lentexwallcoverings.com.
|
||||
LEN-TEX®
|
||||
|
||||
|
||||
U.S. Units
|
||||
Metric Units
|
||||
Total Weight:
|
||||
20.0 oz./LY 612 G/LM
|
||||
13.2 oz./SY
|
||||
442 G/SM
|
||||
Fabric Weight:: 2.5 oz./LY
|
||||
85 G/LM
|
||||
Vinyl Weight: 17.5 oz./LY
|
||||
520 G/LM
|
||||
Material Width: 53/54” 134/137 cm
|
||||
Fabric Type:
|
||||
Poly Cotton Osnaburg
|
||||
|
||||
|
||||
U.S. Units
|
||||
Metric Units
|
||||
Total Weight:
|
||||
30.0 oz./LY 925 G/LM
|
||||
20.0 oz./SY
|
||||
680 G/SM
|
||||
Fabric Weight:: 5.5 oz./LY
|
||||
170 G/LM
|
||||
Vinyl Weight: 24.5 oz./LY
|
||||
756 G/LM
|
||||
Material Width: 53/54” 134/137 cm
|
||||
Fabric Type:
|
||||
Poly Cotton Drill
|
||||
|
||||
|
||||
=== 10 2239 Operable Partitions R1 - SD_1.pdf ===
|
||||
--- Page 1 ---
|
||||
Submittal Cover Sheet
|
||||
* Submials not submied as outlined below will not be reviewed but marked as revise and resubmit. Submials must be submied with this
|
||||
cover sheet, per spec, and with types/models outlined in red. Submials are to be divided as separate files or as a single file with each submial
|
||||
type and spec secon having its own corresponding cover sheet. Submials for each spec secon to be separated and idenfied as Product
|
||||
Data/Other Info, Licenses, Shop Drawings, and Samples.
|
||||
Project:
|
||||
__________________________________
|
||||
Company:
|
||||
__________________________________
|
||||
Date:
|
||||
__________________________________
|
||||
Submial/Spec Title:
|
||||
__________________________________________________________________
|
||||
Submial Revision #:
|
||||
__________________________________________________________________
|
||||
Spec Secon # (00 0000.00): __________________________________________________________________
|
||||
Subcontractor Comments: __________________________________________________________________
|
||||
__________________________________________________________________________________________
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Contractor
|
||||
Comments
|
||||
Architect
|
||||
Comments
|
||||
Engineer
|
||||
Comments
|
||||
Contractor
|
||||
Stamp
|
||||
Architect
|
||||
Stamp
|
||||
Engineer
|
||||
Stamp
|
||||
Submittal Review
|
||||
This submittal has been reviewed for general compliance with the
|
||||
construction documents. The submittal review process does not
|
||||
relieve the subcontractor of the responsibility to verify and provide
|
||||
the correct: quantities, dimensions, details, layouts, and incidentals
|
||||
per the construction documents and onsite project conditions.
|
||||
Reviewed By: Zac Hansen Date: 11/6/2024
|
||||
REVIEWED
|
||||
REVIEWED AS NOTED
|
||||
REVISE & RESUBMIT
|
||||
REJECTED
|
||||
SHS
|
||||
The Best Companies
|
||||
11/6/2024
|
||||
Operable Partitions R1 - SD
|
||||
1
|
||||
10 2239
|
||||
|
||||
|
||||
--- Page 2 ---
|
||||
ELA Classrooms
|
||||
2183/2195
|
||||
REVISED
|
||||
REVISED
|
||||
9:10 AM, November 6, 2024
|
||||
9:10 AM, November 6, 2024
|
||||
|
||||
|
||||
--- Page 3 ---
|
||||
|
||||
|
||||
--- Page 4 ---
|
||||
|
||||
|
||||
--- Page 5 ---
|
||||
|
||||
|
||||
--- Page 6 ---
|
||||
|
||||
|
||||
--- Page 7 ---
|
||||
|
||||
|
||||
--- Page 8 ---
|
||||
ELA Classrooms
|
||||
2197/2199
|
||||
REVISED
|
||||
REVISED
|
||||
9:10 AM, November 6, 2024
|
||||
9:10 AM, November 6, 2024
|
||||
|
||||
|
||||
--- Page 9 ---
|
||||
|
||||
|
||||
--- Page 10 ---
|
||||
|
||||
|
||||
--- Page 11 ---
|
||||
|
||||
|
||||
--- Page 12 ---
|
||||
|
||||
|
||||
--- Page 13 ---
|
||||
|
||||
|
||||
+24
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+74
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"submittal_id": "157.1",
|
||||
"project_id": "0309A",
|
||||
"spec_section": "10 22 39 - Folding Panel Partitions",
|
||||
"csi_division": "10",
|
||||
"package_number": "BP 24 - Specialties",
|
||||
"product_name": "Operable Partitions - SD",
|
||||
"manufacturer": "",
|
||||
"model_number": "",
|
||||
"type": "Shop Drawing",
|
||||
"contractor": "The Best Company",
|
||||
"review_assignee": "Jeff Thomas",
|
||||
"review_due_date": "2024-11-20",
|
||||
|
||||
"overall_status": "CONDITIONAL",
|
||||
"submittal_status_recommended": "Approved as Noted",
|
||||
"draft_status": "DRAFT",
|
||||
|
||||
"conformance_checks": [
|
||||
{ "item": "Submittal cover sheet is complete (project #, sub name, spec section, date)",
|
||||
"result": "pass", "confidence": 0.98,
|
||||
"detail": "Cover sheet contains project 0309A, sub (The Best Company), spec 10 22 39, and dates" },
|
||||
{ "item": "Submittal number and revision matches the submittal log",
|
||||
"result": "pass", "confidence": 0.97, "detail": "157.1 (Rev 1)" },
|
||||
{ "item": "Sub has stamped and signed the submittal",
|
||||
"result": "insufficient_data", "confidence": 0.60,
|
||||
"detail": "Stamp presence could not be verified from extracted text" },
|
||||
{ "item": "Spec section referenced matches the scope being submitted",
|
||||
"result": "pass", "confidence": 0.97, "detail": "10 22 39 Folding Panel Partitions matches scope" },
|
||||
{ "item": "Manufacturer appears in the spec's approved manufacturer list",
|
||||
"result": "insufficient_data", "confidence": 0.50,
|
||||
"detail": "Spec book not loaded in v1 — PM must verify against Part 2 Products list" },
|
||||
{ "item": "Shop drawings show actual project dimensions, not generic drawings",
|
||||
"result": "pass", "confidence": 0.96,
|
||||
"detail": "Attachments reference project-specific opening locations and dimensions" },
|
||||
{ "item": "Coordination dimensions tie to structural drawings (steel beam condition)",
|
||||
"result": "warn", "confidence": 0.97,
|
||||
"detail": "Drawings show partition head connection to steel beam; no evidence the submittal was routed to the steel fabricator for coordination" },
|
||||
{ "item": "Submittal is complete — no 'additional data to follow' placeholders",
|
||||
"result": "pass", "confidence": 0.96,
|
||||
"detail": "All sheets numbered consecutively; no placeholder language detected" }
|
||||
],
|
||||
|
||||
"pattern_flags": [
|
||||
{ "source": "playbook",
|
||||
"pattern": "Missing Multi-Trade Coordination",
|
||||
"flag": "Operable partitions historically require routing to the steel fabricator for beam coordination. Confirm submittal copy has been distributed to the steel sub before architect review.",
|
||||
"severity": "warn" },
|
||||
{ "source": "playbook",
|
||||
"pattern": "Multi-Round Trades (Plan for R2+)",
|
||||
"flag": "This is already R1; confirm R0 comments have been fully addressed before architect re-review.",
|
||||
"severity": "info" }
|
||||
],
|
||||
|
||||
"deviations": [],
|
||||
|
||||
"suggested_action": "Confirm this submittal has been routed to the steel fabricator for beam coordination, then forward to architect.",
|
||||
|
||||
"pushback_draft": "",
|
||||
|
||||
"redline_suggestions": [],
|
||||
|
||||
"attachments": [
|
||||
"10 2239 Operable Partitions R1 - SD.pdf",
|
||||
"10 2239 Operable Partitions R1 - SD_1.pdf"
|
||||
],
|
||||
|
||||
"citations": [],
|
||||
|
||||
"confidence_tier": "yellow",
|
||||
|
||||
"skill_version": "v1.0",
|
||||
"run_timestamp": "2026-04-24T14:30:00-05:00"
|
||||
}
|
||||
BIN
Binary file not shown.
+211
@@ -0,0 +1,211 @@
|
||||
{
|
||||
"submittal_id": "157.1",
|
||||
"project_id": "0309A",
|
||||
"spec_section": "10 22 39 - Folding Panel Partitions",
|
||||
"csi_division": "10",
|
||||
"package_number": "BP 24 - Specialties",
|
||||
"product_name": "Operable Partitions - SD",
|
||||
"manufacturer": "",
|
||||
"model_number": "",
|
||||
"type": "Shop Drawing",
|
||||
"contractor": "The Best Company",
|
||||
"review_assignee": "Jeff Thomas",
|
||||
"review_due_date": "2024-11-20",
|
||||
|
||||
"overall_status": "CONDITIONAL",
|
||||
"submittal_status_recommended": "Approved as Noted",
|
||||
"draft_status": "DRAFT",
|
||||
|
||||
"conformance_checks": [
|
||||
{
|
||||
"item": "Submittal cover sheet is complete (project, sub, spec section, date)",
|
||||
"result": "pass",
|
||||
"confidence": 0.98,
|
||||
"detail": "Cover sheet shows SHS, The Best Companies, 10 2239, Rev 1, 11/6/2024",
|
||||
"citation": null
|
||||
},
|
||||
{
|
||||
"item": "Spec section referenced matches scope being submitted",
|
||||
"result": "pass",
|
||||
"confidence": 0.99,
|
||||
"detail": "Submittal spec 10 2239 matches 'FOLDING PANEL PARTITIONS' in spec book",
|
||||
"citation": {
|
||||
"spec_section": "10 22 39",
|
||||
"spec_paragraph": "Title block",
|
||||
"page": 1,
|
||||
"excerpt": "SECTION 10 2239 - FOLDING PANEL PARTITIONS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"item": "Shop Drawings include plans, elevations, sections, details, and attachments to other work",
|
||||
"result": "pass",
|
||||
"confidence": 0.96,
|
||||
"detail": "Submittal pages 2, 5, 7, 8, 11, 13 show plans + elevations with dimensions at ELA Classrooms 2183/2195 and 2197/2199",
|
||||
"citation": {
|
||||
"spec_section": "10 22 39",
|
||||
"spec_paragraph": "1.4.B.1",
|
||||
"page": 1,
|
||||
"excerpt": "Include plans, elevations, sections, details, and attachments to other work."
|
||||
}
|
||||
},
|
||||
{
|
||||
"item": "Shop Drawings indicate stacking/operating clearances and hardware, track, blocking, and direction of travel",
|
||||
"result": "warn",
|
||||
"confidence": 0.96,
|
||||
"detail": "Panel widths (5'-7 1/4\"), ceiling height (10'-0\"), and 4'-0\" pocket are shown; stacking clearance and direction of travel callouts are not clearly annotated on the extracted drawings",
|
||||
"citation": {
|
||||
"spec_section": "10 22 39",
|
||||
"spec_paragraph": "1.4.B.2",
|
||||
"page": 1,
|
||||
"excerpt": "Indicate stacking and operating clearances. Indicate location and installation requirements for hardware and track, blocking, and direction of travel."
|
||||
}
|
||||
},
|
||||
{
|
||||
"item": "Shop Drawings include diagrams for power, signal, and control wiring",
|
||||
"result": "not_applicable",
|
||||
"confidence": 0.96,
|
||||
"detail": "Spec §1.2.A.1 scopes this section as 'Manually operated' partitions — no power, signal, or control wiring required",
|
||||
"citation": {
|
||||
"spec_section": "10 22 39",
|
||||
"spec_paragraph": "1.2.A.1 (N/A per manual operation)",
|
||||
"page": 1,
|
||||
"excerpt": "Manually operated, acoustical panel partitions."
|
||||
}
|
||||
},
|
||||
{
|
||||
"item": "Samples for Verification: Textile Facing Material (8in x 10in) and Panel Edge Material (min 3in) included",
|
||||
"result": "fail",
|
||||
"confidence": 0.97,
|
||||
"detail": "Submittal includes Sherwin-Williams paint color chips (White, Smoke Gray, Natural Choice, Dark Bronze) for trim/hinge color selection, but the physical textile facing sample (8x10) and panel edge material sample (min 3 inches) required by spec are NOT in this submittal package",
|
||||
"citation": {
|
||||
"spec_section": "10 22 39",
|
||||
"spec_paragraph": "1.4.C.1 and 1.4.C.2",
|
||||
"page": 1,
|
||||
"excerpt": "Textile Facing Material: 8 inch by 10 inch section of fabric from dye lot to be used for the Work... Panel Edge Material: Not less than 3 inches long."
|
||||
}
|
||||
},
|
||||
{
|
||||
"item": "Coordination with Section 05 5000 Metal Fabrications for track supports to overhead structural system",
|
||||
"result": "warn",
|
||||
"confidence": 0.97,
|
||||
"detail": "Submittal shows track connection to structure at finished ceiling height but contains no evidence (transmittal, distribution list, or stamp) that the package was routed to the steel fabricator for beam coordination. Historical pattern: operable partitions on 0309A returned with this exact comment at the architect's review.",
|
||||
"citation": {
|
||||
"spec_section": "10 22 39",
|
||||
"spec_paragraph": "1.2.B.1",
|
||||
"page": 1,
|
||||
"excerpt": "Section 05 5000 'Metal Fabrications' for supports that attach supporting tracks to overhead structural system."
|
||||
}
|
||||
},
|
||||
{
|
||||
"item": "Basis-of-Design manufacturer (Modernfold Acousti-Seal Legacy Paired Panel) or documented equal identified",
|
||||
"result": "insufficient_data",
|
||||
"confidence": 0.55,
|
||||
"detail": "The manufacturer name was not extractable from text. Shop drawing may include a title block or product callout not captured by text extraction — PM to verify Modernfold Acousti-Seal Legacy Paired Panel, Kwik-Wall, or Panelfold is identified.",
|
||||
"citation": {
|
||||
"spec_section": "10 22 39",
|
||||
"spec_paragraph": "2.2.A.1",
|
||||
"page": 2,
|
||||
"excerpt": "Basis-of-Design Product: ... Modernfold Inc. Acousti-Seal Legacy Paired Panel model or comparable product by... Kwik-Wall, Panelfold Inc."
|
||||
}
|
||||
},
|
||||
{
|
||||
"item": "STC rating of not less than 51 declared and tested per ASTM E 90 / E 413",
|
||||
"result": "insufficient_data",
|
||||
"confidence": 0.45,
|
||||
"detail": "STC value not visible in extracted drawings text. PM to confirm product data sheet (if included) declares STC ≥ 51 and references ASTM E 90 / E 413 test method.",
|
||||
"citation": {
|
||||
"spec_section": "10 22 39",
|
||||
"spec_paragraph": "2.1.A.1 and 2.2.G",
|
||||
"page": 2,
|
||||
"excerpt": "Operable panel partition assembly tested for laboratory sound-transmission loss performance according to ASTM E 90, determined by ASTM E 413, and rated for not less than the STC indicated... STC: Not less than 51."
|
||||
}
|
||||
},
|
||||
{
|
||||
"item": "Finish colors within manufacturer's standard selection; adjacent wall color coordination",
|
||||
"result": "pass",
|
||||
"confidence": 0.96,
|
||||
"detail": "Smoke Grey specified for trim and hinge (submittal p. 2); paint swatch page shows Sherwin-Williams color chip selection for comparable-to-adjacent-field-wall color",
|
||||
"citation": {
|
||||
"spec_section": "10 22 39",
|
||||
"spec_paragraph": "2.2.D and 2.2.E",
|
||||
"page": 3,
|
||||
"excerpt": "Color shall be comparable to adjacent field wall colors... Exposed metal trim and seal color shall be as selected from manufacturer's standard trim selector."
|
||||
}
|
||||
},
|
||||
{
|
||||
"item": "Sub has stamped and signed the submittal",
|
||||
"result": "insufficient_data",
|
||||
"confidence": 0.70,
|
||||
"detail": "The Best Companies is named on cover sheet and Willowbrook reviewer Zac Hansen stamped 11/6/2024. A separate sub-company stamp per Willowbrook submittal procedures was not verifiable from extracted text."
|
||||
},
|
||||
{
|
||||
"item": "Warranty period meets or exceeds 2 years from Substantial Completion",
|
||||
"result": "insufficient_data",
|
||||
"confidence": 0.60,
|
||||
"detail": "No warranty language visible in the shop drawing attachments. Product data sheet (not part of this submittal, or not extracted) would carry this.",
|
||||
"citation": {
|
||||
"spec_section": "10 22 39",
|
||||
"spec_paragraph": "1.8.A.2",
|
||||
"page": 2,
|
||||
"excerpt": "Warranty Period: Two years from date of Substantial Completion."
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"pattern_flags": [
|
||||
{
|
||||
"source": "playbook",
|
||||
"pattern": "Missing Multi-Trade Coordination",
|
||||
"flag": "Operable partitions historically require routing to the steel fabricator for beam coordination. Confirm submittal copy has been distributed to the steel sub before architect review. Spec §1.2.B.1 makes this an explicit requirement.",
|
||||
"severity": "warn"
|
||||
},
|
||||
{
|
||||
"source": "playbook",
|
||||
"pattern": "Samples Required Separate from Product Data",
|
||||
"flag": "Textile facing and panel edge samples are a separate submittal requirement per §1.4.C. Color selection in this package does not satisfy the sample requirement.",
|
||||
"severity": "warn"
|
||||
},
|
||||
{
|
||||
"source": "playbook",
|
||||
"pattern": "Multi-Round Trades",
|
||||
"flag": "This is R1. R0 would have been Revise and Resubmit — confirm the R0 reviewer comments have been fully addressed before sending R1 to architect re-review.",
|
||||
"severity": "info"
|
||||
}
|
||||
],
|
||||
|
||||
"deviations": [],
|
||||
|
||||
"suggested_action": "Confirm steel fabricator has received a coordination copy (spec §1.2.B.1); obtain the textile facing and panel edge samples required by §1.4.C; verify Modernfold Acousti-Seal Legacy (or documented equal) and STC ≥ 51 are declared on the product data sheet. Forward to architect with these items flagged.",
|
||||
|
||||
"pushback_draft": "Before this R1 submittal is forwarded to 505 Architects, please confirm: (1) the package has been routed to the steel fabricator for track-to-structure coordination per spec section 10 2239 §1.2.B.1; (2) the required verification samples — 8\"x10\" textile facing and 3\" panel edge — are being provided (spec §1.4.C); (3) product data sheets confirming the Modernfold Acousti-Seal Legacy Paired Panel (or documented equal per §2.2.A.1) and STC ≥ 51 per ASTM E 90 / E 413 (§2.1.A.1, §2.2.G) are attached. Please respond with confirmation or updated package by [DATE].",
|
||||
|
||||
"redline_suggestions": [
|
||||
{ "page": 2, "item": "Stacking / operating clearance callout", "note": "Add dimensional callout for stacking clearance and direction of travel per spec §1.4.B.2" },
|
||||
{ "page": 1, "item": "Cover sheet — spec section block", "note": "Spec Section field is filled (10 2239); verify subcontractor stamp is attached to the full package" }
|
||||
],
|
||||
|
||||
"attachments": [
|
||||
"10 2239 Operable Partitions R1 - SD.pdf",
|
||||
"10 2239 Operable Partitions R1 - SD_1.pdf"
|
||||
],
|
||||
|
||||
"citations": [
|
||||
"10 22 39 §1.2.A.1",
|
||||
"10 22 39 §1.2.B.1",
|
||||
"10 22 39 §1.4.B.1",
|
||||
"10 22 39 §1.4.B.2",
|
||||
"10 22 39 §1.4.C.1",
|
||||
"10 22 39 §1.4.C.2",
|
||||
"10 22 39 §1.8.A.2",
|
||||
"10 22 39 §2.1.A.1",
|
||||
"10 22 39 §2.2.A.1",
|
||||
"10 22 39 §2.2.D",
|
||||
"10 22 39 §2.2.E",
|
||||
"10 22 39 §2.2.G"
|
||||
],
|
||||
|
||||
"confidence_tier": "yellow",
|
||||
|
||||
"skill_version": "v1.1",
|
||||
"run_timestamp": "2026-04-24T15:45:00-05:00"
|
||||
}
|
||||
Reference in New Issue
Block a user