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.
Files changed (104) hide show
  1. package/.env.example +4 -0
  2. package/README.md +11 -18
  3. package/package.json +2 -3
  4. package/skills/abelien-appearance/SKILL.md +1 -0
  5. package/skills/abelien-clarify/SKILL.md +12 -2
  6. package/skills/abelien-clarify/references/examples.md +22 -0
  7. package/skills/abelien-clarify/scripts/check_result.py +38 -3
  8. package/skills/abelien-clarify/scripts/test_check_result.py +32 -1
  9. package/skills/abelien-guide/SKILL.md +1 -1
  10. package/skills/abelien-hardware/SKILL.md +10 -5
  11. package/skills/abelien-hardware/domain.json +3 -3
  12. package/skills/abelien-hardware/references/search-index-format.md +13 -1
  13. package/skills/abelien-hardware/references/search-index-release.md +4 -2
  14. package/skills/abelien-hardware/references/web-evidence-contract.md +3 -3
  15. package/skills/abelien-hardware/scripts/dimensions.mjs +23 -8
  16. package/skills/abelien-hardware/scripts/electrical.mjs +168 -0
  17. package/skills/abelien-hardware/scripts/finalize-selection.mjs +4 -2
  18. package/skills/abelien-hardware/scripts/provider-chain-contract.mjs +27 -6
  19. package/skills/abelien-hardware/scripts/public-page.mjs +11 -121
  20. package/skills/abelien-hardware/scripts/response-redaction.mjs +1 -130
  21. package/skills/abelien-hardware/scripts/search-index-release.mjs +30 -17
  22. package/skills/abelien-hardware/scripts/selection-request.mjs +6 -5
  23. package/skills/abelien-hardware/scripts/tos-wiki-search.mjs +13 -8
  24. package/skills/abelien-hardware/scripts/volc-web-search.mjs +10 -27
  25. package/skills/abelien-hardware/scripts/web-common.mjs +8 -11
  26. package/skills/abelien-hardware/scripts/web-integrity.mjs +1 -1
  27. package/skills/abelien-hardware/scripts/web-provider-chain.mjs +10 -3
  28. package/skills/abelien-hardware/scripts/web-receipt.mjs +4 -2
  29. package/skills/abelien-hardware/scripts/web-search-plan.mjs +21 -2
  30. package/skills/abelien-hardware/scripts/web-search.mjs +5 -2
  31. package/skills/abelien-hardware-selection/SKILL.md +1 -1
  32. package/skills/abelien-hardware-selection/domain.json +2 -2
  33. package/skills/abelien-hardware-selection/references/selection-service-contract.md +30 -3
  34. package/skills/abelien-hardware-selection/scripts/select.mjs +35 -8
  35. package/skills/abelien-harness/scripts/ab.mjs +24 -2
  36. package/skills/abelien-harness/scripts/web/common.mjs +20 -0
  37. package/skills/abelien-harness/scripts/web/public-read.mjs +168 -0
  38. package/skills/abelien-harness/scripts/web/redact.mjs +136 -0
  39. package/skills/abelien-harness/scripts/web/volc.mjs +49 -0
  40. package/skills/abelien-reference/SKILL.md +51 -0
  41. package/skills/abelien-reference/domain.json +92 -0
  42. package/skills/abelien-reference/fixtures/create.final.json +102 -0
  43. package/skills/abelien-reference/fixtures/restore.collected.json +189 -0
  44. package/skills/abelien-reference/fixtures/restore.final.json +267 -0
  45. package/skills/abelien-reference/references/research-contract.md +74 -0
  46. package/skills/abelien-reference/schemas/reference-evidence.v1.json +140 -0
  47. package/skills/abelien-reference/scripts/evidence.mjs +60 -0
  48. package/skills/abelien-reference/scripts/finalize.mjs +116 -0
  49. package/skills/abelien-reference/scripts/measure.mjs +62 -0
  50. package/skills/abelien-reference/scripts/png.mjs +86 -0
  51. package/skills/abelien-reference/scripts/research.mjs +179 -0
  52. package/skills/abelien-structure/SKILL.md +2 -2
  53. package/skills/abelien-structure/domain.json +64 -0
  54. package/skills/abelien-structure/references/build123d-execution.md +1 -1
  55. package/skills/abelien-structure/references/handoff.md +1 -1
  56. package/skills/abelien-structure/worker/abelien_cad.py +585 -0
  57. package/skills/abelien-structure/worker/assembly-validation.mjs +106 -0
  58. package/skills/abelien-structure/worker/cad.py +1623 -0
  59. package/skills/abelien-structure/worker/cli-adapter.mjs +207 -0
  60. package/skills/abelien-structure/worker/component-fit-glb.mjs +173 -0
  61. package/skills/abelien-structure/worker/component-fit.d.mts +82 -0
  62. package/skills/abelien-structure/worker/component-fit.mjs +167 -0
  63. package/skills/abelien-structure/worker/component-target-image.d.mts +23 -0
  64. package/skills/abelien-structure/worker/component-target-image.mjs +107 -0
  65. package/skills/abelien-structure/worker/component-target-render.py +58 -0
  66. package/skills/abelien-structure/worker/config.mjs +6 -0
  67. package/skills/abelien-structure/worker/engineering-fit.d.mts +48 -0
  68. package/skills/abelien-structure/worker/engineering-fit.mjs +118 -0
  69. package/skills/abelien-structure/worker/engineering-policy.json +16 -0
  70. package/skills/abelien-structure/worker/engineering-repair.py +147 -0
  71. package/skills/abelien-structure/worker/engineering.py +324 -0
  72. package/skills/abelien-structure/worker/identity.mjs +98 -0
  73. package/skills/abelien-structure/worker/inspection-geometry.mjs +71 -0
  74. package/skills/abelien-structure/worker/jobs.mjs +451 -0
  75. package/skills/abelien-structure/worker/library-runtime.mjs +15 -0
  76. package/skills/abelien-structure/worker/main.mjs +102 -0
  77. package/skills/abelien-structure/worker/manifest-digest.mjs +11 -0
  78. package/skills/abelien-structure/worker/materials.mjs +52 -0
  79. package/skills/abelien-structure/worker/native-launcher.py +13 -0
  80. package/skills/abelien-structure/worker/native.mjs +157 -0
  81. package/skills/abelien-structure/worker/representation.mjs +47 -0
  82. package/skills/abelien-structure/worker/sandbox.mjs +41 -0
  83. package/skills/abelien-structure/worker/source-contract.mjs +143 -0
  84. package/skills/abelien-structure/worker/source_call_stats.py +59 -0
  85. package/skills/abelien-structure/worker/supervisor.mjs +38 -0
  86. package/skills/abelien-workspace/SKILL.md +11 -0
  87. package/skills/abelien-workspace/assets/journey-stage-plan.json +155 -0
  88. package/skills/abelien-workspace/domain.json +25 -2
  89. package/skills/abelien-workspace/references/hardware-journey-orchestration.md +1 -1
  90. package/skills/abelien-workspace/references/harness-binding.md +2 -2
  91. package/skills/abelien-workspace/references/user-journey.md +10 -0
  92. package/skills/abelien-workspace/scripts/journey-next.mjs +38 -0
  93. package/skills/abelien-workspace/workflows/definitions/0852e4949f5a6246880c1a26bf5ccaa42f8cda06b960e7c7ce234a0a176878f9.json +229 -0
  94. package/skills/abelien-workspace/workflows/definitions/399ef372790b7330d0ef88ce5096e7f69943d4793de97b2ed6c25165a332612f.json +230 -0
  95. package/skills/abelien-workspace/workflows/definitions/591a7f21e0b0ad586e340163f7533ac910356091ba8d2a26e250c0acbc80cd8b.json +176 -0
  96. package/skills/abelien-workspace/workflows/definitions/7c69ea0d51099b1ae947f08b056ea67f5cad6b40ae8bcb00efb496fa5b9d5ed3.json +149 -0
  97. package/skills/abelien-workspace/workflows/definitions/9b5c65ad03fdecc0148931a7677cfa684a3fb04ab2738d6f7a1086a2c79ac86c.json +149 -0
  98. package/skills/abelien-workspace/workflows/definitions/c1b5c6c653f0c4761d51efee923aafbbeea05512e7f35c60f7f7d36ec49fc1d1.json +229 -0
  99. package/skills/abelien-workspace/workflows/definitions/d9f8c6cdf0b2c41cdeb1c72ac5d1e37d95a7caac3272d434a3c7e8483fc565de.json +150 -0
  100. package/skills/abelien-workspace/workflows/definitions/e391e14c14cb39408a0a9e226ed54ffab7a36badea305ae4a3eafbac8124fc02.json +175 -0
  101. package/skills/abelien-workspace/workflows/definitions/fd8ce7074e2044985778446e93153864940bb9465471c1c71c41328cefff70bf.json +175 -0
  102. package/skills/abelien-workspace/workflows/user-journey-hardware-algorithm.json +5 -4
  103. package/skills/abelien-workspace/workflows/user-journey-hardware-candidates.json +5 -4
  104. 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
+ }