abelien-harness 0.4.0 → 0.6.0
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/.env.example +4 -0
- package/README.md +11 -18
- package/package.json +2 -3
- package/skills/abelien-appearance/SKILL.md +1 -0
- package/skills/abelien-clarify/SKILL.md +12 -2
- package/skills/abelien-clarify/references/examples.md +22 -0
- package/skills/abelien-clarify/scripts/check_result.py +38 -3
- package/skills/abelien-clarify/scripts/test_check_result.py +32 -1
- package/skills/abelien-guide/SKILL.md +1 -1
- package/skills/abelien-hardware/SKILL.md +10 -5
- package/skills/abelien-hardware/domain.json +3 -3
- package/skills/abelien-hardware/references/search-index-format.md +13 -1
- package/skills/abelien-hardware/references/search-index-release.md +4 -2
- package/skills/abelien-hardware/references/web-evidence-contract.md +3 -3
- package/skills/abelien-hardware/scripts/dimensions.mjs +23 -8
- package/skills/abelien-hardware/scripts/electrical.mjs +168 -0
- package/skills/abelien-hardware/scripts/finalize-selection.mjs +4 -2
- package/skills/abelien-hardware/scripts/provider-chain-contract.mjs +27 -6
- package/skills/abelien-hardware/scripts/public-page.mjs +11 -121
- package/skills/abelien-hardware/scripts/response-redaction.mjs +1 -130
- package/skills/abelien-hardware/scripts/search-index-release.mjs +30 -17
- package/skills/abelien-hardware/scripts/selection-request.mjs +6 -5
- package/skills/abelien-hardware/scripts/tos-wiki-search.mjs +13 -8
- package/skills/abelien-hardware/scripts/volc-web-search.mjs +10 -27
- package/skills/abelien-hardware/scripts/web-common.mjs +8 -11
- package/skills/abelien-hardware/scripts/web-integrity.mjs +1 -1
- package/skills/abelien-hardware/scripts/web-provider-chain.mjs +10 -3
- package/skills/abelien-hardware/scripts/web-receipt.mjs +4 -2
- package/skills/abelien-hardware/scripts/web-search-plan.mjs +21 -2
- package/skills/abelien-hardware/scripts/web-search.mjs +5 -2
- package/skills/abelien-hardware-selection/SKILL.md +1 -1
- package/skills/abelien-hardware-selection/domain.json +2 -2
- package/skills/abelien-hardware-selection/references/selection-service-contract.md +30 -3
- package/skills/abelien-hardware-selection/scripts/select.mjs +35 -8
- package/skills/abelien-harness/scripts/ab.mjs +24 -2
- package/skills/abelien-harness/scripts/web/common.mjs +20 -0
- package/skills/abelien-harness/scripts/web/public-read.mjs +168 -0
- package/skills/abelien-harness/scripts/web/redact.mjs +136 -0
- package/skills/abelien-harness/scripts/web/volc.mjs +49 -0
- package/skills/abelien-reference/SKILL.md +51 -0
- package/skills/abelien-reference/domain.json +92 -0
- package/skills/abelien-reference/fixtures/create.final.json +102 -0
- package/skills/abelien-reference/fixtures/restore.collected.json +189 -0
- package/skills/abelien-reference/fixtures/restore.final.json +267 -0
- package/skills/abelien-reference/references/research-contract.md +74 -0
- package/skills/abelien-reference/schemas/reference-evidence.v1.json +140 -0
- package/skills/abelien-reference/scripts/evidence.mjs +60 -0
- package/skills/abelien-reference/scripts/finalize.mjs +116 -0
- package/skills/abelien-reference/scripts/measure.mjs +62 -0
- package/skills/abelien-reference/scripts/png.mjs +86 -0
- package/skills/abelien-reference/scripts/research.mjs +179 -0
- package/skills/abelien-structure/SKILL.md +2 -2
- package/skills/abelien-structure/domain.json +64 -0
- package/skills/abelien-structure/references/build123d-execution.md +1 -1
- package/skills/abelien-structure/references/handoff.md +1 -1
- package/skills/abelien-structure/worker/abelien_cad.py +585 -0
- package/skills/abelien-structure/worker/assembly-validation.mjs +106 -0
- package/skills/abelien-structure/worker/cad.py +1623 -0
- package/skills/abelien-structure/worker/cli-adapter.mjs +207 -0
- package/skills/abelien-structure/worker/component-fit-glb.mjs +173 -0
- package/skills/abelien-structure/worker/component-fit.d.mts +82 -0
- package/skills/abelien-structure/worker/component-fit.mjs +167 -0
- package/skills/abelien-structure/worker/component-target-image.d.mts +23 -0
- package/skills/abelien-structure/worker/component-target-image.mjs +107 -0
- package/skills/abelien-structure/worker/component-target-render.py +58 -0
- package/skills/abelien-structure/worker/config.mjs +6 -0
- package/skills/abelien-structure/worker/engineering-fit.d.mts +48 -0
- package/skills/abelien-structure/worker/engineering-fit.mjs +118 -0
- package/skills/abelien-structure/worker/engineering-policy.json +16 -0
- package/skills/abelien-structure/worker/engineering-repair.py +147 -0
- package/skills/abelien-structure/worker/engineering.py +324 -0
- package/skills/abelien-structure/worker/identity.mjs +98 -0
- package/skills/abelien-structure/worker/inspection-geometry.mjs +71 -0
- package/skills/abelien-structure/worker/jobs.mjs +451 -0
- package/skills/abelien-structure/worker/library-runtime.mjs +15 -0
- package/skills/abelien-structure/worker/main.mjs +102 -0
- package/skills/abelien-structure/worker/manifest-digest.mjs +11 -0
- package/skills/abelien-structure/worker/materials.mjs +52 -0
- package/skills/abelien-structure/worker/native-launcher.py +13 -0
- package/skills/abelien-structure/worker/native.mjs +157 -0
- package/skills/abelien-structure/worker/representation.mjs +47 -0
- package/skills/abelien-structure/worker/sandbox.mjs +41 -0
- package/skills/abelien-structure/worker/source-contract.mjs +143 -0
- package/skills/abelien-structure/worker/source_call_stats.py +59 -0
- package/skills/abelien-structure/worker/supervisor.mjs +38 -0
- package/skills/abelien-workspace/SKILL.md +11 -0
- package/skills/abelien-workspace/assets/journey-stage-plan.json +155 -0
- package/skills/abelien-workspace/domain.json +25 -2
- package/skills/abelien-workspace/references/hardware-journey-orchestration.md +1 -1
- package/skills/abelien-workspace/references/harness-binding.md +2 -2
- package/skills/abelien-workspace/references/user-journey.md +10 -0
- package/skills/abelien-workspace/scripts/journey-next.mjs +38 -0
- package/skills/abelien-workspace/workflows/definitions/0852e4949f5a6246880c1a26bf5ccaa42f8cda06b960e7c7ce234a0a176878f9.json +229 -0
- package/skills/abelien-workspace/workflows/definitions/399ef372790b7330d0ef88ce5096e7f69943d4793de97b2ed6c25165a332612f.json +230 -0
- package/skills/abelien-workspace/workflows/definitions/591a7f21e0b0ad586e340163f7533ac910356091ba8d2a26e250c0acbc80cd8b.json +176 -0
- package/skills/abelien-workspace/workflows/definitions/7c69ea0d51099b1ae947f08b056ea67f5cad6b40ae8bcb00efb496fa5b9d5ed3.json +149 -0
- package/skills/abelien-workspace/workflows/definitions/9b5c65ad03fdecc0148931a7677cfa684a3fb04ab2738d6f7a1086a2c79ac86c.json +149 -0
- package/skills/abelien-workspace/workflows/definitions/c1b5c6c653f0c4761d51efee923aafbbeea05512e7f35c60f7f7d36ec49fc1d1.json +229 -0
- package/skills/abelien-workspace/workflows/definitions/d9f8c6cdf0b2c41cdeb1c72ac5d1e37d95a7caac3272d434a3c7e8483fc565de.json +150 -0
- package/skills/abelien-workspace/workflows/definitions/e391e14c14cb39408a0a9e226ed54ffab7a36badea305ae4a3eafbac8124fc02.json +175 -0
- package/skills/abelien-workspace/workflows/definitions/fd8ce7074e2044985778446e93153864940bb9465471c1c71c41328cefff70bf.json +175 -0
- package/skills/abelien-workspace/workflows/user-journey-hardware-algorithm.json +5 -4
- package/skills/abelien-workspace/workflows/user-journey-hardware-candidates.json +5 -4
- package/skills/abelien-workspace/workflows/user-journey-hardware.json +5 -4
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Trusted bounded analytic plate carrier + one faceted solid decoration.
|
|
2
|
+
Never executed as model source. Every final check consumes fresh STEP readback.
|
|
3
|
+
"""
|
|
4
|
+
import base64
|
|
5
|
+
import collections
|
|
6
|
+
import copy
|
|
7
|
+
import math
|
|
8
|
+
import json
|
|
9
|
+
import runpy
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import build123d as bd
|
|
12
|
+
import numpy as np
|
|
13
|
+
from OCP.BRepAdaptor import BRepAdaptor_Surface
|
|
14
|
+
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeSolid, BRepBuilderAPI_Sewing
|
|
15
|
+
from OCP.BRepCheck import BRepCheck_Analyzer
|
|
16
|
+
from OCP.BRep import BRep_Tool
|
|
17
|
+
from OCP.BRepLib import BRepLib
|
|
18
|
+
from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace
|
|
19
|
+
from OCP.BOPAlgo import BOPAlgo_CheckerSI
|
|
20
|
+
from OCP.GeomAbs import GeomAbs_Cylinder, GeomAbs_Plane
|
|
21
|
+
from OCP.TopAbs import TopAbs_SHELL
|
|
22
|
+
from OCP.TopExp import TopExp_Explorer
|
|
23
|
+
from OCP.TopTools import TopTools_ListOfShape
|
|
24
|
+
from OCP.TopoDS import TopoDS
|
|
25
|
+
from OCP.gp import gp_Pnt, gp_Pln, gp_Dir
|
|
26
|
+
|
|
27
|
+
VERSION = 'planar-two-hole-fit.v1'
|
|
28
|
+
POLICY = json.loads(Path(__file__).with_name('engineering-policy.json').read_text())
|
|
29
|
+
REPAIR = runpy.run_path(Path(__file__).with_name('engineering-repair.py'))
|
|
30
|
+
EPS = POLICY['linearToleranceMm']
|
|
31
|
+
VOL = POLICY['volumeToleranceMm3']
|
|
32
|
+
NOT_CHECKED = ['strength', 'manufacturability', 'physical_assembly', 'local_thin_features', 'texture_fidelity', 'hardware_geometry_accuracy']
|
|
33
|
+
CHECK_IDS = ['baseline', 'closed_solid', 'self_intersection', 'carrier_preservation', 'interface_surfaces',
|
|
34
|
+
'protected_regions', 'continuous_join', 'static_collision', 'clearance', 'planar_insertion',
|
|
35
|
+
'preserved_occurrences', 'step_readback']
|
|
36
|
+
|
|
37
|
+
def require(condition, code):
|
|
38
|
+
if not condition:
|
|
39
|
+
raise ValueError('engineering_' + code)
|
|
40
|
+
|
|
41
|
+
def keys(value, names):
|
|
42
|
+
return isinstance(value, dict) and set(value) == set(names)
|
|
43
|
+
|
|
44
|
+
def number(value, low=0, high=500):
|
|
45
|
+
return isinstance(value, (float, int)) and not isinstance(value, bool) and math.isfinite(value) and low < value <= high
|
|
46
|
+
|
|
47
|
+
def proposal(value):
|
|
48
|
+
require(keys(value, ['schema', 'targetOccurrenceId', 'baseOccurrenceId', 'plate', 'baseThicknessMm', 'decoration', 'join', 'clearanceMm']), 'proposal')
|
|
49
|
+
require(value['schema'] == 'abelien.planar-attachment-proposal.v1', 'proposal_schema')
|
|
50
|
+
p,d,j = value['plate'],value['decoration'],value['join']
|
|
51
|
+
require(keys(p, ['widthMm','depthMm','thicknessMm','holeRadiusMm','holeCentersMm']) and
|
|
52
|
+
keys(d, ['heightMm','maximumWidthMm','maximumDepthMm','trimBottomFraction']) and
|
|
53
|
+
keys(j, ['radiusMm','embedMm','gapMm']), 'proposal_fields')
|
|
54
|
+
require(all(number(p[x]) for x in ['widthMm','depthMm','thicknessMm','holeRadiusMm']) and
|
|
55
|
+
all(number(d[x]) for x in ['heightMm','maximumWidthMm','maximumDepthMm']) and
|
|
56
|
+
all(number(j[x]) for x in j) and number(value['baseThicknessMm']) and number(value['clearanceMm']), 'dimensions')
|
|
57
|
+
require(isinstance(d['trimBottomFraction'], (float,int)) and not isinstance(d['trimBottomFraction'],bool) and 0 <= d['trimBottomFraction'] <= .25, 'trim')
|
|
58
|
+
holes=p['holeCentersMm']; require(isinstance(holes,list) and len(holes)==2 and all(isinstance(v,list) and len(v)==2 and all(number(abs(x)+EPS) for x in v) for v in holes), 'holes')
|
|
59
|
+
r=p['holeRadiusMm']; guard=value['clearanceMm']
|
|
60
|
+
for x,y in holes:
|
|
61
|
+
require(abs(x)+r+guard < p['widthMm']/2 and abs(y)+r+guard < p['depthMm']/2, 'hole_boundary')
|
|
62
|
+
require(math.hypot(x,y) > r+j['radiusMm']+guard, 'join_hole_overlap')
|
|
63
|
+
require(math.dist(holes[0],holes[1]) > 2*(r+guard), 'hole_overlap')
|
|
64
|
+
require(j['embedMm'] < p['thicknessMm'] and j['radiusMm']+guard < min(p['widthMm'],p['depthMm'])/2, 'join_dimensions')
|
|
65
|
+
require(value['targetOccurrenceId'] != value['baseOccurrenceId'], 'target')
|
|
66
|
+
return copy.deepcopy(value)
|
|
67
|
+
|
|
68
|
+
def vol(shape):
|
|
69
|
+
if shape is None: return 0.0
|
|
70
|
+
if isinstance(shape, bd.Shape):
|
|
71
|
+
value=float(shape.volume); require(math.isfinite(value) and value >= -VOL, 'volume')
|
|
72
|
+
return max(0,value)
|
|
73
|
+
return sum(vol(item) for item in shape)
|
|
74
|
+
|
|
75
|
+
def as_shape(value):
|
|
76
|
+
if isinstance(value,bd.Shape): return value
|
|
77
|
+
items=list(value or [])
|
|
78
|
+
return items[0] if len(items)==1 else bd.Compound(items)
|
|
79
|
+
|
|
80
|
+
def diff(a,b):
|
|
81
|
+
if vol(a)==0:return vol(b)
|
|
82
|
+
if vol(b)==0:return vol(a)
|
|
83
|
+
a,b=as_shape(a),as_shape(b)
|
|
84
|
+
return vol(a.cut(b))+vol(b.cut(a))
|
|
85
|
+
def cyl(radius, height, z=0, x=0, y=0):
|
|
86
|
+
return bd.Cylinder(radius,height,align=(bd.Align.CENTER,bd.Align.CENTER,bd.Align.MIN)).moved(bd.Location((x,y,z)))
|
|
87
|
+
def plate(p, thickness, z=0):
|
|
88
|
+
result=bd.Box(p['widthMm'],p['depthMm'],thickness,align=(bd.Align.CENTER,bd.Align.CENTER,bd.Align.MIN)).moved(bd.Location((0,0,z)))
|
|
89
|
+
for x,y in p['holeCentersMm']: result=result.cut(cyl(p['holeRadiusMm'],thickness,z,x,y))
|
|
90
|
+
return result
|
|
91
|
+
def half(z, above):
|
|
92
|
+
face=BRepBuilderAPI_MakeFace(gp_Pln(gp_Pnt(0,0,z),gp_Dir(0,0,1))).Face()
|
|
93
|
+
return bd.Solid(BRepPrimAPI_MakeHalfSpace(face,gp_Pnt(0,0,z+(1 if above else -1))).Solid())
|
|
94
|
+
def valid(shape):
|
|
95
|
+
return isinstance(shape,bd.Shape) and shape.is_valid and len(shape.solids())==1 and vol(shape)>VOL and len(shape.shells())==1 and BRep_Tool.IsClosed_s(shape.shells()[0].wrapped)
|
|
96
|
+
def local_shapes(children, manifest, target):
|
|
97
|
+
rows=manifest['occurrences']; require(len(children)==len(rows) and [s.label for s in children]==[r['occurrenceId'] for r in rows], 'step_occurrences')
|
|
98
|
+
row=next((r for r in rows if r['occurrenceId']==target),None);require(row is not None,'target_missing')
|
|
99
|
+
transform=row['views']['normal']; location=bd.Location(tuple(transform['translationMm']),tuple(transform['rotationDeg']))
|
|
100
|
+
return {r['occurrenceId']:s.moved(location.inverse()) for r,s in zip(rows,children)},location
|
|
101
|
+
|
|
102
|
+
def surfaces(shape, p):
|
|
103
|
+
holes=[]; seat_area=0.0
|
|
104
|
+
for face in shape.faces():
|
|
105
|
+
surface=BRepAdaptor_Surface(face.wrapped)
|
|
106
|
+
if surface.GetType()==GeomAbs_Cylinder:
|
|
107
|
+
c=surface.Cylinder(); a=c.Axis(); direction=a.Direction(); origin=a.Location()
|
|
108
|
+
if abs(abs(direction.Z())-1)>EPS:continue
|
|
109
|
+
bounds=face.bounding_box()
|
|
110
|
+
holes.append({'center':[origin.X(),origin.Y()], 'radius':c.Radius(), 'zMin':bounds.min.Z,'zMax':bounds.max.Z})
|
|
111
|
+
elif surface.GetType()==GeomAbs_Plane:
|
|
112
|
+
plane=surface.Plane(); n=plane.Axis().Direction()
|
|
113
|
+
if abs(abs(n.Z())-1)<=EPS and abs(plane.Location().Z())<=EPS: seat_area+=float(face.area)
|
|
114
|
+
measurements=[]
|
|
115
|
+
for center in p['holeCentersMm']:
|
|
116
|
+
found=[h for h in holes if math.dist(h['center'],center)<=EPS and abs(h['radius']-p['holeRadiusMm'])<=EPS and abs(h['zMin'])<=EPS and abs(h['zMax']-p['thicknessMm'])<=EPS]
|
|
117
|
+
require(len(found)==1,'interface_cylinder')
|
|
118
|
+
measurements.append(found[0])
|
|
119
|
+
expected=p['widthMm']*p['depthMm']-2*math.pi*p['holeRadiusMm']**2
|
|
120
|
+
require(abs(seat_area-expected)<=max(VOL,expected*1e-8),'interface_seat')
|
|
121
|
+
return {'holes':measurements,'seatAreaMm2':seat_area}
|
|
122
|
+
|
|
123
|
+
def baseline(children, manifest, raw, step_bytes, api):
|
|
124
|
+
spec=proposal(raw); p=spec['plate'];target=spec['targetOccurrenceId'];base_id=spec['baseOccurrenceId']
|
|
125
|
+
require(manifest['mode']=='occurrence_assembly' and manifest['units']=='mm' and len(children)<=POLICY['maximumOccurrences'],'baseline_mode')
|
|
126
|
+
shapes,location=local_shapes(children,manifest,target)
|
|
127
|
+
require(base_id in shapes and all(r['geometryPrecision'] in ['custom_brep','envelope'] for r in manifest['occurrences']) and
|
|
128
|
+
all(r['geometryPrecision']=='custom_brep' for r in manifest['occurrences'] if r['occurrenceId'] in [target,base_id]), 'baseline_precision')
|
|
129
|
+
require(all(valid(s) for s in shapes.values()),'baseline_solids')
|
|
130
|
+
carrier,base=shapes[target],shapes[base_id]
|
|
131
|
+
carrier_error=diff(carrier,plate(p,p['thicknessMm']));base_error=diff(base,plate(p,spec['baseThicknessMm'],-spec['baseThicknessMm']))
|
|
132
|
+
require(carrier_error<=VOL and base_error<=VOL,'baseline_template')
|
|
133
|
+
dims=surfaces(carrier,p)
|
|
134
|
+
for identifier,s in shapes.items():
|
|
135
|
+
if identifier!=target:require(vol(s.intersect(half(EPS,True)))<=VOL,'baseline_insertion')
|
|
136
|
+
ids=list(shapes)
|
|
137
|
+
for i,a in enumerate(ids):
|
|
138
|
+
for b in ids[i+1:]:require(vol(shapes[a].intersect(shapes[b]))<=VOL,'baseline_collision')
|
|
139
|
+
row=next(r for r in manifest['occurrences'] if r['occurrenceId']==target)
|
|
140
|
+
evidence={'schema':'abelien.planar-attachment-baseline.v1','checkerVersion':VERSION,'policy':POLICY,
|
|
141
|
+
'proposal':spec,'sourceSha256':manifest['sourceSha256'],'modelStepSha256':api['digest'](step_bytes),
|
|
142
|
+
'assemblySha256':manifest['assemblySha256'],'occurrenceSetSha256':manifest['occurrenceSetSha256'],
|
|
143
|
+
'asModeledOnlyOccurrenceIds':[r['occurrenceId'] for r in manifest['occurrences'] if r['geometryPrecision']=='envelope'],
|
|
144
|
+
'targetGeometrySha256':row['geometrySha256'],'targetTransformSha256':row['transformSha256'],
|
|
145
|
+
'checks':{'carrierSymmetricDifferenceMm3':carrier_error,'baseSymmetricDifferenceMm3':base_error,'interfaces':dims,'staticCollision':'pass','planarInsertion':'pass'}}
|
|
146
|
+
evidence['attachmentSha256']=api['digest'](evidence)
|
|
147
|
+
return evidence
|
|
148
|
+
|
|
149
|
+
def self_evidence(shape):
|
|
150
|
+
args=TopTools_ListOfShape();args.Append(shape.wrapped)
|
|
151
|
+
checker=BOPAlgo_CheckerSI();checker.SetArguments(args);checker.SetLevelOfCheck(9);checker.Perform()
|
|
152
|
+
require(not checker.HasErrors(),'self_check_failed')
|
|
153
|
+
ds=checker.DS()
|
|
154
|
+
count=ds.Interferences().Extent()
|
|
155
|
+
points=[]
|
|
156
|
+
for pair in ds.Interferences():
|
|
157
|
+
for index in pair.Indices():
|
|
158
|
+
points.extend([list(v.center()) for v in bd.Compound.cast(ds.Shape(index)).vertices()])
|
|
159
|
+
return {'count':count,'points':points}
|
|
160
|
+
|
|
161
|
+
def self_check(shape):
|
|
162
|
+
evidence=self_evidence(shape);require(evidence['count']==0,'self_intersection')
|
|
163
|
+
return evidence['count']
|
|
164
|
+
|
|
165
|
+
def mesh_solid(triangles, check_self=True):
|
|
166
|
+
array=np.asarray(triangles,dtype=np.float64)
|
|
167
|
+
require(array.ndim==3 and array.shape[1:]==(3,3) and 4<=len(array)<=POLICY['maximumTriangles'] and np.isfinite(array).all(),'mesh_shape')
|
|
168
|
+
# Exact welding only: no geometric repair/displacement. UV seams may duplicate positions.
|
|
169
|
+
vertices,indices=np.unique(array.reshape(-1,3),axis=0,return_inverse=True);faces=indices.reshape(-1,3)
|
|
170
|
+
edges=collections.defaultdict(list); links=collections.defaultdict(list)
|
|
171
|
+
for i,f in enumerate(faces):
|
|
172
|
+
require(len(set(map(int,f)))==3,'mesh_degenerate')
|
|
173
|
+
require(float(np.linalg.norm(np.cross(vertices[f[1]]-vertices[f[0]],vertices[f[2]]-vertices[f[0]])))>1e-14,'mesh_degenerate')
|
|
174
|
+
for k in range(3):
|
|
175
|
+
a,b=int(f[k]),int(f[(k+1)%3]); edges[tuple(sorted((a,b)))].append((i,a,b))
|
|
176
|
+
links[int(f[k])].append((int(f[(k+1)%3]),int(f[(k+2)%3])))
|
|
177
|
+
adjacency=[set() for _ in faces]
|
|
178
|
+
for es in edges.values():
|
|
179
|
+
require(len(es)==2 and es[0][1:]==es[1][1:][::-1],'mesh_nonmanifold')
|
|
180
|
+
adjacency[es[0][0]].add(es[1][0]);adjacency[es[1][0]].add(es[0][0])
|
|
181
|
+
def connected(adjacency,start):
|
|
182
|
+
seen={start};todo=[start]
|
|
183
|
+
while todo:
|
|
184
|
+
for n in adjacency[todo.pop()]:
|
|
185
|
+
if n not in seen:seen.add(n);todo.append(n)
|
|
186
|
+
return seen
|
|
187
|
+
require(len(connected(adjacency,0))==len(faces),'mesh_components')
|
|
188
|
+
for pairs in links.values():
|
|
189
|
+
ring=collections.defaultdict(set)
|
|
190
|
+
for a,b in pairs:ring[a].add(b);ring[b].add(a)
|
|
191
|
+
require(all(len(v)==2 for v in ring.values()) and len(connected(ring,next(iter(ring))))==len(ring),'mesh_vertex_nonmanifold')
|
|
192
|
+
signed=float(np.sum(np.einsum('ij,ij->i',array[:,0],np.cross(array[:,1],array[:,2])))/6)
|
|
193
|
+
require(signed>1e-12,'mesh_orientation')
|
|
194
|
+
sewing=BRepBuilderAPI_Sewing(EPS)
|
|
195
|
+
for tri in array:
|
|
196
|
+
wire=BRepBuilderAPI_MakePolygon(*[gp_Pnt(*map(float,p)) for p in tri],True).Wire()
|
|
197
|
+
sewing.Add(BRepBuilderAPI_MakeFace(wire).Face())
|
|
198
|
+
sewing.Perform();shells=[];it=TopExp_Explorer(sewing.SewedShape(),TopAbs_SHELL)
|
|
199
|
+
while it.More():shells.append(TopoDS.Shell_s(it.Current()));it.Next()
|
|
200
|
+
require(len(shells)==1 and sewing.NbFreeEdges()==0 and sewing.NbMultipleEdges()==0 and BRep_Tool.IsClosed_s(shells[0]),'mesh_open_shell')
|
|
201
|
+
wrapped=BRepBuilderAPI_MakeSolid(shells[0]).Solid();require(BRepLib.OrientClosedSolid_s(wrapped),'mesh_orientation')
|
|
202
|
+
solid=bd.Solid(wrapped);require(valid(solid),'mesh_invalid_solid')
|
|
203
|
+
if check_self:self_check(solid)
|
|
204
|
+
return solid
|
|
205
|
+
|
|
206
|
+
def prepare_decoration(triangles,spec,candidate_bytes):
|
|
207
|
+
array=np.asarray(triangles,dtype=np.float64)
|
|
208
|
+
require(array.ndim==3 and array.shape[1:]==(3,3),'mesh_shape')
|
|
209
|
+
# glTF Y-up -> carrier Z-up. No inferred provider metres are used.
|
|
210
|
+
array=array[:,:,[0,2,1]]*np.array([1,-1,1])
|
|
211
|
+
low=array.min(axis=(0,1));high=array.max(axis=(0,1));span=high-low
|
|
212
|
+
require(np.isfinite(span).all() and np.all(span>1e-9),'mesh_span')
|
|
213
|
+
d=spec['decoration'];p=spec['plate'];j=spec['join'];scale=d['heightMm']/span[2]
|
|
214
|
+
require(span[0]*scale<=d['maximumWidthMm']+EPS and span[1]*scale<=d['maximumDepthMm']+EPS,'decoration_size')
|
|
215
|
+
crop=d['heightMm']*d['trimBottomFraction'];cut_z=p['thicknessMm']+j['gapMm']
|
|
216
|
+
translation=np.array([-(low[0]+high[0])/2*scale,-(low[1]+high[1])/2*scale,cut_z-crop-low[2]*scale])
|
|
217
|
+
placed=array*scale+translation
|
|
218
|
+
initial=mesh_solid(placed,check_self=False)
|
|
219
|
+
placement={'method':'declared_design_height','sourceUnits':'unknown','scaleMmPerSourceUnit':scale,
|
|
220
|
+
'translationMm':translation.tolist(),'trimBottomMm':crop,'cutZMm':cut_z,'heightBeforeTrimMm':d['heightMm']}
|
|
221
|
+
derived,derived_bytes,repair=REPAIR['derive'](placed,candidate_bytes,self_evidence(initial),placement,POLICY,mesh_solid,self_evidence)
|
|
222
|
+
solid=initial if repair['status']=='not_needed' else mesh_solid(derived)
|
|
223
|
+
decoration=as_shape(solid.intersect(half(cut_z,True)))
|
|
224
|
+
require(valid(decoration),'trim_disconnected')
|
|
225
|
+
connector=cyl(j['radiusMm'],j['embedMm']*2+j['gapMm'],p['thicknessMm']-j['embedMm'])
|
|
226
|
+
witness=cyl(j['radiusMm'],j['embedMm'],cut_z)
|
|
227
|
+
require(vol(witness.cut(decoration))<=VOL,'join_not_embedded')
|
|
228
|
+
return decoration,connector,placement,derived_bytes,repair
|
|
229
|
+
|
|
230
|
+
def source_read(request,api):
|
|
231
|
+
encoded=request['baselineStep'];body=base64.b64decode(encoded,validate=True)
|
|
232
|
+
require(api['digest'](body)==request['attachment']['modelStepSha256'],'baseline_step_hash')
|
|
233
|
+
path=api['root']/'baseline.step';path.write_bytes(body);model=bd.import_step(str(path));children=list(model.children)
|
|
234
|
+
actual=baseline(children,request['baselineAssembly'],request['attachment']['proposal'],body,api)
|
|
235
|
+
require(actual==request['attachment'],'attachment_changed')
|
|
236
|
+
return body,children
|
|
237
|
+
|
|
238
|
+
def construct(request,api):
|
|
239
|
+
body,children=source_read(request,api);original=request['baselineAssembly'];attachment=request['attachment'];spec=attachment['proposal'];target=spec['targetOccurrenceId']
|
|
240
|
+
local,location=local_shapes(children,original,target);decoration,connector,placement,derived_bytes,repair=prepare_decoration(request['triangles'],spec,base64.b64decode(request['candidateGlb'],validate=True))
|
|
241
|
+
result=local[target].fuse(decoration,connector);require(valid(result),'union_not_single_solid')
|
|
242
|
+
self_check(result)
|
|
243
|
+
newchildren=[]
|
|
244
|
+
for child in children:
|
|
245
|
+
replacement=result.moved(location) if child.label==target else child
|
|
246
|
+
replacement.label=child.label;newchildren.append(replacement)
|
|
247
|
+
model=bd.Compound(children=newchildren);model.label='assembly';bd.export_step(model,str(api['step']))
|
|
248
|
+
recipe={'schema':'abelien.component-engineering-recipe.v1','checkerVersion':VERSION,'policy':POLICY,
|
|
249
|
+
'baseline':{k:attachment[k] for k in ['sourceSha256','modelStepSha256','assemblySha256','occurrenceSetSha256','attachmentSha256','targetGeometrySha256','targetTransformSha256']},
|
|
250
|
+
'targetOccurrenceId':target,'candidateGlbSha256':request['candidateGlbSha256'],'placement':placement,
|
|
251
|
+
'decorationGlbSha256':api['digest'](derived_bytes),'repair':repair}
|
|
252
|
+
return {'step':api['encode'](api['step']),'recipe':recipe,'decorationGlb':base64.b64encode(derived_bytes).decode()}
|
|
253
|
+
|
|
254
|
+
def verify_final(request,api):
|
|
255
|
+
baseline_bytes,previous=source_read(request,api);original=request['baselineAssembly'];spec=request['attachment']['proposal'];target=spec['targetOccurrenceId'];base_id=spec['baseOccurrenceId']
|
|
256
|
+
final_bytes=base64.b64decode(request['step'],validate=True);api['step'].write_bytes(final_bytes);model=bd.import_step(str(api['step']));children=list(model.children)
|
|
257
|
+
before,location=local_shapes(previous,original,target);after,_=local_shapes(children,original,target)
|
|
258
|
+
require(set(before)==set(after) and all(valid(s) for s in after.values()),'readback_solids')
|
|
259
|
+
decoration,connector,placement,derived_bytes,repair=prepare_decoration(request['triangles'],spec,base64.b64decode(request['candidateGlb'],validate=True))
|
|
260
|
+
expected_recipe={'schema':'abelien.component-engineering-recipe.v1','checkerVersion':VERSION,'policy':POLICY,
|
|
261
|
+
'baseline':{k:request['attachment'][k] for k in ['sourceSha256','modelStepSha256','assemblySha256','occurrenceSetSha256','attachmentSha256','targetGeometrySha256','targetTransformSha256']},
|
|
262
|
+
'targetOccurrenceId':target,'candidateGlbSha256':request['candidateGlbSha256'],'placement':placement,
|
|
263
|
+
'decorationGlbSha256':api['digest'](derived_bytes),'repair':repair}
|
|
264
|
+
require(api['digest'](base64.b64decode(request['candidateGlb'],validate=True))==request['candidateGlbSha256'],'candidate_hash')
|
|
265
|
+
require(expected_recipe==request['recipe'] and derived_bytes==base64.b64decode(request['decorationGlb'],validate=True),'recipe_changed')
|
|
266
|
+
expected=before[target].fuse(decoration,connector);require(diff(after[target],expected)<=VOL,'composition_changed')
|
|
267
|
+
result=after[target];carrier=before[target];p=spec['plate'];j=spec['join'];clearance=spec['clearanceMm']
|
|
268
|
+
lost=vol(carrier.cut(result));require(lost<=VOL,'carrier_changed')
|
|
269
|
+
surface_evidence=surfaces(result,p)
|
|
270
|
+
protected_errors=[];blocked=[]
|
|
271
|
+
zmax=p['thicknessMm']+j['gapMm']+spec['decoration']['heightMm']+1
|
|
272
|
+
for x,y in p['holeCentersMm']:
|
|
273
|
+
region=cyl(p['holeRadiusMm']+clearance,zmax+1,-1,x,y)
|
|
274
|
+
error=diff(carrier.intersect(region),result.intersect(region));require(error<=VOL,'protected_interface_changed');protected_errors.append(error)
|
|
275
|
+
keepout=cyl(p['holeRadiusMm']-EPS,zmax+spec['baseThicknessMm'],-spec['baseThicknessMm'],x,y)
|
|
276
|
+
overlap=vol(result.intersect(keepout));require(overlap<=VOL,'hole_access_blocked');blocked.append(overlap)
|
|
277
|
+
seat=half(EPS,False);seat_error=diff(carrier.intersect(seat),result.intersect(seat));require(seat_error<=VOL,'seat_changed')
|
|
278
|
+
witness=connector;missing=vol(witness.cut(result));require(missing<=VOL,'join_discontinuous')
|
|
279
|
+
carrier_end=cyl(j['radiusMm'],j['embedMm'],p['thicknessMm']-j['embedMm'])
|
|
280
|
+
decorative_end=cyl(j['radiusMm'],j['embedMm'],placement['cutZMm'])
|
|
281
|
+
require(vol(carrier_end.cut(carrier))<=VOL and vol(decorative_end.cut(decoration))<=VOL,'join_embedding')
|
|
282
|
+
preserved=[];overlaps=[];distances=[];added=as_shape(result.cut(carrier))
|
|
283
|
+
for identifier,shape in after.items():
|
|
284
|
+
if identifier==target:continue
|
|
285
|
+
delta=diff(before[identifier],shape);require(delta<=VOL,'preserved_occurrence_changed');preserved.append({'occurrenceId':identifier,'symmetricDifferenceMm3':delta})
|
|
286
|
+
overlap=vol(result.intersect(shape));require(overlap<=VOL,'static_collision');overlaps.append({'occurrenceId':identifier,'overlapMm3':overlap})
|
|
287
|
+
require(vol(shape.intersect(half(EPS,True)))<=VOL,'insertion_fixed_above_plane')
|
|
288
|
+
distance=float(added.distance_to(shape));require(distance+EPS>=clearance,'clearance');distances.append({'occurrenceId':identifier,'distanceMm':distance})
|
|
289
|
+
require(vol(result.intersect(half(-EPS,False)))<=VOL,'insertion_target_below_plane')
|
|
290
|
+
intersections=self_check(result)
|
|
291
|
+
measurements=[{'validatedAttachmentSha256':request['attachment']['attachmentSha256']},{'solidCount':1,'shellCount':1},
|
|
292
|
+
{'interferenceCount':intersections},{'lostVolumeMm3':lost},surface_evidence,
|
|
293
|
+
{'symmetricDifferenceMm3':protected_errors,'holeObstructionMm3':blocked,'seatDifferenceMm3':seat_error},
|
|
294
|
+
{'missingWitnessMm3':missing,'radiusMm':j['radiusMm'],'sectionAreaMm2':math.pi*j['radiusMm']**2,'embedMm':j['embedMm']},
|
|
295
|
+
{'pairs':overlaps},{'minimumRequiredMm':clearance,'pairs':distances},{'method':'brep_separating_halfspaces','outwardLocal':[0,0,1]},
|
|
296
|
+
{'occurrences':preserved},{'compositionSymmetricDifferenceMm3':diff(result,expected),'solidCount':len(children)}]
|
|
297
|
+
checks=[{'id':name,'status':'pass','method':'occt_brep_independent_readback','measured':value} for name,value in zip(CHECK_IDS,measurements)]
|
|
298
|
+
recipe=request['recipe'];recipe_bytes=api['json'].dumps(recipe,separators=(',',':'),ensure_ascii=False).encode();recipe_sha=api['digest'](recipe_bytes)
|
|
299
|
+
assembly=copy.deepcopy(original)
|
|
300
|
+
for k in ['assemblySha256','occurrenceSetSha256','artifacts','views']:assembly.pop(k,None)
|
|
301
|
+
assembly['sourceSha256']=recipe_sha;assembly['revision']=None
|
|
302
|
+
for row in assembly['occurrences']:
|
|
303
|
+
if row['occurrenceId']==target:
|
|
304
|
+
row['geometrySha256']=api['shape_digest'](result);row['source']={'kind':'generated_custom','sha256':recipe_sha};row['lineage']=None
|
|
305
|
+
display=bd.Compound(children=children);display.label='assembly-root';glb=api['root']/'model.glb'
|
|
306
|
+
require(bd.export_gltf(display,str(glb),binary=True,linear_deflection=.05),'glb_export')
|
|
307
|
+
raw=api['apply_materials_to_glb'](glb.read_bytes(),assembly);glb.write_bytes(raw)
|
|
308
|
+
api['validate_occurrence_nodes'](api['gltf_document'](raw),assembly)
|
|
309
|
+
# Renderer functions depend on trusted globals initialized by the caller.
|
|
310
|
+
images=api['render_assembly_views'](api['root'],children,assembly,.3)
|
|
311
|
+
manifest=api['final_manifest'](assembly,final_bytes,raw,images)
|
|
312
|
+
report={'schema':'abelien.component-engineering-fit-report.v1','checkerVersion':VERSION,'policy':POLICY,
|
|
313
|
+
'representation':'analytic_carrier_faceted_decoration','scope':'planar_two_hole_solid_decoration.v1',
|
|
314
|
+
'fitStatus':'pass','adoptionEligible':True,'baseline':recipe['baseline'],'targetOccurrenceId':target,
|
|
315
|
+
'asModeledOnlyOccurrenceIds':request['attachment']['asModeledOnlyOccurrenceIds'],'repair':repair,'decorationGlbSha256':api['digest'](derived_bytes),
|
|
316
|
+
'candidateGlbSha256':request['candidateGlbSha256'],'recipeSha256':recipe_sha,'modelStepSha256':api['digest'](final_bytes),
|
|
317
|
+
'modelGlbSha256':api['digest'](raw),'assemblySha256':manifest['assemblySha256'],
|
|
318
|
+
'subject':{k:manifest[k] for k in ['sourceSha256','assemblySha256','occurrenceSetSha256']} | {'modelStepSha256':api['digest'](final_bytes),'modelGlbSha256':api['digest'](raw)},'checks':checks,
|
|
319
|
+
'notChecked':NOT_CHECKED,'decorationShellWall':{'status':'not_applicable','reason':'filled_decoration_no_cavity'}}
|
|
320
|
+
artifacts={'decoration.glb':base64.b64encode(derived_bytes).decode(),'model.step':base64.b64encode(final_bytes).decode(),'model.glb':base64.b64encode(raw).decode(),'recipe.json':base64.b64encode(recipe_bytes).decode(),
|
|
321
|
+
'fit.json':base64.b64encode(api['json'].dumps(report,separators=(',',':')).encode()).decode(),
|
|
322
|
+
'assembly.json':base64.b64encode(api['json'].dumps(manifest,separators=(',',':')).encode()).decode()}
|
|
323
|
+
artifacts.update({f'{name}.png':base64.b64encode(body).decode() for name,body in images.items()})
|
|
324
|
+
return {'artifacts':artifacts}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Worker-owned capability identity, including opt-in actual-runtime API manifest. No request field participates. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {createHash} from 'node:crypto';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {execFileSync} from 'node:child_process';
|
|
7
|
+
import {STRUCTURE_SOURCE_CONTRACT,sourceApiDigest,sourceApiReference} from './source-contract.mjs';
|
|
8
|
+
|
|
9
|
+
export const STRUCTURE_PROTOCOL_VERSION='abelien.structure-worker.v3';
|
|
10
|
+
export const STRUCTURE_CHECKER_VERSION='abelien.structure-checker.v2';
|
|
11
|
+
const here=path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const checker=path.join(here,'cad.py');
|
|
13
|
+
const packagePrefixes=['build123d-','matplotlib-','numpy-','cadquery_ocp_novtk-','cadquery_ocp_proxy-'];
|
|
14
|
+
|
|
15
|
+
const inside=(parent,child)=>child===parent||child.startsWith(`${parent}${path.sep}`);
|
|
16
|
+
const add=(digest,label,bytes)=>{
|
|
17
|
+
const name=Buffer.from(label),body=Buffer.isBuffer(bytes)?bytes:Buffer.from(bytes);
|
|
18
|
+
const size=Buffer.allocUnsafe(8);size.writeBigUInt64BE(BigInt(body.length));
|
|
19
|
+
digest.update(name).update(Buffer.from([0])).update(size).update(body);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function nativeMaterials(runtime){
|
|
23
|
+
const root=fs.realpathSync(runtime),venv=path.join(root,'venv');
|
|
24
|
+
const python=fs.realpathSync(path.join(venv,'bin/python3'));
|
|
25
|
+
if(!inside(path.join(root,'python'),python))throw new Error('structure_runtime_identity_invalid');
|
|
26
|
+
const lib=path.join(venv,'lib'),pythonDirs=fs.readdirSync(lib).filter(name=>/^python3\.\d+$/.test(name));
|
|
27
|
+
if(pythonDirs.length!==1)throw new Error('structure_runtime_identity_invalid');
|
|
28
|
+
const site=path.join(lib,pythonDirs[0],'site-packages'),entries=fs.readdirSync(site);
|
|
29
|
+
const files=[path.join(venv,'pyvenv.cfg'),python];
|
|
30
|
+
for(const prefix of packagePrefixes){
|
|
31
|
+
const matches=entries.filter(name=>name.startsWith(prefix)&&name.endsWith('.dist-info'));
|
|
32
|
+
if(matches.length!==1)throw new Error('structure_runtime_identity_invalid');
|
|
33
|
+
files.push(path.join(site,matches[0],'METADATA'),path.join(site,matches[0],'RECORD'));
|
|
34
|
+
}
|
|
35
|
+
return files.map(file=>{
|
|
36
|
+
const real=fs.realpathSync(file);if(!inside(root,real))throw new Error('structure_runtime_identity_invalid');
|
|
37
|
+
const bytes=fs.readFileSync(real);if(!bytes.length||bytes.length>64*1024*1024)throw new Error('structure_runtime_identity_invalid');
|
|
38
|
+
return [path.relative(root,real),bytes];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createWorkerIdentity(config){
|
|
43
|
+
if(!config||!['docker','native-macos'].includes(config.backend))throw new Error('structure_runtime_identity_invalid');
|
|
44
|
+
const digest=createHash('sha256');
|
|
45
|
+
add(digest,'protocol',STRUCTURE_PROTOCOL_VERSION);add(digest,'checkerVersion',STRUCTURE_CHECKER_VERSION);
|
|
46
|
+
add(digest,'validator/materials.mjs',fs.readFileSync(path.join(here,'materials.mjs')));
|
|
47
|
+
for(const name of ['engineering-fit.mjs','engineering-policy.json','assembly-validation.mjs','manifest-digest.mjs','component-fit-glb.mjs'])add(digest,`validator/${name}`,fs.readFileSync(path.join(here,name)));
|
|
48
|
+
add(digest,'validator/inspection-geometry.mjs',fs.readFileSync(path.join(here,'inspection-geometry.mjs')));
|
|
49
|
+
add(digest,'validator/representation.mjs',fs.readFileSync(path.join(here,'representation.mjs')));
|
|
50
|
+
add(digest,'contract/source-contract.mjs',fs.readFileSync(path.join(here,'source-contract.mjs')));
|
|
51
|
+
for(const name of ['component-target-image.mjs','component-target-render.py'])add(digest,`renderer/${name}`,fs.readFileSync(path.join(here,name)));
|
|
52
|
+
if(config.sourceSymbolPrecheck){
|
|
53
|
+
add(digest,'contract/source-api',sourceApiDigest(config.sourceApi));
|
|
54
|
+
if(config.backend==='native-macos'&&createWorkerSourceContract(config).runtime.build123dVersion!==config.sourceApi.build123dVersion)
|
|
55
|
+
throw new Error('structure_runtime_identity_invalid');
|
|
56
|
+
}
|
|
57
|
+
if(config.backend==='native-macos'){
|
|
58
|
+
add(digest,'checker/cad.py',fs.readFileSync(checker));
|
|
59
|
+
add(digest,'checker/abelien_cad.py',fs.readFileSync(path.join(here,'abelien_cad.py')));
|
|
60
|
+
add(digest,'checker/source_call_stats.py',fs.readFileSync(path.join(here,'source_call_stats.py')));
|
|
61
|
+
add(digest,'checker/engineering.py',fs.readFileSync(path.join(here,'engineering.py')));
|
|
62
|
+
add(digest,'checker/engineering-repair.py',fs.readFileSync(path.join(here,'engineering-repair.py')));
|
|
63
|
+
for(const [name,bytes] of nativeMaterials(config.runtime))add(digest,`runtime/${name}`,bytes);
|
|
64
|
+
}
|
|
65
|
+
else{
|
|
66
|
+
if(typeof config.imageId!=='string'||!/^sha256:[a-f0-9]{64}$/.test(config.imageId))throw new Error('structure_runtime_identity_invalid');
|
|
67
|
+
add(digest,'runtime/docker-image-id',config.imageId);
|
|
68
|
+
}
|
|
69
|
+
return {ok:true,protocolVersion:STRUCTURE_PROTOCOL_VERSION,runtimeDigest:digest.digest('hex'),checkerVersion:STRUCTURE_CHECKER_VERSION};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Native version comes from the same installed METADATA bound into runtimeDigest; Docker remains image-pinned. */
|
|
73
|
+
export function createWorkerSourceContract(config) {
|
|
74
|
+
let build123dVersion=null;
|
|
75
|
+
if(config.backend==='native-macos'){
|
|
76
|
+
const metadata=nativeMaterials(config.runtime).find(([name])=>/build123d-[^/]+\.dist-info\/METADATA$/.test(name));
|
|
77
|
+
build123dVersion=metadata?.[1].toString('utf8').match(/^Version: ([A-Za-z0-9.+-]+)$/m)?.[1];
|
|
78
|
+
if(!build123dVersion)throw new Error('structure_runtime_identity_invalid');
|
|
79
|
+
}else if(config.backend!=='docker')throw new Error('structure_runtime_identity_invalid');
|
|
80
|
+
if(config.sourceSymbolPrecheck){
|
|
81
|
+
const reference=sourceApiReference(config.sourceApi);
|
|
82
|
+
if(build123dVersion!==null&&reference.build123dVersion!==build123dVersion)throw new Error('structure_runtime_identity_invalid');
|
|
83
|
+
return {...STRUCTURE_SOURCE_CONTRACT,apiReference:reference,runtime:{backend:config.backend,build123dVersion:reference.build123dVersion,
|
|
84
|
+
versionEvidence:'isolated_installed_module',sourceSymbolPrecheck:true,sourceApiSha256:reference.manifestSha256}};
|
|
85
|
+
}
|
|
86
|
+
return {...STRUCTURE_SOURCE_CONTRACT,runtime:{backend:config.backend,build123dVersion,
|
|
87
|
+
versionEvidence:config.backend==='native-macos'?'installed_metadata':'immutable_image_id'}};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Resolve a mutable operator-facing tag once, then use the immutable image ID for both identity and every job. */
|
|
91
|
+
export function resolveDockerImage(image,inspect=execFileSync){
|
|
92
|
+
if(typeof image!=='string'||!image.trim()||image.length>300)throw new Error('structure_runtime_identity_invalid');
|
|
93
|
+
try{
|
|
94
|
+
const id=inspect('docker',['image','inspect','--format','{{.Id}}',image],{encoding:'utf8',timeout:10000,maxBuffer:4096}).trim();
|
|
95
|
+
if(!/^sha256:[a-f0-9]{64}$/.test(id))throw new Error();
|
|
96
|
+
return id;
|
|
97
|
+
}catch{throw new Error('structure_runtime_identity_invalid');}
|
|
98
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Validate bounded inspection-only BRep witnesses; never add product occurrences. */
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
const HEX=/^[a-f0-9]{64}$/,ID=/^[a-z0-9][a-z0-9_-]{0,127}$/;
|
|
5
|
+
const fail=()=>{throw new Error('invalid_inspection_geometry');};
|
|
6
|
+
const keys=(value,expected)=>value&&typeof value==='object'&&!Array.isArray(value)&&Object.keys(value).length===expected.length&&expected.every(key=>Object.hasOwn(value,key));
|
|
7
|
+
const finite=value=>typeof value==='number'&&Number.isFinite(value)&&value>=0;
|
|
8
|
+
const hash=value=>createHash('sha256').update(value).digest('hex');
|
|
9
|
+
export const INSPECTION_GEOMETRY_TOLERANCE_MM3=1e-6;
|
|
10
|
+
export const INSPECTION_GEOMETRY_MAX_BYTES=8*1024*1024;
|
|
11
|
+
|
|
12
|
+
export function validateInspectionGeometryPlan(value,assembly){
|
|
13
|
+
const regions=new Map();
|
|
14
|
+
if(value===undefined)return regions;
|
|
15
|
+
if(!keys(value,['schema','sourceSha256','regions','shells'])||value.schema!=='abelien.structure-inspection-geometry.v1'||value.sourceSha256!==assembly.sourceSha256||!HEX.test(value.sourceSha256)||
|
|
16
|
+
!Array.isArray(value.regions)||value.regions.length<2||value.regions.length>128||!Array.isArray(value.shells)||!value.shells.length||value.shells.length>32)fail();
|
|
17
|
+
const owners=new Map(assembly.occurrences.map(item=>[item.occurrenceId,item]));
|
|
18
|
+
for(const region of value.regions){
|
|
19
|
+
if(!keys(region,['regionId','kind','ownerOccurrenceId','brepSha256'])||!ID.test(region.regionId??'')||owners.has(region.regionId)||regions.has(region.regionId)||
|
|
20
|
+
!['outer','cavity','opening'].includes(region.kind)||!owners.has(region.ownerOccurrenceId)||!HEX.test(region.brepSha256??''))fail();
|
|
21
|
+
regions.set(region.regionId,region);
|
|
22
|
+
}
|
|
23
|
+
const used=new Set(),shells=new Set();
|
|
24
|
+
for(const shell of value.shells){
|
|
25
|
+
if(!keys(shell,['occurrenceId','geometrySha256','transformSha256','outerRegionId','cavityRegionId','openingRegionIds'])||shells.has(shell.occurrenceId)||
|
|
26
|
+
!Array.isArray(shell.openingRegionIds)||shell.openingRegionIds.length>32)fail();
|
|
27
|
+
const owner=owners.get(shell.occurrenceId);
|
|
28
|
+
if(!owner||owner.geometryPrecision!=='custom_brep'||!HEX.test(shell.geometrySha256??'')||!HEX.test(shell.transformSha256??'')||owner.geometrySha256!==shell.geometrySha256||owner.transformSha256!==shell.transformSha256)fail();
|
|
29
|
+
shells.add(shell.occurrenceId);
|
|
30
|
+
for(const [id,kind] of [[shell.outerRegionId,'outer'],[shell.cavityRegionId,'cavity'],...shell.openingRegionIds.map(id=>[id,'opening'])]){
|
|
31
|
+
const region=regions.get(id);
|
|
32
|
+
if(!region||used.has(id)||region.kind!==kind||region.ownerOccurrenceId!==shell.occurrenceId)fail();
|
|
33
|
+
used.add(id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if(used.size!==regions.size)fail();
|
|
37
|
+
return regions;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function validateInspectionGeometryTransport(plan,assembly,breps){
|
|
41
|
+
const regions=validateInspectionGeometryPlan(plan.inspectionGeometry,assembly);
|
|
42
|
+
if(!regions.size){if(breps!==undefined)fail();return;}
|
|
43
|
+
if(!keys(breps,[...regions.keys()]))fail();
|
|
44
|
+
let bytes=0;
|
|
45
|
+
for(const [id,region] of regions){
|
|
46
|
+
const encoded=breps[id];
|
|
47
|
+
if(typeof encoded!=='string'||encoded.length>12*1024*1024||!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded))fail();
|
|
48
|
+
const body=Buffer.from(encoded,'base64');bytes+=body.length;
|
|
49
|
+
if(!body.length||body.toString('base64')!==encoded||bytes>INSPECTION_GEOMETRY_MAX_BYTES||hash(body)!==region.brepSha256)fail();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function validateInspectionGeometryEvidence(evidence,metadata,assembly){
|
|
54
|
+
validateInspectionGeometryPlan(metadata,assembly);
|
|
55
|
+
if(metadata===undefined){if(evidence!==undefined)fail();return;}
|
|
56
|
+
if(!keys(evidence,['schema','sourceSha256','assemblySha256','inspectionPlanSha256','plan','status','method','toleranceMm3','checks'])||
|
|
57
|
+
evidence.schema!=='abelien.structure-inspection-geometry-evidence.v1'||evidence.sourceSha256!==assembly.sourceSha256||evidence.assemblySha256!==assembly.assemblySha256||
|
|
58
|
+
evidence.inspectionPlanSha256!==assembly.inspectionPlanSha256||JSON.stringify(evidence.plan)!==JSON.stringify(metadata)||evidence.status!=='pass'||
|
|
59
|
+
evidence.method!=='occt_brep_boolean_reconstruction'||evidence.toleranceMm3!==INSPECTION_GEOMETRY_TOLERANCE_MM3||!Array.isArray(evidence.checks)||evidence.checks.length!==metadata.shells.length)fail();
|
|
60
|
+
const epsilon=INSPECTION_GEOMETRY_TOLERANCE_MM3;
|
|
61
|
+
evidence.checks.forEach((check,index)=>{
|
|
62
|
+
const shell=metadata.shells[index];
|
|
63
|
+
if(!keys(check,['shellOccurrenceId','cavityOutsideMm3','minimumWallMm','shellSymmetricDifferenceMm3','openings'])||check.shellOccurrenceId!==shell.occurrenceId||
|
|
64
|
+
!finite(check.cavityOutsideMm3)||check.cavityOutsideMm3>epsilon||!finite(check.minimumWallMm)||check.minimumWallMm<=epsilon||
|
|
65
|
+
!finite(check.shellSymmetricDifferenceMm3)||check.shellSymmetricDifferenceMm3>epsilon||!Array.isArray(check.openings)||check.openings.length!==shell.openingRegionIds.length)fail();
|
|
66
|
+
check.openings.forEach((opening,openingIndex)=>{
|
|
67
|
+
if(!keys(opening,['regionId','cavityOverlapMm3','outsideOuterVolumeMm3','uniqueWallCutVolumeMm3'])||opening.regionId!==shell.openingRegionIds[openingIndex]||
|
|
68
|
+
['cavityOverlapMm3','outsideOuterVolumeMm3','uniqueWallCutVolumeMm3'].some(key=>!finite(opening[key])||opening[key]<=epsilon))fail();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|