Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions PUBLIC_DATA_POLICY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ distribution.
- Invented dimensions and quantities that do not reproduce a private project.
- Placeholder configuration such as `Example Fabricator` and `Example City, ST`.
- Product behavior and generic steel-domain terminology.
- Public demo media reviewed before publication and pinned in the scanner by exact
repository path and SHA-256 digest. Replacing it requires a new review and digest.

## Prohibited

Expand Down
160 changes: 105 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,100 +1,150 @@
# pi-steel

Structural steel estimating skills for the [Pi coding agent](https://pi.dev).
[![npm version](https://img.shields.io/npm/v/%40structupath%2Fpi-steel)](https://www.npmjs.com/package/@structupath/pi-steel)
[![CI](https://github.com/StructuPath/pi-steel/actions/workflows/ci.yml/badge.svg)](https://github.com/StructuPath/pi-steel/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Pi package](https://img.shields.io/badge/Pi-package-7c3aed)](https://pi.dev/packages?name=pi-steel)

By [StructuPath](https://structupath.ai).
Structural-steel estimating skills for the [Pi coding agent](https://pi.dev):
validated takeoffs, plate nesting, guarded DXF output, draft vendor RFQs, and a
review-gated estimate pipeline.

Built by [StructuPath](https://structupath.ai).

## Demo

![pi-steel builds a synthetic estimate package](https://raw.githubusercontent.com/StructuPath/pi-steel/main/docs/assets/pi-steel-demo.gif)

This demo uses only the repository's synthetic fixture. It runs the complete
`takeoff → nest → draft RFQ` pipeline and publishes an isolated package with a
machine-readable `rfq_ready_for_review` outcome.

## Install

```bash
pi install npm:@structupath/pi-steel
```

## What's Inside
Then ask Pi:

```text
Do a structural-steel takeoff from these drawings and build a validated BOM.
Nest these rectangular plate parts on 96 × 48 stock.
Prepare a draft RFQ from this estimate.
```

Run the local dependency check when using the source repository:

```bash
npm run doctor
```

### `steel-takeoff` — takeoffs, BOMs, tonnage
## What is included

Structural steel quantity takeoff with a bundled **AISC 16th Edition shapes database (477 shapes)** — W, HSS, angles, channels, pipe — plus scripts the agent runs directly:
| Skill | Purpose | Primary outputs |
| --- | --- | --- |
| `steel-takeoff` | Validate member designations and calculate BOM weight and tonnage | Validated BOM and findings |
| `steel-nest` | Lay out plate parts with kerf, gap, and edge-margin controls | Nest results, cut list, reference drawings, and guarded burn DXFs |
| `steel-rfq` | Compile estimate data into a reviewable vendor request | Draft `.xlsx` RFQ and semantic workbook record |
| `steel-estimate` | Orchestrate the complete review-gated workflow | Immutable run directory, QA report, lineage, manifests, nesting, and draft RFQ |

- `lookup-member.sh` — full property set for any AISC designation (`W14X30` → plf, d, bf, A, Ix, Sx, …)
- `calculate-weight.sh` — BOM weight totals with connection and misc-steel allowances and tonnage; it does not invent pricing
- `validate-bom.py` — catches invalid designations, wrong grades, duplicate marks, unreasonable weights
### Takeoff

Also includes reference guides for AISC shape families, takeoff procedures with worked examples, connection types and hardware weights, material grades, and bolt capacities.
`steel-takeoff` provides AISC shape lookup, weight calculation, and BOM
validation helpers. It checks member designations, grades, duplicate marks, and
unreasonable weights. It calculates from supplied quantities and allowances; it
does not invent pricing.

Ask your agent things like:
### Nesting and DXF safety

> "Do a takeoff from these drawings and build me a BOM"
> "What's the lightest W-shape with depth ≥ 18" and Ix ≥ 1000?"
> "Total tonnage on this BOM with 12% connections"
`steel-nest` uses MaxRects bin packing for rectangular parts and reports yield,
scrap, reusable drops, and unplaced material. Irregular parts are estimated by
bounding box and are always flagged.

### `steel-nest` — plate nesting & guarded DXF output
Per-sheet `burn_plate_N.dxf` files are emitted only when the full nest is
complete and every supported hole remains inside its part. Otherwise, pi-steel
suppresses burn DXFs for the entire run and leaves clearly labeled estimating
and reference artifacts for review. It does not emit G-code or claim
machine-specific CAM compatibility.

The plate-layout step CAM software does, minus the CAM seat: MaxRects bin-packing of parts onto stock plates with kerf/gap/edge-margin spacing, holes and rectangular cutouts, yield/scrap/reusable-drop numbers, and material cost. Outputs include a labeled layout (PDF + PNG per plate), a cut list, and an explicitly named all-sheets reference DXF.
### Draft RFQs

Per-sheet `burn_plate_N.dxf` files are emitted only for a complete rectangular nest whose supported holes remain inside their parts. Those files contain cut entities only: closed outlines on `PROFILE` and holes/cutouts on `HOLES`. Sheet outlines and labels remain in clearly named reference files. Any irregular part, unplaced part, or out-of-bounds hole suppresses burn DXFs for the whole job and leaves the safe estimating/reference artifacts available with an explicit warning.
`steel-rfq` groups material by stock family, carries approved nesting data into
the workbook, and provides fill-in columns for vendor pricing. Workbooks remain
drafts until a person reviews and issues them.

Honest about its limits: rectangular parts nest exactly; irregular parts nest by bounding box (flagged, never hidden); reference DXFs are not cutting instructions; and the package deliberately does **not** emit G-code. Kerf compensation, lead-ins, pierce points, and machine-specific verification belong to the table's real CAM and post-processor.
Copy the example company profile to a private project location:

Each command publishes an isolated run under the requested output root and updates
`latest-run.json`. Exit `0` is geometry-verified, `2` requires review, and `3` is
blocked. No named CAM compatibility is claimed.
```bash
mkdir -p .pi-steel
cp skills/steel-rfq/assets/company-profile.example.json \
.pi-steel/company-profile.json
Comment on lines +76 to +81

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files and inspect the README section around the cited lines.
git ls-files README.md package.json skills/steel-rfq/assets/company-profile.example.json
printf '\n--- README excerpt ---\n'
sed -n '60,95p' README.md

printf '\n--- package.json excerpt ---\n'
sed -n '1,220p' package.json

printf '\n--- example profile file ---\n'
cat -n skills/steel-rfq/assets/company-profile.example.json

Repository: StructuPath/pi-steel

Length of output: 4396


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the npm package definition would include the example profile asset
# and whether the README describes repo checkout vs package install.
rg -n '"files"|assets|company-profile.example.json|npm:`@structupath/pi-steel`|npm install|npx' -S README.md package.json .npmignore .gitignore

Repository: StructuPath/pi-steel

Length of output: 513


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for any packaging or release metadata that would ship the assets folder.
fd -a 'package*.json' .
fd -a '.npmignore' .
fd -a '.gitignore' .
rg -n '"files"|publish|prepack|postpack|assets/' -S .

Repository: StructuPath/pi-steel

Length of output: 20834


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the install/setup context around the README and the RFQ skill docs.
sed -n '1,40p' README.md
printf '\n--- skills/steel-rfq/SKILL.md excerpt ---\n'
sed -n '1,120p' skills/steel-rfq/SKILL.md

Repository: StructuPath/pi-steel

Length of output: 5566


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect package-related tests to see what is expected to ship and how users consume it.
sed -n '1,120p' tests/test_package_contents.py
printf '\n--- installed script test excerpt ---\n'
sed -n '1,220p' tests/test_installed_scripts.py

Repository: StructuPath/pi-steel

Length of output: 6644


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether README or scripts reference the example profile from an installed package path.
rg -n "company-profile\.example\.json|company-profile\.json|skills/steel-rfq/assets" README.md skills tests scripts -S

Repository: StructuPath/pi-steel

Length of output: 1466


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Determine whether the README's example command is intended to work from a package checkout
# or from an installed package copy by inspecting nearby setup language.
sed -n '1,120p' README.md

Repository: StructuPath/pi-steel

Length of output: 4686


Clarify the company profile copy path. The command uses a repo-relative source path, so it only works from a source checkout. If this is meant for installed-package users, point to the shipped example path; otherwise mark it as source-only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 76 - 81, Clarify the company-profile setup
instructions around the example copy command: either replace the repo-relative
source with the shipped example path for installed-package users, or explicitly
label the command as source-checkout-only. Update the README text surrounding
the company profile example while preserving the destination
.pi-steel/company-profile.json.

```

> "How many sheets does this job need?"
> "Nest these parts on 96×48 plate and give me the yield"
> "Lay this out for the burn table"
Complete that local copy with approved company information. Never commit the
completed profile.

Requires `ezdxf`, `matplotlib`, `numpy` (`pip install ezdxf matplotlib numpy`).
## Workflow and outcomes

### `steel-rfq` — vendor quote requests
```text
estimate input
contract validation → plate nesting → draft RFQ compilation
QA findings + lineage + immutable run manifest
ready | needs_review | blocked
```

Turns a steel estimate/takeoff spreadsheet into a standardized vendor RFQ (.xlsx): materials grouped the way vendors stock them (W-shapes / plate / flat bar), yellow fill-in pricing columns, nesting/drop reference, and terms & conditions — branded with **your** company profile. When `steel-nest` has run for the job, its cutting plan flows straight into the RFQ's nesting table.
Each run is published below the requested output root and referenced by
`latest-run.json`. Exit status `0` means geometry-verified, `2` means human
review is required, and `3` means the workflow is blocked. A blocked run never
contains an RFQ workbook.

The `steel-estimate` orchestrator chains the skills into a review-gated estimating
pipeline: **takeoff → nest → draft RFQ**. It publishes immutable run directories,
QA findings, lineage, and readiness labels; blocked runs never contain a workbook.
## Public-repository safety

One-time setup: copy `skills/steel-rfq/assets/company-profile.example.json` to the
ignored path `.pi-steel/company-profile.json` in your project and enter approved
company and commercial information there. Never add the completed profile to this
repository.
This repository and all committed examples are public. Do not add:

Keep company profiles, customer files, vendor information, live pricing, and generated
artifacts outside this repository. Public examples are synthetic and must follow
[`PUBLIC_DATA_POLICY.md`](PUBLIC_DATA_POLICY.md).
- company profiles or internal business context;
- customer, vendor, employee, project, bid, or contract data;
- live pricing, margins, terms, credentials, or private contact information;
- generated RFQs, takeoffs, PDFs, DXFs, spreadsheets, or output directories.

> "Prepare a draft RFQ from this takeoff"
> "Generate an RFQ from this estimate"
Keep operational inputs and generated artifacts outside the repository. See the
[public data policy](PUBLIC_DATA_POLICY.md) and
[data provenance record](DATA_PROVENANCE.md) before contributing.

## Requirements
## Documentation

- `jq` and `python3` (with `pandas` + `openpyxl` for RFQ generation)
- macOS or Linux
- [GitHub wiki](https://github.com/StructuPath/pi-steel/wiki)
- [Public data policy](PUBLIC_DATA_POLICY.md)
- [Data provenance](DATA_PROVENANCE.md)
- [Pi package catalog](https://pi.dev/packages?name=pi-steel)
- [npm package](https://www.npmjs.com/package/@structupath/pi-steel)

## Development and release checks
## Development

```bash
npm test # base, no-render suite
npm run test:full # optional PDF/PNG/DXF/LibreOffice smoke tests
npm run privacy:check # public-repository data guard
npm run privacy:history # redacted audit of every reachable commit
npm run pack:check # npm contents plus unpacked-runtime smoke test
npm run test:full # PDF/PNG/DXF/LibreOffice smoke tests
npm run privacy:check # current public-repository data guard
npm run privacy:history # redacted audit of reachable history
npm run pack:check # npm contents and installed-runtime smoke test
npm run provenance:check # shape-data integrity and recorded decision
npm run release:check # complete release gate
```

`release:check` is intentionally blocked while redistribution permission for the
checked-in transformed AISC shape dataset remains unverified. See
[`DATA_PROVENANCE.md`](DATA_PROVENANCE.md). Do not publish a new package release
by bypassing that gate.
The latest npm release is `0.2.2`. The source repository also declares `0.2.2`
until a later release passes every publishing gate.

`release:check` intentionally blocks a new npm publication while redistribution
permission for the transformed AISC shape dataset remains unverified. Do not
bypass that gate. An updated README or demo reaches the Pi catalog only through
a future, fully approved npm release.

## License

StructuPath-authored code and documentation are MIT licensed. That license does
not grant rights in third-party data. The checked-in AISC-derived shape data has
a separate, currently blocked redistribution decision documented in
[`DATA_PROVENANCE.md`](DATA_PROVENANCE.md).

---

Building software for steel fabricators? See [structupath.ai](https://structupath.ai).
[DATA_PROVENANCE.md](DATA_PROVENANCE.md).
Binary file added docs/assets/pi-steel-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"requirements-render.txt",
"DATA_PROVENANCE.md",
"DATA_PROVENANCE.json",
"docs/assets/pi-steel-demo.gif",
"README.md",
"PUBLIC_DATA_POLICY.md",
"LICENSE"
Expand Down
17 changes: 16 additions & 1 deletion scripts/check-public-data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import argparse
import hashlib
import re
import subprocess
import sys
Expand Down Expand Up @@ -34,6 +35,10 @@
"private-key",
"private_key",
}
AUDITED_PUBLIC_BINARY_SHA256 = {
Path("docs/assets/pi-steel-demo.gif"):
"ae6ad7286fc5f1eca31e960d4ac4414b7a32a566b975ddacc7d662824c34d91c",
}
PATTERNS = {
"private operating-company claim": re.compile(
r"team behind (?:a|the) production structural[- ]steel", re.I
Expand All @@ -59,6 +64,11 @@
}


def is_audited_public_binary(relative: Path, content: bytes) -> bool:
expected = AUDITED_PUBLIC_BINARY_SHA256.get(relative)
return expected is not None and hashlib.sha256(content).hexdigest() == expected


def tracked_files(root: Path = ROOT) -> list[Path]:
result = subprocess.run(
["git", "ls-files", "--cached", "--others", "--exclude-standard"],
Expand Down Expand Up @@ -128,8 +138,11 @@ def scan_paths(
if suffix in FORBIDDEN_BINARY_SUFFIXES:
findings.append(f"{relative}: public repository must not contain {suffix} artifacts")
continue
content = path.read_bytes()
if is_audited_public_binary(relative, content):
continue
try:
text = path.read_bytes().decode("utf-8")
text = content.decode("utf-8")
Comment on lines +141 to +145

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed on hash mismatch at the audited path.

Both scanner entry points treat a digest mismatch as ordinary content; benign UTF-8 replacements can therefore pass with zero findings, despite the policy requiring an exact path and SHA-256 digest.

  • scripts/check-public-data.py#L141-L145: reject mismatched allowlisted files instead of decoding them.
  • scripts/check-public-data.py#L179-L182: apply the same rejection to staged and historical blobs.
  • tests/test_public_data_policy.py#L82-L94: add regression coverage for altered UTF-8 bytes through these scanner paths.
Proposed fail-closed handling
         content = path.read_bytes()
-        if is_audited_public_binary(relative, content):
-            continue
+        if relative in AUDITED_PUBLIC_BINARY_SHA256:
+            if is_audited_public_binary(relative, content):
+                continue
+            findings.append(f"{relative}: audited public binary hash mismatch")
+            continue

Apply the equivalent logic in _scan_bytes, returning the mismatch finding instead of falling through.

📍 Affects 2 files
  • scripts/check-public-data.py#L141-L145 (this comment)
  • scripts/check-public-data.py#L179-L182
  • tests/test_public_data_policy.py#L82-L94
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-public-data.py` around lines 141 - 145, Make both scanner entry
points in scripts/check-public-data.py fail closed on audited-path SHA-256
mismatches: update the file-system scan around is_audited_public_binary and the
staged/historical blob scan to return the mismatch finding before UTF-8 decoding
or content scanning. Add regression coverage in tests/test_public_data_policy.py
for altered UTF-8 bytes through both scanner paths, confirming each reports the
digest mismatch.

except UnicodeDecodeError:
findings.append(f"{relative}: unknown binary file")
continue
Expand Down Expand Up @@ -163,6 +176,8 @@ def _scan_bytes(
return [
f"{prefix}{relative}: public repository must not contain {suffix} artifacts"
]
if is_audited_public_binary(relative, content):
return []
try:
text = content.decode("utf-8")
except UnicodeDecodeError:
Expand Down
1 change: 1 addition & 0 deletions tests/test_package_contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def test_npm_dry_run_contains_runtime_contract_and_excludes_private_artifacts():
"DATA_PROVENANCE.md",
"DATA_PROVENANCE.json",
"PUBLIC_DATA_POLICY.md",
"docs/assets/pi-steel-demo.gif",
"scripts/check-data-provenance.py",
"scripts/doctor.py",
"skills/_shared/schemas/estimate-package.schema.json",
Expand Down
13 changes: 13 additions & 0 deletions tests/test_public_data_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ def test_scanner_checks_unlisted_text_and_unquoted_credentials(self):

self.assertTrue(any("credential assignment" in item for item in findings))

def test_audited_binary_requires_exact_path_and_hash(self):
scanner = load_scanner()
relative = Path("docs/assets/pi-steel-demo.gif")
content = (ROOT / relative).read_bytes()

self.assertTrue(scanner.is_audited_public_binary(relative, content))
self.assertFalse(
scanner.is_audited_public_binary(relative, content + b"modified")
)
self.assertFalse(
scanner.is_audited_public_binary(Path("docs/assets/other.gif"), content)
)

def test_scanner_detects_high_confidence_secret_formats_without_echoing_value(self):
scanner = load_scanner()
root = self._fixture_root()
Expand Down
Loading