@structupath/pi-steel 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DATA_PROVENANCE.json +23 -0
- package/DATA_PROVENANCE.md +35 -0
- package/PUBLIC_DATA_POLICY.md +52 -0
- package/README.md +114 -33
- package/docs/assets/pi-steel-demo.gif +0 -0
- package/docs/assets/pi-steel-gallery.webp +0 -0
- package/package.json +35 -4
- package/pyproject.toml +28 -0
- package/requirements-dev.txt +5 -0
- package/requirements-render.txt +1 -0
- package/requirements-tested.txt +9 -0
- package/requirements.txt +7 -0
- package/scripts/check-data-provenance.py +140 -0
- package/scripts/check-public-data.py +401 -0
- package/scripts/doctor.py +127 -0
- package/skills/_shared/bootstrap.py +32 -0
- package/skills/_shared/pi_steel/__init__.py +47 -0
- package/skills/_shared/pi_steel/cli.py +167 -0
- package/skills/_shared/pi_steel/contracts.py +87 -0
- package/skills/_shared/pi_steel/geometry_verify.py +243 -0
- package/skills/_shared/pi_steel/parsing.py +205 -0
- package/skills/_shared/pi_steel/run_manifest.py +317 -0
- package/skills/_shared/pi_steel/validation.py +597 -0
- package/skills/_shared/schemas/estimate-package.schema.json +424 -0
- package/skills/_shared/schemas/nest-result.schema.json +443 -0
- package/skills/_shared/schemas/run-manifest.schema.json +145 -0
- package/skills/steel-estimate/SKILL.md +119 -0
- package/skills/steel-estimate/references/estimate-package-example.json +91 -0
- package/skills/steel-estimate/references/output-contract.md +47 -0
- package/skills/steel-estimate/scripts/acknowledge-finding.py +193 -0
- package/skills/steel-estimate/scripts/build-estimate-package.py +836 -0
- package/skills/steel-nest/SKILL.md +50 -23
- package/skills/steel-nest/references/FIXTURE_PROVENANCE.md +12 -0
- package/skills/steel-nest/references/example_job.json +21 -25
- package/skills/steel-nest/references/job_template.json +11 -9
- package/skills/steel-nest/scripts/nest.py +1276 -250
- package/skills/steel-rfq/SKILL.md +93 -175
- package/skills/steel-rfq/assets/company-profile.example.json +11 -5
- package/skills/steel-rfq/references/rfq-input.md +58 -0
- package/skills/steel-rfq/scripts/generate-rfq.py +1221 -0
- package/skills/steel-rfq/scripts/recalc.py +33 -10
- package/skills/steel-takeoff/SKILL.md +12 -8
- package/skills/steel-takeoff/assets/bom-template.csv +1 -1
- package/skills/steel-takeoff/references/takeoff-procedures.md +3 -1
- package/skills/steel-takeoff/scripts/calculate-weight.sh +5 -10
- package/skills/steel-takeoff/scripts/lookup-member.sh +2 -2
- package/skills/steel-takeoff/scripts/validate-bom.py +151 -196
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0.0",
|
|
3
|
+
"datasets": [
|
|
4
|
+
{
|
|
5
|
+
"dataset_id": "structupath-structural-shapes-v1",
|
|
6
|
+
"dataset_name": "StructuPath Structural Shapes Database",
|
|
7
|
+
"shipped_file": "skills/steel-takeoff/assets/aisc-shapes-database.json",
|
|
8
|
+
"source": "Original StructuPath dataset created from StructuPath's internal structural-shape records",
|
|
9
|
+
"copyright_holder": "StructuPath",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"authorization": "Authorized by StructuPath for public redistribution in @structupath/pi-steel under the MIT license on 2026-07-28",
|
|
12
|
+
"rows": 477,
|
|
13
|
+
"sha256": "5a7c975c4c290c34df6f7df3b4d0d0d13a00ef7c2b1f45097a49a78d245dcc91",
|
|
14
|
+
"required_fields": [
|
|
15
|
+
"type",
|
|
16
|
+
"designation",
|
|
17
|
+
"weight_per_ft"
|
|
18
|
+
],
|
|
19
|
+
"redistribution_permission": "authorized",
|
|
20
|
+
"release_readiness": "ready"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Data Provenance
|
|
2
|
+
|
|
3
|
+
This file records the evidence and release decision for datasets shipped by
|
|
4
|
+
pi-steel. [`DATA_PROVENANCE.json`](DATA_PROVENANCE.json) is the machine-readable
|
|
5
|
+
release record checked against the shipped bytes.
|
|
6
|
+
|
|
7
|
+
## StructuPath Structural Shapes Database
|
|
8
|
+
|
|
9
|
+
| Field | Recorded value |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Dataset name | StructuPath Structural Shapes Database |
|
|
12
|
+
| Shipped file | `skills/steel-takeoff/assets/aisc-shapes-database.json` |
|
|
13
|
+
| Source | Original StructuPath dataset created from StructuPath's internal structural-shape records |
|
|
14
|
+
| Rows in shipped JSON | 477 |
|
|
15
|
+
| SHA-256 | `5a7c975c4c290c34df6f7df3b4d0d0d13a00ef7c2b1f45097a49a78d245dcc91` |
|
|
16
|
+
| Repository introduction | Initial repository commit |
|
|
17
|
+
| Copyright holder | StructuPath |
|
|
18
|
+
| Redistribution permission | Authorized |
|
|
19
|
+
| Authorization | Authorized by StructuPath for public redistribution in @structupath/pi-steel under the MIT license on 2026-07-28 |
|
|
20
|
+
| License | MIT |
|
|
21
|
+
| Release gate | Ready |
|
|
22
|
+
|
|
23
|
+
StructuPath confirms that it created this database from its own internal
|
|
24
|
+
structural-shape records and that the dataset is not sourced from AISC. StructuPath owns the
|
|
25
|
+
dataset and has authorized its public redistribution in `@structupath/pi-steel`
|
|
26
|
+
under the MIT license.
|
|
27
|
+
|
|
28
|
+
The dataset uses standard structural-shape designations and property names.
|
|
29
|
+
Those references identify industry-standard members; they do not imply that the
|
|
30
|
+
shipped database was copied, transformed, or licensed from AISC.
|
|
31
|
+
|
|
32
|
+
The release check verifies the recorded path, row count, required fields,
|
|
33
|
+
designation uniqueness, SHA-256 digest, ownership, license, authorization, and
|
|
34
|
+
release decision. Any change to the shipped bytes or provenance record requires
|
|
35
|
+
a new review.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Public Data Policy
|
|
2
|
+
|
|
3
|
+
This repository is public. Source code, documentation, tests, fixtures, generated
|
|
4
|
+
goldens, commit metadata, issue text, and release artifacts must be safe for public
|
|
5
|
+
distribution.
|
|
6
|
+
|
|
7
|
+
## Allowed
|
|
8
|
+
|
|
9
|
+
- Clearly labeled synthetic projects, customers, vendors, people, and identifiers.
|
|
10
|
+
- Published standards data whose source, edition, license, and redistribution rights
|
|
11
|
+
are documented.
|
|
12
|
+
- Invented dimensions and quantities that do not reproduce a private project.
|
|
13
|
+
- Placeholder configuration such as `Example Fabricator` and `Example City, ST`.
|
|
14
|
+
- Product behavior and generic steel-domain terminology.
|
|
15
|
+
- Public gallery media reviewed before publication and pinned in the scanner by
|
|
16
|
+
exact repository path and SHA-256 digest. Replacing it requires a new review and
|
|
17
|
+
digest.
|
|
18
|
+
|
|
19
|
+
## Prohibited
|
|
20
|
+
|
|
21
|
+
- Private company names or identifiers other than the public StructuPath product
|
|
22
|
+
identity.
|
|
23
|
+
- Customer, vendor, employee, subcontractor, or project names and contact details.
|
|
24
|
+
- Real job numbers, drawing references, addresses, schedules, quotes, bids, rates,
|
|
25
|
+
margins, payroll, costs, terms, or contract language.
|
|
26
|
+
- Production drawings, takeoffs, BOMs, nests, RFQs, quotes, reports, screenshots, or
|
|
27
|
+
generated artifacts, including "anonymized" copies derived from them.
|
|
28
|
+
- Company profiles, logos, signatures, credentials, tokens, local absolute paths, or
|
|
29
|
+
exported cloud files.
|
|
30
|
+
- Facts that link this product to a private operating company, facility, customer, or
|
|
31
|
+
internal workflow unless separately approved for publication.
|
|
32
|
+
|
|
33
|
+
## Fixture Rules
|
|
34
|
+
|
|
35
|
+
1. Create fixtures from scratch; do not sanitize production files for public use.
|
|
36
|
+
2. Prefix project-like identifiers with `SYNTHETIC-` or `EXAMPLE-`.
|
|
37
|
+
3. Use `Example Customer`, `Example Vendor`, `Example Fabricator`, and
|
|
38
|
+
`Example City, ST` unless a test requires another obviously fictional value.
|
|
39
|
+
4. Omit prices and commercial terms unless the test specifically requires them. When
|
|
40
|
+
required, label values as synthetic in the same fixture.
|
|
41
|
+
5. Keep private inputs and generated outputs outside the repository in ignored
|
|
42
|
+
directories such as `private/`, `local-data/`, `customer-data/`, or `outputs/`.
|
|
43
|
+
6. Run `python3 scripts/check-public-data.py` before every commit and release. Before
|
|
44
|
+
pushing, also scan the outgoing range with
|
|
45
|
+
`python3 scripts/check-public-data.py --range <remote-ref>..HEAD`.
|
|
46
|
+
7. Put private names and identifiers, one per line, in the ignored local file
|
|
47
|
+
`.pi-steel/private-terms.txt`; the scanner checks them without committing the
|
|
48
|
+
denylist or echoing matched text.
|
|
49
|
+
|
|
50
|
+
Git history is public too. Removing a value from the current tree does not remove it
|
|
51
|
+
from prior commits. Use `npm run privacy:history` for a redacted audit; history
|
|
52
|
+
cleanup requires an explicit coordinated rewrite.
|
package/README.md
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
# pi-steel
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@structupath/pi-steel)
|
|
4
|
+
[](https://github.com/StructuPath/pi-steel/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://pi.dev/packages?name=pi-steel)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
Structural-steel estimating skills for the [Pi coding agent](https://pi.dev):
|
|
9
|
+
validated takeoffs, plate nesting, guarded DXF output, draft vendor RFQs, and a
|
|
10
|
+
review-gated estimate pipeline.
|
|
11
|
+
|
|
12
|
+
Built by [StructuPath](https://structupath.ai).
|
|
13
|
+
|
|
14
|
+
## Demo
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
This demo uses only the repository's synthetic fixture. It runs the complete
|
|
19
|
+
`takeoff → nest → draft RFQ` pipeline and publishes an isolated package with a
|
|
20
|
+
machine-readable `rfq_ready_for_review` outcome.
|
|
6
21
|
|
|
7
22
|
## Install
|
|
8
23
|
|
|
@@ -10,56 +25,122 @@ By [StructuPath](https://structupath.ai), from the team behind a production stru
|
|
|
10
25
|
pi install npm:@structupath/pi-steel
|
|
11
26
|
```
|
|
12
27
|
|
|
13
|
-
|
|
28
|
+
Then ask Pi:
|
|
14
29
|
|
|
15
|
-
|
|
30
|
+
```text
|
|
31
|
+
Do a structural-steel takeoff from these drawings and build a validated BOM.
|
|
32
|
+
Nest these rectangular plate parts on 96 × 48 stock.
|
|
33
|
+
Prepare a draft RFQ from this estimate.
|
|
34
|
+
```
|
|
16
35
|
|
|
17
|
-
|
|
36
|
+
Run the local dependency check when using the source repository:
|
|
18
37
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
38
|
+
```bash
|
|
39
|
+
npm run doctor
|
|
40
|
+
```
|
|
22
41
|
|
|
23
|
-
|
|
42
|
+
## What is included
|
|
24
43
|
|
|
25
|
-
|
|
44
|
+
| Skill | Purpose | Primary outputs |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| `steel-takeoff` | Validate member designations and calculate BOM weight and tonnage | Validated BOM and findings |
|
|
47
|
+
| `steel-nest` | Lay out plate parts with kerf, gap, and edge-margin controls | Nest results, cut list, reference drawings, and guarded burn DXFs |
|
|
48
|
+
| `steel-rfq` | Compile estimate data into a reviewable vendor request | Draft `.xlsx` RFQ and semantic workbook record |
|
|
49
|
+
| `steel-estimate` | Orchestrate the complete review-gated workflow | Immutable run directory, QA report, lineage, manifests, nesting, and draft RFQ |
|
|
26
50
|
|
|
27
|
-
|
|
28
|
-
> "What's the lightest W-shape with depth ≥ 18" and Ix ≥ 1000?"
|
|
29
|
-
> "Total tonnage on this BOM with 12% connections"
|
|
51
|
+
### Takeoff
|
|
30
52
|
|
|
31
|
-
|
|
53
|
+
`steel-takeoff` provides structural-shape lookup, weight calculation, and BOM
|
|
54
|
+
validation helpers. It checks member designations, grades, duplicate marks, and
|
|
55
|
+
unreasonable weights. It calculates from supplied quantities and allowances; it
|
|
56
|
+
does not invent pricing.
|
|
32
57
|
|
|
33
|
-
|
|
58
|
+
### Nesting and DXF safety
|
|
34
59
|
|
|
35
|
-
|
|
60
|
+
`steel-nest` uses MaxRects bin packing for rectangular parts and reports yield,
|
|
61
|
+
scrap, reusable drops, and unplaced material. Irregular parts are estimated by
|
|
62
|
+
bounding box and are always flagged.
|
|
36
63
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
64
|
+
Per-sheet `burn_plate_N.dxf` files are emitted only when the full nest is
|
|
65
|
+
complete and every supported hole remains inside its part. Otherwise, pi-steel
|
|
66
|
+
suppresses burn DXFs for the entire run and leaves clearly labeled estimating
|
|
67
|
+
and reference artifacts for review. It does not emit G-code or claim
|
|
68
|
+
machine-specific CAM compatibility.
|
|
40
69
|
|
|
41
|
-
|
|
70
|
+
### Draft RFQs
|
|
42
71
|
|
|
43
|
-
|
|
72
|
+
`steel-rfq` groups material by stock family, carries approved nesting data into
|
|
73
|
+
the workbook, and provides fill-in columns for vendor pricing. Workbooks remain
|
|
74
|
+
drafts until a person reviews and issues them.
|
|
44
75
|
|
|
45
|
-
|
|
76
|
+
Copy the example company profile to a private project location:
|
|
46
77
|
|
|
47
|
-
|
|
78
|
+
```bash
|
|
79
|
+
mkdir -p .pi-steel
|
|
80
|
+
cp skills/steel-rfq/assets/company-profile.example.json \
|
|
81
|
+
.pi-steel/company-profile.json
|
|
82
|
+
```
|
|
48
83
|
|
|
49
|
-
|
|
84
|
+
Complete that local copy with approved company information. Never commit the
|
|
85
|
+
completed profile.
|
|
50
86
|
|
|
51
|
-
|
|
52
|
-
> "Generate an RFQ from this estimate"
|
|
87
|
+
## Workflow and outcomes
|
|
53
88
|
|
|
54
|
-
|
|
89
|
+
```text
|
|
90
|
+
estimate input
|
|
91
|
+
↓
|
|
92
|
+
contract validation → plate nesting → draft RFQ compilation
|
|
93
|
+
↓
|
|
94
|
+
QA findings + lineage + immutable run manifest
|
|
95
|
+
↓
|
|
96
|
+
ready | needs_review | blocked
|
|
97
|
+
```
|
|
55
98
|
|
|
56
|
-
|
|
57
|
-
-
|
|
99
|
+
Each run is published below the requested output root and referenced by
|
|
100
|
+
`latest-run.json`. Exit status `0` means geometry-verified, `2` means human
|
|
101
|
+
review is required, and `3` means the workflow is blocked. A blocked run never
|
|
102
|
+
contains an RFQ workbook.
|
|
58
103
|
|
|
59
|
-
##
|
|
104
|
+
## Public-repository safety
|
|
60
105
|
|
|
61
|
-
|
|
106
|
+
This repository and all committed examples are public. Do not add:
|
|
62
107
|
|
|
63
|
-
|
|
108
|
+
- company profiles or internal business context;
|
|
109
|
+
- customer, vendor, employee, project, bid, or contract data;
|
|
110
|
+
- live pricing, margins, terms, credentials, or private contact information;
|
|
111
|
+
- generated RFQs, takeoffs, PDFs, DXFs, spreadsheets, or output directories.
|
|
112
|
+
|
|
113
|
+
Keep operational inputs and generated artifacts outside the repository. See the
|
|
114
|
+
[public data policy](PUBLIC_DATA_POLICY.md) and
|
|
115
|
+
[data provenance record](DATA_PROVENANCE.md) before contributing.
|
|
116
|
+
|
|
117
|
+
## Documentation
|
|
118
|
+
|
|
119
|
+
- [GitHub wiki](https://github.com/StructuPath/pi-steel/wiki)
|
|
120
|
+
- [Public data policy](PUBLIC_DATA_POLICY.md)
|
|
121
|
+
- [Data provenance](DATA_PROVENANCE.md)
|
|
122
|
+
- [Pi package catalog](https://pi.dev/packages?name=pi-steel)
|
|
123
|
+
- [npm package](https://www.npmjs.com/package/@structupath/pi-steel)
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm test # base, no-render suite
|
|
129
|
+
npm run test:full # PDF/PNG/DXF/LibreOffice smoke tests
|
|
130
|
+
npm run privacy:check # current public-repository data guard
|
|
131
|
+
npm run privacy:history # redacted audit of reachable history
|
|
132
|
+
npm run pack:check # npm contents and installed-runtime smoke test
|
|
133
|
+
npm run provenance:check # shape-data integrity and recorded decision
|
|
134
|
+
npm run release:check # complete release gate
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The current package version is `0.2.3`. `release:check` verifies the test,
|
|
138
|
+
privacy, package-content, shape-data integrity, ownership, license, and
|
|
139
|
+
redistribution contracts before publication.
|
|
140
|
+
|
|
141
|
+
## License
|
|
64
142
|
|
|
65
|
-
|
|
143
|
+
StructuPath-authored code, documentation, and the bundled StructuPath Structural
|
|
144
|
+
Shapes Database are distributed under the MIT license. Dataset ownership,
|
|
145
|
+
authorization, and integrity evidence are documented in
|
|
146
|
+
[DATA_PROVENANCE.md](DATA_PROVENANCE.md).
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@structupath/pi-steel",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Structural steel estimating
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Structural steel estimating for Pi — validated takeoffs, plate nesting, guarded DXF output, and review-ready RFQ packages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"structural-steel",
|
|
11
11
|
"estimating",
|
|
12
12
|
"takeoff",
|
|
13
|
-
"aisc",
|
|
14
13
|
"bom",
|
|
15
14
|
"rfq",
|
|
16
15
|
"construction",
|
|
@@ -30,14 +29,46 @@
|
|
|
30
29
|
"engines": {
|
|
31
30
|
"node": ">=20.6.0"
|
|
32
31
|
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"doctor": "python3 scripts/doctor.py",
|
|
34
|
+
"lint": "ruff check skills scripts tests",
|
|
35
|
+
"test": "python3 -m pytest -m 'not full_render'",
|
|
36
|
+
"test:contracts": "python3 -m pytest tests/test_contracts.py tests/test_run_manifests.py tests/test_rfq_workbook_contract.py",
|
|
37
|
+
"test:full": "python3 -m pytest -m full_render",
|
|
38
|
+
"test:runtime": "python3 -m pytest tests/test_runtime_bootstrap.py tests/test_run_manifests.py tests/test_installed_scripts.py",
|
|
39
|
+
"privacy:check": "python3 scripts/check-public-data.py",
|
|
40
|
+
"privacy:history": "python3 scripts/check-public-data.py --history",
|
|
41
|
+
"provenance:check": "python3 scripts/check-data-provenance.py",
|
|
42
|
+
"pack:check": "python3 -m pytest tests/test_package_contents.py tests/test_installed_scripts.py",
|
|
43
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
44
|
+
"release:check": "npm test && npm run privacy:check && npm run pack:check && python3 scripts/check-data-provenance.py --release",
|
|
45
|
+
"prepublishOnly": "npm run release:check"
|
|
46
|
+
},
|
|
33
47
|
"pi": {
|
|
34
48
|
"skills": [
|
|
35
49
|
"./skills"
|
|
36
|
-
]
|
|
50
|
+
],
|
|
51
|
+
"image": "https://raw.githubusercontent.com/StructuPath/pi-steel/main/docs/assets/pi-steel-gallery.webp"
|
|
37
52
|
},
|
|
38
53
|
"files": [
|
|
39
54
|
"skills",
|
|
55
|
+
"!skills/**/__pycache__",
|
|
56
|
+
"!skills/**/*.pyc",
|
|
57
|
+
"!skills/**/company-profile.json",
|
|
58
|
+
"scripts/doctor.py",
|
|
59
|
+
"scripts/check-public-data.py",
|
|
60
|
+
"scripts/check-data-provenance.py",
|
|
61
|
+
"pyproject.toml",
|
|
62
|
+
"requirements.txt",
|
|
63
|
+
"requirements-tested.txt",
|
|
64
|
+
"requirements-dev.txt",
|
|
65
|
+
"requirements-render.txt",
|
|
66
|
+
"DATA_PROVENANCE.md",
|
|
67
|
+
"DATA_PROVENANCE.json",
|
|
68
|
+
"docs/assets/pi-steel-demo.gif",
|
|
69
|
+
"docs/assets/pi-steel-gallery.webp",
|
|
40
70
|
"README.md",
|
|
71
|
+
"PUBLIC_DATA_POLICY.md",
|
|
41
72
|
"LICENSE"
|
|
42
73
|
]
|
|
43
74
|
}
|
package/pyproject.toml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pi-steel-runtime"
|
|
3
|
+
version = "0.2.3"
|
|
4
|
+
description = "Python runtime dependencies and test configuration for pi-steel"
|
|
5
|
+
requires-python = ">=3.11,<3.14"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"ezdxf>=1.3,<2",
|
|
8
|
+
"jsonschema>=4.23,<5",
|
|
9
|
+
"matplotlib>=3.9,<4",
|
|
10
|
+
"numpy>=2.1,<3",
|
|
11
|
+
"openpyxl>=3.1,<4",
|
|
12
|
+
"pandas>=2.2,<3",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
test = [
|
|
17
|
+
"pytest>=8.3,<10",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.pytest.ini_options]
|
|
21
|
+
addopts = "-ra"
|
|
22
|
+
testpaths = ["tests"]
|
|
23
|
+
markers = [
|
|
24
|
+
"full_render: requires LibreOffice and Poppler system tools",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.ruff.lint.per-file-ignores]
|
|
28
|
+
"tests/*.py" = ["E402"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Full-render tests additionally require LibreOffice and Poppler system tools.
|
package/requirements.txt
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Verify shipped shape data and enforce its public-release decision."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
13
|
+
PROVENANCE_PATH = ROOT / "DATA_PROVENANCE.md"
|
|
14
|
+
PROVENANCE_RECORD_PATH = ROOT / "DATA_PROVENANCE.json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def audit() -> dict:
|
|
18
|
+
errors: list[str] = []
|
|
19
|
+
try:
|
|
20
|
+
parsed_record = json.loads(PROVENANCE_RECORD_PATH.read_text(encoding="utf-8"))
|
|
21
|
+
except (OSError, json.JSONDecodeError):
|
|
22
|
+
errors.append("DATA_PROVENANCE.json is missing or invalid")
|
|
23
|
+
parsed_record = {}
|
|
24
|
+
if not isinstance(parsed_record, dict):
|
|
25
|
+
errors.append("DATA_PROVENANCE.json must contain an object")
|
|
26
|
+
parsed_record = {}
|
|
27
|
+
|
|
28
|
+
datasets = parsed_record.get("datasets", [])
|
|
29
|
+
if (
|
|
30
|
+
not isinstance(datasets, list)
|
|
31
|
+
or len(datasets) != 1
|
|
32
|
+
or not isinstance(datasets[0], dict)
|
|
33
|
+
):
|
|
34
|
+
errors.append("expected exactly one declared shipped dataset object")
|
|
35
|
+
dataset = {}
|
|
36
|
+
else:
|
|
37
|
+
dataset = datasets[0]
|
|
38
|
+
|
|
39
|
+
shipped_file = dataset.get("shipped_file")
|
|
40
|
+
if not isinstance(shipped_file, str) or not shipped_file:
|
|
41
|
+
errors.append("declared dataset must have a non-empty shipped_file")
|
|
42
|
+
raw = b""
|
|
43
|
+
else:
|
|
44
|
+
shapes_path = ROOT / shipped_file
|
|
45
|
+
try:
|
|
46
|
+
raw = shapes_path.read_bytes()
|
|
47
|
+
except OSError:
|
|
48
|
+
errors.append("declared shape data is missing or unreadable")
|
|
49
|
+
raw = b""
|
|
50
|
+
|
|
51
|
+
checksum = hashlib.sha256(raw).hexdigest()
|
|
52
|
+
if checksum != dataset.get("sha256"):
|
|
53
|
+
errors.append("shape data checksum differs from DATA_PROVENANCE.json")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
rows = json.loads(raw)
|
|
57
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
58
|
+
errors.append("declared shape data is not valid JSON")
|
|
59
|
+
rows = []
|
|
60
|
+
|
|
61
|
+
expected_rows = dataset.get("rows")
|
|
62
|
+
if not isinstance(rows, list) or len(rows) != expected_rows:
|
|
63
|
+
errors.append(f"expected {expected_rows} shape rows")
|
|
64
|
+
rows = rows if isinstance(rows, list) else []
|
|
65
|
+
|
|
66
|
+
required_fields = set(dataset.get("required_fields", []))
|
|
67
|
+
designations: list[str] = []
|
|
68
|
+
for index, row in enumerate(rows):
|
|
69
|
+
if not isinstance(row, dict):
|
|
70
|
+
errors.append(f"shape row {index} is not an object")
|
|
71
|
+
continue
|
|
72
|
+
missing = required_fields - row.keys()
|
|
73
|
+
if missing:
|
|
74
|
+
errors.append(f"shape row {index} is missing {sorted(missing)}")
|
|
75
|
+
designation = row.get("designation")
|
|
76
|
+
if isinstance(designation, str):
|
|
77
|
+
designations.append(designation)
|
|
78
|
+
|
|
79
|
+
if len(designations) != len(set(designations)):
|
|
80
|
+
errors.append("shape designations are not unique")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
provenance = PROVENANCE_PATH.read_text(encoding="utf-8")
|
|
84
|
+
except OSError:
|
|
85
|
+
errors.append("DATA_PROVENANCE.md is missing or unreadable")
|
|
86
|
+
provenance = ""
|
|
87
|
+
|
|
88
|
+
permission = dataset.get("redistribution_permission")
|
|
89
|
+
release_readiness = dataset.get("release_readiness")
|
|
90
|
+
if release_readiness == "ready" and permission != "authorized":
|
|
91
|
+
errors.append("release-ready data must have authorized redistribution")
|
|
92
|
+
|
|
93
|
+
documented_values = (
|
|
94
|
+
dataset.get("dataset_name"),
|
|
95
|
+
dataset.get("source"),
|
|
96
|
+
dataset.get("copyright_holder"),
|
|
97
|
+
dataset.get("license"),
|
|
98
|
+
dataset.get("authorization"),
|
|
99
|
+
str(expected_rows),
|
|
100
|
+
dataset.get("sha256"),
|
|
101
|
+
"Redistribution permission | Authorized",
|
|
102
|
+
"Release gate | Ready",
|
|
103
|
+
)
|
|
104
|
+
if not all(value and value in provenance for value in documented_values):
|
|
105
|
+
errors.append("DATA_PROVENANCE.md differs from the machine-readable record")
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"schema_version": "1.0.0",
|
|
109
|
+
"integrity": "passed" if not errors else "failed",
|
|
110
|
+
"dataset_id": dataset.get("dataset_id"),
|
|
111
|
+
"dataset_name": dataset.get("dataset_name"),
|
|
112
|
+
"copyright_holder": dataset.get("copyright_holder"),
|
|
113
|
+
"license": dataset.get("license"),
|
|
114
|
+
"release_readiness": release_readiness,
|
|
115
|
+
"redistribution_permission": permission,
|
|
116
|
+
"shape_rows": len(rows),
|
|
117
|
+
"sha256": checksum,
|
|
118
|
+
"errors": errors,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def main(argv=None) -> int:
|
|
123
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"--release",
|
|
126
|
+
action="store_true",
|
|
127
|
+
help="fail while redistribution permission is unresolved",
|
|
128
|
+
)
|
|
129
|
+
args = parser.parse_args(argv)
|
|
130
|
+
report = audit()
|
|
131
|
+
print(json.dumps(report, indent=2, sort_keys=True))
|
|
132
|
+
if report["errors"]:
|
|
133
|
+
return 1
|
|
134
|
+
if args.release and report["release_readiness"] != "ready":
|
|
135
|
+
return 2
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
raise SystemExit(main())
|