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,23 @@
|
|
|
1
|
+
import type {ComponentFitBinding} from './component-fit.mjs';
|
|
2
|
+
export interface TargetImageProvenance {
|
|
3
|
+
schema: 'abelien.appearance-target-image.v1';
|
|
4
|
+
baseline: ComponentFitBinding;
|
|
5
|
+
targetGlbSha256: string;
|
|
6
|
+
targetNodeNames: string[];
|
|
7
|
+
targetWorldMatrix: number[];
|
|
8
|
+
triangleCount: number;
|
|
9
|
+
imageSha256?: string;
|
|
10
|
+
rendererSha256?: string;
|
|
11
|
+
view?: {preset: 'isometric'; elevationDeg: 25; azimuthDeg: -55; size: 640; basis: 'gltf_y_up_to_render_z_up'; appearance: 'diffuse_base_color'};
|
|
12
|
+
}
|
|
13
|
+
/** Input baseline is server-resolved current owned draft; does not independently authorize ownership. */
|
|
14
|
+
export function extractTargetComponent(input: {baseline: ComponentFitBinding; baselineGlb: Uint8Array}): {targetGlb: Buffer; provenance: TargetImageProvenance};
|
|
15
|
+
/** Private trusted-host diagnostics; never part of a public asset or user-facing error. */
|
|
16
|
+
export interface TargetRenderDiagnostic {
|
|
17
|
+
schema: 'abelien.appearance-target-render-diagnostic.v1'; baseline: ComponentFitBinding; rendererSha256: string;
|
|
18
|
+
durationMs: number; status: 'succeeded'|'failed'; exitCode: number|null; signal: string|null;
|
|
19
|
+
stderr: string; stderrBytes: number; stderrTruncated: boolean;
|
|
20
|
+
error: {code:string;name:string;message:string;stack?:string}|null;
|
|
21
|
+
}
|
|
22
|
+
/** Administrator-selected absolute executable path; no model/browser path input, no silent fallback. */
|
|
23
|
+
export function renderTargetComponentImage(input: {baseline: ComponentFitBinding; baselineGlb: Uint8Array}, options: {pythonPath: string; signal?: AbortSignal; onDiagnostic?: (event: TargetRenderDiagnostic)=>void}): Promise<{targetGlb: Buffer; png: Buffer; provenance: TargetImageProvenance}>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Trusted same-baseline target-only extraction and actual fixed-camera raster rendering. */
|
|
2
|
+
import {spawn} from 'node:child_process';
|
|
3
|
+
import {mkdtemp,readFile,rm} from 'node:fs/promises';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {fileURLToPath} from 'node:url';
|
|
7
|
+
import {encodeGlb,fail,MAX_GLB_BYTES,parseGlb,sha256,triangles} from './component-fit-glb.mjs';
|
|
8
|
+
const here=path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const textureInfos=material=>[material.pbrMetallicRoughness?.baseColorTexture,material.pbrMetallicRoughness?.metallicRoughnessTexture,material.normalTexture,material.occlusionTexture,material.emissiveTexture].filter(Boolean);
|
|
10
|
+
const components={SCALAR:1,VEC2:2,VEC3:3,VEC4:4,MAT2:4,MAT3:9,MAT4:16},bytes={5120:1,5121:1,5122:2,5123:2,5125:4,5126:4};
|
|
11
|
+
|
|
12
|
+
export function extractTargetComponent({baseline,baselineGlb}){
|
|
13
|
+
if(!baseline||typeof baseline.draftId!=='string'||!baseline.draftId||typeof baseline.baselineId!=='string'||!baseline.baselineId||!Number.isSafeInteger(baseline.baselineRevision)||baseline.baselineRevision<0||typeof baseline.targetOccurrenceId!=='string'||!baseline.targetOccurrenceId||!(baselineGlb instanceof Uint8Array)||baselineGlb.byteLength>MAX_GLB_BYTES||sha256(baselineGlb)!==baseline.baselineGlbSha256)fail('invalid_target_image_baseline');
|
|
14
|
+
const parsed=parseGlb(baselineGlb),source=parsed.doc;
|
|
15
|
+
const matches=source.nodes.flatMap((node,index)=>node.name===baseline.targetOccurrenceId?[index]:[]);
|
|
16
|
+
if(matches.length!==1)fail('missing_or_ambiguous_target_image_node');
|
|
17
|
+
const target=matches[0],selected=[];
|
|
18
|
+
const visit=index=>{selected.push(index);for(const child of source.nodes[index].children??[])visit(child);};visit(target);
|
|
19
|
+
const mapping=new Map(selected.map((index,next)=>[index,next]));
|
|
20
|
+
const doc={asset:structuredClone(source.asset),scene:0,scenes:[{nodes:[0]}],nodes:[],meshes:[],accessors:[],bufferViews:[],materials:[],textures:[],images:[],samplers:[],buffers:[{byteLength:0}]};
|
|
21
|
+
const chunks=[],maps=Object.fromEntries(['meshes','accessors','materials','textures','images','samplers'].map(key=>[key,new Map()]));let length=0,triangleCount=0;
|
|
22
|
+
const appendView=buffer=>{
|
|
23
|
+
const padding=(4-length%4)%4;if(padding){chunks.push(Buffer.alloc(padding));length+=padding;}
|
|
24
|
+
const index=doc.bufferViews.length;doc.bufferViews.push({buffer:0,byteOffset:length,byteLength:buffer.length});chunks.push(buffer);length+=buffer.length;return index;
|
|
25
|
+
};
|
|
26
|
+
const copyResource=(key,index)=>{
|
|
27
|
+
if(maps[key].has(index))return maps[key].get(index);
|
|
28
|
+
const next=doc[key].length,row=structuredClone(source[key][index]);maps[key].set(index,next);doc[key].push(row);
|
|
29
|
+
if(key==='meshes')for(const primitive of row.primitives){
|
|
30
|
+
for(const attribute of Object.keys(primitive.attributes))primitive.attributes[attribute]=copyResource('accessors',primitive.attributes[attribute]);
|
|
31
|
+
if(primitive.indices!==undefined)primitive.indices=copyResource('accessors',primitive.indices);
|
|
32
|
+
if(primitive.material!==undefined)primitive.material=copyResource('materials',primitive.material);
|
|
33
|
+
}else if(key==='accessors'){
|
|
34
|
+
const view=source.bufferViews[row.bufferView],itemBytes=components[row.type]*bytes[row.componentType],stride=view.byteStride??itemBytes,offset=(view.byteOffset??0)+(row.byteOffset??0),buffer=Buffer.alloc(row.count*itemBytes);
|
|
35
|
+
for(let i=0;i<row.count;i++)parsed.bin.copy(buffer,i*itemBytes,offset+i*stride,offset+i*stride+itemBytes);
|
|
36
|
+
row.bufferView=appendView(buffer);delete row.byteOffset;
|
|
37
|
+
}else if(key==='materials')for(const texture of textureInfos(row))texture.index=copyResource('textures',texture.index);
|
|
38
|
+
else if(key==='textures'){row.source=copyResource('images',row.source);if(row.sampler!==undefined)row.sampler=copyResource('samplers',row.sampler);}
|
|
39
|
+
else if(key==='images'){const view=source.bufferViews[row.bufferView];row.bufferView=appendView(Buffer.from(parsed.bin.subarray(view.byteOffset??0,(view.byteOffset??0)+view.byteLength)));}
|
|
40
|
+
return next;
|
|
41
|
+
};
|
|
42
|
+
for(const index of selected){
|
|
43
|
+
const node=structuredClone(source.nodes[index]);if(node.mesh!==undefined)node.mesh=copyResource('meshes',node.mesh);
|
|
44
|
+
if(node.children)node.children=node.children.map(child=>mapping.get(child));
|
|
45
|
+
if(index===target){delete node.translation;delete node.rotation;delete node.scale;node.matrix=[...parsed.world.get(target)];}
|
|
46
|
+
doc.nodes.push(node);
|
|
47
|
+
for(const _ of triangles(parsed,index,parsed.world.get(index)))if(++triangleCount>200000)fail('target_image_geometry_too_large');
|
|
48
|
+
}
|
|
49
|
+
if(!triangleCount)fail('target_image_has_no_geometry');
|
|
50
|
+
for(const key of ['extensionsUsed','extensionsRequired'])if(source[key])doc[key]=structuredClone(source[key]);
|
|
51
|
+
for(const key of ['materials','textures','images','samplers'])if(!doc[key].length)delete doc[key];
|
|
52
|
+
doc.buffers[0].byteLength=length;
|
|
53
|
+
const targetGlb=encodeGlb(doc,Buffer.concat(chunks));parseGlb(targetGlb);
|
|
54
|
+
return {targetGlb,provenance:{schema:'abelien.appearance-target-image.v1',baseline:structuredClone(baseline),targetGlbSha256:sha256(targetGlb),targetNodeNames:selected.map(index=>source.nodes[index].name??`node:${index}`),targetWorldMatrix:[...parsed.world.get(target)],triangleCount}};
|
|
55
|
+
}
|
|
56
|
+
function renderMeshes(targetGlb){
|
|
57
|
+
const parsed=parseGlb(targetGlb),mesh=[];
|
|
58
|
+
for(let index=0;index<parsed.doc.nodes.length;index++){
|
|
59
|
+
const node=parsed.doc.nodes[index];if(node.mesh===undefined)continue;
|
|
60
|
+
for(const primitive of parsed.doc.meshes[node.mesh].primitives){
|
|
61
|
+
const material=parsed.doc.materials?.[primitive.material]??{},pbr=material.pbrMetallicRoughness??{},color=pbr.baseColorFactor??[1,1,1,1];
|
|
62
|
+
if(textureInfos(material).length||primitive.attributes.COLOR_0!==undefined||material.extensions?.KHR_materials_unlit||(material.alphaMode??'OPAQUE')!=='OPAQUE'||(material.emissiveFactor??[0,0,0]).some(value=>value!==0)||!Array.isArray(color)||color.length!==4||color.some(value=>!Number.isFinite(value)||value<0||value>1)||color[3]!==1)fail('target_renderer_appearance_unsupported');
|
|
63
|
+
const isolated={...parsed,doc:{...parsed.doc,meshes:[{primitives:[primitive]}],nodes:[{mesh:0}]}};
|
|
64
|
+
const values=[...triangles(isolated,0,parsed.world.get(index))].map(triangle=>triangle.map(([x,y,z])=>[x*1000,-z*1000,y*1000]));
|
|
65
|
+
const hex='#'+color.slice(0,3).map(linear=>Math.round((linear<=0.0031308?12.92*linear:1.055*linear**(1/2.4)-0.055)*255).toString(16).padStart(2,'0')).join('');
|
|
66
|
+
mesh.push({occurrenceId:node.name??`target-${index}`,color:hex,triangles:values});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return mesh;
|
|
70
|
+
}
|
|
71
|
+
export async function renderTargetComponentImage(input,{pythonPath,signal,onDiagnostic}={}){
|
|
72
|
+
if(!path.isAbsolute(pythonPath??'')||/[\x00-\x1f]/.test(pythonPath??''))fail('target_renderer_unavailable');
|
|
73
|
+
if(signal?.aborted)fail('target_renderer_aborted');
|
|
74
|
+
const extracted=extractTargetComponent(input),meshes=renderMeshes(extracted.targetGlb);
|
|
75
|
+
const script=path.join(here,'component-target-render.py'),renderer=path.join(here,'cad.py');
|
|
76
|
+
const rendererSha256=sha256(Buffer.concat([await readFile(script),await readFile(renderer)]));
|
|
77
|
+
const body=Buffer.from(JSON.stringify({meshes}));if(body.length>32*1024*1024)fail('target_image_geometry_too_large');
|
|
78
|
+
const root=await mkdtemp(path.join(tmpdir(),'abelien-target-render-'));
|
|
79
|
+
try{
|
|
80
|
+
const output=await new Promise((resolve,reject)=>{
|
|
81
|
+
const child=spawn(pythonPath,['-I','-B',script],{cwd:root,env:{PATH:'/usr/bin:/bin',LANG:'en_US.UTF-8',MPLBACKEND:'Agg',MPLCONFIGDIR:root,HOME:root,TMPDIR:root,OPENBLAS_NUM_THREADS:'1',OMP_NUM_THREADS:'1',VECLIB_MAXIMUM_THREADS:'1'},stdio:['pipe','pipe','pipe']});
|
|
82
|
+
const chunks=[],started=performance.now();let size=0,settled=false,stderr=Buffer.alloc(0),stderrBytes=0;
|
|
83
|
+
// Private process diagnostics retain the tail so a Python traceback survives verbose warnings.
|
|
84
|
+
// The child receives only a fixed credential-free environment and numeric mesh data.
|
|
85
|
+
const end=(code,value,details={})=>{if(settled)return;settled=true;clearTimeout(timer);signal?.removeEventListener('abort',abort);
|
|
86
|
+
const error=code?Object.assign(new Error(code),{code}):null;
|
|
87
|
+
const normalizedStderr=Buffer.from(stderr.toString('utf8'));
|
|
88
|
+
const diagnostic={schema:'abelien.appearance-target-render-diagnostic.v1',baseline:structuredClone(input.baseline),rendererSha256,
|
|
89
|
+
durationMs:performance.now()-started,status:code?'failed':'succeeded',exitCode:details.exitCode??null,signal:details.signal??null,
|
|
90
|
+
stderr:normalizedStderr.subarray(Math.max(0,normalizedStderr.length-65532)).toString('utf8'),stderrBytes,stderrTruncated:stderrBytes>stderr.length||normalizedStderr.length>65532,
|
|
91
|
+
error:code?{code,name:details.error?.name??error.name,message:details.error?.message??error.message,stack:details.error?.stack??error.stack}:null};
|
|
92
|
+
try{const observed=onDiagnostic?.(diagnostic);observed?.catch?.(()=>{});}catch{/* diagnostics must not alter render state */}
|
|
93
|
+
if(error){child.kill('SIGKILL');reject(error);}else resolve(value);};
|
|
94
|
+
const abort=()=>end('target_renderer_aborted'),timer=setTimeout(()=>end('target_renderer_timeout'),60000);
|
|
95
|
+
signal?.addEventListener('abort',abort,{once:true});if(signal?.aborted)abort();
|
|
96
|
+
child.once('error',error=>end('target_renderer_unavailable',undefined,{error}));child.stdin.on('error',error=>end('target_renderer_failed',undefined,{error}));
|
|
97
|
+
child.stderr.on('data',chunk=>{stderrBytes+=chunk.length;stderr=Buffer.concat([stderr,chunk]);if(stderr.length>65536)stderr=stderr.subarray(stderr.length-65536);});
|
|
98
|
+
child.stdout.on('data',chunk=>{size+=chunk.length;if(size>5*1024*1024)end('target_renderer_output_too_large');else chunks.push(chunk);});
|
|
99
|
+
child.once('close',(code,exitSignal)=>end(code===0?null:'target_renderer_failed',Buffer.concat(chunks),{exitCode:code,signal:exitSignal}));child.stdin.end(body);
|
|
100
|
+
});
|
|
101
|
+
let result;try{result=JSON.parse(output.toString('utf8'));}catch{fail('target_renderer_invalid_output');}
|
|
102
|
+
if(result.rendererSha256!==rendererSha256)fail('target_renderer_source_drift');
|
|
103
|
+
const png=Buffer.from(result.png??'','base64');
|
|
104
|
+
if(!png.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10]))||png.length<33||png.readUInt32BE(16)!==640||png.readUInt32BE(20)!==640)fail('target_renderer_invalid_output');
|
|
105
|
+
return {...extracted,png,provenance:{...extracted.provenance,imageSha256:sha256(png),rendererSha256,view:{preset:'isometric',elevationDeg:25,azimuthDeg:-55,size:640,basis:'gltf_y_up_to_render_z_up',appearance:'diffuse_base_color'}}};
|
|
106
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
107
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Data-only local target rasterizer; reuse the trusted Worker render_scene function."""
|
|
2
|
+
import ast
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
import traceback
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
import numpy as np
|
|
14
|
+
import matplotlib
|
|
15
|
+
matplotlib.use('Agg')
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
|
|
18
|
+
source_path = Path(__file__).with_name('cad.py')
|
|
19
|
+
source_bytes = source_path.read_bytes()
|
|
20
|
+
source = source_bytes.decode('utf-8')
|
|
21
|
+
renderer_sha256 = hashlib.sha256(Path(__file__).read_bytes() + source_bytes).hexdigest()
|
|
22
|
+
functions = [item for item in ast.parse(source).body if isinstance(item, ast.FunctionDef) and item.name == 'render_scene']
|
|
23
|
+
if len(functions) != 1:
|
|
24
|
+
raise ValueError('renderer_unavailable')
|
|
25
|
+
# Do not import cad.py: its top-level consumes a modeling request. Only the
|
|
26
|
+
# reviewed renderer function is compiled, with fixed numeric/image globals.
|
|
27
|
+
namespace = {'np': np, 'plt': plt, 'math': math, 'hashlib': hashlib}
|
|
28
|
+
exec(compile(ast.Module(body=functions, type_ignores=[]), str(source_path), 'exec'), namespace)
|
|
29
|
+
raw = sys.stdin.buffer.read(32 * 1024 * 1024 + 1)
|
|
30
|
+
if len(raw) > 32 * 1024 * 1024:
|
|
31
|
+
raise ValueError('geometry_too_large')
|
|
32
|
+
request = json.loads(raw)
|
|
33
|
+
mesh = []
|
|
34
|
+
count = 0
|
|
35
|
+
for item in request['meshes']:
|
|
36
|
+
triangles = np.asarray(item['triangles'], dtype=np.float64)
|
|
37
|
+
if triangles.ndim != 3 or triangles.shape[1:] != (3, 3) or not np.isfinite(triangles).all():
|
|
38
|
+
raise ValueError('invalid_triangles')
|
|
39
|
+
count += len(triangles)
|
|
40
|
+
if count > 200000:
|
|
41
|
+
raise ValueError('geometry_too_large')
|
|
42
|
+
color = item['color']
|
|
43
|
+
if not isinstance(color, str) or len(color) != 7 or color[0] != '#' or any(ch not in '0123456789abcdef' for ch in color[1:]):
|
|
44
|
+
raise ValueError('invalid_color')
|
|
45
|
+
occurrence = {'occurrenceId': item['occurrenceId'], 'bomItemId': None, 'material': {'baseColorSrgb': color}}
|
|
46
|
+
mesh.append((occurrence, triangles, tuple(triangles.reshape(-1, 3).mean(axis=0))))
|
|
47
|
+
if not count:
|
|
48
|
+
raise ValueError('empty_geometry')
|
|
49
|
+
png = namespace['render_scene'](Path.cwd(), 'target', mesh, 25, -55)
|
|
50
|
+
print(json.dumps({'png': base64.b64encode(png).decode('ascii'), 'rendererSha256': renderer_sha256}, separators=(',', ':')))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == '__main__':
|
|
54
|
+
try:
|
|
55
|
+
main()
|
|
56
|
+
except Exception:
|
|
57
|
+
traceback.print_exc(file=sys.stderr)
|
|
58
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Shared local defaults for the Structure Worker and its low-level native helper. */
|
|
2
|
+
export const DEFAULT_STRUCTURE_PORT = 16424;
|
|
3
|
+
|
|
4
|
+
export function structureLoopbackOrigin(env = process.env) {
|
|
5
|
+
return env.AGENT_STRUCTURE_ORIGIN ?? `http://127.0.0.1:${DEFAULT_STRUCTURE_PORT}`;
|
|
6
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type EngineeringJson = null | boolean | number | string | EngineeringJson[] | { [key: string]: EngineeringJson };
|
|
2
|
+
export interface EngineeringProposal {
|
|
3
|
+
schema: 'abelien.planar-attachment-proposal.v1'; targetOccurrenceId: string; baseOccurrenceId: string;
|
|
4
|
+
plate: { widthMm: number; depthMm: number; thicknessMm: number; holeRadiusMm: number; holeCentersMm: [number,number][] };
|
|
5
|
+
baseThicknessMm: number; decoration: { heightMm: number; maximumWidthMm: number; maximumDepthMm: number; trimBottomFraction: number };
|
|
6
|
+
join: { radiusMm: number; embedMm: number; gapMm: number }; clearanceMm: number;
|
|
7
|
+
}
|
|
8
|
+
export interface EngineeringBaselineIdentity { sourceSha256: string; modelStepSha256: string; assemblySha256: string; occurrenceSetSha256: string; attachmentSha256: string; targetGeometrySha256: string; targetTransformSha256: string }
|
|
9
|
+
export interface AttachmentBaseline extends EngineeringBaselineIdentity {
|
|
10
|
+
schema: 'abelien.planar-attachment-baseline.v1'; checkerVersion: 'planar-two-hole-fit.v1';
|
|
11
|
+
asModeledOnlyOccurrenceIds:string[]; policy: Record<string,EngineeringJson>; proposal: EngineeringProposal; checks: Record<string,EngineeringJson>;
|
|
12
|
+
}
|
|
13
|
+
export interface EngineeringRecipe {
|
|
14
|
+
schema: 'abelien.component-engineering-recipe.v1'; checkerVersion: 'planar-two-hole-fit.v1'; policy: Record<string,EngineeringJson>;
|
|
15
|
+
baseline: EngineeringBaselineIdentity; targetOccurrenceId: string; candidateGlbSha256: string;
|
|
16
|
+
decorationGlbSha256:string; repair:EngineeringRepair;
|
|
17
|
+
placement: { method: 'declared_design_height'; sourceUnits: 'unknown'; scaleMmPerSourceUnit: number; translationMm: number[]; trimBottomMm: number; cutZMm: number; heightBeforeTrimMm: number };
|
|
18
|
+
}
|
|
19
|
+
export interface EngineeringFitReport {
|
|
20
|
+
schema: 'abelien.component-engineering-fit-report.v1'; checkerVersion: 'planar-two-hole-fit.v1'; policy: Record<string,EngineeringJson>;
|
|
21
|
+
representation: 'analytic_carrier_faceted_decoration'; scope: 'planar_two_hole_solid_decoration.v1'; fitStatus: 'pass'; adoptionEligible: true;
|
|
22
|
+
baseline: EngineeringBaselineIdentity; targetOccurrenceId: string; candidateGlbSha256: string; recipeSha256: string; modelStepSha256: string; modelGlbSha256: string; assemblySha256: string;
|
|
23
|
+
asModeledOnlyOccurrenceIds:string[]; decorationGlbSha256:string; repair:EngineeringRepair;
|
|
24
|
+
subject: {sourceSha256:string;modelStepSha256:string;modelGlbSha256:string;assemblySha256:string;occurrenceSetSha256:string};
|
|
25
|
+
checks: {id:string;status:'pass';method:'occt_brep_independent_readback';measured:Record<string,EngineeringJson>}[];
|
|
26
|
+
notChecked: string[]; decorationShellWall: { status:'not_applicable'; reason:'filled_decoration_no_cavity' };
|
|
27
|
+
}
|
|
28
|
+
export interface EngineeringFitRequest {
|
|
29
|
+
operation:'component_fit';key:string;basis:string;title:string;pass:'final';
|
|
30
|
+
fit:{priorJobId:string;priorSubject:{sourceSha256:string;assemblySha256:string;occurrenceSetSha256:string};attachmentSha256:string;candidateGlb:{sha256:string;base64:string}};
|
|
31
|
+
}
|
|
32
|
+
export const ENGINEERING_VERSION:'planar-two-hole-fit.v1';
|
|
33
|
+
export const ENGINEERING_MAX_BODY_BYTES:number;
|
|
34
|
+
export const ENGINEERING_ARTIFACT_TYPES:Readonly<Record<string,string>>;
|
|
35
|
+
export const ENGINEERING_CHECKS:readonly string[];
|
|
36
|
+
export const ENGINEERING_NOT_CHECKED:readonly string[];
|
|
37
|
+
export function normalizeEngineeringFitRequest(input:unknown):{input:EngineeringFitRequest;triangles:number[][][]};
|
|
38
|
+
/** Caller must obtain assembly/STEP from the same trusted worker/baseline job. */
|
|
39
|
+
export function validateAttachmentBaseline(value:unknown,assembly:unknown,step:Uint8Array):AttachmentBaseline;
|
|
40
|
+
/** Byte/identity verifier; geometric truth comes from the pinned fresh native STEP checker. */
|
|
41
|
+
export function validateEngineeringArtifacts<T>(files:Map<string,Uint8Array>,expected:{attachment:AttachmentBaseline;assembly:T;candidateGlbSha256:string}):{report:EngineeringFitReport;recipe:EngineeringRecipe;assembly:T};
|
|
42
|
+
|
|
43
|
+
export const ENGINEERING_POLICY:Readonly<Record<string,EngineeringJson>>;
|
|
44
|
+
export function validateEngineeringStoredArtifacts<T>(files:Map<string,Uint8Array>,expected:{attachment:AttachmentBaseline;assembly:T;candidateGlbSha256:string}):{report:EngineeringFitReport;recipe:EngineeringRecipe;assembly:T};
|
|
45
|
+
|
|
46
|
+
export interface EngineeringRepair {
|
|
47
|
+
status:'not_needed'|'applied';method:'none'|'bounded_local_laplacian';originalGlbSha256:string;derivedGlbSha256:string;topologySha256:string;topologyBinding:'identical_bytes'|'all_non_position_bytes';iterations:number;originalInterferenceCount:number;remainingInterferenceCount:0;uniqueVertexCount:number;changedVertexCount:number;changedVertexFraction:number;maximumDisplacementMm:number;rmsDisplacementMm:number;signedVolumeRelativeChange:number;boundsMaximumChangeMm:number;movedVertexIds:number[];
|
|
48
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Strict server-only engineering transport/artifact identity; kernel supplies geometric proof. */
|
|
2
|
+
import engineeringPolicy from './engineering-policy.json' with {type:'json'};
|
|
3
|
+
import {validateAssemblyEvidence} from './assembly-validation.mjs';
|
|
4
|
+
import {parseGlb,triangles,sha256} from './component-fit-glb.mjs';
|
|
5
|
+
import {manifestHash} from './manifest-digest.mjs';
|
|
6
|
+
export const ENGINEERING_VERSION='planar-two-hole-fit.v1';
|
|
7
|
+
export const ENGINEERING_POLICY=Object.freeze(engineeringPolicy);
|
|
8
|
+
export const ENGINEERING_MAX_BODY_BYTES=8*1024*1024;
|
|
9
|
+
export const ENGINEERING_ARTIFACT_TYPES=Object.freeze({'model.step':'model/step','model.glb':'model/gltf-binary','decoration.glb':'model/gltf-binary',
|
|
10
|
+
'front.png':'image/png','top.png':'image/png','iso.png':'image/png','normal.png':'image/png','internal.png':'image/png','exploded.png':'image/png',
|
|
11
|
+
'assembly.json':'application/json','fit.json':'application/json','recipe.json':'application/json'});
|
|
12
|
+
export const ENGINEERING_CHECKS=Object.freeze(['baseline','closed_solid','self_intersection','carrier_preservation','interface_surfaces','protected_regions','continuous_join','static_collision','clearance','planar_insertion','preserved_occurrences','step_readback']);
|
|
13
|
+
export const ENGINEERING_NOT_CHECKED=Object.freeze(['strength','manufacturability','physical_assembly','local_thin_features','texture_fidelity','hardware_geometry_accuracy']);
|
|
14
|
+
const HEX=/^[a-f0-9]{64}$/,UUID=/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/;
|
|
15
|
+
const fail=()=>{throw Object.assign(new Error('invalid_engineering_artifact'),{code:'invalid_input'});};
|
|
16
|
+
const keys=(v,n)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).length===n.length&&n.every(k=>Object.hasOwn(v,k));
|
|
17
|
+
const same=(a,b)=>manifestHash(a)===manifestHash(b);
|
|
18
|
+
export function normalizeEngineeringFitRequest(input){
|
|
19
|
+
if(!keys(input,['operation','key','basis','title','pass','fit'])||input.operation!=='component_fit'||input.pass!=='final'||!HEX.test(input.key??'')||!HEX.test(input.basis??'')||typeof input.title!=='string'||!input.title.trim()||input.title.length>100)fail();
|
|
20
|
+
const f=input.fit;
|
|
21
|
+
if(!keys(f,['priorJobId','priorSubject','attachmentSha256','candidateGlb'])||!UUID.test(f.priorJobId??'')||!HEX.test(f.attachmentSha256??'')||
|
|
22
|
+
!keys(f.priorSubject,['sourceSha256','assemblySha256','occurrenceSetSha256'])||Object.values(f.priorSubject).some(v=>!HEX.test(v??''))||
|
|
23
|
+
!keys(f.candidateGlb,['sha256','base64'])||!HEX.test(f.candidateGlb.sha256??'')||typeof f.candidateGlb.base64!=='string'||f.candidateGlb.base64.length>ENGINEERING_MAX_BODY_BYTES-65536)fail();
|
|
24
|
+
const bytes=Buffer.from(f.candidateGlb.base64,'base64');if(!bytes.length||bytes.toString('base64')!==f.candidateGlb.base64||sha256(bytes)!==f.candidateGlb.sha256)fail();
|
|
25
|
+
const parsed=parseGlb(bytes),faces=[];
|
|
26
|
+
if((parsed.doc.textures??[]).length||(parsed.doc.images??[]).length)fail();
|
|
27
|
+
for(const [node,matrix] of parsed.world){
|
|
28
|
+
for(const triangle of triangles(parsed,node,matrix)){faces.push(triangle);if(faces.length>ENGINEERING_POLICY.maximumTriangles)fail();}
|
|
29
|
+
}
|
|
30
|
+
if(faces.length<4)fail();
|
|
31
|
+
return {input:structuredClone(input),triangles:faces};
|
|
32
|
+
}
|
|
33
|
+
export function validateAttachmentBaseline(value,assembly,step){
|
|
34
|
+
if(!keys(value,['schema','checkerVersion','policy','proposal','sourceSha256','modelStepSha256','assemblySha256','occurrenceSetSha256','targetGeometrySha256','targetTransformSha256','asModeledOnlyOccurrenceIds','checks','attachmentSha256'])||
|
|
35
|
+
value.schema!=='abelien.planar-attachment-baseline.v1'||value.checkerVersion!==ENGINEERING_VERSION||!same(value.policy,ENGINEERING_POLICY)||
|
|
36
|
+
value.sourceSha256!==assembly.sourceSha256||value.modelStepSha256!==sha256(step)||value.assemblySha256!==assembly.assemblySha256||value.occurrenceSetSha256!==assembly.occurrenceSetSha256)fail();
|
|
37
|
+
if(!same(value.asModeledOnlyOccurrenceIds,assembly.occurrences.filter(o=>o.geometryPrecision==='envelope').map(o=>o.occurrenceId)))fail();
|
|
38
|
+
const target=assembly.occurrences.find(o=>o.occurrenceId===value.proposal?.targetOccurrenceId);
|
|
39
|
+
if(!target||target.geometrySha256!==value.targetGeometrySha256||target.transformSha256!==value.targetTransformSha256||value.checks?.staticCollision!=='pass'||value.checks?.planarInsertion!=='pass')fail();
|
|
40
|
+
const measured=value.checks;
|
|
41
|
+
if(!measured||!withinVolume(measured.carrierSymmetricDifferenceMm3)||!withinVolume(measured.baseSymmetricDifferenceMm3)||!validInterfaces(measured.interfaces,value.proposal.plate))fail();
|
|
42
|
+
const unsigned={...value};delete unsigned.attachmentSha256;if(value.attachmentSha256!==manifestHash(unsigned))fail();
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
const STORED_NAMES=Object.freeze(Object.keys(ENGINEERING_ARTIFACT_TYPES).filter(n=>!['front.png','top.png','iso.png'].includes(n)));
|
|
46
|
+
export function validateEngineeringArtifacts(files,expected){return validateFiles(files,expected,Object.keys(ENGINEERING_ARTIFACT_TYPES));}
|
|
47
|
+
export function validateEngineeringStoredArtifacts(files,expected){return validateFiles(files,expected,STORED_NAMES);}
|
|
48
|
+
const finite=v=>typeof v==='number'&&Number.isFinite(v);
|
|
49
|
+
const withinVolume=v=>finite(v)&&v>=0&&v<=ENGINEERING_POLICY.volumeToleranceMm3;
|
|
50
|
+
const near=(a,b)=>finite(a)&&finite(b)&&Math.abs(a-b)<=ENGINEERING_POLICY.linearToleranceMm;
|
|
51
|
+
function validInterfaces(value,p){
|
|
52
|
+
if(!keys(value,['holes','seatAreaMm2'])||!Array.isArray(value.holes)||value.holes.length!==2||!near(value.seatAreaMm2,p.widthMm*p.depthMm-2*Math.PI*p.holeRadiusMm**2))return false;
|
|
53
|
+
return value.holes.every((h,i)=>keys(h,['center','radius','zMin','zMax'])&&Array.isArray(h.center)&&h.center.length===2&&h.center.every((v,k)=>near(v,p.holeCentersMm[i][k]))&&near(h.radius,p.holeRadiusMm)&&near(h.zMin,0)&&near(h.zMax,p.thicknessMm));
|
|
54
|
+
}
|
|
55
|
+
function validateMeasurements(report,expected){
|
|
56
|
+
const m=report.checks.map(c=>c.measured),p=expected.attachment.proposal;
|
|
57
|
+
const ids=expected.assembly.occurrences.filter(o=>o.occurrenceId!==p.targetOccurrenceId).map(o=>o.occurrenceId);
|
|
58
|
+
const pairs=(v,field,check)=>Array.isArray(v)&&v.length===ids.length&&v.every((row,i)=>keys(row,['occurrenceId',field])&&row.occurrenceId===ids[i]&&check(row[field]));
|
|
59
|
+
if(!same(m[0],{validatedAttachmentSha256:expected.attachment.attachmentSha256})||!same(m[1],{solidCount:1,shellCount:1})||!same(m[2],{interferenceCount:0})||
|
|
60
|
+
!keys(m[3],['lostVolumeMm3'])||!withinVolume(m[3].lostVolumeMm3)||!validInterfaces(m[4],p.plate)||
|
|
61
|
+
!keys(m[5],['symmetricDifferenceMm3','holeObstructionMm3','seatDifferenceMm3'])||!['symmetricDifferenceMm3','holeObstructionMm3'].every(k=>Array.isArray(m[5][k])&&m[5][k].length===2&&m[5][k].every(withinVolume))||!withinVolume(m[5].seatDifferenceMm3)||
|
|
62
|
+
!keys(m[6],['missingWitnessMm3','radiusMm','sectionAreaMm2','embedMm'])||!withinVolume(m[6].missingWitnessMm3)||!near(m[6].radiusMm,p.join.radiusMm)||!near(m[6].sectionAreaMm2,Math.PI*p.join.radiusMm**2)||!near(m[6].embedMm,p.join.embedMm)||
|
|
63
|
+
!keys(m[7],['pairs'])||!pairs(m[7].pairs,'overlapMm3',withinVolume)||!keys(m[8],['minimumRequiredMm','pairs'])||m[8].minimumRequiredMm!==p.clearanceMm||!pairs(m[8].pairs,'distanceMm',v=>finite(v)&&v+ENGINEERING_POLICY.linearToleranceMm>=p.clearanceMm)||
|
|
64
|
+
!same(m[9],{method:'brep_separating_halfspaces',outwardLocal:[0,0,1]})||!keys(m[10],['occurrences'])||!pairs(m[10].occurrences,'symmetricDifferenceMm3',withinVolume)||
|
|
65
|
+
!keys(m[11],['compositionSymmetricDifferenceMm3','solidCount'])||!withinVolume(m[11].compositionSymmetricDifferenceMm3)||m[11].solidCount!==expected.assembly.occurrenceCount)fail();
|
|
66
|
+
}
|
|
67
|
+
function validateRepair(r,originalSha,derivedSha,height){
|
|
68
|
+
const fields=['status','method','originalGlbSha256','derivedGlbSha256','topologySha256','topologyBinding','iterations','originalInterferenceCount','remainingInterferenceCount','uniqueVertexCount','changedVertexCount','changedVertexFraction','maximumDisplacementMm','rmsDisplacementMm','signedVolumeRelativeChange','boundsMaximumChangeMm','movedVertexIds'];
|
|
69
|
+
if(!keys(r,fields)||r.originalGlbSha256!==originalSha||r.derivedGlbSha256!==derivedSha||!HEX.test(r.topologySha256??'')||r.remainingInterferenceCount!==0||!Number.isSafeInteger(r.uniqueVertexCount)||r.uniqueVertexCount<4||!Array.isArray(r.movedVertexIds)||r.movedVertexIds.length!==r.changedVertexCount||new Set(r.movedVertexIds).size!==r.movedVertexIds.length||r.movedVertexIds.some(i=>!Number.isSafeInteger(i)||i<0||i>=r.uniqueVertexCount)||r.changedVertexFraction!==r.changedVertexCount/r.uniqueVertexCount)fail();
|
|
70
|
+
const metrics=['maximumDisplacementMm','rmsDisplacementMm','signedVolumeRelativeChange','boundsMaximumChangeMm'];
|
|
71
|
+
if(metrics.some(k=>!finite(r[k])||r[k]<0))fail();
|
|
72
|
+
const rule=ENGINEERING_POLICY.repair,max=Math.min(rule.maximumDisplacementMm,height*rule.maximumHeightFraction);
|
|
73
|
+
if(r.status==='not_needed'){
|
|
74
|
+
if(r.method!=='none'||r.topologyBinding!=='identical_bytes'||r.topologySha256!==originalSha||originalSha!==derivedSha||r.iterations!==0||r.originalInterferenceCount!==0||r.changedVertexCount!==0||metrics.some(k=>r[k]!==0))fail();
|
|
75
|
+
}else if(r.status==='applied'){
|
|
76
|
+
if(r.method!=='bounded_local_laplacian'||r.topologyBinding!=='all_non_position_bytes'||r.iterations!==rule.maximumIterations||!Number.isSafeInteger(r.originalInterferenceCount)||r.originalInterferenceCount<1||r.changedVertexCount<1||r.changedVertexFraction>rule.maximumVertexFraction||r.maximumDisplacementMm>max||r.rmsDisplacementMm>r.maximumDisplacementMm||r.boundsMaximumChangeMm>r.maximumDisplacementMm+ENGINEERING_POLICY.linearToleranceMm||r.signedVolumeRelativeChange>rule.maximumSignedVolumeRelativeChange)fail();
|
|
77
|
+
}else fail();
|
|
78
|
+
}
|
|
79
|
+
function validateFiles(files,expected,names){
|
|
80
|
+
if(!(files instanceof Map)||files.size!==names.length||names.some(n=>!files.has(n)))fail();
|
|
81
|
+
files=new Map([...files].map(([name,body])=>{
|
|
82
|
+
if(!(body instanceof Uint8Array)||!body.byteLength||body.byteLength>(name.endsWith('.png')?4*1024*1024:name.endsWith('.json')?128*1024:24*1024*1024))fail();
|
|
83
|
+
const value=Buffer.from(body);
|
|
84
|
+
if(name.endsWith('.png')&&!value.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))fail();
|
|
85
|
+
if(name.endsWith('.step')&&!value.subarray(0,128).toString().includes('ISO-10303-21'))fail();
|
|
86
|
+
if(name.endsWith('.glb'))parseGlb(value);
|
|
87
|
+
return [name,value];
|
|
88
|
+
}));
|
|
89
|
+
const report=JSON.parse(files.get('fit.json')),recipe=JSON.parse(files.get('recipe.json')),assembly=JSON.parse(files.get('assembly.json'));
|
|
90
|
+
const a=expected.attachment,baseline=Object.fromEntries(['sourceSha256','modelStepSha256','assemblySha256','occurrenceSetSha256','attachmentSha256','targetGeometrySha256','targetTransformSha256'].map(k=>[k,a[k]]));
|
|
91
|
+
if(!keys(report,['schema','checkerVersion','policy','representation','scope','fitStatus','adoptionEligible','baseline','targetOccurrenceId','candidateGlbSha256','recipeSha256','modelStepSha256','modelGlbSha256','assemblySha256','subject','asModeledOnlyOccurrenceIds','repair','decorationGlbSha256','checks','notChecked','decorationShellWall'])||!same(report.policy,ENGINEERING_POLICY)||report.schema!=='abelien.component-engineering-fit-report.v1'||report.checkerVersion!==ENGINEERING_VERSION||report.scope!=='planar_two_hole_solid_decoration.v1'||
|
|
92
|
+
report.representation!=='analytic_carrier_faceted_decoration'||report.fitStatus!=='pass'||report.adoptionEligible!==true||!same(report.baseline,baseline)||
|
|
93
|
+
report.targetOccurrenceId!==a.proposal.targetOccurrenceId||report.candidateGlbSha256!==expected.candidateGlbSha256||report.recipeSha256!==sha256(files.get('recipe.json'))||
|
|
94
|
+
report.modelStepSha256!==sha256(files.get('model.step'))||report.modelGlbSha256!==sha256(files.get('model.glb'))||report.assemblySha256!==assembly.assemblySha256||
|
|
95
|
+
!same(report.notChecked,ENGINEERING_NOT_CHECKED)||!same(report.decorationShellWall,{status:'not_applicable',reason:'filled_decoration_no_cavity'})||!Array.isArray(report.checks)||report.checks.length!==ENGINEERING_CHECKS.length||
|
|
96
|
+
report.checks.some((c,i)=>!keys(c,['id','status','method','measured'])||c.id!==ENGINEERING_CHECKS[i]||c.status!=='pass'||c.method!=='occt_brep_independent_readback'||!c.measured))fail();
|
|
97
|
+
if(!keys(recipe,['schema','checkerVersion','policy','baseline','targetOccurrenceId','candidateGlbSha256','placement','decorationGlbSha256','repair'])||!same(recipe.policy,ENGINEERING_POLICY)||recipe.schema!=='abelien.component-engineering-recipe.v1'||recipe.checkerVersion!==ENGINEERING_VERSION||!same(recipe.baseline,baseline)||recipe.candidateGlbSha256!==expected.candidateGlbSha256||recipe.targetOccurrenceId!==report.targetOccurrenceId)fail();
|
|
98
|
+
if(!same(report.asModeledOnlyOccurrenceIds,a.asModeledOnlyOccurrenceIds)||report.decorationGlbSha256!==sha256(files.get('decoration.glb'))||recipe.decorationGlbSha256!==report.decorationGlbSha256||!same(report.repair,recipe.repair))fail();
|
|
99
|
+
const p=a.proposal,placement=recipe.placement;
|
|
100
|
+
validateRepair(recipe.repair,expected.candidateGlbSha256,report.decorationGlbSha256,p.decoration.heightMm);
|
|
101
|
+
if(!keys(placement,['method','sourceUnits','scaleMmPerSourceUnit','translationMm','trimBottomMm','cutZMm','heightBeforeTrimMm'])||placement.method!=='declared_design_height'||placement.sourceUnits!=='unknown'||!finite(placement.scaleMmPerSourceUnit)||placement.scaleMmPerSourceUnit<=0||!Array.isArray(placement.translationMm)||placement.translationMm.length!==3||!placement.translationMm.every(finite)||!near(placement.trimBottomMm,p.decoration.heightMm*p.decoration.trimBottomFraction)||!near(placement.cutZMm,p.plate.thicknessMm+p.join.gapMm)||placement.heightBeforeTrimMm!==p.decoration.heightMm)fail();
|
|
102
|
+
const subject={sourceSha256:report.recipeSha256,modelStepSha256:report.modelStepSha256,modelGlbSha256:report.modelGlbSha256,assemblySha256:assembly.assemblySha256,occurrenceSetSha256:assembly.occurrenceSetSha256};
|
|
103
|
+
if(!same(report.subject,subject))fail();
|
|
104
|
+
validateMeasurements(report,expected);
|
|
105
|
+
validateAssemblyEvidence(assembly,report.recipeSha256,files.get('model.step'),files.get('model.glb'),files,()=>{
|
|
106
|
+
if(assembly.revision!==null||assembly.mode!=='occurrence_assembly')fail();
|
|
107
|
+
});
|
|
108
|
+
const unsigned={...assembly};delete unsigned.assemblySha256;
|
|
109
|
+
if(assembly.schema!=='abelien.structure-assembly.v2'||assembly.assemblySha256!==manifestHash(unsigned)||assembly.sourceSha256!==report.recipeSha256||assembly.units!=='mm'||
|
|
110
|
+
assembly.artifacts?.modelStepSha256!==report.modelStepSha256||assembly.artifacts?.modelGlbSha256!==report.modelGlbSha256||assembly.occurrenceCount!==expected.assembly.occurrenceCount)fail();
|
|
111
|
+
for(const old of expected.assembly.occurrences){const row=assembly.occurrences.find(r=>r.occurrenceId===old.occurrenceId);if(!row)fail();
|
|
112
|
+
if(row.occurrenceId!==report.targetOccurrenceId&&!same(row,old))fail();
|
|
113
|
+
if(row.occurrenceId===report.targetOccurrenceId&&!same(row,{...old,geometrySha256:row.geometrySha256,source:{kind:'generated_custom',sha256:report.recipeSha256},lineage:null}))fail();
|
|
114
|
+
if(!same(row.views,old.views)||row.interfaceSha256!==old.interfaceSha256||row.appearanceSha256!==old.appearanceSha256)fail();
|
|
115
|
+
}
|
|
116
|
+
for(const name of ['normal','internal','exploded'])if(assembly.views?.[name]?.imageSha256!==sha256(files.get(`${name}.png`))||assembly.views[name].modelGlbSha256!==report.modelGlbSha256)fail();
|
|
117
|
+
return {report,recipe,assembly};
|
|
118
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "planar-two-hole-fit.v1",
|
|
3
|
+
"maximumTriangles": 4000,
|
|
4
|
+
"linearToleranceMm": 1e-05,
|
|
5
|
+
"volumeToleranceMm3": 1e-05,
|
|
6
|
+
"maximumDimensionMm": 500,
|
|
7
|
+
"maximumOccurrences": 32,
|
|
8
|
+
"repair": {
|
|
9
|
+
"maximumIterations": 1,
|
|
10
|
+
"lambda": 0.25,
|
|
11
|
+
"maximumDisplacementMm": 0.25,
|
|
12
|
+
"maximumHeightFraction": 0.01,
|
|
13
|
+
"maximumVertexFraction": 0.05,
|
|
14
|
+
"maximumSignedVolumeRelativeChange": 0.001
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""One explicitly reported local position-only repair. Never changes input bytes or topology."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import struct
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def require(condition, code):
|
|
10
|
+
if not condition:
|
|
11
|
+
raise ValueError('engineering_repair_' + code)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def sha(body):
|
|
15
|
+
return hashlib.sha256(body).hexdigest()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def layout(body, maximum_positions):
|
|
19
|
+
"""Repair is deliberately limited to one static mesh primitive with float32 POSITION."""
|
|
20
|
+
size = struct.unpack_from('<I', body, 12)[0]
|
|
21
|
+
doc = json.loads(body[20:20+size])
|
|
22
|
+
nodes = [(i, n) for i, n in enumerate(doc['nodes']) if 'mesh' in n]
|
|
23
|
+
require(len(nodes) == 1, 'unsupported_mesh_instances')
|
|
24
|
+
node_index, node = nodes[0]
|
|
25
|
+
primitives = doc['meshes'][node['mesh']]['primitives']
|
|
26
|
+
require(len(primitives) == 1, 'unsupported_primitives')
|
|
27
|
+
primitive = primitives[0]
|
|
28
|
+
accessor = doc['accessors'][primitive['attributes']['POSITION']]
|
|
29
|
+
require(accessor['componentType'] == 5126 and accessor['type'] == 'VEC3', 'positions')
|
|
30
|
+
view = doc['bufferViews'][accessor['bufferView']]
|
|
31
|
+
offset = 28 + size + view.get('byteOffset', 0) + accessor.get('byteOffset', 0)
|
|
32
|
+
require(accessor['count'] <= maximum_positions, 'position_budget')
|
|
33
|
+
stride = view.get('byteStride', 12)
|
|
34
|
+
positions = np.array([struct.unpack_from('<fff', body, offset+i*stride) for i in range(accessor['count'])])
|
|
35
|
+
parents = {child: i for i, n in enumerate(doc['nodes']) for child in n.get('children', [])}
|
|
36
|
+
def matrix(index):
|
|
37
|
+
n = doc['nodes'][index]
|
|
38
|
+
if 'matrix' in n:
|
|
39
|
+
result = np.array(n['matrix']).reshape(4, 4, order='F')
|
|
40
|
+
else:
|
|
41
|
+
x, y, z, w = n.get('rotation', [0, 0, 0, 1])
|
|
42
|
+
result = np.eye(4)
|
|
43
|
+
result[:3, :3] = [[1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+y*w)],
|
|
44
|
+
[2*(x*y+z*w), 1-2*(x*x+z*z), 2*(y*z-x*w)],
|
|
45
|
+
[2*(x*z-y*w), 2*(y*z+x*w), 1-2*(x*x+y*y)]]
|
|
46
|
+
result[:3, 3] = n.get('translation', [0, 0, 0])
|
|
47
|
+
return matrix(parents[index]) @ result if index in parents else result
|
|
48
|
+
transform = matrix(node_index)
|
|
49
|
+
require(np.allclose(transform[:3, :3].T @ transform[:3, :3], np.eye(3), atol=1e-8), 'nonrigid')
|
|
50
|
+
if 'indices' in primitive:
|
|
51
|
+
index = doc['accessors'][primitive['indices']]
|
|
52
|
+
iv = doc['bufferViews'][index['bufferView']]
|
|
53
|
+
kind = {5121: 'B', 5123: 'H', 5125: 'I'}[index['componentType']]
|
|
54
|
+
start = 28+size+iv.get('byteOffset', 0)+index.get('byteOffset', 0)
|
|
55
|
+
indexes = np.array([struct.unpack_from('<'+kind, body, start+i*iv.get('byteStride', struct.calcsize(kind)))[0]
|
|
56
|
+
for i in range(index['count'])])
|
|
57
|
+
else:
|
|
58
|
+
indexes = np.arange(len(positions))
|
|
59
|
+
return positions, indexes.reshape(-1, 3), transform, offset, stride
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def physical(positions, transform, scale, translation):
|
|
63
|
+
world = positions @ transform[:3, :3].T + transform[:3, 3]
|
|
64
|
+
return world[:, [0, 2, 1]] * np.array([1, -1, 1]) * scale + translation
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def signed_volume(triangles):
|
|
68
|
+
return float(np.sum(np.einsum('ij,ij->i', triangles[:, 0], np.cross(triangles[:, 1], triangles[:, 2]))) / 6)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def derive(original_triangles, source, self_evidence, placement, policy, build_solid, inspect_self):
|
|
72
|
+
vertices, indexes = np.unique(original_triangles.reshape(-1, 3), axis=0, return_inverse=True)
|
|
73
|
+
faces = indexes.reshape(-1, 3)
|
|
74
|
+
rule = policy['repair']
|
|
75
|
+
report = {'status': 'not_needed', 'method': 'none', 'originalGlbSha256': sha(source), 'derivedGlbSha256': sha(source),
|
|
76
|
+
'topologySha256': sha(source), 'topologyBinding': 'identical_bytes', 'iterations': 0,
|
|
77
|
+
'originalInterferenceCount': self_evidence['count'], 'remainingInterferenceCount': 0,
|
|
78
|
+
'uniqueVertexCount': len(vertices), 'changedVertexCount': 0, 'changedVertexFraction': 0,
|
|
79
|
+
'maximumDisplacementMm': 0, 'rmsDisplacementMm': 0, 'signedVolumeRelativeChange': 0,
|
|
80
|
+
'boundsMaximumChangeMm': 0, 'movedVertexIds': []}
|
|
81
|
+
if self_evidence['count'] == 0:
|
|
82
|
+
return original_triangles, source, report
|
|
83
|
+
neighbors = [set() for _ in vertices]
|
|
84
|
+
for face in faces:
|
|
85
|
+
for i in face:
|
|
86
|
+
neighbors[i].update(set(map(int, face)) - {int(i)})
|
|
87
|
+
seeds = set()
|
|
88
|
+
for point in self_evidence['points']:
|
|
89
|
+
distances = np.linalg.norm(vertices-point, axis=1)
|
|
90
|
+
index = int(distances.argmin())
|
|
91
|
+
require(distances[index] <= policy['linearToleranceMm'], 'unmapped_interference')
|
|
92
|
+
seeds.add(index)
|
|
93
|
+
selected = seeds | set().union(*(neighbors[i] for i in seeds))
|
|
94
|
+
require(0 < len(selected) / len(vertices) <= rule['maximumVertexFraction'], 'vertex_budget')
|
|
95
|
+
moved = vertices.copy()
|
|
96
|
+
maximum = min(rule['maximumDisplacementMm'], placement['heightBeforeTrimMm'] * rule['maximumHeightFraction'])
|
|
97
|
+
for i in selected:
|
|
98
|
+
delta = rule['lambda'] * (np.mean(vertices[list(neighbors[i])], axis=0)-vertices[i])
|
|
99
|
+
length = float(np.linalg.norm(delta))
|
|
100
|
+
moved[i] += delta * min(1, maximum/max(length, 1e-20))
|
|
101
|
+
raw, raw_faces, transform, offset, stride = layout(source, policy['maximumTriangles']*3)
|
|
102
|
+
scale = placement['scaleMmPerSourceUnit']
|
|
103
|
+
translation = np.array(placement['translationMm'])
|
|
104
|
+
before = physical(raw, transform, scale, translation)
|
|
105
|
+
require(raw_faces.shape == faces.shape and np.max(np.abs(before[raw_faces]-original_triangles)) < 1e-7, 'source_geometry')
|
|
106
|
+
# Write only original POSITION records; all indices, nodes, materials and other bytes stay exact.
|
|
107
|
+
derived = bytearray(source)
|
|
108
|
+
inverse = np.linalg.inv(transform)
|
|
109
|
+
touched = []
|
|
110
|
+
for i, point in enumerate(before):
|
|
111
|
+
distances = np.linalg.norm(vertices-point, axis=1)
|
|
112
|
+
index = int(distances.argmin())
|
|
113
|
+
if distances[index] > 1e-7 or index not in selected:
|
|
114
|
+
continue
|
|
115
|
+
normal = (moved[index]-translation)/scale
|
|
116
|
+
world = normal[[0, 2, 1]] * np.array([1, 1, -1])
|
|
117
|
+
local = inverse[:3, :3] @ world + inverse[:3, 3]
|
|
118
|
+
struct.pack_into('<fff', derived, offset+i*stride, *map(float, local))
|
|
119
|
+
touched.append(i)
|
|
120
|
+
next_raw, next_faces, next_transform, _, _ = layout(derived, policy['maximumTriangles']*3)
|
|
121
|
+
require(np.array_equal(raw_faces, next_faces) and np.array_equal(transform, next_transform), 'topology_changed')
|
|
122
|
+
after = physical(next_raw, transform, scale, translation)
|
|
123
|
+
delta = np.linalg.norm(after-before, axis=1)
|
|
124
|
+
require(float(delta.max()) <= maximum, 'displacement_budget')
|
|
125
|
+
# Equal logical vertices must still weld exactly; no components/triangles or vertex link can disappear.
|
|
126
|
+
next_vertices = np.unique(after[raw_faces].reshape(-1, 3), axis=0)
|
|
127
|
+
require(len(next_vertices) == len(vertices), 'vertex_count_changed')
|
|
128
|
+
zero_before, zero_after = bytearray(source), bytearray(derived)
|
|
129
|
+
for i in range(len(raw)):
|
|
130
|
+
zero_before[offset+i*stride:offset+i*stride+12] = b'\0'*12
|
|
131
|
+
zero_after[offset+i*stride:offset+i*stride+12] = b'\0'*12
|
|
132
|
+
require(zero_before == zero_after, 'non_position_bytes_changed')
|
|
133
|
+
triangles = after[raw_faces]
|
|
134
|
+
solid = build_solid(triangles)
|
|
135
|
+
remaining = inspect_self(solid)
|
|
136
|
+
require(remaining['count'] == 0, 'self_intersection_remaining')
|
|
137
|
+
old_volume, new_volume = signed_volume(original_triangles), signed_volume(triangles)
|
|
138
|
+
relative = abs(new_volume-old_volume)/abs(old_volume)
|
|
139
|
+
require(relative <= rule['maximumSignedVolumeRelativeChange'], 'volume_budget')
|
|
140
|
+
changed = sorted(i for i in selected if np.linalg.norm(moved[i]-vertices[i]) > 1e-12)
|
|
141
|
+
bounds = max(np.max(np.abs(before.min(axis=0)-after.min(axis=0))), np.max(np.abs(before.max(axis=0)-after.max(axis=0))))
|
|
142
|
+
report.update({'status': 'applied', 'method': 'bounded_local_laplacian', 'derivedGlbSha256': sha(derived),
|
|
143
|
+
'topologySha256': sha(zero_before), 'topologyBinding': 'all_non_position_bytes', 'iterations': 1,
|
|
144
|
+
'changedVertexCount': len(changed), 'changedVertexFraction': len(changed)/len(vertices),
|
|
145
|
+
'maximumDisplacementMm': float(delta.max()), 'rmsDisplacementMm': float(np.sqrt(np.mean(delta**2))),
|
|
146
|
+
'signedVolumeRelativeChange': relative, 'boundsMaximumChangeMm': float(bounds), 'movedVertexIds': changed})
|
|
147
|
+
return triangles, bytes(derived), report
|