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,451 @@
1
+ /** Persistent queue; preflight/symbol failures allocate no job; views require artifact/hash/node-map validation. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { containerName, removeContainer } from './sandbox.mjs';
6
+ import {manifestCanonical} from './manifest-digest.mjs';
7
+ import {ENGINEERING_ARTIFACT_TYPES,normalizeEngineeringFitRequest,validateAttachmentBaseline,validateEngineeringArtifacts} from './engineering-fit.mjs';
8
+ import {glbJson,validateAssemblyEvidence} from './assembly-validation.mjs';
9
+ import {validateInspectionGeometryPlan,validateInspectionGeometryTransport,validateInspectionGeometryEvidence} from './inspection-geometry.mjs';
10
+ import {validateRepresentation} from './representation.mjs';
11
+ const hash = value => createHash('sha256').update(value).digest('hex');
12
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
13
+ const HEX = /^[a-f0-9]{64}$/;
14
+ const STABLE = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
15
+ const TARGET_ID = /^[A-Za-z0-9][A-Za-z0-9:_.-]{0,127}$/;
16
+ const NODE_ID = /^[a-z0-9][a-z0-9_-]{0,127}$/;
17
+ const VIEWS = ['normal', 'internal', 'exploded'];
18
+ const DEFERRED = ['voltage', 'peak_current', 'port_occupancy', 'logic_level', 'concurrent_resources', 'actuator_load'];
19
+ const REVISION_CHANGES=['geometry','transform','interfaces','appearance_mapping'];
20
+ const LIBRARY_DIAGNOSTIC_CODES=new Set(['library_unknown','library_argument_unknown','library_argument_positional','build123d_unknown']);
21
+ /** One printable abelien_cad signature, e.g. extrude_profile(profile, *, height); generated from the trusted library AST. */
22
+ const LIBRARY_SIGNATURE=/^[a-z_][a-z0-9_]{0,63}\([\x20-\x7e]{0,230}\)$/;
23
+ const libraryDiagnostic=value=>{
24
+ if(!LIBRARY_DIAGNOSTIC_CODES.has(value?.code)||!Array.isArray(value.names)||!value.names.length||value.names.length>5)return undefined;
25
+ const pattern=value.code==='library_argument_unknown'?/^[A-Za-z_][A-Za-z0-9_]{0,63}\.[A-Za-z_][A-Za-z0-9_]{0,63}$/:/^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
26
+ if(!value.names.every(name=>typeof name==='string'&&pattern.test(name)))return undefined;
27
+ if(!value.code.startsWith('library_argument_'))return value.signatures===undefined?{code:value.code,names:value.names}:undefined;
28
+ // Argument errors must name the exact signature of every affected function, in name order.
29
+ const functions=[...new Set(value.names.map(name=>name.split('.')[0]))].sort();
30
+ return Array.isArray(value.signatures)&&value.signatures.length===functions.length
31
+ &&value.signatures.every((signature,index)=>typeof signature==='string'&&LIBRARY_SIGNATURE.test(signature)&&signature.startsWith(`${functions[index]}(`))
32
+ ?{code:value.code,names:value.names,signatures:value.signatures}:undefined;
33
+ };
34
+ export const ARTIFACT_TYPES = { 'model.step': 'model/step', 'model.glb': 'model/gltf-binary',
35
+ 'front.png': 'image/png', 'top.png': 'image/png', 'iso.png': 'image/png',
36
+ 'normal.png': 'image/png', 'internal.png': 'image/png', 'exploded.png': 'image/png',
37
+ 'geometry.json': 'application/json', 'assembly.json': 'application/json', 'model.py': 'text/plain' };
38
+ const error = code => Object.assign(new Error(code), { code });
39
+ const canonical = value => Array.isArray(value) ? `[${value.map(canonical).join(',')}]`
40
+ : value && typeof value === 'object' ? `{${Object.entries(value).sort(([a],[b]) => a.localeCompare(b)).map(([key,item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(',')}}`
41
+ : JSON.stringify(value);
42
+ const finiteVector = value => Array.isArray(value) && value.length === 3 && value.every(item => typeof item === 'number' && Number.isFinite(item));
43
+ /** Repair must change an authorized physical target, not only a source or display/report version. */
44
+ export function revisionHasPhysicalChange(previous,current,summary){
45
+ const targets=new Set(summary.targetOccurrenceIds);
46
+ const identities={geometry:item=>item.geometrySha256,transform:item=>manifestCanonical(item.views.normal),appearance_mapping:item=>manifestCanonical(item.material??null)};
47
+ return summary.replacementMappings.some(mapping=>mapping.previousOccurrenceIds.some(id=>targets.has(id))&&
48
+ Object.entries(identities).some(([change,identity])=>summary.allowedChanges.includes(change)&&
49
+ canonical(mapping.previousOccurrenceIds.map(id=>identity(previous.get(id))).sort())!==canonical(mapping.nextOccurrenceIds.map(id=>identity(current.get(id))).sort())));
50
+ }
51
+ const nodeList=(value,{empty=false}={})=>Array.isArray(value)&&(empty||value.length>0)&&value.length<=128&&new Set(value).size===value.length&&value.every(item=>typeof item==='string'&&NODE_ID.test(item));
52
+ const targetList=(value,{empty=false,maximum=512}={})=>Array.isArray(value)&&(empty||value.length>0)&&value.length<=maximum&&new Set(value).size===value.length&&value.every(item=>typeof item==='string'&&TARGET_ID.test(item));
53
+ function normalizeRevisionInput(value){
54
+ if(value===undefined||value===null)return null;
55
+ const keys=['priorJobId','priorSubject','targetOccurrenceIds','allowedChanges','preserveOccurrenceIds','preserveAppearanceTargetIds','appearanceTargetBindings','replacementMappings','mountAlignments'];
56
+ if(!exactKeys(value,keys)||!UUID.test(value.priorJobId??'')||!exactKeys(value.priorSubject,['sourceSha256','assemblySha256','occurrenceSetSha256'])||
57
+ Object.values(value.priorSubject).some(item=>typeof item!=='string'||!HEX.test(item))||!nodeList(value.targetOccurrenceIds)||!nodeList(value.preserveOccurrenceIds,{empty:true})||
58
+ value.targetOccurrenceIds.some(id=>value.preserveOccurrenceIds.includes(id))||
59
+ !Array.isArray(value.allowedChanges)||!value.allowedChanges.length||value.allowedChanges.length>REVISION_CHANGES.length||new Set(value.allowedChanges).size!==value.allowedChanges.length||value.allowedChanges.some(item=>!REVISION_CHANGES.includes(item))||
60
+ !targetList(value.preserveAppearanceTargetIds,{empty:true})||!Array.isArray(value.appearanceTargetBindings)||value.appearanceTargetBindings.length!==value.preserveAppearanceTargetIds.length||!Array.isArray(value.replacementMappings)||!value.replacementMappings.length||value.replacementMappings.length>128||
61
+ !Array.isArray(value.mountAlignments)||value.mountAlignments.length>128)throw error('invalid_input');
62
+ const appearanceTargetBindings=value.appearanceTargetBindings.map((item,index)=>{
63
+ if(!exactKeys(item,['targetId','occurrenceId'])||item.targetId!==value.preserveAppearanceTargetIds[index]||!TARGET_ID.test(item.targetId??'')||!NODE_ID.test(item.occurrenceId??''))throw error('invalid_input');
64
+ return {targetId:item.targetId,occurrenceId:item.occurrenceId};
65
+ });
66
+ if(new Set(appearanceTargetBindings.map(item=>item.targetId)).size!==appearanceTargetBindings.length)throw error('invalid_input');
67
+ const replacementMappings=value.replacementMappings.map(item=>{
68
+ if(!exactKeys(item,['kind','previousOccurrenceIds','nextOccurrenceIds'])||!['preserved','replaced','split','merged'].includes(item.kind)||!nodeList(item.previousOccurrenceIds)||!nodeList(item.nextOccurrenceIds))throw error('invalid_input');
69
+ if(['preserved','replaced'].includes(item.kind)&&(item.previousOccurrenceIds.length!==1||item.nextOccurrenceIds.length!==1)||item.kind==='split'&&(item.previousOccurrenceIds.length!==1||item.nextOccurrenceIds.length<2)||item.kind==='merged'&&(item.previousOccurrenceIds.length<2||item.nextOccurrenceIds.length!==1))throw error('invalid_input');
70
+ return {kind:item.kind,previousOccurrenceIds:[...item.previousOccurrenceIds],nextOccurrenceIds:[...item.nextOccurrenceIds]};
71
+ });
72
+ const mountAlignments=value.mountAlignments.map(item=>{
73
+ const alignmentKeys=['previousOccurrenceId','previousFrameId','nextOccurrenceId','nextFrameId','compatibilityKey'];
74
+ if(!exactKeys(item,alignmentKeys)||!NODE_ID.test(item.previousOccurrenceId??'')||!NODE_ID.test(item.previousFrameId??'')||!NODE_ID.test(item.nextOccurrenceId??'')||!NODE_ID.test(item.nextFrameId??'')||!STABLE.test(item.compatibilityKey??''))throw error('invalid_input');
75
+ return Object.fromEntries(alignmentKeys.map(key=>[key,item[key]]));
76
+ });
77
+ if(new Set(mountAlignments.map(item=>item.previousOccurrenceId)).size!==mountAlignments.length||new Set(mountAlignments.map(item=>item.nextOccurrenceId)).size!==mountAlignments.length)throw error('invalid_input');
78
+ const unsigned={schema:'abelien.structure-revision.v1',prior:{jobId:value.priorJobId,...value.priorSubject},targetOccurrenceIds:[...value.targetOccurrenceIds],allowedChanges:[...value.allowedChanges],
79
+ preserveOccurrenceIds:[...value.preserveOccurrenceIds],preserveAppearanceTargetIds:[...value.preserveAppearanceTargetIds],appearanceTargetBindings,replacementMappings,mountAlignments};
80
+ return {...unsigned,requestSha256:hash(manifestCanonical(unsigned))};
81
+ }
82
+ function revisionErrors(previous,next,previousFrameId,nextFrameId){
83
+ const matrix=occurrence=>{
84
+ const frame=occurrence.mountFrames.find(item=>item.frameId===(occurrence===previous?previousFrameId:nextFrameId));if(!frame)throw error('invalid_artifact');
85
+ const rotation=degrees=>{const [x,y,z]=degrees.map(value=>value*Math.PI/180),cx=Math.cos(x),sx=Math.sin(x),cy=Math.cos(y),sy=Math.sin(y),cz=Math.cos(z),sz=Math.sin(z);return [[cy*cz,-cy*sz,sy],[sx*sy*cz+cx*sz,-sx*sy*sz+cx*cz,-sx*cy],[-cx*sy*cz+sx*sz,cx*sy*sz+sx*cz,cx*cy]];};
86
+ const multiply=(left,right)=>left.map((row,i)=>right[0].map((_,j)=>row.reduce((sum,item,k)=>sum+item*right[k][j],0)));
87
+ const normal=occurrence.views.normal,rn=rotation(normal.rotationDeg),rf=rotation(frame.rotationDeg),position=normal.translationMm.map((value,index)=>value+rn[index].reduce((sum,item,axis)=>sum+item*frame.translationMm[axis],0));
88
+ return {rotation:multiply(rn,rf),position};
89
+ };
90
+ const before=matrix(previous),after=matrix(next),translationErrorMm=Math.sqrt(before.position.reduce((sum,item,index)=>sum+(item-after.position[index])**2,0));
91
+ let trace=0;for(let axis=0;axis<3;axis++)for(let row=0;row<3;row++)trace+=before.rotation[row][axis]*after.rotation[row][axis];
92
+ const rotationErrorDeg=Math.acos(Math.max(-1,Math.min(1,(trace-1)/2)))*180/Math.PI;
93
+ return {translationErrorMm,rotationErrorDeg};
94
+ }
95
+ function validateRevisionManifest(value,sourceSha256,expected){
96
+ if(!expected){if(value.revision!==null||value.occurrences.some(item=>item.lineage!==null||item.source.sha256!==sourceSha256))throw error('invalid_artifact');return;}
97
+ const {summary,priorAssembly}=expected;if(canonical(value.revision)!==canonical(summary))throw error('invalid_artifact');
98
+ const previous=new Map(priorAssembly.occurrences.map(item=>[item.occurrenceId,item])),current=new Map(value.occurrences.map(item=>[item.occurrenceId,item])),alignedTargets=new Set(summary.mountAlignments.map(item=>item.nextOccurrenceId));
99
+ const priorSeen=new Set(),nextSeen=new Set(),changes={geometry:'geometrySha256',transform:'transformSha256',interfaces:'interfaceSha256',appearance_mapping:'appearanceSha256'};
100
+ for(const mapping of summary.replacementMappings){
101
+ for(const id of mapping.previousOccurrenceIds){if(!previous.has(id)||priorSeen.has(id))throw error('invalid_artifact');priorSeen.add(id);}
102
+ for(const id of mapping.nextOccurrenceIds){if(!current.has(id)||nextSeen.has(id))throw error('invalid_artifact');nextSeen.add(id);const lineage=current.get(id).lineage;if(canonical(lineage)!==canonical({kind:mapping.kind,priorAssemblySha256:priorAssembly.assemblySha256,previousOccurrenceIds:mapping.previousOccurrenceIds}))throw error('invalid_artifact');}
103
+ for(const [change,field] of Object.entries(changes))if(canonical(mapping.previousOccurrenceIds.map(id=>previous.get(id)[field]).sort())!==canonical(mapping.nextOccurrenceIds.map(id=>current.get(id)[field]).sort())&&!(change==='transform'&&mapping.nextOccurrenceIds.every(id=>alignedTargets.has(id)))&&!summary.allowedChanges.includes(change))throw error('invalid_artifact');
104
+ }
105
+ if(priorSeen.size!==previous.size||nextSeen.size!==current.size)throw error('invalid_artifact');
106
+ for(const id of summary.preserveOccurrenceIds){const before=previous.get(id),after=current.get(id),mapping=summary.replacementMappings.find(item=>canonical(item.previousOccurrenceIds)===canonical([id]));if(!before||!after||mapping?.kind!=='preserved'||canonical(mapping.nextOccurrenceIds)!==canonical([id])||['geometrySha256','transformSha256','interfaceSha256','appearanceSha256'].some(field=>before[field]!==after[field])||after.source.sha256!==before.source.sha256)throw error('invalid_artifact');}
107
+ for(const binding of summary.appearanceTargetBindings){const before=previous.get(binding.occurrenceId),mapping=summary.replacementMappings.find(item=>item.previousOccurrenceIds.includes(binding.occurrenceId));if(!before||!mapping||mapping.previousOccurrenceIds.length!==1||mapping.nextOccurrenceIds.length!==1||current.get(mapping.nextOccurrenceIds[0])?.appearanceSha256!==before.appearanceSha256)throw error('invalid_artifact');}
108
+ for(const alignment of summary.mountAlignments){const before=previous.get(alignment.previousOccurrenceId),after=current.get(alignment.nextOccurrenceId);if(!before||!after||!before.interfaces.some(item=>item.frameId===alignment.previousFrameId&&item.compatibilityKey===alignment.compatibilityKey)||!after.interfaces.some(item=>item.frameId===alignment.nextFrameId&&item.compatibilityKey===alignment.compatibilityKey))throw error('invalid_artifact');const errors=revisionErrors(before,after,alignment.previousFrameId,alignment.nextFrameId);if(errors.translationErrorMm>1e-6||errors.rotationErrorDeg>1e-6)throw error('invalid_artifact');}
109
+ }
110
+ function validateAssembly(value, sourceSha256, step, glb, files, expectedRevision) {
111
+ return validateAssemblyEvidence(value,sourceSha256,step,glb,files,()=>validateRevisionManifest(value,sourceSha256,expectedRevision));
112
+ }
113
+ const GEOMETRY_CHECKS=['collision','containment','opening_alignment','motion_envelope'];
114
+ const exactKeys=(value,keys)=>value&&typeof value==='object'&&!Array.isArray(value)&&canonical(Object.keys(value).sort())===canonical([...keys].sort());
115
+ const threshold=value=>typeof value==='number'&&Number.isFinite(value)&&value>=0;
116
+ function expectedInspection(plan,assembly){
117
+ const ids=new Set(assembly.occurrences.map(item=>item.occurrenceId));
118
+ const extended=plan?.schema==='abelien.structure-inspection-plan.v2';
119
+ if(!exactKeys(plan,['schema','collision','containment','openings','motionEnvelopes',...(extended?['inspectionGeometry']:[])])||(!extended&&plan.schema!=='abelien.structure-inspection-plan.v1'))throw error('invalid_artifact');
120
+ const regions=validateInspectionGeometryPlan(plan.inspectionGeometry,assembly);
121
+ if(extended&&!regions.size)throw error('invalid_artifact');
122
+ const listed=(value,minimum=1)=>Array.isArray(value)&&value.length>=minimum&&value.length<=128&&new Set(value).size===value.length&&value.every(id=>ids.has(id));
123
+ const omitted=value=>exactKeys(value,['kind','reason'])&&value.kind==='not_applicable'&&typeof value.reason==='string'&&value.reason.length>0&&value.reason.length<=300;
124
+ const result={};
125
+ const prepare=(name,spec,signatures)=>{
126
+ if(spec===null){result[name]={kind:'unknown',reason:'inspection_not_declared',signatures:[]};return;}
127
+ if(omitted(spec)){result[name]={kind:'not_applicable',reason:spec.reason,signatures:[]};return;}
128
+ result[name]={kind:'rules',reason:null,signatures:signatures(spec)};
129
+ if(result[name].signatures.length>256)throw error('invalid_artifact');
130
+ };
131
+ prepare('collision',plan.collision,spec=>{
132
+ if(!exactKeys(spec,['kind','occurrenceIds','allowedPairs','maximumOverlapMm3'])||spec.kind!=='rules'||!listed(spec.occurrenceIds,2)||!threshold(spec.maximumOverlapMm3)||!Array.isArray(spec.allowedPairs))throw error('invalid_artifact');
133
+ const allowed=new Set();
134
+ for(const pair of spec.allowedPairs){if(!Array.isArray(pair)||pair.length!==2||pair[0]>=pair[1]||!spec.occurrenceIds.includes(pair[0])||!spec.occurrenceIds.includes(pair[1]))throw error('invalid_artifact');const key=pair.join('\0');if(allowed.has(key))throw error('invalid_artifact');allowed.add(key);}
135
+ const signatures=[];for(let left=0;left<spec.occurrenceIds.length;left++)for(let right=left+1;right<spec.occurrenceIds.length;right++){
136
+ const pair=[spec.occurrenceIds[left],spec.occurrenceIds[right]].sort();if(!allowed.has(pair.join('\0')))signatures.push({subjectIds:pair,metric:'overlap_volume_mm3',operator:'<=',threshold:spec.maximumOverlapMm3});
137
+ }return signatures;
138
+ });
139
+ prepare('containment',plan.containment,spec=>{
140
+ if(!exactKeys(spec,['kind','rules'])||spec.kind!=='rules'||!Array.isArray(spec.rules)||!spec.rules.length||spec.rules.length>128)throw error('invalid_artifact');
141
+ return spec.rules.flatMap(rule=>{if(!exactKeys(rule,['container','targets','maximumOutsideMm3'])||(!ids.has(rule.container)&&regions.get(rule.container)?.kind!=='cavity')||!listed(rule.targets)||rule.targets.includes(rule.container)||!threshold(rule.maximumOutsideMm3))throw error('invalid_artifact');return rule.targets.map(target=>({subjectIds:[rule.container,target],metric:'outside_volume_mm3',operator:'<=',threshold:rule.maximumOutsideMm3}));});
142
+ });
143
+ prepare('opening_alignment',plan.openings,spec=>{
144
+ if(!exactKeys(spec,['kind','rules'])||spec.kind!=='rules'||!Array.isArray(spec.rules)||!spec.rules.length||spec.rules.length>128)throw error('invalid_artifact');
145
+ return spec.rules.flatMap(rule=>{if(!exactKeys(rule,['opening','target','maximumDistanceMm','minimumOverlapMm3'])||(!ids.has(rule.opening)&&regions.get(rule.opening)?.kind!=='opening')||!ids.has(rule.target)||rule.opening===rule.target||!threshold(rule.maximumDistanceMm)||!threshold(rule.minimumOverlapMm3)||(regions.has(rule.opening)&&rule.minimumOverlapMm3<=0))throw error('invalid_artifact');return [
146
+ {subjectIds:[rule.opening,rule.target],metric:'distance_mm',operator:'<=',threshold:rule.maximumDistanceMm},
147
+ {subjectIds:[rule.opening,rule.target],metric:'overlap_volume_mm3',operator:'>=',threshold:rule.minimumOverlapMm3},
148
+ ];});
149
+ });
150
+ prepare('motion_envelope',plan.motionEnvelopes,spec=>{
151
+ if(!exactKeys(spec,['kind','rules'])||spec.kind!=='rules'||!Array.isArray(spec.rules)||!spec.rules.length||spec.rules.length>128)throw error('invalid_artifact');
152
+ return spec.rules.flatMap(rule=>{if(!exactKeys(rule,['envelope','obstacles','maximumOverlapMm3'])||!ids.has(rule.envelope)||!listed(rule.obstacles)||rule.obstacles.includes(rule.envelope)||!threshold(rule.maximumOverlapMm3))throw error('invalid_artifact');return rule.obstacles.map(obstacle=>({subjectIds:[rule.envelope,obstacle],metric:'overlap_volume_mm3',operator:'<=',threshold:rule.maximumOverlapMm3}));});
153
+ });
154
+ return result;
155
+ }
156
+ function validateRevisionChecks(value,assembly,expected){
157
+ if(!exactKeys(value,['delta','preservation','mount']))throw error('invalid_artifact');
158
+ if(!expected){for(const name of ['delta','preservation','mount'])if(canonical(value[name])!==canonical({status:'not_applicable',method:'declared_not_applicable',reason:'initial_build',findings:[]}))throw error('invalid_artifact');return;}
159
+ const previous=new Map(expected.priorAssembly.occurrences.map(item=>[item.occurrenceId,item])),current=new Map(assembly.occurrences.map(item=>[item.occurrenceId,item]));
160
+ if(!revisionHasPhysicalChange(previous,current,expected.summary))throw error('invalid_artifact');
161
+ const fields={geometry:'geometrySha256',transform:'transformSha256',interfaces:'interfaceSha256',appearance_mapping:'appearanceSha256'},alignedTargets=new Set(expected.summary.mountAlignments.map(item=>item.nextOccurrenceId));
162
+ const delta=expected.summary.replacementMappings.map(mapping=>({kind:mapping.kind,previousOccurrenceIds:mapping.previousOccurrenceIds,nextOccurrenceIds:mapping.nextOccurrenceIds,changes:Object.entries(fields).filter(([name,field])=>canonical(mapping.previousOccurrenceIds.map(id=>previous.get(id)[field]).sort())!==canonical(mapping.nextOccurrenceIds.map(id=>current.get(id)[field]).sort())&&!(name==='transform'&&mapping.nextOccurrenceIds.every(id=>alignedTargets.has(id)))).map(([name])=>name)}));
163
+ const preservation=expected.summary.preserveOccurrenceIds.map(id=>({previousOccurrenceId:id,nextOccurrenceId:id,geometryPreserved:true,transformPreserved:true,interfacesPreserved:true,appearancePreserved:true}));
164
+ const mount=expected.summary.mountAlignments.map(alignment=>{const before=previous.get(alignment.previousOccurrenceId),after=current.get(alignment.nextOccurrenceId),errors=revisionErrors(before,after,alignment.previousFrameId,alignment.nextFrameId);return {...alignment,...errors,compatible:true,aligned:errors.translationErrorMm<=1e-6&&errors.rotationErrorDeg<=1e-6};});
165
+ const expectedChecks={delta:{status:'pass',method:'manifest_digest',reason:null,findings:delta},preservation:{status:'pass',method:'manifest_digest',reason:null,findings:preservation},mount:{status:'pass',method:'frame_alignment',reason:null,findings:mount}};
166
+ const near=(left,right)=>typeof left==='number'&&typeof right==='number'?Math.abs(left-right)<=1e-8:canonical(left)===canonical(right);
167
+ for(const name of ['delta','preservation'])if(canonical(value[name])!==canonical(expectedChecks[name]))throw error('invalid_artifact');
168
+ const actualMount=value.mount,expectedMount=expectedChecks.mount;
169
+ if(!exactKeys(actualMount,['status','method','reason','findings'])||actualMount.status!=='pass'||actualMount.method!=='frame_alignment'||actualMount.reason!==null||!Array.isArray(actualMount.findings)||actualMount.findings.length!==expectedMount.findings.length)throw error('invalid_artifact');
170
+ actualMount.findings.forEach((finding,index)=>{const expectedFinding=expectedMount.findings[index];if(!exactKeys(finding,Object.keys(expectedFinding))||Object.keys(expectedFinding).some(key=>!near(finding[key],expectedFinding[key])))throw error('invalid_artifact');});
171
+ }
172
+ function validateInspection(value, assembly, plan, expectedRevision, representation) {
173
+ if(!value||typeof value!=='object'||value.schema!=='abelien.structure-inspection.v2'||value.units!=='mm'||typeof value.build123d!=='string'||!value.build123d||value.visual_review!=='review_required'||
174
+ !value.assembly||value.assembly.mode!==assembly.mode||value.assembly.sha256!==assembly.assemblySha256||value.assembly.occurrence_count!==assembly.occurrenceCount||
175
+ value.assembly.node_mapping!==(assembly.mode==='occurrence_assembly')||value.assembly.inspection_plan_sha256!==assembly.inspectionPlanSha256||!value.checks||
176
+ value.checks.step_readback!==true||value.checks.solid_validity!==true||!Number.isSafeInteger(value.checks.solid_count)||value.checks.solid_count<1||
177
+ !finiteVector(value.checks.dimensions_mm)||value.checks.dimensions_mm.some(item=>item<=0)||typeof value.checks.volume_mm3!=='number'||!Number.isFinite(value.checks.volume_mm3)||value.checks.volume_mm3<=0||
178
+ !value.checks.geometry||!value.checks.revision||!Array.isArray(value.not_checked)||value.not_checked.length>32||new Set(value.not_checked).size!==value.not_checked.length||
179
+ value.not_checked.some(item=>typeof item!=='string'||!/^[a-z][a-z0-9_]{0,63}$/.test(item))||DEFERRED.some(item=>!value.not_checked.includes(item)))throw error('invalid_artifact');
180
+ const ids=new Set(assembly.occurrences.map(occurrence=>occurrence.occurrenceId));
181
+ if(hash(manifestCanonical(plan))!==assembly.inspectionPlanSha256)throw error('invalid_artifact');
182
+ const expected=expectedInspection(plan,assembly);
183
+ const regions=validateInspectionGeometryPlan(plan.inspectionGeometry,assembly);
184
+ validateInspectionGeometryEvidence(value.inspectionGeometry,plan.inspectionGeometry,assembly);
185
+ for(const name of GEOMETRY_CHECKS){
186
+ const check=value.checks.geometry[name],planned=expected[name];
187
+ if(!check||!['pass','conflict','unknown','not_applicable'].includes(check.status)||!['occt_brep','unavailable','declared_not_applicable'].includes(check.method)||check.planSha256!==assembly.inspectionPlanSha256||
188
+ !(check.reason===null||typeof check.reason==='string'&&check.reason.length>0&&check.reason.length<=300)||!Array.isArray(check.findings)||check.findings.length>256)throw error('invalid_artifact');
189
+ for(const finding of check.findings){
190
+ if(!finding||!Array.isArray(finding.subjectIds)||finding.subjectIds.length!==2||finding.subjectIds[0]===finding.subjectIds[1]||!ids.has(finding.subjectIds[1])||
191
+ (!ids.has(finding.subjectIds[0])&&!(name==='containment'&&regions.get(finding.subjectIds[0])?.kind==='cavity')&&!(name==='opening_alignment'&&regions.get(finding.subjectIds[0])?.kind==='opening'))||
192
+ !['overlap_volume_mm3','outside_volume_mm3','distance_mm'].includes(finding.metric)||typeof finding.measured!=='number'||!Number.isFinite(finding.measured)||finding.measured<0||
193
+ !['<=','>='].includes(finding.operator)||typeof finding.threshold!=='number'||!Number.isFinite(finding.threshold)||finding.threshold<0||typeof finding.passed!=='boolean'||
194
+ finding.passed!==(finding.operator==='<='?finding.measured<=finding.threshold:finding.measured>=finding.threshold))throw error('invalid_artifact');
195
+ }
196
+ if(check.status==='unknown'&&(check.method!=='unavailable'||!check.reason||check.findings.length||!value.not_checked.includes(name)))throw error('invalid_artifact');
197
+ if(check.status==='not_applicable'&&(check.method!=='declared_not_applicable'||!check.reason||check.findings.length||value.not_checked.includes(name)))throw error('invalid_artifact');
198
+ if(['pass','conflict'].includes(check.status)&&(check.method!=='occt_brep'||check.reason!==null||value.not_checked.includes(name)||check.status!==(check.findings.every(item=>item.passed)?'pass':'conflict')))throw error('invalid_artifact');
199
+ if(planned.kind==='unknown'&&(check.status!=='unknown'||check.reason!==planned.reason)||planned.kind==='not_applicable'&&(check.status!=='not_applicable'||check.reason!==planned.reason)||
200
+ planned.kind==='rules'&&check.status==='not_applicable'||planned.kind==='rules'&&check.status==='unknown'&&check.reason!=='brep_operation_failed'||
201
+ planned.kind==='rules'&&['pass','conflict'].includes(check.status)&&canonical(check.findings.map(({subjectIds,metric,operator,threshold})=>({subjectIds,metric,operator,threshold})))!==canonical(planned.signatures))throw error('invalid_artifact');
202
+ }
203
+ validateRevisionChecks(value.checks.revision,assembly,expectedRevision);
204
+ const reported=validateRepresentation(value.representation,assembly,value.not_checked);
205
+ if(canonical(reported)!==canonical(representation))throw error('invalid_artifact');
206
+ return value;
207
+ }
208
+ function requireScope(scope) { if (typeof scope !== 'string' || !HEX.test(scope)) throw error('invalid_input'); }
209
+ function atomic(file, body) {
210
+ const temp = `${file}.${randomUUID()}.tmp`;
211
+ fs.writeFileSync(temp, JSON.stringify(body), { mode: 0o600 });
212
+ fs.renameSync(temp, file);
213
+ }
214
+ const DIAGNOSTIC_STAGES = new Set(['source_exec', 'assembly_contract', 'step_export', 'geometry_validation']);
215
+ /** Re-whitelist the sandbox's failure reason; lines count from the submitted source (or revision patch). */
216
+ function failureDiagnostic(value, offset = 0) {
217
+ if (!value || typeof value !== 'object' || !DIAGNOSTIC_STAGES.has(value.stage)) return null;
218
+ const exceptionType = typeof value.exceptionType === 'string' && /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(value.exceptionType) ? value.exceptionType : 'Error';
219
+ const message = typeof value.message === 'string' ? value.message.replace(/\p{C}/gu, ' ').slice(0, 200) : '';
220
+ const line = Number.isSafeInteger(value.line) && value.line > offset && value.line - offset <= 32768 ? value.line - offset : null;
221
+ return { stage: value.stage, exceptionType, message, line };
222
+ }
223
+ function publicJob(job) {
224
+ const { id, title, pass, basis, status, createdAt, updatedAt, error: failure, diagnostic, artifacts, assembly } = job;
225
+ return { id, title, pass, basis, status, createdAt, updatedAt, ...(failure ? { error: failure } : {}),
226
+ ...(diagnostic ? { diagnostic } : {}), artifacts,
227
+ ...(assembly ? { assembly } : {}),
228
+ ...(job.attachment?{attachment:job.attachment}:{}),
229
+ ...(job.operation?{operation:job.operation}:{}),
230
+ ...(job.engineeringFit?{engineeringFit:job.engineeringFit}:{}),
231
+ visualReview: 'review_required', adopted: false };
232
+ }
233
+ export function createJobQueue({ root, sandbox, sourceApi, timeoutMs = 180000, preflightTimeoutMs = 10000, maxJobs = 1000, cleanup = removeContainer, build123dNames = null }) {
234
+ if (!path.isAbsolute(root) || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || !Number.isSafeInteger(preflightTimeoutMs) || preflightTimeoutMs < 1 || preflightTimeoutMs > 10000) throw error('configuration_invalid');
235
+ if(build123dNames!==null&&(!Array.isArray(build123dNames)||!build123dNames.length||build123dNames.length>4096||build123dNames.some(name=>typeof name!=='string'||!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(name))))throw error('configuration_invalid');
236
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
237
+ const jobs = new Map(), keyIndex=new Map(), controllers = new Map();
238
+ let pumping = false, closed = false, preflightController;
239
+ const folder = id => path.join(root, id);
240
+ const save = job => { job.updatedAt = new Date().toISOString(); atomic(path.join(folder(job.id), 'job.json'), job); };
241
+ const get = (scope, id) => {
242
+ requireScope(scope);
243
+ if (typeof id !== 'string' || !UUID.test(id)) throw error('invalid_input');
244
+ const job = jobs.get(id);
245
+ if (!job || job.scope !== scope) throw error('not_found');
246
+ return job;
247
+ };
248
+ const upstreamIndex=(scope,key)=>{requireScope(scope);if(typeof key!=='string'||!HEX.test(key))throw error('invalid_input');return `${scope}:${key}`;};
249
+ const readJobArtifact=(job,name)=>{
250
+ const asset=job.artifacts.find(item=>item.name===name);if(job.status!=='succeeded'||!asset)throw error('invalid_input');
251
+ const file=path.join(folder(job.id),name),stat=fs.lstatSync(file);if(!stat.isFile()||stat.isSymbolicLink()||stat.size!==asset.bytes)throw error('artifact_changed');
252
+ const body=fs.readFileSync(file);if(hash(body)!==asset.sha256)throw error('artifact_changed');return body;
253
+ };
254
+ const revisionExpected=(scope,summary)=>{
255
+ if(!summary)return null;
256
+ const prior=get(scope,summary.prior.jobId);if(prior.status!=='succeeded'||prior.assembly?.schema!=='abelien.structure-assembly.v2'||prior.assembly.mode!=='occurrence_assembly')throw error('invalid_input');
257
+ const source=readJobArtifact(prior,'model.py'),assembly=JSON.parse(readJobArtifact(prior,'assembly.json').toString('utf8'));
258
+ if(hash(source)!==summary.prior.sourceSha256||assembly.sourceSha256!==summary.prior.sourceSha256||assembly.assemblySha256!==summary.prior.assemblySha256||assembly.occurrenceSetSha256!==summary.prior.occurrenceSetSha256||canonical(assembly)!==canonical(prior.assembly))throw error('conflict');
259
+ const ids=new Set(assembly.occurrences.map(item=>item.occurrenceId)),partition=[...summary.targetOccurrenceIds,...summary.preserveOccurrenceIds];
260
+ if(new Set(partition).size!==ids.size||partition.some(id=>!ids.has(id)))throw error('invalid_input');
261
+ return {summary,priorAssembly:assembly,priorSource:source.toString('utf8')};
262
+ };
263
+ const effectiveSource=(patch,expected)=>expected?`${expected.priorSource}\n# abelien-structure revision boundary\n${patch}`:patch;
264
+ const fitExpected=(scope,fit)=>{
265
+ const prior=get(scope,fit.priorJobId);
266
+ if(prior.status!=='succeeded'||prior.operation||!prior.attachment)throw error('invalid_input');
267
+ const assembly=JSON.parse(readJobArtifact(prior,'assembly.json')),step=readJobArtifact(prior,'model.step'),source=readJobArtifact(prior,'model.py');
268
+ if(hash(source)!==fit.priorSubject.sourceSha256||['sourceSha256','assemblySha256','occurrenceSetSha256'].some(k=>assembly[k]!==fit.priorSubject[k])||canonical(assembly)!==canonical(prior.assembly))throw error('conflict');
269
+ const attachment=validateAttachmentBaseline(JSON.parse(readJobArtifact(prior,'attachment.json')),assembly,step);
270
+ if(attachment.attachmentSha256!==fit.attachmentSha256||canonical(attachment)!==canonical(prior.attachment))throw error('conflict');
271
+ return {assembly,step,attachment,candidateGlbSha256:fit.candidateGlb.sha256};
272
+ };
273
+ async function runEngineering(job,signal){
274
+ const expected=fitExpected(job.scope,job.fit);
275
+ const {triangles}=normalizeEngineeringFitRequest(Object.fromEntries(['operation','key','basis','title','pass','fit'].map(k=>[k,job[k]])));
276
+ const payload={baselineStep:expected.step.toString('base64'),baselineAssembly:expected.assembly,attachment:expected.attachment,
277
+ candidateGlbSha256:expected.candidateGlbSha256,candidateGlb:job.fit.candidateGlb.base64,triangles};
278
+ const built=await sandbox(job.id,'build',{mode:'component_fit_build',...payload},signal);
279
+ if(built?.diagnostic)throw Object.assign(error('source_failed'),{diagnostic:failureDiagnostic(built.diagnostic)});
280
+ if(signal.aborted)throw error('interrupted');
281
+ if(typeof built.step!=='string'||!built.recipe)throw error('invalid_artifact');
282
+ job.status='validating';save(job);
283
+ const inspected=await sandbox(job.id,'inspect',{mode:'component_fit_inspect',...payload,step:built.step,recipe:built.recipe,decorationGlb:built.decorationGlb},signal);
284
+ if(inspected?.diagnostic)throw Object.assign(error('source_failed'),{diagnostic:failureDiagnostic(inspected.diagnostic)});
285
+ if(signal.aborted)throw error('interrupted');
286
+ const decoded=new Map(),outputs=[];
287
+ if(!inspected?.artifacts||Object.keys(inspected.artifacts).sort().join(',')!==Object.keys(ENGINEERING_ARTIFACT_TYPES).sort().join(','))throw error('invalid_artifact');
288
+ for(const [name,mediaType] of Object.entries(ENGINEERING_ARTIFACT_TYPES)){
289
+ const encoded=inspected.artifacts[name];if(typeof encoded!=='string')throw error('invalid_artifact');
290
+ const body=Buffer.from(encoded,'base64'),max=name.endsWith('.png')?4*1024*1024:name.endsWith('.json')?128*1024:24*1024*1024;
291
+ if(!body.length||body.length>max||body.toString('base64')!==encoded)throw error('invalid_artifact');
292
+ if(name.endsWith('.png')&&!body.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))throw error('invalid_artifact');
293
+ if(name.endsWith('.step')&&!body.subarray(0,128).toString().includes('ISO-10303-21'))throw error('invalid_artifact');
294
+ if(name.endsWith('.glb'))glbJson(body);
295
+ decoded.set(name,body);outputs.push({name,mediaType,bytes:body.length,sha256:hash(body)});
296
+ }
297
+ const verified=validateEngineeringArtifacts(decoded,expected);
298
+ for(const [name,body] of decoded)fs.writeFileSync(path.join(folder(job.id),name),body,{mode:0o600,flag:'wx'});
299
+ job.artifacts=outputs;job.assembly=verified.assembly;job.engineeringFit=verified.report;job.status='succeeded';save(job);
300
+ }
301
+ async function pump() {
302
+ if (pumping || closed) return;
303
+ pumping = true;
304
+ try {
305
+ for (const job of jobs.values()) {
306
+ if (closed || job.status !== 'queued') continue;
307
+ const controller = new AbortController(); controllers.set(job.id, controller);
308
+ const timer = setTimeout(() => { job.status = 'timed_out'; save(job); controller.abort(); }, timeoutMs);
309
+ try {
310
+ job.status = 'running'; save(job);
311
+ if(job.operation==='component_fit'){await runEngineering(job,controller.signal);continue;}
312
+ const expectedRevision=revisionExpected(job.scope,job.revision),source=effectiveSource(job.source,expectedRevision),sourceSha256=hash(source);
313
+ if(Buffer.byteLength(source)>128*1024)throw error('invalid_input');
314
+ const built = await sandbox(job.id, 'build', { mode: 'build', source, sourceSha256,...(expectedRevision?{revision:{...expectedRevision.summary,priorAssembly:expectedRevision.priorAssembly}}:{}) }, controller.signal);
315
+ if (controller.signal.aborted) throw error('interrupted');
316
+ // A revision build prefixes the prior source plus one boundary line; report lines of the patch itself.
317
+ const lineOffset = expectedRevision ? expectedRevision.priorSource.split('\n').length + 1 : 0;
318
+ if (built?.diagnostic) throw Object.assign(error('source_failed'), { diagnostic: failureDiagnostic(built.diagnostic, lineOffset) });
319
+ if (typeof built.step !== 'string' || built.step.length > 32 * 1024 * 1024 || !built.assembly || typeof built.assembly !== 'object' || !built.inspection || typeof built.inspection !== 'object' ||
320
+ Buffer.byteLength(JSON.stringify(built.inspection))>64*1024||built.assembly.inspectionPlanSha256!==hash(manifestCanonical(built.inspection))) throw error('invalid_artifact');
321
+ if(!Array.isArray(built.assembly.occurrences)||built.assembly.occurrences.some(item=>!item||!NODE_ID.test(item.occurrenceId??''))||
322
+ new Set(built.assembly.occurrences.map(item=>item.occurrenceId)).size!==built.assembly.occurrences.length)throw error('invalid_artifact');
323
+ expectedInspection(built.inspection,built.assembly);
324
+ validateRepresentation(built.representation,built.assembly);
325
+ validateInspectionGeometryTransport(built.inspection,built.assembly,built.inspectionGeometryBreps);
326
+ job.status = 'validating'; save(job);
327
+ const inspected = await sandbox(job.id, 'inspect', { mode: 'inspect', pass: job.pass, step: built.step, sourceSha256, assembly: built.assembly, inspection:built.inspection,revisionChecks:built.revisionChecks,
328
+ ...(built.representation?{representation:built.representation}:{}),
329
+ ...(built.inspectionGeometryBreps?{inspectionGeometryBreps:built.inspectionGeometryBreps}:{}),
330
+ ...(built.attachmentProposal?{attachmentProposal:built.attachmentProposal}:{}) }, controller.signal);
331
+ if (controller.signal.aborted) throw error('interrupted');
332
+ if (inspected?.diagnostic) throw Object.assign(error('source_failed'), { diagnostic: failureDiagnostic(inspected.diagnostic) });
333
+ const files = inspected.artifacts;
334
+ if (!files || typeof files !== 'object') throw error('invalid_artifact');
335
+ files['model.py'] = Buffer.from(source).toString('base64');
336
+ const outputs = [],decoded=new Map();
337
+ const types={...ARTIFACT_TYPES,...(built.attachmentProposal?{'attachment.json':'application/json'}:{})};
338
+ for (const [name, mediaType] of Object.entries(types)) {
339
+ const encoded = files[name];
340
+ if (typeof encoded !== 'string' || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded)) throw error('invalid_artifact');
341
+ const bytes = Buffer.from(encoded, 'base64');
342
+ const maximum = name.endsWith('.png') ? 4 * 1024 * 1024 : name.endsWith('.json') ? 128 * 1024 : 24 * 1024 * 1024;
343
+ if (!bytes.length || bytes.length > maximum) throw error('invalid_artifact');
344
+ if (name.endsWith('.png') && !bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10]))) throw error('invalid_artifact');
345
+ if (name.endsWith('.glb')) glbJson(bytes);
346
+ if (name.endsWith('.step') && !bytes.subarray(0,128).toString().includes('ISO-10303-21')) throw error('invalid_artifact');
347
+ outputs.push({ name, mediaType, bytes: bytes.length, sha256: hash(bytes) });
348
+ decoded.set(name,bytes);
349
+ }
350
+ const assembly=validateAssembly(JSON.parse(decoded.get('assembly.json').toString('utf8')),sourceSha256,decoded.get('model.step'),decoded.get('model.glb'),decoded,expectedRevision);
351
+ validateInspection(JSON.parse(decoded.get('geometry.json').toString('utf8')),assembly,built.inspection,expectedRevision,built.representation);
352
+ if(built.attachmentProposal)job.attachment=validateAttachmentBaseline(JSON.parse(decoded.get('attachment.json')),assembly,decoded.get('model.step'));
353
+ for(const [name,file] of decoded)fs.writeFileSync(path.join(folder(job.id), name), file, { mode: 0o600, flag: 'wx' });
354
+ job.artifacts = outputs; job.assembly=assembly; job.status = 'succeeded'; save(job);
355
+ } catch (failure) {
356
+ atomic(path.join(folder(job.id),'private-failure.json'),{schema:'abelien.structure-private-failure.v1',at:new Date().toISOString(),message:failure?.message,stack:failure?.stack,diagnostic:failure?.diagnostic});
357
+ if (!['cancelled', 'timed_out', 'interrupted'].includes(job.status)) {
358
+ job.status = 'failed'; job.error = 'build_or_validation_failed';
359
+ if (failure?.diagnostic) job.diagnostic = failure.diagnostic;
360
+ }
361
+ job.artifacts = []; delete job.assembly; delete job.attachment; delete job.engineeringFit; save(job);
362
+ } finally { clearTimeout(timer); controllers.delete(job.id); }
363
+ }
364
+ } finally { pumping = false; if (!closed && [...jobs.values()].some(job => job.status === 'queued')) queueMicrotask(pump); }
365
+ }
366
+ return {
367
+ async restore() {
368
+ for (const id of fs.readdirSync(root).filter(id => UUID.test(id))) {
369
+ const job = JSON.parse(fs.readFileSync(path.join(folder(id), 'job.json'), 'utf8'));
370
+ if (job.id !== id || !HEX.test(job.scope)||!HEX.test(job.key??'')) throw error('corrupt_store');
371
+ const index=upstreamIndex(job.scope,job.key);if(keyIndex.has(index))throw error('corrupt_store');
372
+ keyIndex.set(index,id);
373
+ jobs.set(id, job);
374
+ if (['running', 'validating'].includes(job.status)) {
375
+ for (const phase of ['build', 'inspect']) await cleanup(containerName(id, phase));
376
+ job.status = 'interrupted'; job.artifacts = []; delete job.assembly; delete job.attachment; delete job.engineeringFit; save(job);
377
+ }
378
+ }
379
+ queueMicrotask(pump);
380
+ },
381
+ async submit(scope, input) {
382
+ requireScope(scope);
383
+ if(input?.operation==='component_fit'){
384
+ input=normalizeEngineeringFitRequest(input).input;
385
+ const digest=hash(canonical(input)),index=upstreamIndex(scope,input.key),existingId=keyIndex.get(index),existing=existingId?jobs.get(existingId):null;
386
+ if(existing){if(existing.digest!==digest)throw error('conflict');return publicJob(existing);}
387
+ if(closed||jobs.size>=maxJobs||[...jobs.values()].filter(j=>['queued','running','validating'].includes(j.status)).length>=32)throw error('queue_full');
388
+ fitExpected(scope,input.fit);
389
+ const job={...input,id:randomUUID(),scope,digest,status:'queued',createdAt:new Date().toISOString(),artifacts:[]};
390
+ fs.mkdirSync(folder(job.id),{mode:0o700});save(job);jobs.set(job.id,job);keyIndex.set(index,job.id);queueMicrotask(pump);return publicJob(job);
391
+ }
392
+ if (!input || typeof input !== 'object' || Object.keys(input).some(key => !['key','basis','title','pass','source','revision'].includes(key)) ||
393
+ !HEX.test(input.key ?? '') || !HEX.test(input.basis ?? '') || !['preview','final'].includes(input.pass) ||
394
+ typeof input.title !== 'string' || !input.title.trim() || input.title.length > 100 ||
395
+ typeof input.source !== 'string' || !input.source.trim() || Buffer.byteLength(input.source) > 32768) throw error('invalid_input');
396
+ input = { ...input,revision:normalizeRevisionInput(input.revision) }; // Freeze and normalize the server-resolved revision reference.
397
+ const digest = hash(JSON.stringify({ basis: input.basis, pass: input.pass, source: input.source, title: input.title,...(input.revision?{revision:input.revision}:{}) }));
398
+ const index=upstreamIndex(scope,input.key),existingId=keyIndex.get(index),existing=existingId?jobs.get(existingId):undefined;
399
+ if (existing) { if (existing.digest !== digest) throw error('conflict'); return publicJob(existing); }
400
+ if (closed || jobs.size >= maxJobs || [...jobs.values()].filter(job => ['queued','running','validating'].includes(job.status)).length >= 32) throw error('queue_full');
401
+ const expectedRevision=revisionExpected(scope,input.revision),source=effectiveSource(input.source,expectedRevision);
402
+ if(Buffer.byteLength(source)>128*1024)throw error('invalid_input');
403
+ if (preflightController) throw error('queue_full');
404
+ const controller = new AbortController(); preflightController = controller;
405
+ const timer = setTimeout(() => controller.abort(), preflightTimeoutMs);
406
+ try {
407
+ const result = await sandbox(randomUUID(), 'preflight', { mode: 'preflight', source,...(sourceApi?{sourceApi}:{}),...(build123dNames?{build123dNames}:{}) }, controller.signal);
408
+ if (controller.signal.aborted || closed) throw error('source_preflight_unavailable');
409
+ if (result?.ok !== true) {
410
+ const d = result?.diagnostic;
411
+ const diagnostic = d?.code === 'source_symbol'&&d.stage==='source_precheck'&&Number.isSafeInteger(d.line)&&d.line>=1&&d.line<=32768&&
412
+ typeof d.symbol==='string'&&/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*){0,3}$/.test(d.symbol)&&d.symbol.length<=192
413
+ ?{code:d.code,stage:d.stage,line:d.line,symbol:d.symbol}
414
+ : d?.code === 'source_unbound' && Array.isArray(d.names) && d.names.length <= 5 && d.names.every(name => typeof name === 'string' && /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(name))
415
+ ? { code: d.code, names: d.names }
416
+ : libraryDiagnostic(d) ?? (d?.code === 'source_syntax' && Number.isSafeInteger(d.line) && d.line >= 1 && d.line <= 32768 ? { code: d.code, line: d.line }
417
+ : d?.code === 'source_unsupported' && ['desktop_import','manual_export','missing_output','assembly_shape','import_unavailable'].includes(d.feature)
418
+ && Number.isSafeInteger(d.line) && d.line >= 1 && d.line <= 32768 ? {code:d.code,feature:d.feature,line:d.line} : undefined);
419
+ if (!diagnostic) throw error('source_preflight_unavailable');
420
+ throw Object.assign(error('source_invalid'), { diagnostic });
421
+ }
422
+ } catch (failure) {
423
+ if (failure?.code === 'source_invalid') throw failure;
424
+ throw error('source_preflight_unavailable');
425
+ } finally { clearTimeout(timer); preflightController = undefined; }
426
+ if (closed || jobs.size >= maxJobs) throw error('queue_full');
427
+ const job = { ...input, id: randomUUID(), scope, digest, status: 'queued', createdAt: new Date().toISOString(), artifacts: [] };
428
+ fs.mkdirSync(folder(job.id), { mode: 0o700 }); save(job);jobs.set(job.id, job);keyIndex.set(index,job.id);queueMicrotask(pump);
429
+ return publicJob(job);
430
+ },
431
+ list(scope) { requireScope(scope); return [...jobs.values()].filter(job => job.scope === scope).reverse().slice(0,30).map(publicJob); },
432
+ get(scope, id) { return publicJob(get(scope,id)); },
433
+ byUpstreamKey(scope,key){const id=keyIndex.get(upstreamIndex(scope,key));if(!id)throw error('not_found');return publicJob(get(scope,id));},
434
+ cancel(scope, id) {
435
+ const job = get(scope,id);
436
+ if (['queued','running','validating'].includes(job.status)) { job.status = 'cancelled'; job.artifacts = []; delete job.assembly; delete job.attachment; delete job.engineeringFit; save(job); controllers.get(id)?.abort(); }
437
+ return publicJob(job);
438
+ },
439
+ artifact(scope,id,name) {
440
+ const job = get(scope,id);
441
+ if (job.status !== 'succeeded' || !Object.hasOwn({...ARTIFACT_TYPES,...ENGINEERING_ARTIFACT_TYPES,'attachment.json':'application/json'},name)) throw error('not_found');
442
+ const asset = job.artifacts.find(asset => asset.name === name);if(!asset)throw error('not_found');
443
+ return { ...asset, body: readJobArtifact(job,name) };
444
+ },
445
+ close() {
446
+ closed = true;
447
+ preflightController?.abort();
448
+ for (const [id,controller] of controllers) { const job = jobs.get(id); job.status = 'interrupted'; save(job); controller.abort(); }
449
+ },
450
+ };
451
+ }
@@ -0,0 +1,15 @@
1
+ /** Pure validation of the library signatures exported in T17's single installed-runtime manifest. */
2
+ const same=(left,right)=>JSON.stringify(left)===JSON.stringify(right);
3
+
4
+ export function validateCadLibraryIndex(index,library){
5
+ if(!library||typeof library.version!=='string'||!Array.isArray(library.functions)||
6
+ index?.version!==library.version||!Array.isArray(index.functions)||
7
+ !same(index.functions,library.functions.map(({name,params})=>({name,params}))))
8
+ throw new Error('structure_cad_library_invalid');
9
+ return index;
10
+ }
11
+
12
+ /** Names are always checked; the deeper T17 member/argument inference stays opt-in. */
13
+ export function cadPreflightOptions(config,manifest){
14
+ return {build123dNames:manifest.moduleNames,...(config.sourceSymbolPrecheck?{sourceApi:manifest}:{})};
15
+ }
@@ -0,0 +1,102 @@
1
+ /** Authenticated service; one actual-runtime API/library export completes before accepting jobs; stdout is status-only. */
2
+ import http from 'node:http';
3
+ import {ENGINEERING_MAX_BODY_BYTES} from './engineering-fit.mjs';
4
+ import { timingSafeEqual, createHash } from 'node:crypto';
5
+ import { createJobQueue } from './jobs.mjs';
6
+ import { createSandbox } from './sandbox.mjs';
7
+ import { createNativeSandbox } from './native.mjs';
8
+ import { acquireSupervisorLock, readWorkerConfig } from './supervisor.mjs';
9
+ import {createWorkerIdentity,createWorkerSourceContract,resolveDockerImage,STRUCTURE_CHECKER_VERSION,STRUCTURE_PROTOCOL_VERSION} from './identity.mjs';
10
+ import {STRUCTURE_SOURCE_CONTRACT_SCHEMA,prepareSourceApi} from './source-contract.mjs';
11
+ import {cadPreflightOptions} from './library-runtime.mjs';
12
+ import path from 'node:path';
13
+ import {renderTargetComponentImage} from './component-target-image.mjs';
14
+ import {MAX_GLB_BYTES} from './component-fit-glb.mjs';
15
+ const hex=/^[a-f0-9]{64}$/;
16
+ const TARGET_IMAGE_MAX_BODY_BYTES=Math.ceil(MAX_GLB_BYTES/3)*4+65536;
17
+ const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
18
+ /** Data-only target render inside the Worker runtime; the private diagnostic goes back to the trusted host only. */
19
+ async function targetImage(req,reply,renderer){
20
+ if(req.method!=='POST')return reply(405,{error:'method_not_allowed'});
21
+ if(req.headers['content-type']?.split(';')[0]!=='application/json')return reply(415,{error:'media_type'});
22
+ if(!renderer)return reply(503,{error:'target_renderer_unavailable'});
23
+ const chunks=[];let size=0;
24
+ for await(const chunk of req){size+=chunk.length;if(size>TARGET_IMAGE_MAX_BODY_BYTES)return reply(413,{error:'body_too_large'});chunks.push(chunk);}
25
+ let body;try{body=JSON.parse(Buffer.concat(chunks).toString('utf8'));}catch{return reply(400,{error:'invalid_input'});}
26
+ const glb=body?.baselineGlb,bytes=Buffer.from(typeof glb?.base64==='string'?glb.base64:'','base64');
27
+ if(!body||typeof body!=='object'||Array.isArray(body)||Object.keys(body).sort().join(',')!=='baseline,baselineGlb'
28
+ ||!bytes.length||!hex.test(glb.sha256??'')||sha256(bytes)!==glb.sha256)return reply(400,{error:'invalid_input'});
29
+ let diagnostic=null;
30
+ try{
31
+ const rendered=await renderer({baseline:body.baseline,baselineGlb:bytes},{onDiagnostic:event=>{diagnostic=event;}});
32
+ return reply(200,{provenance:rendered.provenance,diagnostic,png:{sha256:sha256(rendered.png),base64:rendered.png.toString('base64')},
33
+ targetGlb:{sha256:sha256(rendered.targetGlb),base64:rendered.targetGlb.toString('base64')}});
34
+ }catch(error){
35
+ const code=typeof error?.code==='string'&&/^[a-z][a-z0-9_]{0,47}$/.test(error.code)?error.code:'target_image_invalid';
36
+ return reply(code.startsWith('target_renderer_')?503:400,{error:code,diagnostic});
37
+ }
38
+ }
39
+ export function createHandler(queue, token, health={ok:true}, sourceContract=null, targetRenderer=null) {
40
+ const keys=Object.keys(health??{}).sort().join(','),legacy=keys==='ok'&&health?.ok===true;
41
+ const current=keys==='checkerVersion,ok,protocolVersion,runtimeDigest'&&health?.ok===true&&health.protocolVersion===STRUCTURE_PROTOCOL_VERSION&&
42
+ hex.test(health.runtimeDigest??'')&&health.checkerVersion===STRUCTURE_CHECKER_VERSION;
43
+ if(!legacy&&!current)throw new Error('structure_health_identity_invalid');
44
+ if(sourceContract!==null&&(!current||sourceContract.schema!==STRUCTURE_SOURCE_CONTRACT_SCHEMA))throw new Error('structure_source_contract_invalid');
45
+ const contract=sourceContract===null?null:JSON.parse(JSON.stringify(sourceContract));
46
+ const expected = Buffer.from(`Bearer ${token}`);
47
+ return async (req,res) => {
48
+ const reply = (status,value) => { res.writeHead(status, {'content-type':'application/json','cache-control':'no-store'}); res.end(JSON.stringify(value)); };
49
+ try {
50
+ const auth = Buffer.from(req.headers.authorization ?? '');
51
+ if (auth.length !== expected.length || !timingSafeEqual(auth,expected)) return reply(401,{error:'unauthorized'});
52
+ const url = new URL(req.url,'http://worker');
53
+ if (url.pathname === '/health' && req.method === 'GET') return reply(200,health);
54
+ if (url.pathname === '/source-contract' && req.method === 'GET')
55
+ return contract?reply(200,{health,contract}):reply(503,{error:'source_contract_unavailable'});
56
+ if (url.pathname === '/target-image') return targetImage(req,reply,targetRenderer);
57
+ const upstream=/^\/jobs\/by-upstream-key\/([a-f0-9]{64})$/.exec(url.pathname),scope=req.headers['x-structure-scope'];
58
+ if(upstream){if(req.method!=='GET')return reply(405,{error:'method_not_allowed'});return reply(200,queue.byUpstreamKey(scope,upstream[1]));}
59
+ const match = /^\/jobs(?:\/([a-f0-9-]+)(?:\/(cancel|artifacts\/([a-z.]+)))?)?$/.exec(url.pathname);
60
+ if (!match) return reply(404,{error:'not_found'});
61
+ if (req.method === 'GET') {
62
+ if (match[3]) {
63
+ const asset=queue.artifact(scope,match[1],match[3]);
64
+ res.writeHead(200,{'content-type':asset.mediaType,'content-length':asset.bytes,'x-content-sha256':asset.sha256,'cache-control':'no-store','x-content-type-options':'nosniff'});
65
+ return res.end(asset.body);
66
+ }
67
+ if (match[2]) return reply(405,{error:'method_not_allowed'});
68
+ return reply(200,match[1]?queue.get(scope,match[1]):{jobs:queue.list(scope)});
69
+ }
70
+ if (req.method !== 'POST' || (match[1] && match[2] !== 'cancel')) return reply(405,{error:'method_not_allowed'});
71
+ if (req.headers['content-type']?.split(';')[0] !== 'application/json') return reply(415,{error:'media_type'});
72
+ const chunks=[];let size=0;
73
+ for await(const chunk of req){size+=chunk.length;if(size>ENGINEERING_MAX_BODY_BYTES)return reply(413,{error:'body_too_large'});chunks.push(chunk);}
74
+ let body;try{body=JSON.parse(Buffer.concat(chunks).toString('utf8'));}catch{return reply(400,{error:'invalid_input'});}
75
+ if(size>65536&&body?.operation!=='component_fit')return reply(413,{error:'body_too_large'});
76
+ if(match[2]==='cancel') {
77
+ if(!body||typeof body!=='object'||Array.isArray(body)||Object.keys(body).length)return reply(400,{error:'invalid_input'});
78
+ return reply(200,queue.cancel(scope,match[1]));
79
+ }
80
+ return reply(202,await queue.submit(scope,body));
81
+ }catch(error){const code=error?.code,status={invalid_input:400,source_invalid:400,not_found:404,conflict:409,queue_full:429}[code]??503;reply(status,{error:status===503?'worker_unavailable':code,...(code==='source_invalid'?{diagnostic:error.diagnostic}:{})});}
82
+ };
83
+ }
84
+ if (process.argv[1]?.endsWith('/main.mjs')) {
85
+ const config=readWorkerConfig(process.env),release=acquireSupervisorLock(config.root);
86
+ process.once('exit', release);
87
+ const imageId=config.backend==='docker'?resolveDockerImage(config.image):undefined;
88
+ const sandbox=config.backend==='native-macos'
89
+ ?await createNativeSandbox({runtime:config.runtime,root:config.root})
90
+ :createSandbox(imageId);
91
+ const sourceApi=await prepareSourceApi(config,sandbox),runtimeConfig={...config,...(imageId?{imageId}:{}),...(sourceApi?{sourceApi}:{})};
92
+ const identity=createWorkerIdentity(runtimeConfig);
93
+ const queue=createJobQueue({root:config.root,sandbox,timeoutMs:config.timeoutMs,...cadPreflightOptions(config,sourceApi),...(sandbox.cleanup?{cleanup:sandbox.cleanup}:{})});
94
+ await queue.restore();
95
+ const targetRenderer=config.backend==='native-macos'
96
+ ?(input,options)=>renderTargetComponentImage(input,{...options,pythonPath:path.join(config.runtime,'venv/bin/python3')}):null;
97
+ const server=http.createServer(createHandler(queue,config.token,identity,createWorkerSourceContract(runtimeConfig),targetRenderer));
98
+ server.requestTimeout=15000;server.headersTimeout=10000;
99
+ server.listen(config.port,config.host,()=>console.log(`structure_worker_ready backend=${config.backend}`));
100
+ const stop=()=>{queue.close();server.close(()=>process.exit(0));setTimeout(()=>process.exit(0),10000).unref();};
101
+ process.on('SIGTERM',stop);process.on('SIGINT',stop);
102
+ }
@@ -0,0 +1,11 @@
1
+ /** Canonical cross-language geometry identities; Python uses IEEE-754 encoding too. */
2
+ import {createHash} from 'node:crypto';
3
+ export function manifestCanonical(value){
4
+ if(value===null)return 'null';
5
+ if(typeof value==='boolean'||typeof value==='string')return JSON.stringify(value);
6
+ if(typeof value==='number'&&Number.isFinite(value)){const b=Buffer.allocUnsafe(8);b.writeDoubleBE(value);return `#${b.toString('hex')}`;}
7
+ if(Array.isArray(value))return `[${value.map(manifestCanonical).join(',')}]`;
8
+ if(value&&typeof value==='object')return `{${Object.keys(value).sort().map(k=>`${JSON.stringify(k)}:${manifestCanonical(value[k])}`).join(',')}}`;
9
+ throw new Error('invalid_manifest_value');
10
+ }
11
+ export const manifestHash=value=>createHash('sha256').update(manifestCanonical(value)).digest('hex');