Skip to content

Feat/trustworthy estimate package - #1

Merged
Steel-tech merged 15 commits into
mainfrom
feat/trustworthy-estimate-package
Jul 28, 2026
Merged

Feat/trustworthy estimate package#1
Steel-tech merged 15 commits into
mainfrom
feat/trustworthy-estimate-package

Conversation

@Steel-tech

@Steel-tech Steel-tech commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added an end-to-end steel estimating workflow with validation, nesting, inventory tracking, draft RFQ workbooks, and run manifests.
    • Added deterministic artifact publishing, QA reports, lineage tracking, and review-gated outcomes.
    • Added guarded DXF generation for verified rectangular geometry.
    • Added dependency diagnostics and machine-readable failure reporting.
  • Documentation

    • Expanded guidance for estimating, nesting, RFQ compilation, data handling, provenance, and release requirements.
  • Tests

    • Added comprehensive contract, workflow, rendering, packaging, provenance, and public-data validation coverage.

@Steel-tech

Copy link
Copy Markdown
Contributor Author

@copilot, please review this PR

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a deterministic, review-gated “estimate package” workflow across the takeoff → nest → RFQ pipeline, with machine-readable run publication (manifests + QA reports), public-data/provenance guardrails, and extensive contract/CLI/runtime tests to enforce the new semantics.

Changes:

  • Adds shared runtime primitives (pi_steel) for run publication, CLI failure diagnostics, parsing/adapters, and geometry verification.
  • Tightens documentation and tooling boundaries around pricing, draft-only artifacts, and public-data safety (policy + provenance gate).
  • Adds comprehensive pytest/unittest coverage plus synthetic fixtures/goldens and CI jobs (base + full-render).

Reviewed changes

Copilot reviewed 87 out of 89 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/test_takeoff_cli.py CLI regression tests for legacy BOM validation + weight output.
tests/test_structured_failure_cli.py Ensures post-parse failures still publish machine-readable diagnostics.
tests/test_runtime_bootstrap.py Verifies entrypoints run from arbitrary CWD and bootstrap works.
tests/test_run_manifests.py Contract tests for run publication, hashing, allow-lists, rollback.
tests/test_rfq_workbook_contract.py Workbook structure + semantic projection contract tests.
tests/test_rfq_generator.py RFQ normalization, legacy adapter, profile, CLI, and nest-handoff tests.
tests/test_recalc.py Tests recalc-on-open error handling and CLI messaging.
tests/test_public_data_policy.py Tests public-data scanner behavior and gitignore defaults.
tests/test_package_contents.py Verifies npm pack contents and release gate wiring.
tests/test_nest_outputs.py Nest publication + DXF output gating/invariants tests.
tests/test_nest_invariants.py Geometry verifier invariants + performance expectations.
tests/test_nest_engine.py Nest engine behavioral contracts (fit, grouping, cost basis, metrics).
tests/test_nest_cli_contract.py CLI contract tests for nest outcomes and artifacts.
tests/test_nest_burn_guard.py Burn-DXF guard tests for suppression/eligibility conditions.
tests/test_installed_scripts.py Smoke-tests packed artifact runtime and pipeline behavior.
tests/test_full_render_smoke.py Optional full-render smoke suite (LibreOffice + poppler).
tests/test_data_provenance.py Verifies shipped dataset integrity and release blocking.
tests/test_acknowledge_finding_cli.py Tests acknowledgement helper behavior and safety checks.
tests/golden/rfq/semantic-workbook.json Golden semantic projection for RFQ workbook.
tests/golden/pipeline/ready-artifacts.json Golden expected artifact set for “ready” pipeline runs.
tests/fixtures/workflows/README.md Documents synthetic workflow fixture provenance and intent.
tests/fixtures/workflows/mixed_plate_job.json Synthetic mixed rectangular/irregular nesting workflow fixture.
tests/fixtures/workflows/member_only.csv Synthetic member-only takeoff fixture.
tests/fixtures/workflows/blocked_plate_job.json Synthetic blocked nesting fixture (oversize part).
tests/fixtures/workflows/ambiguous_scope.csv Synthetic ambiguous-scope takeoff fixture.
tests/fixtures/rfq/synthetic-profile.json Synthetic company profile fixture for RFQ tests.
tests/fixtures/rfq/README.md Documents RFQ fixture provenance and constraints.
tests/fixtures/rfq/nest-handoff.json Synthetic RFQ nest-handoff contract fixture.
tests/fixtures/rfq/estimate-package.json Canonical estimate-package fixture for RFQ tests.
tests/fixtures/pipeline/synthetic-profile.json Pipeline profile fixture (no logo).
tests/fixtures/pipeline/synthetic-estimate.json Canonical estimate-package fixture for end-to-end pipeline.
tests/fixtures/pipeline/README.md Documents pipeline fixture provenance and constraints.
tests/fixtures/nest/README.md Documents nest fixture provenance and constraints.
tests/fixtures/nest/irregular-reference-only.json Synthetic irregular reference-only nest job fixture.
tests/fixtures/nest/grouped-engine.json Synthetic grouped-material nest fixture for engine contracts.
tests/fixtures/contracts/README.md Documents contract fixtures provenance.
tests/fixtures/contracts/legacy-nest.json Legacy nest-job fixture for adapter/contract coverage.
tests/fixtures/contracts/legacy-bom.csv Legacy BOM fixture for adapter/contract coverage.
skills/steel-takeoff/SKILL.md Updates BOM field contract + pricing boundary documentation.
skills/steel-takeoff/scripts/calculate-weight.sh Removes pricing output; handles unspecified grade explicitly.
skills/steel-takeoff/references/takeoff-procedures.md Removes market-rate language from public guide.
skills/steel-takeoff/assets/bom-template.csv Updates BOM template headers to new field set.
skills/steel-rfq/SKILL.md Rewrites skill doc to deterministic compiler + boundary contract.
skills/steel-rfq/scripts/recalc.py Adds explicit error type + structured CLI semantics.
skills/steel-rfq/references/rfq-input.md Adds explicit RFQ compiler input contract (canonical + legacy).
skills/steel-rfq/assets/company-profile.example.json Replaces example profile with approval-bound terms template shape.
skills/steel-nest/SKILL.md Updates nesting docs for verification, guarded burn output, manifests.
skills/steel-nest/references/job_template.json Updates template to explicit material basis + synthetic public example.
skills/steel-nest/references/FIXTURE_PROVENANCE.md Documents provenance for public synthetic examples.
skills/steel-nest/references/example_job.json Replaces example with synthetic public geometry + explicit basis.
skills/steel-estimate/SKILL.md New orchestrator documentation for review-gated pipeline publication.
skills/steel-estimate/scripts/acknowledge-finding.py New CLI helper to record explicit review acknowledgements safely.
skills/steel-estimate/references/output-contract.md Defines published artifact/outcome contract and readiness semantics.
skills/steel-estimate/references/estimate-package-example.json Adds canonical synthetic estimate-package example.
skills/_shared/schemas/run-manifest.schema.json Adds run-manifest JSON schema contract.
skills/_shared/pi_steel/run_manifest.py Implements isolated run publication + hashing + pointer updates.
skills/_shared/pi_steel/parsing.py Adds legacy adapters for BOM CSV + nest JSON into canonical package.
skills/_shared/pi_steel/geometry_verify.py Adds independent placement verification + grouping helpers.
skills/_shared/pi_steel/contracts.py Adds canonical contract constants + deterministic ID helpers.
skills/_shared/pi_steel/cli.py Adds shared CLI parsing + structured failure diagnostic publication.
skills/_shared/pi_steel/init.py Exposes shared runtime API surface for shipped scripts.
skills/_shared/bootstrap.py Adds relocatable bootstrap to locate shipped skills tree.
scripts/doctor.py Adds runtime capability/dep doctor with machine-readable output.
scripts/check-data-provenance.py Adds provenance integrity + release gate check for shipped dataset.
requirements.txt Adds base Python runtime dependencies.
requirements-tested.txt Records exact dependency set used for tested runtime environment.
requirements-render.txt Adds optional render-tier Python dependencies.
requirements-dev.txt Adds dev/test dependencies.
README.md Updates product boundary, safety, and dev/release check documentation.
pyproject.toml Adds Python project metadata + pytest/ruff config.
PUBLIC_DATA_POLICY.md Adds explicit public-data policy + fixture rules.
package.json Adds lint/test/privacy/provenance/pack/release scripts and files allowlist.
docs/research/v0.3-workflow-evidence.md Adds workflow evidence doc for reliability milestone.
DATA_PROVENANCE.md Adds human-readable dataset provenance + release decision.
DATA_PROVENANCE.json Adds machine-readable dataset provenance + checksum contract.
.npmignore Ensures private/generated artifacts and local configs are excluded from npm.
.gitignore Ensures private/generated artifacts and local configs are excluded from git.
.github/workflows/ci.yml Adds CI for base and full-render suites across Python versions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +18 to +25
def parse_length_ft(raw: str) -> float:
value = raw.strip()
if "'" not in value:
return float(value)
feet, _, inches = value.replace('"', "").partition("'")
return float(feet.replace("-", "").strip()) + (
float(inches.replace("-", "").strip()) / 12 if inches.strip() else 0
)
Comment on lines +17 to +32
def audit() -> dict:
errors: list[str] = []
record = json.loads(PROVENANCE_RECORD_PATH.read_text(encoding="utf-8"))
datasets = record.get("datasets", [])
if len(datasets) != 1:
errors.append("expected exactly one declared shipped dataset")
dataset = {}
else:
dataset = datasets[0]
shapes_path = ROOT / dataset.get("shipped_file", "")
raw = shapes_path.read_bytes()
checksum = hashlib.sha256(raw).hexdigest()
if checksum != dataset.get("sha256"):
errors.append("shape data checksum differs from DATA_PROVENANCE.json")

rows = json.loads(raw)

Copilot AI commented Jul 28, 2026

Copy link
Copy Markdown

@copilot, please review this PR

Here's my review. I found three issues:


🔴 Medium — publish() can orphan a run directory on post-replace failure

File: skills/_shared/pi_steel/run_manifest.py ~line 286

After os.replace(self.staging_path, self.final_path) commits the run directory, both sha256_file(...) and tempfile.mkstemp(...) execute outside the existing try/except rollback block. If either raises (e.g., OSError on a full disk), the run directory is left on disk with _published = False. The __exit__ handler won't clean it up because staging_path no longer exists, and any subsequent call with the same run_id fails with "run already exists".

Fix: Wrap the entire post-replace sequence (hash computation + pointer file write) in a single try block that includes shutil.rmtree(self.final_path) as a rollback step.


🔴 Medium — Duplicate canonical_json_bytes with incompatible behaviour breaks reproducible hashes

Files: skills/_shared/pi_steel/contracts.py:24 and skills/_shared/pi_steel/run_manifest.py:57

contracts.py defines canonical_json_bytes that produces bytes without a trailing newline; run_manifest.py defines the same name with a trailing "\n". __init__.py exports the run_manifest version. All contract identity hashes (content_hash, item_id_for, finding_id_for, estimate_input_hash) are computed with the no-newline variant inside contracts.py, but any caller importing from pi_steel import canonical_json_bytes gets the with-newline variant and will produce a different hash for the same input — breaking external reproducibility.

There is also a concrete inconsistency in cli.py:124: the sentinel input_hash for unavailable inputs is computed with the with-newline version, while all real input_hash values in the same field use the no-newline version from contracts.estimate_input_hash.

Fix: Rename one variant (e.g., canonical_json_file_bytes for the newline-appended version used for file output), update all call sites, and ensure __init__.py exports the contracts/no-newline version for reproducibility.


🟡 Low — parse_length_ft silently converts negative lengths to positive

File: skills/_shared/pi_steel/parsing.py:23

feet.replace("-", "") strips all hyphens from the feet component (intended to remove the separator in 10'-6"), but it also silently removes the minus sign from negative inputs. parse_length_ft("-5'") returns 5.0 instead of raising an error. Because the value is positive and finite, it passes all downstream finite_positive guards with no finding raised — a typo'd negative BOM entry is silently accepted as valid.

Fix: Use feet.rstrip("-") instead of feet.replace("-", "") to only strip a trailing separator hyphen while preserving a leading minus.

@Steel-tech

Copy link
Copy Markdown
Contributor Author

@CodeRabbit, review this project.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

@Steel-tech: I’ll review the changes in #1.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a versioned, review-gated steel estimating workflow with canonical contracts, validated nesting, deterministic draft RFQ compilation, isolated run manifests, provenance and public-data checks, CI automation, and extensive synthetic integration coverage.

Changes

Estimate workflow

Layer / File(s) Summary
Release, provenance, and runtime foundation
.github/workflows/*, scripts/*, skills/_shared/*, package.json, requirements*.txt, README.md, *DATA_PROVENANCE*, PUBLIC_DATA_POLICY.md
Adds CI tiers, runtime diagnostics, public-data scanning, dataset provenance checks, npm package controls, and relocatable shared Python bootstrapping.
Canonical contracts and validation
skills/_shared/pi_steel/*, skills/_shared/schemas/*
Defines versioned estimate, nest-result, and run-manifest contracts with deterministic hashes, identity helpers, geometry checks, review findings, and atomic artifact publication.
Verified nesting and guarded geometry outputs
skills/steel-nest/*
Normalizes jobs, segregates compatible stock, verifies placements, produces versioned RFQ handoffs, reports remnant candidates, and suppresses burn DXFs unless geometry is verified.
Estimate package orchestration
skills/steel-estimate/*
Adds review-gated pipeline orchestration, BOM and inventory projections, acknowledgement handling, deterministic artifacts, and outcome-based publication.
RFQ compiler and takeoff behavior
skills/steel-rfq/*, skills/steel-takeoff/*
Adds deterministic draft-only workbook compilation, profile and nesting-handoff validation, explicit recalculation statuses, and weight-only takeoff outputs.
Synthetic fixtures and workflow verification
tests/fixtures/*, tests/golden/*, tests/test_*.py
Adds synthetic fixtures and tests covering contracts, nesting, manifests, rendering, RFQ workbooks, package contents, provenance, public-data scanning, runtime bootstrap, and end-to-end pipeline outcomes.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Input
  participant SteelEstimate
  participant SteelNest
  participant SteelRFQ
  participant RunPublisher
  Input->>SteelEstimate: load canonical estimate package
  SteelEstimate->>SteelNest: submit eligible plate items and stock
  SteelNest->>SteelEstimate: return verified nest result and RFQ handoff
  SteelEstimate->>SteelRFQ: compile draft workbook from package and handoff
  SteelRFQ->>RunPublisher: write workbook and semantic projection
  SteelEstimate->>RunPublisher: write QA report and run manifest
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the PR’s main theme: building a trustworthy estimate package workflow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/trustworthy-estimate-package

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (8)
skills/steel-takeoff/scripts/calculate-weight.sh-7-7 (1)

7-7: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Usage help still omits Source_ID.

The header now documents Source_ID as the first BOM column, but the usage text printed at Line 20 still lists the old column set. Worth syncing so users generating a CSV from --help output produce a compatible file.

🤖 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 `@skills/steel-takeoff/scripts/calculate-weight.sh` at line 7, Update the
usage/help text in calculate-weight.sh to include Source_ID as the first BOM
column, matching the documented CSV header and ensuring generated CSV files are
compatible.
skills/steel-estimate/scripts/acknowledge-finding.py-136-146 (1)

136-146: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Wrap the os.link failure in AcknowledgementInputError.

The output_path.exists() precheck at Line 66 leaves a race window; if the file appears in between, os.link raises FileExistsError and escapes main's handler, printing a traceback instead of the standard Acknowledgement failed: message.

🛡️ Proposed fix
     try:
         with os.fdopen(file_descriptor, "wb") as stream:
             stream.write(canonical_json_bytes(package))
-        os.link(temporary_name, output_path)
+        try:
+            os.link(temporary_name, output_path)
+        except FileExistsError as exc:
+            raise AcknowledgementInputError(
+                f"output already exists: {output_path}"
+            ) from exc
     finally:
🤖 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 `@skills/steel-estimate/scripts/acknowledge-finding.py` around lines 136 - 146,
Wrap the os.link call in the acknowledgement-writing flow with handling that
converts FileExistsError into AcknowledgementInputError, preserving the original
failure context so main’s existing handler emits the standard “Acknowledgement
failed:” message. Keep the temporary-file cleanup in the finally block
unchanged.
skills/steel-rfq/references/rfq-input.md-22-34 (1)

22-34: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Documented handoff example omits the identity fields the validator requires.

validate_nest_handoff (skills/steel-rfq/scripts/generate-rfq.py Lines 529-546) compares project_id, revision_id, and estimate_input_hash against the estimate package and emits stale_nest_handoff on mismatch. The example object here has none of them, so copying it verbatim always blocks. Add those keys to the snippet and mention the staleness rule alongside the version/boundary rules on Line 33.

🤖 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 `@skills/steel-rfq/references/rfq-input.md` around lines 22 - 34, Update the
versioned rfq_nesting.json example to include project_id, revision_id, and
estimate_input_hash fields required by validate_nest_handoff. Add a concise note
stating these identity values must match the estimate package or the handoff is
rejected as stale, alongside the existing version and boundary rules.
skills/_shared/pi_steel/validation.py-447-471 (1)

447-471: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

reserved on-hand stock passes validation but is silently dropped from eligible stock.

Line 454 treats status in {"available", "reserved"} as confirmed, so no blocker or warning is raised, yet eligible_on_hand_stock (Line 570) requires status == "available" and excludes it. The estimator gets a clean validated package while the reserved inventory is silently ignored in consumption. Either warn on reserved on-hand stock or align the two predicates.

🛡️ Proposed fix — surface reserved stock explicitly
-        required = (
-            stock.get("inventory_id"),
-            stock.get("measured_at"),
-            stock.get("source"),
-            stock.get("status") in {"available", "reserved"},
-        )
+        required = (
+            stock.get("inventory_id"),
+            stock.get("measured_at"),
+            stock.get("source"),
+            stock.get("status") == "available",
+        )

Or keep reserved acceptable and emit a reserved_on_hand_stock warning so the exclusion from consumption is visible.

Also applies to: 557-576

🤖 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 `@skills/_shared/pi_steel/validation.py` around lines 447 - 471, Align the
on-hand stock validation near the confirmation predicate with the
eligible_on_hand_stock filtering logic so reserved stock is not silently
ignored. Prefer preserving reserved as valid while adding a
reserved_on_hand_stock warning for confirmed reserved entries, and ensure the
warning points to the stock item without changing available-stock consumption
behavior.
skills/steel-nest/references/job_template.json-29-29 (1)

29-29: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

_comment_holes promises burn-DXF geometry unconditionally.

render_burn_dxfs emits nothing unless burn_dxf_eligible is true, and this template itself contains an irregular part (Part B), which suppresses burn output. Align the wording with the gate documented in skills/steel-nest/SKILL.md.

📝 Proposed wording
-  "_comment_holes": "Optional per-part \"holes\": each hole's x,y is its CENTER measured from the part's lower-left corner. Round hole = {\"dia\": D, \"x\":, \"y\":}. Rectangular cutout = {\"w\":, \"h\":, \"x\":, \"y\":}. Holes are subtracted from weight/cost, rotate with the part, and are cut as real geometry on the HOLES layer of the burn-table DXF.",
+  "_comment_holes": "Optional per-part \"holes\": each hole's x,y is its CENTER measured from the part's lower-left corner. Round hole = {\"dia\": D, \"x\":, \"y\":}. Rectangular cutout = {\"w\":, \"h\":, \"x\":, \"y\":}. Holes are subtracted from weight/cost, rotate with the part, and appear on the HOLES layer of the burn-table DXF only when the whole job passes the burn gate (all parts rectangular, all placed, all holes in bounds).",
🤖 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 `@skills/steel-nest/references/job_template.json` at line 29, Update the
_comment_holes description in job_template.json to qualify burn-table DXF
geometry by the burn_dxf_eligible gate, matching the behavior documented in
SKILL.md. Keep the hole geometry, rotation, and weight/cost wording intact, but
avoid promising HOLES-layer output when burn DXF generation is ineligible, such
as for irregular parts.
skills/steel-nest/SKILL.md-92-94 (1)

92-94: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Qualify the doctor script path
python3 scripts/nest.py is skill-relative, so python3 scripts/doctor.py points at skills/steel-nest/scripts/doctor.py instead of the repo-root scripts/doctor.py. Use an explicit repo-root path or note the required working directory.

🤖 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 `@skills/steel-nest/SKILL.md` around lines 92 - 94, Update the dependency and
doctor-script instructions in SKILL.md to qualify the doctor.py location from
the repository root, avoiding the skill-relative scripts/doctor.py resolution;
use an explicit repo-root path or clearly specify the required working directory
while preserving the existing optional formula-baking behavior.
skills/_shared/pi_steel/__init__.py-26-47 (1)

26-47: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Sort __all__ to satisfy Ruff RUF022.

The active static analysis flags this export list as unsorted; alphabetize it so lint remains green.

🤖 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 `@skills/_shared/pi_steel/__init__.py` around lines 26 - 47, Sort the export
names in __all__ alphabetically to satisfy Ruff RUF022, preserving every
existing exported symbol and its spelling.

Source: Linters/SAST tools

skills/_shared/pi_steel/cli.py-87-92 (1)

87-92: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

.parents[3] can raise an uncaught IndexError in an error-handling helper.

The index access happens outside the try/except. Since this helper is invoked precisely while handling other failures (CLI usage errors, failure diagnostics), an out-of-range path would raise an unrelated IndexError and mask the original error instead of degrading to "unknown".

🛠️ Proposed fix
 def package_version(entry_file: str) -> str:
-    package_path = Path(entry_file).resolve().parents[3] / "package.json"
     try:
+        package_path = Path(entry_file).resolve().parents[3] / "package.json"
         return json.loads(package_path.read_text(encoding="utf-8"))["version"]
-    except (OSError, KeyError, json.JSONDecodeError):
+    except (OSError, KeyError, IndexError, json.JSONDecodeError):
         return "unknown"
🤖 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 `@skills/_shared/pi_steel/cli.py` around lines 87 - 92, Move the package_path
construction inside the try block in package_version so an out-of-range
Path.parents access is handled by the existing fallback. Preserve the current
JSON loading behavior and return "unknown" for IndexError alongside the existing
filesystem, key, and JSON errors.
🧹 Nitpick comments (17)
skills/_shared/schemas/nest-result.schema.json (1)

33-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

outcome and run_outcome duplicate the same enum with no documented precedence.

outcome is required, run_outcome optional, and both accept identical values. Consumers can't tell which wins if they disagree. Document the relationship in the schema (description) or drop the redundant field.

🤖 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 `@skills/_shared/schemas/nest-result.schema.json` around lines 33 - 50, Clarify
the relationship between the required outcome and optional run_outcome
properties in the schema by adding descriptions that define their roles and
precedence when values differ, or remove run_outcome if it is not independently
necessary. Keep the enum values unchanged.
skills/_shared/pi_steel/geometry_verify.py (1)

78-83: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unify the placement finiteness predicate.

Line 178-183 rejects bool values, but _finite_placement_values (Line 79-83) accepts them, so a w: true placement is reported as nonfinite_placement yet still participates in overlap candidate generation. Share one predicate to keep the two paths consistent.

♻️ Proposed refactor
+def _finite_number(value: Any) -> bool:
+    return (
+        isinstance(value, (int, float))
+        and not isinstance(value, bool)
+        and math.isfinite(value)
+    )
+
+
 def _finite_placement_values(placement: dict[str, Any]) -> bool:
-    return all(
-        isinstance(placement.get(field), (int, float))
-        and math.isfinite(placement[field])
-        for field in ("x", "y", "w", "h")
-    )
+    return all(_finite_number(placement.get(field)) for field in ("x", "y", "w", "h"))
-            if not all(
-                isinstance(value, (int, float))
-                and not isinstance(value, bool)
-                and math.isfinite(value)
-                for value in values
-            ):
+            if not all(_finite_number(value) for value in values):

Also applies to: 178-183

🤖 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 `@skills/_shared/pi_steel/geometry_verify.py` around lines 78 - 83, Unify
placement validation by making _finite_placement_values use the same type
predicate as the validation logic around the nonfinite_placement check,
explicitly rejecting bool values while accepting finite numeric coordinates and
dimensions. Reuse the shared predicate in both overlap candidate generation and
validation so placements cannot pass one path and fail the other.
skills/steel-nest/SKILL.md (1)

80-80: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Artifact list understates when rfq_nesting.json is absent.

publish_nest_run writes it only for ready/review_required, so it is also missing for dependency_missing and internal-error runs, not just blocked ones.

🤖 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 `@skills/steel-nest/SKILL.md` at line 80, Update the rfq_nesting.json artifact
description in the artifact list to state that it is absent for
dependency_missing and internal-error runs as well as blocked runs, while
preserving that it is written for ready and review_required runs.
skills/_shared/schemas/run-manifest.schema.json (1)

119-122: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Constrain artifact.path to a relative POSIX path.

The publisher enforces "no absolute, no .., not a reserved name", but the schema only requires minLength: 1, so any consumer validating a third-party manifest gets no traversal protection. Mirror the runtime rule in the contract:

🛡️ Proposed schema tightening
         "path": {
           "type": "string",
-          "minLength": 1
+          "minLength": 1,
+          "pattern": "^(?!/)(?!.*(^|/)\\.\\.(/|$))[^\\\\]+$"
         },

Note JSON Schema pattern uses ECMA-262 regex; verify lookahead support in the validator in use.

🤖 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 `@skills/_shared/schemas/run-manifest.schema.json` around lines 119 - 122,
Update the artifact.path schema definition to enforce the publisher’s relative
POSIX path rules: reject absolute paths, any .. traversal segment, and reserved
path names while retaining the non-empty string constraint. Use an
ECMA-262-compatible pattern supported by the project’s JSON Schema validator,
and change only the path validation under the visible path property.
skills/steel-nest/scripts/nest.py (2)

1118-1123: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Group utilization is re-derived from an already-rounded percentage.

pr["packing_utilization_pct"]["value"] is rounded to one decimal in _metric, so packing_area here is a lossy reconstruction and the grouped utilization inherits that error. Carry the raw packed area (or raw ratio) on the plate report and aggregate that instead.

🤖 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 `@skills/steel-nest/scripts/nest.py` around lines 1118 - 1123, Update the
plate-report generation and group utilization aggregation around _metric to
retain raw packed area or ratio data in each plate report; calculate
packing_area from that unrounded value instead of
pr["packing_utilization_pct"]["value"], while preserving the existing rounded
output format for the final utilization.

1609-1616: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Burn artifacts are registered on outcome == "ready" but written on burn_dxf_eligible.

Today the two conditions are equivalent, so this works; if either predicate drifts, render_burn_dxfs returns [] while the paths are already registered and publisher.publish() fails with an allow-list mismatch (or, in the reverse direction, files land unregistered). Gate registration on the same flag the renderer uses.

♻️ Suggested tightening
-            if outcome == "ready":
+            if outcome == "ready" and result["burn_dxf_eligible"]:
                 for plate in result["plate_reports"]:
🤖 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 `@skills/steel-nest/scripts/nest.py` around lines 1609 - 1616, Update the burn
artifact registration block near render_burn_dxfs to use the same
burn_dxf_eligible predicate that controls rendering, instead of checking outcome
== "ready". Keep registration, rendering, and publishing aligned so only
artifacts actually produced by render_burn_dxfs are registered.
skills/_shared/pi_steel/run_manifest.py (1)

285-314: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

latest-run.json update is unsynchronized and non-durable.

Two concurrent publishes can interleave between os.replace(staging_path, final_path) and the pointer swap, leaving the pointer on the older run even though both directories exist. Also, neither the run directory nor destination is fsynced after the rename, so a crash can leave a pointer that references a manifest whose bytes are not yet on disk. If concurrent stage invocations against one publication root are expected, consider an advisory lock file (or O_EXCL sentinel) around the rename + pointer swap, plus os.fsync of the directory fd before returning.

🤖 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 `@skills/_shared/pi_steel/run_manifest.py` around lines 285 - 314, Synchronize
the publish flow surrounding self.final_path renaming and latest-run.json
replacement with an advisory lock or O_EXCL sentinel scoped to the publication
destination, so concurrent publishes cannot interleave. Hold the lock through
rollback and pointer replacement, then fsync the published run directory and
destination directory before returning to ensure the pointer references durable
manifest bytes. Preserve the existing rollback and temporary-pointer cleanup
behavior.
requirements-dev.txt (1)

5-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update Ruff pins in both requirements files ruff>=0.11,<0.12 and ruff==0.11.13 are both behind the current 0.16.0 release. Raise the ceiling and exact pin together so the dev and tested environments stay aligned.

🤖 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 `@requirements-dev.txt` at line 5, Update Ruff in requirements-dev.txt at lines
5-5 to allow the current 0.16.0 release, and update the exact Ruff pin in
requirements-tested.txt at lines 6-6 to 0.16.0 so both development and tested
environments remain aligned.
tests/fixtures/rfq/synthetic-profile.json (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document tests/fixtures/rfq/synthetic-profile.json as the missing-logo case.
It already feeds RFQ tests that expect the text_fallback logo path, so a short note in tests/fixtures/rfq/README.md would make the scenario explicit.

🤖 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 `@tests/fixtures/rfq/synthetic-profile.json` at line 4, Add a brief note to the
RFQ fixtures README identifying synthetic-profile.json as the
missing-optional-logo fixture and documenting that it exercises the
text_fallback logo behavior; leave the fixture data unchanged.
tests/test_data_provenance.py (1)

27-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert on parsed JSON instead of formatted substring. Line 36 couples the test to the script's indent=2 formatting; parsing stdout is equally cheap and survives formatting changes.

♻️ Parse the report
         self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
-        self.assertIn('"redistribution_permission": "unverified"', result.stdout)
+        report = json.loads(result.stdout)
+        self.assertEqual(report["redistribution_permission"], "unverified")

(add import json)

🤖 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 `@tests/test_data_provenance.py` around lines 27 - 36, Update
test_release_check_blocks_unverified_redistribution to import json and parse
result.stdout as JSON, then assert the parsed report contains
redistribution_permission equal to "unverified" instead of checking a formatted
substring. Preserve the existing return-code assertion and failure diagnostics.
tests/test_estimate_pipeline.py (2)

49-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Failure diagnostics get masked here. If the CLI exits before publishing (import error, argparse failure), Line 73 raises FileNotFoundError and completed.stdout/stderr are never shown, making failures hard to diagnose.

♻️ Surface the CLI output when no run was published
-    pointer = json.loads((output / "latest-run.json").read_text())
+    pointer_path = output / "latest-run.json"
+    assert pointer_path.exists(), completed.stdout + completed.stderr
+    pointer = json.loads(pointer_path.read_text())
     return completed, output / pointer["run_directory"]
🤖 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 `@tests/test_estimate_pipeline.py` around lines 49 - 74, Update the test helper
around the subprocess invocation and latest-run pointer loading to check whether
the CLI completed successfully and published the expected output before reading
latest-run.json. When no run is published, surface completed.stdout and
completed.stderr in the failure instead of allowing an uninformative
FileNotFoundError; preserve the existing pointer-based return path for
successful runs.

253-272: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Line 272 is tautological. blocked_path is built from the same pointer read inside run_pipeline, so this comparison always holds. Assert on the run id (or that the pointer no longer references ready_path) to actually prove the pointer was replaced.

🤖 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 `@tests/test_estimate_pipeline.py` around lines 253 - 272, The assertion
comparing the published pointer-derived path to blocked_path in
test_blocked_run_replaces_latest_pointer_without_reusing_ready_artifacts is
tautological because run_pipeline already uses that pointer. Assert instead that
pointer["run_directory"] identifies the blocked run or that the published
pointer no longer references ready_path, while preserving the existing
blocked-run and artifact assertions.
tests/test_full_render_smoke.py (2)

176-182: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Duplicate words collapse silently in word_positions. Keying by uppercase text keeps only the last occurrence's yMin, so if "TOTAL"/"TERMS"/"RESPONSE" appear more than once in the rendered sheet, Lines 180-182 assert ordering against an arbitrary occurrence (and raise KeyError if a label is missing/split across words). Collect all occurrences and compare a well-defined extreme instead.

♻️ Compare min/max positions per label
-    word_positions = {
-        (word.text or "").upper(): float(word.attrib["yMin"]) for word in words
-    }
-    assert word_positions["REQUEST"] < page_height * 0.2
-    assert word_positions["RESPONSE"] < word_positions["TOTAL"]
-    assert word_positions["TOTAL"] < word_positions["TERMS"]
-    assert word_positions["TERMS"] < page_height * 0.95
+    positions: dict[str, list[float]] = {}
+    for word in words:
+        positions.setdefault((word.text or "").upper(), []).append(
+            float(word.attrib["yMin"])
+        )
+    for label in ("REQUEST", "RESPONSE", "TOTAL", "TERMS"):
+        assert positions.get(label), f"{label} missing from rendered PDF"
+    assert min(positions["REQUEST"]) < page_height * 0.2
+    assert min(positions["RESPONSE"]) < min(positions["TOTAL"])
+    assert min(positions["TOTAL"]) < max(positions["TERMS"])
+    assert max(positions["TERMS"]) < page_height * 0.95
🤖 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 `@tests/test_full_render_smoke.py` around lines 176 - 182, Update the
word-position collection in the smoke test to retain every occurrence of each
uppercase label instead of overwriting duplicates in a single-value dictionary.
Use the collected positions to compare a defined extreme, such as each label’s
minimum yMin, while preserving the existing ordering and page-boundary
assertions for REQUEST, RESPONSE, TOTAL, and TERMS.

63-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Environment isolation differs from the other pipeline harness. tests/test_estimate_pipeline.py pops PYTHONPATH and pins XDG_CONFIG_HOME before invoking the same script; here the full ambient environment is inherited, so a developer's PYTHONPATH/user config can mask runtime bootstrap regressions in the render path.

♻️ Match the isolation used elsewhere
     environment = os.environ.copy()
+    environment.pop("PYTHONPATH", None)
+    environment["XDG_CONFIG_HOME"] = str(tmp_path / "empty-config")
     environment["PI_STEEL_CONFIG"] = str(profile_path)
🤖 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 `@tests/test_full_render_smoke.py` around lines 63 - 65, Update the environment
setup in the full-render smoke test around the environment variable preparation
to match tests/test_estimate_pipeline.py: remove PYTHONPATH from the copied
environment and pin XDG_CONFIG_HOME to the test-controlled configuration
location before invoking the pipeline script. Preserve the existing
PI_STEEL_CONFIG assignment and other test behavior.
tests/test_acknowledge_finding_cli.py (1)

108-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test name promises more than it asserts. It verifies the warning stays active but never checks the rejected acknowledgement was recorded in the output package.

♻️ Assert the recorded disposition
     reviewed = json.loads(output.read_text(encoding="utf-8"))
+    assert reviewed["review"]["acknowledgements"][-1]["disposition"] == "rejected"
     assert any(
🤖 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 `@tests/test_acknowledge_finding_cli.py` around lines 108 - 127, Update
test_rejected_decision_is_recorded_without_clearing_warning to assert that the
reviewed package records an acknowledgement for the target finding with
disposition “rejected”, in addition to verifying the finding remains active.
tests/test_contracts.py (2)

486-503: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reorder-stability assertion is vacuous. forward == list(reversed(reverse)) holds by construction of the two comprehensions for any pure function of source; it proves nothing about ordering independence. Consider asserting the ids against a set/dict built from a shuffled input instead.

🤖 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 `@tests/test_contracts.py` around lines 486 - 503, Replace the vacuous
forward/reverse list comparison in
test_identifiers_are_reorder_stable_and_quantity_expansion_is_predictable with
an order-independent assertion that maps each source to its generated identifier
and compares results from shuffled or reordered input. Keep the existing
revision-equivalence and quantity-expansion assertions unchanged.

544-559: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assertion doesn't verify the stated behavior. The test name claims the explicit cost "preserves basis", but it only asserts one code is absent — it would pass even if the whole package were rejected. Note the extra "synthetic": True key (Line 555): if commercial_basis.costs entries are closed-typed like dimensions (Lines 163-166), this package is schema-invalid and the test still passes silently.

♻️ Tighten the assertion
     result = validate_estimate_package(package)
-    assert "invalid_cost_basis" not in blocker_codes(result)
+    assert "schema_validation" not in blocker_codes(result)
+    assert result.status == "review_required"
+    assert package["commercial_basis"]["costs"][0]["unit_basis"] == "per_pound"
🤖 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 `@tests/test_contracts.py` around lines 544 - 559, Strengthen
test_absent_costs_do_not_create_prices_and_explicit_cost_preserves_basis by
asserting the package validates successfully, not merely that invalid_cost_basis
is absent. Remove the synthetic field from the appended cost entry if cost
entries are closed-typed, and assert the resulting validation status or blocker
set demonstrates that the explicit per_pound basis is preserved.
🤖 Prompt for all review comments with 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.

Inline comments:
In @.gitignore:
- Around line 10-24: Add the outputs/ directory pattern to the local/private
generated-data ignore entries in .gitignore, alongside private/, local-data/,
and customer-data/, so generated output artifacts are excluded from Git.

In `@scripts/check-data-provenance.py`:
- Around line 54-64: Update the documented_values tuple in the provenance
consistency check to derive the redistribution-permission entry from the
existing permission variable instead of hardcoding “Unverified”. Preserve the
current validation of claimed_edition, expected_rows, and sha256 while ensuring
changes to redistribution_permission are detected.
- Around line 19-36: Update the dataset validation flow after checking
len(datasets) in the provenance-checking function: when the record does not
contain exactly one dataset, report the existing field-specific error and stop
before constructing shapes_path or reading bytes. Preserve normal checksum and
row validation when exactly one dataset is present, and ensure malformed records
return the collected diagnostics rather than raising a filesystem exception.

In `@scripts/check-public-data.py`:
- Line 41: Correct the “local absolute path” regex in the public-data checks to
match single backslashes in normal Windows paths such as C:\Users\alice, while
preserving the existing Unix path matching.

In `@skills/_shared/pi_steel/contracts.py`:
- Around line 69-79: Update estimate_input_projection to remove
reviewer_confirmation only when stock is a list and each entry is an object;
otherwise preserve the malformed JSON value unchanged so estimate_input_hash
remains computable and schema validation can report the schema_validation
blocker.

In `@skills/_shared/pi_steel/parsing.py`:
- Around line 18-25: Update parse_length_ft and the CSV field parsing around the
quantity, length, and unit-weight handling to tolerate blank or non-numeric
cells instead of raising ValueError. Preserve missing or invalid values as None
or raw text so validation.py can emit invalid_quantity, invalid_member_length,
and invalid_unit_weight findings; ensure present-but-empty cells receive the
same treatment as missing values.
- Around line 79-105: The legacy BOM adaptation in adapt_legacy_bom_csv adds
fabricated-part-only fields to every intent, causing schema rejection for
non-fabricated variants. In skills/_shared/pi_steel/parsing.py lines 79-105,
gate designation, length_ft, unit_weight_plf, connections, notes, and
total_weight_lbs on intent == fabricated_part; place identity_warning on the
shared itemBase-compatible structure if it must apply across intents. In
skills/_shared/schemas/estimate-package.schema.json lines 166-222, verify the
resulting placement matches the shared schema and requires no non-fabricated
variant properties.
- Around line 179-196: The legacy stock mapping in the parsing flow should
preserve unlimited inventory instead of converting it to quantity 1. Update the
stock objects created in the legacy stock loop to carry the unlimited flag
through the canonical stock model, while retaining existing quantity behavior
for finite stock and using the established canonical field name.

In `@skills/_shared/pi_steel/validation.py`:
- Around line 107-112: Update the package dependency configuration for
jsonschema to include its format extra, or explicitly add rfc3339-validator.
Keep _schema_validator using jsonschema.FormatChecker() so date and date-time
fields such as estimate_as_of, measured_at, and timestamp reject invalid values.

In `@skills/steel-estimate/scripts/build-estimate-package.py`:
- Around line 89-102: Update the findings and acknowledgements sort keys in
normalized_package to handle non-dict entries safely, matching the defensive
behavior used for items and stock. Ensure malformed entries such as strings do
not raise AttributeError, allowing validation-blocked packages to continue
producing their normal blocked-run or failure diagnostic.
- Around line 464-473: Update the profile blocking decision near
profile_findings so only findings whose severity is “error” set profile_blocked
to true; warning-level findings must remain non-blocking and follow the
documented review_required output path while preserving their warning severity
in findings.
- Around line 809-830: Extend the top-level exception handling around
build_pipeline in the main execution flow to catch RfqInputError and
ManifestError alongside the existing failure types. Ensure these errors use the
same publish_failure_diagnostic and stderr reporting path, preserving the
current return code and diagnostic-publication fallback behavior.

In `@skills/steel-nest/scripts/nest.py`:
- Around line 858-871: Update the per-part aggregation around parts_on and
part_net_cost to key entries by each placement’s item_id instead of
placement.label, preserving separate buckets for distinct normalized items.
Carry the corresponding display label alongside each keyed entry, then update
render_text and affected golden fixtures to display labels while retaining
item_id-based aggregation and separate rows for matching names.
- Around line 1648-1667: Add ManifestError to the pi_steel imports and include
it in the exception tuple around publish_nest_run in the main failure handler,
so manifest-related publication failures use publish_failure_diagnostic, emit
the controlled error message, and return exit code 1 instead of escaping as
tracebacks.

In `@skills/steel-rfq/scripts/generate-rfq.py`:
- Around line 406-415: Validate the result of json.loads in the profile-loading
loop before assigning _profile_path, and raise RfqInputError for arrays or
scalar JSON values. Keep valid object profiles returning with their source
unchanged so publish_rfq_run and main continue handling malformed profiles
through their existing diagnostic paths.
- Around line 339-357: Normalize and validate legacy Thickness in the workbook
adapter before constructing each item, rejecting free-text or otherwise
non-numeric values rather than passing them through. Ensure accepted values use
one consistent numeric representation so the item sort and compile_workbook
grouping sort can compare them with the -1 fallback without TypeError, while
preserving the adapter’s no-guessing contract.
- Around line 1197-1215: Extend the structured exception handling around
publish_rfq_run in the main generation flow to catch RecalculationError
alongside the existing OSError, JSON decode, and input errors. Ensure this
exception follows the same publish_failure_diagnostic, stderr reporting, and
exit-1 path, preserving the documented failure behavior.

In `@tests/test_installed_scripts.py`:
- Line 25: Update the test around package.extractall to remain compatible with
Python 3.11: use the filter argument only on Python 3.12 and newer, while using
the existing compatible extraction path on older supported interpreters.
Preserve the test’s extraction behavior across the supported Python range.

---

Minor comments:
In `@skills/_shared/pi_steel/__init__.py`:
- Around line 26-47: Sort the export names in __all__ alphabetically to satisfy
Ruff RUF022, preserving every existing exported symbol and its spelling.

In `@skills/_shared/pi_steel/cli.py`:
- Around line 87-92: Move the package_path construction inside the try block in
package_version so an out-of-range Path.parents access is handled by the
existing fallback. Preserve the current JSON loading behavior and return
"unknown" for IndexError alongside the existing filesystem, key, and JSON
errors.

In `@skills/_shared/pi_steel/validation.py`:
- Around line 447-471: Align the on-hand stock validation near the confirmation
predicate with the eligible_on_hand_stock filtering logic so reserved stock is
not silently ignored. Prefer preserving reserved as valid while adding a
reserved_on_hand_stock warning for confirmed reserved entries, and ensure the
warning points to the stock item without changing available-stock consumption
behavior.

In `@skills/steel-estimate/scripts/acknowledge-finding.py`:
- Around line 136-146: Wrap the os.link call in the acknowledgement-writing flow
with handling that converts FileExistsError into AcknowledgementInputError,
preserving the original failure context so main’s existing handler emits the
standard “Acknowledgement failed:” message. Keep the temporary-file cleanup in
the finally block unchanged.

In `@skills/steel-nest/references/job_template.json`:
- Line 29: Update the _comment_holes description in job_template.json to qualify
burn-table DXF geometry by the burn_dxf_eligible gate, matching the behavior
documented in SKILL.md. Keep the hole geometry, rotation, and weight/cost
wording intact, but avoid promising HOLES-layer output when burn DXF generation
is ineligible, such as for irregular parts.

In `@skills/steel-nest/SKILL.md`:
- Around line 92-94: Update the dependency and doctor-script instructions in
SKILL.md to qualify the doctor.py location from the repository root, avoiding
the skill-relative scripts/doctor.py resolution; use an explicit repo-root path
or clearly specify the required working directory while preserving the existing
optional formula-baking behavior.

In `@skills/steel-rfq/references/rfq-input.md`:
- Around line 22-34: Update the versioned rfq_nesting.json example to include
project_id, revision_id, and estimate_input_hash fields required by
validate_nest_handoff. Add a concise note stating these identity values must
match the estimate package or the handoff is rejected as stale, alongside the
existing version and boundary rules.

In `@skills/steel-takeoff/scripts/calculate-weight.sh`:
- Line 7: Update the usage/help text in calculate-weight.sh to include Source_ID
as the first BOM column, matching the documented CSV header and ensuring
generated CSV files are compatible.

---

Nitpick comments:
In `@requirements-dev.txt`:
- Line 5: Update Ruff in requirements-dev.txt at lines 5-5 to allow the current
0.16.0 release, and update the exact Ruff pin in requirements-tested.txt at
lines 6-6 to 0.16.0 so both development and tested environments remain aligned.

In `@skills/_shared/pi_steel/geometry_verify.py`:
- Around line 78-83: Unify placement validation by making
_finite_placement_values use the same type predicate as the validation logic
around the nonfinite_placement check, explicitly rejecting bool values while
accepting finite numeric coordinates and dimensions. Reuse the shared predicate
in both overlap candidate generation and validation so placements cannot pass
one path and fail the other.

In `@skills/_shared/pi_steel/run_manifest.py`:
- Around line 285-314: Synchronize the publish flow surrounding self.final_path
renaming and latest-run.json replacement with an advisory lock or O_EXCL
sentinel scoped to the publication destination, so concurrent publishes cannot
interleave. Hold the lock through rollback and pointer replacement, then fsync
the published run directory and destination directory before returning to ensure
the pointer references durable manifest bytes. Preserve the existing rollback
and temporary-pointer cleanup behavior.

In `@skills/_shared/schemas/nest-result.schema.json`:
- Around line 33-50: Clarify the relationship between the required outcome and
optional run_outcome properties in the schema by adding descriptions that define
their roles and precedence when values differ, or remove run_outcome if it is
not independently necessary. Keep the enum values unchanged.

In `@skills/_shared/schemas/run-manifest.schema.json`:
- Around line 119-122: Update the artifact.path schema definition to enforce the
publisher’s relative POSIX path rules: reject absolute paths, any .. traversal
segment, and reserved path names while retaining the non-empty string
constraint. Use an ECMA-262-compatible pattern supported by the project’s JSON
Schema validator, and change only the path validation under the visible path
property.

In `@skills/steel-nest/scripts/nest.py`:
- Around line 1118-1123: Update the plate-report generation and group
utilization aggregation around _metric to retain raw packed area or ratio data
in each plate report; calculate packing_area from that unrounded value instead
of pr["packing_utilization_pct"]["value"], while preserving the existing rounded
output format for the final utilization.
- Around line 1609-1616: Update the burn artifact registration block near
render_burn_dxfs to use the same burn_dxf_eligible predicate that controls
rendering, instead of checking outcome == "ready". Keep registration, rendering,
and publishing aligned so only artifacts actually produced by render_burn_dxfs
are registered.

In `@skills/steel-nest/SKILL.md`:
- Line 80: Update the rfq_nesting.json artifact description in the artifact list
to state that it is absent for dependency_missing and internal-error runs as
well as blocked runs, while preserving that it is written for ready and
review_required runs.

In `@tests/fixtures/rfq/synthetic-profile.json`:
- Line 4: Add a brief note to the RFQ fixtures README identifying
synthetic-profile.json as the missing-optional-logo fixture and documenting that
it exercises the text_fallback logo behavior; leave the fixture data unchanged.

In `@tests/test_acknowledge_finding_cli.py`:
- Around line 108-127: Update
test_rejected_decision_is_recorded_without_clearing_warning to assert that the
reviewed package records an acknowledgement for the target finding with
disposition “rejected”, in addition to verifying the finding remains active.

In `@tests/test_contracts.py`:
- Around line 486-503: Replace the vacuous forward/reverse list comparison in
test_identifiers_are_reorder_stable_and_quantity_expansion_is_predictable with
an order-independent assertion that maps each source to its generated identifier
and compares results from shuffled or reordered input. Keep the existing
revision-equivalence and quantity-expansion assertions unchanged.
- Around line 544-559: Strengthen
test_absent_costs_do_not_create_prices_and_explicit_cost_preserves_basis by
asserting the package validates successfully, not merely that invalid_cost_basis
is absent. Remove the synthetic field from the appended cost entry if cost
entries are closed-typed, and assert the resulting validation status or blocker
set demonstrates that the explicit per_pound basis is preserved.

In `@tests/test_data_provenance.py`:
- Around line 27-36: Update test_release_check_blocks_unverified_redistribution
to import json and parse result.stdout as JSON, then assert the parsed report
contains redistribution_permission equal to "unverified" instead of checking a
formatted substring. Preserve the existing return-code assertion and failure
diagnostics.

In `@tests/test_estimate_pipeline.py`:
- Around line 49-74: Update the test helper around the subprocess invocation and
latest-run pointer loading to check whether the CLI completed successfully and
published the expected output before reading latest-run.json. When no run is
published, surface completed.stdout and completed.stderr in the failure instead
of allowing an uninformative FileNotFoundError; preserve the existing
pointer-based return path for successful runs.
- Around line 253-272: The assertion comparing the published pointer-derived
path to blocked_path in
test_blocked_run_replaces_latest_pointer_without_reusing_ready_artifacts is
tautological because run_pipeline already uses that pointer. Assert instead that
pointer["run_directory"] identifies the blocked run or that the published
pointer no longer references ready_path, while preserving the existing
blocked-run and artifact assertions.

In `@tests/test_full_render_smoke.py`:
- Around line 176-182: Update the word-position collection in the smoke test to
retain every occurrence of each uppercase label instead of overwriting
duplicates in a single-value dictionary. Use the collected positions to compare
a defined extreme, such as each label’s minimum yMin, while preserving the
existing ordering and page-boundary assertions for REQUEST, RESPONSE, TOTAL, and
TERMS.
- Around line 63-65: Update the environment setup in the full-render smoke test
around the environment variable preparation to match
tests/test_estimate_pipeline.py: remove PYTHONPATH from the copied environment
and pin XDG_CONFIG_HOME to the test-controlled configuration location before
invoking the pipeline script. Preserve the existing PI_STEEL_CONFIG assignment
and other test behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bf9c2878-c70c-4c6b-ad0d-afc2dfb0feac

📥 Commits

Reviewing files that changed from the base of the PR and between 49bd076 and 7102481.

⛔ Files ignored due to path filters (4)
  • skills/steel-takeoff/assets/bom-template.csv is excluded by !**/*.csv
  • tests/fixtures/contracts/legacy-bom.csv is excluded by !**/*.csv
  • tests/fixtures/workflows/ambiguous_scope.csv is excluded by !**/*.csv
  • tests/fixtures/workflows/member_only.csv is excluded by !**/*.csv
📒 Files selected for processing (85)
  • .github/workflows/ci.yml
  • .gitignore
  • .npmignore
  • DATA_PROVENANCE.json
  • DATA_PROVENANCE.md
  • PUBLIC_DATA_POLICY.md
  • README.md
  • docs/plans/2026-07-28-001-feat-trustworthy-estimate-package-plan.md
  • docs/research/v0.3-workflow-evidence.md
  • package.json
  • pyproject.toml
  • requirements-dev.txt
  • requirements-render.txt
  • requirements-tested.txt
  • requirements.txt
  • scripts/check-data-provenance.py
  • scripts/check-public-data.py
  • scripts/doctor.py
  • skills/_shared/bootstrap.py
  • skills/_shared/pi_steel/__init__.py
  • skills/_shared/pi_steel/cli.py
  • skills/_shared/pi_steel/contracts.py
  • skills/_shared/pi_steel/geometry_verify.py
  • skills/_shared/pi_steel/parsing.py
  • skills/_shared/pi_steel/run_manifest.py
  • skills/_shared/pi_steel/validation.py
  • skills/_shared/schemas/estimate-package.schema.json
  • skills/_shared/schemas/nest-result.schema.json
  • skills/_shared/schemas/run-manifest.schema.json
  • skills/steel-estimate/SKILL.md
  • skills/steel-estimate/references/estimate-package-example.json
  • skills/steel-estimate/references/output-contract.md
  • skills/steel-estimate/scripts/acknowledge-finding.py
  • skills/steel-estimate/scripts/build-estimate-package.py
  • skills/steel-nest/SKILL.md
  • skills/steel-nest/references/FIXTURE_PROVENANCE.md
  • skills/steel-nest/references/example_job.json
  • skills/steel-nest/references/job_template.json
  • skills/steel-nest/scripts/nest.py
  • skills/steel-rfq/SKILL.md
  • skills/steel-rfq/assets/company-profile.example.json
  • skills/steel-rfq/references/rfq-input.md
  • skills/steel-rfq/scripts/generate-rfq.py
  • skills/steel-rfq/scripts/recalc.py
  • skills/steel-takeoff/SKILL.md
  • skills/steel-takeoff/references/takeoff-procedures.md
  • skills/steel-takeoff/scripts/calculate-weight.sh
  • skills/steel-takeoff/scripts/validate-bom.py
  • tests/fixtures/contracts/README.md
  • tests/fixtures/contracts/legacy-nest.json
  • tests/fixtures/nest/README.md
  • tests/fixtures/nest/grouped-engine.json
  • tests/fixtures/nest/irregular-reference-only.json
  • tests/fixtures/pipeline/README.md
  • tests/fixtures/pipeline/synthetic-estimate.json
  • tests/fixtures/pipeline/synthetic-profile.json
  • tests/fixtures/rfq/README.md
  • tests/fixtures/rfq/estimate-package.json
  • tests/fixtures/rfq/nest-handoff.json
  • tests/fixtures/rfq/synthetic-profile.json
  • tests/fixtures/workflows/README.md
  • tests/fixtures/workflows/blocked_plate_job.json
  • tests/fixtures/workflows/mixed_plate_job.json
  • tests/golden/pipeline/ready-artifacts.json
  • tests/golden/rfq/semantic-workbook.json
  • tests/test_acknowledge_finding_cli.py
  • tests/test_contracts.py
  • tests/test_data_provenance.py
  • tests/test_estimate_pipeline.py
  • tests/test_full_render_smoke.py
  • tests/test_installed_scripts.py
  • tests/test_nest_burn_guard.py
  • tests/test_nest_cli_contract.py
  • tests/test_nest_engine.py
  • tests/test_nest_invariants.py
  • tests/test_nest_outputs.py
  • tests/test_package_contents.py
  • tests/test_public_data_policy.py
  • tests/test_recalc.py
  • tests/test_rfq_generator.py
  • tests/test_rfq_workbook_contract.py
  • tests/test_run_manifests.py
  • tests/test_runtime_bootstrap.py
  • tests/test_structured_failure_cli.py
  • tests/test_takeoff_cli.py

Comment thread .gitignore
Comment thread scripts/check-data-provenance.py
Comment on lines +54 to +64
provenance = PROVENANCE_PATH.read_text(encoding="utf-8")
permission = dataset.get("redistribution_permission")
release_readiness = dataset.get("release_readiness")
documented_values = (
dataset.get("claimed_edition"),
str(expected_rows),
dataset.get("sha256"),
"Redistribution permission | Unverified",
)
if not all(value and value in provenance for value in documented_values):
errors.append("DATA_PROVENANCE.md differs from the machine-readable record")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Consistency check ignores the permission value it reads.

permission is read from the record but the documentation cross-check hardcodes the literal string "Redistribution permission | Unverified" instead of interpolating permission. The check currently only passes because the live value happens to be "Unverified"; it will silently stop validating (or wrongly fail) once redistribution_permission changes, which the plan explicitly calls out as an upcoming release-viability gate.

🛠️ Proposed fix
     documented_values = (
         dataset.get("claimed_edition"),
         str(expected_rows),
         dataset.get("sha256"),
-        "Redistribution permission | Unverified",
+        f"Redistribution permission | {permission}",
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
provenance = PROVENANCE_PATH.read_text(encoding="utf-8")
permission = dataset.get("redistribution_permission")
release_readiness = dataset.get("release_readiness")
documented_values = (
dataset.get("claimed_edition"),
str(expected_rows),
dataset.get("sha256"),
"Redistribution permission | Unverified",
)
if not all(value and value in provenance for value in documented_values):
errors.append("DATA_PROVENANCE.md differs from the machine-readable record")
provenance = PROVENANCE_PATH.read_text(encoding="utf-8")
permission = dataset.get("redistribution_permission")
release_readiness = dataset.get("release_readiness")
documented_values = (
dataset.get("claimed_edition"),
str(expected_rows),
dataset.get("sha256"),
f"Redistribution permission | {permission}",
)
if not all(value and value in provenance for value in documented_values):
errors.append("DATA_PROVENANCE.md differs from the machine-readable record")
🤖 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-data-provenance.py` around lines 54 - 64, Update the
documented_values tuple in the provenance consistency check to derive the
redistribution-permission entry from the existing permission variable instead of
hardcoding “Unverified”. Preserve the current validation of claimed_edition,
expected_rows, and sha256 while ensuring changes to redistribution_permission
are detected.

"private operating-company claim": re.compile(
r"team behind (?:a|the) production structural[- ]steel", re.I
),
"local absolute path": re.compile(r"(?:/Users/|/home/|[A-Z]:\\\\Users\\\\)"),

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

🧩 Analysis chain

🌐 Web query:

Does re.compile(r"[A-Z]:\\\\Users\\\\") match a string containing a single backslash "C:\\Users\\"?

💡 Result:

The regular expression pattern r"[A-Z]:\\Users\\" will not match a string containing a single backslash like C:\Users. [1][2] In Python regular expressions, the backslash character is an escape character used to signal special sequences or to escape metacharacters [1][2]. To match a literal backslash in a regular expression, you must escape it with another backslash within the regex pattern itself [1]. Because you are using a raw string literal (r""), the pattern is passed to the regex engine as [A-Z]:\\Users\\. 1. Within the regular expression language, a double backslash (\) represents a single literal backslash character [1][2]. 2. Your pattern r"[A-Z]:\\Users\\" contains two pairs of double backslashes: - The first pair matches a literal backslash after the colon [1][2]. - The second pair matches a literal backslash at the end [1][2]. Consequently, the regex expects the format C:\Users\ (with two literal backslashes). Since your target string C:\Users\ only contains a single literal backslash at those positions, the match will fail [1][3]. To match a string with a single literal backslash at those positions, you should use a pattern that expects only one literal backslash, such as r"[A-Z]:\Users\" [1][4].

Citations:


🏁 Script executed:

python3 - <<'PY'
import re

pattern = re.compile(r"(?:/Users/|/home/|[A-Z]:\\\\Users\\\\)")
samples = [
    r"C:\Users\alice",
    r"C:\\Users\\alice",
    r"C:\Users\",
    r"C:\\Users\\",
]
for s in samples:
    print(repr(s), bool(pattern.search(s)))
PY

Repository: StructuPath/pi-steel

Length of output: 267


🏁 Script executed:

python3 - <<'PY'
import re

pattern = re.compile(r"(?:/Users/|/home/|[A-Z]:\\\\Users\\\\)")

samples = {
    "single-backslash path": "C:\\Users\\alice",
    "double-backslash text": "C:\\\\Users\\\\alice",
    "single-backslash root": "C:\\Users\\",
    "double-backslash root text": "C:\\\\Users\\\\",
}

for label, s in samples.items():
    m = pattern.search(s)
    print(label, repr(s), bool(m), m.group(0) if m else None)
PY

Repository: StructuPath/pi-steel

Length of output: 381


Windows absolute-path pattern is over-escaped

r"(?:/Users/|/home/|[A-Z]:\\\\Users\\\\)" only matches doubled-backslash text, so a normal Windows path like C:\Users\alice slips through. Use r"(?:/Users/|/home/|[A-Z]:\\Users\\)" instead.

🤖 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` at line 41, Correct the “local absolute path”
regex in the public-data checks to match single backslashes in normal Windows
paths such as C:\Users\alice, while preserving the existing Unix path matching.

Comment on lines +69 to +79
def estimate_input_projection(package: dict[str, Any]) -> dict[str, Any]:
"""Return source/config semantics, excluding review and confirmation bookkeeping."""
projection = {
key: value
for key, value in package.items()
if key not in {"review"}
}
projection = json.loads(json.dumps(projection))
for stock in projection.get("stock", []):
stock.pop("reviewer_confirmation", None)
return projection

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Malformed stock crashes hashing before schema validation can report it.

validate_estimate_package computes estimate_input_hash(package) (validation.py Line 299) before any schema check. If stock is not a list of objects (e.g. "stock": ["oops"] or a dict), stock.pop(...) raises AttributeError, so an invalid package aborts the CLI with a traceback instead of producing a schema_validation blocker finding. Guard the shape here so the identity hash is computable for any JSON input.

🛡️ Proposed fix
     projection = json.loads(json.dumps(projection))
-    for stock in projection.get("stock", []):
-        stock.pop("reviewer_confirmation", None)
+    stock_entries = projection.get("stock")
+    if isinstance(stock_entries, list):
+        for stock in stock_entries:
+            if isinstance(stock, dict):
+                stock.pop("reviewer_confirmation", None)
     return projection
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def estimate_input_projection(package: dict[str, Any]) -> dict[str, Any]:
"""Return source/config semantics, excluding review and confirmation bookkeeping."""
projection = {
key: value
for key, value in package.items()
if key not in {"review"}
}
projection = json.loads(json.dumps(projection))
for stock in projection.get("stock", []):
stock.pop("reviewer_confirmation", None)
return projection
def estimate_input_projection(package: dict[str, Any]) -> dict[str, Any]:
"""Return source/config semantics, excluding review and confirmation bookkeeping."""
projection = {
key: value
for key, value in package.items()
if key not in {"review"}
}
projection = json.loads(json.dumps(projection))
stock_entries = projection.get("stock")
if isinstance(stock_entries, list):
for stock in stock_entries:
if isinstance(stock, dict):
stock.pop("reviewer_confirmation", None)
return projection
🧰 Tools
🪛 ast-grep (0.45.0)

[info] 75-75: use jsonify instead of json.dumps for JSON output
Context: json.dumps(projection)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 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 `@skills/_shared/pi_steel/contracts.py` around lines 69 - 79, Update
estimate_input_projection to remove reviewer_confirmation only when stock is a
list and each entry is an object; otherwise preserve the malformed JSON value
unchanged so estimate_input_hash remains computable and schema validation can
report the schema_validation blocker.

Comment on lines +1648 to +1667
try:
with open(args.job, encoding="utf-8") as f:
job = json.load(f)
result, qa_report, report, final_path = publish_nest_run(job, args)
except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
diagnostic_path = publish_failure_diagnostic(
args.out,
stage="steel-nest",
input_path=args.job,
error=exc,
tool_version=package_version(__file__),
run_id=args.run_id,
)
suffix = (
f"; diagnostic published: {diagnostic_path}"
if diagnostic_path is not None
else "; diagnostic publication unavailable"
)
print(f"Nesting failed: {exc}{suffix}", file=sys.stderr)
return 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

ManifestError escapes the structured failure path.

ManifestError subclasses RuntimeError, so publication failures raised inside publish_nest_run (run already exists, artifact allow-list mismatch, latest-run pointer update failed) bypass this handler and surface as a traceback with no published diagnostic and no controlled exit code.

🐛 Proposed fix
-    except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
+    except (
+        OSError,
+        json.JSONDecodeError,
+        TypeError,
+        ValueError,
+        ManifestError,
+    ) as exc:

with ManifestError added to the pi_steel import block at lines 55-66.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 1648-1648: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(args.job, encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 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 `@skills/steel-nest/scripts/nest.py` around lines 1648 - 1667, Add
ManifestError to the pi_steel imports and include it in the exception tuple
around publish_nest_run in the main failure handler, so manifest-related
publication failures use publish_failure_diagnostic, emit the controlled error
message, and return exit code 1 instead of escaping as tracebacks.

Comment on lines +339 to +357
"thickness": row["Thickness"],
"quantity": quantity,
"stock_length": "",
"purchase_weight_lbs": row["Purchase Weight"],
"replaces_item_ids": (),
}
items.append(item)
if len(currencies) != 1:
raise RfqInputError("legacy workbook must use one explicit currency")
items.sort(
key=lambda item: (
CATEGORY_ORDER.get(item["category"], 80),
item["material"],
item["grade"],
item["thickness"] if item["thickness"] is not None else -1,
item["size"],
item["item_id"],
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Legacy Thickness is passed through untyped, so the sort can raise TypeError.

row["Thickness"] keeps whatever the cell held (float, int, or free text such as 1/2"). The sort key at Lines 348-357 mixes those with the -1 fallback, and str vs float comparison raises TypeError; the same values later feed the grouping sort in compile_workbook (Lines 762-771). Reject or coerce non-numeric thickness in the adapter, consistent with its "no guessing" contract.

🛠️ Proposed fix
+        thickness = row["Thickness"]
+        if thickness is not None and not isinstance(thickness, (int, float)):
+            raise RfqInputError(
+                f"legacy row {row_number} has non-numeric Thickness"
+            )
@@
-            "thickness": row["Thickness"],
+            "thickness": thickness,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"thickness": row["Thickness"],
"quantity": quantity,
"stock_length": "",
"purchase_weight_lbs": row["Purchase Weight"],
"replaces_item_ids": (),
}
items.append(item)
if len(currencies) != 1:
raise RfqInputError("legacy workbook must use one explicit currency")
items.sort(
key=lambda item: (
CATEGORY_ORDER.get(item["category"], 80),
item["material"],
item["grade"],
item["thickness"] if item["thickness"] is not None else -1,
item["size"],
item["item_id"],
)
)
thickness = row["Thickness"]
if thickness is not None and not isinstance(thickness, (int, float)):
raise RfqInputError(
f"legacy row {row_number} has non-numeric Thickness"
)
item = {
"thickness": thickness,
"quantity": quantity,
"stock_length": "",
"purchase_weight_lbs": row["Purchase Weight"],
"replaces_item_ids": (),
}
items.append(item)
if len(currencies) != 1:
raise RfqInputError("legacy workbook must use one explicit currency")
items.sort(
key=lambda item: (
CATEGORY_ORDER.get(item["category"], 80),
item["material"],
item["grade"],
item["thickness"] if item["thickness"] is not None else -1,
item["size"],
item["item_id"],
)
)
🤖 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 `@skills/steel-rfq/scripts/generate-rfq.py` around lines 339 - 357, Normalize
and validate legacy Thickness in the workbook adapter before constructing each
item, rejecting free-text or otherwise non-numeric values rather than passing
them through. Ensure accepted values use one consistent numeric representation
so the item sort and compile_workbook grouping sort can compare them with the -1
fallback without TypeError, while preserving the adapter’s no-guessing contract.

Comment on lines +406 to +415
for path, source in candidates:
if not path.is_file():
continue
try:
profile = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise RfqInputError(f"company profile could not be read: {exc}") from exc
profile["_profile_path"] = str(path)
return profile, source
return None, "missing"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Non-object profile JSON raises an unhandled TypeError.

A profile file containing a JSON array or scalar passes json.loads, then profile["_profile_path"] = ... raises TypeError, which is not an RfqInputError and so bypasses both the invalid_company_profile finding in publish_rfq_run and main's diagnostic handler.

🛠️ Proposed fix
         except (OSError, json.JSONDecodeError) as exc:
             raise RfqInputError(f"company profile could not be read: {exc}") from exc
+        if not isinstance(profile, dict):
+            raise RfqInputError(
+                f"company profile must be a JSON object: {path}"
+            )
         profile["_profile_path"] = str(path)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for path, source in candidates:
if not path.is_file():
continue
try:
profile = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise RfqInputError(f"company profile could not be read: {exc}") from exc
profile["_profile_path"] = str(path)
return profile, source
return None, "missing"
for path, source in candidates:
if not path.is_file():
continue
try:
profile = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise RfqInputError(f"company profile could not be read: {exc}") from exc
if not isinstance(profile, dict):
raise RfqInputError(
f"company profile must be a JSON object: {path}"
)
profile["_profile_path"] = str(path)
return profile, source
return None, "missing"
🤖 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 `@skills/steel-rfq/scripts/generate-rfq.py` around lines 406 - 415, Validate
the result of json.loads in the profile-loading loop before assigning
_profile_path, and raise RfqInputError for arrays or scalar JSON values. Keep
valid object profiles returning with their source unchanged so publish_rfq_run
and main continue handling malformed profiles through their existing diagnostic
paths.

Comment on lines +1197 to +1215
try:
qa_report, final_path = publish_rfq_run(args)
except (OSError, json.JSONDecodeError, RfqInputError) as exc:
diagnostic_path = publish_failure_diagnostic(
args.out,
stage="steel-rfq",
input_path=args.input,
error=exc,
tool_version=package_version(__file__),
run_id=args.run_id,
explicit_dates={"issued_date": args.issued_date},
)
suffix = (
f"; diagnostic published: {diagnostic_path}"
if diagnostic_path is not None
else "; diagnostic publication unavailable"
)
print(f"RFQ generation failed: {exc}{suffix}", file=sys.stderr)
return 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

RecalculationError escapes the structured failure path.

recalc.recalculate (called at Line 994) raises RecalculationError(RuntimeError), which is not in the caught tuple. A workbook that cannot be re-saved with recalc flags therefore aborts with a raw traceback, leaving the staging run unpublished and no failure diagnostic — while the documented contract (skills/steel-rfq/SKILL.md Lines 76-81) promises exit 1 plus a diagnostic for internal errors.

🛠️ Proposed fix
     try:
         qa_report, final_path = publish_rfq_run(args)
-    except (OSError, json.JSONDecodeError, RfqInputError) as exc:
+    except (
+        OSError,
+        json.JSONDecodeError,
+        RfqInputError,
+        recalc.RecalculationError,
+    ) as exc:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
qa_report, final_path = publish_rfq_run(args)
except (OSError, json.JSONDecodeError, RfqInputError) as exc:
diagnostic_path = publish_failure_diagnostic(
args.out,
stage="steel-rfq",
input_path=args.input,
error=exc,
tool_version=package_version(__file__),
run_id=args.run_id,
explicit_dates={"issued_date": args.issued_date},
)
suffix = (
f"; diagnostic published: {diagnostic_path}"
if diagnostic_path is not None
else "; diagnostic publication unavailable"
)
print(f"RFQ generation failed: {exc}{suffix}", file=sys.stderr)
return 1
try:
qa_report, final_path = publish_rfq_run(args)
except (
OSError,
json.JSONDecodeError,
RfqInputError,
recalc.RecalculationError,
) as exc:
diagnostic_path = publish_failure_diagnostic(
args.out,
stage="steel-rfq",
input_path=args.input,
error=exc,
tool_version=package_version(__file__),
run_id=args.run_id,
explicit_dates={"issued_date": args.issued_date},
)
suffix = (
f"; diagnostic published: {diagnostic_path}"
if diagnostic_path is not None
else "; diagnostic publication unavailable"
)
print(f"RFQ generation failed: {exc}{suffix}", file=sys.stderr)
return 1
🤖 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 `@skills/steel-rfq/scripts/generate-rfq.py` around lines 1197 - 1215, Extend
the structured exception handling around publish_rfq_run in the main generation
flow to catch RecalculationError alongside the existing OSError, JSON decode,
and input errors. Ensure this exception follows the same
publish_failure_diagnostic, stderr reporting, and exit-1 path, preserving the
documented failure behavior.


with tarfile.open(archive) as package:
members = set(package.getnames())
package.extractall(tmp_path / "unpacked", filter="data")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

for path in pyproject.toml .python-version .tool-versions .github/workflows/ci.yml; do
  if [ -f "$path" ]; then
    echo "=== $path ==="
    rg -n 'requires-python|python-version|PYTHON_VERSION|python:' "$path" || true
  fi
done

Repository: StructuPath/pi-steel

Length of output: 387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import pathlib, re, sys

files = [
    pathlib.Path("tests/test_installed_scripts.py"),
    pathlib.Path("pyproject.toml"),
    pathlib.Path(".github/workflows/ci.yml"),
]

for path in files:
    if path.exists():
        print(f"=== {path} ===")
        text = path.read_text()
        if path.name == "tests/test_installed_scripts.py":
            for i, line in enumerate(text.splitlines(), 1):
                if "extractall(" in line or "filter=" in line:
                    print(f"{i}: {line}")
        else:
            for i, line in enumerate(text.splitlines(), 1):
                if re.search(r"requires-python|python-version|python:", line):
                    print(f"{i}: {line}")
PY

Repository: StructuPath/pi-steel

Length of output: 431


Guard this test for Python 3.12+

TarFile.extractall(filter="data") is not available on the repo’s supported Python 3.11 target (requires-python = ">=3.11,<3.14", and CI runs 3.11). Use a 3.11-compatible extraction path or skip this test on older interpreters.

🤖 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 `@tests/test_installed_scripts.py` at line 25, Update the test around
package.extractall to remain compatible with Python 3.11: use the filter
argument only on Python 3.12 and newer, while using the existing compatible
extraction path on older supported interpreters. Preserve the test’s extraction
behavior across the supported Python range.

@Steel-tech
Steel-tech merged commit 0c5263d into main Jul 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants