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
73 changes: 73 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Changelog

All notable changes to `@structupath/pi-steel` are documented here. The format
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.0] - 2026-08-23

### Added

- **`steel-cutlist` skill** — deterministic 1D bar nesting for long products
(beams, channels, angles, HSS, tube, pipe). Packs required member lengths
onto purchasable mill lengths with explicit saw kerf and end-trim
allowances, a documented fit contract, and a strategy portfolio per
designation + grade group (mixed-stock greedy plus each single-stock-length
restriction) ranked by fewest unplaced members, least total stock length,
lowest known cost, then fewest bars.
- Independent post-placement cut-list verification (bar overcommitment,
material mismatch, duplicate or missing member instances) gating
publication; the per-bar `cutting_list.csv` is emitted only for verified,
fully placed runs.
- Member weights from explicit `unit_weight_plf` or the bundled AISC shape
database; unknown weights surface as warnings, never silent zeros.
- Bar diagrams (`layout.pdf`, `bars.png`), purchase summary by stock length,
drop candidates against a reusable threshold, and a versioned
`rfq_linear.json` handoff.
- **Pipeline integration** — `steel-estimate` now optimizes member items
(designation + length, no plate geometry) onto configurable mill lengths
(`--mill-lengths-ft`, default `40,50,60`; `--cutlist-kerf-in`,
`--end-trim-in`, `--min-drop-in`). Cut-list blockers, verifier findings,
and members longer than every mill length gate the run exactly like nest
blockers, publishing `cutlist_partial` diagnostics.
- **RFQ integration** — the draft workbook renders a
"LINEAR STOCK / CUT-LIST REFERENCE" section from a validated linear
handoff; the standalone `generate-rfq.py` accepts `--linear` with schema
and staleness validation against the estimate identity.
- `cutlist-result` schema (`1.0.0`) and the `cutlist_partial` /
`cutlist_verified` package statuses.
- **Linear stock in the canonical contract** — `stock` entries with
`stock_form: "linear"` model vendor-declared purchasable lengths and
on-hand sticks. Declared purchasable lengths replace the default mill
lengths for their designation + grade group; on-hand sticks require the
same hash-bound reviewer confirmation as on-hand plate and enter the
optimizer as finite, cost-free inventory. The portfolio ranks by
purchased length first, so confirmed sticks reduce buying whenever they
genuinely can, and purchase summaries, handoffs, and reports label
`on_hand` rows explicitly.

## [0.2.3] - 2026-07-28

### Fixed

- Corrected shape dataset ownership metadata in the provenance record.

### Added

- Pi gallery preview image, public demo recording, and public project guide.

## [0.2.2] - 2026-07-28

### Added

- Trustworthy estimate package pipeline: canonical estimate contract and
shared validation, grouped nest placement verification, deterministic draft
RFQ compilation, review-gated orchestration with immutable manifested runs,
and release gates for privacy and provenance.
- `steel-nest` plate nesting with guarded burn DXF output.

## [0.1.0] - 2026-07-19

### Added

- Initial release: `steel-takeoff` and `steel-rfq` skills for Pi.
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
[![Pi package](https://img.shields.io/badge/Pi-package-7c3aed)](https://pi.dev/packages?name=pi-steel)

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.
validated takeoffs, plate nesting, linear cut-list optimization, guarded DXF
output, draft vendor RFQs, and a review-gated estimate pipeline.

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

Expand All @@ -30,6 +30,7 @@ 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.
Optimize these beam lengths onto 40/50/60 ft mill lengths.
Prepare a draft RFQ from this estimate.
```

Expand All @@ -44,7 +45,8 @@ npm run doctor
| 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-nest` | Lay out plate parts with kerf, gap, and edge-margin controls | Nest results, reference drawings, and guarded burn DXFs |
| `steel-cutlist` | Optimize member lengths onto purchasable mill lengths (1D bar nesting) | Verified cutting lists, purchase summaries, drop candidates, and bar diagrams |
| `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 |

Expand All @@ -67,6 +69,15 @@ 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.

### Linear cut-lists

`steel-cutlist` packs member lengths (beams, channels, angles, HSS, tube, pipe) onto
purchasable mill lengths with explicit saw kerf and end-trim allowances. A
deterministic strategy portfolio per designation and grade minimizes unplaced
members, total stock length, and known cost, and every bar is independently
re-verified before a cutting list is published. Drops are reported as
candidates for review, never as certified reusable stock.

### Draft RFQs

`steel-rfq` groups material by stock family, carries approved nesting data into
Expand All @@ -89,7 +100,7 @@ completed profile.
```text
estimate input
contract validation → plate nesting → draft RFQ compilation
contract validation → plate nesting + linear cut-lists → draft RFQ compilation
QA findings + lineage + immutable run manifest
Expand Down Expand Up @@ -119,6 +130,7 @@ Keep operational inputs and generated artifacts outside the repository. See the
- [GitHub wiki](https://github.com/StructuPath/pi-steel/wiki)
- [Public data policy](PUBLIC_DATA_POLICY.md)
- [Data provenance](DATA_PROVENANCE.md)
- [Changelog](CHANGELOG.md)
- [Pi package catalog](https://pi.dev/packages?name=pi-steel)
- [npm package](https://www.npmjs.com/package/@structupath/pi-steel)

Expand All @@ -134,7 +146,7 @@ npm run provenance:check # shape-data integrity and recorded decision
npm run release:check # complete release gate
```

The current package version is `0.2.3`. `release:check` verifies the test,
The current package version is `0.3.0`. `release:check` verifies the test,
privacy, package-content, shape-data integrity, ownership, license, and
redistribution contracts before publication.

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@structupath/pi-steel",
"version": "0.2.3",
"description": "Structural steel estimating for Pi validated takeoffs, plate nesting, guarded DXF output, and review-ready RFQ packages.",
"version": "0.3.0",
"description": "Structural steel estimating for Pi \u2014 validated takeoffs, plate nesting, guarded DXF output, and review-ready RFQ packages.",
"type": "module",
"keywords": [
"pi-package",
Expand Down Expand Up @@ -67,6 +67,7 @@
"DATA_PROVENANCE.json",
"docs/assets/pi-steel-demo.gif",
"docs/assets/pi-steel-gallery.webp",
"CHANGELOG.md",
"README.md",
"PUBLIC_DATA_POLICY.md",
"LICENSE"
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pi-steel-runtime"
version = "0.2.3"
version = "0.3.0"
description = "Python runtime dependencies and test configuration for pi-steel"
requires-python = ">=3.11,<3.14"
dependencies = [
Expand Down
10 changes: 9 additions & 1 deletion skills/_shared/pi_steel/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
"""Shared deterministic runtime primitives for pi-steel skills."""

from .cli import StageArgumentParser, package_version, publish_failure_diagnostic
from .cli import (
StageArgumentParser,
missing_optional_modules,
package_version,
publish_failure_diagnostic,
)
from .contracts import (
ESTIMATE_PACKAGE_VERSION,
ITEM_INTENTS,
NEST_RESULT_VERSION,
estimate_input_hash,
instance_ids,
is_sha256,
item_id_for,
placement_ids,
)
Expand Down Expand Up @@ -37,8 +43,10 @@
"canonical_json_bytes",
"estimate_input_hash",
"instance_ids",
"is_sha256",
"item_id_for",
"placement_ids",
"missing_optional_modules",
"outcome_exit_code",
"package_version",
"publish_failure_diagnostic",
Expand Down
12 changes: 11 additions & 1 deletion skills/_shared/pi_steel/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
from __future__ import annotations

import argparse
import importlib.util
import json
import sys
from pathlib import Path
from typing import Any
from typing import Any, Iterable

from .run_manifest import (
ManifestError,
Expand Down Expand Up @@ -84,6 +85,15 @@ def error(self, message):
self.exit(1, f"{self.prog}: error: {message}{suffix}\n")


def missing_optional_modules(module_names: Iterable[str]) -> list[str]:
"""Return the named optional modules unavailable to this interpreter."""
return [
name
for name in module_names
if importlib.util.find_spec(name) is None
]


def package_version(entry_file: str) -> str:
package_path = Path(entry_file).resolve().parents[3] / "package.json"
try:
Expand Down
9 changes: 9 additions & 0 deletions skills/_shared/pi_steel/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@
)


def is_sha256(value: Any) -> bool:
"""Return whether a value is a lowercase hex SHA-256 digest."""
return (
isinstance(value, str)
and len(value) == 64
and all(character in "0123456789abcdef" for character in value)
)


def canonical_json_bytes(value: Any) -> bytes:
return json.dumps(
value, sort_keys=True, separators=(",", ":"), ensure_ascii=False
Expand Down
7 changes: 7 additions & 0 deletions skills/_shared/pi_steel/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
)


def normalize_designation(value: Any) -> str:
"""Canonical AISC-style designation key: uppercase, no spaces."""
if not isinstance(value, str):
return ""
return value.upper().replace(" ", "").strip()


def parse_length_ft(raw: str) -> float:
value = raw.strip()
if "'" not in value:
Expand Down
2 changes: 2 additions & 0 deletions skills/_shared/pi_steel/run_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
"validated",
"nested_partial",
"nest_verified",
"cutlist_partial",
"cutlist_verified",
"rfq_draft_review_required",
"rfq_ready_for_review",
}
Expand Down
45 changes: 45 additions & 0 deletions skills/_shared/pi_steel/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,16 @@ def validate_estimate_package(package: dict[str, Any]) -> ValidationResult:
)

for index, stock in enumerate(package.get("stock", [])):
if stock.get("stock_form") == "linear" and stock.get("unlimited"):
if stock.get("stock_kind") == "on_hand":
_add(
findings,
input_hash,
"unlimited_on_hand_stock",
"blocker",
f"$.stock[{index}].unlimited",
"On-hand stock must be a finite, measured quantity.",
)
if stock.get("stock_kind") != "on_hand":
continue
required = (
Expand Down Expand Up @@ -576,6 +586,41 @@ def eligible_on_hand_stock(package: dict[str, Any]) -> list[dict[str, Any]]:
return eligible


def eligible_on_hand_linear_stock(package: dict[str, Any]) -> list[dict[str, Any]]:
"""Confirmed, available on-hand linear sticks eligible to reduce purchasing."""
input_hash = estimate_input_hash(package)
eligible = []
for stock in package.get("stock", []):
confirmation = stock.get("reviewer_confirmation", {})
if (
stock.get("stock_form") == "linear"
and stock.get("stock_kind") == "on_hand"
and stock.get("inventory_id")
and stock.get("designation")
and stock.get("grade")
and finite_positive(stock.get("length_ft"))
and not stock.get("unlimited")
and stock.get("measured_at")
and stock.get("source")
and stock.get("status") == "available"
and confirmation.get("actor")
and confirmation.get("timestamp")
and confirmation.get("estimate_hash") == input_hash
):
eligible.append(stock)
return eligible


def linear_purchasable_stock(package: dict[str, Any]) -> list[dict[str, Any]]:
"""Vendor-declared purchasable linear stock lengths."""
return [
stock
for stock in package.get("stock", [])
if stock.get("stock_form") == "linear"
and stock.get("stock_kind") == "purchasable"
]


def purchasable_items(package: dict[str, Any]) -> list[dict[str, Any]]:
return [
item
Expand Down
Loading
Loading