@structupath/pi-steel 0.2.1 → 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
- package/skills/steel-rfq/scripts/__pycache__/recalc.cpython-312.pyc +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: steel-estimate
|
|
3
|
+
description: "Build a deterministic, review-gated steel estimate package from canonical estimate JSON. Use when a complete takeoff-to-nest-to-draft-RFQ workflow is needed with validation, lineage, QA, and isolated run artifacts."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Steel Estimate Package
|
|
7
|
+
|
|
8
|
+
## Boundary
|
|
9
|
+
|
|
10
|
+
This skill produces estimating and draft purchasing artifacts for human review.
|
|
11
|
+
It does not send an RFQ, select a vendor, approve substitutions, award work, or
|
|
12
|
+
authorize a purchase. The workbook remains `DRAFT — NOT SENT OR AWARDED`.
|
|
13
|
+
|
|
14
|
+
## Required inputs
|
|
15
|
+
|
|
16
|
+
- Canonical estimate-package JSON at schema version `1.0.0`.
|
|
17
|
+
- Explicit prepared and RFQ issue dates.
|
|
18
|
+
- A runtime company profile accepted by `steel-rfq`.
|
|
19
|
+
- Plate stock compatible with every plate part by material, grade, thickness,
|
|
20
|
+
and usable dimensions.
|
|
21
|
+
|
|
22
|
+
Use `references/estimate-package-example.json` as a synthetic input example.
|
|
23
|
+
The company profile resolution and approval rules are documented by
|
|
24
|
+
`../steel-rfq/SKILL.md`.
|
|
25
|
+
|
|
26
|
+
## Workflow and gates
|
|
27
|
+
|
|
28
|
+
The orchestrator composes the shared estimate validator, deterministic nesting
|
|
29
|
+
engine, RFQ compiler, and run publisher. It does not duplicate their
|
|
30
|
+
calculations.
|
|
31
|
+
|
|
32
|
+
1. Normalize and validate the canonical package.
|
|
33
|
+
2. Build the typed BOM projection without converting exclusions or allowances
|
|
34
|
+
into vendor quantities.
|
|
35
|
+
3. Nest plate parts only within compatible stock groups.
|
|
36
|
+
4. Verify placements and retain diagnostic nest artifacts.
|
|
37
|
+
5. Compile a draft RFQ only when validation, placement, and company-profile
|
|
38
|
+
gates pass.
|
|
39
|
+
6. Publish an isolated run manifest and QA report.
|
|
40
|
+
|
|
41
|
+
Validation failures, unplaced parts, invalid company data, failed nest
|
|
42
|
+
verification, or required rendering dependencies block workbook generation.
|
|
43
|
+
Complete bounding-box nests for irregular geometry may produce a workbook, but
|
|
44
|
+
the run and workbook remain explicitly review-required. Reference DXF is never
|
|
45
|
+
burn-ready DXF.
|
|
46
|
+
|
|
47
|
+
## Run
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python3 scripts/build-estimate-package.py \
|
|
51
|
+
--input <estimate-package.json> \
|
|
52
|
+
--out <publication-root> \
|
|
53
|
+
--prepared-date <YYYY-MM-DD> \
|
|
54
|
+
--issued-date <YYYY-MM-DD> \
|
|
55
|
+
--project-location "Example City, ST"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use `--no-render` when only JSON and workbook artifacts are needed. Use
|
|
59
|
+
`--no-bake` to defer formula calculation to spreadsheet open. Explicit dates
|
|
60
|
+
and unchanged input/configuration yield stable semantic artifacts.
|
|
61
|
+
|
|
62
|
+
Exit codes:
|
|
63
|
+
|
|
64
|
+
- `0`: package is ready for human RFQ review.
|
|
65
|
+
- `2`: a draft exists, but warnings or reference-only geometry require review.
|
|
66
|
+
- `3`: blocked; diagnostics are published and no workbook is produced.
|
|
67
|
+
- `4`: a required runtime dependency is missing; no workbook is produced.
|
|
68
|
+
- `1`: usage, file, or input parsing error.
|
|
69
|
+
|
|
70
|
+
See `references/output-contract.md` for artifact and status semantics.
|
|
71
|
+
|
|
72
|
+
## Review acknowledgements
|
|
73
|
+
|
|
74
|
+
The acknowledgement helper records a decision already made by a named human.
|
|
75
|
+
It never infers a disposition, chooses an actor or timestamp, or changes source
|
|
76
|
+
facts. Read the finding in `qa-report.json`, then supply every decision field
|
|
77
|
+
explicitly and write a new package:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python3 scripts/acknowledge-finding.py \
|
|
81
|
+
--input <estimate-package.json> \
|
|
82
|
+
--output <reviewed-estimate-package.json> \
|
|
83
|
+
--finding-id <finding-id> \
|
|
84
|
+
--input-hash <finding-relevant-hash> \
|
|
85
|
+
--actor "<reviewer name>" \
|
|
86
|
+
--timestamp <ISO-8601-date-time> \
|
|
87
|
+
--disposition accepted
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Allowed dispositions are `accepted`, `rejected`, and `deferred`. An accepted
|
|
91
|
+
warning is cleared only while its relevant input hash still matches. Recording
|
|
92
|
+
acceptance of a blocker does not waive that blocker. Re-run the estimate
|
|
93
|
+
pipeline with the newly written package and inspect the new QA report.
|
|
94
|
+
|
|
95
|
+
## Dependency recovery
|
|
96
|
+
|
|
97
|
+
If a command reports exit `4` or names a missing capability, run the package
|
|
98
|
+
doctor from the package root:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python3 scripts/doctor.py --json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Install the calculation, workbook, PNG, and DXF dependencies from
|
|
105
|
+
`requirements.txt`. Full PDF and LibreOffice-assisted checks also require the
|
|
106
|
+
system tools documented by `requirements-render.txt`.
|
|
107
|
+
`requirements-tested.txt` records the exact dependency versions exercised by
|
|
108
|
+
CI; it is a reproducibility reference, not the general installation range. Run
|
|
109
|
+
the doctor again after installation before retrying the estimate.
|
|
110
|
+
|
|
111
|
+
## Delivery checks
|
|
112
|
+
|
|
113
|
+
- Read `run-manifest.json` and `qa-report.json`; do not infer readiness from a
|
|
114
|
+
workbook filename.
|
|
115
|
+
- Confirm there are no blocker findings or unplaced parts.
|
|
116
|
+
- Confirm material, grade, thickness, source IDs, and replacement lineage.
|
|
117
|
+
- Surface every warning and approximation to the reviewer.
|
|
118
|
+
- Treat all workbook and rendered artifacts according to their manifest
|
|
119
|
+
readiness labels.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0.0",
|
|
3
|
+
"project": {
|
|
4
|
+
"project_id": "SYNTHETIC-EXAMPLE-001",
|
|
5
|
+
"name": "Synthetic Estimate Example",
|
|
6
|
+
"revision": {
|
|
7
|
+
"revision_id": "SYNTHETIC-REV-A",
|
|
8
|
+
"source_document": "SYNTHETIC-SOURCE-001"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"unit_system": "imperial",
|
|
12
|
+
"items": [
|
|
13
|
+
{
|
|
14
|
+
"intent": "fabricated_part",
|
|
15
|
+
"source_id": "SYNTHETIC-SRC-P1",
|
|
16
|
+
"item_id": "item:synthetic-example-p1",
|
|
17
|
+
"quantity": 2,
|
|
18
|
+
"mark": "P1",
|
|
19
|
+
"description": "Synthetic rectangular plate",
|
|
20
|
+
"material": "carbon_steel",
|
|
21
|
+
"grade": "A36",
|
|
22
|
+
"geometry": {
|
|
23
|
+
"shape": "rect",
|
|
24
|
+
"width": 8,
|
|
25
|
+
"height": 6,
|
|
26
|
+
"thickness": 0.5,
|
|
27
|
+
"holes": [],
|
|
28
|
+
"rotatable": true
|
|
29
|
+
},
|
|
30
|
+
"source_evidence": [
|
|
31
|
+
{
|
|
32
|
+
"source": "SYNTHETIC-SOURCE-001",
|
|
33
|
+
"locator": "SYNTHETIC-DETAIL-P1"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"intent": "purchased_stock",
|
|
39
|
+
"source_id": "SYNTHETIC-SRC-S1",
|
|
40
|
+
"item_id": "item:synthetic-example-s1",
|
|
41
|
+
"quantity": 1,
|
|
42
|
+
"description": "Synthetic stock plate purchase",
|
|
43
|
+
"material": "carbon_steel",
|
|
44
|
+
"grade": "A36",
|
|
45
|
+
"specification": "PLATE",
|
|
46
|
+
"dimensions": {
|
|
47
|
+
"category": "PLATE STOCK",
|
|
48
|
+
"width": 24,
|
|
49
|
+
"height": 12,
|
|
50
|
+
"thickness": 0.5,
|
|
51
|
+
"size": "24 x 12 x 0.5",
|
|
52
|
+
"replaces_item_ids": [
|
|
53
|
+
"item:synthetic-example-p1"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"source_evidence": [
|
|
57
|
+
{
|
|
58
|
+
"source": "SYNTHETIC-SOURCE-001",
|
|
59
|
+
"locator": "SYNTHETIC-STOCK-S1"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"stock": [
|
|
65
|
+
{
|
|
66
|
+
"stock_kind": "purchasable",
|
|
67
|
+
"inventory_id": "SYNTHETIC-STOCK-A36",
|
|
68
|
+
"material": "carbon_steel",
|
|
69
|
+
"grade": "A36",
|
|
70
|
+
"width": 24,
|
|
71
|
+
"height": 12,
|
|
72
|
+
"thickness": 0.5,
|
|
73
|
+
"quantity": 1,
|
|
74
|
+
"status": "available"
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
"commercial_basis": {
|
|
78
|
+
"currency": "USD",
|
|
79
|
+
"costs": []
|
|
80
|
+
},
|
|
81
|
+
"review": {
|
|
82
|
+
"status": "validated",
|
|
83
|
+
"findings": [],
|
|
84
|
+
"acknowledgements": []
|
|
85
|
+
},
|
|
86
|
+
"lineage": {
|
|
87
|
+
"source_type": "synthetic_example",
|
|
88
|
+
"source_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
89
|
+
"configuration_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Estimate package output contract
|
|
2
|
+
|
|
3
|
+
Each invocation publishes an immutable directory at `runs/<run-id>/` and
|
|
4
|
+
updates `latest-run.json` only after the run is complete. Read the manifest
|
|
5
|
+
before using any artifact.
|
|
6
|
+
|
|
7
|
+
## Core artifacts
|
|
8
|
+
|
|
9
|
+
| Artifact | Purpose | Produced when |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `estimate-package.json` | Deterministically ordered canonical input | Always |
|
|
12
|
+
| `normalized-bom.json` | Typed BOM and calculated weight projection | Always |
|
|
13
|
+
| `nest-result.json` | Placements, verification, utilization, and unplaced parts | Valid input contains plate parts |
|
|
14
|
+
| `rfq-nesting.json` | Versioned nesting lineage for RFQ compilation | A nest was attempted |
|
|
15
|
+
| `inventory-consumption.json` | Confirmed on-hand sheets consumed and corresponding RFQ demand reduction | Eligible on-hand inventory was consumed |
|
|
16
|
+
| `qa-report.json` | Findings, approximations, gate decisions, and recalculation status | Always |
|
|
17
|
+
| `run-manifest.json` | Input/configuration hashes and artifact hashes/readiness | Always |
|
|
18
|
+
| `<project>_RFQ_Material_List.xlsx` | Draft RFQ workbook | All blocking gates pass |
|
|
19
|
+
| `workbook-semantic.json` | Stable workbook content projection | Workbook exists |
|
|
20
|
+
|
|
21
|
+
Rendered PDF, PNG, and DXF files are optional. Reference artifacts are not
|
|
22
|
+
fabrication authority. Burn DXF is eligible only for a fully ready run with
|
|
23
|
+
verified exact geometry.
|
|
24
|
+
|
|
25
|
+
## Outcomes
|
|
26
|
+
|
|
27
|
+
- `ready` / `rfq_ready_for_review`: all gates pass; the workbook is still a
|
|
28
|
+
draft requiring human review.
|
|
29
|
+
- `review_required` / `rfq_draft_review_required`: a workbook exists, but
|
|
30
|
+
warnings or reference-only geometry must be resolved or accepted by a human.
|
|
31
|
+
- `blocked`: diagnostics exist, but no workbook exists.
|
|
32
|
+
- `dependency_missing`: required rendering support is absent and no workbook
|
|
33
|
+
exists.
|
|
34
|
+
|
|
35
|
+
Unplaced parts use `blocked` with package status `nested_partial`; their nest
|
|
36
|
+
diagnostics remain available. A validation failure stops before nesting.
|
|
37
|
+
|
|
38
|
+
## Determinism and lineage
|
|
39
|
+
|
|
40
|
+
Canonical JSON is sorted before publication. The manifest records the canonical
|
|
41
|
+
input hash, effective configuration hash, tool/schema versions, explicit dates,
|
|
42
|
+
and SHA-256 for each artifact. Run IDs and filesystem paths are volatile and do
|
|
43
|
+
not affect the semantic hash. A changed revision, quantity, profile, date, or
|
|
44
|
+
nest setting changes the applicable downstream lineage.
|
|
45
|
+
|
|
46
|
+
Never treat warnings, approximations, exclusions, allowances, or source
|
|
47
|
+
evidence as presentation-only metadata. They are part of the review contract.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Record one explicit human review decision in a canonical estimate package."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SHARED_ROOT = Path(__file__).resolve().parents[2] / "_shared"
|
|
17
|
+
if str(SHARED_ROOT) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(SHARED_ROOT))
|
|
19
|
+
from bootstrap import bootstrap_shared # noqa: E402
|
|
20
|
+
|
|
21
|
+
bootstrap_shared(__file__)
|
|
22
|
+
from pi_steel import canonical_json_bytes # noqa: E402
|
|
23
|
+
from pi_steel.validation import ( # noqa: E402
|
|
24
|
+
acknowledge_finding,
|
|
25
|
+
validate_estimate_package,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AcknowledgementInputError(ValueError):
|
|
30
|
+
"""Raised when an explicit acknowledgement cannot be recorded safely."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _valid_timestamp(value: str) -> bool:
|
|
34
|
+
try:
|
|
35
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
36
|
+
except ValueError:
|
|
37
|
+
return False
|
|
38
|
+
return "T" in value and parsed.tzinfo is not None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _findings_for_decision(
|
|
42
|
+
package: dict[str, Any], current_hash: str
|
|
43
|
+
) -> list[dict[str, Any]]:
|
|
44
|
+
generated = validate_estimate_package(package).findings
|
|
45
|
+
supplied = package.get("review", {}).get("findings", [])
|
|
46
|
+
combined: dict[tuple[str, str], dict[str, Any]] = {}
|
|
47
|
+
for finding in [*generated, *supplied]:
|
|
48
|
+
finding_id = finding.get("finding_id")
|
|
49
|
+
relevant_hash = finding.get("relevant_hash")
|
|
50
|
+
if isinstance(finding_id, str) and isinstance(relevant_hash, str):
|
|
51
|
+
combined[(finding_id, relevant_hash)] = finding
|
|
52
|
+
return [
|
|
53
|
+
finding
|
|
54
|
+
for finding in combined.values()
|
|
55
|
+
if finding.get("relevant_hash") == current_hash
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def record_acknowledgement(args: argparse.Namespace) -> dict[str, Any]:
|
|
60
|
+
input_path = Path(args.input).resolve()
|
|
61
|
+
output_path = Path(args.output).resolve()
|
|
62
|
+
if input_path == output_path:
|
|
63
|
+
raise AcknowledgementInputError(
|
|
64
|
+
"--output must be a new file; the source package is never overwritten"
|
|
65
|
+
)
|
|
66
|
+
if output_path.exists():
|
|
67
|
+
raise AcknowledgementInputError(
|
|
68
|
+
f"output already exists: {output_path}"
|
|
69
|
+
)
|
|
70
|
+
if not args.actor.strip():
|
|
71
|
+
raise AcknowledgementInputError("--actor must name the human reviewer")
|
|
72
|
+
if not _valid_timestamp(args.timestamp):
|
|
73
|
+
raise AcknowledgementInputError(
|
|
74
|
+
"--timestamp must be an explicit ISO-8601 date-time"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
package = json.loads(input_path.read_text(encoding="utf-8"))
|
|
79
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
80
|
+
raise AcknowledgementInputError(
|
|
81
|
+
f"estimate package could not be read: {exc}"
|
|
82
|
+
) from exc
|
|
83
|
+
if not isinstance(package, dict):
|
|
84
|
+
raise AcknowledgementInputError("estimate package must be a JSON object")
|
|
85
|
+
|
|
86
|
+
validation = validate_estimate_package(package)
|
|
87
|
+
if args.input_hash != validation.input_hash:
|
|
88
|
+
raise AcknowledgementInputError(
|
|
89
|
+
"--input-hash is stale for the current estimate input"
|
|
90
|
+
)
|
|
91
|
+
matches = [
|
|
92
|
+
finding
|
|
93
|
+
for finding in _findings_for_decision(package, validation.input_hash)
|
|
94
|
+
if finding["finding_id"] == args.finding_id
|
|
95
|
+
]
|
|
96
|
+
if not matches:
|
|
97
|
+
stale = [
|
|
98
|
+
finding
|
|
99
|
+
for finding in package.get("review", {}).get("findings", [])
|
|
100
|
+
if finding.get("finding_id") == args.finding_id
|
|
101
|
+
]
|
|
102
|
+
if stale:
|
|
103
|
+
raise AcknowledgementInputError(
|
|
104
|
+
"finding exists but is stale for the current estimate input hash"
|
|
105
|
+
)
|
|
106
|
+
raise AcknowledgementInputError(
|
|
107
|
+
"finding ID is not active for the current estimate input"
|
|
108
|
+
)
|
|
109
|
+
if len(matches) != 1:
|
|
110
|
+
raise AcknowledgementInputError(
|
|
111
|
+
"finding ID is ambiguous for the current estimate input"
|
|
112
|
+
)
|
|
113
|
+
finding = matches[0]
|
|
114
|
+
duplicate = any(
|
|
115
|
+
acknowledgement.get("finding_id") == args.finding_id
|
|
116
|
+
and acknowledgement.get("input_hash") == validation.input_hash
|
|
117
|
+
and acknowledgement.get("actor") == args.actor.strip()
|
|
118
|
+
and acknowledgement.get("timestamp") == args.timestamp
|
|
119
|
+
and acknowledgement.get("disposition") == args.disposition
|
|
120
|
+
for acknowledgement in package.get("review", {}).get(
|
|
121
|
+
"acknowledgements", []
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
if duplicate:
|
|
125
|
+
raise AcknowledgementInputError(
|
|
126
|
+
"this exact acknowledgement is already recorded"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
acknowledgement = acknowledge_finding(
|
|
130
|
+
package,
|
|
131
|
+
finding,
|
|
132
|
+
actor=args.actor.strip(),
|
|
133
|
+
timestamp=args.timestamp,
|
|
134
|
+
disposition=args.disposition,
|
|
135
|
+
)
|
|
136
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
file_descriptor, temporary_name = tempfile.mkstemp(
|
|
138
|
+
prefix=f".{output_path.name}.", dir=output_path.parent
|
|
139
|
+
)
|
|
140
|
+
try:
|
|
141
|
+
with os.fdopen(file_descriptor, "wb") as stream:
|
|
142
|
+
stream.write(canonical_json_bytes(package))
|
|
143
|
+
os.link(temporary_name, output_path)
|
|
144
|
+
finally:
|
|
145
|
+
if os.path.exists(temporary_name):
|
|
146
|
+
os.unlink(temporary_name)
|
|
147
|
+
return acknowledgement
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def parser() -> argparse.ArgumentParser:
|
|
151
|
+
result = argparse.ArgumentParser(description=__doc__)
|
|
152
|
+
result.add_argument("--input", required=True)
|
|
153
|
+
result.add_argument("--output", required=True)
|
|
154
|
+
result.add_argument("--finding-id", required=True)
|
|
155
|
+
result.add_argument(
|
|
156
|
+
"--input-hash",
|
|
157
|
+
required=True,
|
|
158
|
+
help="Relevant estimate input hash copied from the reviewed finding.",
|
|
159
|
+
)
|
|
160
|
+
result.add_argument("--actor", required=True)
|
|
161
|
+
result.add_argument("--timestamp", required=True)
|
|
162
|
+
result.add_argument(
|
|
163
|
+
"--disposition",
|
|
164
|
+
required=True,
|
|
165
|
+
choices=("accepted", "rejected", "deferred"),
|
|
166
|
+
)
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main(argv: list[str] | None = None) -> int:
|
|
171
|
+
args = parser().parse_args(argv)
|
|
172
|
+
try:
|
|
173
|
+
acknowledgement = record_acknowledgement(args)
|
|
174
|
+
except AcknowledgementInputError as exc:
|
|
175
|
+
print(f"Acknowledgement failed: {exc}", file=sys.stderr)
|
|
176
|
+
return 1
|
|
177
|
+
print(
|
|
178
|
+
json.dumps(
|
|
179
|
+
{
|
|
180
|
+
"finding_id": acknowledgement["finding_id"],
|
|
181
|
+
"disposition": acknowledgement["disposition"],
|
|
182
|
+
"input_hash": acknowledgement["input_hash"],
|
|
183
|
+
"recorded": True,
|
|
184
|
+
},
|
|
185
|
+
sort_keys=True,
|
|
186
|
+
separators=(",", ":"),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
raise SystemExit(main())
|