Skip to content
Back to skills

Tag Audit And Takeoff

ASecurity

Count-based quantity takeoff and tag completeness auditing for construction drawings. Vision + OCR reconciliation, sheet markup, Excel QTO output. Triggers: 'tag audit', 'quantity takeoff', 'QTO', 'count fixtures'.

  • 43 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 26, 2026
datapythongobashsqlapidatabase

Works with

  • claude code
  • api

Security analysis

A100/100

Pro scans all 11 files and shows the line behind each finding

Scanned October 5, 2026

npx -y skills add dleerdefi/claude-code-construction --skill tag-audit-and-takeoff --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Tag Audit And Takeoff?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Tag Audit And Takeoff
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/dleerdefi-tag-audit-and-takeoff/badge)](https://www.skillsdirectory.com/skills/dleerdefi-tag-audit-and-takeoff)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: tag-audit-and-takeoff
description: >
  Count-based quantity takeoff and tag completeness auditing for construction
  drawings. Vision + OCR reconciliation, sheet markup, Excel QTO output.
  Triggers: 'tag audit', 'quantity takeoff', 'QTO', 'count fixtures'.
disable-model-invocation: true
---

# Tag Audit & Quantity Takeoff

## Purpose

Count-based QTO for tagged construction elements — fixtures, devices,
doors, equipment, and any element identified by a tag/symbol on drawings.
Does NOT do area (sqft) or linear (lf) takeoffs — only discrete counts.

The QTO is a byproduct of the audit. The real value: the completeness
engine that finds what's tagged, what's missing, and produces a defensible
count with full provenance for every number.

Does NOT: do area/linear takeoffs, create outbound documents autonomously,
override user-approved groupings, or assume quantities without provenance.

---

## Step 0: Detect Operating Mode

Check for AgentCM: `.construction/project.yaml` at the project root.

**AgentCM mode** (`.construction/project.yaml` exists):
- Read `.construction/CLAUDE.md` for project context
- Read `.construction/database.yaml` for `project_id` and `api_url`; run each query with `sh .construction/query.sh "SQL"`
- Sheet images at `.construction/rasters/{sheet_number}.png`

**Verify raster images exist:** Check `.construction/rasters/` for PNG files.
If empty, tell the user: "Raster images not found. Open this project in
AgentCM to trigger the export, or run:
`curl -s -X POST '{api_url}/projects/{project_id}/graph/export' -H 'Content-Type: application/json' -d '{\"rootPath\": \"'$(pwd)'\"}'`"
You can also rasterize individual sheets on demand using the rasterize_page.py script.
- OCR data queryable via `extracted_items` table in PostgreSQL
- Write results back via API: `POST /api/projects/{id}/tag-detections/ingest`

**Flat File mode** (no `.construction/project.yaml`):
- Discover sheet images from CLAUDE.md or user-provided paths; rasterize sheet
  PDFs with `"${CLAUDE_PLUGIN_ROOT}/bin/construction-python" "${CLAUDE_PLUGIN_ROOT}/scripts/pdf/rasterize_page.py" "<pdf>" <page> --dpi 200 --output "<png>"`
- Vision-only pipeline (Steps 2-3 skipped)
- Write marked-up PNGs and the Excel workbook to the project directory; the QTO
  JSON goes to `.construction/skills/tag-audit-and-takeoff/qto/` (Step 8)

## Step 1: User Scopes the Task

User provides: (1) tag type — e.g., "plumbing fixtures", "doors",
"light fixtures" (see `references/tag-types.md` for full list), and
(2) sheet scope — specific sheets, a discipline, or "all sheets".
Confirm scope before proceeding.

**Custom tag types:** If the user requests a tag type not in
`references/tag-types.md` (e.g., "keynote legends", "fire dampers",
"seismic bracing"), treat it as a custom type. Use it in the `tag_type`
field at ingest — the server auto-registers unknown types in the
`tag_types` table with default styling. The normalized name will be
`qto_{type}` (e.g., `qto_keynote_legend`). Custom types appear in the
Group Review gallery and support the full accept/reject/promote workflow.

**REQUIRED: Tag type resolution.** Before using any `tag_type` value,
you MUST resolve it against existing types:

1. Query `GET /api/projects/{id}/group-review/available-types` to see
   all active tag types for this project
2. Check if the user's requested type matches ANY existing type name
3. Apply alias resolution — these names are synonyms of existing types:
   - `detail_callout`, `section_callout`, `interior_elevation`,
     `callout_single_ref`, `single_ref_callout` → `simple_callout`
   - `multi_callout`, `callout_multi_ref`, `multi_ref_callout`
     → `multi_detail_callout`
4. If the resolved type exists in available-types, USE THAT NAME
5. Report to user: "Resolved type: '{resolved_name}' (matched from
   '{user_input}')" — get confirmation before proceeding
6. **If there is ANY ambiguity** about whether a requested tag type
   maps to an existing type, **ASK THE USER** before proceeding.
   Do not guess or invent a new type name.
7. Only auto-register a truly new type if no existing type or alias
   matches after checking both the available-types list and the alias
   map above

NEVER invent a new type name for concepts that already have canonical
types. When in doubt, ask.

## Step 1.5: Check Existing Coverage [REQUIRED GATE] (AgentCM mode only)

**HARD GATE — you MUST complete this step before proceeding to Step 2.**
Do not skip this step. If the API is unreachable, stop and tell the user.

Before scanning sheets, query what's already tagged per sheet:

```bash
curl -s "{api_url}/projects/{project_id}/sheets/{sheet_id}/claimed-elements"
```

Response:
```json
{
  "claimed_element_ids": ["el_001", "el_002", ...],
  "suggestion_count": 12,
  "by_type": { "qto_room_tag": 8, "room_tag": 4 },
  "existing_tags": [
    { "id": "...", "proposedType": "qto_room_tag", "proposedText": "KITCHEN",
      "status": "accepted", "constituentIds": [...], "combinedBbox": {...}, "confidence": 0.92 }
  ]
}
```

Report to user: "Sheet A-1.4: 12 tags already detected (8 accepted, 4 pending)."

**Reconciliation rules:**
- **EXCLUDE** claimed element IDs from spatial queries (Steps 3-4) —
  add `AND id NOT IN ('{id1}', '{id2}', ...)` to SQL WHERE clauses
- **INCLUDE** `existing_tags` in the final QTO output — the QTO captures
  ALL items (existing + newly detected), not just new ones
- During vision (Step 2), note tags that match existing `proposedText`
  values — these are "confirmed" not "new"
- New detections that survive dedup → POST to ingest as usual
- The server also deduplicates at ingest: detections with element IDs
  overlapping existing suggestions are skipped (safety net)
- Final QTO JSON must merge: `existing_tags` (from this endpoint) +
  newly detected (from ingest response)

**The QTO is always a complete count.** Reconciliation avoids redundant
detection *work*, not redundant *output*.

**Before proceeding to Step 2, report coverage and get confirmation:**
"Existing coverage: {N} sheets have {M} accepted, {P} pending detections
for this tag type. {Q} sheets have zero coverage.
Proceed with vision scan on {Q} uncovered sheets?"
Wait for explicit user confirmation before continuing.

## Step 2: Vision Tag Identification

**CRITICAL — Vision is MANDATORY for this step.**
- You MUST read each sheet's raster image via vision to identify tags
- You may NOT substitute OCR text pattern matching (regex, ILIKE queries)
  for visual identification — OCR regex finds text, not tags
- OCR data is used ONLY in Steps 3-4 to spatially anchor
  vision-identified tags, not to replace the visual identification step
- If you cannot read a raster image for a sheet, FLAG it as
  "skipped — raster unavailable" and move to the next sheet
- Do NOT report OCR text matches as "detected tags"

Examine each sheet image. Identify all visible tags of the requested
type. Return tag texts and view type classification — no coordinates.

Vision must classify each tag's view: FLOOR PLAN, ENLARGED PLAN,
DETAIL, SECTION, or ELEVATION. This determines record type in Step 5.
Classification signals are in `references/tag-types.md`.

**In Flat File mode:** This is the primary (and only) identification step.
Provide rough locations ("upper-left quadrant") for markup purposes.

## Step 3: Text-Anchor Search (AgentCM mode only)

For each vision-identified tag, find its spatial anchor in the DB
using the **most distinctive word** in the tag text. "WALK-IN FREEZER"
→ search "FREEZER". "WOMEN'S RESTROOM" → search "WOMEN".

```bash
sh .construction/query.sh "SELECT id, text, x_min, y_min, x_max, y_max FROM extracted_items WHERE sheet_id = '{sheet_id}' AND is_deleted = false AND text ILIKE '%FREEZER%'"
```

Multiple hits expected — each is a potential tag location. Zero hits
→ try alternate words. Still zero → flag "vision-only, no OCR anchor".

## Step 4: Spatial Neighborhood Query (AgentCM mode only)

For each anchor, pull nearby extracted items. Padding values by tag type
in `references/spatial-params.md`. Target: 8-25 candidates per tag.

```bash
sh .construction/query.sh "SELECT id, text, x_min, y_min, x_max, y_max FROM extracted_items WHERE sheet_id = '{sheet_id}' AND is_deleted = false AND x_min BETWEEN {anchor_x - pad} AND {anchor_x + pad} AND y_min BETWEEN {anchor_y - pad} AND {anchor_y + pad}"
```

Adaptive: double padding if < 3 items returned, reduce 30% if > 40.

## Step 5: Grouping

Give Claude the vision tag text and the small candidate set (~15-20
items). Group constituent items following `references/grouping-rules.md`
— text reconstruction, spatial coherence, include/exclude criteria,
confidence scoring.

**Batching**: Process ALL tags per sheet in one call. 12 tags × ~200
tokens each = ~2400 tokens. Never load the full extracted items set.

**Flat File mode**: Skip grouping. Tags come from vision only. Mark
all detections as `confidence: "vision_only"` in output.

**Validation checkpoint (after first 3 sheets):**
After grouping tags on the first 3 sheets, STOP and present 5 sample
grouped detections to the user. Show for each:
- Vision-identified text
- OCR-reconstructed text (from constituents)
- Confidence score
- Number of constituent element IDs
- Approximate sheet location (combined bounding box)

Get explicit user confirmation: "Grouping quality acceptable? Proceed
with remaining {N} sheets?" Do not continue until confirmed.

## Step 6: Sheet Markup

**REQUIRED for all modes.** Mark up each sheet with visual highlights
so the user can see exactly what was identified.

**QTO markup must show ALL tagged items per sheet** — not just newly detected.
Pull existing items from the Step 1.5 claimed-elements response (`existing_tags`)
and include them in the markup alongside new detections. Use color to distinguish:
- `--color "green"` for accepted items (from previous detection/review)
- `--color "blue"` for newly detected items (this session)
- `--color "orange"` for pending review items

Run the markup script once per color group per sheet, layering onto the same
output image (use the previous output as `--base` for the next pass).

```bash
# Build items JSON with one entry per detected tag
# Then call the shared markup script:
"${CLAUDE_PLUGIN_ROOT}/bin/construction-python" \
  "${CLAUDE_SKILL_DIR}/scripts/markup_tags.py" \
  --base "{sheet_image_path}" \
  --items "{items_json_path}" \
  --output "{output_path}" \
  --color "blue" \
  --label-style "numbered"
```

Items JSON format (pixel coordinates):
```json
[
  {"x": 2500, "y": 1800, "shape": "circle", "radius": 20, "label": "WALK-IN FREEZER"},
  {"x": 3100, "y": 2200, "shape": "circle", "radius": 20, "label": "DRY STORAGE"}
]
```

Convert normalized 0-1 coordinates to pixels using sheet image dimensions.

**If PDF is available**, also create native PDF annotations:
```bash
"${CLAUDE_PLUGIN_ROOT}/bin/construction-python" \
  "${CLAUDE_PLUGIN_ROOT}/scripts/pdf/annotate_pdf.py" \
  --pdf "{pdf_path}" --items "{items_json}" --output "{output_pdf}" \
  --author "Claude Code QTO"
```

## Step 7: Record Classification

Record type depends on view type. Full data model in
`references/quantity-model.md`.

**Floor plan tags → Element Instance.** One tag = one installed item.
**Detail/section/elevation tags → Element Type Definition.** Template.

## Step 8: Write Results

**Pre-ingest confirmation (REQUIRED before POSTing):**
Before making ANY ingest API calls, present this summary to the user:
- Total new detections to ingest: {N}
- Total existing from claimed-elements: {M}
- Resolved tag type: {type_name}
- 3-5 sample detections (tag_text, confidence, element_ids count)
- Any flagged items (low confidence, no OCR anchor, skipped sheets)

"Ready to ingest {N} new detections as '{type_name}'? (Y/N)"

Do NOT POST to the ingest endpoint until the user explicitly confirms.
Ingesting without confirmation is a blocking violation of this skill.
The server enforces a maximum of 100 detections per request — batch
your ingest calls per sheet.

**AgentCM mode:** POST detection results to the API:
```bash
curl -X POST "{api_url}/projects/{project_id}/tag-detections/ingest" \
  -H "Content-Type: application/json" \
  -d '{"sheet_id": "{sheet_id}", "detections": [...]}'
```

Each detection: `{ tag_text, tag_type, element_ids, bounding_box, confidence, view_type, room_id }`.
`bounding_box` must be `{ x, y, width, height }` in normalized 0-1 coordinates.
Results appear as highlighted overlays in AgentCM's canvas UI.

In AgentCM mode, also write the finding to `.construction/agent_findings/qto_{tag_type}_{timestamp}.json`.

**CRITICAL — use the EXACT schema from `references/qto-output-format.md`.**
The Excel script is rigid and parses specific key names. Required keys:
- `line_items[]` (NOT `qto_lines`) — one entry per unique element/designation
- `totals{}` with `sheet_instances`, `derived_instances`, `deduplicated`, `building_quantity`
- Each line item has `instance_details[]` with `detection_id`, `sheet`, `room`, `status`, `confidence`
- `scope{}`, `completeness{}`, `issues[]`

**Include ALL items** — existing pipeline detections + newly detected this session.
The QTO is a complete count. Query the claimed-elements endpoint (Step 1.5) for
existing items and merge them into `line_items[].instance_details[]`.

**Both modes:** Save the QTO JSON (working data) to
`.construction/skills/tag-audit-and-takeoff/qto/{tag_type}_{timestamp}.json` and the
marked-up PNGs (deliverables) to the project directory.

**Excel export** (both modes): Pass that QTO JSON as `{qto_json_path}`
and invoke the export script to produce a styled multi-sheet workbook:

```bash
"${CLAUDE_PLUGIN_ROOT}/bin/construction-python" \
  "${CLAUDE_SKILL_DIR}/scripts/qto_to_xlsx.py" \
  --data "{qto_json_path}" \
  --project "{project_name}" \
  --scope "{tag_type_display}" \
  --output "QTO_{tag_type}_{date}.xlsx"
```

The workbook has 4 sheets: QTO Summary (line items with counts),
Instance Detail (every detection with provenance), Type Definitions
(templates applied to rooms), and Completeness (coverage metrics).

---

## Reference Resolution (Type Defs → Derived Instances)

After type definitions are identified from details/sections/enlarged
plans, resolve where they apply. Full logic: `references/quantity-model.md`.

Three methods (in order of reliability):
1. **Callout reference** — detail bubble on floor plan references source
2. **Room type match** — room tags match the type def's parent view label
3. **Schedule cross-ref** — finish/room schedule references the detail

Deduplication: direct instances always take precedence over derived.

---

## Completeness Metrics

Compare detected counts against schedules (door, fixture, equipment)
when available. Report: "[tag type]: X detected, Y expected, Z%
coverage. Gaps on sheets: [list]." No schedule → report raw counts.

---

## Allowed Scripts

**Allowed scripts — exhaustive list.** Only execute these scripts during this skill:
- `${CLAUDE_PLUGIN_ROOT}/scripts/pdf/rasterize_page.py` — rasterize a sheet PDF page to PNG (flat file mode, or on demand)
- `scripts/markup_tags.py` — sheet markup with tag highlights
- `${CLAUDE_PLUGIN_ROOT}/scripts/pdf/annotate_pdf.py` — native PDF annotations
- `scripts/qto_to_xlsx.py` — QTO Excel export (4-sheet workbook)
- `${CLAUDE_PLUGIN_ROOT}/scripts/issue_manager.py` — log schedule/drawing count mismatches to the Issue Registry (command and severity mapping in construction-guide)

Files in this skill

  • SKILL.md14.4 KB
  • agents/openai.yaml300 B
  • references/detection-record.schema.json3.6 KB
  • references/grouping-rules.md4.1 KB
  • references/qto-output-format.md4.5 KB
  • references/quantity-model.md5.5 KB
  • references/spatial-params.md3.3 KB
  • references/tag-types.md3.7 KB
  • scripts/markup_tags.py3.5 KB
  • scripts/qto_to_xlsx.py17.9 KB
  • scripts/shared.py554 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.