@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.
Files changed (47) hide show
  1. package/DATA_PROVENANCE.json +23 -0
  2. package/DATA_PROVENANCE.md +35 -0
  3. package/PUBLIC_DATA_POLICY.md +52 -0
  4. package/README.md +114 -33
  5. package/docs/assets/pi-steel-demo.gif +0 -0
  6. package/docs/assets/pi-steel-gallery.webp +0 -0
  7. package/package.json +35 -4
  8. package/pyproject.toml +28 -0
  9. package/requirements-dev.txt +5 -0
  10. package/requirements-render.txt +1 -0
  11. package/requirements-tested.txt +9 -0
  12. package/requirements.txt +7 -0
  13. package/scripts/check-data-provenance.py +140 -0
  14. package/scripts/check-public-data.py +401 -0
  15. package/scripts/doctor.py +127 -0
  16. package/skills/_shared/bootstrap.py +32 -0
  17. package/skills/_shared/pi_steel/__init__.py +47 -0
  18. package/skills/_shared/pi_steel/cli.py +167 -0
  19. package/skills/_shared/pi_steel/contracts.py +87 -0
  20. package/skills/_shared/pi_steel/geometry_verify.py +243 -0
  21. package/skills/_shared/pi_steel/parsing.py +205 -0
  22. package/skills/_shared/pi_steel/run_manifest.py +317 -0
  23. package/skills/_shared/pi_steel/validation.py +597 -0
  24. package/skills/_shared/schemas/estimate-package.schema.json +424 -0
  25. package/skills/_shared/schemas/nest-result.schema.json +443 -0
  26. package/skills/_shared/schemas/run-manifest.schema.json +145 -0
  27. package/skills/steel-estimate/SKILL.md +119 -0
  28. package/skills/steel-estimate/references/estimate-package-example.json +91 -0
  29. package/skills/steel-estimate/references/output-contract.md +47 -0
  30. package/skills/steel-estimate/scripts/acknowledge-finding.py +193 -0
  31. package/skills/steel-estimate/scripts/build-estimate-package.py +836 -0
  32. package/skills/steel-nest/SKILL.md +50 -23
  33. package/skills/steel-nest/references/FIXTURE_PROVENANCE.md +12 -0
  34. package/skills/steel-nest/references/example_job.json +21 -25
  35. package/skills/steel-nest/references/job_template.json +11 -9
  36. package/skills/steel-nest/scripts/nest.py +1276 -250
  37. package/skills/steel-rfq/SKILL.md +93 -175
  38. package/skills/steel-rfq/assets/company-profile.example.json +11 -5
  39. package/skills/steel-rfq/references/rfq-input.md +58 -0
  40. package/skills/steel-rfq/scripts/generate-rfq.py +1221 -0
  41. package/skills/steel-rfq/scripts/recalc.py +33 -10
  42. package/skills/steel-takeoff/SKILL.md +12 -8
  43. package/skills/steel-takeoff/assets/bom-template.csv +1 -1
  44. package/skills/steel-takeoff/references/takeoff-procedures.md +3 -1
  45. package/skills/steel-takeoff/scripts/calculate-weight.sh +5 -10
  46. package/skills/steel-takeoff/scripts/lookup-member.sh +2 -2
  47. package/skills/steel-takeoff/scripts/validate-bom.py +151 -196
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env python3
2
+ """Fail when public repository files contain known private-data indicators."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import hashlib
8
+ import re
9
+ import subprocess
10
+ import sys
11
+ from bisect import bisect_left
12
+ from pathlib import Path
13
+
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+ SKIP_PARTS = {".git", ".a5c", "__pycache__", "node_modules"}
17
+ SKIP_FILES = {Path("scripts/check-public-data.py")}
18
+ FORBIDDEN_BINARY_SUFFIXES = {
19
+ ".doc",
20
+ ".docx",
21
+ ".dxf",
22
+ ".jpeg",
23
+ ".jpg",
24
+ ".pdf",
25
+ ".png",
26
+ ".xls",
27
+ ".xlsx",
28
+ }
29
+ SENSITIVE_SUFFIXES = {".key", ".p12", ".pem", ".pfx"}
30
+ PRIVATE_KEY_NAMES = {
31
+ "id_dsa",
32
+ "id_ecdsa",
33
+ "id_ed25519",
34
+ "id_rsa",
35
+ "private-key",
36
+ "private_key",
37
+ }
38
+ AUDITED_PUBLIC_BINARY_SHA256 = {
39
+ Path("docs/assets/pi-steel-demo.gif"):
40
+ "ae6ad7286fc5f1eca31e960d4ac4414b7a32a566b975ddacc7d662824c34d91c",
41
+ Path("docs/assets/pi-steel-gallery.webp"):
42
+ "9d8f2b1dedb00fa6c53c78f4578a12225f84bded4f7bbabe33182486372d12a8",
43
+ }
44
+ PATTERNS = {
45
+ "private operating-company claim": re.compile(
46
+ r"team behind (?:a|the) production structural[- ]steel", re.I
47
+ ),
48
+ "local absolute path": re.compile(r"(?:/Users/|/home/|[A-Z]:\\\\Users\\\\)"),
49
+ "realistic sales-order identifier": re.compile(r"\bSO-\d{3,}\b", re.I),
50
+ "current-market pricing claim": re.compile(r"\bcurrent (?:market )?rates?\b", re.I),
51
+ "contact email": re.compile(r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", re.I),
52
+ "phone number": re.compile(
53
+ r"(?<!\d)(?:\+?1[-. ]?)?\(?[2-9]\d{2}\)?[-. ]\d{3}[-. ]\d{4}(?!\d)"
54
+ ),
55
+ "credential assignment": re.compile(
56
+ r"\b(?:api[_-]?key|password|secret|token)\s*[:=]\s*"
57
+ r"(?:[\"'][^\"'\r\n]+[\"']|[^\s#;,]+)",
58
+ re.I,
59
+ ),
60
+ "private key material": re.compile(r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----"),
61
+ "AWS access key identifier": re.compile(r"\b(?:AKIA|ASIA)[A-Z0-9]{16}\b"),
62
+ "GitHub access token": re.compile(
63
+ r"\b(?:gh[opusr]_[A-Za-z0-9]{36,255}|github_pat_[A-Za-z0-9_]{22,255})\b"
64
+ ),
65
+ "OpenAI API key": re.compile(r"\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b"),
66
+ }
67
+
68
+
69
+ def is_audited_public_binary(relative: Path, content: bytes) -> bool:
70
+ expected = AUDITED_PUBLIC_BINARY_SHA256.get(relative)
71
+ return expected is not None and hashlib.sha256(content).hexdigest() == expected
72
+
73
+
74
+ def tracked_files(root: Path = ROOT) -> list[Path]:
75
+ result = subprocess.run(
76
+ ["git", "ls-files", "--cached", "--others", "--exclude-standard"],
77
+ cwd=root,
78
+ check=False,
79
+ capture_output=True,
80
+ text=True,
81
+ )
82
+ if result.returncode == 0:
83
+ return [root / line for line in result.stdout.splitlines() if line]
84
+ return [
85
+ path
86
+ for path in root.rglob("*")
87
+ if path.is_file() and not any(part in SKIP_PARTS for part in path.parts)
88
+ ]
89
+
90
+
91
+ def scan_patterns(root: Path = ROOT) -> dict[str, re.Pattern[str]]:
92
+ patterns = dict(PATTERNS)
93
+ private_terms = root / ".pi-steel" / "private-terms.txt"
94
+ if private_terms.is_file():
95
+ terms = [
96
+ line.strip()
97
+ for line in private_terms.read_text(encoding="utf-8").splitlines()
98
+ if line.strip() and not line.lstrip().startswith("#")
99
+ ]
100
+ if terms:
101
+ patterns["private local denylist term"] = re.compile(
102
+ "|".join(re.escape(term) for term in sorted(terms, key=len, reverse=True)),
103
+ re.I,
104
+ )
105
+ return patterns
106
+
107
+
108
+ def sensitive_path_reason(relative: Path) -> str | None:
109
+ name = relative.name.lower()
110
+ if name == ".env" or name.startswith(".env."):
111
+ return "sensitive environment file"
112
+ if relative.suffix.lower() in SENSITIVE_SUFFIXES:
113
+ return "sensitive key or certificate file"
114
+ if name in PRIVATE_KEY_NAMES:
115
+ return "private key file"
116
+ return None
117
+
118
+
119
+ def scan_paths(
120
+ paths: list[Path],
121
+ *,
122
+ root: Path = ROOT,
123
+ patterns: dict[str, re.Pattern[str]] | None = None,
124
+ ) -> list[str]:
125
+ findings: list[str] = []
126
+ active_patterns = scan_patterns(root) if patterns is None else patterns
127
+ for path in paths:
128
+ relative = path.relative_to(root)
129
+ if (
130
+ relative in SKIP_FILES
131
+ or any(part in SKIP_PARTS for part in relative.parts)
132
+ or not path.is_file()
133
+ ):
134
+ continue
135
+ sensitive_reason = sensitive_path_reason(relative)
136
+ if sensitive_reason:
137
+ findings.append(f"{relative}: {sensitive_reason}")
138
+ continue
139
+ suffix = path.suffix.lower()
140
+ if suffix in FORBIDDEN_BINARY_SUFFIXES:
141
+ findings.append(f"{relative}: public repository must not contain {suffix} artifacts")
142
+ continue
143
+ content = path.read_bytes()
144
+ if is_audited_public_binary(relative, content):
145
+ continue
146
+ try:
147
+ text = content.decode("utf-8")
148
+ except UnicodeDecodeError:
149
+ findings.append(f"{relative}: unknown binary file")
150
+ continue
151
+ if "\x00" in text:
152
+ findings.append(f"{relative}: unknown binary file")
153
+ continue
154
+ newline_offsets = [
155
+ index for index, character in enumerate(text) if character == "\n"
156
+ ]
157
+ for label, pattern in active_patterns.items():
158
+ for match in pattern.finditer(text):
159
+ line = bisect_left(newline_offsets, match.start()) + 1
160
+ findings.append(f"{relative}:{line}: {label}")
161
+ return findings
162
+
163
+
164
+ def _scan_bytes(
165
+ relative: Path,
166
+ content: bytes,
167
+ *,
168
+ patterns: dict[str, re.Pattern[str]],
169
+ prefix: str = "",
170
+ ) -> list[str]:
171
+ if relative in SKIP_FILES or any(part in SKIP_PARTS for part in relative.parts):
172
+ return []
173
+ sensitive_reason = sensitive_path_reason(relative)
174
+ if sensitive_reason:
175
+ return [f"{prefix}{relative}: {sensitive_reason}"]
176
+ suffix = relative.suffix.lower()
177
+ if suffix in FORBIDDEN_BINARY_SUFFIXES:
178
+ return [
179
+ f"{prefix}{relative}: public repository must not contain {suffix} artifacts"
180
+ ]
181
+ if is_audited_public_binary(relative, content):
182
+ return []
183
+ try:
184
+ text = content.decode("utf-8")
185
+ except UnicodeDecodeError:
186
+ return [f"{prefix}{relative}: unknown binary file"]
187
+ if "\x00" in text:
188
+ return [f"{prefix}{relative}: unknown binary file"]
189
+
190
+ findings: list[str] = []
191
+ newline_offsets = [
192
+ index for index, character in enumerate(text) if character == "\n"
193
+ ]
194
+ for label, pattern in patterns.items():
195
+ for match in pattern.finditer(text):
196
+ line = bisect_left(newline_offsets, match.start()) + 1
197
+ findings.append(f"{prefix}{relative}:{line}: {label}")
198
+ return findings
199
+
200
+
201
+ def staged_findings(
202
+ root: Path = ROOT,
203
+ *,
204
+ patterns: dict[str, re.Pattern[str]] | None = None,
205
+ ) -> list[str]:
206
+ """Scan the exact stage-zero blobs that would be included in the next commit."""
207
+ changed = subprocess.run(
208
+ ["git", "diff", "--cached", "--name-only", "-z", "--diff-filter=ACMR"],
209
+ cwd=root,
210
+ check=False,
211
+ capture_output=True,
212
+ )
213
+ if changed.returncode != 0:
214
+ return []
215
+
216
+ active_patterns = scan_patterns(root) if patterns is None else patterns
217
+ findings: list[str] = []
218
+ blob_cache: dict[str, bytes] = {}
219
+ for raw_path in changed.stdout.split(b"\0"):
220
+ if not raw_path:
221
+ continue
222
+ relative = Path(raw_path.decode("utf-8", errors="surrogateescape"))
223
+ index_entry = subprocess.run(
224
+ ["git", "ls-files", "--stage", "-z", "--", str(relative)],
225
+ cwd=root,
226
+ check=False,
227
+ capture_output=True,
228
+ )
229
+ entries = [entry for entry in index_entry.stdout.split(b"\0") if entry]
230
+ stage_zero = [
231
+ entry for entry in entries if entry.split(b"\t", 1)[0].endswith(b" 0")
232
+ ]
233
+ if not stage_zero:
234
+ continue
235
+ metadata, _ = stage_zero[0].split(b"\t", 1)
236
+ _, object_id, _ = metadata.decode("ascii").split()
237
+ if object_id not in blob_cache:
238
+ blob = subprocess.run(
239
+ ["git", "cat-file", "blob", object_id],
240
+ cwd=root,
241
+ check=False,
242
+ capture_output=True,
243
+ )
244
+ if blob.returncode != 0:
245
+ continue
246
+ blob_cache[object_id] = blob.stdout
247
+ findings.extend(
248
+ _scan_bytes(
249
+ relative,
250
+ blob_cache[object_id],
251
+ patterns=active_patterns,
252
+ prefix="staged ",
253
+ )
254
+ )
255
+ return findings
256
+
257
+
258
+ def revision_findings(
259
+ revision_args: list[str],
260
+ root: Path = ROOT,
261
+ *,
262
+ patterns: dict[str, re.Pattern[str]] | None = None,
263
+ ) -> list[str]:
264
+ """Scan committed blobs reachable from the supplied ``git rev-list`` arguments."""
265
+ commits = subprocess.run(
266
+ ["git", "rev-list", *revision_args],
267
+ cwd=root,
268
+ check=False,
269
+ capture_output=True,
270
+ text=True,
271
+ )
272
+ if commits.returncode != 0:
273
+ raise ValueError(commits.stderr.strip() or "invalid Git revision")
274
+
275
+ active_patterns = scan_patterns(root) if patterns is None else patterns
276
+ findings: list[str] = []
277
+ blob_cache: dict[str, bytes] = {}
278
+ scanned: set[tuple[str, Path]] = set()
279
+ for commit in commits.stdout.splitlines():
280
+ tree = subprocess.run(
281
+ ["git", "ls-tree", "-r", "-z", commit],
282
+ cwd=root,
283
+ check=False,
284
+ capture_output=True,
285
+ )
286
+ if tree.returncode != 0:
287
+ continue
288
+ for entry in tree.stdout.split(b"\0"):
289
+ if not entry:
290
+ continue
291
+ metadata, raw_path = entry.split(b"\t", 1)
292
+ _, object_type, object_id = metadata.decode("ascii").split()
293
+ if object_type != "blob":
294
+ continue
295
+ relative = Path(raw_path.decode("utf-8", errors="surrogateescape"))
296
+ identity = (object_id, relative)
297
+ if identity in scanned:
298
+ continue
299
+ scanned.add(identity)
300
+ if object_id not in blob_cache:
301
+ blob = subprocess.run(
302
+ ["git", "cat-file", "blob", object_id],
303
+ cwd=root,
304
+ check=False,
305
+ capture_output=True,
306
+ )
307
+ if blob.returncode != 0:
308
+ continue
309
+ blob_cache[object_id] = blob.stdout
310
+ findings.extend(
311
+ _scan_bytes(
312
+ relative,
313
+ blob_cache[object_id],
314
+ patterns=active_patterns,
315
+ prefix=f"commit {commit[:12]} ",
316
+ )
317
+ )
318
+ return findings
319
+
320
+
321
+ def revision_metadata_findings(
322
+ revision_args: list[str],
323
+ root: Path = ROOT,
324
+ ) -> list[str]:
325
+ """Report non-noreply author addresses without exposing the address itself."""
326
+ commits = subprocess.run(
327
+ ["git", "log", "--format=%H%x00%ae", *revision_args],
328
+ cwd=root,
329
+ check=False,
330
+ capture_output=True,
331
+ text=True,
332
+ )
333
+ if commits.returncode != 0:
334
+ raise ValueError(commits.stderr.strip() or "invalid Git revision")
335
+
336
+ findings: list[str] = []
337
+ seen_addresses: set[str] = set()
338
+ for entry in commits.stdout.splitlines():
339
+ if "\0" not in entry:
340
+ continue
341
+ commit, address = entry.split("\0", 1)
342
+ normalized = address.strip().lower()
343
+ if (
344
+ not normalized
345
+ or normalized in seen_addresses
346
+ or normalized.endswith("@users.noreply.github.com")
347
+ ):
348
+ continue
349
+ seen_addresses.add(normalized)
350
+ findings.append(
351
+ f"commit {commit[:12]} author metadata: non-noreply email address"
352
+ )
353
+ return findings
354
+
355
+
356
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
357
+ parser = argparse.ArgumentParser(
358
+ description="Check public repository content without printing matched values."
359
+ )
360
+ revisions = parser.add_mutually_exclusive_group()
361
+ revisions.add_argument(
362
+ "--history",
363
+ action="store_true",
364
+ help="also scan every commit reachable from every local ref",
365
+ )
366
+ revisions.add_argument(
367
+ "--range",
368
+ metavar="REVISION_RANGE",
369
+ help="also scan commits selected by a git rev-list revision range",
370
+ )
371
+ return parser.parse_args(argv)
372
+
373
+
374
+ def main(argv: list[str] | None = None) -> int:
375
+ args = parse_args(argv)
376
+ findings = scan_paths(tracked_files(), root=ROOT)
377
+ findings.extend(staged_findings(ROOT))
378
+ if args.history:
379
+ findings.extend(revision_findings(["--all"], ROOT))
380
+ findings.extend(revision_metadata_findings(["--all"], ROOT))
381
+ elif args.range:
382
+ try:
383
+ findings.extend(revision_findings([args.range], ROOT))
384
+ findings.extend(revision_metadata_findings([args.range], ROOT))
385
+ except ValueError as error:
386
+ print(f"Public-data check could not scan revisions: {error}", file=sys.stderr)
387
+ return 2
388
+ findings = list(dict.fromkeys(findings))
389
+
390
+ if findings:
391
+ print("Public-data check failed:", file=sys.stderr)
392
+ for finding in findings:
393
+ print(f" - {finding}", file=sys.stderr)
394
+ return 1
395
+
396
+ print("Public-data check passed.")
397
+ return 0
398
+
399
+
400
+ if __name__ == "__main__":
401
+ raise SystemExit(main())
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ """Diagnose the supported pi-steel Python runtime and optional capabilities."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import importlib
8
+ import importlib.util
9
+ import json
10
+ import shutil
11
+ import sys
12
+ from importlib.metadata import PackageNotFoundError, version
13
+ from pathlib import Path
14
+
15
+
16
+ SHARED_ROOT = Path(__file__).resolve().parents[1] / "skills" / "_shared"
17
+ sys.path.insert(0, str(SHARED_ROOT))
18
+ bootstrap_shared = getattr(importlib.import_module("bootstrap"), "bootstrap_shared")
19
+ bootstrap_shared(__file__)
20
+ outcome_exit_code = getattr(importlib.import_module("pi_steel"), "outcome_exit_code")
21
+
22
+
23
+ REQUIRED_MODULES = {
24
+ "jsonschema": "contract validation",
25
+ "openpyxl": "RFQ workbook generation",
26
+ "pandas": "spreadsheet adapters",
27
+ }
28
+ OPTIONAL_CAPABILITIES = {
29
+ "nest_rendering": ("ezdxf", "matplotlib", "numpy"),
30
+ }
31
+
32
+
33
+ def _module_status(name, module_finder):
34
+ available = module_finder(name) is not None
35
+ item = {"name": name, "kind": "python_module", "available": available}
36
+ if available:
37
+ try:
38
+ item["version"] = version(name)
39
+ except PackageNotFoundError:
40
+ item["version"] = "unknown"
41
+ return item
42
+
43
+
44
+ def diagnose(
45
+ *,
46
+ version_info=None,
47
+ module_finder=importlib.util.find_spec,
48
+ command_finder=shutil.which,
49
+ ):
50
+ current = version_info or sys.version_info
51
+ python_supported = (3, 11) <= tuple(current[:2]) < (3, 14)
52
+ required = [
53
+ {
54
+ "name": "python",
55
+ "kind": "runtime",
56
+ "available": python_supported,
57
+ "version": ".".join(str(value) for value in current[:3]),
58
+ "supported": ">=3.11,<3.14",
59
+ }
60
+ ]
61
+ for name, purpose in REQUIRED_MODULES.items():
62
+ item = _module_status(name, module_finder)
63
+ item["purpose"] = purpose
64
+ required.append(item)
65
+
66
+ jq_path = command_finder("jq")
67
+ required.append(
68
+ {
69
+ "name": "jq",
70
+ "kind": "command",
71
+ "available": jq_path is not None,
72
+ "path": jq_path,
73
+ "purpose": "structural shape lookup helpers",
74
+ }
75
+ )
76
+
77
+ optional = {}
78
+ for capability, modules in OPTIONAL_CAPABILITIES.items():
79
+ checks = [_module_status(name, module_finder) for name in modules]
80
+ optional[capability] = {
81
+ "available": all(check["available"] for check in checks),
82
+ "dependencies": checks,
83
+ }
84
+
85
+ office_path = command_finder("soffice") or command_finder("libreoffice")
86
+ optional["formula_baking"] = {
87
+ "available": office_path is not None,
88
+ "dependencies": [
89
+ {
90
+ "name": "libreoffice",
91
+ "kind": "command",
92
+ "available": office_path is not None,
93
+ "path": office_path,
94
+ }
95
+ ],
96
+ }
97
+
98
+ missing = [item["name"] for item in required if not item["available"]]
99
+ outcome = "dependency_missing" if missing else "ready"
100
+ return {
101
+ "schema_version": "1.0.0",
102
+ "run_outcome": outcome,
103
+ "exit_code": outcome_exit_code(outcome),
104
+ "required": required,
105
+ "optional_capabilities": optional,
106
+ "missing_required": missing,
107
+ }
108
+
109
+
110
+ def main(argv=None):
111
+ parser = argparse.ArgumentParser(description=__doc__)
112
+ parser.add_argument(
113
+ "--json",
114
+ action="store_true",
115
+ help="emit compact JSON instead of indented JSON",
116
+ )
117
+ args = parser.parse_args(argv)
118
+ report = diagnose()
119
+ if args.json:
120
+ print(json.dumps(report, sort_keys=True, separators=(",", ":")))
121
+ else:
122
+ print(json.dumps(report, indent=2, sort_keys=True))
123
+ return report["exit_code"]
124
+
125
+
126
+ if __name__ == "__main__":
127
+ raise SystemExit(main())
@@ -0,0 +1,32 @@
1
+ """Relocatable import bootstrap for scripts shipped inside the npm package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ class BootstrapError(RuntimeError):
10
+ """Raised when an entry point cannot locate the shipped skills tree."""
11
+
12
+
13
+ def find_skills_root(entry_file: str | Path) -> Path:
14
+ """Find ``skills/`` relative to an entry point, without using the cwd."""
15
+ entry = Path(entry_file).resolve()
16
+ for parent in entry.parents:
17
+ if parent.name == "skills" and (parent / "_shared").is_dir():
18
+ return parent
19
+ candidate = parent / "skills"
20
+ if (candidate / "_shared").is_dir():
21
+ return candidate
22
+ raise BootstrapError(f"cannot locate the shipped skills directory from {entry.name}")
23
+
24
+
25
+ def bootstrap_shared(entry_file: str | Path) -> Path:
26
+ """Put the shipped shared module directory on ``sys.path`` and return skills root."""
27
+ skills_root = find_skills_root(entry_file)
28
+ shared_root = skills_root / "_shared"
29
+ shared_text = str(shared_root)
30
+ if shared_text not in sys.path:
31
+ sys.path.insert(0, shared_text)
32
+ return skills_root
@@ -0,0 +1,47 @@
1
+ """Shared deterministic runtime primitives for pi-steel skills."""
2
+
3
+ from .cli import StageArgumentParser, package_version, publish_failure_diagnostic
4
+ from .contracts import (
5
+ ESTIMATE_PACKAGE_VERSION,
6
+ ITEM_INTENTS,
7
+ NEST_RESULT_VERSION,
8
+ estimate_input_hash,
9
+ instance_ids,
10
+ item_id_for,
11
+ placement_ids,
12
+ )
13
+ from .run_manifest import (
14
+ ARTIFACT_READINESS,
15
+ OUTCOME_EXIT_CODES,
16
+ PACKAGE_STATUSES,
17
+ RUN_OUTCOMES,
18
+ ManifestError,
19
+ RunPublisher,
20
+ canonical_json_bytes,
21
+ outcome_exit_code,
22
+ sha256_bytes,
23
+ sha256_file,
24
+ )
25
+
26
+ __all__ = [
27
+ "ARTIFACT_READINESS",
28
+ "ESTIMATE_PACKAGE_VERSION",
29
+ "ITEM_INTENTS",
30
+ "NEST_RESULT_VERSION",
31
+ "OUTCOME_EXIT_CODES",
32
+ "PACKAGE_STATUSES",
33
+ "RUN_OUTCOMES",
34
+ "ManifestError",
35
+ "RunPublisher",
36
+ "StageArgumentParser",
37
+ "canonical_json_bytes",
38
+ "estimate_input_hash",
39
+ "instance_ids",
40
+ "item_id_for",
41
+ "placement_ids",
42
+ "outcome_exit_code",
43
+ "package_version",
44
+ "publish_failure_diagnostic",
45
+ "sha256_bytes",
46
+ "sha256_file",
47
+ ]